ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 전역상태관리 Recoil (feat.ContextAPI) 아티클
    React/Recoil 2023. 5. 2. 21:05

    0. 본격적인 Recoil 소개에 앞서 Recoil, ContextAPI 같은 전역상태관리 툴을 왜 사용하는 걸까?

    프론트엔드에서 상태(State)란 컴포넌트 내부에서 관리되며 웹앱의 렌더링에 영향을 미치는 객체이다.

    처음 리액트 앱을 npm에서 실행하면 couter가 시작되는데 여기서 

    + 를 누르면 증가하고 -를 누르면 감소하는 그 "숫자" 가 바로 상태의 형태로 관리된다는 말이다.

     

    상태의 종류에는 총 3가지로 나눌 수 있는데

     

    지역상태 : 하나의 컴포넌트 내부에서만 관리되는 상태로 , 해당 상태를 다른 컴포넌트들과 공유하지 않는다.

    컴포넌트 간 상태 : 하나의 상태를 여러가지 컴포넌트에서 사용되고 변경되며 관리되는 모습을 보여준다.

    이러한 상호작용을 위해 바로 "Prop Drilling" 의 방식을 사용한다.

    전역상태 : 하나의 상태가 전체 프로젝트, 혹은 컴포넌트 전체에 영향을 미치는 상태이다.

    이또한 "Prop Drilling"을 통해 상위 컴포넌트에서 하위 컴포넌트로 상태를 전달한다.

     

    ✔️ Prop Drilling 이 잘 이해가 안된다면 아래 예시코드 보기!

     

    value="Hello World" 를 RoomFour 까지 보내주기 위해 {value} 를 각 컴포넌트에 매번 할당해 주었다....!

    function App() {
      return <ArtificialDom value="Hello World!" />;
    }
    
    function ArtificialDom({ value }) {
      return <RoomOne value={value} />;
    }
    
    function RoomOne({ value }) {
      return <RoomTwo value={value} />;
    }
    
    function RoomTwo({ value }) {
      return <RoomThree value={value} />;
    }
    
    function RoomThree({ value }) {
      return <RoomFour value={value} />;
    }
    
    function RoomFour({ value }) {
      return <div>Received: {value}</div>;
    }

    위와 같은 상황 (사실 위의 상황은 어쩌면 우리가 마주한 문제보다 쉬운 상황일지 모른다...) 이 발생하게 되면

    우린 끝없는 혹은 번잡한 Prop drilling 을 하고 있으며 이는 코드의 가독성과 관리 유지보수 사용 모든 측면에서

    우릴 불편하게 만든다.

     

    https://www.youtube.com/shorts/u86KSUc9Ngg

     
    (코딩애플님의 유명한 prop drilling shorts)

    따라서 value 라는 값을 전역으로 선언하여

    value 값을 그 어떤 컴포넌트에서도 쉽게 접근 수정할 수 있도록 하면

    전역상태 툴을 사용한 효율적인 상태관리가 가능해지며 상태가 어디서 수정되었는지

    버그는 어디에서 발생했는지 추적이 훨씬 더 쉽게 이루어질 수 있다.

     

    더불어 전역상태 툴을 사용하면 코드의 재사용성과 유연성이 증가한다.

    이는 상태를 사용하거나 수정하는 로직이 컴포넌트 밖에 위치함으로

    해당 로직의 재사용성이 높아지고 묘듈화를 가능케 하기 때문이다.

     

    1. ContextAPI 의 소개와 간단한 사용법 

     

    Context API는 리액트에서 전역 상태를 공유하기 위한 도구로, 컴포넌트 트리 안에서 종속성을 주입하는 역할을 한다.

    상태관리를 위한 도구는 useState, useReducer 등이 있으며, Context API는 이러한 상태들을 직접적으로 관리하지 않고,

    이미 존재하는 상태를 다른 컴포넌트와 공유할 수 있게 해주는 역할을 한다.

     

    이러한 특징 때문에 테마나 언어 등 전역적으로 사용되는 데이터를 다룰 때 자주 사용된다.

     

    하지만 Context 는 이러한 상태들을 직접적으로 관리해주지 않고,

    단순히 이미 존재하는 상태를 다른 컴포넌트들과 쉽게 공유할 수 있게 해주는 역할을 한다.

     

    Context API는 Context, Provider, Consumer 로 이루어져 있으며

    Context 내부에 Provider와 Consumer가 정의되어있다.

     Provider는 전역 상태를 제공하는 역할을 하며 Context에 상태를 제공해서

    다른 컴포넌트가 상태에 접근할 수 있도록 도와준다.

    제공된 상태에 접근하기 위해서는 Provider 하위에 컴포넌트가 포함되어있어야한다.

     

    보통 모든 컴포넌트에 접근해야하는 상태를 제공하기 위해서는

    Root Component (index.js / app.js)에서 Provider를 정의한다.

    Consumer는 제공받은 전역 상태를 받아서 사용하는 역할을 한다.

     

    이런 Context API는, 그 범위를 제대로 지정하지 않는다면, 불필요한 rerender를 일으키기도 한다.

    각 Context를 잘 나누고 또 필요하다면 useMemo 등을 사용해 코드를 작성하는 것이 좋다.

    하지만 useMemo 또한 연산이 들어가는 작업이기 때문에, 정말 필요한 곳에 잘 사용하는 것이 중요하다.

    곧 살펴볼 Recoil (+Redux)도 코드를 뜯어보면 Context API를 기반으로 만들어져있다고 한다.

     

     

    ContextAPI 사용법

    import React, { createContext, useContext } from 'react';
    
    const MyContext = createContext();
    
    function MyComponent() {
      return (
        <MyContext.Provider value="Hello World">
          <ChildComponent />
        </MyContext.Provider>
      );
    }
    
    function ChildComponent() {
      const myData = useContext(MyContext);
      return <div>{myData}</div>;
    }

     

    2. 이제 본격적으로 Recoil 에 대해서 알아봅시다.

    기본적으로 호환성 및 단순함을 이유로 한다면 외부의 상태관리 라이브러리보다는

    React prop drilling 혹은 자체 내장된 상태관리기능인 ContextAPI를 이용하는 것이 좋아보일 수 있다.

     

    그러나 prop drilling 은 컴포넌트 최상단까지 끌어올려야 공유할 수 있으며 이 과정에서 다시 거대한 트리가 랜더링 되는 문제를 야기한다. 또한 ContextAPI 는 단일 값만 저장할 수 있어 비효율적인 렌더링 발생의 여지가 있다.

    // ContextAPI의 한계 
    // value 하나가 변경되면 컴포넌트 전체렌더링이 되는 문제가 발생
    
    import React, { createContext } from 'react';
    
    const MyContext = createContext({});
    
    const App = () => {
      const myState = {
        value1: 'Hello',
        value2: 'World'
      };
    
      return (
        <MyContext.Provider value={myState}>
          <MyComponent />
        </MyContext.Provider>
      );
    };
    
    const MyComponent = () => {
      const { value1, value2 } = useContext(MyContext);
    
      return (
        <div>
          {value1} {value2}
        </div>
      );
    };

    이러한 문제점들로 인해 전역으로 상태를 관리하는 것이 어려워 졌다.

     

    그래서 Recoil 을 사용하자!

     

    Recoil은 복잡한 상태 관리를 간편하게 할 수 있는 라이브러리인데, 이는 상태들을 계층적인 구조로 관리한다.

    이 상태들은 "atoms"라 불리는 뿌리 역할을 하는 작은 단위로 분리되어 있으며,

    "selectors" 함수를 이용하여 컴포넌트로 전달될 수 있다.

    이러한 방식은 단순하면서 확장성이 뛰어나 컴포넌트 간, 서로 영향을 미치지 않고도 독립적으로 상태를 관리할 수 있다.

    Recoil은 React에서 공유상태를 간단한 get/set 인터페이스로 사용할 수 있게 해주는 API이라고 이해하면 좀 더 쉽다.

    더불어 Recoil 에 정의된 상태는 상태를 사용하는 컴포넌트를 수정하지 않고도 상태를 파생된 데이터로 대체할 수 있으며 더불어 동기식 혹은 비동기적으로 해당 상태를 업데이트 할 수 있다.

     

    Recoil 의 기본 구성요소는 2가지가 있는데 , atom과 selector 이다. 

    쉬운 이해를 위해 직접 사용해보는 코드를 보자.

     

    ✔️ atom 사용 및 수정 이용법

     

    컨셉 :  인공돔 안에 밤과 낮을 바꿀 수 있는 스위치가 있고

    모든 컴포넌트는 인공돔이 현재 밤인지 낮인지를 Recoil 에 저장된 isNight 를 통해 확인 할 수 있다.

     

    전역상태 isNight 를 통해 인공돔의 낮과 밤을 지정해보자 : 낮상태

     

    전역상태 isNight 를 통해 인공돔의 낮과 밤을 지정해보자 : 밤상태

     

    - 전역으로 밤인지 낮인지 알 수 있는 atom

    //atoms.ts
    export const isNightAtom = atom({
      key: 'isNight',
      default: false,
    });

    - 인공돔(ArtificialDom)이 현재 밤인지 낮인지 출력하는 컴포넌트

    // ArtificialDom.tsx
    import { useRecoilValue } from 'recoil';
    import { isNightAtom } from './atoms';
    
    function ArtificialDom() {
      const isNight = useRecoilValue(isNightAtom);
    
      return <div> isNight : {isNight ? night : day}</div>; 
      //현재 atom 의 값이 false => day 출력 
    }

    - 인공돔(ArtificialDom)의 밤과 낮을 변경할 수 있는 버튼 컴포넌트

    // ChangeButton.tsx
    import { useSetRecoilState } from 'recoil';
    import { isNightAtom } from './atoms';
    
    function ChangeButton() {
      const setNightAtom = useSetRecoilState(isNightAtom);
    
      const change = () => setNightAtom((prev) => !prev);
    
      return (
        <div>
          <button onClick={change}>Change night or day</button>
        </div>
      );
    }

    ✔️ selectors 이용법

    //atom.ts
    import {atom, selector, useRecoilValue} from 'recoil';
    
    const namesState = atom({
      key: 'namesState',
      default: ['', 'Ella', 'Chris', '', 'Paul'],
    });
    
    const filteredNamesState = selector({
      key: 'filteredNamesState',
      get: ({get}) => get(namesState).filter((str) => str !== ''),
    });
    
    
    //NameDisplay.tsx
    function NameDisplay() {
      const names = useRecoilValue(namesState);
      const filteredNames = useRecoilValue(filteredNamesState);
    
      return (
        <>
          Original names: {names.join(',')}
          <br />
          Filtered names: {filteredNames.join(',')}
        </>
      );
    }

     

    selector 를 이용하여 비동기적으로 데이터를 가져오고 싶다면 어떻게 해야할까?

    Synchronous Example (동기 예제)

    user 이름을 얻기 위한 동기적 atom selector 예제

    const currentUserIDState = atom({
      key: 'CurrentUserID',
      default: 1,
    });
    
    const currentUserNameState = selector({
      key: 'CurrentUserName',
      get: ({get}) => {
        return tableOfUsers[get(currentUserIDState)].name;
      },
    });
    
    function CurrentUserInfo() {
      const userName = useRecoilValue(currentUserNameState);
      return <div>{userName}</div>;
    }
    
    function MyApp() {
      return (
        <RecoilRoot>
          <CurrentUserInfo />
        </RecoilRoot>
      );
    }

    Asynchronous Example (비동기 예제)

    만약 user의 이름이 데이터베이스에 저장되어 있었다면, 

    Promise를 리턴하거나 혹은 async 함수를 사용하기만 하면 된다.

    의존성에 하나라도 변경점이 생긴다면, selector는 다시 실행시키게 된다.

    const currentUserNameQuery = selector({
      key: 'CurrentUserName',
      get: async ({get}) => {
        const response = await myDBQuery({
          userID: get(currentUserIDState),
        });
        return response.name;
      },
    });
    
    function CurrentUserInfo() {
      const userName = useRecoilValue(currentUserNameQuery);
      return <div>{userName}</div>;
    }

    그렇다면 비동기적으로 처리될 동안 화면에는 대신 띄워둘 수있는 무언가가 있어야 하는데 어떻게 처리해야할까?

    React Suspense 를 함께 사용하면 된다.

    function MyApp() {
      return (
        <RecoilRoot>
          <React.Suspense fallback={<div>Loading...</div>}>
            <CurrentUserInfo />
          </React.Suspense>
        </RecoilRoot>
      );
    }

     

    Redux, Recoil, ReactQuery 언제 사용하면 좋을까?

    3 라이브러리의 공통점은 모두 상태관리를 가능케 한다는 점이다.

     

    지난스터디 시간에 배웠듯

    React Query 는 비동기 데이터 요청을 다루는 데 적합하다.

    서버에서 데이터를 가져와 캐싱하고 , 로딩과 에러처등을 관리하기에 유용하다.

     

    Recoil 과 Redux 의 경우 하는일은 비슷하기는 하나

    기본적으로 Redux 는 대규모 , Recoil 은 중소 규모에 적합하다고 한다.

     

    아무래도 이러한 말들을 하는 이유는

    Redux 의 경우 디버깅과 모니터링에 유용한 도구들을 갖추고 있어

    조금 더 세밀하고 디테일한 작업들을 가능케 하는 높은 성능을 제공하기 때문이다.

     

    반대로 비록 기능은 좀 적지만 React와 비슷하게 함수를 부르고 수정하는 모습을 보이는 Recoil 은

    조금 단순하지만 사용법이 쉬운 전역상태관리라는 강력한 기능을 제공하기 때문이라고 생각한다.

     

    + 조금 더 많은 정보는 Recoil 공식문서를 참고해주세요!

     

    -주 참고자료-

    https://mingule.tistory.com/74

    https://velog.io/@velopert/react-context-tutorial

    https://recoiljs.org/ko/

     

     

     

     

     

    댓글

Designed by Tistory.