useState는 컴포넌트에 state 변수를 추가할 수 있게 해주는 React Hook이에요.
const [state, setState] = useState(initialState)
useState(initialState) {/usestate/}컴포넌트의 최상위 레벨에서 useState를 호출해서 state 변수를 선언하세요.
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(28);
const [name, setName] = useState('Taylor');
const [todos, setTodos] = useState(() => createTodos());
// ...
관례적으로 state 변수는 배열 구조 분해를 사용해서 [something, setSomething]처럼 이름을 지어요.
initialState: state가 처음에 가지길 원하는 값이에요. 어떤 타입의 값이든 될 수 있지만, 함수에 대해서는 특별한 동작이 있어요. 이 인자는 초기 렌더링 이후에는 무시돼요.initialState로 함수를 전달하면, 초기화 함수(initializer function)로 취급돼요. 순수 함수여야 하고, 인자를 받지 않아야 하며, 어떤 타입의 값이든 반환해야 해요. React는 컴포넌트를 초기화할 때 초기화 함수를 호출하고, 그 반환값을 초기 state로 저장해요. 아래 예제를 확인하세요.useState는 정확히 두 개의 값을 가진 배열을 반환해요:
initialState와 일치할 거예요.set 함수.useState는 Hook이기 때문에, 컴포넌트의 최상위 레벨 또는 커스텀 Hook에서만 호출할 수 있어요. 반복문이나 조건문 안에서는 호출할 수 없어요. 만약 그렇게 해야 한다면, 새 컴포넌트를 분리해서 state를 그쪽으로 옮기세요.set 함수, 예를 들어 setSomething(nextState) {/setstate/}useState가 반환하는 set 함수를 사용하면 state를 다른 값으로 업데이트하고 리렌더링을 트리거할 수 있어요. 다음 state를 직접 전달하거나, 이전 state로부터 계산하는 함수를 전달할 수 있어요:
const [name, setName] = useState('Edward');
function handleClick() {
setName('Taylor');
setAge(a => a + 1);
// ...
nextState: state가 되길 원하는 값이에요. 어떤 타입의 값이든 될 수 있지만, 함수에 대해서는 특별한 동작이 있어요.nextState로 함수를 전달하면, 업데이터 함수(updater function)로 취급돼요. 순수 함수여야 하고, 대기 중인 state를 유일한 인자로 받아서 다음 state를 반환해야 해요. React는 업데이터 함수를 큐에 넣고 컴포넌트를 리렌더링할 거예요. 다음 렌더링 중에 React는 큐에 있는 모든 업데이터를 이전 state에 적용해서 다음 state를 계산해요. 아래 예제를 확인하세요.set 함수는 반환값이 없어요.
set 함수는 다음 렌더링을 위한 state 변수만 업데이트해요. set 함수를 호출한 후에 state 변수를 읽으면, 호출 전에 화면에 있던 이전 값을 여전히 얻게 돼요.
여러분이 제공한 새 값이 현재 state와 동일하다면 (Object.is 비교로 판단), React는 컴포넌트와 그 자식들의 리렌더링을 건너뛸 거예요. 이건 최적화예요. 일부 경우에 React가 자식들을 건너뛰기 전에 컴포넌트를 호출해야 할 수도 있지만, 여러분의 코드에는 영향을 주지 않아야 해요.
React는 state 업데이트를 일괄 처리(batch)해요. 모든 이벤트 핸들러가 실행되고 set 함수들을 호출한 후에 화면을 업데이트해요. 이렇게 하면 단일 이벤트 중에 여러 번 리렌더링되는 걸 방지해요. 드문 경우에 React가 더 일찍 화면을 업데이트하도록 강제해야 한다면(예: DOM에 접근하기 위해), flushSync를 사용할 수 있어요.
set 함수는 안정적인 정체성(stable identity)을 가지고 있어서, Effect 의존성에서 생략하는 걸 자주 볼 수 있지만, 포함해도 Effect가 발동되지 않아요. 린터가 에러 없이 의존성을 생략할 수 있게 해준다면, 그렇게 해도 안전해요. Effect 의존성 제거하기에 대해 더 알아보세요.
렌더링 중에 set 함수를 호출하는 건 현재 렌더링 중인 컴포넌트 내에서만 허용돼요. React는 그 출력을 버리고 즉시 새 state로 다시 렌더링하려고 시도할 거예요. 이 패턴은 거의 필요하지 않지만, 이전 렌더링의 정보를 저장하는 데 사용할 수 있어요. 아래 예제를 확인하세요.
Strict Mode에서는 React가 우발적인 불순물을 찾는 데 도움이 되도록 업데이터 함수를 두 번 호출해요. 이건 개발 환경에서만 일어나는 동작이고, 프로덕션에는 영향을 주지 않아요. 업데이터 함수가 순수하다면(당연히 그래야 하고요), 이게 동작에 영향을 주지 않을 거예요. 두 번의 호출 중 하나의 결과는 무시돼요.
컴포넌트의 최상위 레벨에서 useState를 호출해서 하나 이상의 state 변수를 선언하세요.
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(42);
const [name, setName] = useState('Taylor');
// ...
관례적으로 state 변수는 배열 구조 분해를 사용해서 [something, setSomething]처럼 이름을 지어요.
useState는 정확히 두 개의 항목이 있는 배열을 반환해요:
set 함수.화면에 표시되는 것을 업데이트하려면, 다음 state를 전달하면서 set 함수를 호출하세요:
function handleClick() {
setName('Robin');
}
React는 다음 state를 저장하고, 새 값으로 컴포넌트를 다시 렌더링하고, UI를 업데이트해요.
⚠️ 주의
set함수를 호출해도 이미 실행 중인 코드에서는 현재 state가 변경되지 않아요:function handleClick() { setName('Robin'); console.log(name); // 여전히 "Taylor"! }다음 렌더링부터
useState가 반환할 값에만 영향을 줘요.
이 예제에서 count state 변수는 숫자를 담고 있어요. 버튼을 클릭하면 증가해요.
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}
이 예제에서 text state 변수는 문자열을 담고 있어요. 타이핑하면, handleChange가 브라우저 input DOM 요소에서 최신 입력 값을 읽고, setText를 호출해서 state를 업데이트해요. 이렇게 하면 아래에 현재 text를 표시할 수 있어요.
import { useState } from 'react';
export default function MyInput() {
const [text, setText] = useState('hello');
function handleChange(e) {
setText(e.target.value);
}
return (
<>
<input value={text} onChange={handleChange} />
<p>You typed: {text}</p>
<button onClick={() => setText('hello')}>
Reset
</button>
</>
);
}
이 예제에서 liked state 변수는 불리언을 담고 있어요. input을 클릭하면, setLiked가 브라우저 체크박스 input이 체크되었는지 여부로 liked state 변수를 업데이트해요. liked 변수는 체크박스 아래의 텍스트를 렌더링하는 데 사용돼요.
import { useState } from 'react';
export default function MyCheckbox() {
const [liked, setLiked] = useState(true);
function handleChange(e) {
setLiked(e.target.checked);
}
return (
<>
<label>
<input
type="checkbox"
checked={liked}
onChange={handleChange}
/>
I liked this
</label>
<p>You {liked ? 'liked' : 'did not like'} this.</p>
</>
);
}
같은 컴포넌트에서 하나 이상의 state 변수를 선언할 수 있어요. 각 state 변수는 완전히 독립적이에요.
import { useState } from 'react';
export default function Form() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);
return (
<>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<button onClick={() => setAge(age + 1)}>
Increment age
</button>
<p>Hello, {name}. You are {age}.</p>
</>
);
}
button { display: block; margin-top: 10px; }
age가 42라고 가정해봐요. 이 핸들러는 setAge(age + 1)를 세 번 호출해요:
function handleClick() {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
}
하지만 한 번 클릭한 후에, age는 45가 아니라 43만 될 거예요! 이건 set 함수를 호출해도 이미 실행 중인 코드에서 age state 변수를 업데이트하지 않기 때문이에요. 그래서 각 setAge(age + 1) 호출이 setAge(43)이 되는 거예요.
이 문제를 해결하려면, 다음 state 대신 업데이터 함수를 setAge에 전달할 수 있어요:
function handleClick() {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}
여기서 a => a + 1이 업데이터 함수예요. 대기 중인 state를 받아서 그것으로부터 다음 state를 계산해요.
React는 업데이터 함수들을 큐에 넣어요. 그런 다음 다음 렌더링 중에 같은 순서로 호출할 거예요:
a => a + 1은 대기 중인 state로 42를 받고 다음 state로 43을 반환해요.a => a + 1은 대기 중인 state로 43을 받고 다음 state로 44를 반환해요.a => a + 1은 대기 중인 state로 44를 받고 다음 state로 45를 반환해요.다른 큐에 있는 업데이트가 없으므로, React는 결국 45를 현재 state로 저장할 거예요.
관례적으로, 대기 중인 state 인자의 이름은 state 변수 이름의 첫 글자로 짓는 게 일반적이에요, age에 대해 a처럼요. 하지만 prevAge처럼 또는 더 명확하다고 생각하는 다른 이름으로 부를 수도 있어요.
React는 개발 환경에서 업데이터가 순수한지 확인하기 위해 업데이터를 두 번 호출할 수 있어요.
설정하려는 state가 이전 state로부터 계산되는 경우 항상 setAge(a => a + 1) 같은 코드를 작성하라는 권장 사항을 들을 수 있어요. 그렇게 해도 해가 되지는 않지만, 항상 필요한 건 아니에요.
대부분의 경우에는 이 두 접근법 사이에 차이가 없어요. React는 클릭 같은 의도적인 사용자 액션에 대해서는 다음 클릭 전에 age state 변수가 업데이트되도록 항상 보장해요. 이는 이벤트 핸들러의 시작 부분에서 클릭 핸들러가 "오래된" age를 볼 위험이 없다는 걸 의미해요.
하지만 같은 이벤트 내에서 여러 업데이트를 수행한다면, 업데이터가 도움이 될 수 있어요. state 변수 자체에 접근하는 게 불편한 경우에도 유용해요 (리렌더링을 최적화할 때 이런 상황에 부딪힐 수 있어요).
약간 더 장황한 문법보다 일관성을 선호한다면, 설정하려는 state가 이전 state로부터 계산되는 경우 항상 업데이터를 작성하는 게 합리적이에요. 만약 다른 state 변수의 이전 state로부터 계산되는 거라면, 하나의 객체로 합치고 리듀서를 사용하는 걸 고려해볼 수 있어요.
이 예제는 업데이터 함수를 전달하므로, "+3" 버튼이 작동해요.
import { useState } from 'react';
export default function Counter() {
const [age, setAge] = useState(42);
function increment() {
setAge(a => a + 1);
}
return (
<>
<h1>Your age: {age}</h1>
<button onClick={() => {
increment();
increment();
increment();
}}>+3</button>
<button onClick={() => {
increment();
}}>+1</button>
</>
);
}
button { display: block; margin: 10px; font-size: 20px; }
h1 { display: block; margin: 10px; }
이 예제는 업데이터 함수를 전달하지 않으므로, "+3" 버튼이 의도한 대로 작동하지 않아요.
import { useState } from 'react';
export default function Counter() {
const [age, setAge] = useState(42);
function increment() {
setAge(age + 1);
}
return (
<>
<h1>Your age: {age}</h1>
<button onClick={() => {
increment();
increment();
increment();
}}>+3</button>
<button onClick={() => {
increment();
}}>+1</button>
</>
);
}
button { display: block; margin: 10px; font-size: 20px; }
h1 { display: block; margin: 10px; }
객체와 배열을 state에 넣을 수 있어요. React에서 state는 읽기 전용으로 간주되므로, 기존 객체를 변경(mutate)하는 게 아니라 교체해야 해요. 예를 들어, state에 form 객체가 있다면, 이걸 변경하지 마세요:
// 🚩 state의 객체를 이렇게 변경하지 마세요:
form.firstName = 'Taylor';
대신, 새 객체를 생성해서 전체 객체를 교체하세요:
// ✅ 새 객체로 state를 교체하세요
setForm({
...form,
firstName: 'Taylor'
});
더 자세한 내용은 state에서 객체 업데이트하기와 state에서 배열 업데이트하기를 읽어보세요.
이 예제에서 form state 변수는 객체를 담고 있어요. 각 input은 전체 폼의 다음 state로 setForm을 호출하는 change 핸들러를 가지고 있어요. { ...form } 스프레드 문법은 state 객체가 변경되는 게 아니라 교체되도록 보장해요.
import { useState } from 'react';
export default function Form() {
const [form, setForm] = useState({
firstName: 'Barbara',
lastName: 'Hepworth',
email: 'bhepworth@sculpture.com',
});
return (
<>
<label>
First name:
<input
value={form.firstName}
onChange={e => {
setForm({
...form,
firstName: e.target.value
});
}}
/>
</label>
<label>
Last name:
<input
value={form.lastName}
onChange={e => {
setForm({
...form,
lastName: e.target.value
});
}}
/>
</label>
<label>
Email:
<input
value={form.email}
onChange={e => {
setForm({
...form,
email: e.target.value
});
}}
/>
</label>
<p>
{form.firstName}{' '}
{form.lastName}{' '}
({form.email})
</p>
</>
);
}
label { display: block; }
input { margin-left: 5px; }
이 예제에서 state는 더 중첩되어 있어요. 중첩된 state를 업데이트할 때는 업데이트하려는 객체의 복사본을 만들어야 하고, 위쪽으로 가는 경로에 있는 객체들도 "포함"해야 해요. 더 자세한 내용은 중첩된 객체 업데이트하기를 읽어보세요.
import { useState } from 'react';
export default function Form() {
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
function handleNameChange(e) {
setPerson({
...person,
name: e.target.value
});
}
function handleTitleChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
title: e.target.value
}
});
}
function handleCityChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
city: e.target.value
}
});
}
function handleImageChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
image: e.target.value
}
});
}
return (
<>
<label>
Name:
<input
value={person.name}
onChange={handleNameChange}
/>
</label>
<label>
Title:
<input
value={person.artwork.title}
onChange={handleTitleChange}
/>
</label>
<label>
City:
<input
value={person.artwork.city}
onChange={handleCityChange}
/>
</label>
<label>
Image:
<input
value={person.artwork.image}
onChange={handleImageChange}
/>
</label>
<p>
<i>{person.artwork.title}</i>
{' by '}
{person.name}
<br />
(located in {person.artwork.city})
</p>
<img
src={person.artwork.image}
alt={person.artwork.title}
/>
</>
);
}
label { display: block; }
input { margin-left: 5px; margin-bottom: 5px; }
img { width: 200px; height: 200px; }
이 예제에서 todos state 변수는 배열을 담고 있어요. 각 버튼 핸들러는 해당 배열의 다음 버전으로 setTodos를 호출해요. [...todos] 스프레드 문법, todos.map(), todos.filter()는 state 배열이 변경되는 게 아니라 교체되도록 보장해요.
// src/App.js
import { useState } from 'react';
import AddTodo from './AddTodo.js';
import TaskList from './TaskList.js';
let nextId = 3;
const initialTodos = [
{ id: 0, title: 'Buy milk', done: true },
{ id: 1, title: 'Eat tacos', done: false },
{ id: 2, title: 'Brew tea', done: false },
];
export default function TaskApp() {
const [todos, setTodos] = useState(initialTodos);
function handleAddTodo(title) {
setTodos([
...todos,
{
id: nextId++,
title: title,
done: false
}
]);
}
function handleChangeTodo(nextTodo) {
setTodos(todos.map(t => {
if (t.id === nextTodo.id) {
return nextTodo;
} else {
return t;
}
}));
}
function handleDeleteTodo(todoId) {
setTodos(
todos.filter(t => t.id !== todoId)
);
}
return (
<>
<AddTodo
onAddTodo={handleAddTodo}
/>
<TaskList
todos={todos}
onChangeTodo={handleChangeTodo}
onDeleteTodo={handleDeleteTodo}
/>
</>
);
}
// src/AddTodo.js
import { useState } from 'react';
export default function AddTodo({ onAddTodo }) {
const [title, setTitle] = useState('');
return (
<>
<input
placeholder="Add todo"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<button onClick={() => {
setTitle('');
onAddTodo(title);
}}>Add</button>
</>
)
}
// src/TaskList.js
import { useState } from 'react';
export default function TaskList({
todos,
onChangeTodo,
onDeleteTodo
}) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<Task
todo={todo}
onChange={onChangeTodo}
onDelete={onDeleteTodo}
/>
</li>
))}
</ul>
);
}
function Task({ todo, onChange, onDelete }) {
const [isEditing, setIsEditing] = useState(false);
let todoContent;
if (isEditing) {
todoContent = (
<>
<input
value={todo.title}
onChange={e => {
onChange({
...todo,
title: e.target.value
});
}} />
<button onClick={() => setIsEditing(false)}>
Save
</button>
</>
);
} else {
todoContent = (
<>
{todo.title}
<button onClick={() => setIsEditing(true)}>
Edit
</button>
</>
);
}
return (
<label>
<input
type="checkbox"
checked={todo.done}
onChange={e => {
onChange({
...todo,
done: e.target.checked
});
}}
/>
{todoContent}
<button onClick={() => onDelete(todo.id)}>
Delete
</button>
</label>
);
}
button { margin: 5px; }
li { list-style-type: none; }
ul, li { margin: 0; padding: 0; }
변경 없이 배열과 객체를 업데이트하는 게 번거롭게 느껴진다면, Immer 같은 라이브러리를 사용해서 반복적인 코드를 줄일 수 있어요. Immer를 사용하면 마치 객체를 직접 변경하는 것처럼 간결한 코드를 작성할 수 있지만, 내부적으로는 불변 업데이트를 수행해요:
import { useState } from 'react';
import { useImmer } from 'use-immer';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [list, updateList] = useImmer(initialList);
function handleToggle(artworkId, nextSeen) {
updateList(draft => {
const artwork = draft.find(a =>
a.id === artworkId
);
artwork.seen = nextSeen;
});
}
return (
<>
<h1>Art Bucket List</h1>
<h2>My list of art to see:</h2>
<ItemList
artworks={list}
onToggle={handleToggle} />
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
{artworks.map(artwork => (
<li key={artwork.id}>
<label>
<input
type="checkbox"
checked={artwork.seen}
onChange={e => {
onToggle(
artwork.id,
e.target.checked
);
}}
/>
{artwork.title}
</label>
</li>
))}
</ul>
);
}
{
"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"
}
}
React는 초기 state를 한 번 저장하고 다음 렌더링에서는 무시해요.
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos());
// ...
createInitialTodos()의 결과는 초기 렌더링에서만 사용되지만, 여전히 매 렌더링마다 이 함수를 호출하고 있어요. 큰 배열을 생성하거나 비싼 계산을 수행하는 경우 낭비가 될 수 있어요.
이걸 해결하려면, useState에 초기화(initializer) 함수로 전달할 수 있어요:
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos);
// ...
createInitialTodos()가 아니라, 함수 자체인 createInitialTodos를 전달하고 있다는 걸 주목하세요. 함수를 useState에 전달하면, React는 초기화 중에만 호출할 거예요.
React는 개발 환경에서 초기화 함수가 순수한지 확인하기 위해 초기화 함수를 두 번 호출할 수 있어요.
이 예제는 초기화 함수를 전달하므로, createInitialTodos 함수는 초기화 중에만 실행돼요. input에 타이핑하는 것처럼 컴포넌트가 리렌더링될 때는 실행되지 않아요.
import { useState } from 'react';
function createInitialTodos() {
const initialTodos = [];
for (let i = 0; i < 50; i++) {
initialTodos.push({
id: i,
text: 'Item ' + (i + 1)
});
}
return initialTodos;
}
export default function TodoList() {
const [todos, setTodos] = useState(createInitialTodos);
const [text, setText] = useState('');
return (
<>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => {
setText('');
setTodos([{
id: todos.length,
text: text
}, ...todos]);
}}>Add</button>
<ul>
{todos.map(item => (
<li key={item.id}>
{item.text}
</li>
))}
</ul>
</>
);
}
이 예제는 초기화 함수를 전달하지 않으므로, input에 타이핑하는 것처럼 매 렌더링마다 createInitialTodos 함수가 실행돼요. 동작에서 관찰 가능한 차이는 없지만, 이 코드는 덜 효율적이에요.
import { useState } from 'react';
function createInitialTodos() {
const initialTodos = [];
for (let i = 0; i < 50; i++) {
initialTodos.push({
id: i,
text: 'Item ' + (i + 1)
});
}
return initialTodos;
}
export default function TodoList() {
const [todos, setTodos] = useState(createInitialTodos());
const [text, setText] = useState('');
return (
<>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => {
setText('');
setTodos([{
id: todos.length,
text: text
}, ...todos]);
}}>Add</button>
<ul>
{todos.map(item => (
<li key={item.id}>
{item.text}
</li>
))}
</ul>
</>
);
}
리스트를 렌더링할 때 key 속성을 자주 접하게 될 거예요. 하지만 다른 용도도 있어요.
컴포넌트에 다른 key를 전달해서 컴포넌트의 state를 리셋할 수 있어요. 이 예제에서 Reset 버튼은 version state 변수를 변경하고, 이걸 Form에 key로 전달해요. key가 변경되면, React는 Form 컴포넌트(와 그 모든 자식들)를 처음부터 다시 생성하므로, state가 리셋돼요.
더 자세한 내용은 state 보존 및 리셋하기를 읽어보세요.
// src/App.js
import { useState } from 'react';
export default function App() {
const [version, setVersion] = useState(0);
function handleReset() {
setVersion(version + 1);
}
return (
<>
<button onClick={handleReset}>Reset</button>
<Form key={version} />
</>
);
}
function Form() {
const [name, setName] = useState('Taylor');
return (
<>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<p>Hello, {name}.</p>
</>
);
}
button { display: block; margin-bottom: 20px; }
보통은 이벤트 핸들러에서 state를 업데이트할 거예요. 하지만 드문 경우에 렌더링에 대한 응답으로 state를 조정하고 싶을 수 있어요 -- 예를 들어, prop이 변경될 때 state 변수를 변경하고 싶을 수 있어요.
대부분의 경우에는 이게 필요 없어요:
useMemo Hook이 도움이 될 수 있어요.key를 전달하세요.이런 것들이 하나도 적용되지 않는 드문 경우에, 컴포넌트가 렌더링되는 동안 set 함수를 호출해서 지금까지 렌더링된 값을 기반으로 state를 업데이트하는 패턴을 사용할 수 있어요.
여기 예제가 있어요. 이 CountLabel 컴포넌트는 전달된 count prop을 표시해요:
// src/CountLabel.js
export default function CountLabel({ count }) {
return <h1>{count}</h1>
}
마지막 변경 이후 카운터가 증가했는지 감소했는지를 보여주고 싶다고 해봐요. count prop은 이걸 알려주지 않아요 -- 이전 값을 추적해야 해요. 이걸 추적하기 위해 prevCount state 변수를 추가하세요. 카운트가 증가했는지 감소했는지를 나타내는 trend라는 또 다른 state 변수를 추가하세요. prevCount와 count를 비교하고, 같지 않다면 prevCount와 trend를 모두 업데이트하세요. 이제 현재 count prop과 마지막 렌더링 이후 어떻게 변경되었는지를 모두 보여줄 수 있어요.
// src/App.js
import { useState } from 'react';
import CountLabel from './CountLabel.js';
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(count - 1)}>
Decrement
</button>
<CountLabel count={count} />
</>
);
}
// src/CountLabel.js (active)
import { useState } from 'react';
export default function CountLabel({ count }) {
const [prevCount, setPrevCount] = useState(count);
const [trend, setTrend] = useState(null);
if (prevCount !== count) {
setPrevCount(count);
setTrend(count > prevCount ? 'increasing' : 'decreasing');
}
return (
<>
<h1>{count}</h1>
{trend && <p>The count is {trend}</p>}
</>
);
}
button { margin-bottom: 10px; }
렌더링 중에 set 함수를 호출한다면, prevCount !== count 같은 조건문 안에 있어야 하고, 조건문 안에 setPrevCount(count) 같은 호출이 있어야 한다는 걸 주목하세요. 그렇지 않으면, 컴포넌트가 크래시할 때까지 루프에서 리렌더링될 거예요. 또한, 이렇게 현재 렌더링 중인 컴포넌트의 state만 업데이트할 수 있어요. 렌더링 중에 다른 컴포넌트의 set 함수를 호출하는 건 에러예요. 마지막으로, set 호출은 여전히 변경 없이 state를 업데이트해야 해요 -- 이게 순수 함수의 다른 규칙을 어겨도 된다는 의미는 아니에요.
이 패턴은 이해하기 어려울 수 있고 보통 피하는 게 좋아요. 하지만 effect에서 state를 업데이트하는 것보다는 나아요. 렌더링 중에 set 함수를 호출하면, React는 컴포넌트가 return 문으로 종료된 직후, 자식들을 렌더링하기 전에 즉시 해당 컴포넌트를 리렌더링할 거예요. 이렇게 하면 자식들이 두 번 렌더링될 필요가 없어요. 컴포넌트 함수의 나머지 부분은 여전히 실행될 거고 (결과는 버려질 거예요). 조건이 모든 Hook 호출 아래에 있다면, 일찍 return;을 추가해서 렌더링을 더 일찍 다시 시작할 수 있어요.
set 함수를 호출해도 실행 중인 코드에서는 state가 변경되지 않아요:
function handleClick() {
console.log(count); // 0
setCount(count + 1); // 1로 리렌더링 요청
console.log(count); // 여전히 0!
setTimeout(() => {
console.log(count); // 이것도 0!
}, 5000);
}
이건 state가 스냅샷처럼 동작하기 때문이에요. state를 업데이트하면 새 state 값으로 다른 렌더링을 요청하지만, 이미 실행 중인 이벤트 핸들러의 count JavaScript 변수에는 영향을 주지 않아요.
다음 state를 사용해야 한다면, set 함수에 전달하기 전에 변수에 저장할 수 있어요:
const nextCount = count + 1;
setCount(nextCount);
console.log(count); // 0
console.log(nextCount); // 1
React는 Object.is 비교로 판단했을 때 다음 state가 이전 state와 같으면 업데이트를 무시해요. 이건 보통 state의 객체나 배열을 직접 변경할 때 발생해요:
obj.x = 10; // 🚩 잘못됨: 기존 객체를 변경함
setObj(obj); // 🚩 아무것도 하지 않음
기존 obj 객체를 변경하고 다시 setObj에 전달했기 때문에, React가 업데이트를 무시한 거예요. 이걸 고치려면, 항상 객체와 배열을 변경하는 대신 교체해야 해요:
// ✅ 올바름: 새 객체 생성
setObj({
...obj,
x: 10
});
Too many re-renders. React limits the number of renders to prevent an infinite loop.라는 에러를 볼 수 있어요. 일반적으로 이건 렌더링 중에 무조건적으로 state를 설정하고 있어서, 컴포넌트가 루프에 빠지는 걸 의미해요: 렌더링, state 설정 (렌더링을 유발), 렌더링, state 설정 (렌더링을 유발), 이런 식으로요. 매우 흔하게, 이벤트 핸들러를 지정하는 데 실수가 있어서 발생해요:
// 🚩 잘못됨: 렌더링 중에 핸들러를 호출함
return <button onClick={handleClick()}>Click me</button>
// ✅ 올바름: 이벤트 핸들러를 전달함
return <button onClick={handleClick}>Click me</button>
// ✅ 올바름: 인라인 함수를 전달함
return <button onClick={(e) => handleClick(e)}>Click me</button>
이 에러의 원인을 찾을 수 없다면, 콘솔에서 에러 옆의 화살표를 클릭해서 JavaScript 스택을 살펴보면 에러를 유발하는 특정 set 함수 호출을 찾을 수 있어요.
Strict Mode에서는 React가 함수들 중 일부를 한 번 대신 두 번 호출해요:
function TodoList() {
// 이 컴포넌트 함수는 매 렌더링마다 두 번 실행될 거예요.
const [todos, setTodos] = useState(() => {
// 이 초기화 함수는 초기화 중에 두 번 실행될 거예요.
return createTodos();
});
function handleClick() {
setTodos(prevTodos => {
// 이 업데이터 함수는 매 클릭마다 두 번 실행될 거예요.
return [...prevTodos, createTodo()];
});
}
// ...
이건 예상된 거고 코드를 깨뜨리면 안 돼요.
이건 개발 환경에서만 일어나는 동작이고, 컴포넌트를 순수하게 유지하는 데 도움을 줘요. React는 두 번의 호출 중 하나의 결과를 사용하고, 다른 호출의 결과는 무시해요. 컴포넌트, 초기화 함수, 업데이터 함수가 순수하다면 이게 로직에 영향을 주지 않아야 해요. 하지만 실수로 불순한 경우에는 실수를 알아차리는 데 도움이 돼요.
예를 들어, 이 불순한 업데이터 함수는 state의 배열을 직접 변경해요:
setTodos(prevTodos => {
// 🚩 실수: state를 변경함
prevTodos.push(createTodo());
});
React가 업데이터 함수를 두 번 호출하기 때문에, todo가 두 번 추가된 걸 볼 수 있고, 실수가 있다는 걸 알 수 있어요. 이 예제에서는 배열을 변경하는 대신 교체해서 실수를 고칠 수 있어요:
setTodos(prevTodos => {
// ✅ 올바름: 새 state로 교체
return [...prevTodos, createTodo()];
});
이제 이 업데이터 함수가 순수하므로, 한 번 더 호출해도 동작에 차이가 없어요. 이것이 React가 두 번 호출해서 실수를 찾는 데 도움을 주는 이유예요. 오직 컴포넌트, 초기화 함수, 업데이터 함수만 순수해야 해요. 이벤트 핸들러는 순수할 필요가 없으므로, React는 이벤트 핸들러를 두 번 호출하지 않아요.
더 자세한 내용은 컴포넌트를 순수하게 유지하기를 읽어보세요.
함수를 이렇게 state에 넣을 수 없어요:
const [fn, setFn] = useState(someFunction);
function handleClick() {
setFn(someOtherFunction);
}
함수를 전달하고 있기 때문에, React는 someFunction이 초기화 함수이고, someOtherFunction이 업데이터 함수라고 가정하고, 호출해서 결과를 저장하려고 해요. 실제로 함수를 저장하려면, 두 경우 모두 앞에 () =>을 붙여야 해요. 그러면 React가 전달한 함수들을 저장할 거예요.
const [fn, setFn] = useState(() => someFunction);
function handleClick() {
setFn(() => someOtherFunction);
}