< 목차 >
1. Adding Interactivity | Responding to Events
2. Adding Interactivity | State: A Component's Memory
3. Adding Interactivity | Render and Commit
4. Adding Interactivity | State as a Snapshot
5. 금일 소감
1. Adding Interactivity | Responding to Events
0. TL;DR
- 이벤트 핸들러의 이름은 handle 뒤에 이벤트 이름을 붙이는 것이 일반적
- 규칙에 따라 이벤트 핸들러 prop은 on으로 시작해야 하며 카멜케이스로 작성
- e.stopPropagation(): 위의 태그에 연결된 이벤트 핸들러의 실행을 중지
- e.preventDefault(): 해당 이벤트가 있는 몇 가지 이벤트에 대한 기본 브라우저 동작을 방지
1. 서문
* state: React에서 시간이 지남에 따라 변경되는 데이터
이 장에서는 상호작용을 처리하고, 상태를 업데이트하고, 시간에 따라 다른 출력을 표시하는 컴포넌트를 작성하는 방법을 설명한다.
2. [이벤트 핸들러 이름] 핸들(handle) 뒤에 이벤트 이름을 붙일 것
관례에 따라 이벤트 핸들러의 이름은 handle 뒤에 이벤트 이름을 붙이는 것이 일반적이다. 예를 들어, onClick={handleClick}, onMouseEnter={handleMouseEnter} 등을 자주 볼 수 있다.
또는 JSX에서 이벤트 핸들러를 인라인으로 정의할 수도 있다. 인라인 이벤트 핸들러는 짧은 함수에 편리하다.
// 기본 함수 형태
<button onClick={function handleClick() {
alert('You clicked me!');
}}>
// 화살표 함수 형태
<button onClick={() => {
alert('You clicked me!');
}}>
3. 이벤트 핸들러 프로퍼티 이름 지정
<button> 및 <div>와 같은 기본 제공 컴포넌트는 onClick과 같은 브라우저 이벤트 이름만 지원한다. 그러나 자체 컴포넌트를 빌드할 때는 이벤트 핸들러 프롭의 이름을 원하는 방식으로 지정할 수 있다.
규칙에 따라 이벤트 핸들러 prop은 on으로 시작해야 하며 카멜케이스로 작성해야 한다.
예를 들어, 버튼 컴포넌트의 onClick 프로퍼티는 onSmash라고 할 수 있다.
(onSmash: 비디오 재생 버튼이나 이미지 업로드 버튼에 쓰일 수 있어, 해당 버튼이 눌릴 때 사용자에게 알림을 주는 기능으로 사용 가능)
function Button({ onSmash, children }) {
return (
<button onClick={onSmash}>
{children}
</button>
);
}
export default function App() {
return (
<div>
<Button onSmash={() => alert('Playing!')}>
Play Movie
</Button>
<Button onSmash={() => alert('Uploading!')}>
Upload Image
</Button>
</div>
);
}
이벤트 핸들러에 적절한 HTML 태그를 사용해야 한다. 예를 들어 클릭을 처리하려면 <div onClick={handleClick}> 대신 <button onClick={handleClick}>을 사용하라. 실제 브라우저 <button>을 사용하면 *키보드 탐색과 같은 *기본 브라우저 동작을 사용할 수 있다.
- * 키보드 탐색: 웹 페이지에서 사용자가 키보드만으로 모든 상호작용을 할 수 있어야 한다.
예를 들어, Tab 키를 사용해 페이지 내 다양한 요소(링크, 버튼 등) 사이를 이동할 수 있어야 한다. <button> 같은 요소는 자동으로 키보드 포커스를 받을 수 있으므로 키보드 사용자가 쉽게 접근할 수 있다. 반면, <div> 태그에 onClick 이벤트 핸들러를 사용할 경우, 추가적으로 tabindex 속성을 설정해야 하며, 엔터 키나 스페이스바 키 입력을 처리하는 로직도 직접 구현해야 할 수 있다. - * 기본 브라우저 동작: HTML 요소들은 각각 기본적으로 내장된 행동들을 가지고 있다.
예를 들어, <button> 요소는 클릭 이벤트가 발생할 때 자동으로 'activate' 되는 기능을 가진다. 이는 스크린 리더 사용자들이 버튼을 인식하고 적절하게 반응할 수 있도록 도와준다. 또한, <form> 내의 버튼은 기본적으로 폼을 제출하는 역할을 한다.
4. Event Propagation(이벤트 전파)
아래와 같이 코드를 작성하면 버튼을 눌렀을 때 상위 <div>에 있는 onClick까지 포함해서 2번의 alert가 발생한다.
JSX 태그에서만 작동하는 onScroll을 제외한 모든 이벤트는 React에서 전파된다.
export default function Toolbar() {
return (
<div className="Toolbar" onClick={() => {
alert('You clicked on the toolbar!');
}}>
<button onClick={() => alert('Playing!')}>
Play Movie
</button>
<button onClick={() => alert('Uploading!')}>
Upload Image
</button>
</div>
);
}
핵심 요점
- onClickCapture: 이벤트가 타겟 요소에 도달하기 전에 이벤트를 처리해야 할 때 사용
이는 상위 DOM 트리에서 이벤트를 가로채거나 사전 처리해야 하는 상황에서 유용 - onClickCapture를 이해하면 React 애플리케이션에서 복잡한 이벤트 처리 시나리오를 관리하는 데 도움됨. 특히 중첩된 컴포넌트와 이벤트 위임을 다룰 때 유용함
- 하위 핸들러에서 이벤트 핸들러 프로퍼티를 명시적으로 호출하는 것이 전파를 방지하는 좋은 대안
전파 중지
이벤트 핸들러는 이벤트 객체를 유일한 인수로 받는다.
관례에 따라 일반적으로 '이벤트'를 의미하는 e라고 한다. 이 객체를 사용하여 이벤트에 대한 정보를 읽을 수 있다.
이 이벤트 객체를 사용하면 전파를 중지할 수도 있다. 이벤트가 상위 컴포넌트에 도달하지 못하도록 하려면 버튼 컴포넌트처럼 e.stopPropagation()을 호출해야 한다.
function Button({ onClick, children }) {
return (
<button onClick={e => {
e.stopPropagation();
onClick();
}}>
{children}
</button>
);
}
export default function Toolbar() {
return (
<div className="Toolbar" onClick={() => {
alert('You clicked on the toolbar!');
}}>
<Button onClick={() => alert('Playing!')}>
Play Movie
</Button>
<Button onClick={() => alert('Uploading!')}>
Upload Image
</Button>
</div>
);
}
e.stopPropagation()의 결과로, 이제 버튼을 클릭하면 2개의 알림(<button>과 상위 Toolbar <div>)이 아닌 하나의 알림(<button>에서)만 표시된다. 버튼을 클릭하는 것은 주변 툴바를 클릭하는 것과는 다르므로 전파를 중지하는 것이 이 UI에 적합하다.
이벤트 전파 단계
드물지만 하위 요소의 모든 이벤트를 포착해야 하는 경우가 있을 수 있다(전파가 중지된 경우에도). 예를 들어, 전파 로직에 관계없이 모든 클릭을 애널리틱스에 기록하고자 할 수 있다. 이벤트 이름 끝에 Capture를 추가하면 이 작업을 수행할 수 있다.
onClickCapture: React에서 이벤트 캡처 단계 동안 이벤트를 처리하기 위해 사용하는 이벤트 핸들러 속성
이벤트는 위쪽으로 전달된다. 총 3가지 단계가 있으며 다음과 같다.
- 캡처 단계: 이벤트가 DOM 트리의 루트에서 시작하여 타겟 요소로 전파됨
- 타겟 단계: 이벤트가 타겟 요소에 도달함
- 버블 단계: 이벤트가 타겟 요소에서 다시 DOM 트리의 루트로 *버블링됨
*버블링(Bubbling)
: 이벤트 전파 방식 중 하나로, 이벤트가 발생한 요소에서 시작하여 상위 요소로 전파되는 과정
onClickCapture의 용도
- onClickCapture를 사용하면 이벤트 캡처 단계에서 이벤트 핸들러가 실행됨
- 이는 버블 단계에서 이벤트 핸들러가 실행되는 onClick과 대조적
예시
function ExampleComponent() {
function handleClickCapture(event) {
console.log('캡처 단계');
}
function handleClick(event) {
console.log('버블 단계');
}
return (
<div onClickCapture={handleClickCapture} onClick={handleClick}>
Click Me
</div>
);
}
이 예시는
- div를 클릭하면 먼저 handleClickCapture가 실행되어 '캡처 단계'를 로그함
- 그 다음 handleClick이 실행되어 '버블 단계'를 로그함
5. 기본 동작 방지
전파에 의존하고 있는데 어떤 핸들러가 왜 실행되는지 추적하기 어렵다면 이 접근 방식을 시도해 보자.
일부 브라우저 이벤트에는 기본 동작이 연관되어 있다. 예를 들어, <form> 제출 이벤트는 내부의 버튼이 클릭될 때 발생하며 기본적으로 전체 페이지를 다시 로드한다.
이벤트 객체에서 e.preventDefault()를 호출하여 이런 일이 발생하지 않도록 할 수 있다.
export default function Signup() {
return (
<form onSubmit={e => {
e.preventDefault();
alert('Submitting!');
}}>
<input />
<button>Send</button>
</form>
);
}
2. Adding Interactivity | State: A Component's Memory
0. TL;DR
- 컴포넌트가 렌더링 사이에 일부 정보를 ‘기억’해야 할 때 State 변수 사용
- State 변수는 useState Hook을 호출하여 선언 → 현재 상태와 이를 업데이트하는 함수라는 값 쌍을 반환
- Hook은 use로 시작하는 ‘특수 함수’임. state와 같은 React 기능을 ‘연결’할 수 있게 해줌 useState를 포함한 Hook을 호출하는 것은 컴포넌트의 최상위 레벨이나 다른 Hook에서만 유효함
1. 서문
이벤트 핸들러에 Side effect가 생길 수 있는가?
물론이다! 이벤트 핸들러는 ‘Side effect가 가장 많이 발생하는 곳’이다.
렌더링 함수와 달리 이벤트 핸들러는 '순수할 필요가 없으므로' 입력에 반응하여 입력 값을 변경하거나 버튼 누름에 반응하여 목록을 변경하는 등 무언가를 변경하기에 좋다. 하지만 일부 정보를 변경하려면 먼저 정보를 저장할 방법이 필요하다. React에서는 컴포넌트의 메모리인 state를 사용하여 이 작업을 수행한다. 여기서 이에 대한 모든 것을 배우게 된다.
컴포넌트는 현재 입력값, 현재 이미지, 장바구니와 같은 것들을 ‘기억’해야 한다. React에서는 이런 종류의 컴포넌트별 메모리를 state라고 한다.
2. 일반 변수로 충분하지 않을 경우
컴포넌트를 새 데이터로 업데이트해야 한다. 그럼 아래의 2가지 일이 일어나야 한다.
- 렌더링 사이에 데이터를 유지
- 새로운 데이터로 컴포넌트를 렌더링하도록 React를 트리거(리렌더링).
useState Hook은 이 2가지를 제공한다.
- 렌더링 사이에 데이터를 유지하기 위한 State 변수
- 변수를 업데이트하고 React가 컴포넌트를 리렌더링하도록 트리거하는 state setter 함수
3. State 변수 추가하기
const [index, setIndex] = useState(0);
index는 State 변수이고, setIndex는 설정자 함수다.
useState를 호출하면 이 컴포넌트가 무언가를 기억하기를 원한다고 React에 말하는 것
const [index, setIndex] = useState(0);
이 쌍의 이름은 const [something, setSomething]과 같이 지정하는 것이 규칙이다. 원하는 대로 이름을 지을 수 있지만 규칙을 따를 경우, 프로젝트 전반에서 이해하기 쉽다.
컴포넌트가 렌더링될 때마다 useState는 2개의 값을 포함하는 배열을 제공한다.
- 저장한 값을 가진 상태 변수(index)
- state setter 함수(setIndex): 상태 변수를 업데이트하고 React가 컴포넌트를 리렌더링하도록 트리거할 수 있음
실제 작동 방식
const [index, setIndex] = useState(0);
- 컴포넌트가 처음으로 렌더링된다. 인덱스의 초기값으로 useState에 0을 전달했기 때문에 [0, setIndex]를 반환한다. React는 0이 최신 상태 값임을 기억한다.
- 상태를 업데이트한다. 사용자가 버튼을 클릭하면 setIndex(index + 1)를 호출한다. index가 0이므로 setIndex(1)이 된다. 이렇게 하면 React가 index가 이제 1임을 기억하고 다른 렌더링을 트리거한다.
- 컴포넌트가 두 번째로 재렌더링된다. React는 여전히 useState(0)을 보지만, 사용자가 index를 1로 설정한 것을 기억하기 때문에 대신 [1, setIndex]를 반환한다.
4. Hook에 대하여
: React에서는 useState를 비롯해 "use"로 시작하는 다른 모든 함수를 Hook이라고 부름
- Hook은 React가 렌더링하는 동안에만 사용할 수 있는 특별한 함수
- Hook(use로 시작하는 함수)은 컴포넌트의 최상위 레벨 또는 자체 Hook에서만 호출 가능
- 조건, 루프 또는 기타 중첩된 함수 내부에서는 Hook을 호출할 수 없다. Hook은 함수이지만 컴포넌트의 필요에 대한 무조건적인 선언으로 생각하면 도움이 된다. 파일 상단에서 모듈을 ‘import’하는 것과 비슷하게 컴포넌트 상단에서 React 기능을 ‘use’한다.
5. State: 격리되고 비공개
상태는 화면의 컴포넌트 인스턴스에 로컬이다. 즉, 동일한 컴포넌트를 두 번 렌더링하면 각 복사본은 완전히 분리된 상태를 갖게 된다! 그 중 하나를 변경해도 다른 컴포넌트에는 영향을 미치지 않는다.
// App.js
import Gallery from './Gallery.js';
export default function Page() {
return (
<div className="Page">
<Gallery />
<Gallery />
</div>
);
}
// Gallery.js
import { useState } from 'react';
import { sculptureList } from './data.js';
export default function Gallery() {
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);
function handleNextClick() {
setIndex(index + 1);
}
function handleMoreClick() {
setShowMore(!showMore);
}
let sculpture = sculptureList[index];
return (
<section>
<button onClick={handleNextClick}>
Next
</button>
<h2>
<i>{sculpture.name} </i>
by {sculpture.artist}
</h2>
<h3>
({index + 1} of {sculptureList.length})
</h3>
<button onClick={handleMoreClick}>
{showMore ? 'Hide' : 'Show'} details
</button>
{showMore && <p>{sculpture.description}</p>}
<img
src={sculpture.url}
alt={sculpture.alt}
/>
</section>
);
}
// data.js
export const sculptureList = [{
name: 'Homenaje a la Neurocirugía',
artist: 'Marta Colvin Andrade',
description: 'Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.',
url: '<https://i.imgur.com/Mx7dA2Y.jpg>',
alt: 'A bronze statue of two crossed hands delicately holding a human brain in their fingertips.'
},
// 이하 총 12개의 데이터가 있다고 가정
}];
이것이 모듈 맨 위에 선언할 수 있는 일반 변수와 state가 다른 점이다. 상태는 특정 함수 호출이나 코드의 특정 위치에 묶여 있지 않고 화면의 특정 위치에 ‘local’이다. 2개의 <Gallery /> 컴포넌트를 렌더링했으므로 상태는 별도로 저장된다.
또한 페이지 컴포넌트는 갤러리 상태나 상태가 있는지 여부에 대해 아무것도 ‘알지 못한다’는 점에 주목하자. prop과 달리 state는 이를 선언하는 컴포넌트에게 완전히 비공개한다. 부모 컴포넌트는 이를 변경할 수 없다. 따라서 다른 컴포넌트에 영향을 주지 않고 상태를 추가하거나 제거할 수 있다.
두 갤러리의 상태를 동기화하려면 어떻게 해야 할까? React에서 이를 수행하는 올바른 방법은 자식 컴포넌트에서 state를 제거하고 가장 가까운 공유 부모에 추가하는 것이다.
[Curiosity] React는 어떤 상태를 반환할지 어떻게 알 수 있을까?
useState 호출이 어떤 상태 변수를 참조하는지에 대한 정보를 받지 못한다는 것을 눈치챘을 것이다. useState에 전달되는 ‘식별자’가 없는데, 어떤 상태 변수를 반환할지 어떻게 알 수 있을까? 함수 구문 분석과 같은 마법에 의존할까? 대답은 '아니다'이다.
대신, Hook은 간결한 구문을 구현하기 위해 동일한 컴포넌트의 모든 렌더링에서 안정적인 호출 순서에 의존한다. 위의 규칙(’최상위 레벨에서만 Hook 호출’)을 따르면 Hook은 항상 같은 순서로 호출되기 때문에 실제로 잘 작동한다. 또한 Linter 플러그인(코드 작성 시 문법 오류, 스타일링 문제, 잠재적 버그 등을 자동으로 감지하고 경고하는 도구)은 대부분의 실수를 잡아낸다.
내부적으로 React는 모든 컴포넌트에 대해 상태 쌍의 배열을 보유한다. 또한 렌더링 전에 0으로 설정된 현재 쌍 인덱스를 유지한다. useState를 호출할 때마다 React는 다음 상태 쌍을 제공하고 인덱스를 증가시킨다.
이 메커니즘에 대한 자세한 내용은 React Hooks에서 확인할 수 있다(React hooks: not magic, just arrays)
3. Adding Interactivity | Render and Commit
0. TL;DR
- React 앱의 모든 화면 업데이트는 3단계로 진행됨
- Trigger
- Rendering
- Commit
- Strict Mode를 사용하여 컴포넌트에서 실수를 찾을 수 있음
- 렌더링 결과가 지난번과 동일한 경우, React는 DOM을 건드리지 않음
Epilogue | Browser paint
렌더링이 완료되고 React가 DOM을 업데이트하면 브라우저는 화면을 다시 칠한다. 이 과정을 ‘브라우저 렌더링’이라고 부르지만 문서 전체에서 혼동을 피하기 위해 ‘painting’이라고 부르겠다.
1. 서문
컴포넌트가 화면에 표시되기 전에 React에서 ‘렌더링’해야 한다.
이 과정의 단계를 이해하면 코드가 어떻게 실행되는지 생각하고 동작을 설명하는 데 도움이 된다.
이 장에서 학습할 내용은 다음과 같다.
- React에서 렌더링의 의미
- React가 컴포넌트를 렌더링하는 시기와 이유
- 컴포넌트를 화면에 표시하는 단계
- 렌더링이 항상 DOM 업데이트를 생성하지 않는 이유
컴포넌트가 주방에서 재료로 맛있는 요리를 만드는 요리사라고 상상해 보자.
이 시나리오에서 React는 고객의 요청을 접수하고 주문을 가져오는 웨이터 역할을 한다.
UI를 요청하고 제공하는 이 과정은 3단계로 이루어진다.
- 렌더링 트리거 (손님의 주문을 주방에 전달)
- 컴포넌트 렌더링 (주방에서 주문 준비)
- DOM에 commit (주문을 테이블에 배치)
2. 1단계: 렌더링 트리거
컴포넌트가 렌더링되는 이유는 2가지다.
- 컴포넌트의 초기 렌더링
- 컴포넌트(또는 그 상위 컴포넌트 중 하나)의 상태가 업데이트되었을 때
(1) 초기 렌더링
앱이 시작되면 초기 렌더링을 *트리거해야 한다.
* 트리거(trigger)?
: 특정 이벤트나 조건이 발생했을 때 실행되는 동작
예를 들어, 버튼 클릭, 폼 제출, 또는 특정 상태 변화가 트리거가 되어 함수나 콜백이 실행된다. 이는 주로 onClick, onChange 같은 이벤트 핸들러를 통해 구현된다.
(2) 상태 업데이트 시, 리렌더링
컴포넌트가 처음 렌더링된 후에는 설정 함수로 상태를 업데이트하여 추가 렌더링을 트리거할 수 있다. 컴포넌트의 상태를 업데이트하면 렌더링이 자동으로 대기열에 추가된다. (식당에서 손님이 첫 주문을 한 후 갈증이나 배고픔의 상태에 따라 차, 디저트 등 다양한 음식을 주문하는 것을 상상해 보자)
3. 2단계: React가 컴포넌트를 렌더링하기
*렌더링을 트리거한 후 React는 컴포넌트를 호출하여 화면에 표시할 내용을 파악한다.
* 렌더링(Rendering)?
: React가 컴포넌트를 호출하는 것
- 초기 렌더링에서 React는 root 컴포넌트를 호출한다.
- 이후 렌더링에서 React는 상태 업데이트가 렌더링을 트리거한 함수 컴포넌트를 호출한다.
- 업데이트된 컴포넌트가 다른 컴포넌트를 반환하면 React는 그 컴포넌트를 다음에 렌더링하고,
- 그 컴포넌트에서도 무언가를 반환하면 그 컴포넌트를 다음에 렌더링하는 식으로 재귀적인 프로세스를 수행한다.
- 이 프로세스는 중첩된 컴포넌트가 더 이상 존재하지 않을 때까지 계속되고, React는 화면에 표시해야 할 내용을 정확히 파악한다.
다음 예시에서는 React가 Gallery() 및 Image()를 여러 번 호출한다.
// index.js
import Gallery from './Gallery.js';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'))
root.render(<Gallery />);
// Gallery.js
export default function Gallery() {
return (
<section>
<h1>Inspiring Sculptures</h1>
<Image />
<Image />
<Image />
</section>
);
}
function Image() {
return (
<img
src="https://i.imgur.com/ZF6s192.jpg"
alt="'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals"
/>
);
}
초기 렌더링 중에 React는 <section>, <h1>, 3개의 <img> 태그에 대한 DOM 노드를 생성한다.
다시 렌더링하는 동안 React는 이전 렌더링 이후 변경된 프로퍼티가 있다면 어떤 것인지 계산한다. 다음 단계인 커밋 단계가 될 때까지 해당 정보로 아무 작업도 하지 않는다.
렌더링은 항상 순수한 계산이어야 한다.
- 동일한 입력, 동일한 출력. 동일한 입력이 주어지면 컴포넌트는 항상 동일한 JSX를 반환해야 한다(토마토가 들어간 샐러드를 주문한 사람이 양파가 들어간 샐러드를 받아서는 안 된다!).
- 컴포넌트는 자기 일에 신경을 쓴다. 렌더링 전에 존재했던 객체나 변수를 변경해서는 안 된다(하나의 주문이 다른 사람의 주문을 변경해서는 안 된다).
그렇지 않으면 코드베이스가 복잡해지면서 혼란스러운 버그와 예측할 수 없는 동작이 발생할 수 있다. ‘Strict Mode’에서 개발할 때 React는 각 컴포넌트의 함수를 2번 호출하므로 불순한 함수로 인한 실수를 발견하는 데 도움이 될 수 있다.
♻️ 성능 최적화
업데이트된 컴포넌트가 트리에서 매우 높은 위치에 있는 경우, 업데이트된 컴포넌트 내에 중첩된 모든 컴포넌트를 렌더링하는 기본 동작은 성능에 최적이 아니다. 성능 문제가 발생하는 경우, 성능 섹션에 설명된 몇 가지 선택적 해결 방법이 있다.
4. 3단계: React가 DOM에 변경 사항을 commit하기
컴포넌트를 렌더링(호출)한 후, React는 DOM을 수정한다.
- 초기 렌더링의 경우: React는 appendChild() DOM API를 사용하여 생성한 모든 DOM 노드를 화면에 배치한다.
- 리렌더링의 경우: React는 DOM이 최신 렌더링 출력과 일치하도록 하기 위해 필요한 최소한의 연산(렌더링 중에 계산!)을 적용한다.
React는 렌더링 간에 차이가 있는 경우에만 DOM 노드를 변경한다.
예를 들어, 다음은 부모로부터 매초마다 다른 prop을 전달받아 다시 렌더링하는 컴포넌트다. <input>에 텍스트를 추가하고 value를 업데이트해도 컴포넌트가 다시 렌더링할 때 텍스트가 사라지지 않는 것을 볼 수 있다.
export default function Clock({ time }) {
return (
<>
<h1>{time}</h1>
<input />
</>
);
}
이 마지막 단계에서 React는 <h1>의 내용만 새로운 시간으로 업데이트하기 때문에 작동한다. <input>이 지난번과 같은 위치에 JSX에 나타나는 것을 확인하므로 React는 <input>이나 그 value를 건드리지 않는다.
4. Adding Interactivity | State as a Snapshot
0. TL;DR
- React는 마치 선반에 보관하듯 컴포넌트 외부에 state를 저장함
- useState를 호출하면 React는 해당 렌더링에 대한 상태의 스냅샷을 제공
- 변수와 이벤트 핸들러는 리렌더링에서 '살아남지' 않음. 모든 렌더링에는 자체 이벤트 핸들러가 있음.
과거에 생성된 이벤트 핸들러는 생성된 렌더링의 상태 값을 가짐 - 모든 렌더링(및 그 안에 있는 함수)은 항상 React가 해당 렌더링에 제공한 상태의 스냅샷을 '보게' 됨
1. 서문
스냅샷으로서의 상태
State 변수는 읽고 쓸 수 있는 일반 자바스크립트 변수처럼 보일 수 있다.
하지만 State는 ‘스냅샷처럼 동작’한다. 상태 변수를 설정해도 이미 가지고 있는 상태 변수는 변경되지 않고 대신 리렌더링이 트리거된다.
이 장에서 학습할 내용은 다음과 같다.
- 상태 설정이 리렌더링을 트리거하는 방법
- 상태 업데이트 시기 및 방법
- 상태를 설정한 후 즉시 상태가 업데이트되지 않는 이유
- 이벤트 핸들러가 상태의 '스냅샷'에 액세스하는 방법
2. 상태 트리거를 설정하면 렌더링
사용자 인터페이스가 클릭과 같은 사용자 이벤트에 반응하여 직접 변경되는 것으로 생각할 수 있다.
React에서는 이 mental model과는 조금 다르게 작동한다. 이전 페이지에서 state를 설정하면 React에서 리렌더링을 요청하는 것을 봤다. 즉, 인터페이스가 이벤트에 반응하려면 state를 업데이트해야 한다.
다음 예제에서는 Send를 누르면 setIsSent(true)가 React에 UI를 리렌더링하도록 지시한다.
import { useState } from 'react';
export default function Form() {
const [isSent, setIsSent] = useState(false);
const [message, setMessage] = useState('Hi!');
if (isSent) {
return <h1>Your message is on its way!</h1>
}
return (
<form onSubmit={(e) => {
e.preventDefault();
setIsSent(true);
sendMessage(message);
}}>
<textarea
placeholder="Message"
value={message}
onChange={e => setMessage(e.target.value)}
/>
<button type="submit">Send</button>
</form>
);
}
function sendMessage(message) {
// ...
}
위 코드의 버튼을 클릭하면 다음과 같은 일이 발생한다.
- onSubmit 이벤트 핸들러가 실행됨
- setIsSent(true)는 isSent를 true로 설정하고 새 렌더링을 대기열에 넣음
- React는 새로운 isSent 값에 따라 컴포넌트를 리렌더링함
상태와 렌더링의 관계를 자세히 살펴 보자.
3. * 렌더링은 시간의 Snapshot을 찍는다.
* 렌더링?
: React가 컴포넌트인 함수를 호출하는 것
해당 함수에서 반환하는 JSX는 시간의 흐름에 따른 UI의 스냅샷과 같다. 프로퍼티, 이벤트 핸들러, 로컬 변수는 모두 렌더링 시점의 상태를 사용해 계산된다.
사진이나 영화 프레임과 달리 반환하는 UI ‘스냅샷’은 인터랙티브하다. 여기에는 입력에 대한 응답으로 어떤 일이 일어날지 지정하는 이벤트 핸들러와 같은 로직이 포함된다. React는 이 스냅샷에 맞게 화면을 업데이트하고 이벤트 핸들러를 연결한다. 결과적으로 버튼을 누르면 JSX에서 클릭 핸들러가 트리거된다.
React가 컴포넌트를 리렌더링할 때
- React가 함수를 다시 호출한다.
- 함수는 새로운 JSX 스냅샷을 반환한다.
- 그러면 React는 함수가 반환한 스냅샷과 일치하도록 화면을 업데이트한다.
컴포넌트의 메모리로서 state는 함수가 반환된 후 사라지는 일반 변수와 다르다. state는 실제로 함수 외부에 마치 선반에 있는 것처럼 React 자체에 '존재'한다. React가 컴포넌트를 호출하면 특정 렌더링에 대한 상태의 스냅샷을 제공한다. 컴포넌트는 해당 렌더링의 상태 값을 사용해 계산된 새로운 프로퍼티와 이벤트 핸들러 세트가 포함된 UI의 스냅샷을 JSX에 반환한다.
예시
다음은 이것이 어떻게 작동하는지 보여주는 간단한 실험이다.
이 예제에서는 '+3' 버튼을 클릭하면 setNumber(숫자 + 1)를 3번 호출하므로 당연히 카운터가 3번 증가한다고 예상할 수 있다.
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
setTimeout(() => {
alert(number);
}, 3000);
}}>+5</button>
</>
)
}
이 숫자는 클릭당 한 번만 증가한다는 점에 유의하자!
상태를 설정하면 다음 렌더링에 대해서만 변경된다. 첫 번째 렌더링에서는 숫자가 0이었다. 따라서 해당 렌더링의 onClick 핸들러에서 setNumber(숫자 + 1)가 호출된 후에도 숫자 값은 여전히 0이다.
이 버튼의 클릭 핸들러가 React에 지시하는 작업은 다음과 같다.
- setNumber(number + 1): 숫자는 0이므로 setNumber(0 + 1). React는 다음 렌더링에서 숫자를 1로 변경할 준비를 한다. (* 세 번 반복됨)
setNumber(number + 1)를 3번 호출했지만 이 렌더링의 이벤트 핸들러에서 숫자는 항상 0이므로 상태를 1로 3번 설정한 것이다. 이것이 이벤트 핸들러가 완료된 후 React가 컴포넌트를 3이 아닌 1로 다시 렌더링하는 이유다.
4. 시간 경과에 따른 상태
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
setTimeout(() => {
alert(number);
}, 3000);
}}>+5</button>
</>
)
}
위와 같이 대체 방법을 사용하면 알림에 전달된 상태의 ‘snapshot’을 볼 수 있다.
React에 저장된 상태는 알림이 실행될 때 변경되었을 수 있지만, 사용자가 상호작용한 시점의 상태 스냅샷을 사용하여 예약되었다.
상태 변수의 값은 이벤트 핸들러의 코드가 비동기적이라 하더라도 렌더링 내에서 절대 변경되지 않는다. 해당 렌더링의 onClick 내에서 setNumber(number + 5)가 호출된 후에도 number의 값은 계속 0이다. 이 값은 React가 컴포넌트를 호출하여 UI의 스냅샷을 ‘찍을’ 때 ‘고정’된 것이다.
[Reference]
- [React.dev] Responding to Events - https://react.dev/learn/responding-to-events
- [React.dev] State: A Component's Memory - https://react.dev/learn/state-a-components-memory
- [React.dev] Render and Commit - https://react.dev/learn/render-and-commit
- [React.dev] State as a Snapshot - https://react.dev/learn/state-as-a-snapshot
5. 금일 소감
생각보다 이번 파트를 정리하는 데에 오래 걸렸다.
보통 몰랐던 내용이나 새로운 파생된 내용을 알게 된 걸 위주로 정리하는데, 이번엔 많은 부분을 모르겠어서 모르는 거 전부 정리했다.. 이해가 잘 안 되면 봤던 내용 다시 읽고, 다시 읽었다. '머야 나 난독증..?' 싶을 정도로 이해가 안 되는 부분이 있어서 간격을 두고 반복해서 읽었다.
스터디에서 발표하기 전에 다시 한번 더 읽어보니 그래도 거의 다 이해가 되긴 했어서 안도했다. 정말 이해가 안 되는 부분은 할 만큼 열심히 해보고, 한숨 자거나 환기 한 번 한 다음에 다시 보면 전보단 이해가 잘 되는 듯하다.
이해가 안 돼서 이런저런 짤 찾다가 위 움짤도 만들었다 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 맘에 든다....
하 그리고 이번 포스팅 내용이 너무 길어서 임시저장이 계속 안 돼서 2번이나 일부 내용이 날라갔다. 그리고 작성하는 지금도 텍스트 입력할 때 렉이 걸린다.. 길게 작성할 것 같으면 그냥 바로 업로드해야겠다. 휴,, 시행착오는 있었으나 새로운 걸 알았다.
스터디원의 사정으로 인해 다음 스터디는 2주 뒤다! 내 파트가 아닌 부분도 부지런히 읽어서 요약할 내용 있으면 또 업로드해야겠다.
'[Front-end] 개발자 공부' 카테고리의 다른 글
[개발 공부 74일차] 댓글 미반영, 유저 유형 안 보이는 이슈 해결 (0) | 2024.06.24 |
---|---|
[개발 공부 73일차] React에서 State 변형을 권장하지 않는 이유 5가지 (0) | 2024.06.18 |
[개발 공부 71일차] 비동기 DAY | React Query, Thunk, Promise (0) | 2024.06.12 |
[개발 공부 70일차] DOM과 React의 작동 원리, 가비지 컬렉터 (2) | 2024.06.10 |
[개발 공부 69일차] React 공식 문서 Study | Markup 뜻 (0) | 2024.06.09 |