[WEB] 프론트엔드

[React] 상태(State)에 대하여

[FE] Lighthouse 2024. 9. 19. 13:43

Overview

React는 다른 UI 프레임워크에 비해서 상태 관리가 복잡하고 어렵다는 이야기가 종종 들립니다. 이번 글에서는 상태에 대해 알아보고 상태 관리가 왜 복잡한지에 대해서 고민해보도록 하겠습니다.
 

왜 React의 상태 관리는 어려울까?

React의 상태는 랜더링과 직접적인 관계가 있습니다. 상태가 변경이 되면 해당 상태와 연관된 컴포넌트는 랜더링이 발생합니다.
그렇기 때문에 상태를 최적화 하는 것이 중요합니다. React의 상태를 잘 관리하는 것이 곧 컴포넌트 랜더링 최적화이기 때문입니다. (물론 랜더링 최적화 수단은 상태 관리 외 여러가지 것들이 있습니다.)
 

React의 데이터의 흐름

React의 데이터는 단방향으로 흐릅니다. 위에서 아래로, 부모 노드에서 자식 노드로 데이터가 흘러갑니다.
이는 React의 디자인과 관련이 있는데, 위에서 아래로 데이터를 흐르게 함으로써 컴포넌트를 순수하며 예측 가능하게 설계하고 디버깅이 용이하게 하기 위함입니다.
데이터가 위에서 아래로 흐르기 때문에 상태 또한 마찬가지로 위에서 아래로 흐릅니다. 부모 컴포넌트에서 소유하고 있는 상태는 props를 통해 자식 컴포넌트로 흘러갑니다.
종종 자식 컴포넌트에서 관리하는 상태를 부모 컴포넌트로 끌어 올리기도(리프팅) 하지만 React의 데이터는 위에서 아래로 흐르는 단방향 바인딩 입니다.
 

단방향 바인딩의 특징

선언적 프로그래밍

  • React는 선언적 프로그래밍 패러다임을 따릅니다. 즉, UI는 주어진 상태에 따라 어떻게 보일지를 설명하는 방식입니다. 단방향 데이터 흐름은 이 패러다임과 매우 잘 맞습니다. 상태(state)가 변경되면 해당 상태에 맞춰 UI가 자동으로 업데이트되므로 개발자는 데이터를 어디서 변경해야 할지 명확하게 이해할 수 있습니다.

UI , 데이터 일관성

  • 단방향 데이터 흐름은 데이터의 흐름을 한 방향으로 제한하기 때문에, 데이터의 변동과 그에 따른 UI 업데이트가 일관성 있게 유지됩니다.
  • 만약 데이터 흐름이 양방향이라면, 어떤 컴포넌트가 데이터를 변경했는지 파악하기 어려워질 수 있습니다. 이것은 특히 복잡한 애플리케이션에서 상태 불일치(state inconsistency)와 같은 문제를 야기할 수 있습니다.

유지 보수성 및 확장성

  • 단방향 데이터 흐름은 애플리케이션 구조가 더 깔끔하고 모듈화된 컴포넌트 설계를 가능하게 합니다. 각각의 컴포넌트가 자신이 받은 데이터(props)에만 의존하여 동작하기 때문에, 컴포넌트 간의 의존성을 최소화할 수 있습니다.
  • 또한 단방향 바인딩 디자인 패턴을 통해서 재사용 가능한 컴포넌트를 설계, 구현할 수 있습니다.
  • 이러한 구조는 코드베이스가 커지더라도 각 컴포넌트를 독립적으로 관리할 수 있게 해주며, 유지 보수와 확장이 용이합니다.

예측 가능성

  • React에서는 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달하는 방식으로 props를 사용합니다. 이는 데이터가 항상 부모에서 자식으로 흐르기 때문에, 데이터가 어떻게 전달되는지 추적하기가 쉽습니다.

 

상태(State)

React는 변할 수 있는 값을 상태라고 부릅니다. 사용자의 인터렉션, 시간의 흐름에 따른 값의 변화, 외부에 의한 변화 등 이러한 요인에 의해 변할 수 있는 값을 상태라고 정의하고 우리는 함수 컴포넌트에서 useState라는 특정 API를 사용해 값을 다루고 있습니다.
 
React의 state는 불변성을 유지해야 합니다. 이 말은 state의 Snapshot은 절대로 직접 변경할 수 없으며 state를 변경하고자 할 때는 Modifier 함수를 이용해야 한다는 것 입니다.
 
만약 state를 Modifier 함수를 이용하지 않고 직접 변경하려고 한다면 애플리케이션의 변화를 예상할 수 없게 됩니다. 또한 직접 값을 변경하려고 해도 컴파일 타임에 에러를 뱉게 됩니다.

 
useState를 통해 리턴 받는 Modifier 함수의 파라미터로는 변경될 값 또는 익명 함수를 전달할 수 있습니다. 익명 함수의 파라미터로는 변경할 이전 값이 제공이 되며 이를 활용해 순수 함수를 넘겨줄 수 있게 됩니다.
 
아래는 가장 쉽게 확인할 수 있는 counter 예제입니다.

import { useState } from "react";

const State = () => {
  const [counter, setCounter] = useState(0);

  const handleDecrease = () => {
    setCounter((prev) => prev - 1);
  };

  const handleIncrease = () => {
    setCounter((prev) => prev + 1);
  };
  return (
    <div>
      <button onClick={handleDecrease}>-</button>
      <p>{counter}</p>
      <button onClick={handleIncrease}>+</button>
    </div>
  );
};

export default State;

 
사용자 인터렉션 (버튼 클릭)을 통해서 카운터의 값을 변경하고 있습니다. 여기서 카운터는 변할 수 있는 값이기 때문에 상태로 관리하게 됩니다.
 

 
상태가 변경됨에 따라서 해당 상태를 구독 중인 컴포넌트(태그)가 변화하고 있습니다. 이는 상태가 변경됨에 따라서 랜더링이 발생하고 있는 것 입니다.
 

상태 최적화

위 예제를 통해 상태를 변경한다면 상태를 구독하고 있는 (상태 값을 사용하고 있는 것을 상태를 구독한다고 표현하겠습니다.) 컴포넌트에서는 랜더링이 발생한다는 것을 확인했습니다. 랜더링을 최적화 하기 위해서는 상태를 최적화 해야 한다는 사실도 알게 됐습니다. 상태를 최적화 하는 여러 방법을 살펴보겠습니다.

상태를 적절한 컴포넌트에서 소유하기

특정 상태를 어느 컴포넌트에서 관리해야 한다는 정답은 없습니다. 다만 React의 특성 상 자식 컴포넌트에서 상태를 사용해야 한다면 props로 내려줘야하고, props를 타고 내려간다면 props drilling 현상이 발생함으로 적당한 위치에서 state를 선언해야 합니다.
 
state를 props로 계속해서 내리다보면 상태가 변경될 때 디버깅이 어려울 뿐더러 어느 컴포넌트에서 변화가 생길 지 예측하기 어렵습니다.
 
또한 state를 자식 컴포넌트에서 잘못 소유하고 있다면 부모 컴포넌트에서 해당 state를 사용하기 어렵기 때문에 이 문제를 해결하기 위해 전역 상태 관리를 하게 됩니다. 단순히 이 문제만을 해결하기 위해 프로젝트에 전역 상태관리 도구를 도입하기에는 오버헤드가 크기 때문에 컴포넌트 구조를 재설계 하여 상태를 적절한 컴포넌트에서 관리하는 것으로 문제를 해결할 수 있게 됩니다.

단순하게 State 만들기

State는 정답이 존재하지 않습니다. 원시 값을 State로 다룰 수 있고 객체 형태의 값을 State로 다룰 수 있습니다. 만약 객체 형태의 값을 State로 다룬다면 해당 객체가 복잡하게 설계가 되어있는지 확인해봐야 합니다. 복잡한 형태의 State는 값을 변경하기 어렵습니다.
 
아래와 같은 타입을 가진 객체를 State로 다룬다고 가정합시다. 해당 객체는 info라는 프로퍼티에 name이라는 객체를 가지고 있고 name이라는 객체의 프로퍼티를 통해 값을 관리하고 있습니다.

type FormType = {
  id: string;
  pwd: string;
  phone: string;
  addr: string;
  info: {
    name: {
      last: string;
      first: string;
    };
  };
};

 
위 객체를 State로 관리한다면 last, first 값을 변경하기 위해서는 다음과 같은 로직을 작성해야 합니다.

const Form = () => {
  const [form, setForm] = useState<FormType>(initValue);

  const handleChangeName = (event: SyntheticEvent<HTMLInputElement>) => {
    const id = event.currentTarget.id;
    const value = event.currentTarget.value;
    setForm((prev) => ({
      ...prev,
      info: {
        ...prev.info,
        name: {
          ...prev.info.name,
          ...(id === "last"
            ? {
                last: value,
              }
            : {
                first: value,
              }),
        },
      },
    }));
  };
  
  ... 생략
}

 
React의 State는 불변성을 유지해야 하기 때문에 복잡한 구조의 객체를 다루기 위해서는 복잡한 과정을 거쳐야 합니다.
immer라는 도구를 활용해 위 과정을 조금 더 쉽게 해결 할 수 있겠지만, 이 또한 문제를 해결하기 위해 도구를 도입해야 하기 때문에 관리해야 할 의존성이 추가된다는 오버헤드가 발생합니다. 그렇기 때문에 State는 가급적 단순하게 관리해 다루기 편리하게 최적화 하는 것이 좋습니다.

State 중복 피하기

같은 역할이지만 다른 명칭을 가진 여러 개의 State가 존재할 수 있습니다. 해당 State들은 추상화를 통해 상태를 일원화 할 수 있기 때문에 제거해주도록 합시다.
마찬가지로 여러 개로 작성된 State가 값이 변할 때 함께 변경되는 케이스가 존재할 수 있습니다. 해당 State들은 같은 역할을 하는 State일 수 있으니 통합을 고려해볼 수 있습니다.

이외에 State 구조에 관한 내용은 다음의 공식 문서를 살펴봐 주세요.

공식 문서 살펴보기

State 구조 선택하기 – React

The library for web and native user interfaces

ko.react.dev

 

다양한 상태 관리 도구

마지막으로 다양한 상태 관리 도구에 대해서 간단하게 살펴봅시다.
useState를 통해서 컴포넌트에서 직접 State를 소유하고 관리할 수 있습니다. 하지만 props drilling 문제가 발생하거나 여러 컴포넌트에서 하나의 상태를 공유해서 사용해야 한다면 전역 상태 관리 도구의 도입을 고려해볼 수 있습니다.

Redux

Redux는 가장 많이 사용중인 상태관리 라이브러리 입니다. Flux 패턴으로 상태를 관리하며 이는 위에서 아래로 데이터가 흐르는 React의 디자인과 상당히 잘 어울리는 라이브러리 입니다.
비동기 데이터를 관리하기 위해서는 Redux-thunk나 Redux-saga와 같은 미들웨어를 추가로 도입해야 한다는 단점이 있습니다. 그리고 Flux패턴을 활용한 상태관리에 미들웨어까지 더해져 상태 관리를 위해 상당한 보일러 플레이트를 생성, 관리 해야 한다는 점과 가파른 러닝 커브가 존재한다는 점이 존재해 요즘은 Redux를 벗어나고 있는 추세를 보이고 있습니다.

복잡하고 어려운 Redux 적응기

복잡하고 어려운 Redux 적응기 - 오픈소스컨설팅 테크블로그 %

안녕하세요 오픈소스컨설팅 Playce Dev 팀 Front-end 개발자 이정현입니다!이번에는 React 프로젝트를 개발하며, 규모가 확장됨에 따라 많아지는 데이터들을 어떻게 효율적으로 관리할지, 상태 관리를

tech.osci.kr

 

Zustand

Zustand는 Redux와 비슷한 중앙 집중식 상태 관리 아키텍처를 택하고 있습니다. 하지만 Redux와 다르게 Provider을 제공하지 않으며 더욱 간편하게 상태를 관리할 수 있다는 장점이 있습니다.
immer, persist 와 같은 유용한 미들웨어도 내장하고 있기 때문에 최근 시작된 프로젝트에서 많이 도입하고 있는 추세입니다.

Hello Zustand

Hello Zustand!

Overview프론트엔드에서 상태관리는 중요합니다. 핵심적으로 상태를 통해서 변하는 값에 따라서 UI 를 동적으로 다룰수 있기 때문입니다.React 에서는 기본적으로 useState 라는 훅을 사용해 상태를

kangs-develop.tistory.com

 

Recoil

Atom을 활용해 바텀업 패턴으로 상태를 관리하는 라이브러리 입니다. Meta에서 출시한 오픈소스 라이브러리 이지만 23년 4월 12일 이후로 유지 보수가 멈춘 상태입니다.
라이브러리 생명주기가 멈췄기 때문에 바텀-업 패턴의 상태관리 라이브러리를 찾는다면 Jotai가 좋은 선택일 것 같습니다.

Recoil, 리액트의 상태관리 라이브러리

Recoil, 리액트의 상태관리 라이브러리 - 오픈소스컨설팅 테크블로그 %

Recoil 만을 위한 글이지만, 해당 기술을 탐구하기 전에 같은 문제를 해결하기 위해 사용되고 있는 라이브러리와 비교를 하는것은 상당히 중요한 일이라고 생각합니다.Frontend 개발을 하면서 state

tech.osci.kr

 

Jotai

Jotai는 Atom이란 원자적 상태를 활용해 상태를 바텀업 패턴으로 관리할 수 있습니다. 작은 원자단위로 정의한 상태를 컴포넌트에서 구독해 사용하는 방식으로 정말 간단하게 상태 관리를 사용할 수 있습니다.
Recoil과 같은 컨셉의 라이브러리지만 Recoil의 유지 보수가 멈췄다는 점으로 인해 수혜를 받고 있는 라이브러리 입니다. (단순히 Recoil의 대안이라는 점으로 급부상 한 것은 아닙니다.)
 

@tanstack/react-query

서버에서 받아오는 데이터를 관리할 수 있는 라이브러리 입니다. 요즘은 React에서 사용하는 상태를 클라이언트 상태와 서버 상태로 구분하고는 하는데 구분의 시작점이 된 라이브러리가 react-query 라이브러리 입니다. 단순히 서버 패칭만을 쉽게 도와주는 라이브러리의 범주에서 벗어나 서버 상태라는 개념을 통해 네트워크 요청 최적화, 캐싱, 네트워크 로직 추상화 등 다양한 기능을 제공해주고 있습니다.

React-Query 도입을 위한 고민 (feat. Recoil)

React-Query 도입을 위한 고민 (feat. Recoil) - 오픈소스컨설팅 테크블로그 - 강동희

Web Frontend 개발을 할 때 React 를 사용하면서 마주하게 되는 여러 가지 문제점 중 하나는 state, 상태 관리에 관한 부분입니다. 프론트엔드 개발자라면 state 와 뗄 수 없는 인연을 맺고 있습니다.오늘

tech.osci.kr

 

마치며

React의 상태 관리가 왜 어려울까 생각해보면 결국은 랜더링과 직접적인 연관 관계가 있기 때문이지 않을까라고 생각하고 있습니다.
 
프론트엔드에서 성능 최적화를 위해서는 불필요한 랜더링은 반드시 제거해야 합니다. 그렇기 때문에 상태를 보다 더 엄격하게 관리해야 하고, 데이터가 단방향으로 위에서 아래로 흐르는 React의 특성 상 상태 관리를 위해서는 여러가지 기술을 추가로 학습해야 한다는 점이 상태 관리를 더욱 어렵게 하는 것 같습니다.
 
이번 글을 통해서 간단하게 상태 관리에 대해서 알아보았고 상태에 대해서 조금 더 깊게 생각해볼 수 있었습니다. 읽어봐주셔서 감사합니다.