State를 보존하고 초기화하기
각 컴포넌트는 독립된 State를 가짐
React는 UI 트리에서의 위치를 통해 각 State가 어떤 컴포넌트에 속하는지 추적
리렌더링마다 언제 State를 보존하고 초기화할지 컨트롤 가능
✔️ State는 렌더트리의 위치에 연결됨
React는 UI 안에 있는 컴포넌트 구조로 렌더 트리를 만듦
컴포넌트 안에 State를 선언
- State는 해당 컴포넌트 내부에 종속되어 존재 (X)
- State는 React 엔진 내부(메모리 장부)에 존재 (O)
React는 컴포넌트가 UI 트리에 있는 위치를 이용해 React가 가지고 있는 각 State를 알맞은 컴포넌트와 연결
// App.js
import { useState } from 'react';
export default function App() {
const counter = <Counter />;
return (
<div>
{counter}
{counter}
</div>
);
}
function Counter() {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}

이 둘은 트리에서 자기 고유의 위치에 렌더링되어 있음 == 분리되어 있는 카운터
React에서 화면의 각 컴포넌트는 완전히 분리된 State를 가짐
ex) 두 Counter 컴포넌트를 나란히 렌더링하면 그들은 자신만의 독립된 score과 hover State를 가지게 됨
// App.js
import { useState } from 'react';
export default function App() {
const [showB, setShowB] = useState(true);
return (
<div>
<Counter />
{showB && <Counter />}
<label>
<input
type="checkbox"
checked={showB}
onChange={e => {
setShowB(e.target.checked)
}}
/>
Render the second counter
</label>
</div>
);
}
function Counter() {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
^ 두 번째 카운터를 렌더링하지 않을 때 그 State가 완전히 사라짐
--> React가 컴포넌트를 제거할 때 그 State도 같이 제거하기 때문
React는 컴포넌트가 UI 트리에서 그 자리에 렌더링되는 한 State를 유지함
만약 그것을 제거하거나 같은 자리에 다른 컴포넌트가 렌더링되면 그 State를 버림
✔️ 같은 자리의 같은 컴포넌트는 State를 보존
// App.js
import { useState } from 'react';
export default function App() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{isFancy ? (
<Counter isFancy={true} />
) : (
<Counter isFancy={false} />
)}
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
Use fancy styling
</label>
</div>
);
}
function Counter({ isFancy }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
if (isFancy) {
className += ' fancy';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
^ 체크박스를 선택하거나 선택 해제할 때 카운터 State는 초기화되지 X
isFancy가 true이든 false이든 <Counter />는 같은 자리에 존재
ㄴ root App 컴포넌트가 반환한 div의 첫 번째 자식으로
첫 번째 자리의 같은 컴포넌트이기 때문에 React의 관점에서는 같은 카운터
✔️ 같은 위치의 다른 컴포넌트는 State를 초기화
// App.js
import { useState } from 'react';
export default function App() {
const [isPaused, setIsPaused] = useState(false);
return (
<div>
{isPaused ? (
<p>See you later!</p>
) : (
<Counter />
)}
<label>
<input
type="checkbox"
checked={isPaused}
onChange={e => {
setIsPaused(e.target.checked)
}}
/>
Take a break
</label>
</div>
);
}
function Counter() {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
^ 체크박스를 선택하면 <Couter>가 <p>로 교체됨
같은 자리의 다른 컴포넌트 타입으로 바꿈
처음에는 <div>가 Counter를 갖고 있지만, p로 바꾸면 React는 UI 트리에서 Counter와 그 State를 제거


같은 위치에 다른 컴포넌트를 렌더링할 때 컴포넌트는 그의 전체 서브 트리의 State를 초기화함
// App.js
import { useState } from 'react';
export default function App() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{isFancy ? (
<div>
<Counter isFancy={true} />
</div>
) : (
<section>
<Counter isFancy={false} />
</section>
)}
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
Use fancy styling
</label>
</div>
);
}
function Counter({ isFancy }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
if (isFancy) {
className += ' fancy';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
^ Counter를 렌더링하지만, div의 첫 번째 자식이 section에서 div로 바뀜
ㄴ 자식 section이 DOM에서 제거되었을 때, 그 아래 전체 트리도 함께 제거됨


리렌더링할 때 State를 유지하고 싶다면, 트리 구조가 같아야 함
만약 구조가 다르다면 React가 트리에서 컴포넌트를 지울 때 State를 지우기 때문에 State가 유지되지 않음
✔️ 같은 위치에서 State를 초기화하기
기본적으로 React는 컴포넌트가 같은 위치를 유지하면 State를 유지
// App.js
import { useState } from 'react';
export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA ? (
<Counter person="Taylor" />
) : (
<Counter person="Sarah" />
)}
<button onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
Next player!
</button>
</div>
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{person}'s score: {score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
^ 선수를 바꿀 때 점수가 유지됨
ㄴ 두 Counter가 같은 위치에 나타나기 때문에 React는 그들을 person props가 변경된 같은 Counter로 봄
개념적으로 App에는 두 개의 분리된 카운터가 있어야 함
그들은 UI에 같은 위치에 나타나지만, 하나는 Taylor의 카운터이고 다른 하나는 Sarah의 카운터
둘을 바꿀 때 각 State를 초기화하기 위한 두 가지 방법
- 다른 위치에 컴포넌트를 렌더링하기
- 각 컴포넌트에 key로 명시적인 식별자를 제공하기
✔️ 선택지 1: 다른 위치에 컴포넌트를 렌더링하기
두 Counter가 독립적이기를 원한다면 둘을 다른 위치에 렌더링할 수 O
// App.js
import { useState } from 'react';
export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA &&
<Counter person="Taylor" />
}
{!isPlayerA &&
<Counter person="Sarah" />
}
<button onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
Next player!
</button>
</div>
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{person}'s score: {score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
- 처음에는 isPlayerA가 true
- 첫 번째 자리에 Counter가 있고, 두 번째 자리는 비어있음
- 'Next player' 클릭
- 첫 번째 자리는 비워지고, 두 번째 자리에 Counter가 옴

* 해당 방법은 같은 자리에 적은 수의 독립된 컴포넌트만을 가지고 있을 때 편리함
✔️ 선택지 2: key를 이용해 State를 초기화하기
React가 컴포넌트를 구별할 수 있도록 key를 사용할 수도 있음
기본적으로 React는 컴포넌트를 구별하기 위해 부모 안에서의 순서('첫 번째 카운터', '두 번째 카운터')를 이용
그러나, key를 이용하면 React에게 단지 첫 번째, 두 번째 카운터가 아니라 특정한 카운터라고 정해줄 수 있음
ex) Taylor의 카운터, Sarah의 카운터
// App.js
import { useState } from 'react';
export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
<button onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
Next player!
</button>
</div>
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{person}'s score: {score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
^ 두 <Counter />는 JSX에서 같은 위치에 나타나지만, State를 공유하지는 않음
ㄴ 다른 key를 주었기 때문
key를 명시하면 React는 부모 내에서의 순서 대신에 key 자체를 위치의 일부로 사용
* key는 전역적으로 유일하지 않고, 오직 부모 안에서만 자리를 명시함
✔️ key를 이용해 폼을 초기화하기
// App.js
import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';
export default function Messenger() {
const [to, setTo] = useState(contacts[0]);
return (
<div>
<ContactList
contacts={contacts}
selectedContact={to}
onSelect={contact => setTo(contact)}
/>
<Chat key={to.id} contact={to} />
</div>
)
}
const contacts = [
{ id: 0, name: 'Taylor', email: 'taylor@mail.com' },
{ id: 1, name: 'Alice', email: 'alice@mail.com' },
{ id: 2, name: 'Bob', email: 'bob@mail.com' }
];
// ContactList.js
export default function ContactList({
selectedContact,
contacts,
onSelect
}) {
return (
<section className="contact-list">
<ul>
{contacts.map(contact =>
<li key={contact.id}>
<button onClick={() => {
onSelect(contact);
}}>
{contact.name}
</button>
</li>
)}
</ul>
</section>
);
}
// Chat.js
import { useState } from 'react';
export default function Chat({ contact }) {
const [text, setText] = useState('');
return (
<section className="chat">
<textarea
value={text}
placeholder={'Chat to ' + contact.name}
onChange={e => setText(e.target.value)}
/>
<br />
<button>Send to {contact.email}</button>
</section>
);
}
State 로직을 Reducer로 작성하기
한 컴포넌트에서 State 업데이트가 여러 이벤트 핸들러로 분산되는 경우 O
--> 컴포넌트를 관리하기 어려워짐
문제 해결을 위해 State를 업데이트하는 모든 로직을 Reducer를 사용해 컴포넌트 외부의 단일 함수로 통합해 관리할 수 O
✔️ Reducer를 사용하여 State 로직 통합하기
컴포넌트가 복잡해지면 컴포넌트의 State가 업데이트되는 다양한 경우를 한눈에 파악하기 어려워짐
// App.js
import { useState } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
export default function TaskApp() {
const [tasks, setTasks] = useState(initialTasks);
function handleAddTask(text) {
setTasks([...tasks, {
id: nextId++,
text: text,
done: false
}]);
}
function handleChangeTask(task) {
setTasks(tasks.map(t => {
if (t.id === task.id) {
return task;
} else {
return t;
}
}));
}
function handleDeleteTask(taskId) {
setTasks(
tasks.filter(t => t.id !== taskId)
);
}
return (
<>
<h1>Prague itinerary</h1>
<AddTask
onAddTask={handleAddTask}
/>
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
let nextId = 3;
const initialTasks = [
{ id: 0, text: 'Visit Kafka Museum', done: true },
{ id: 1, text: 'Watch a puppet show', done: false },
{ id: 2, text: 'Lennon Wall pic', done: false },
];
^ 각 이벤트 핸들러는 State를 업데이트하기 위해 setTasks를 호출
--> 컴포넌트가 커질수록 그 안에서 State를 다루는 로직의 양도 늘어나게 됨
복잡성은 줄이고 접근성을 높이기 위해, 컴포넌트 내부의 State 로직을 컴포넌트 외부의 Reducer라는 단일 함수로 옮길 수 있음
Reducer는 State를 다루는 다른 방법
useState에서 useReducer로 바꾸는 단계
- State를 설정하는 방식에서 Action을 Dispatch하는 방식으로 전환하기
- Reducer 함수 작성하기
- 컴포넌트에서 Reducer 사용하기
✔️ 1단계: State를 설정하는 방식에서 Action을 Dispatch하는 방식으로 전환하기
function handleAddTask(text) {
setTasks([...tasks, {
id: nextId++,
text: text,
done: false
}]);
}
function handleChangeTask(task) {
setTasks(tasks.map(t => {
if (t.id === task.id) {
return task;
} else {
return t;
}
}));
}
function handleDeleteTask(taskId) {
setTasks(
tasks.filter(t => t.id !== taskId)
);
}
^ 현재 이벤트 핸들러는 State를 설정함으로써 무엇을 할 것인지를 명시
Reducer를 사용한 State 관리는 State를 직접 설정하는 것과 약간 다름
State를 설정하여 React에게 무엇을 할지를 지시하는 대신, 이벤트 핸들러에서 Action을 전달하여 사용자가 방금 한 일을 지정
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId
});
}
function handleDeleteTask(taskId) {
dispatch(
// "Action" 객체:
{
type: 'deleted',
id: taskId
}
);
}
^ dispatch 함수에 넣어준 객체를 Action이라고 함
ㄴ 일반적인 자바스크립트 객체 (어떤 상황이 발생하는지에 대한 최소한의 정보를 담고 있어야 함)
✔️ 2단계: Reducer 함수 작성하기
Reducer 함수는 State에 대한 로직을 넣는 곳
현재의 State 값과 Action 객체, 두 개의 인자를 받고 다음 State 값을 반환
function yourReducer(state, action) {
// React가 설정하게될 다음 State 값을 반환합니다.
}
React는 Reducer에서 반환한 값을 State에 설정
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
}
case 'changed': {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
^ State 설정과 관련 모든 로직을 Reducer 함수로 마이그레이션한 코드
✔️ 3단계: 컴포넌트에서 Reducer 사용하기
import { useReducer } from 'react';
^ React에서 useReducer Hook을 불러오기
const [tasks, setTasks] = useState(initialTasks);
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
^ useState를 useReducer로 바꾸기
useReducer Hook은 두 개의 인자를 넘겨받음
- Reducer 함수
- 초기 State 값
반환하는 값
- State를 담을 수 있는 값
- Dispatch 함수 (사용자의 Action을 Reducer 함수에게 전달하게 될)
// App.js
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId
});
}
return (
<>
<h1>Prague itinerary</h1>
<AddTask
onAddTask={handleAddTask}
/>
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
}
case 'changed': {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
let nextId = 3;
const initialTasks = [
{ id: 0, text: 'Visit Kafka Museum', done: true },
{ id: 1, text: 'Watch a puppet show', done: false },
{ id: 2, text: 'Lennon Wall pic', done: false }
];
✔️ useState와 useReducer 비교하기
- 코드 크기
- 일반적으로 useState를 사용하면 미리 작성해야 하는 코드가 줄어듬
- useReducer를 사용하면 Reducer 함수, Action을 전달하는 부분 둘다 작성해야 함
- 여러 이벤트 핸들러에서 비슷한 방식으로 State를 업데이트하는 경우, useReducer를 사용하면 코드의 양을 줄일 수 있음
- 가독성
- useState로 간단한 State를 업데이트하는 경우에는 가독성이 좋은 편
- 그렇지만 더 복잡한 구조의 State를 다루게 되면 컴포넌트의 코드 양이 더 많아져 한눈에 읽기 어려워질 수 있음
- 이 경우 useReducer를 사용하면 업데이트 로직이 어떻게 동작하는지와 이벤트 핸들러를 통해서 무엇이 발생했는지 구현한 부분을 명확하게 구분 가능
- 디버깅
- useState를 사용하면 버그를 발견했을 때 왜, 어디서 State가 잘못 설정됐는지 찾기 어려울 수 있음
- useReducer를 사용하면 콘솔 로그를 Reducer에 추가하여 State가 업데이트되는 모든 부분과 왜 해당 버그가 발생했는지 확인 가능
- 그렇지만 useState를 사용하는 경우보다 더 많은 코드를 단계별로 실행해서 디버깅해야 하는 점이 있음
- 테스팅
- Reducer는 컴포넌트에 의존하지 않는 순수 함수 --> Reducer를 독립적으로 분리해서 내보내거나 테스트할 수 있음
✔️ Reducer 잘 작성하기
- Reducer는 반드시 순수해야 함
- Reducer는 렌더링 중에 실행됨
- 입력 값이 같다면 결과 값도 같아야 함
- 요청을 보내거나 timeout을 스케줄링하거나 사이드 이펙트(컴포넌트 외부에 영향을 미치는 작업)를 수행해서는 안 됨
- Reducer는 객체와 배열을 변경하지 않고 업데이트해야 함
- 각 Action은 데이터 안에서 여러 변경들이 있더라도 하나의 사용자 상호작용을 설명해야 함
- 사용자가 Reducer가 관리하는 5개의 필드가 있는 양식에서 '재설정'을 누른 경우, 5개의 개별 set_field Action보다는 하나의 reset_from Action을 전송하는 것이 더 합리적
✔️ Immer로 간결한 Reducer 작성하기
일반적인 State에서 객체와 배열을 업데이트하는 것처럼, Immer 라이브러리를 사용하면 Reducer를 더 간결하게 작성 가능
// package.json
{
"dependencies": {
"immer": "1.7.3",
"react": "latest",
"react-dom": "latest",
"react-scripts": "latest",
"use-immer": "0.5.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"devDependencies": {}
}
// App.js
import { useImmerReducer } from 'use-immer';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
function tasksReducer(draft, action) {
switch (action.type) {
case 'added': {
draft.push({
id: action.id,
text: action.text,
done: false
});
break;
}
case 'changed': {
const index = draft.findIndex(t =>
t.id === action.task.id
);
draft[index] = action.task;
break;
}
case 'deleted': {
return draft.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
export default function TaskApp() {
const [tasks, dispatch] = useImmerReducer(
tasksReducer,
initialTasks
);
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId
});
}
return (
<>
<h1>Prague itinerary</h1>
<AddTask
onAddTask={handleAddTask}
/>
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
let nextId = 3;
const initialTasks = [
{ id: 0, text: 'Visit Kafka Museum', done: true },
{ id: 1, text: 'Watch a puppet show', done: false },
{ id: 2, text: 'Lennon Wall pic', done: false },
];
^ useImmerReducer를 사용하여 push 또는 arr[i] = 로 값을 할당함으로써 State를 변경
'React > 공식 문서' 카테고리의 다른 글
| Context API (0) | 2026.05.27 |
|---|---|
| 상태 구조 설계와 공유 (0) | 2026.05.07 |
| 불변 업데이트 패턴 (0) | 2026.04.28 |
| 렌더링과 State 업데이트 (0) | 2026.04.08 |
| 이벤트 처리와 State (0) | 2026.04.02 |