상태라는 것은 리액트에서 중요한 개념입니다. 궁극적으로 상태라는 것은 컴포넌트를 리렌더링하고 화면에 있는 것을 바꿔주기 때문입니다.
일반적으로 useState
라는 훅을 이용해 상태를 사용했습니다. useState
훅은 react 라이브러리에서 import하여 사용합니다.
여기서는 react 라이브러이에서 가져온 useState
훅에 대해서 좀 더 깊이 있게 살펴보겠습니다.
우리는 함수 컴포넌트 내부에서 useState
라는 훅을 사용하면 리액트에 상태를 등록하고, 리액트가 관리한다고 알고 있습니다. 하지만 컴포넌트에서 useState
가 반환한 배열의 요소 중 상태 변경 함수가 호출되면 컴포넌트가 다시 실행되어 useState
도 다시 호출되어 기본값으로 다시 초기화될 것 같지만 그렇게 동작하지 않습니다.
useState
훅은 "클로저"로 동작합니다. useState
훅의 로직을 간단하게 작성해보겠습니다(실제 코드와는 다릅니다).
// react.js
let _value; // -> 상태
export useState(initialValue) { // -> useState 훅
// 상태가 한 번이라도 초기화가 되었는지 검사
if (_value === undefined) {
// 이전에 초기화되지 않았더라면 초기값으로 초기화
_value = initialValue;
}
// 상태 변경 함수는 _value 자유 변수의 값을 재할당
const setState = newValue => { // -> 상태 변경 함수
_value = newValue;
};
// useState 훅이 최신 상태와 상태 변경 함수를 배열로 반환
return [_value, setState];
}
react에 존재하는 useState
함수는 상위 컨텍스트에 선언되어 있는 _value라는 변수를 참조하고 있는 "클로저"입니다.
useState
훅을 호출하면 배열이 반환되어 우리는 지금까지 배열 디스트럭처링 할당 문법을 사용한 것 또한 알 수 있습니다.
중요한 것은 useState
함수 외부에 정의된 _value 변수입니다.
_value 변수가 우리가 useState를 통해 관리하는 "상태"입니다. setState 함수는 컴포넌트에 선언된 상태 변수가 아닌 useState 훅의 "상위(react)에 선언된 _value 변수 값을 변경"합니다.
우리는 컴포넌트에서 상태값을 할당받는 상태 변수를 아무리 재할당하여도 "상태는 react 내부에서 관리"하기 때문에 변경되지 않습니다. 그러므로 컴포넌트 내 사용되는 상태 변수를 "불변 변수"로 사용하여 이를 재할당하는 것을 방지하도록 합니다.
함수 컴포넌트가 처음 실행될 때 useState
훅의 인수로 초기값(initialValue)을 전달하면서 호출합니다.
useState
훅은 호출될 때마다 초기값을 전달받습니다. 하지만 if 문에서도 알 수 있듯이 상태가 한 번이라고 초기화가 된 상태라면, 즉 undefined가 아닌 경우에는 초기값으로 상태를 재할당하지 않습니다.
초기값(initialValue)은 useState
훅이 "최초로 호출될 때만 사용"되고 이후에는 사용되지 않습니다.
이후 "상태를 나타내는 _value"와 "_value 변수 값을 재할당하는 setState 함수(상태 변경 함수)"를 배열의 요소로 담아 반환합니다.
참고로 useState
를 여러번 사용해도 각기 다른 상태 값과 갱신 함수를 사용할 수 있는것은 단순히 _value 하나가 아니라 여러 개를 react가 갖고 있기 때문입니다.
만약 setState 함수, 즉 상태 변경 함수가 호출되면 인수로 전달 받은 값을 "_value에 재할당"합니다. 그리고 현재 코드에서는 구현하지 않았지만 "함수 컴포넌트가 재평가"되도록 시킵니다.
컴포넌트가 상태 변경 함수의 호출로 재실행될 때는 useState
함수도 다시 호출됩니다. 이때 초기값이 전달되지만 _value에는 이미 setState 함수에게 전달된 인수를 갖고 있으므로 초기값으로 초기화되지 않게 됩니다.
정리하자면, 상태 변경 함수(setState)는 컴포넌트에 선언된 상태 변수를 변경시키는 것이 아니라 "react 라이브러리에 선언된 변수(_value)를 변경"하게 됩니다. 그리고 "컴포넌트를 재실행" 시키는 역할을 합니다.
단, 상태 변경 함수가 호출되고 이전 상태값과 현재 상태값을 "단순 비교(=== 연산자)"하여 다른 경우에만 상태를 변경하고 컴포넌트를 재실행시킵니다. 이전 상태값과 동일한 경우 상태를 변경시키지 않고 컴포넌트도 재실행하지 않습니다.
상태 변경 함수는 상태를 변경 시키는 역할과 컴포넌트를 재실행 시키는 역할을 하고, 실질적으로 "변경된 상태는 useState가 가져오게 됩니다".
중요한 점은 상태 변경 함수가 컴포넌트 내부에 선언한 상태 변수를 변경하는 것이 아니기 때문에 상태 변경 함수 호출 이후 로직에서도 상태 변수의 값이 동일하다는 것입니다. 변경된 값은 다음 컴포넌트 함수가 재실행될 때 useState
훅이 가져오게 됩니다.
이는 상태 변경 함수 호출 이후에 상태 변수 값이 변경되지 않는 것과도 연관이 있습니다. 상태 변경 함수가 호출된 시점의 실행 컨텍스트에서 상태 변수값이 변경되지 않았기 때문에 상태 변경 함수를 호출한 다음 해당 컨텍스트에서 상태 변수를 참조해도 변경되지 않습니다. 변경된 값은 컴포넌트가 재실행되는 시점에 useState
훅이 새로운 상태 변수에 변경된 상태값을 할당합니다.
위에서 상태 관리가 react에서 이루어진다는 것을 알아보았습니다. 또 하나 중요한 것이 있습니다. 상태 변경 함수를 통해 상태를 업데이트하는 처리는 즉시 처리되는 것이 아니라 "스케줄링"을 하게 됩니다. 이후 스케줄링된 상태 변화를 리액트가 처리합니다. 즉, 리액트가 즉시 처리하지 않는다는 점에 유의해야 합니다.
위에서 살펴본 상태 변경 함수의 역할은 "상태를 변경"하고, "함수 컴포넌트를 재실행"시키는 역할을 합니다. 그렇기 때문에 변경된 상태 값은 컴포넌트가 재실행될 때, 즉 리렌더링될 때 반영이 된다는 것 또한 위에서 알아보았습니다.
만약 상태 변경 함수를 동기적으로 처리한다면 상태 변경 함수가 짧은 시간동안 여러 번 호출되는 경우 그만큼 많은 리렌더링이 발생하게 되어 문제가 될 수 있습니다.
그렇기 때문에 우리는 상태 변경 함수를 "비동기적으로 bath update(일괄 업데이트)"를 사용합니다. 상태 변경 함수를 짧은 시간 동안 여러 번 호출하더라도 변경 사항을 즉시 처리하는 것이 아니라 Queue에 "호출 순서대로 넣어서 16ms 단위로 일괄적으로 업데이트"합니다. 이로 인해 불필요한 렌더링을 줄여줍니다.
import {useState} from 'react';
const App = () => {
const [count, setCount] = useState(0);
const addCountHandler = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
return <div onClick={addCountHandler}>{count}</div>;
div 요소를 클릭하면 3이 나올것 같지만 실제로는 1이 출력됩니다. 그 이유는 위에서 설명한 것처럼 비동기로 bath update를 사용하기 때문입니다.
상태 변경 함수의 호출 정보는 모두 Queue에 호출 순서대로 푸시되어 순서대로 모두 처리됩니다. 단, 16ms 단위로 일괄 처리함으로서 count 상태가 0인 상태로 업데이트가 진행됩니다.
이를 해결하기 위해서는 우리는 상태 변경 함수에 "콜백 함수"를 전달하여 해결 했습니다. 콜백 함수에는 언제나 최신 상태 값이 인수로 전달된다는 것이 보장됩니다. 콜백 함수를 전달하는 로직으로 수정해보겠습니다.
import {useState} from 'react';
const App = () => {
const [count, setCount] = useState(0);
const addCountHandler = () => {
setCount(prevCount => preCount + 1);
setCount(prevCount => preCount + 1);
setCount(prevCount => preCount + 1);
};
return <div onClick={addCountHandler}>{count}</div>;
이제는 div 요소를 클릭할 때마다 3씩 증가되는 것을 확인할 수 있습니다. prevCount라는 매개변수에는 언제나 최신 상태가 보장되기 때문입니다.
또 다른 방법으로는 useEffect
훅을 사용하는 방법이 존재합니다. useEffect
훅에는 dependencies 배열에 상태를 추가한다면, 그 상태값이 언제나 가장 최신의 상태를 가질 것을 보장합니다.
useEffect 훅은 컴포넌트가 렌더링이 끝난 뒤에 dependencies 배열의 요소로 작성한 값이 변경된 경우 인수로 전달한 콜백 함수가 호출됩니다.
상태는 상태 변경 함수가 호출되어 리액트에서 관리하는 상태를 변경합니다. 이후 컴포넌트가 재평가되고 리렌더링됩니다. useEffect 훅의 인수로 전달한 콜백함수는 컴포넌트가 "리렌더링된 이후에 호출"되는 함수로서 콜백 함수가 호출되는 시점에는 이미 상태가 최신으로 업데이트가 된 이후입니다. 그러므로 언제나 최신 상태를 보장하게 됩니다.
이외에도 useState 훅이 아닌 useReducer
훅으로 상태를 관리하는 경우 useReducer
훅의 첫 번째 인수로 전달하는 reducer 함수는 첫 번째 인수로 언제나 최신 상태값을 가져오는 것을 보장하기 때문에 useReducer
훅으로 상태를 관리하는 방법으로도 해결할 수 있습니다.