react 학습하기 - 상호작용 추가하기
state : 시간에 따라 변화하는 데이터
1️⃣ 이벤트에 응답하기
React에서는 JSX에 이벤트 핸들러를 추가할 수 있다.
이벤트 핸들러는 이벤트(클릭, 마우스 호버, 폼인풋 포커스 등 사용자 상호작용)가 발생했을 때, 브라우저나 React가 자동으로 호출하도록 등록된 함수.
React 이벤트 핸들러
- 함수 정의
- 적절한 JSX 태그에 prop 형태로 전달
📌 이벤트 핸들러 특징
이벤트 핸들러에 적절한 HTML 태그를 사용하는 지 확인하자!
- 주로 컴포넌트 내부에서 정의한다.
- handle로 시작하고 그 뒤에 이벤트명을 붙인 함수명으로 작성한다.
이벤트 핸들러로 등록될 함수는 호출이 아닌 전달되어야한다!
함수를 전달하기 함수를 호출하기 onClick={handleClick}onClick={handleClick()}onClick={() => alert('clicked')}onClick={alert('clicked')}두번째에서 ()가 렌더링 과정 중 클릭이 없음에도 즉시 함수 실행됨. 이는 JSX의 {} 안에 있는 자바스크립트 표현식은 렌더링 시점에 바로 실행되기 때문이다.
-
DOM 태그(
<button>,<input>)의 React가 미리 정해둔 이벤트 prop(onClick,onChange,onSubmit등)에 함수를 전달하면, 그 함수가 실제 브라우저 이벤트 핸들러가 된다. -
관습적으로 이벤트용 props 이름은
on으로 시작하고,onClick,onChange,onClose처럼 카멜케이스로 짓는다.
📌 이벤트 전파
이벤트는 트리를 따라 bubble 되거나 전파된다. 부여된 JSX태그 내에서만 실행되는 onScroll을 제외한 모든 이벤트는 전파된다.
1. 전파 멈추기
이벤트 핸들러는 이벤트 오브젝트를 유일한 매개변수로 받음. event.stopPropagation()을 호출하여 전파를 멈출 수 있다.
2. 이벤트 기본 동작 방지하기
일부 브라우저 이벤트는 기본 동작을 가진다. 일례로 <form>의 제출 이벤트는 페이지 전체를 리로드 하는 것이 기본 동작이다.
이벤트 핸들러에서 event.preventDefault()를 호출하여 기본 동작을 방지할 수 있다.
event.stopPropagation()은 이벤트 핸들러가 상위 태그에서 실행되지 않도록 멈추는 것. event.preventDefault()는 이벤트 핸들러가 기본 동작을 실행하지 않도록 방지하는 것.
2️⃣ state: 컴포넌트의 기억 저장소
- 지역 변수는 렌더링 간에 유지 되지 않는다.
- 지역 변수를 변경해도 렌더링을 일으키지 않는다.
📌 state의 필요성
- 렌더링 사이에 데이터를 유지 할 무언가가 필요하다.
- React가 새로운 데이터로 컴포넌트를 렌더링하도록 유발할 무언가가 필요하다.
useState훅은 이 두 가지를 제공
- 렌더링 간에 데이터를 유지하기 위한 state 변수
- 변수를 업데이트하고 렌더링을 일으키는 state를 변경하는 setter 함수(set 함수)
📌 첫 번째 훅 만나기
use로 시작하는 다른 모든 함수를 hook이라고 한다.
hook: React가 오직 렌더링 중일 때만 사용할 수 있는 특별한 함수. 컴포넌트의 최상위 수준 또는 커스텀 훅에서만 호출 가능. 조건문, 반복문, 기타 중첩 함수 내부에서 호출 불가능.
React는 어떤 state를 반환하는지 어떻게 알 수 있을까?
useState 호출이 어떤 state 변수를 참조하는지에 대한 정보를 받지 못한다는 것을 알 수 있다.
const [count, setCount] = useState(0); const [name, setName] = useState('Kim');useState에 "이건 count야", "이건 name이야" 같은 식별자를 넘기지 않는다.
그런데도 React는 렌더링마다 정확히 같은 state를 다시 돌려준다.
정답은 state의 호출 순서
- React는 동일한 컴포넌트의 모든 렌더링에서 훅이 항상 같은 순서로 호출된다는 규칙에 의존
- 내부적으로
useState가 호출된 순서대로 0, 1, 2… 번 "슬롯"에 state를 저장- 이후 렌더링에서도 같은 순서로 훅을 호출해 같은 슬롯에서 state를 꺼내서 반환
📌 State의 특징
state는 컴포넌트 인스턴스에 지역적임 => 화면에 있는 컴포넌트 하나하나 마다 state를 따로 관리. 따로 관리 하기 때문에 완전 독립적.
3️⃣ 렌더링 그리고 커밋
실제로 컴포넌트가 화면에 띄워지기 전에 React에서는 몇 가지 단계를 거친다.
렌더링, 커밋, 그리고 이 렌더링을 알아채는 렌더링 트리거까지 총 3 단계로 공식문서에서 소개하고 있다.
📌 1단계: 렌더링 트리거
컴포넌트 렌더링이 일어나는 데는 2가지 이유가 있다.
- 컴포넌트가 초기 렌더링인 경우
- 컴포넌트의 state가 업데이트 된 경우
1. 초기 렌더링
앱을 시작할 때 초기 렌더링을 트리거 해야함. 보통 createRoot를 사용하여 초기 렌더링을 트리거한다.
2. state 업데이트 시 리렌더링
set함수를 통해 상태를 업데이트하여 추가적인 렌더링을 트리거 할 수 있음. 컴포넌트의 상태를 업데이트하면 자동으로 렌더링 대기열에 추가됨.
📌 2단계: React 컴포넌트 렌더링
렌더링 : React에서 컴포넌트를 호출하는 것
- 초기 렌더링에서 React는 루트 컴포넌트 호출
- 이후 렌더링에서 React는 state 업데이트가 일어나 렌더링을 트리거한 컴포넌트만 호출
업데이트된 컴포넌트가 다른 컴포넌트를 반환하면 React는 다음으로 해당 컴포넌트를 렌더링하고 중첩된 컴포넌트가 없을 때까지 재귀적으로 렌더링을 계속한다.
이러한 기본 동작은, 업데이트가 된 컴포넌트가 트리에서 매우 높은 곳에 있을 경우, 성능이 최적화 되지 않음. 성급하게 최적화 하지마라!
렌더링은 항상 순수한 계산
- 동일한 입력에는 항상 동일한 출력을 반환
- 이전의 state를 변경해서는 안됨.
📌 3단계: React가 DOM에 변경사항을 커밋
- 초기 렌더링의 경우, 생성한 모든 DOM 노드를 브라우저에 추가
- 리렌더링 할 경우, 필요한 최소한의 작업을 적용하여 DOM이 최신 렌더링 출력과 일치하도록 함
React는 렌더링 간에 차이가 있을 경우만 DOM을 변경함. 이는 최적화 되어 있음. 따라서 변하지 않는 DOM은 변경되지 않음.
📌 에필로그: 브라우저 페인트
렌더링이 완료되고 React가 DOM을 업데이트한 후 브라우저는 화면을 다시 그린다. 이 단계를 브라우저 렌더링이라고 하지만 이 문서의 나머지 부분에서 혼동을 피하고자 페인팅이라고 명명.
4️⃣ 스냅샷으로서의 state
state는 일반 변수로 보일 수 있지만, 실제로는 스냅샷처럼 동작함. set함수는 지금 이 순간 변수를 변경하는 것이 아닌 리렌더링을 발동 시킬 뿐이다.
렌더링이란 React가 컴포넌트, 즉 함수를 호출한다는 뜻. 해당 함수에서 반환하는 값은 시간상 UI의 스냅샷과 동일. prop, 이벤트 핸들러, 로컬 변수는 모두 렌더링 당시의 state를 사용해 계산됨.
- React가 함수를 호출
- 함수가 스냅샷 반환
- 스냅샷과 일치하도록 화면 업데이트
컴포넌트의 메모리로써 state는 함수가 반환된 후 사라지는 일반 변수와 다름. state는 실제로 함수 외부에 마치 선반에 있는 것처럼 React 자체에 존재함.
import { useState} from 'react';
function Counter() {
const [number, setNumber] = useState(0);
return <button onClick={() => {
setNumber(number + 1)
setNumber(number + 1)
setNumber(number + 1)
}}>{number}</button>;
}
버튼을 누르면, 숫자는 단 1만 증가한다. 첫번째에서 number이 0이므로,
- setNumber(number + 1): number는 0이므로 setNumber(0 + 1)
- React는 다음 렌더링에서 number를 1로 변경할 준비를 함
이 동작을 3번 하는 것이다.
📌 시간 경과에 따른 State
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
alert(number);
}}>+5</button>
</>
)
}
위 예제에서도 alert에는 항상 0이 찍힌다. 그렇다면 setTimeout 안에서 alert를 부르면 어떻게 될까?
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이 나온다. 리렌더링되서 5가 되었지만, 상호작용한 시점에서 state 스냅샷을 사용하고 있다. state 변수의 값은 이벤트 핸들러의 코드가 비동기적이더라도 렌더링 내에서 절대 변경되지 않는다.
5️⃣ state 업데이트 큐
React는 state 업데이트를 하기 전에 이벤트 핸들러의 모든 코드가 실행될 때까지 기다림. 이렇게 하면 리렌더링을 묶어서 처리하여 최적화할 수 있는데 이것을 batching이라고 한다. 그러나 때에 따라 다음 렌더링 전에 여러 작업을 수행하고 싶을 때가 있음.
이럴 때는 state 값을 대체하는 것이 아닌 state 값으로 무언가를 하라라고 지시하면 됨.
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
}}>+3</button>
</>
)
}
여기서 n => n+1은 업데이터 함수(updater function)라고 함. 이를 state를 변경하는 setter 함수에 전달 할 때,
- React는 이벤트 핸들러의 다른 코드가 모두 실행된 후에 이 함수가 처리되도록 큐에 넣음.
- 다음 렌더링 중에 React는 큐를 순회하여 최종 업데이트된 state를 제공.
위의 코드를 보면 setNumber(n => n + 1);: n => n+1함수를 큐에 3번 추가한다.
📌 업데이터 함수의 명명 규칙
인수의 이름은 해당 state 변수의 첫 글자로 지정하는 것이 일반적.
6️⃣ 객체 state 업데이트 하기
state는 객체를 포함한 모든 종류의 JS 값을 가질 수 있음. 하지만, React state가 가진 객체를 직접 변경해서는 안됨. 복사본을 사용해야함.
state를 읽기 전용인 것처럼 다루어라
📌 전개 문법으로 객체 복사하기
기존에 존재하는 다른 데이터를 복사할 때는 ...객체 전개 구문을 사용하면 모든 프로퍼티를 각각 복사하지 않아도 됨. 하지만 ...객체 전개 구문은 얕은 복사임. 중첩된 프로퍼티를 업데이트하고 싶다면 한 번 이상 사용해야 한다는 뜻.
📌 Immer로 간결한 갱신 로직 작성하기
state가 깊이 중첩되어 있다면 평탄화를 고려해보자. Immer는 편리하고, 변경 구문을 사용할 수 있게 해주며 복사본 생성을 도와주는 인기 있는 라이브러리.
개인적인 생각 : 상태 설계가 좋으면 Immer 안써도 될듯.
📌 왜 React에서 state 변경을 권장되지 않을까?
- 디버깅: state 직접 변경 시 렌더링 사이에 어떻게 바뀌었는지 명확하게 알 수 없음.
- 최적화 : React 최적화 전략은 이전 props 또는 state가 다음 것과 동일 할 때는 일을 건너뛰는 것에 의존.
- 새로운 기능 : React는 새로운 기능을 설계할 때도 이 '스냅샷' 개념에 많이 의존.
- 요구사항 변화: 변화를 원하는 기능(취소,복원)은 아무것도 변경되지 않았을 때 더 쉬움.
- 더 간단한 구현: React는 변경에 의존하지 않기 때문엥 객체로 뭔가 특별한 것을 할 필요가 없음.
7️⃣ 배열 state 업데이트 하기
React state에서 배열을 다룰 때는,
| 비선호(배열을 변경) | 선호(새 배열을 반환) | |
|---|---|---|
| 추가 | push, unshift |
concat,[...arr] |
| 제거 | pop, shift,splice |
filter,slice |
| 교체 | splice,arr[i] = ... |
map |
| 정렬 | reverse,sort |
배열을 복사한 이후 처리 |
slice와 splice는 매우 다른 함수
- slice는 배열 또는 그 일부를 복사
- splice는 배열을 변경
📌 배열 내부의 객체 업데이트 하기
객체는 실제로 배열 "내부"에 위치하지 않음. 단순히 "가리키는" 별도의 값. 따라서, 중첩된 state를 업데이트 할 때, 업데이트하려는 지점부터 최상위 레벨까지의 복사본을 생성해야함

