React 상호작용 더하기: 이벤트 사용하기, 상태 업데이트하기
상호작용 더하기(Adding interactivity)
- 이벤트에 응답하기
- State: 컴포넌트의 기억 저장소
- 렌더링 그리고 커밋
[리액트v19 공식문서-상호작용 더하기] https://ko.react.dev/learn/adding-interactivity
이벤트에 응답하기
이벤트 핸들러 추가하기
const Button = () => {
// 이벤트 핸들러 함수 선언
const handleClick = () => {
alert("you clicked me!");
};
// button에 prop형태로 함수 전달
return <button onClick={handleClick}>Click me!</button>;
};
const ButtonInlineEvent = () => {
return (
// 짧은 함수들을 정의할 땐 JSX내 인라인으로 이벤트 핸들러 정의할 수 있음
<button onClick={() => alert("you clicked me!")}>
Inline Event Handler
</button>
);
};
이벤트 핸들러 함수의 특징
- 주로 컴포넌트 내부에서 정의
- 이벤트 핸들러 명명법 :
handle
+ 이벤트명
🔴 이벤트 핸들러로 전달한 함수들은 호출이 아닌 전달되어야 한다.
<button onClick={handleClick}> // 올바른 사용
<button onClick={handleClick()}> // 잘못된 예시(함수 호출하기)
// 인라인으로 이벤트 핸들러 정의할 때
<button onClick={() => alert('...')}> // 올바른 사용
<button onClick={alert('...')}> // 잘못된 예시(함수 호출하기)
리액트에서 <button onClick={handleClick()}>
처럼 ()
를 붙여서 함수를 전달하면, 클릭이 없어도 컴포넌트가 렌더링되는 순간 즉시 함수가 실행된다.
이는 JSX가 결국 자바스크립트 코드로 변환되기 때문이고, handleClick()
은 함수의 ‘리턴값’을 전달하게 되어 실행되어 버리기 때문이다. handleClick()
함수에 state
를 변경하는 로직이 포함되어있으면 무한 렌더링 루프가 발생할 수 있다.
따라서 이벤트 핸들러로 전달할 땐 onClick={handleClick}
처럼 함수 참조만 전달해야 사용자가 클릭 이벤트를 발생시킬 때만 실행된다.
이벤트 핸들러 내에서 Prop 읽기
이벤트 핸들러는 컴포넌트 내부에서 선언되기 때문에 해당 컴포넌트의 prop에 접근할 수 있다.
const AlertButton = ({ message, children }) => {
// 이벤트 핸들러 alert에서 prop으로 받은 message 사용
return <button onClick={() => alert(message)}>{children}</button>;
};
const Toolbar = () => {
return (
<div>
<AlertButton message="Playing!">Play Movie</AlertButton>
<AlertButton message="Uploading!">Upload Image</AlertButton>
</div>
);
};
이벤트 핸들러를 Prop으로 전달
// PlayButton과 UploadButton에서 클릭 이벤트 핸들러를 전달
const Button = ({ onClick, children }) => {
return <button onClick={onClick}>{children}</button>;
};
const PlayButton = ({ movieName }) => {
const handlePlayClick = () => {
alert(`Playing ${movieName}`);
};
// 이벤트 핸들러 전달 handlePlayClick -> <Button>의 onClick prop으로
return <Button onClick={handlePlayClick}>Play "{movieName}"</Button>;
};
const UploadButton = () => {
// 이벤트 핸들러 전달 () => alert('Uploading') -> <Button>의 onClick prop으로
return <Button onClick={() => alert("Uploading!")}>Upload Image</Button>;
};
const Toolbar = () => {
return (
<div className="toolbar">
<PlayButton movieName="Kiki's Delivery Service" />
<UploadButton />
</div>
);
};
이벤트 핸들러를 Prop 명명하기
<button>
과<div>
같은 빌트인 컴포넌트는onClick
과 같은 브라우저 이벤트 이름만을 지원- 사용자 정의 컴포넌트에서는 이벤트 핸들러 prop의 이름을 원하는 대로 명명할 수 있음
- 이벤트 핸들러 prop의 이름은
on
+ 영문 대문자 시작
- 이벤트 핸들러 prop의 이름은
const MyComponent = ({ onSmash }) => {
return <button onClick={onSmash} />;
};
const App = () => {
return <MyComponent onSmash={() => alert("click!")} />;
};
이벤트 전파
리액트 Synthetic Event 시스템
리액트는 모든 DOM 이벤트를 루트에서 한 번만 등록하고, 그걸 SyntheticEvent로 감싸서 버블링 처리한다.
SyntheticEvent 시스템은 이벤트 위임(event delegation)방식으로 동작하고, 대부분의 이벤트는 버블링(bubbling)을 통해 부모로 전파된다(onScroll 이벤트 예외).
const Button = ({ onClick, children }) => {
return <button onClick={onClick}>{children}</button>;
};
const Toolbar = () => {
return (
<div
className="Toolbar"
onClick={() => alert("You clicked on the toolbar!")}
>
// 버튼 클릭시 alert('Playing!') -> alert('You clicked on the toolbar!')
순으로 실행
<Button onClick={() => alert("Playing!")}>Play Movie</Button>
</div>
);
};
export default Toolbar;
⇒ 주의
컴포넌트 내에서만이 아닌 컴포넌트 간에도 이벤트 전파가 이루어짐
e.stopPropagation()
: 이벤트가 더 이상 bubbling 되지 않도록 함e.preventDefault()
: 이벤트의 기본 브라우저 동작 실행 방지
💡 단계별 이벤트 캡처 - 이벤트 전파는 3단계로 진행된다
- 캡처 단계(Capture Phase) : 이벤트가 최상위 요소부터 클릭된 요소까지 내려가면서
onClickCapture
와 같은 Capture 이벤트 핸들러를 호출한다. - 타겟 단계(Target Phase) : 실제로 클릭한 요소의
onClick
핸들러가 실행된다. - 버블링 단계(Bubbling Phase) : 이벤트가 클릭된 요소에서 다시 위로 올라가면서
onClick
같은 일반 핸들러들이 호출된다.
<div
onClickCapture={() => {
console.log("캡처 - div");
}}
>
<button onClick={(e) => e.stopPropagation()}>버튼</button>
</div>
onClickCapture
는 이벤트가 내려올 때 실행됨button
에서e.stopPropagation()
을 써서 이벤트 전파를 막아도, 캡처 단계에서 이미 실행된 핸들러는 막을 수 없음 ⇒onClickCapture
와 같은 Capture 이벤트 핸들러는 전파가 막히든 말든 무조건 먼저 실행됨
State: 컴포넌트의 기억 저장소
컴포넌트 내 지역 변수는 렌더링 간에 유지되지 않고, 지역변수를 변경하더라도 리액트는 렌더링을 다시 하지 않기 때문에 리액트의 컴포넌트별 메모리인 state가 필요하다.
useState
훅
useState 훅은 state
변수, state setter
함수를 제공한다.
state
변수는 렌더링 간에 데이터를 유지한다.state setter
함수는 변수를 업데이트하고 리액트가 컴포넌트를 다시 렌더링하도록 유발한다.
(1) useState 사용하기
import { useState } from "react";
- useState는
state
변수와state setter
함수 두 가지를 반환한다.
const [index, setIndex] = useState(0);
const [something, setSomething]
과 같이 지정하는 것이 규칙useState
의 유일한 인수는state
변수의 초깃값이다. 여기서index
의 초깃값은useState(0)
에 의해0
으로 설정된다.
(2) useState의 작동방식
const [index, setIndex] = useState(0);
- 컴포넌트가 처음 렌더링 된다.
index
의 초깃값으로0
을 전달했으므로[0, setIndex]
를 반환한다.
- state를 업데이트한다.
- 사용자가 버튼을 클릭하면
setIndex(index + 1)
을 호출한다. - index가 0이므로
setIndex(1)
이다. - 리액트는 최신
index
값을1
로 기억하고 또 다른 렌더링을 유발한다.
- 사용자가 버튼을 클릭하면
- 컴포넌트가 두 번째로 렌더링된다.
- 리액트는
index
를1
로 설정한 것을 기억하고 있어 이번에는[1, setIndex]
를 반환한다.
- 리액트는
🔅 컴포넌트 내 state가 여러 가지일 때 리액트는 어떻게 상태들을 추적하고 관리할 수 있을까?
⇒ 리액트는 내부적으로 상태를순서
기반 배열로 추적한다.
⇒useState()
는 호출 순서에 따라 각각의 상태를 기억한다.
⇒ 따라서useState
는 컴포넌트 최상단에서 항상 같은 순서로 호출되어야한다.
function MyComponent() {
const [count, setCount] = useState(0); // 1번째 상태 => count용 상태
const [name, setName] = useState(""); // 2번째 상태 => name용 상태
}
useState
의 작동원리 : 렌더가 시작되면, 상태값을 초기값으로 초기화하고 useState()가 호출될 때마다 다음 인덱스로 넘어가며 상태를 꺼내준다.
let stateArray = [0, ""];
let currentIndex = 0;
function useState(initialValue) {
const index = currentIndex;
if (stateArray[index] === undefined) {
stateArray[index] = initialValue;
}
const setState = (newValue) => {
stateArray[index] = newValue;
render(); // 다시 렌더링
};
currentIndex++;
return [stateArray[index], setState];
}
(3) State is isolated
and private
Isolated
: 컴포넌트의state
는 다른 컴포넌트와 공유되지 않는다. 즉, 컴포넌트의 state는 부모 컴포넌트나 형제 컴포넌트와 같이 다른 컴포넌트에서 직접 접근하거나 수정할 수 없다.Private
:state
는 컴포넌트 내부에서만 관리되고 사용된다. 외부에서 직접 볼 수도, 바꿀 수도 없다.
export default function Page() {
return (
<div className="Page">
<Gallery />
<Gallery />
</div>
);
- 같은
Gallery
컴포넌트를 여러번 사용하더라도 각Gallery
컴포넌트는 자기만의state
를 갖고 서로 영향을 주지 않는다. ⇒ 여러 개의Gallery
컴포넌트가 같은 값을 공유하길 원한다면 부모 컴포넌트인Page
컴포넌트의state
값을 prop으로 전달받아 사용해야 한다.
⛳️ 챌린지 - 불필요한 state 제거하기
import { useState } from "react";
export default function FeedbackForm() {
const [name, setName] = useState("");
function handleClick() {
setName(prompt("What is your name?"));
alert(`Hello, ${name}!`);
}
return <button onClick={handleClick}>Greet</button>;
}
name
은 이벤트 핸들러 내에서 일회성으로 사용하는 변수이기 때문에 다시 렌더링될 때 상태값을 유지할 필요가 없다.
export default function FeedbackForm() {
function handleClick() {
const name = prompt("What is your name?");
alert(`Hello, ${name}!`);
}
return <button onClick={handleClick}>Greet</button>;
}
주의
컴포넌트가 다시 렌더링 될 때 정보를 유지하기 위해state
변수가 필요한 것임을 잊지않기 ⇒ 일반 변수로도 잘 작동할 때 state 변수를 사용하지 않도록 한다.
렌더링 그리고 커밋
리액트의 렌더링 동작은 3가지 단계를 거친다.
- 렌더링
트리거
(손님의 주문을 주방으로 전달) - 컴포넌트
렌더링
(주방에서 주문 준비하기 - DOM에
커밋
(테이블에 주문한 요리 내놓기)
1단계: 트리거
컴포넌트 렌더링이 일어나는 2가지 이유
- 컴포넌트의 초기 렌더링인 경우
- 컴포넌트의 state가 업데이트된 경우
(1) 초기 렌더링
createRoot
호출 후render
메서드 호출
import Image from "./Image.js";
import { createRoot } from "react-dom/client";
const root = createRoot(document.getElementById("root"));
root.render(<Image />);
(2) state 업데이트 시 리렌더링
setState
함수를 통해 상태를 업데이트하여 추가적인 렌더링 트리거
⇒ 컴포넌트 상태를 업데이트하면 자동으로 렌더링 대기열에 추가된다.
2단계: React 컴포넌트 렌더링
렌더링? 리액트에서 컴포넌트를 호출하는 것
- 초기 렌더링에서 리액트는
루트 컴포넌트
를 호출한다. - 이후 렌더링에서는 리액트는
state 업데이트가 일어나 렌더링을 트리거한 컴포넌트
를 호출한다. - 업데이트된 컴포넌트가 다른 컴포넌트를 반환하면, 리액트는 다음으로 해당 컴포넌트를 렌더링하는 식으로 중첩된 컴포넌트가 더 이상 없을 때까지 재귀적으로 렌더링을 처리한다.
3단계: React가 DOM에 변경사항을 커밋
컴포넌트를 호출한 후 리액트는 DOM을 수정한다.
- 초기 렌더링에서 리액트는
appendChild()
DOM API를 사용하여 생성한 모든 DOM 노드를 화면에 표시한다. -
리렌더링의 경우 필요한 최소한의 작업을 적용해 DOM이 최신 렌더링 출력과 일치하도록 한다. ⇒ 리액트는 렌더링 간에 차이가 있는 경우에만 DOM 노드를 변경한다.
export default function Clock({ time }) { return ( <> <h1>{time}</h1> <input /> </> ); }
time
이 업데이트된 경우<h1>
의 내용만 새로 업데이트하고<input>
과<input>의 value
는 건드리지 않는다
Leave a comment