useState 에 대한 고찰

손준호·2023년 9월 13일

React

목록 보기
7/11
post-thumbnail

배경


필자는 한밭대학교 무선통신소프트웨어 연구실에서 교수님과 연구원님의 지도 아래 2023 Wisoft React Seminar를 진행하고 있다.

세미나 간 서로 맡은 부분을 공부해 발표하기로 했다.

4명중 1명은 Event, 1명(본인) 은 useState, 나머지 2명은(useEffect) 를 공부해 발표하기로 했다.

따라서 React의 가장 기본적인 hooks인 useState를 학습하기로 했으며, 보다 깊은 공부를 위해 React16.8 이전의 class component와 비교해보려고 한다.

useState


Using the State Hook – React

먼저 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 분석


우클릭으로 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 로만 변경되어야 하기 때문인다!

참고


[번역] 심층 분석: React Hook은 실제로 어떻게 동작할까?

profile
디지털 노마드가 되고싶은 개발자

1개의 댓글

comment-user-thumbnail
2023년 9월 13일

제대로 공부하시네요.

답글 달기