본문 바로가기
사이드 프로젝트

[React] Recoli을 이용하여 여행 리스트 만들기

by 반류연 2025. 3. 26.

노마드코더의 '리엑트JS 마스터 챌린지'를 진행하던 중, recoil을 활용한 이번 과제가 너무 재밌었어서 따로 기록을 남긴다.

 

Recoli이란?

React에서 전역 상태관리를 쉽게 할 수 있도록 도와주는 상태 관리 라이브러리.

컴포넌트 기반의 라이브러리로 'atom' 이라는 곳에 데이터를 담아 모든 컴포넌트에 전달할 수 있다.

여러 컴포넌트에서 동일한 상태를 공유하거나 동기화 해야할 때 유용하며, 비동기 상태를 지원하기에 fetch 도 쉽게 처리할 수 있다.

 

 

기본 개념

  • Atom: 상태의 최소 단위. 컴포넌트들이 atom을 공유하며 그 속에 담긴 데이터를 읽고 쓸 수 있다.
  • Selector: 다른 상태값을 기반으로 새로운 값을 생성. 보통 Atom 값에 의존하나 외부 데이터를 이용할 수도 있다. (보통 fetch 할 때)
  • 사용법
import { atom } from 'recoil';

// 아톰
const countState = atom({
  key: 'countState', // 고유한 키
  default: 0,        // 초기값
});
// 셀렉터

import { selector } from 'recoil';
import { countState } from './atoms';

const doubleCountState = selector({
  key: 'doubleCountState',
  get: ({ get }) => get(countState) * 2,
});

 

✅프로젝트 소개

 

 

  • 사용기술: react-form, recoil, styled-components
  • 구현해야 할 기능
    • 가고싶은 나라를 입력하면 '내가 가본 나라들'에 항목이 추가됨.
    • '내가 가본 나라들'에서 좋아요 버튼을 누를 시 '좋아하는 나라들' 항목으로 이동되고, 취소 버튼을 누를경우 다시 가고싶은 나라들 항목으로 이동됨.
    • '좋아하는 나라들' 에서 싫어요 버튼을 누를 시 다시 '내가 가본 나라들' 항목으로 이동함.
    • 새로고침해도 입력한 값이 남아있어야 함.
    • 아무것도 입력하지 않은 채 버튼을 누르면 'required!' 라는 경고문구 발생.

※ 결과물 미리 보기(코드 확인)

 

https://stackblitz.com/edit/reactstudy-a16-blueprint-js5mem-1dgdnjnw?file=README.md

 

ReactStudy-A16-Blueprint (forked) - StackBlitz

Next generation frontend tooling. It's fast!

stackblitz.com

 

✅진행과정

// App.tsx

import { useState } from 'react';
import { useForm } from 'react-hook-form';
import styled from 'styled-components';
import { atom, useRecoilState } from 'recoil';
import VisitedCountry from './components/VisitedCountry';
import LikeCountry from './components/LikeCountry';
import AddCountry from './components/AddCountry';

export default function App() {
  return (
    <main>
      <AddCountry />
      <VisitedCountry />
      <LikeCountry />
    </main>
  );
}

 

 

 

가고 싶은 나라들/가본 나라들/좋아하는 나라들 < 세 영역을 각각 컴포넌트로 분리한 다음, 가장 먼저 '가고 싶은 나라들(AddCountry.jsx)를 구현하였다.

 

1. Recoil

// atom.jsx

import { atom } from 'recoil';

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

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

export const likeState = atom({
  key: 'like',
  default: [],
});
  • 각 컴포넌트에서 사용할 값을 담은 배열 필요: Recoil의 atom에 저장.
    • countriesState: 가고싶은 나라들 배열
    • visitedState: 가본 나라들 배열
    • likeState: 좋아하는 나라들 배열
  • useRecoilState를 사용해 AddCountry.jsx에서 atom에 저장된 값을 가져와 업데이트 함.

2. React-hook-form

//AddCountry.jsx

import { useState } from 'react';
import { useForm } from 'react-hook-form';
import styled from 'styled-components';
import { atom, useRecoilState } from 'recoil';
import { countriesState, visitedState, likeState } from '../atom';

// style-components 코드는 생략

function AddCountry() {
  const [icountry, setICountry] = useRecoilState(countriesState);
  const [vcountry, setVCountry] = useRecoilState(visitedState);
  const [lcountry, setLCountry] = useRecoilState(likeState);

  const { register, handleSubmit, formState: { errors }, setValue} = useForm();
  const onValid = (data) => {
    setICountry((prev) => [...prev, data.countries]);
    //setValue의  key는 register에 등록한 키와 같은 걸로 써야함
    setValue('countries', '');
  };
  const addVisitedFn = (newVisited) => {
    if (!vcountry.includes(newVisited)) {
      // 가본 나라들 배열에 추가
      setVCountry((prev) => [...prev, newVisited]);
      // 가고싶은 나라들 배열에서 삭제
      setICountry((prev) => prev.filter((item) => item !== newVisited));
    }
  };
  const deleteFn = (deleteCountry) => {
    setICountry((prev) => prev.filter((item) => item !== deleteCountry));
  };

  return (
    <>
      <Title>내가 가고싶은 나라들</Title>
      <form onSubmit={handleSubmit(onValid)}>
        {/* resigst가 가지고 있는 함수를 모두 input에 전달 */}
        <Input
          {...register('countries', { required: '☹️ required!' })}
          type="text"
          placeholder="이름"
        />
        <ErrMsg>{errors?.countries?.message}</ErrMsg>
        <BtnSubmit>가자!</BtnSubmit>
      </form>

      <CountryList>
        {icountry.map((item) => (
          <li key={item}>
            <span>{item}</span>
            <button onClick={() => addVisitedFn(item)}>✅</button>
            <button onClick={() => deleteFn(item)}>🗑️</button>
          </li>
        ))}
      </CountryList>
    </>
  );
}

export default AddCountry;
  • react-hook-form의 useForm()을 사용해 form 생성. 
    • register: form 에 들어온 데이터와 함수를 담은 요소
    • handelSubmit: form에 submit 됐을 때 실행되는 함수 제어
    • formState: form 의 상태값을 담은 객체. 그 중 error 코드만 가져와 사용자가 아무것도 입력하지 않았을때 경고문구를 출력함.
    • setValue: form이 업데이트 된 후의 값을 설정. 입력 완료 후 빈칸이 되도록 사용.
  • 체크 버튼을 누를 경우, countriesState에서 해당 값 삭제, visitedStated에 추가.
  • 쓰레기통 버튼을 누를 경우, countriesState에서 해당 값 삭제.

 

AddCountry 구현을 끝낸 뒤 같은 방식으로 recoil을 사용해 VisitedCountry와 LikeCountry를 구현하였다. 이 두 컴포넌트는 form이 필요하지 않아 좀 더 간단했다. 

// VisitedCountry.jsx

import styled from 'styled-components';
import { countriesState, visitedState, likeState } from '../atom';
import { atom, useRecoilState } from 'recoil';
import { useState } from 'react';

// styled-components 코드는 생략

function VisitedCountry() {
  const [icountry, setICountry] = useRecoilState(countriesState);
  const [vcountry, setVCountry] = useRecoilState(visitedState);
  const [lcountry, setLCountry] = useRecoilState(likeState);

  const addLikeFn = (likeCountry) => {
    if (!lcountry.includes(likeCountry)) {
      setLCountry((prev) => [...prev, likeCountry]);
    }
    setVCountry((prev) => prev.filter((item) => item != likeCountry));
  };
  const cancelVisitedFn = (item) => {
    setICountry((prev) => [...prev, item]);
    setVCountry((prev) => prev.filter((item) => item != item));
  };

  return (
    <>
      <Title>내가 가본 나라들</Title>
      <CountryList>
        {vcountry.map((item) => (
          <li key={item}>
            <span>{item}</span>
            <button onClick={() => addLikeFn(item)}>👍</button>
            <button onClick={() => cancelVisitedFn(item)}>❌</button>
          </li>
        ))}
      </CountryList>
    </>
  );
}

export default VisitedCountry;
  • 좋아요 버튼을 누를 경우, visitedState에서는 해당 값 삭제, likeState에 추가.
  • X 버튼을 누를 경우, visitedState에서는 해당 값 삭제, countriesState에 추가.

좋아하는 나라들(LikeContry.jsx)도 같은 형식으로 구현하였다. VisitedCountry.jsx와 차이점은 버튼을 눌렀을 때 동작이 다르다는 것 하나!

  • 싫어요 버튼을 누를 경우, likeState에서는 해당값 삭제, VisitedState에 추가.

 

3. Recoil와 localStorage

마지막 관문, 입력한 값들을 새로고침해도 날아가지 않도록 localStorage에 저장하는 것!

이건 생각보다 쉬웠다. recoil에서 이미 제공하는 옵션이 있었던 것! Atom Effects의 'Local Storage Persistence (로컬 스토리지 지속성)' 을 사용하면 된다.

 

공식문서 확인: https://recoiljs.org/ko/docs/guides/atom-effects/#local-storage-persistence-%EB%A1%9C%EC%BB%AC-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80-%EC%A7%80%EC%86%8D%EC%84%B1

 

Atom Effects | Recoil

Atom Effects는 부수효과를 관리하고 Recoil의 atom을 초기화 또는 동기화하기 위한 API입니다. Atom Effects는 state persistence(상태 지속성), state synchronization(상태 동기화), managing history(히스토리 관리), loggin

recoiljs.org

 

// 로컬 스토리지 지속성을 추가해 최종 완성한 atom.jsx

import { atom } from 'recoil';

const localStorageEffect =
  (key) =>
  ({ setSelf, onSet }) => {
    const savedValue = localStorage.getItem(key);
    if (savedValue != null) {
      setSelf(JSON.parse(savedValue));
    }

    onSet((newValue, _, isReset) => {
      isReset
        ? localStorage.removeItem(key)
        : localStorage.setItem(key, JSON.stringify(newValue));
    });
  };

export const countriesState = atom({
  key: 'country',
  default: [],
  effects: [localStorageEffect('country')],
});

export const visitedState = atom({
  key: 'visited',
  default: [],
  effects: [localStorageEffect('visited')],
});

export const likeState = atom({
  key: 'like',
  default: [],
  effects: [localStorageEffect('like')],
});

 

이렇게 하니 원하는 조건을 모두 충족시킨 페이지가 탄생했다! 처음엔 낯설고 어려웠는데 하다보니 recoil에 대한 감이 잡혔다. 한 번 길이 드니 그 다음부터는 쭉쭉 진도 잘 나가더라.

짜릿

 

✅사담

이번 프로젝트를 하며 문득 예전 회사일로 Vue 프로젝트 했던 일이 생각났다. 페이지에서 넘기는 값을 받아 모달에 적용해야 했는데 당시엔 방법을 몰라 부모-자식 간의 단방향성 props 전달방식을 써서 매우 비효율적인 코드가 탄생했었다. 프로젝트가 끝난 후, 나중에 Vue에도 상태관리 라이브러리가 있다는 걸 알았지만 이미 프로젝트는 끝났고... (왜 바로 찾지 못했냐 물으신다면...잘못된 부분을 고칠 시간조차 없을 만큼 데드라인이 가까웠다고 답하겠습니다...) 아쉬움을 품고 있던 프로젝트였는데 이번에 react로 비슷한 기능을 활용해 구현해 볼 수 있어 즐거웠다.

 

그런데... 이 글을 쓰는 시점(25/3/26)에 들린 청천벽력같은 소식

 

https://medium.com/@clockclcok/recoil-%EC%9D%B4%EC%A0%9C%EB%8A%94-%EB%96%A0%EB%82%98-%EB%B3%B4%EB%82%BC-%EC%8B%9C%EA%B0%84%EC%9D%B4%EB%8B%A4-ff2c8674cdd5

 

Recoil, 이제는 떠나 보낼 시간이다

개요

medium.com

 

네?

 

 

Recoil의 사용자가 급격히 줄고 있다는 칼럼이었다. 이유는 메모리 누수, 더딘 업데이트 등 다양했고 실제 스터디방에서도 recoil 대신 jotai로 마이그레이션 했다는 개발자가 많았다. 흑흑 이제 만나 정붙였는데 퇴행중이었다니... 슬프지만 변화가 빠른 프론트업계에서 이런 신호를 무시할 순 없기에 나도 Jotai를 공부할 예정이다. (하나 익혔더니 바로 다른거 익혀야되네...)  다행히 recoil과 유사한 형태라고 해서 부담은 덜...덜....덜 수 있겠지?😂 공부의 길은 정말 끝이 없다.