React를 함수형 컴포넌트로 작성할 때 가장 많이 쓰는 Hook이라고 한다면 아마 useState와 useEffect 이 두개가 아닐까 싶다. 이 중에서 useState는 이름 그대로, '상태'를 관리하는 Hook인데, 물론 웹 프로그래밍 특성상 독립된 컴포넌트간의 상태를 공유하는 경우가 굉장히 많기 때문에 이런 복잡한 상태관리를 편하게 하기 위해 Redux, MobX 혹은 Context API등 여러 방법들을 사용하기도 한다. 하지만 하나의 컴포넌트에서만 사용한다던지등 작은 규모의 상태 관리는 React에서 제공하는 useState를 자주 사용하고 있을 것이다.
그러나 한번도 이 useState를 자세히 살펴본적 없이, 그저 막연히 함수형 컴포넌트에서 실시간으로 상태관리를 해주며 리랜더링을 해주는 Hook 정도로만 알고 사용하였다. 그러다보니 다음과 같은 문제를 굉장히 빈번하게 마주친다.
const [value, setValue] = useState(0)
useEffect(() => {
setValue(value + 1);
console.log(value) // value의 증가가 이루어지지 않은 상태로 함수를 실행, 결과값 0
},[])
나는 분명히 value의 값을 증가 시킨후 어떤 함수의 패러미터로 value를 전달하였는데, value는 변화하기 전의 값으로 전달된다. 처음에 이러한 문제를 마주했을땐 '아, useState가 비동기적으로 동작하나? 그래서 변화시킨 값이 바로 안들어가나?' 따위의 생각을 하였다. 그리고는 구글에 다음과 같은것 따위를 검색한다.
'useState 동기적으로 사용하는법...'
리렌더링 (ReRendering)
컴포넌트는 한번 렌더링하고 나면 변하지 않는다. 내부의 값을 변화시키기 위해서는 반드시 이 컴포넌트를 다시 그리는, 리렌더링을 거쳐야한다. 특정 상태가 변하고 이 변한 상태를 나타내기 위해서는 컴포넌트 전체를 다시 그려야만 한다. 당장 개발자 도구를 키고 이런 저런 작업들을 해보면 수많은 컴포넌트들이 다시 그려지면서 번쩍이고 있는것을 볼 수 있다. 앞서 말했듯 useState 역시 상태를 변화시키고, 컴포넌트가 전부 리렌더링되어 마치 특정 값만 변한것처럼 보일 뿐이다.
Hook을 사용하는 '함수형' 컴포넌트는 이름 그대로 함수의 성질을 가진다. 즉 컴포넌트를 렌더링 하는 함수 자체를 다시 실행해야만 한다.
다시 위의 코드를 보면, 위의 코드는 useEffect 함수가 종료되기 전에는 리랜더링을 진행하지 못한다. JS는 싱글스레드로 돌아가기 때문에 리랜더링을 하는 함수의 실행이 막혀있기 때문이다. 때문에 변화한 상태가 아닌 이전의 값으로 동작하는것이다.
useState의 동작원리
useState의 동작원리는 대략 다음과 같다. (실제 React 코드가 아니라, 개념을 재현한 코드)
const MyReact = (function() {
let _val // hold our state in module scope
return {
render(Component) {
const Comp = Component()
Comp.render()
return Comp
},
useState(initialValue) {
_val = _val || initialValue // assign anew every run
function setState(newVal) {
_val = newVal
}
return [_val, setState]
}
}
})()
위 코드를 보면 useState는 JS의 클로저 개념을 통해 구현되고 있음을 알 수 있다. setState는 실제로 state의 값을 증감시키는것이 아니라, 내부적으로 _val 변수를 아예 새로 할당하고 반환해주는 개념이다. 밖에서는 render()를 통해 컴포넌트 함수를 리렌더링하고있다.
이번에는 다음과 같은 코드를 살펴보자.
const [value, setValue] = useState(0)
useEffect(() => {
setValue(value + 1);
setValue(value + 1);
setValue(value + 1);
},[])
이 코드는 아까와 마찬가지로 useEffect 콜백이 전부 끝나기 전까지는 상태가 변화하지 않는다. 그렇다면 useEffect가 끝나면 value값은 3이 되어 있을까?
Batch Process
React는 효율적인 퍼포먼스를 위해 Batch Process라는 개념을 사용한다. 이 개념은 간단히 말해 하나의 이벤트 안에서 여러번에 거쳐 일어나는 상태의 업데이트는 한번에 묶어서, 마지막을 기준으로 딱 '한번만' 리랜더링 한다. 때문에 위의 코드에서 결과적으로 value의 값은 3이 아니라 1이된다.
React에서는 위와 같은 상황에서 value의 값을 동기적으로 계속 업데이트 하고 싶으면, 다음과같이 이전의 상태값 대신 함수형으로 값을 넣으라고 한다. (그리고 이 코드가 바로 맨 처음 막혀서 검색한 useState 동기적으로 사용하는법..의 결과물이다.)
const [value, setValue] = useState(0)
useEffect(() => {
setValue(value => value + 1);
setValue(value => value + 1);
setValue(value => value + 1);
},[])
참고로 직접 사용해보진 않았지만, React v18부터는 다음과같이 flushSync 라는 기능을 제공하여, 업데이트를 강제할 수 있다.
function handleClick() {
flushSync(() => {
setCounter((c) => c + 1)
})
// React has updated the DOM by now
flushSync(() => {
setFlag((f) => !f)
})
// React has updated the DOM by now
}
Referenece:
[React] 클로저와 useState Hooks (2)
Overview 이번 포스팅은 리액트 함수형 컴포넌트에서 단순히 다음 두 함수의 실행결과가 다른 이유를 명확히 찾아내기 위해서 작성하게 되었습니다. App.js import React, { useState } from "react"; export defaul
yeoulcoding.me
https://www.netlify.com/blog/2019/03/11/deep-dive-how-do-react-hooks-really-work/
Deep dive: How do React hooks really work?
In this article, we reintroduce closures by building a tiny clone of React Hooks. This will serve two purposes - to demonstrate the effective use of closures in building our React Hooks clone itself, and also to implement a naive mental model of how Hooks
www.netlify.com
'WEB' 카테고리의 다른 글
| Next.js의 Middleware를 사용하여 A/B Testing하기 (with GA4) (0) | 2023.02.16 |
|---|---|
| [React] React-query와 전역상태관리 라이브러리로 관심사분리하기 (0) | 2023.01.03 |
| next-sitemap을 사용하여 동적 생성 페이지 sitemap 생성하기 (0) | 2022.12.19 |
| DOM(Document Object Model) (0) | 2022.06.27 |
| CSS em & rem (0) | 2022.06.26 |