아마 리액트를 배우다보면 가장먼저 마주하는, 리액트스러운 함수(Hook)일겁니다. 처음 배우시는 분들이라면, 왜 이런 번거로운게 있나 싶으실겁니다. 하지만 개인적으로 useState가 리액트의 작동방식에 밀접한 관련이 있다고 생각하고, 단순히 어떻게 쓰는지 뿐만 아니라, 어떻게 작동하는지도 꼭 알고 있어야 된다고 생각합니다. 지금부터 조금씩 적어나가 보겠습니다.
리액트는 local variable로 컴포넌트를 조작하지 않습니다. 이 useState가지고, 필요한 state를 mutate하고, 컴포넌트 상태를 업데이트 하게 되죠.
const [state, setState] = useState(initialState)
리액트 공식문서에는 useState를 이렇게 정의하고 있습니다.
state안에 원하는 variable을 저장하고, setState으로 업데이트를 해주는 형식입니다.
좀 더 구체적인 예를 들자면
const [age, setAge] = useState(20)
return (
<Button onClick={
()=>{
setAge(age+1);
}}/>
)
이런 형식으로 업데이트를 진행합니다.
근데 저는 처음 배울때 좀 의아한 부분이 몇개 있었는데요, 바로 이 훅 자체가 왜 있는지 몰랐습니다.
그냥 local variable을 만들어서 사용하면 되는거 아닌가 싶었던거죠.
그래서 조금 찾아보니, useState가 필요한 이유는 기존의 local variable이 리액트가 작동하는 방식과 충돌이 일어나기 때문입니다.
리액트의 작동방식을 설명하려면 조금 먼 길을 돌아와야하는데, 그건 따로 페이지를 만들어 다뤄보기로하고, 지금은 최대한 짧게 설명해보겠습니다.
일단 어떤 웹페이지에는 그 페이지를 구성하게 하는 DOM 이라는 object가 존재합니다. JS는 DOM에 직접 접근하여 필요한 부분을 수정하는 것이고요. 하지만, 이 방식은 DOM 자체를 처음부터 다시 만들게 합니다. 즉, 오래걸린다는 것이죠. 아주 작은 변화라도, 처음부터 끝까지 다시 만듭니다. 하지만, 리액트는 VDOM이라는것을 통해 변화가 있는 부분만(컴포넌트 단위로) 캐치해 바꾸어줍니다. 다 다시 랜더링 할 필요 없이요.
문제는 바꿔야할 부분에 해당되는 컴포넌트는 처음부터 랜더링된다는 겁니다.
function Component() {
let age = 0;
const mutate = () => {
age = age + 1;
console.log(`${getTime()}| count: ${age}`);
};
return (
<div>
<h1>{age}</h1>
<Button onClick={mutate}>Mutate</Button>
</div>
);
}
//Partially Retrieved from: https://theodorusclarence.com/blog/react-core-concept-rendering-state
위의 예시를 보겠습니다. 만약 위의 컴포넌트가 Call이 된다면, count는 다시 0을 저장하게되고, 가지고 있던 정보가 날라가게 됩니다.
그럼 글로벌 변수로 만들면 되지 않을까요?
let age = 0;
function Component() {
const mutate = () => {
age = age + 1;
console.log(`${getTime()}| count: ${age}`);
};
return (
<div>
<h1>{age}</h1>
<Button onClick={mutate}>Mutate</Button>
</div>
);
}
//Partially Retrieved from: https://theodorusclarence.com/blog/react-core-concept-rendering-state
위 문제는 해결된것 처럼 보입니다. 전역변수로 계속 값을 가지고 있을테니까요. 하지만, 전역변수는 또 다른 문제를 가져옵니다.
예를들어, 다른 페이지를 서핑하고 다시 돌아왔을때도, 이 값을 계속 들고 있을 수 있습니다.
이렇게 component function단위로 rerendering이 일어나다보니, 지역변수로 할수 있는 한계가 있고, 그것을 대체할 무언가가 필요하게 된겁니다. 그것이 useState이고요.
뜬금없는 부제목이네요ㅎㅎ 바로 이게 제가 useState를 쓰면서 어려웠던 부분중 하나입니다.
가끔 state가 어떻게 바뀌고 있는지, 잘 바뀌고 있는지 체크하려고 값을 console.log() 하던 적이 있었는데요, 콘솔에 값은 틀리지만, 원하는 결과가 나올때가 있었습니다. 그때는 그냥 '아 뭔 개같은 버그여' 라고 생각하고 가볍게 넘어갔는데요, 이번 useState를 복습하면서, 이게 왜 이런지 알게 되었습니다.
이제 state가 어떤 경로로 mutate가 일어나는지 파악했습니다. 그리고, 간단하게나마 setState로 state를 바꾸는것도 알아봤었죠.
function Component() {
const [age,setAge] = useState(10)
return (
<div>
<h1>{age}</h1>
<Button onClick={
()={
setAge(age+1)
console.log(age)
}}>Mutate</Button>
</div>
);
}
//Partially Retrieved from: https://theodorusclarence.com/blog/react-core-concept-rendering-state
위의 컴포넌트를 useState를 사용하게끔 변경하고, age를 출력하게끔 console.log했습니다. 어떤 값이 나올까요? 10? 11?
정답은 10입니다. 하지만 그렇다고 mutate가 안일어난 것은 아닙니다. age에는 정확히 11이 저장되어있고, h1태그에도 11이 나올겁니다. 이런 이유는 setState함수가 async하게 일어나기때문입니다.
Asynchronous function은 JS의 특장점입니다. 이것도 나중에 한번 따로 다뤄보겠습니다. 간단하게 설명하자면, call stack에서 실행은 하고, return을 기다리지 않고 다음 call을 실행하는 겁니다. 좀 더 쉽게 말하면, 그 함수가 어떻게 되던말던, 그 다음 함수를 실행하는 것이죠. 예시를 다시 보면, setAge 의 다음 call은 console.log입니다. setAge는 async이므로, console.log가 실행될때는 아직 age가 mutate되지 않은 시점일수 있는겁니다. 하지만, 컴포넌트가 다시 랜더링은 state의 변화를 감지하고 실행이 되는거니까, age mutate 후에 랜더링이 되는겁니다.
그래서 만약 state를 바꾸고 나서, 그 값으로 다른 무언가를 하고 싶을때, 그때는 다른 훅인 useEffect가 들어오게 될 차례입니다.
지금까지 useState에 대해서 알아보았습니다. useState는 리액트 생태계에 맞는 변수 관리 방법이었고, setState는 async하게 작동한다는게 포스팅의 핵심같네요.
기술 스택에 대해 처음 글을 쓰다보니, 너무 횡설수설 하는 느낌이 드네요. 또 쓰다보니, 앞으로 해야할 포스팅이 넘친다는게 보입니다..ㅎㅎ
다음엔 useEffect, JS callstack & Async function, React VDOM 중 하나로 돌아오겠습니다!
https://beta.reactjs.org/learn/state-a-components-memory
https://codingapple.com/unit/react-setstate-async-problems/
https://felixgerschau.com/react-rerender-components/
https://theodorusclarence.com/blog/react-core-concept-rendering-state