JavaScript/Recoil

상태 관리를 위해 Redux 대신 Recoil을 사용하기

제이널 2022. 7. 22. 13:26

이 글은 아래 링크의 글을 번역해 작성했습니다.

https://blog.openreplay.com/using-recoil-instead-of-redux-for-state-management-in-react-applications

 

----

 

 

React 앱에서 사용되는 모든 상태 관리 라이브러리들 중, Redux는 React의 Context API에 앞서 가장 인기가 많습니다. React 앱에서 사용되는 또 다른 훌륭한 라이브러리들이 있는데, 그 중 하나가 Recoil입니다. Recoil은 Redux와는 달리 구축하기가 매우 쉽고, 심지어 새로운 Redux 툴킷 라이브러리 보다 더 쉽습니다. 이 글에선, React 앱에 Redux 대신 Recoil을 사용해 상태를 관리하는 방법에 대해 알아보겠습니다.

 

Recoil이란?

Recoil은 페이스북에서 개발한 오픈 소스 상태 관리 라이브러리입니다. 

전역 상태를 제공해주기 때문에 모든 컴포넌트에서 상태를 쉽게 공유할 수 있고, Redux와 비교해 보일러 플레이트 코드를 작성할 필요가 없습니다.

 

공식 문서에 따르면, Recoil의 두 가지 핵심 개념은 아래와 같습니다.

  1. Atoms : Recoil이 제공하는 전역 상태 단위입니다. 컴포넌트들은 Atom에 접근할 수 있고, 변화를 구독할 수 있습니다.
  2. Selectors : 동기/비동기적으로 변경할 수 있는 상태(State)이며, 컴포넌트가 접근하고 구독할 수 있습니다.

 

왜 Recoil을 사용해야 할까?

수많은 상태 관리 라이브러리들, 그리고 React의 Context API를 고려해봤을 때, 왜 Recoil을 사용해야 할까요?

몇 가지 이유를 강조해 보겠습니다.

  • Recoil은 Redux와 같이 전역 상태를 제공합니다. Recoil에선, 상태를 다른 자식 컴포넌트에게 prop으로 전달하지 않아도 됩니다.(prop drilling)
  • 컴포넌트에서 Atom 또는 Selector를 연결하면, 상태를 변경했을 때 이를 사용하는 모든 컴포넌트에 반영됩니다.
  • Recoil의 Selector를 사용하면, 상태를 동기/비동기적으로 변경할 수 있고, 앱 내 어디에서나 파생된 상태를 사용할 수 있습니다.
  • Recoil은 보일러 플레이트 코드가 필요없습니다. Redux는 인기가 많지만 여전히 많은 개발자들이 Redux를 구축하는 데 필요한 코드의 양 때문에 힘들어 하고 있습니다.

 

Recoil을 사용해서 React 앱 만들기

이제 우리는 Recoil의 핵심개념, 그리고 왜 사용해야 하는지에 대해 알게 되었습니다. 이 섹션에선, anime-quote-generator를 만들어 볼 것이며, 선택된 애니메이션을 기반으로 외부 API에서 인용문을 가져올 것입니다.

 

아래 명령어를 통해 새로운 React 앱을 만들어 주세요.

 

$ npx create-react-app anime-quote-generator

 

다음으로, Recoil을 설치하고 앱에서 사용할 컴포넌트들을 만들어 보겠습니다. 아래 명령어를 실행해서 Recoil을 설치해 주세요.

 

$ npm i recoil

#OR

$ yarn add recoil

 

이제 Recoil을 사용해서 앱을 설정해 봅시다. src/index.js로 이동합니다. 여기서 우리에게 필요한 건, 전체 앱을 Recoil의 컴포넌트인 <RecoilRoot>로 감싸는 것 뿐입니다. 아래처럼 말이죠.

 

/* src/index.js */

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

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

 

 

<App> 컴포넌트를 단지 <RecoilRoot>로 감싸서 Recoil을 구축했습니다. 스토어가 포함된 React-Redux Provider 컴포넌트를 <App>에 감싸기 전에, 스토어를 만들기 위해서 몇 줄의 코드를 작성해야 했던 Redux에 비해 Recoil이 얼마나 구축하기 쉬운지 알 수 있습니다.

 

이제 Recoil을 구축했으니 컴포넌트, 페이지, 그리고 공유 상태를 Recoil을 사용해 만들어 보겠습니다.

 

AnimePills 컴포넌트 만들기

이 컴포넌트는 애니메이션의 제목을 렌더링하고, 클릭하면 애니메이션의 인용문을 볼 수 있는 페이지로 이동시켜줍니다. 우선, 앱과 styled-components에서 페이지를 라우팅하기 위해선, react-router-dom을 설치해야 합니다. 아래 명령어를 통해 설치해 주세요.

 

$ npm i react-router-dom styled-components

#OR

$ yarn add react-router-dom styled-components

 

다음으로, src 폴더에 components라는 폴더를 새로 만들겠습니다. 또한 components 폴더에 AnimePills 폴더를 만들고, AnimePills.jsx 파일을 만들어 줍니다. AnimePills.jsx 파일의 최종 경로는 src/components/AnimePills/AnimePills.jsx가 되어야 합니다. 그리고 아래의 코드를 파일에 추가해 주세요.

 

/* AnimePills.jsx */

import { Link } from 'react-router-dom';
import styled from "styled-components";

const AnimePills = ({ anime, color}) => {
  return (
    <StyledPill style={{ background: color }}>
      <Link to={`/anime/${anime}`}>{anime}</Link>
    </StyledPill>
  );
};

const StyledPill = styled.div`
  border-radius: 999px;
  & a {
    display: block;
    text-decoration: none;
    color: #333;
    padding: 1rem 2rem;
  }
`;

export default AnimePills;

 

위에서 AnimePills라는 파일을 만들었습니다. 이 컴포넌트는 anime와 color라는 2개의 props를 가지고 있는데, anime는 react-router-dom의 <Link> 컴포넌트에 링크로 사용되고, color는 배경색을 지정하는 데 사용됩니다. 그런 다음, styled-components를 사용해 컴포넌트를 꾸며줬습니다.

 

Quote, SmallQuote 컴포넌트 만들기

이 섹션에선 두 개의 컴포넌트를 만들 것입니다. Quote 컴포넌트부터 시작해 보죠. 이전에 만들었던 components 폴더에 Quote 폴더와 Quote.jsx 파일을 만들어 주세요. 이 컴포넌트에선 단순히 나루토 애니메이션의 인용문을 렌더링하고, 컴포넌트를 styled-components로 꾸며줄 것입니다. 아래의 코드를 파일에 추가해주세요.

 

/* Quote.jsx */

import styled from "styled-components";
const Quote = () => {
  const quote = {
    anime: "Naruto",
    character: "Pain",
    quote:
      "Because of the existence of love - sacrifice is born. As well as hate. Then one comprehends... one knows PAIN.",
  };
  return (
    <StyledQuote>
      <p>"{quote.quote}"</p>
      <h4>
        <span className="character">{quote.character}</span> <em>in</em>{" "}
        <span className="anime">{quote.anime}</span>
      </h4>
    </StyledQuote>
  );
};
const StyledQuote = styled.div`
  background: #dbece5;
  padding: 3rem 5rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  & > p {
    font-size: 2rem;
    letter-spacing: 2px;
    text-align: center;
    font-style: italic;
    margin-bottom: 3rem;
    background: #fff;
    border-radius: 0.5rem;
    padding: 3rem;
  }
  & > h4 {
    font-size: 1.5rem;
    font-weight: 500;
    letter-spacing: 2px;
    span {
      padding: 5px 10px;
    }
    em {
      font-size: 1.2rem;
    }
    & > .character {
      background: #b8dace;
    }
    & > .anime {
      background: #f5e7e4;
    }
  }
`;
export default Quote;

 

다음으로, SmallQuote 컴포넌트를 만들어 봅시다. 이 컴포넌트는 3개의 props(anime, character, quote)을 가지며, props들을 렌더링하고 컴포넌트를 styled-components로 꾸며줄 것입니다. 이를 위해선, src/components 폴더에 SmallQuote 폴더와 SmallQuote.jsx 파일을 만들고 아래의 코드를 추가해 주세요.

 

/* SmallQuote.jsx */

import styled from "styled-components";
const SmallQuote = ({ quote, character, anime }) => {
  return (
    <StyledQuote>
      <p>"{quote}"</p>
      <h4>
        <span className="character">{character}</span> <em>in</em>
        <span className="anime">{anime}</span>
      </h4>
    </StyledQuote>
  );
};
const StyledQuote = styled.div`
  background: #dbece5;
  padding: 1.5rem 2.5rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  & > p {
    font-size: 1rem;
    letter-spacing: 2px;
    text-align: center;
    font-style: italic;
    background: #fff;
    border-radius: 0.5rem;
    padding: 1.5rem;
    margin-bottom: 1.5rem;
  }
  & > h4 {
    font-size: 1rem;
    font-weight: 500;
    letter-spacing: 2px;
    span {
      padding: 3px 5px;
    }
    em {
      font-size: 1rem;
    }
    & > .character {
      background: #b8dace;
    }
    & > .anime {
      background: #f5e7e4;
    }
  }
`;
export default SmallQuote;

 

위에서 SmallQuote라는 컴포넌트를 만들고 styled-components를 사용해 꾸며줬습니다. Quote 컴포넌트와 매우 흡사합니다만, 코드를 좀 더 쉽게 이해할 수 있도록 Quote를 분할하는 역할을 합니다. 따라서 Quote 컴포넌트를 재사용하고 SmallQuote 컴포넌트의 기능을 추가하셔도 좋습니다.

 

다음으로, atom과 selector를 만들어 보겠습니다.

 

앱에 전역 상태 만들기(Atom, Selector)

시작하기 위해서, src 폴더로 이동하고 store라는 폴더를 새로 만들어서 그 안에 index.js라는 파일을 만들어 주세요. 이 파일에서, 필요한 모든 atom을 만들고 atom 중 하나를 수정할 수 있는 selector를 만들 겁니다. animeTitles라는 첫 번째 atom을 만들어 봅시다. 아래의 코드를 추가해서 첫 번째 atom을 만들어 주세요.

 

/* store/index.js */

import { atom } from 'recoil';

export const animeTitles = atom({
  key: 'animeTitleList',
  default: []
});

 

위에서 recoil의 atom을 import 해서 animeTitles라는 atom을 만들었고, 프로퍼티를 정의했습니다.

  • key : 앱 내 atom과 selector들 사이에서 유일한 ID이어야 합니다.
  • default : atom의 기본 값입니다.

 

이걸 Redux에서 하려면, 구체적인 type으로 action 생성자를 만들어야 하며, action 생성자는 type과 payload를 반환할 것입니다. 또한 store를 변경하기 위해 reducer를 만들어야 하죠. 하지만 Recoil을 사용하면 그럴 필요가 없습니다. key prop을 통해, 변경할 전역 상태를 알고 있기 때문에, 데이터를 전달해서 상태를 변경할 때 Redux처럼 action의 타입을 비교하지 않아도 됩니다.

 

같은 패턴으로, animePageNum이라는 atom을 만들어 봅시다. 이 atom을 사용해서 페이지의 숫자를 갖고 있게 하고, 이를 통해 pagination에 활용할 것입니다. 파일에 아래의 코드를 추가해 주세요.

 

/* store/index.js */

...(생략)

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

 

다음으로 데이터 배열을 변경하기 위해 selector를 만들어 보겠습니다. 이 selector는 animeListPageNum atom으로부터 가져올 수 있는 페이지 숫자를 기준으로 배열을 슬라이스 해서 한 페이지에서 50개의 animeTitles atom을 반환합니다.

 

/* store/index.js */

...(생략)

export const slicedAnimeTitles = selector({
  key: "slicedAnimeTitles",
  get: ({ get }) => {
    const animes = get(animeTitles);
    const pageNum = get(animeListPageNum);

    const newAnimeList = [...animes];
    const arrIndex = pageNum === 0 ? 0 : pageNum * 50 + 1;

    return newAnimeList.splice(arrIndex, 50);
  }
});

 

위에서 slicedAnimeTitles라는 selector를 만들었습니다. 전에 만들었던 atom들처럼 key 프로퍼티를 정의했고, 새로운 프로퍼티인 get을 가집니다. get의 값은 함수이고, 오직 selector 안에서만 사용할 수 있습니다. 이 함수는 2개의 인자를 갖는데, 위에선 atom이나 selector의 값에 접근할 수 있는 get 인자 하나만 인자로 받도록 했습니다. 함수 안에서, 인자로 받았던 get 함수를 사용해 animeTitles와 animeListPageNum atom의 값을 가져와서 animes와 pageNum라는 변수에 저장했습니다. 그리고 pageNum를 사용해 index와 슬라이스 시작점을 구했고, 50개의 항목을 가지는 배열을 반환했습니다.

 

이제 이 애플리케이션에서 사용될 모든 공유 상태를 만들었습니다. 다음으로 유저가 페이지 숫자를 클릭하고 animeListPageNum 상태(atom)를 변경하기 위한 pagination 컴포넌트를 만들어 보겠습니다. 그렇게 하면 방금 만들었던 selector를 통해 반환했던 애니메이션 리스트를 갱신할 수 있을 겁니다.

 

Pagination 컴포넌트 만들기

시작하려면, src/components로 이동하고 Pagination 폴더를 만들어서 그 안에 Pagination.jsx 파일을 만들어 주세요. 그런 다음 아래 코드를 파일에 붙여넣어 주세요.

 

/* Pagination.jsx */

import { useState, useEffect } from "react";
import { useRecoilState } from "recoil";
import styled from "styled-components";
import { animeListPageNum } from "../../store";

const Pagination = ({ listLength }) => {
  const [pageNum, setPageNum] = useRecoilState(animeListPageNum);
  const [numsArr, setNumsArr] = useState([]);
  
  ...(아래에서 이어짐)

 

위에서 Pagination이라는 컴포넌트를 만들었습니다. 이 컴포넌트는 listLength라는 인자를 가지고, 페이지를 렌더링하기 위한 페이지 숫자를 결정하는 데 도움이 될 것입니다. 그리고 useRecoilState를 import 했는데, 이는 React의 useState 훅처럼 하나의 인자를 받습니다. 또한 Redux의 useSelector와 비슷하게 동작하지만, Recoil은 상태를 직접적으로 변경할 수 있고 dispatch와 action을 할 필요가 없습니다. useRecoilState를 통해서 상태에 접근할 수 있고 변경도 할 수 있습니다. 또한 이 컴포넌트에서 페이지를 렌더링하기 위한 페이지 숫자의 배열을 가지고 있을 상태를 useState로 만들었습니다.

 

/* Pagination.jsx */

...(생략)

useEffect(() => {
    const paginationNums = () => {
      const max = Math.floor(listLength / 50);
      let nums = [];
      for (let i = 0; i <= max; i++) {
        nums.push(max - i);
      }
      setNumsArr(
        nums.sort((a, b) => {
          return a - b;
        })
      );
    };
    paginationNums();
  }, [listLength]);
  return (
    <StyledPagination>
      {numsArr?.length
        ? numsArr?.map((num) => (
            <button
              className={pageNum === num ? "active" : ""}
              onClick={() => setPageNum(num)}
              key={num}
            >
              {num + 1}
            </button>
          ))
        : null}
    </StyledPagination>
  );
};

...(아래에서 이어짐)

 

위에서 import 한 useEffect에서, listLength 프로퍼티로 숫자 배열을 만들고, 만들었던 nums 배열로 numsArr 상태를 변경해준 다음, nums 배열을 루프해서 그것들을 버튼으로 렌더링했으며, 각 버튼을 클릭하면 animeListPageNum가 변경될 것입니다. 아래 코드를 추가해서 컴포넌트를 완성시켜 봅시다.

 

/* Pagination.jsx */

...(생략)

const StyledPagination = styled.div`
  display: flex;
  align-items: center;
  border-width: 2px 2px 2px 0;
  border-style: solid;
  width: max-content;
  & button {
    outline: none;
    background: transparent;
    border: none;
    border-left: 2px solid;
    width: 35px;
    height: 30px;
    display: flex;
    align-items: center;
    justify-content: center;
    &:hover,
    &.active {
      background: #fae1da;
    }
  }
`;
export default Pagination;

 

이제 Pagination 컴포넌트를 완성했으니, 이제 애플리케이션 페이지들을 빌드하고 완성할 수 있습니다.

 

Homepage 컴포넌트 만들기

이 섹션에선, homepage 컴포넌트를 만들어 보겠습니다. 이 페이지는 인용문을 렌더링하고, 전에 만들었던 AnimePills 컴포넌트를 사용해 애니메이션들의 목록을 보여주며, pagination을 위한 Pagination 컴포넌트를 렌더링합니다. 이렇게 하려면, src 폴더에 pages라는 폴더를 만들고, 여기에 home이라는 폴더를 만들어서 그 안에 index.jsx라는 파일을 만들어 주세요. 최종 경로는 src/pages/home/index.jsx가 되겠죠. 아래 코드를 파일에 추가해 주세요.

 

/* src/pages/home/index.jsx */

import { useRecoilValue } from "recoil";
import styled from "styled-components";
import AnimePill from "../../components/AnimePill/AnimePill";
import Pagination from "../../components/Pagination/Pagination";
import Quote from "../../components/Quote/Quote";
import { slicedAnimeTitles, animeTitles } from "../../store";

const Homepage = () => {
  const animes = useRecoilValue(animeTitles);
  const slicedAnimes = useRecoilValue(slicedAnimeTitles);
  const colors = ["#FAE1DA", "#E8C6AD", "#F2E2ED", "#D6EBE4", "#BFDCD0"];
  const generateColor = () => {
    const randNum = Math.floor(Math.random() * 5);
    return colors[randNum];
  };

  return (
    <StyledHomePage>
      <header>
        <h2>Anime Quote Generator</h2>
      </header>
      <main>
        <Quote />
        <div className="animes">
          <h3>All Animes</h3>
          {animes?.length ? (
            <p>Click on any anime to see a quote from it</p>
          ) : null}
          <div className="flex">
            {animes?.length ? (
              slicedAnimes?.map((anime) => (
                <div key={anime} style={{ margin: "0 1.3rem 1.3rem 0" }}>
                  <AnimePill anime={anime} color={generateColor()} />
                </div>
              ))
            ) : (
              <p className="nodata">No anime found 😞 </p>
            )}
          </div>
          {animes?.length > 50 ? (
            <div className="pagination">
              <Pagination listLength={animes?.length} />
            </div>
          ) : null}
        </div>
      </main>
    </StyledHomePage>
  );
};

const StyledHomePage = styled.div`
  max-width: 80%;
  margin: 2rem auto;
  & header {
    margin-bottom: 3rem;
    & > h2 {
      font-weight: 400;
      letter-spacing: 3px;
      text-align: center;
    }
  }
  & .animes {
    margin-top: 4rem;
    & > h3 {
      font-weight: 400;
      font-size: 1.4rem;
      background: #ece4f1;
      width: max-content;
      padding: 0.3rem 1rem;
    }
    & > p {
      margin: 1.2rem 0;
    }
    & > .flex {
      display: flex;
      justify-content: center;
      flex-wrap: wrap;
      & > .nodata {
        margin: 2rem 0 4rem;
        font-size: 1.3rem;
      }
    }
    & .pagination {
      display: flex;
      flex-direction: column;
      align-items: center;
      margin: 2rem 0 4rem;
    }
  }
`;
export default Homepage;

 

위에선, 아래를 import 했습니다.

  • useRecoilValue : 상태 값을 가져올 수 있는 Recoil 라이브러리입니다.
  • styled : 홈페이지 컴포넌트를 꾸미기 위한 styled-components 입니다.
  • AnimePill : 애니메이션 제목을 렌더링 합니다.
  • Pagination : pagination을 처리합니다.
  • Quote : 애니메이션의 정적 인용문을 표시합니다.
  • SlicedAnimeTitles : 전에 만들었던 selector입니다.
  • animeTitles : 애니메이션의 목록을 가지고 있게 하기 위한 첫 번째 atom입니다.

 

위에서 Homepage라는 함수 컴포넌트를 만들었고, useRecoilValue를 사용해 animeTitles와 sliceAnimeTitles 상태에 접근했습니다. 그리고 AnimePill에 전달할 랜덤한 색상 값을 만들기 위해 generateColor 함수를 만들었습니다. 이 함수는 colors 배열로부터 랜덤한 색상을 반환합니다. 다음으로 헤더, Quote 컴포넌트, 사용자가 무엇을 해야 하는지 알려주는 작은 알림으로 구성된 styled-components를 반환했습니다. 애니메이션 데이터가 있으면 sliceAnimes를 루프하고, AnimePill 컴포넌트에 애니메이션 데이터와 색상값을 전달해서 렌더링하도록 했고, 애니메이션 데이터가 없으면 'no data' 화면을 렌더링하도록 했습니다. 다음으로, 애니메이션 데이터의 상태가 50개보다 많은지 확인하고, true이면 Pagination 컴포넌트를 렌더링 하도록 했습니다.. 마지막으로, styled-components 블럭을 추가했습니다.

 

이제 Homepage 컴포넌트가 완성됐습니다. 다음 섹션에선 아무 AnimePill을 클릭하면 라우팅 해줄 페이지를 만들어보겠습니다. 이 컴포넌트에선, 선택된 애니메이션의 모든 인용문을 가져올 수 있는 외부 API를 호출하고 렌더링 해줍니다.

 

Animepage 컴포넌트 만들기

pages 폴더로 이동하고, 그 안에 anime라는 폴더를 만들어섯 index.jsx라는 파일을 만들어 주세요. 

우선, 외부 API와 비동기 통신을 위해 아래 명령어로 axios를 설치해 주세요.

$ npm i axios

 

다음으로, 아래의 코드를 파일에 추가해 주세요.

/* pages/anime/index.jsx */

import { useState, useEffect } from "react";
import { Link, useParams } from "react-router-dom";
import axios from "axios";
import styled from "styled-components";
import SmallQuote from "../../components/SmallQuote/SmallQuote";

const Animepage = () => {
  const param = useParams();
  const [quotes, setQuotes] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (param?.name) {
      setLoading(true);
      const fetchAnimeQuotes = async () => {
        try {
          const res = await axios.get(
            `https://animechan.vercel.app/api/quotes/anime?title=${param?.name}`
          );
          setQuotes(res?.data);
          setLoading(false);
        } catch (error) {
          console.log(error);
          setLoading(false);
        }
      };
      fetchAnimeQuotes();
    }
  }, [param]);
  
  return (
    <StyledAnimePage>
      <h2>Quotes from {param?.name}</h2>
      <Link to="/">Go back</Link>
      <div className="grid">
        {loading ? (
          <p>Loading...</p>
        ) : quotes?.length ? (
          quotes?.map((quote, index) => (
            <div key={quote?.quote + index} className="anime">
              <SmallQuote
                anime={quote?.anime}
                character={quote?.character}
                quote={quote?.quote}
              />
            </div>
          ))
        ) : (
          <p className="nodata">No Quote found 😞</p>
        )}
      </div>
    </StyledAnimePage>
  );
};

const StyledAnimePage = styled.div`
  max-width: 80%;
  margin: 2rem auto;
  position: relative;
  & > a {
    position: absolute;
    top: 1rem;
    text-decoration: none;
  }
  & > h2 {
    font-weight: 400;
    letter-spacing: 3px;
    text-align: center;
    margin-bottom: 2rem;
  }
  & > .grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    grid-template-rows: max-content;
    & .anime {
      margin: 1rem;
      height: max-content;
    }
    & > p {
      margin: 2rem 0 4rem;
      font-size: 1.3rem;
      text-align: center;
    }
  }
`;
export default Animepage;

 

위에서 애플리케이션에서 사용할 모든 컴포넌트들을 import 했고, 브라우저 URL로부터 애니메이션 제목을 가져올 수 있도록 하기 위해 useParam 훅을 초기화했습니다. 다음으로, useState 훅을 사용해서 2개의 컴포넌트 상태를 만들었고, 하나는 API로부터 가져온 인용문을 저장할 상태와 로딩 중인 걸 저장할 상태입니다. 그리고 useEffect에선, URL로부터 가져온 애니메이션 제목을 기반으로 인용문을 가져오고 인용문 상태를 설정했습니다. 그리고선 styled-components를 사용해 jsx 블럭을 리턴했습니다.

 

위에서 우리는 Recoil을 사용하지 않았다는 걸 알 수 있는데, 상태가 이 컴포넌트에서만 사용되기 때문입니다.

 

App Routes, 애니메이션 fetching 만들기

이 애플리케이션을 완성하기 위해선, src/App.js로 이동해서 아래 두 가지를 해야 합니다.

  1. 외부 API로부터 애니메이션 목록을 가져오고, 전에 만들었던 animeTitles atom을 변경하기
  2. react-router-dom을 사용해 애플리케이션의 라우트를 정의하기

 

src/App.js로 이동하고 아래 코드로 수정해 주세요.

import { useEffect } from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { useSetRecoilState } from "recoil";
import axios from "axios";
import { animeTitles } from "./store";
import Homepage from "./pages/home";
import Animepage from "./pages/anime";

 

위에서 아래를 import 했습니다.

  • useEffect : API 호출을 useEffect 안에서 할 것이고, 페이지가 렌더링 됐을 때, 애니메이션 배열을 가져올 수 있도록 할 것입니다.
  • react-router-dom의 BrowserRouter, Route, Switch : 페이지 라우팅을 구현하기 위한 컴포넌트들입니다.
  • recoil의 useSetRecoilState : 이걸 사용해서, animeTitles atom의 값을 변경해줄 것입니다.
  • axios : 외부 API로부터 데이터를 가져오기 위해 사용합니다.

 

다음으로, App 컴포넌트를 만들고 그 안에서 애니메이션들을 가져올 것입니다. 아래의 코드를 추가해 주세요.

...(생략)

const App = () => {
  const setTitles = useSetRecoilState(animeTitles);
  const fetchAnimes = async () => {
    try {
      const res = await axios.get(
        "https://animechan.vercel.app/api/available/anime"
      );
      setTitles(res?.data);
    } catch (error) {
      console.log(error?.response?.data?.error);
    }
  };
  useEffect(() => {
    fetchAnimes();
  }, []);
  
  ...(아래에서 이어짐)

 

위에서 App 컴포넌트를 만들었고, animeTitles atom을 변경할 setTitles라는 변수를 만들었습니다. 다음으로, fetchAnimes라는 비동기 함수를 만들었습니다. 그 안에서 axios를 사용해 외부 API로부터 애니메이션들을 가져오게 했고, 에러 처리를 위해 try-catch 문을 사용해 animeTitles 상태를 변경해줬습니다. 이후로 페이지가 렌더링 됐을 때 이 함수가 동작할 수 있도록, useEffect 내에서 fetchAnimes 함수를 호출했습니다.

 

이제 라우트를 추가해서 App 컴포넌트를 완성시켜 봅시다.

(아래에선 react-router-dom 6 버전을 기준으로 작성되었습니다.)

...(생략)

return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Homepage />} />
        <Route path="/anime/:name" element={<Animepage />} />
      </Routes>
    </BrowserRouter>
  );
};

export default App;

 

이걸로 앱이 완성되었습니다. 앱이 어떻게 동작하는지 개발 서버를 시작해봅시다. 터미널에서 아래의 명령어를 실행해 주세요.

 

$ npm start

#OR

$ yarn start

 

정확히 따라왔다면, 아래 페이지를 볼 수 있을 겁니다.

 

결론

이 글에서 Recoil이 무엇인지, 왜 사용하는지, 그리고 anime-quote-generator 앱에 Redux 대신 Recoil을 사용해서 어떻게 상태 관리를 하는지에 대해 알아봤습니다. 또한 Redux와 비교해서 Recoil이 어떻게 동작하는지를 봤고, Recoil을 사용하기가 얼마나 쉬운지를 볼 수 있었습니다. 공식 도큐먼트를 통해 Recoil에 대해 더 알아볼 수 있습니다.