일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 자바스크립트
- docker
- 백엔드
- 쿠버네티스
- 타입스크립트
- CSS
- 리액트
- JavaScript
- BFS
- next.js
- k8s
- es6
- react
- 웹
- 이슈
- 프론트엔드
- 이더리움
- 컴퓨터공학
- 알고리즘
- 솔리디티
- kubernetes
- 블록체인
- 파이썬
- 백준
- AWS
- 클라우드
- 가상화
- node.js
- HTML
- TypeScript
- Today
- Total
즐겁게, 코드
useState 똑똑하게 사용하기 본문
아마 리액트에서 가장 많이 사용되는 훅을 꼽아보라 하면 useState
가 주인공이 될 것 같은데요, 오늘은 useState
로 상태를 변경할 때 주의할 점에 대해 다뤄보려 합니다.
const [state, setState] = useState();
useState
훅은 상탯값과 상태를 변경할 수 있는 함수(※ action, setter 함수 등으로 부르는데, 여기서는 세터 함수라고 부르겠습니다.)를 제공하는데요, 세터 함수를 사용하면 원하는 값으로 상탯값을 변경할 수 있습니다.
import React, { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<div>{count}</div>
{/* 버튼을 클릭하면 세터 함수가 호출됩니다. */}
<button onClick={() => setCount(count - 1)}>+</button>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
};
export default Counter;
그런데 이 세터 함수에는 한 가지 함정이 숨어 있는데요, 바로 동일한 블럭에서 사용할 때는 렌더링 성능 향상을 위해 상태가 변경될 때마다 업데이트를 수행하는 대신 상태값의 변경사항을 모아뒀다가 한꺼번에 변경한다는 점입니다.
오늘은 이로 인해 생길 수 있는 몇 가지 상황과 해결 방법에 대해 다뤄보도록 하겠습니다. 😄
🔁 첫 번째 상황. 반복문 내에서 상태값 변경하기
import { useState } from "react";
function App() {
const [state, setState] = useState([]);
const setStateWithArray = () => {
[1, 2, 3, 4, 5].forEach((value) => {
setState([...state, value]);
});
};
console.log(state);
return (
<div className="App">
<div>
{state?.map((value) => (
<div>{value}</div>
))}
</div>
<button onClick={() => setStateWithArray()}>Run</button>
</div>
);
}
export default App;
코드가 조금 어려울 수 있는데, 여기서는 버튼을 눌렀을 때 호출되는 setStateWithArray
함수에만 집중해주시면 됩니다.
setStateWithArray
함수는 forEach를 따라 배열의 원소를 상탯값 배열에 추가하고 있는데, 과연 state에는 어떤 값이 담겼을까요?
forEach를 통해 [1, 2, 3, 4, 5]를 순회하면서 각 값을 상탯값 배열에 추가하는 듯 했지만, 실제로는 5만 배열에 포함되어 있었습니다.
원인은 바로 동일한 블럭 내에서는 세터 함수를 사용하더라도 상태값이 즉시 업데이트되지 않기 때문인데요, 따라서 반복문 내에서 상태값을 업데이트할때는 작업 결과를 임시로 저장해두고, 모든 작업이 끝난 후 저장해둔 임시 값을 상탯값으로 업데이트해야 합니다!
import { useState } from "react";
function App() {
const [state, setState] = useState([]);
const setStateWithArray = () => {
const tempArray = [];
[1, 2, 3, 4, 5].forEach((value) => {
tempArray.push(value);
});
setState(tempArray);
};
console.log(state);
return (
<div className="App">
<div>
{state?.map((value, idx) => (
<div key={idx}>{value}</div>
))}
</div>
<button onClick={() => setStateWithArray()}>Run</button>
</div>
);
}
export default App;
이렇게 반복문 내에서 업데이트를 수행할 때는 각 반복문에서 처리한 결과물을 임시로 저장한 뒤, 작업을 마친 다음 저장해둔 결과물을 세터 함수로 전달해주면 됩니다.
2️⃣ 두 번째 상황. 동일한 블럭에서 상태를 여러번 업데이트하기
import React, { useState } from "react";
function App() {
const [count, setCount] = useState(0);
async function fetchData() {
setCount(1);
await timer();
setCount(2);
}
const timer = async () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("");
}, 5000);
});
};
return (
<div className="App">
<div>{count}</div>
<button onClick={fetchData}>실행</button>
</div>
);
}
export default App;
버튼을 누르면 카운트가 1이 되고, 5초 후에 2가 됩니다.
그런데, 세터 함수에 들어가는 인자를 조금 바꾸면 이상한 일이 일어납니다.
import React, { useState } from "react";
function App() {
const [count, setCount] = useState(0);
async function fetchData() {
setCount(count + 1);
await timer();
setCount(count + 1);
}
const timer = async () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("");
}, 5000);
});
};
return (
<div className="App">
<div>{count}</div>
<button onClick={fetchData}>실행</button>
</div>
);
}
export default App;
5초가 변해도 카운터가 변하지 않는 모습인데요, 왜 첫 번째 setCount
는 동작하고 두 번째 setCount
는 동작하지 않은 걸까요?
사실 두 setCount 모두 동작했습니다.
fetchData
부분을 리액트가 인식하는대로 작성하면 다음과 같이 됩니다.
import React, { useState } from "react";
function App() {
const [count, setCount] = useState(0);
async function fetchData() {
setCount(0 + 1); // count의 초깃값은 0입니다.
await timer();
setCount(0 + 1); // count는 갱신된 상태값이 아닌 클로저를 통해 이전 맥락의 값을 계속 참조합니다.
}
const timer = async () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("");
}, 5000);
});
};
return (
<div className="App">
<div>{count}</div>
<button onClick={fetchData}>실행</button>
</div>
);
}
export default App;
이렇게 두 번째 setCount
는 동작하지 않은 것이 아니라, 클로저에 의해 이전 값을 계속 참조하고 있었기 때문이라는 사실을 알 수 있는데요!
따라서, 동일한 블럭 내에서 이전 상태값을 참조해 상태를 업데이트할때는 세터 함수를 다음과 같이 사용해야 합니다.
setState(state + 1)
setState(state + 1) // ❌, state는 이전 맥락의 값을 계속 참조함.
setState((state) => state + 1)
setState((state) => state + 1) // ✅, 업데이트된 state를 참조함.
세터 함수 내에서 함수를 정의하면 해당 함수의 인자에는 이전 상태가 전달되고 업데이트할 상태값을 반환합니다.
이러면 두 번째 세터 함수에서는 클로저의 영향으로 갱신되기 이전 상태를 참조하지 않고, 첫 번째 세터 함수에서 업데이트한 상태값을 제대로 참조할 수 있습니다! 😁
'🎨 프론트엔드 > React.js' 카테고리의 다른 글
리액트의 key 속성은 왜 필요할까? (0) | 2021.08.16 |
---|---|
redux-saga의 call에 관해 (0) | 2021.05.26 |
forwardRef로 함수 컴포넌트의 ref 전달하기 (0) | 2021.04.28 |
useEffect 내에서 async 함수를 사용할 때의 주의점 (2) | 2021.04.27 |
컴포넌트 내에서 라우팅 경로와 쿼리스트링 추출하기 (0) | 2021.04.12 |