본문 바로가기

javascript/react

[React] useState Hook

리액트는 웹 페이지에 동적인 느낌을 부여한다. 이때, 해당 페이지가 동적으로 작동하기 위해서는 해당 페이지를 수시로 업데이트 해야 하는데, 만약 이 업데이트를 단순히 while 루프로 구현한다면, 성능적인 부분에서 심각한 오버헤드가 발생할지도 모른다. 또한, 웹사이트는 보통 특정 정보를 지니고 있는 경우가 대부분인데, 리액트는 프론트엔드 단에서 작동하기 때문에, 이러한 정보를 사용자의 환경에서 지니고 있어야 한다. 이러한 다양한 이유에 기반하여 리액트는 State, 즉 상태를 프론트엔드 단에서 관리한다. 

State?

프로그램에서 State를 정의하는 방법은 상황에 따라 다양할 수 있지만, 가장 간단한 수준에서는 변수를 의미한다. 해당 흐름에서 State의 변화는 프로그램 내 변수들의 변화를 의미한다. 리액트는 이와 마찬가지로 변수를 state로 간주하고  이를 변환하는 것을 트리거로 하여 웹사이트를 업데이트하도록 설계되어 있다. 이때 리액트는 각각의 state를 단순 변수로 처리하는 대신, useState hook을 이용하여 처리한다.

useState

 리액트 상에서 state를 업데이트 하는데 사용되는 hook이다. 보통 평범한 수준에서 프로그래밍 언어를 사용하게 되면, 우리는 State를 변경하기 위해 직접적으로 변수에 할당되는 expression을 변경할 것이다.

let a = 13;
a = a + 17; // a에 expression을 직접적으로 할당한다.

 위 코드의 경우 <a, 13> 은 a + 17이라는 expression의 할당에 의해 <a, 30> 이라는 값으로 변경되었다. State가 변경되는 것 까지는 좋다. 그런데, 앞서 언급했듯이 리액트는 이런 상태 변화를 트리거로 이용하여 페이지를 다시 렌더링해야 하는데, 위와 같은 자바스크립트 고유의 문법을 사용하여 처리되는 상태 변화를 프로그램에서 추적하는 것이 편리할까? 아마 엄청난 통찰력이 있지 않은 이상, 프로그램 내에서 모든 state를 위와 같이 처리하는 것은 매우 까다로울 것이다.  컴포넌트 내부에서 사용되는 State를 추적하되, 실제 렌더링에 사용되는 변수가 업데이트 되는 경우 웹사이트를 다시 평가하고 … 해당 과정은 지나치게 복잡하며, 모든 변수는 잠재적으로 State로 취급될 수 있기 때문에, 구현에 따라 선언된 모든 변수를 추적하기 위한 시스템을 제공해야 할지도 모른다.

 언급한 이유 뿐만은 아니겠지만, 리액트에서의 State은 단순한 로컬 변수에 의해 관리되지 않는다. 대신 특정 변수를 State로 지정하고, 이를 리액트 라이브러리 내부적으로 관리한다. 이는 useState hook을 통해 가능하다.

해당 hook의 포맷은 다음과 같다.

const [state, setState] = useState(initial_value);
  • state: State로써 관리되는 변수. initial_value의 타입을 따른다.
  • setState : value의 변경에 사용되는 함수
  • useState : 특정 변수를 State로 취급하여 관리하기 위한 hook
  • initial_value : value의 초기값

useState hook은 초기값을 받아 리액트 자체적으로 관리하는 메모리 영역에 해당 변수를 등록한다. 이때 값의 변경은 setValue를 통해서 수행하며, 단순히 value의 값을 바꾸는 행위는 페이지를 다시 렌더링하지 않는다.

 상태가 변경되어 해당 컴포넌트를 다시 렌더링하는 것은 함수를 다시 실행하는 것과 동일하다. 따라서 우리가 실행할 때마다 렌더링되는 각 함수는 서로 다른 렉시컬 환경을 가지고 있어 각자의 고유한 변수를 가지게 되는데, ( 동일 함수를 여러번 실행하더라도, 특정 실행의 지역 변수가 다른 실행에 영향을 주지 않음 ) 리액트 컴포넌트에 존재하는 state 변수 역시 지역 변수에 불과하다. 실제 값은 리액트가 관리하는 메모리 상에 존재하며, 각 렌더링마다 얻는 state 변수는 이에 대한 스냅샷일 뿐이다.

 그런데, 여러번 렌더링 되는 컴포넌트에 대해서 useState 는 한번만 초기값으로 state을 지정하는 모습을 보인다. 보통의 함수라면 useState 역시 실행할 때마다 값을 초기화 했을 것이다. 이런 동작은 useState의 특성에 의한다. 리액트는 특정 컴포넌트가 처음으로 실행되어 처음 해당 변수에 대해 값을 useState을 통해 초기화 한 이후, 해당 컴포넌트의 State들에 대한 일종의 포인터를 유지한다. 이후 해당 컴포넌트가 다시 렌더링되면, 초기화를 진행하는 대신 해당 State에 대해 유지하고 있는 포인터에 접근하여 해당 값을 반환한다. useState의 초기화 동작이 계속 반복되지 않는 이유는 리액트가 각각의 컴포넌트에 대해 위와 같이 정보를 유지하고 있기 때문에 가능하다.

비동기적인 State의 업데이트

대부분의 프로젝트에서 State는 하나가 아니라 여러개로 존재한다. 만약 각각의 State마다 따로 연산을 수행해야 한다면 해당 State들의 변화할 때마다 계속 렌더링이 발생하게 되므로, 성능상 큰 부하를 일으킬 수 있다. 다행히도 리액트는 이러한 변화 각각에 반응하는 대신 setState으로 인한 State의 업데이트 정보를 모은 후, 일괄적으로 이를 처리할 수 있으며, 이 과정에서 업데이트가 비동기적으로 발생할 수 있다. 따라서 기존 값에 대해 State을 정확히 업데이트 하고 싶은 경우, 과거 값에 기반하여 업데이트를 진행해야 한다.

  const numberIncHandler = (event) => {
    console.log(typeof num);
    setNum(prev => prev + 1);
  };

  const setIncHandler = (event) => {
    console.log(typeof str);
    setStr(prev => prev + 1);
  }

과거 값에 기반한 업데이트는 arrow function 등을 통해 정의한다.

상태의 병합

클래스 컴포넌트의 경우 초기 지정한 상태와 이후 업데이트 내용이 서로 병합되지만, 함수 컴포넌트는 값들을 병합해주지 않는다. 따라서 함수 컴포넌트에서는 setState을 이용해 값을 업데이트 하는 경우 반드시 모든 값을 명시해야 한다.

 

만약 ...prev가 없다면 useState로 업데이트 시 name만 있는 객체가 생성된다.

 

몇가지 용어들

  • stateful component : 상태를 가지고 있는 컴포넌트 
  • stateless component : 상태를 가지고 있지 않아 단순히 표현하는 컴포넌트
  • Controlled component : two way data binding을 통해 리액트로부터 값이 제어되는 입력 폼 엘리먼트
  • Uncontrolled component : ref을 사용해 구현하는, two way data binding이 적용되지 않은 컴포넌트

two way data binding

사용자의 입력에 의해 변경되는 값을 state로 관리하는 방식으로, 아래 두가지 과정을 거치는 것과 같다.

  1. 입력 등에 의해 발생하는 이벤트로부터 값을 추출하여, setState에 반영한다.
  2. 생성된 값을 폼 엘리먼트의 value 프로퍼티에 전달하여, 해당 값으로 나타나게 한다. 

 

해당 과정은 아래와 같다.

useState을 통해 name을 State로 등록한다.

원하는 값이 State로써 리액트에 의해 관리될 수 있도록 useState을 통해 등록한다.

 

입력 이벤트에 반응하는 일종의 콜백 함수

폼 엘리먼트(input 등)을 생성하고, onChange처럼 변경을 감지할 수 있는 프로퍼티에 컨트롤러 함수를 등록한다. 이때 value에 위에서 생성한 State을 전달하면 해당 값이 폼 엘리먼트에 반영된다.

만약 input 엘리먼트에서 값이 변경되는 이벤트가 발생하면, onChange에 등록된 콜백함수인 onNameChange을 실행한다. 해당 함수는 React의 이벤트를 입력받는데, 해당 이벤트에는 input에 들어 있는 value를 나타내는 e.target.value가 존재한다. 따라서 해당 값을 이용하여 name을 업데이트 한다. 업데이트 동작은 컴포넌트 함수를 재실행하는 트리거가 되며, 컴포넌트 내에 이전과 변경점이 있는 부분을 찾아 선별적으로 다시 렌더링한다. 

이때 등장하는 이벤트는 리액트 내부에서 관리하는 이벤트로, 웹 상에서 사용하는 이벤트와는 명백하게 구분된다. 타입스크립트를 사용하면 좀 더 와닿는데, 일반적인 이벤트를 나타내는 Event 인터페이스에는 value가 없다. value는 위와 같이 정의된 React 이벤트에 존재한다.

https://reactjs.org/docs/events.html

'javascript > react' 카테고리의 다른 글

[React] useEffect Hook  (0) 2021.12.13
[React] useRef Hook  (0) 2021.12.12
[React] Portal  (0) 2021.11.24
[React] 컴포넌트  (0) 2021.11.17
[React] React에 대한 설명  (0) 2021.11.16