Notice
Recent Posts
Recent Comments
관리 메뉴

즐겁게, 코드

야, 너도 상태관리 할 수 있어 2편 : 리코일 사용하기 본문

🎨 프론트엔드/React.js

야, 너도 상태관리 할 수 있어 2편 : 리코일 사용하기

Chamming2 2021. 8. 25. 00:46

지난 글에서는 리코일이 무엇인지와 리코일과 기존 상태관리 방법의 차이를 간단하게 소개했는데요, 이번 글에서는 실제로 동작하는 여러 앱에서 리코일을 사용해 상태를 관리해보겠습니다.

리코일 설치하기

CRA로 프로젝트를 구성했다고 가정하고, 리코일을 설치해 보겠습니다.

// yarn
yarn add recoil

// npm
npm i recoil

리덕스를 사용할 때는 redux, react-redux, 경우에 따라서는 redux-thunk, redux-saga까지 설치해야 했지만, 이제는 리코일만 설치하면 됩니다!

리코일 루트 컴포넌트 정의하기

리코일을 사용하기 이전, 리코일로 전역 상태를 관리할 범위를 <RecoilRoot> 컴포넌트로 감싸줍니다.

Tip. <RecoilRoot>는 리덕스나 컨텍스트의 Provider와 같은 역할을 합니다.

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

// RecoilRoot 컴포넌트를 named import 로 불러옵니다.
import { RecoilRoot } from "recoil";

ReactDOM.render(
  <RecoilRoot>
    <App />
  </RecoilRoot>,
  document.getElementById("root")
);

아톰 사용하기

지난 글에서 리코일에서는 기본적으로 상태를 아톰(Atom) 이라는 단위로 관리한다고 소개했습니다.
아톰은 atom 이라는 함수로 정의합니다.

// store/counter.js

import { atom } from "recoil";

// 컴포넌트에서 아톰을 사용하기 위해 export를 사용해 내보냅니다.
export const countState = atom({
    key: "countState",
    default: 0
})

0을 기본값으로 갖는 countState 라는 전역 상태(아톰)가 새로 만들어진 모습입니다.

Tip. key 속성은 리액트가 해당 아톰을 식별하기 위한 ID의 역할을 합니다.
따라서 아톰들간 중복된 key 값이 존재해서는 안됩니다.

생성한 아톰은 컴포넌트에서 다음과 같이 불러와 사용할 수 있습니다.

// components/ControlNumber.js

import React from "react";
import { useRecoilState } from "recoil";
import { countState } from "../store/counter";

const ControlNumber = () => {

  // useRecoilState() 훅을 통해 방금 정의한 counterState 아톰을 사용합니다.
  // 보시면 useState 구문을 사용할 때와 매우 유사함을 알 수 있습니다!
  const [count, setCount] = useRecoilState(countState);
  
  const increase = () => {
    setCount(count + 1);
  };

  const decrease = () => {
    setCount(count - 1);
  };
  return (
    <div>
      <button onClick={increase}>+</button>
      <button onClick={decrease}>-</button>
    </div>
  );
};

export default ControlNumber;
// components/DisplayNumber.js

import React from "react";
import { useRecoilState } from "recoil";
import { countState } from "../store/counter";

const DisplayNumber = () => {
  const [count, _] = useRecoilState(countState);

  return (
    <div>
      현재 숫자 : <span>{count}</span>
    </div>
  );
};

export default DisplayNumber;

ControlNumberDisplayNumber 라는 두 컴포넌트에서 counterState 아톰의 값을 공유해 사용하는 모습입니다.

그뿐만이 아니라, 복잡한 리듀서 로직 없이 useRecoilState() 훅이 리턴한 업데이트 함수를 통해 useState() 를 사용하는 것처럼 간단히 전역 상태를 조작할 수 있습니다.

 

리덕스를 사용했다면 지금쯤 스토어를 만들고 한창 리듀서 로직을 작성하고 있었을 텐데, 벌써 끝나버린 모습이네요.

셀렉터 사용하기

셀렉터(selector)는 특정 아톰의 변화를 구독하면서, 해당 아톰을 원하는 대로 조작한 결과를 리턴하는 순수 함수입니다.
예를 들어, countState 아톰이 갖는 숫자 뒤에 "$" 라는 달러 기호가 붙도록 수정해 보겠습니다.

// store/counter.js

import { atom, selector } from "recoil";

export const countState = atom({
    key: "countState",
    default: 0
})

// 셀렉터 사용법
export const countWithUnitState = selector({
    get: ({ get }) => {
        // 인자로 주어진 get은 다른 아톰을 구독하는 역할을 합니다.
        // 따라서 countState 아톰의 값이 변경되면 count가 갖는 값도 재계산됩니다.
        const count = get(countState);

        // 이후, 변경된 아톰값에 "$" 기호를 붙인 결과를 리턴합니다.
        return count + "$";
    }
})

이를 컴포넌트에서 사용하려면 useRecoilValue() 훅을 사용합니다.

// components/DisplayNumber.js

import React from "react";
// useRecoilValue 훅을 불러옵니다.
import { useRecoilValue } from "recoil";
import { countWithUnitState } from "../store/counter";

const DisplayNumber = () => {
    // count 값은 countWithUnitState 셀렉터가 반환한 값이 됩니다.
    const count = useRecoilValue(countWithUnitState);

  return (
    <div>
      현재 숫자 : <span>{count}</span>
    </div>
  );
};

export default DisplayNumber;

전과 같이 아톰을 제어할 수는 있지만, 셀렉터를 통해 아톰값 뒤에 "$" 기호를 붙여준 모습입니다.

셀렉터에 파라미터 전달하기

selectorFamily 를 사용하면 셀렉터에 컴포넌트의 상태나 변수를 인자로 전달할 수도 있습니다.

예를 들어, 컴포넌트에서 특정 아이템의 아이디를 선택하면 해당 아이템을 셀렉터가 반환하도록 해보겠습니다.

 

먼저 아톰과 셀렉터를 다음과 같이 구성합니다.

// store/todo.js
import { atom, selectorFamily } from "recoil";

// 셀렉터가 일정 아이디의 변경을 감지할 수 있도록 아톰을 정의합니다.
export const taskIdState = atom({
  key: "taskId",
  default: 0,
});

export const todoState = atom({
  key: "todoState",
  default: [
    { id: 1, title: "새로운 할일 1" },
    { id: 2, title: "새로운 할일 2" },
    { id: 3, title: "새로운 할일 3" },
  ],
});

// selectorFamily는 셀렉터에 인자를 전달할 수 있게 해줍니다.
// 이 셀렉터는 컴포넌트가 아이디를 선택하면, 아이디에 맞는 일정 정보를 리턴합니다.
export const selectedTodoState = selectorFamily({
  get:
    (id) =>
    ({ get }) => {
      const todoList = get(todoState);
      return todoList[Number(id)];
    },
});

다음은 컴포넌트 부분입니다.
아톰으로 정의된 태스크의 아이디를 변경하면, 셀렉터는 변화를 감지하고 변경된 일정 정보를 리턴합니다.

// components/Todo.js

import React from "react";
import { useRecoilValue, useRecoilState } from "recoil";
import { taskIdState, selectedTodoState } from "../store/todoState";

const Todo = () => {
  const [taskId, setTaskId] = useRecoilState(taskIdState);
  const selectedTodo = useRecoilValue(selectedTodoState(taskId));

  const selectTodo = (e) => {
    setTaskId(e.target.value);
  };

  return (
    <div>
      <select name="taskId" id="taskId" onChange={selectTodo}>
        <option value="0">태스크 1</option>
        <option value="1">태스크 2</option>
        <option value="2">태스크 3</option>
      </select>
      <div>
        <h1>
          선택한 일정 : <span>{selectedTodo.title}</span>
        </h1>
      </div>
    </div>
  );
};

export default Todo;

잘 작동하는 모습입니다!

비동기적인 아톰값 관리하기

리덕스를 사용할 때는 redux-thunk 라는 추가적인 라이브러리를 사용해야 비동기적으로 상태를 제어할 수 있었는데요, 리코일에서는 과연 어떻게 상태를 제어할지 보겠습니다.

 

고객 ID를 입력하고, 서버로부터 해당 고객의 데이터를 불러와 아톰값을 갱신하는 예제를 만들어 보겠습니다.

먼저 다음과 같이 아톰을 정의합니다.

// store/guestState.js

import { atom } from "recoil";

export const guestListState = atom({
  key: "guestList",
  default: [],
});

다음은 고객 ID를 입력하는 컴포넌트입니다.
고객 ID를 입력하면 해당 고객의 정보를 불러오고, 불러온 데이터로 아톰을 업데이트하는 코드입니다.

// components/GuestBook.js

import React, { useState } from "react";
import { guestListState, updatedGuestListState } from "../store/guestState";
import { useRecoilValue, useRecoilState } from "recoil";
import GuestList from "./GuestList";

const GuestBook = () => {
  const [guestId, setGuestId] = useState(null);
  const [guestList, setGuestList] = useRecoilState(guestListState);

  const handleInput = (e) => {
    setGuestId(e.target.value);
  };

  // redux-thunk 등의 플러그인 없이도 간단히 비동기 상태값을 제어할 수 있습니다! 
  const addGuest = async () => {
    const res = await fetch(`https://jsonplaceholder.typicode.com/users/${guestId}`);
    const data = await res.json();
    setGuestList([...guestList, data]);
  };

  return (
    <div>
      <label htmlFor="search">추가할 고객 ID를 입력하세요.</label>
      <input type="text" id="search" onChange={handleInput} />
      <button onClick={addGuest}>추가</button>
      <GuestList />
    </div>
  );
};

export default GuestBook;
// components/GuestList.js

import React from "react";
import GuestInfo from "./GuestInfo";
import { useRecoilState } from "recoil";
import { guestListState } from "../store/guestState";

const GuestList = () => {
  const [guestList, _] = useRecoilState(guestListState);

  return (
    <ul>
      {guestList.map((item) => {
        const { id, name, username, email } = item;
        return <GuestInfo id={id} name={name} username={username} key={id} email={email} />;
      })}
    </ul>
  );
};

export default GuestList;
// components/GuestInfo.js

import React from "react";

const GuestInfo = ({ id, name, username, email }) => {
  return (
    <li>
      <h2>
        {name}
        <span
          style={{
            display: "inline-block",
            marginLeft: "10px",
            color: "#666",
            fontSize: "0.9rem",
          }}
        >
          ({username})
        </span>
      </h2>
      <h4>{email}</h4>
    </li>
  );
};

export default GuestInfo;

고객 목록은 컴포넌트간 공유할 수 있도록 전역으로 관리되는 상태이며, 의도대로 잘 동작하는 것을 확인할 수 있습니다.

마치며

이처럼 아톰을 사용하면 아톰셀렉터라는 개념만으로 정말 간단하게 전역 상태관리를 수행할 수 있습니다.

다만 "셀렉터에 파라미터 전달하기" 예제에서 보았듯 셀렉터가 아톰의 변화를 구독함에 따라 컴포넌트 내에서 관리해도 될 상태(태스크의 ID)를 아톰으로 관리해야 하는 불편함이 있기도 하고, 아직은 리덕스에 비해 다운로드 수도 현저히 적다는 문제가 남아있긴 합니다.

 

그래도 리코일은 리덕스와는 비교도 안될 정도로 쉽고, 컨텍스트보다 뛰어난 성능을 기대할 수 있다는 점에서 너무나도 매력적이라는 사실은 부정할 수 없을 듯 합니다.

 

언젠간 리코일이 리액트 생태계의 대세가 되는 날이 오길 기대하며, 리코일 소개를 마무리하도록 하겠습니다. 😁

반응형
Comments
소소한 팁 : 광고를 눌러주시면, 제가 뮤지컬을 마음껏 보러다닐 수 있어요!
와!! 바로 눌러야겠네요! 😆