필자는 한밭대학교 무선통신소프트웨어 연구실에서 교수님과 연구원님의 지도 아래 2023 Wisoft React Seminar를 진행하고 있다.
세미나 간 서로 맡은 부분을 공부해 발표하기로 했다.
4명중 1명은 Event, 1명(본인) 은 useState, 나머지 2명은(useEffect) 를 공부해 발표하기로 했다.
따라서 React의 가장 기본적인 hooks인 useState를 학습하기로 했으며, 보다 깊은 공부를 위해 React16.8 이전의 class component와 비교해보려고 한다.
먼저 useState를 사용해보자.
React 16.8 버전에 새로 추가된 useState는 클래스 컴포넌트 없이도 state와 같은 특징을 사용할 수 있다.
아래 코드는 useState를 사용한 기본적인 counter 예제이다.
import React, { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
위 코드에서 useState는 변수 count와 count 변수를 업데이트하는 setCount 변수를 반환하고있다.
Click me 라는 버튼을 선언하고, onClick 이벤트를 추가해 콜백으로 setCount를 넘겨줬고, 그 setCount에서는 count 변수를 +1 하는 코드이다.
useState로 선언한 카운터 예제를 클래스 컴포넌트로 바꾸면 아래와 같다.
import React, { Component } from "react";
class Counter extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
increment = () => {
this.setState((prevState) => ({ //컴포넌트 자체
count: prevState.count + 1,
}));
};
render() {
const { count } = this.state;
return (
<div>
<p>{count}</p>
<button onClick={this.increment}>+</button>
</div>
);
}
}
export default Counter;
React 클래스 컴포넌트에 내장되어있는 setState 라는 함수를 이용해 constructor 메서드에 선언한 this.state 의 count 값을 변경하고 있다.
Javascript 에서의 this는 주의해서 사용해야 한다.
this 가 바인딩 되는 곳이 사용처마다 다르기 때문이다.
위에서처럼 constructor() 메서드에서 사용하는 this 는 constructor() 메서드가 나중에 생성할 인스턴스에 바인딩된다 !
또한 increment 화살표 함수의 this 는 Counter 컴포넌트에 바인딩 된다.
화살표 함수에는 this가 없기 때문에, 그 상위 환경에서의 this를 참조하게 된다.
즉 increment 라는 함수에는 this가 없으므로, 그 상위 환경인 Counter 에 바인딩 되는 것이다.
render 또한 마찬가지이다.
기존의 클래스 컴포넌트를 사용하게 되면 state를 변경하는 데 this.setState 처럼 계속 this를 붙어주어야 하고, this 바인딩 같은 개념 때문에 사용하기 난해하다.
함수 컴포넌트의 hooks를 사용하면 보다 편하게 state 를 제어할 수 있다!
우클릭으로 useState 모듈을 분석해보자!
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
위처럼 useState는 JS의 함수이다.
인자로 initialState를 받고, resolveDispatcher() 가 반환하는 값을 dispatcher에 담고, 이후 이 dispatcher의 useState 메서드에 initialState를 인자로 전달한다.
그렇다면 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;
}
resolveDispatcher() 함수는 ReactCurrentDispatcher.current 의 값을 할당받는다.
이 ReactCurrentDispatcher 는 전역에 선언된 객체이며, 이 객체의 current 는 객체의 프로퍼티이다.
즉, useState가 return 하는 값은 애플리케이션의 전역에서 온다는 의미이다.
import React, { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
카운터 예제를 다시 가져와봤다.
위 예제에서 선언된 useState 는 클로저의 개념을 사용해 함수의 상태를 기억한다.
즉, 자신이 어디서 정의되었는지를 기억하고, 그 때 접근 가능했던 상태 값이라면 나중에도 ( 생명주기가 종료된 후 에도) 그 상태에 계속 접근할 수 있다는 뜻이다.
function doubleCount() {
setCount(count+1)
setCount(count+1)
}
위 코드처럼 doubleCount() 를 선언하고 실행하면 어떻게 될까?
예상한 동작은 setCount가 2번 실행되어 count가 2증가하는 것이겠지만, 실제로는 1만 증가된다.
즉 useState가 선언되었던 그때 접근 가능했던 변수들을 기억한다.
맨 처음에 그렇게 접근 가능했던 count 변수의 값은 초기값으로 선언되었던 0이다.
따라서 **setCount(count+1)** 를 2번 호출하더라도, 0 + 1을 2번 실행한것이나 다름 없는것이다.
우리가 예상한대로 동작하게 하려면 아래처럼 setCount 의 콜백으로 이전 값을 전달하면 된다.
setCount(prev => prev+1);
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
위 코드는 useState의 구성이다.
useState가 반환하는 setState 함수에는 인자로 콜백함수를 전달할 수 있다.
setState 에 콜백함수를 전달하면, 해당 상태가 업데이트 된 최신 값을 반영해 로직을 수행한다.
useState를 공부하다가 의문점이 생겼다.
ES6의 문법인 const 는 변경되지 않는 값을 선언할 때 사용한다.
const [count,setCount] = useState(0);
그런데 위처럼 useState를 선언하면, 결국 count 와 setCount는 const로 선언한게 된다!
그런데 우리는 계속 count 의 값을 바꿔주었다.
const 로 선언한 변수가 어떻게 바뀌는걸까?
useState가 선언된 컴포넌트도 결국은 함수이다.
이 함수가 실행되면서 실행될 때 마다 새로운 const 변수가 선언되고, 그 변수를 변경하는 setState함수가 클로저의 원리로 값을 변경할 수 있는 것이다.
const 로 선언한 이유는, useState로 선언한 변수는 다른 외부의 요인으로는 변경되어서는 안되고, useState가 반환하는 setState 로만 변경되어야 하기 때문인다!
제대로 공부하시네요.