useState
는 컴포넌트에 state variable을 더할 수 있도록 도와주는 훅이다.
기본 형태
const [state, setState] = useState(initialState)
useState(initialState)
state의 네임 컨벤션은 [something, setSomething]
형태이다.
Parameters
initialState
: 초기값, 어떤 타입의 값이든 상관없지만 함수에 대해선 특수하게 동작한다.initializer function
으로 다루어진다.Returns
1. current state
: 첫 렌더링 후에는 설정해 둔 initialState가 들어감.
2. set function
: state를 업데이트 하는 함수. 리렌더링을 발생시킴.
주의사항
set functions
set 함수는 state를 업데이트하고 리렌더링을 발생시킨다.
이때 set 함수로
setName('Taylor');
setAge(a => a + 1);
Parameters
어떤 값이든 상관없지만 함수는 특수하게 동작한다.
함수를 전달하면 updater function으로 처리된다.
이때 함수는 순수 함수여야 하고, 대기 중인 state를 유일한 인자로 가져야 하고, 다음 state를 반환해야한다.
Returns
값을 반환하지 않는다.
주의사항
function handleClick() {
setName('Robin');
console.log(name); // Still "Taylor"!
}
flushSync
를 사용할 수 있음.기본적인 예시 ) counter, input field, checkbox, form
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>
</>
);
}
아래의 경우 age는 45가 되는 대신 43이 된다.
(set function이 state를 바로 업데이트하지 않기 때문.)
function handleClick() {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
}
이럴 때 updater function을 넘기면 의도대로 만들 수 있다.
function handleClick() {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}
updater function은 pending state를 받아서 next state를 계산한다.
React는 updater function을 queue에 넣고 다음 렌더링 전에 계산한다.
1. a => a + 1 will receive 42 as the pending state and return 43 as the next state.
2. a => a + 1 will receive 43 as the pending state and return 44 as the next state.
3. a => a + 1 will receive 44 as the pending state and return 45 as the next state.
더 이상 queue에 업데이트할 사항이 없으므로 45를 current state로 저장하게 됨.
pending state argument는 state의 첫 글자로 짓는게 컨벤션이다.
ex) age -> a
혹은 prevAge와 같이 짓는 것도 괜찮다.
React는 updater 함수가 순수 함수인지 검증하기 위해 개발 모드에서 updater 함수를 두 번 호출할 수 있다.
updater 함수를 쓰는게 항상 좋은가?
이전 값을 바탕으로 계산해야 할 때 항상 updater function을 쓰는 것은 불필요하다.
하지만 같은 이벤트 내에서 업데이트를 여러번 해야 할 때는 updater function이 유용할 수 있다. state 변수 자체에 접근하는 것이 어려운 상황에도 유용하다.(ex. 리렌더링 최적화할 때)
만약 state가 다른 state의 이전 값으로부터 계산된다면 하나의 객체로 결합해서 reducer를 사용하는 것이 좋다.
React에서는 state가 읽기 전용으로 간주되기 때문에 기존 객체를 변경(mutate)하기 보다는 교체(replace)해야 한다.
// x
form.firstName = 'Taylor';
// o
setForm({
...form,
firstName: 'Taylor'
});
https://react.dev/learn/updating-objects-in-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}
/>
</>
);
}
배열 업데이트 하는 예제
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}
/>
</>
);
}
배열과 객체를 변경할 때 mutation을 피하는 것이 번거롭다면 Immer과 같은 라이브러리를 사용해 반복적인 코드를 줄일 수 있다.
Immer를 사용하면 마치 객체를 mutation하는 것처럼 간결한 코드를 사용하면서 내부적으로는 불변성을 유지할 수 있다.
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>
);
}
React는 초기값을 한 번만 저장하고 이후 렌더링에서는 무시한다.
아래에서 createInitialTodos()
의 결과값은 초기 렌더링에서만 쓰이지만 매번 렌더링할 때마다 해당 함수를 실행하게 된다.
만약 해당 함수가 큰 배열을 만들거나 계산을 많이 할 경우 낭비일 수 있다.
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos());
// ...
이를 해결하기 위해서는 함수 자체를 넘기면 된다.
이렇게 되면 React는 초기화할 때만 해당 함수를 실행한다. (개발중에서는 순수 함수인지 확인하기 위해 두 번 실행할 수도 있음.)
https://react.dev/learn/keeping-components-pure
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos);
// ...
리스트를 렌더링할 때 자주 사용하는 key를 다른 용도로 쓸 수 있다.
컴포넌트에 다른 key 값을 전달하면 컴포넌트 상태를 초기화할 수 있다.
아래에서 Reset 버튼을 누르면 version이 변경되고(1,2,3,,) 변경된 값을 Form에 key로 전달하여 Form을 리셋할 수 있다.
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>
</>
);
}
보통 state는 이벤트 핸들러에서 업데이트한다. 그러나 간혹 렌더링에 따라 state를 조정하고 싶을 수 있다. (ex. props가 변경될 때 state를 변경)
대부분의 경우는 필요하지 않다.
props로 받은 값이 증가되었는지 감소되었는지 알고 싶은 경우 이전 값을 저장하는 state를 만들어 추적할 수 있다.
렌더링 중에 set 함수를 호출하면, 컴포넌트가 무한 루프에 빠지거나 예기치 않은 동작을 일으킬 수 있다. 이를 방지하기 위해서는 조건문을 사용해서 안전하게 state를 업데이트 해야 한다.
아래 패턴은 가능하면 피하는 것이 좋다. 이해하기 어렵고 예기치 않은 문제를 일으킬 수 있기 때문이다.
일반적으로는 이벤트 핸들러나 useEffect 안에서 이루어진다. (useEffect는 렌더링 후 실행되므로 무한 루프에 빠지지 않을 수 있다.)
렌더링 중에 상태를 업데이트하는 것은 가능하면 피하는 것이 좋다.
import { useState } from 'react';
export default function CountLabel({ count }) {
const [prevCount, setPrevCount] = useState(count);
const [trend, setTrend] = useState(null);
// count가 변경될 때만 setPrevCount를 호출함
if (prevCount !== count) {
setPrevCount(count);
setTrend(count > prevCount ? 'increasing' : 'decreasing');
}
return (
<>
<h1>{count}</h1>
{trend && <p>The count is {trend}</p>}
</>
);
}
-> set function가 실행됐을 때 state가 바로 변경되는게 아니다.
-> state가 snapshot처럼 동작하기 때문. (state는 특정 시점의 스냅샷이기 때문에 state를 업데이트해도 실행 중인 코드에는 변화가 즉시 반영되지 않음.)
-> state는 비동기적으로 업데이트 된다. state 변경 요청을 React에 보내고 그 요청은 다음 렌더링에서 처리된다.
function handleClick() {
console.log(count); // 0
setCount(count + 1); // Request a re-render with 1
console.log(count); // Still 0!
setTimeout(() => {
console.log(count); // Also 0!
}, 5000);
}
따라서 업데이트 된 값을 쓰고 싶다면 값을 set function에 전달하기 전에 변수에 저장해두는게 좋다.
const nextCount = count + 1;
setCount(nextCount);
console.log(count); // 0
console.log(nextCount); // 1
React는 이전 state와 다음 state가 같은 경우 업데이트를 무시한다. (Object.is
로 비교하여 결정됨)
주로 객체나 배열을 변경할 때 위와 같은 문제가 발생한다.
obj.x = 10; // 🚩 Wrong: mutating existing object
setObj(obj); // 🚩 Doesn't do anything
이를 해결하기 위해서는 객체와 배열을 mutate하지 말고 replace해야 한다.
// ✅ Correct: creating a new object
setObj({
...obj,
x: 10
});
해당 에러는 렌더링 중에 state를 변경하려고 했을 때 발생한다.
(렌더링 -> state 변경 -> 렌더링 -> state 변경...의 무한 루프에 빠지게 됨)
주로 이벤트 핸들러를 잘못 지정했을 때 발생한다.
// 🚩 Wrong: calls the handler during render
return <button onClick={handleClick()}>Click me</button>
// ✅ Correct: passes down the event handler
return <button onClick={handleClick}>Click me</button>
// ✅ Correct: passes down an inline function
return <button onClick={(e) => handleClick(e)}>Click me</button>
Strict Mode에서는 React가 state 업데이트 함수를 중복 실행한다. (개발중에만 실행되는 동작)
React가 일부러 두 번 호출해서 컴포넌트를 순수하게 유지할 수 있도록 돕는 것.
initializer 함수나 updater 함수가 순수하다면 로직에 영향이 없어야 한다.
function TodoList() {
// This component function will run twice for every render.
const [todos, setTodos] = useState(() => {
// This initializer function will run twice during initialization.
return createTodos();
});
function handleClick() {
setTodos(prevTodos => {
// This updater function will run twice for every click.
return [...prevTodos, createTodo()];
});
}
// ...
아래와 같이 순수하지 않은 함수를 사용한 경우 실수를 미리 잡아낼 수 있다.
(React가 updater 함수를 두 번 실행하기 때문에 todo가 두 번 더해진 것을 보고 실수를 알아챌 수 있다.)
setTodos(prevTodos => {
// 🚩 Mistake: mutating state
prevTodos.push(createTodo());
});
아래처럼 mutate를 replace로 바꾸어서 문제를 해결할 수 있다.
(updater 함수를 두 번 호출해도 동작에 영향 없음. -> todo가 정상적으로 한 번만 추가됨.)
setTodos(prevTodos => {
// ✅ Correct: replacing with new state
return [...prevTodos, createTodo()];
});
component, initializer, updater function만 순수 함수면 된다.
(이벤트 핸들러는 순수할 필요가 없으므로 React가 두 번 호출하지 않음)
함수를 전달하면 React는 해당 함수를 initializer 함수(아래에서는 someFunction)로 추정하거나 updater 함수(아래에서는 someOtherFunction)로 추정한다.
const [fn, setFn] = useState(someFunction);
function handleClick() {
setFn(someOtherFunction);
}
따라서 함수 자체가 전달되지 않고, 함수를 실행해서 값을 저장하려고 한다.
(함수 자체를 저장하려면 () => someFunction
형태로 넘겨야 함.)
const [fn, setFn] = useState(() => someFunction);
function handleClick() {
setFn(() => someOtherFunction);
}
몇몇 자바스크립트 함수는 순수하다.
순수한 함수는 calculation만 수행하고 이외의 다른 것은 하지 않는다.
컴포넌트에 순수 함수만 작성함으로써 예상치 못한 동작이나 버그를 피할 수 있다.
순수 함수
란 무엇인가?
순수 함수는
수학 공식
과 유사하다.
y = 2x
-> x에 2를 넣으면 항상 y는 4가 된다.
-> x에 3를 넣으면 항상 y는 6이 된다.
이를 javascript 함수로 만들면 아래와 같을 것이다.
이와 같은 함수를 순수 함수
라고 한다.
function double(number) {
return 2 * number;
}
React는 이 개념을 중심으로 설계되었다.
React는 모든 컴포넌트가 순수 함수라고 가정한다. 즉, React component는 같은 입력값이 주어졌을 때 항상 동일한 JSX를 반환해야 한다.
재료가 바뀌지 않는 한 항상 같은 결과를 얻게 된다는 점에서 컴포넌트를 레시피
라고 생각할 수도 있다. 이때 얻게 되는 dish를 JSX라고 생각할 수 있다.
React의 렌더링 과정은 항상 순수해야 한다.
컴포넌트는 렌더링 이전에 존재하던 어떤 값도 변경해선 안 된다.
아래 예제 코드는 컴포넌트 외부에서 선언된 변수를 읽고 쓰고 있기때문에 순수하지 않다.
-> 이 컴포넌트를 실행할 때마다 다른 JSX를 생성하게 된다. (예측 가능성이 떨어진다.)
let guest = 0;
function Cup() {
// Bad: changing a preexisting variable!
guest = guest + 1;
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaSet() {
return (
<>
<Cup /> // 2
<Cup /> // 4
<Cup /> // 6
</>
);
}
guest 값을 prop으로 넘겨주어 해결할 수 있다.
function Cup({ guest }) {
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaSet() {
return (
<>
<Cup guest={1} /> // 1
<Cup guest={2} /> // 2
<Cup guest={3} /> // 3
</>
);
}
일반적으로 컴포넌트가 특정 순서대로 렌더링되리라고 기대해선 안 된다.
각 컴포넌트는 독립적으로 실행되고, 그래야 하기 때문이다.
Detecting impure calculations with StrictMode
Strict Mode를 사용하면 개발 중에 각 컴포넌트의 함수를 두 번 호출하여 impure한 컴포넌트를 찾아내는데 도움을 얻을 수 있다.
- 위의 impure한 함수 예제에서 2,4,6이 출력된 이유는 해당 함수가 impure하기 때문이다. (strict mode에 의해 두 번 실행되었을 때 오류가 발생함. pure하다면 실행 횟수와 상관없이 같은 값을 출력해야 함. 따라서 수정된 pure function은 두 번 호출해도 제대로 동작함.)
- same input, same output 원칙을 지키는지 확인하는 것.
- Strict Mode는 프로덕션 환경에서는 아무 영향을 미치지 않으므로 앱 속도에 영향을 미치지 않는다. Strict Mode를 사용하려면
<React.StrictMode>
로 루트 컴포넌트를 감싸면 된다. 일부 프레임워크는 기본적으로 이를 활성화한다.
위처럼 컴포넌트가 렌더링 중에 기존 변수를 변경하는 것을 mutation
이라고 부른다.
순수 함수는 함수 범위를 벗어나거나 호출 이전에 생성된 객체나 변수를 변경하지 않는다.
하지만 아래와 같이 렌더링 중에 생성한 변수나 객체를 변경하는 것은 문제가 되지 않는다.
function Cup({ guest }) {
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaGathering() {
let cups = [];
for (let i = 1; i <= 12; i++) {
cups.push(<Cup key={i} guest={i} />);
}
return cups;
}
만약 cups가 TeaGathering 외부에서 선언되었다면 큰 문제가 되겠지만 TeaGathering를 내부에서 생성한 것은 문제가 되지 않는다.
TeaGathering 외부의 코드는 해당 사실을 알지 못하기 때문이다.
이를 local mutation
이라 부른다.
함수형 프로그래밍은 순수성을 매우 중요시하지만 변경을 해야만 하는 시점도 있다.
이러한 변경들(화면 업데이트, 애니메이션 시작, 데이터 변경 등)을 side effects
라고 부른다.
useEffect
를 사용해 side effects를 담을 수 있다. 이러면 React가 렌더링을 한 후에 side effects가 허용되는 시점에 코드가 실행된다. (하지만 최후의 수단으로 쓰는 것이 좋음. 가능하다면 렌더링만으로 논리를 표현하려고 노력할 것)Why does React care about purity?
순수 함수를 작성하는 것의 이점은 다음과 같다.
1. 컴포넌트를 다른 환경에서 실행할 수 있다.
2. 성능을 개선할 수 있다.
입력이 변경되지 않는 컴포넌트는 렌더링을 건너뛸 수 있다. 순수 함수는 항상 동일한 결고를 반환하므로 캐시할 수 있다는 점에서 안전함.
3. 깊은 계층의 컴포넌트 트리를 렌더링하는 중에 데이터 변경이 발생한다면 React는 렌더링을 완료하지 않고 렌더링을 재시작할 수 있다. purity를 지키면 언제든 계산을 멈춰도 안전하도록 해줌.
React의 모든 새로운 기능은 purity의 이점을 활용한다. 데이터 fetching부터 성능 향상까지 컴포넌트를 pure하게 유지하는 것이 React 패러다임의 힘을 발휘하게 해준다.
export default function Clock({ time }) {
let hours = time.getHours();
if (hours >= 0 && hours <= 6) {
document.getElementById('time').className = 'night';
} else {
document.getElementById('time').className = 'day';
}
return (
<h1 id="time">
{time.toLocaleTimeString()}
</h1>
);
}
Hint : Rendering is a calculation, it shouldn’t try to “do” things.
내가 고친 답
export default function Clock({ time }) {
let hours = time.getHours();
const type = hours >= 0 && hours <= 6 ? 'night' : 'day'
return (
<h1 className={type}>
{time.toLocaleTimeString()}
</h1>
);
}
정답
className을 계산하고 render output에 포함하여 해결할 수 있다.
이 경우 DOM을 수정하는 side effect는 불필요하다.
export default function Clock({ time }) {
let hours = time.getHours();
let className;
if (hours >= 0 && hours <= 6) {
className = 'night';
} else {
className = 'day';
}
return (
<h1 className={className}>
{time.toLocaleTimeString()}
</h1>
);
}
두 개의 서로 다른 Profile 컴포넌트를 렌더링 했을 때, collapse 버튼을 클릭하고 다시 expand하면 프로필이 같은 인물로 바뀌는 버그가 발생함. 버그 원인을 찾고 고쳐보자.
import Panel from './Panel.js';
import { getImageUrl } from './utils.js';
let currentPerson;
export default function Profile({ person }) {
currentPerson = person;
return (
<Panel>
<Header />
<Avatar />
</Panel>
)
}
function Header() {
return <h1>{currentPerson.name}</h1>;
}
function Avatar() {
return (
<img
className="avatar"
src={getImageUrl(currentPerson)}
alt={currentPerson.name}
width={50}
height={50}
/>
);
}
내가 고친 답
import Panel from './Panel.js';
import { getImageUrl } from './utils.js';
export default function Profile({ person }) {
return (
<Panel>
<Header name={person.name}/>
<Avatar currentPerson={person}/>
</Panel>
)
}
function Header({name}) {
return <h1>{name}</h1>;
}
function Avatar({currentPerson}) {
return (
<img
className="avatar"
src={getImageUrl(currentPerson)}
alt={currentPerson.name}
width={50}
height={50}
/>
);
}
혹은 추상화 단계를 낮춘다면 아래처럼도 가능할 것 같다.
import Panel from './Panel.js';
import { getImageUrl } from './utils.js';
export default function Profile({ person }) {
const name = person.name;
return (
<Panel>
<h1>{name}</h1>
<img
className="avatar"
src={getImageUrl(person)}
alt={name}
width={50}
height={50}
/>
</Panel>
)
}
정답
currentPerson이라는 컴포넌트 외부 값을 쓰는 것이 문제.
currentPerson 변수를 없애고 profile과 header에 props로 값을 전달.
React는 컴포넌트 함수의 실행 순서를 보장하지 않으므로 컴포넌트 간에 변수를 사용해 상태를 공유할 수 없음.
컴포넌트 간의 상태나 데이터 전달은 반드시 props를 통해서 이루어져야 함.
import Panel from './Panel.js';
import { getImageUrl } from './utils.js';
export default function Profile({ person }) {
return (
<Panel>
<Header person={person} />
<Avatar person={person} />
</Panel>
)
}
function Header({ person }) {
return <h1>{person.name}</h1>;
}
function Avatar({ person }) {
return (
<img
className="avatar"
src={getImageUrl(person)}
alt={person.name}
width={50}
height={50}
/>
);
}
Create Story가 여러 번 출력되는 버그 해결하기
export default function StoryTray({ stories }) {
stories.push({
id: 'create',
label: 'Create Story'
});
return (
<ul>
{stories.map(story => (
<li key={story.id}>
{story.label}
</li>
))}
</ul>
);
}
내가 고친 답
export default function StoryTray({ stories }) {
const createStory = { id: 'create', label: 'Create Story' };
const newStories = [...stories, createStory];
return (
<ul>
{newStories.map(story => (
<li key={story.id}>
{story.label}
</li>
))}
</ul>
);
}
정답
위에 시계가 업데이트될 때마다 create story가 두 번씩 추가됨.
-> 렌더링 될 때 mutation이 발생한다는 힌트가 됨.
StoryTray가 렌더링되기 이전에 만들어진 값을 변경하는 것이 문제의 원인임.
가장 간단한 해결책은 array를 아예 건들지 않고 “Create Story”를 따로 렌더링하는 것.
export default function StoryTray({ stories }) {
return (
<ul>
{stories.map(story => (
<li key={story.id}>
{story.label}
</li>
))}
<li>Create Story</li>
</ul>
);
}
자료
- React useState 공식 문서 : https://react.dev/reference/react/useState
- 컴포넌트를 순수하게 유지한다는 것 : https://react.dev/learn/keeping-components-pure