Skip to main content

Command Palette

Search for a command to run...

[React] Hook- useState

Updated
4 min read

⭐들어가며⭐

React에는 여러가지 Hook이 존재한다. 리액트 프로젝트를 만들기 쉽게 만든 API의 일종이다. 리액트 Hook 중에서 가장 기본 및 기초가 되는 useState에 대해 알아보자.

Built-in React Hooks – React

useState

공식문서 정의

useState is a React Hook that lets you add a state variable to your component.

원형

import {useState} from 'react';
const [state, setState] = useState(initialState);

useState는 2개의 return 값을 반환한다.

  • state

    • 현재 state값, 이해를 위해 지금은 변수값이라고 생각하자. 해당 변수의 초기값으로는 initialState 값이 들어간다.
  • setState

    • state를 update하는 함수이다.

→ 해당 구조는 Javascript의 구조분해할당 방식을 사용하여 useState의 리턴값을 저장해준 것이다.

구조 분해 할당 - JavaScript | MDN


❓처음 useState를 공부할때 3가지 의문이 들었다.

Q1. 왜 let을 사용하지 않고 state를 사용해서 변수를 저장하지?

Q2. setState를 따로 만들어서 state를 관리하는 이유가 뭐지?

Q3. setState함수에 함수를 넣어주는 이유는 무엇일까?

해당 질문들을 답하기 위해서는 리액트의 랜더링 동작에 대해 이해해야 한다.

먼저, Javascript처럼 코드를 작성해보자. 예상대로라면, Count 값이 버튼을 누를때마다 1씩 증가해야 한다.

export default const Counter = () => {
    // Count 값 저장 변수
    let count = 0;
    // Count 증가 Event Handler
    const increaseCount= ()=> {
        count += 1;
        console.log(count);
    };

    return (
        <div>
            <button onClick={increaseCount}>+1 Button</button>            
            <p>Count : {count}</p>
        </div>
    );
};

<결과>

→ 아무리 눌러도 Count 값이 바뀌지 않음을 확인 할 수있다.

일반 변수인 let 으로 선언하여 이벤트를 처리하면, 값이 바뀌지 않는 이유는 리액트의 랜더링 방식에있다.

#랜더링이란?

랜더링이란, 컴포넌트가 현재 propsstate의 상태에 기초하여 UI를 어떻게 구성할지 컴포넌트에게 요청하는 작업을 의미한다.

#리액트의 랜더링 방식

리액트는 Declarative(선언적) Programming이다. 컴포넌트를 만들기 위해 복잡한 DOM Element에 접근하지 않고, 리액트 딴에서 자동으로 해준다.

Q1. 왜 let을 사용하지 않고 state를 사용해서 변수를 저장하지?

A1. 리액트는 초기에 전체 컴포넌트를 전체 실행시키고, 일반 변수가 바뀌었다고 해서 재렌더링하지 않는다.

→⭐하지만, State값이 변경되면 이와 관련된 내용들에 ReRendering된다.⭐

이때, 관련된 내용들은 해당 state를 포함한 최상위 컴포넌트와 이의 모든 자식 컴포넌트를 의미한다.

결국, state를 통해 변수를 관리해야, UI에 바뀐 값이 보이는 것이다. 예제 코드를 살펴보자.

import {useState} from "react";

export default const Counter = () => {
    const [count, setCount] = useState(0);

    const increaseCount= () => {
        setCount((prev)=> prev+1);
        console.log(count);
    };

    return (
        <div>
            <button onClick={increaseCount}>+1 Button</button>            
            <p>Count : {count}</p>
        </div>
    );
};

<실행 결과>

Count 값이 1씩 정상적으로 증가함을 볼 수 있다.

하지만! 콘솔창을 살펴보자. (총 3번 버튼을 눌렀다)

setCount로 값을 증가시키고 출력했음에도 증가한 값이 아닌 기존값이 출력됬음을 알 수 있다.

왜 그런 것일까?

원인은 리액트의 set함수 처리 방식에 있다.

해당 그림은, 리액트 라이프 사이클을 표현한 그림이다.

리액트는 리렌더링을 유발하는 동작들을 큐에 저장하여 실행한다.

  • 리렌더링을 유발하는 동작

    • setState()

    • forceUpdate() (ReactDOM.render()과 같다고 할 수 있다.)

    • useState()의 setter(setState함수)

    • useReducer()의 dispatchers

하지만, 해당 큐들에 저장하는 것이 해당 코드를 읽다가 만났을때 실행하는 동작이고, 실제로 실행되는 시점은 getDereivedStateFromProps 단계에 도달했을 때이다. 즉, render직전에 실행되는 것이다. (return 전에 실행된다고 보면 된다.)

다시 예제 코드를 살펴보자. console.log 는 setter 함수 이전에 실행되므로, 변경되지 않는 값이 들어간 것이다.


Q2. setState를 따로 만들어서 state를 관리하는 이유가 뭐지?

A2. state는 불변성(immmutable)을 유지해야하기 때문입니다.

컴포넌트는 현재의 this.state와 setState를 비교해서 업데이트가 필요한 경우에만 render 함수를 호출하는데, state를 직접 수정하면 리액트가 render 함수를 실행하지 않아서, 필요한 리랜더링이 발생하지 않을 수 있습니다. 상태 변경을 추적하기 위해서는 setter함수를 사용합시다!



Q3. setState 함수에 함수를 넣어주는 이유는 무엇일까?

위의 예제 코드를 자세히 살펴보면, setCount(count++)이 아니라, setCount((prev) ⇒ prev + 1)로 작성되어 있음을 알 수 있다. 이렇게 함수를 넣어주는경우, 리액트에서 이전값을 자동으로 참조한다.

해당 예제를 실행해보면 이해하기 쉽다.

export default const Counter = () => {
    const [count, setCount] = useState(0);

    const increaseCount = () => {
      setCount(count + 1);
      setCount(count + 1);
      setCount(count + 1);
  };

    return (
        <div>
            <button onClick={increaseCount}>+3 Button</button>            
            <p>Count : {count}</p>
        </div>
    );
};

<실행결과> (버튼 3번 클릭)

Count : 3이 아닌 이유는 3개의 setCount 모두에 count 값이 0 (초기값)으로 들어갔기 때문이다.

이전에, set함수들은 큐에 저장된다고 하였는데, 이때 현재 count 값이 인자로 저장된 것이다. 그래서 모두 0으로 저장되었고 이를 실행한 것이다. 하지만, 우리가 원하는 값은 Count : 3이 되는것이 아닌가? 이를 위해서, setter 함수에 함수를 넣어주면 이전값을 참조하게끔 리액트는 useState를 설계하였다.

A3. setState 함수 내에서 이전 state값을 참조하기 위해

→ 정상적으로 실행시키기 위해서는 setState 함수에 익명함수를 넣어주면 된다.

export default const Counter = () => {
    const [count, setCount] = useState(0);

    const increaseCount = () => {
      setCount((prev) => prev + 1);
      setCount((prev) => prev + 1);
      setCount((prev) => prev + 1);
  };

    return (
        <div>
            <button onClick={increaseCount}>+3 Button</button>            
            <p>Count : {count}</p>
        </div>
    );
};

해당 방식으로 작성하면, 정상적으로 Count : 3이 UI에 렌더링됨을 확인할 수 있다.

✏️마치며

리액트에서 가장 중요한 주제가 컴포넌트와 상태라고 생각한다. 그만큼, 자주 보게되고 자주 사용할 테니 확실하게 익혀두는게 좋을 것 같다.

edited by 김지호

More from this blog

[ZSH] tree 사용하기

들어가며 큰 규모의 프로젝트를 출시한 뒤, 후일을 위해서 더 늦기 전에 파일 정리 및 문서화를 진행해야했다. 문서화 작업을 하는 중에 기왕 정리하는 거 파일 구조를 이쁘게 트리 구조로 나열하여 코멘트를 달면 나중에 보더라도 이해하기 더 쉬울 것 같았다. 어떻게 해야 간지나는 트리 구조를 만들 수 있을까 방법을 찾다보니 역시나 파일 구조를 트리로 이쁘게 출력해주는 커맨드 툴이 존재했다. tree 커맨드에 대해서 알아보고 알짜배기 내용만 정리했다....

Feb 21, 20242 min read

[Next.js] parallel routes & intercepting routes

트위터 로그인 모달창을 만들어보며 넥스트의 parallel routes 와 intercepting routes 을 학습한 내용을 정리해보았습니다. 트위터 로그인 창을 확인해봅시다. 루트 디렉토리 화면을 배경으로 i/flow/login 페이지가 동시에 표시되고 있습니다. 저는 app router 를 학습하기 전까지는 createPortal 을 사용하여 포탈 영역에 로그인 컴포넌트를 띄우는 방식을 사용했었습니다. const NoLogin =()=...

Feb 1, 20244 min read

C/C++ 이진 트리(binary tree) 개요 및 구현(1)

개요 트리는 노드들이 나무 가지처럼 연결된 비선형 계층적 자료구조이다. 하위 트리가 존재하고, 그 노드에 또 하위 트리가 존재하는 자료구조 이다. 트리의 맨 위에 있는 루트 노드가 존재한다. 우리가 알아볼 트리는 이진 트리이다. 이진 트리는 자식 노드(부모로부터 아래로 이어진 노드)가 2개 이하인 구조를 말한다. 트리의 사용 사례로는 다음과 같다 계층 적 데이터 저장(파일,폴더) 효율적인 검색 속도 힙 데이터 베이스의 인덱싱 트리에 ...

Jan 31, 20244 min read

[React] Server component (RSC)

React.js 18 에 도입된 리액트 서버 컴포넌트는 서버에서 동작하는 리액트 컴포넌트를 의미합니다. Next가 권장하는 라우팅 방식인 app router의 기반이 되는 컴포넌트이기 때문에 app router 를 이해하기 위해서는 server component 에 대한 이해가 필요합니다. server component 리액트는 클라이언트단만을 컴포넌트화하는 대신, server component라는 개념을 통해 서버 영역을 컴포넌트화합니다. ...

Jan 29, 20243 min read

Flutter, JavaScript

42 posts