본문 바로가기

상태와 useState

1. React에서의 상태

1.1 상태(State)란?

상태(State)는 컴포넌트에서 동적으로 관리되는 데이터를 말합니다. 웹 애플리케이션을 만들다 보면 버튼을 클릭했을 때 텍스트가 변경되거나, 사용자가 입력한 값을 저장하거나, 서버에서 받아온 데이터를 화면에 표시하는 등 다양한 상황에서 데이터가 변경됩니다. 이렇게 변경되는 데이터들을 React에서는 상태라고 부릅니다. 상태값이 변경되면 React는 이 상태를 사용하고 있는 컴포넌트를 자동으로 다시 렌더링합니다. 이를 통해 화면의 특정 부분을 새로운 데이터로 업데이트할 수 있습니다.

SNS의 팔로우 기능을 생각해봤을 때 사용자의 프로필 페이지에는 "팔로우" 버튼이 있고, 이 버튼을 클릭하면 "언팔로우"로 텍스트가 변경되며 팔로워 수가 1 증가합니다. 이 경우 우리는 두 가지 데이터를 상태로 관리해야 합니다. 첫 번째는 "현재 사용자를 팔로우하고 있는지 여부"이고, 두 번째는 "팔로워 수"입니다. 이 두 데이터는 사용자의 클릭에 따라 실시간으로 변경되어야 하므로 상태로 관리되어야 합니다.

또 다른 예시로 데이터를 불러오는 상황을 들 수 있습니다. 서버에서 데이터를 요청하고 받아오는 동안 스켈레톤UI를 보여주고, 데이터를 받아오면 실제 내용을 표시하는 경우가 많습니다. 이때 "현재 로딩 중인지 여부"와 "받아온 데이터"도 상태로 관리해야 합니다.

1.2 상태 관리의 중요성

프로젝트의 규모가 커질수록 관리해야 할 상태도 함께 증가합니다. 예를 들어 쇼핑몰 애플리케이션에서는 1. 장바구니에 담긴 상품 목록, 2. 각 상품의 수량, 3. 총 결제 금액, 4. 배송지 정보, 5. 결제 진행 상태 등 수많은 상태를 관리해야 합니다. 이러한 상태들은 서로 연관되어 있는 경우가 많습니다. 장바구니에 상품을 추가하면 총 결제 금액도 함께 변경되어야 하고, 결제가 완료되면 장바구니를 비워야 합니다. 따라서 상태를 어디에 위치시킬지, 상태 간의 관계를 어떻게 관리할지 신중히 고려해야 합니다.

상태 관리가 제대로 이루어지지 않으면 데이터의 흐름을 파악하기 어려워지고, 예상치 못한 버그가 발생할 수 있습니다. 특히 여러 개발자가 함께 작업하는 경우, 잘못된 상태 관리는 코드의 유지보수를 어렵게 만들 수 있습니다. 앞으로 우리는 React에서 제공하는 다양한 도구들을 사용하여 이러한 상태를 효율적으로 관리하는 방법을 배우게 될 것입니다. 상태 관리는 React 애플리케이션 개발에서 가장 중요한 개념 중 하나이므로, 이에 대한 확실한 이해가 필요합니다.

2. 상태관리의 기초 useState

2.1 상태값 변경과 화면 업데이트

변수의 값이 변경되더라도 React는 자동으로 화면을 업데이트하지 않습니다.

다음 예시를 통해 살펴보겠습니다. 좋아요 버튼을 클릭했을 때 좋아요의 수를 1씩 증가시키는 화면을 구현하고 싶습니다.

function LikeButton() {
  let like = 0;

  function handleLike() {
    like += 1;
    console.log('좋아요 수:', like); // 콘솔에는 증가된 값이 출력됨
  }

  return (
    <div>
      <button onClick={handleLike}>
        <svg
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
        >
          <path
            d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"
            strokeWidth="2"
            strokeLinecap="round"
            strokeLinejoin="round"
          />
        </svg>
        <span>{like}</span>
      </button>
    </div>
  );
}

하지만 이 코드에서 버튼을 클릭하면 like 변수는 증가하지만, 화면에는 변경된 값이 표시되지 않습니다. 개발자 도구의 콘솔에서만 값이 증가하는 것을 확인할 수 있습니다. 이는 React가 단순한 변수의 변경을 감지하지 않기 때문입니다.

모든 변수의 변경마다 컴포넌트를 업데이트한다면 성능상 문제가 발생할 수 있습니다. 따라서 React는 특별한 상태 관리 도구인 useState를 제공합니다.

html에서는 <button onclick="함수이름">으로 선언하던 것과 달리 리액트에서는 <button onClick={함수}>로 선언합니다.

2.2 useState 사용하기

useState는 React에서 제공하는 상태 관리 도구입니다. 컴포넌트에서 상태를 사용하려면 먼저 React에서 useState를 불러와야 합니다.

import { useState } from 'react';

useState는 두 가지 요소를 반환합니다.

  • 상태값을 저장하는 변수 (state)
  • 해당 상태를 변경하는 함수 (setState)
// useState의 기본 문법(구조 분해 할당 사용)
const [state, setState] = useState(초깃값);

// 구조 분해 할당을 사용하지 않은 경우
const useState = useState(초깃값);
const state = useState[0];
const setState = useState[1];

이제 useState를 사용하여 상태를 변경하도록 위 코드를 수정해보겠습니다.

import { useState } from 'react';

function LikeButton() {
  const [like, setLike] = useState(0);

  function handleLike() {
    setLike(like + 1);
  }

  return (
    <div>
      <button
        onClick={handleLike}
        style={{
          display: 'flex',
          alignItems: 'center',
          gap: '5px',
          border: 'none',
          background: 'none',
          cursor: 'pointer',
        }}
      >
        <svg
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill={like ? 'red' : 'none'}
          stroke={like ? 'red' : 'currentColor'}
        >
          <path
            d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"
            strokeWidth="2"
            strokeLinecap="round"
            strokeLinejoin="round"
          />
        </svg>
        <span>{like}</span>
      </button>
    </div>
  );
}
  1. 먼저 useState를 React에서 import 하기 위해 import 구문에 {useState}를 추가합니다.

  2. useState를 실행하면 state 변수와 state 변수의 상태를 바꿔줄 수 있는 함수가 반환됩니다. 그러면 구조 분해 할당 문법을 이용해 like와 setLike에 변수와 함수를 할당하게 됩니다. (꼭 구조분해할당을 사용할 필요는 없지만 관습으로 사용합니다.)

  3. useState의 괄호 안에는 초기값을 넣을 수 있습니다. 현재 코드에서는 0을 넣어 like의 초기값은 0이 되었습니다.

  4. svg에서 like가 truthy값인지의 여부에 따라 색상을 조건부렌더링 해주고있습니다.

  5. setState(예시에서는 setLike)를 통해 값이 변경될 때 리액트는 자동으로 해당 컴포넌트를 다시 랜더링해줍니다.

리액트는 어떤 상태(state)가 변경되면 그 부분을 다시 랜더링하는 특징이 있기 때문에 화면에서 계속 바뀌는 부분은 대부분 state를 사용합니다. 또한 다시 랜더링 할 필요가 없는 데이터는 useState를 사용하지 않음으로 자원을 아낄 수 있습니다.

setState는 호출 즉시 상태를 변경하는 것이 아니라, React에게 "이 상태를 변경해달라"고 요청하는 방식으로 동작합니다. React는 이 요청을 받아 적절한 시점에 리렌더링을 수행합니다.

앞서 살펴본 것처럼 React의 elements 즉, 가상돔은 자바스크립트 객체 형태입니다.

컴포넌트의 변화를 감지하고 변경사항을 화면에 반영하는 것은 많은 자원이 소모되는 작업일 수 있습니다. 때문에 리액트는 컴포넌트를 업데이트 해야할 때를 결정하는 특별한 메커니즘을 가지고 있습니다.

useState를 통해 상태를 변경하면, 리액트는 해당 컴포넌트의 재렌더링을 예약합니다. 재렌더링을 하게 될때 리액트는 useState 이전과 이후의 모습을 서로 비교합니다. 이를 ‘재조정(reconciliation)’을 거친다고 표현합니다.

재조정(reconciliation)이란?

reconciliation은 양자를 서로 비교 대조해서 차이를 없애고 같은 값이 되도록 하는 과정을 의미하는 회계 용어입니다. 혹은 종교에서의 고해성사를 의미하기도 합니다.

React의 재조정은 Virtual DOM을 사용하여 React 컴포넌트 트리를 비교하고 최적화하는 프로세스입니다. React는 컴포넌트가 업데이트될 때 마다 Virtual DOM을 사용하여 이전 버전의 컴포넌트 트리와 새 버전의 컴포넌트 트리를 비교합니다.

reconciliation 프로세스는 이전 버전의 컴포넌트 트리와 새 버전의 컴포넌트 트리를 비교하여 다음과 같은 작업을 수행합니다.

  1. 컴포넌트의 타입이 같은지 비교합니다.
  2. 컴포넌트의 속성(prop)이 변경되었는지 비교합니다.
  3. 컴포넌트의 자식 요소가 변경되었는지 비교합니다.

이러한 비교 작업을 통해 React는 변경된 부분만 업데이트하고, 나머지 부분은 그대로 유지합니다. 이렇게 하면 React는 DOM 조작을 최소화하고 더 빠르고 효율적인 애플리케이션을 만들 수 있습니다.

정리하자면, reconciliation 프로세스를 통해 React는 컴포넌트를 업데이트하여 즉각적으로 반응하는 사용자 인터페이스를 만들 수 있습니다. 이것이 바로 React가 다른 프론트엔드 라이브러리와 차별화되는 중요한 기능 중 하나입니다.

참고 | reconciliation

위에서 사용한 useState와 같은 메소드는 Hook 이라고 부릅니다. Hook이라는 개념은 리엑트 16.8 Version부터 나온 개념입니다. Hook은 접두사로 use가 붙으며, 함수형 컴포넌트에서 컴포넌트의 lifecycle과 state와 관련된 함수를 사용할 수 있게 해줍니다.

useState, useEffect, useMemo, useCallback, useRef 등 다양한 Hook이 있으며 추후에 더 자세히 배울 예정입니다.

2.3 함수형 업데이트

함수형 업데이트(Functional Update)는 setState를 호출할 때 새로운 값을 직접 전달하는 대신, 이전 상태값을 인자로 받아 새로운 상태를 반환하는 함수를 전달하는 방식입니다.

// 일반 업데이트
setLike(like + 1);

// 함수형 업데이트
setLike((prevLike) => prevLike + 1);

함수형 업데이트가 필요한 이유

React의 상태 업데이트는 비동기적으로 처리됩니다. 이는 성능 최적화를 위해 여러 상태 업데이트를 배치(batch)로 묶어서 한 번에 처리하기 때문입니다. 이러한 특성 때문에 다음과 같은 상황에서 예상치 못한 결과가 발생할 수 있습니다.

function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    // 일반 업데이트 - 문제 발생!
    setCount(count + 1); // count는 0이므로 0 + 1 = 1
    setCount(count + 1); // count는 여전히 0이므로 0 + 1 = 1
    setCount(count + 1); // count는 여전히 0이므로 0 + 1 = 1
    // 결과: count는 3이 아닌 1이 됨
  }

  return <button onClick={handleClick}>클릭: {count}</button>;
}

위 코드에서 버튼을 클릭하면 setCount가 세 번 호출되지만, 각 호출에서 사용하는 count 값은 모두 같은 값(0)입니다. React가 상태 업데이트를 배치로 처리하기 때문에 세 번 모두 같은 시점의 count 값을 참조하게 됩니다.

함수형 업데이트를 사용하면 이 문제를 해결할 수 있습니다.

function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    // 함수형 업데이트 - 올바르게 동작!
    setCount((prevCount) => prevCount + 1); // 0 + 1 = 1
    setCount((prevCount) => prevCount + 1); // 1 + 1 = 2
    setCount((prevCount) => prevCount + 1); // 2 + 1 = 3
    // 결과: count는 3이 됨
  }

  return <button onClick={handleClick}>클릭: {count}</button>;
}

함수형 업데이트를 사용하면 React가 가장 최신의 상태값을 함수의 인자로 전달해주므로, 여러 번의 업데이트가 순차적으로 올바르게 적용됩니다.

함수형 업데이트 사용 가이드

다음과 같은 경우에 함수형 업데이트를 사용하는 것이 좋습니다.

  1. 연속된 상태 업데이트: 같은 상태를 여러 번 연속으로 업데이트해야 할 때
  2. 이전 상태값 기반 계산: 새로운 상태가 이전 상태값을 기반으로 계산될 때
  3. 비동기 작업 내부: setTimeout, Promise 등 비동기 작업 내부에서 상태를 업데이트할 때

반면, 상태 업데이트가 이전 상태와 무관하고 단순한 경우에는 일반 업데이트를 사용해도 무방합니다.

// 이전 상태와 무관한 경우 - 일반 업데이트 사용 가능
const [name, setName] = useState('');
setName('새로운 이름'); // OK

// 이전 상태에 의존하는 경우 - 함수형 업데이트 권장
const [count, setCount] = useState(0);
setCount((prev) => prev + 1); // 권장

3. 실습

3.1 카운터 만들기

숫자를 증가, 감소시키는 간단한 카운터를 만들어보세요.

[요구사항]

  • useState를 사용하여 카운트 상태를 관리합니다
  • '+' 버튼 클릭 시 카운트가 1 증가합니다
  • '-' 버튼 클릭 시 카운트가 1 감소합니다
  • 카운트가 0일 때는 감소할 수 없습니다
  • 현재 카운트 값을 화면에 표시합니다

3.2 토글 버튼 만들기

다크모드/라이트모드를 전환하는 토글 버튼을 만들어보세요.

[요구사항]

  • useState를 사용하여 현재 테마 상태를 관리합니다
  • 버튼 클릭 시 다크모드와 라이트모드가 전환됩니다
  • 현재 테마에 따라 배경색과 글자색이 변경됩니다
    • 라이트모드: 흰색 배경, 검은색 글자
    • 다크모드: 검은색 배경, 흰색 글자
상태와 useState - React 에센셜 | 위니버시티