전역 상태 관리란?

yhyem·2024년 3월 9일

일반적으로 React에서는 데이터는 부모로부터 props를 통해 전달된다.

그러나 컴포넌트의 깊이가 깊어지면 props-drilling 현상이 나타나기도 한다.

props-drillingprops를 오로지 하위 컴포넌트로 전달하는 용도로만 쓰이는 컴포넌트들을 거치면서 React Component 트리의 한 부분에서 다른 부분으로 데이터를 전달하는 과정이다.

React에서의 상태 관리?

  • 기본적으로는 부모의 state를 자식에게 props로 전달해서 사용하고 있다
    • 만약 깊이가 깊어진다면? 5개 밑에 있는 자식에게 state를 전달해야 한다면?
    • 최상위 컴포넌트에서 모든 상태를 정의 및 관리할 수 있을까?

→ 이러한 이유로 전역 상태 관리를 사용합니다 (ex. recoil, context api)

  • 상태를 저장하는 저장소가 없다면 각 상태에 대해서 직접적으로 접근해줘야 한다.
    • F 컴포넌트에 접근할 경우 : Root → A → B → F
    • G 컴포넌트에 접근할 경우 : Root → A → B → E → G
    • J 컴포넌트에 접근할 경우 : Root → H → J

Recoil

Recoil의 Motivation

💡 기존 Context API가 가지는 문제점이자 Recoil이 해결하고자 하는 문제를 보자.

컴포넌트의 상태는 공통된 상위 요소까지 끌어올려야만 공유될 수 있으며,
이 과정에서 거대한 트리가 다시 렌더링되는 효과를 야기하기도 한다.

예제를 보자.

import { createContext, useState } from "react";
import ContextChild1 from "./context/ContextChild1";
import ContextChild2 from "./context/ContextChild2";
import ContextChild3 from "./context/ContextChild3";

import RecoilChild1 from "./recoil/RecoilChild1";
import RecoilChild2 from "./recoil/RecoilChild2";
import RecoilChild3 from "./recoil/RecoilChild3";

export const ColorContext = createContext({
  color: "red",
  setColor: () => {},
	state:''
});

function App() {
  const [color, setColor] = useState("red");
  const colorValue = { color, setColor, state: "state value" };

  return (
    <>
      <ColorContext.Provider value={colorValue}>
        <ContextChild1 />
        <ContextChild2 />
        <ContextChild3 />
      </ColorContext.Provider>
    </>
  );
}

export default App;

ContextChild3 컴포넌트는 Context를 사용하지 않는다.

단지 Provider의 자식 컴포넌트라는 이유만으로 리렌더링되는 것이다.

이를 해결하기 위해서는 ContextChild3가 리렌더링 할 필요가 없다는 것을 알려줘야 한다.

React.memo 메소드로 컴포넌트를 매핑해준다.

export default memo(ContextChild3);

Context를 사용하지 않는 ContextChild3는 리렌더링이 되지 않는 것을 볼 수 있다.

Memoizing

변경 값이 없을 경우 캐싱된 값을 재사용할 수 있게 하는 기법

Context는 단일 값만 저장할 수 있으며, 자체 소비자(consumer)를 가지는 여러 값들의 집합을 담을 수는 없다.

여기서 자체 소비자(consumer)라는 개념은 Provider가 제공하는 상태를 소비하는 주체이다.

예제를 보자.

export const SomeContext = createContext({
  color: "red",
  setColor: () => {},
  state: "",
});

위 Context는 color에 관한 값과 color와는 무관한 state라는 값을 제공하고자 한다.

위에서 말한 자체 소비자를 가진다는 것은

state라는 값만을 사용할 수 있는 주체가 있을 수 있는가라는 것이다.

import React, { memo, useContext, useEffect } from "react";
import { ColorContext, StateContext } from "../App";

const ContextChild2 = () => {
  useEffect(() => {
    console.log("ContextChild2 render!");
  });
  const { state } = useContext(SomeContext);
  return <div>{state}</div>;
};

export default memo(ContextChild2);

하지만 ContextChild2는 SomeContext에서 state 값 만을 가져왔을 뿐이고,

state 값 만을 소비하는 주체가 아니다.

만약 state 값 만을 소비했다면 state값의 변화는 없었으니 리렌더링 되지 말았어야 했다.

이를 해결하기 위해서는 state값만을 제공하는 Provider를 별도로 생성해야 한다.

//App.js
import { createContext, useMemo, useState } from "react";
import ContextChild1 from "./context/ContextChild1";
import ContextChild2 from "./context/ContextChild2";
import ContextChild3 from "./context/ContextChild3";

export const ColorContext = createContext({
  color: "red",
  setColor: () => {},
});

export const StateContext = createContext("");

function App() {
  const [color, setColor] = useState("red");
  const colorValue = { color, setColor };

  return (
    <>
      <ColorContext.Provider value={colorValue}>
        <StateContext.Provider value="state value">
          <ContextChild1 />
          <ContextChild2 />
          <ContextChild3 />
        </StateContext.Provider>
      </ColorContext.Provider>
    </>
  );
}

export default App;
//ContextChild2
import React, { memo, useContext, useEffect } from "react";
import { StateContext } from "../App";

const ContextChild2 = () => {
  useEffect(() => {
    console.log("ContextChild2 render!");
  });
  const { state } = useContext(StateContext);
  return <div>{state}</div>;
};

export default memo(ContextChild2);

Context를 분리해서 제공함으로써 불필요한 리렌더링을 막을 수 있었다.

위와 같이 Context를 사용하면 렌더링 최적화가 까다로운 경우가 많다.

Recoil은 위 문제들을 쉽게 해결해준다.

기대하는 Recoil의 기능

  1. Provider의 자식 컴포넌트라도 변경되는 값이 없다면 리렌더링되지 않아야 한다.
  2. Provider 하나에서 여러 자체 소비자(Cosumer)를 가질 수 있어야 한다.
    1. 자체 소비자는 다른 소비자들과 별개로 동작해야 한다.

RecoilRoot

Context의 Provider 역할을 하는 루트 컴포넌트로 App 컴포넌트를 감싸준다.

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { RecoilRoot } from "recoil";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <RecoilRoot>
    <App />
  </RecoilRoot>
);

Atom

Atom은 상태의 일부이다.
Atom 값을 구독함으로써 상태를 읽고 수정할 수 있다.

  1. atom 생성
import { atom } from "recoil";

export const colorAtom = atom({
  key: "colorAtomKey",
  default: "red",
});
import { atom } from "recoil";

export const stateAtom = atom({
  key: "stateAtomKey",
  default: "staet value",
});
  • atom 메소드를 이용해 해당 atom 의 key, 기본 값을 지정할 수 있다.
  1. atom 읽기
import React, { useEffect } from "react";
import { useRecoilValue } from "recoil";
import { stateAtom } from "../atom/stateAtom";

const RecoilChild2 = () => {
  useEffect(() => {
    console.log("RecoilChild2 render!");
  });
  const state = useRecoilValue(stateAtom);

  return (
    <>
      <div>{state}</div>
    </>
  );
};

export default RecoilChild2;
  • useRecoilValue 메소드에 생성한 atom을 전달함으로써 상태 값을 읽을 수 있다.
  1. atom 수정
import React, { useEffect } from "react";
import { useRecoilState } from "recoil";
import { colorAtom } from "../atom/colorAtom";

const RecoilChild1 = () => {
  useEffect(() => {
    console.log("RecoilChild1 render!");
  });

  const [color, setColor] = useRecoilState(colorAtom);

  return (
    <>
      <div style={{ color }}>{color}</div>
      <button onClick={() => setColor("blue")}>change color to blue</button>
      <button onClick={() => setColor("red")}>change color to red</button>
    </>
  );
};

export default RecoilChild1;
  • useRecoilState 메소드를 이용해 useState처럼
    recoil의 값을 읽고 수정할 수 있는 수정 함수를 반환 받을 수 있다.

먼저 확인해 볼 것은

color 값을 수정했을 때 RecoilChild3가 리렌더링 되는가?

이다.

색을 변경 시켜도 colorAtom값을 구독하는 RecoilChild1만 리렌더링 되고 있다.

이는

여러 자체 소비자(Consumer)를 가지는가?

에 대한 의문도 해결해 준다.

자체 소비자를 가지지 않는다면

색을 바꿨을 때 stateAtom을 구독하는 RecoilChild2 또한 리렌더링 됐어야 한다.

하지만 각각 자체적인 소비자를 가지기 때문에 서로 영향을 주지 않는 것이다.


실습

  • 영화 웹 초기 메인페이지에서 아래 API를 받아서 atom에 저장

https://developer.themoviedb.org/reference/movie-top-rated-list

  • 영화 상세 페이지에서 비슷한 작품 대신 ‘추천작’ 이라는 섹션으로 atom 의 영화들 렌더링

0개의 댓글