React 프로젝트를 진행하다보면 컴포넌트의 상태관리 혹은 데이터를 저장하기 위한 변수로 useState훅을 자주 사용한다. 이번 포스팅에서는 useState가 내부적으로 간략하게 어떻게 동작하고 Closure과 어떤 관련이 있는지 알아보자.
이번포스팅은 useState에 대해서 어느정도 사용해본 사람들의 기준에 맞춰서 작성된 포스팅이다.
useState
훅은 컴포넌트의 렌더링 및 상태를 관리할 수 있는 훅이다.
useState
훅을 단순히 변수 선언에 이용하는 훅으로 생각하면 안된다.
useState
훅은 컴포넌트의 렌더링과 상태를 컨트롤 하는 훅으로써 사용한다는 점이다.
왜 이런용도로 사용할까?
React는 컴포넌트를 처음 렌더링 시킬때만 한번 탐색하고 그 후에는 별도의 명령을 주지 않는 이상 탐색하지 않는다.
즉, useState
는 컴포넌트를 다시 탐색해서 렌더링에 변화를 주거나 컴포넌트의 상태를 관리하는 일종의 명령이다.
useState
를 사용하기 위해서는 첫번째로 해당 훅을 import해줘야 한다.
import { useState } from "react";
그 후, useState
를 함수 컴포넌트 혹은 커스텀 훅 내부에 선언해서 사용하면 된다.
function ExamComponent(){
const [value, setValue] = useState("apple")
}
useState
훅의 반환값은 배열로 0번째 원소에는 useState
훅의 상태변수에 정의된 값, 1번째 원소에는 0번째 원소를 바꿀 수 있는 set함수가 들어간다.
useState
훅의 인수로는 초기값을 정의할 수 있다.
즉, 위 예시에서는 "apple"이 해당 useState
훅이 정의될 때 초기값으로 정의되는 것이다.
인수를 정의하지 않으면 "undefined"값으로 초기값이 정의된다.
setValue("banana");
set함수의 인수에는 단순한 데이터 뿐만 아니라 콜백함수가 들어갈 수 있다.
setValue((prevState) => !prevState);
위와같이 인수로 콜백함수를 줄 경우에는 이전 state값에 의존성이 있을경우 사용하게 된다.
useState
로 정의할 수 있는 데이터 타입에는 문자열, 숫자, 배열, 객체 등등 다양한 데이터 타입이 들어갈 수 있다.
여기까지는 useState
가 "아 이렇게 사용하는 거구나"라는 마인드로 가볍게 지나갈 수 있지만 문제는 아래 생각에서 발생했다.
"배열의 0번째 원자로 해당 값을 받는다는건 어딘가에 해당 값을 저장해놨다가 가져온다는 얘기인데 어디에 저장하는걸까?"라는 의문이 들기 시작했다.
위 의문을 풀기 위해서 useState
을 뜯어보기로 결심했다.
useState
훅을 console.log로 출력해보면 어떤 값이 나올까?
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
위와같이 "resolveDispatcher()"등 처음보는 함수가 포함된
useState
함수가 "react.development.js"소스파일에 있다는걸 확인할 수 있다.
해당 파일의 정확한 위치는 node_modules/react/cjs/react.development.js
파일에 위치하고 있다.
여기서 "react.development.js"파일은 개발 환경에서 사용되는 JavaScript 파일이다.
개발자 도구의 console.log로 출력한 함수를 더블클릭하면 "Sources"탬으로 넘어가지면서 해당 함수가 위치한 소스파일의 이름을 볼 수 있다.
다시 본론으로 돌아와서 해당 함수의 구성을 자세히 살펴보자
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
위 함수를 보면 초기값으로 initialState
을 인자로 받는걸 확인할 수 있다.
이는 useState("apple")
로 정의할 때 "apple"값이 initialState
에 정의되어서 resolveDispatcher()
함수의 useState
메서드의 인수로 확인할 수 있다.
resolveDispatcher()
함수를 살펴보자
function resolveDispatcher() {
var dispatcher = ReactCurrentDispatcher.current;
{
if (dispatcher === null) {
error('Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' + ' one of the following reasons:\n' + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + '2. You might be breaking the Rules of Hooks\n' + '3. You might have more than one copy of React in the same app\n' + 'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.');
}
} // Will result in a null access error if accessed outside render phase. We
// intentionally don't throw our own error because this is in a hot path.
// Also helps ensure this is inlined.
return dispatcher;
}
해당 함수는 다시 ReactCurrentDispatcher
의 current
를 반환하는 걸 확인할 수 있다.
useState
훅을 함수 컴포넌트나 커스텀 훅 내부에서 사용하지 않으면 에러 문구를dispatcher
가 null일 때 발생하는 걸로 보아ReactCurrentDispatcher
의current
가 해당 구분을 해주는걸로 확인할 수 있다.
var ReactCurrentDispatcher = {
/**
* @internal
* @type {ReactComponent}
*/
current: null
};
ReactCurrentDispatcher
는 객체형태로 이뤄져 있는데 여기서 주의깊게 볼 점은 전역 변수로 선언되어 있다는 점이다.
즉,
resolveDispatcher
함수는 외부 즉, 전역변수인 ReactCurrentDispatcher
에서 값을 가져오는 것이다.
이는 무엇을 의미하는 걸까?
바로 함수 내부 스코프에 있는 변수를 접근하는게 아닌 상위에 있는 스코프에 접근해서 state의 값을 가져오는 것이다. 즉 Closure를 이용하고 있는 것이다.
생각해보면 함수 컴포넌트도 결국 함수이다.
즉, 함수 컴포넌트가 선언될 시점(콜 스택에 실행 컨텍스트가 생성될 시점)에 접근이 가능했던 상위 스코프에 접근할 수 있는 것이다.
그래서 전역으로 선언되어 있던 state값들에 접근이 가능한 것이다.
단순하게 사용만 했던 useState
가 생각외로 꽤 복잡한?로직에 의해서 실행되는걸 확인할 수 있었다.
이번 포스팅은 1,2편으로 나눈 이유는 단순히 한 개의 포스팅으로 useState
의 모든걸 정리하기에는 굉장히 길어질거 같아서 이번 포스팅에서는 useState
가 Closure와의 관계에 대해서 정리해 보았고 다음 포스팅에서는 useState
가 실질적으로 돌아가는 내부 로직에 대해서 알아볼 예정이다.
https://velog.io/@ggong/useState-Hook%EA%B3%BC-%ED%81%B4%EB%A1%9C%EC%A0%80
https://velog.io/@zinukk/useState%EC%9D%98-%EC%9E%91%EB%8F%99-%EC%9B%90%EB%A6%AC
https://kyoung-jnn.com/posts/react-useState
https://goidle.github.io/react/in-depth-react-hooks_1/