react with recoil

Recoil with Selector

지난 두번의 포스팅에서 Recoil 에 대한 소개를 해드렸습니다.

첫 번째 글은 Recoil 에 대한 전반적인 소개를 했고, 두 번째 글은 Recoil 에서 effects 를 활용하는 방법과 Persist 를 구현하는 방법에 대해서 알아봤습니다.

1. Recoil, 리액트의 상태관리 라이브러리
2. Recoil with Storage (feat.effects)

오픈소스컨설팅의 Playce Dev 팀의 Frontend 파트의 새로운 아키텍쳐에서는 Client State Management 로 Recoil 을 사용하고 있습니다.

개발을 하다 보면 초반에 잘 설계를 하여도, 여러 개발자들이 협업을 하다 보면 언젠간 부작용(Side Effect)이 발생하기 마련입니다.

부작용(Side Effect) 없이 잘 설계된 소프트웨어가 설계적 측면에서 좋은 소프트웨어 이지만, 부작용이 전혀 없이 설계하는 것은 이론에서 벗어나 현실적으로는 힘들기 때문에 부작용을 최소화 하며 프로그래밍을 해야 합니다.

이번 포스팅 에서는 Web-Frontend 에서 부작용를 일으키기 쉬운 부분인 Client State Management 에서 Recoil 을 사용할 때 부작용을 최소화 할 수 있는 방법에 대해서 고안해본 부분을 공유해보도록 하겠습니다.

🧐 사실 이 글은 헬스를 하다가 떠올라 고민하고 작성하게 된 글입니다. 모두 운동을 하는 습관을 기릅시다!

함수형 프로그래밍

이론적으로 함수형 프로그래밍이란 데이터를 값으로 보고, 값을 처리하는 함수에 포커스를 두며 프로그래밍 하는 프로그래밍 패러다임 입니다. 함수형 프로그래밍 에서는 데이터를 메모리에 저장하지 않거나 저장하는 변수를 최소화 하여서 메모리 관리를 초점을 맞춥니다.

함수형 프로그래밍에서 함수는 항상 순수 함수여야 합니다. 순수 함수란 항상 동일한 input 을 넣었을 때 항상 동일한 output 이 있어야 하며, 불변성 (immutability)이 유지되어야 합니다.

이번 포스팅은 함수형 프로그래밍의 특징 중 순수 함수에 포커스를 맞출 것이며, 그 중 불변성에 포커스를 맞추겠습니다.

예제와 함께 살펴보기

Selector 을 활용하지 않고..

먼저 Selector 을 활용하지 않고 atom 을 직접 가져와서 이를 가공해 화면에 뿌려보겠습니다.

import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useRecoilState, useRecoilValue } from "recoil";
import { userList } from "../modules/UserList/atom";

const numberList = [1, 2, 3, 4, 5, 6, 7, 8, 9];

export default function List() {
  const [number, setNumber] = useState(1);
  const [list, setList] = useRecoilState(userList);

  const handleChange = useCallback(
    (event: React.ChangeEvent<HTMLSelectElement>) => {
      const number = Number(event.currentTarget.value);
      setNumber(number);
    },
    []
  );

  useEffect(() => {
    setList(() => list.filter((li) => li.phone.includes(`${number}`)));
  }, [list, number]);

  return (
    <div>
      <h2>Select number</h2>
      <select onChange={handleChange} value={number}>
        {numberList.map((num) => (
          <option value={num} key={num}>
            {num}
          </option>
        ))}
      </select>
      <h1>Select List</h1>
      <hr />
      {list.map(({ id, addr, name, phone }) => (
        <div>
          <p>id: {id}</p>
          <p>name: {name}</p>
          <p>addr: {addr}</p>
          <p>phone: {phone}</p>
          <hr />
        </div>
      ))}
    </div>
  );
}

Recoil 에 대해서 이미 알고 계신 분들께서는 위 코드를 보고 atom 을 직접 변경해? 라는 생각을 하실 겁니다. Recoil 을 사용하는 이유는 Client-Side 전역에서 필요한 상태 (Data) 를 관리하기 위해 사용하는 것인데, 만약 위 코드처럼 atom 을 직접 변경할 경우 해당 atom 을 바라보고 있는 모든 컴포넌트 에서 상태가 변경이 되어 예상치 못한 부작용(Side-effects) 이 일어나게 됩니다.

 

그럼 아래의 코드를 살펴보겠습니다.

import React, { useCallback, useMemo, useState } from "react";
import { useRecoilState, useRecoilValue } from "recoil";
import { userList } from "../modules/UserList/atom";

const numberList = [1, 2, 3, 4, 5, 6, 7, 8, 9];

export default function List() {
  const [number, setNumber] = useState(1);
  const [list, setList] = useRecoilState(userList);

  const handleChange = useCallback(
    (event: React.ChangeEvent<HTMLSelectElement>) => {
      const number = Number(event.currentTarget.value);
      setNumber(number);
    },
    []
  );

  const selectList = useMemo(
    () => list.filter((li) => li.phone.includes(`${number}`)),
    [number, list]
  );

  return (
    <div>
      <h2>Select number</h2>
      <select onChange={handleChange} value={number}>
        {numberList.map((num) => (
          <option value={num} key={num}>
            {num}
          </option>
        ))}
      </select>
      <h1>Select List</h1>
      <hr />
      {selectList.map(({ id, addr, name, phone }) => (
        <div>
          <p>id: {id}</p>
          <p>name: {name}</p>
          <p>addr: {addr}</p>
          <p>phone: {phone}</p>
          <hr />
        </div>
      ))}
    </div>
  );

}

위 코드를 살펴보면 useRecoilState 를 활용해 userList 라는 atom 을 가져왔고, select 으로 인해서 변경되는 number 를 활용해 selectList 라는 변수를 컴포넌트 단에서 직접 가공하고 있습니다. 함수형 컴포넌트 내부의 변수인 selectList 에 가공되는 리스트를 담아 놨기 때문에 위 코드를 보면 뭐가 문제일까? 하는 생각이 드실 겁니다.

 

위 코드에서 selectList 라는 변수의 역할을 생각해 봅시다.

 

해당 변수는 atom 과 선택된 number 에 Dependency (의존성) 가 걸려 있으며 useMemo 내부에서 filter 연산을 하여 이를 반환해 주고 있습니다. 의존성과 여러 연산으로 인해서 변수가 어떤 역할을 하는지 직관적으로 파악하기 어려우며 컴포넌트 내부에서 연산을 해주어 컴포넌트도 복잡해지고 (지저분해지고) 있습니다.

위 코드는 예제 이기에 짧은 연산 만을 요구하지만, 실제 프로젝트에서는 데이터 구조가 복잡해 짐으로서 더욱 복잡한 연산을 요구하기에 컴포넌트는 상당히 복잡해질 것입니다.

 

이를 해결하기 위해 우리는 Recoil 의 Selector 을 활용할 수 있습니다.

Selector 을 활용해보자!

모두가 알고 있듯이 Selector 는 Recoil 에서 제공하고 있는 순수함수의 개념입니다. 동일한 Parameter 를 제공하면 동일한 값을 Return 해줍니다. 위 예제를 Selector 을 활용해 다시 작성해보겠습니다.

// Component
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useRecoilState, useRecoilValue } from "recoil";
import { userSelector as selector } from "../modules/UserList/selector";

const numberList = [1, 2, 3, 4, 5, 6, 7, 8, 9];

export default function List() {
  const [number, setNumber] = useState(1);
  const userSelector = useRecoilValue(selector(number));

  const handleChange = useCallback(
    (event: React.ChangeEvent<HTMLSelectElement>) => {
      const number = Number(event.currentTarget.value);
      setNumber(number);
    },
    []
  );

  return (
    <div>
      <h2>Select number</h2>
      <select onChange={handleChange} value={number}>
        {numberList.map((num) => (
          <option value={num} key={num}>
            {num}
          </option>
        ))}
      </select>
      <h1>Select List</h1>
      <hr />
      {userSelector.map(({ id, addr, name, phone }) => (
        <div>
          <p>id: {id}</p>
          <p>name: {name}</p>
          <p>addr: {addr}</p>
          <p>phone: {phone}</p>
          <hr />
        </div>
      ))}
    </div>
  );

}

// Selector
import { selectorFamily } from "recoil";
import { IUser } from "../type";
import { userList } from "./atom";

export const userSelector = selectorFamily<IUser[], number>({
  key: "userSelector",
  get:
    (param: number) =>
    ({ get }) =>
      get(userList).filter((person) => person.phone.includes(`${param}`)),
}
);

위 코드에서는 컴포넌트는 Selector 에 값을 제공하고, Selector 가 Return 해주는 값을 화면에 뿌려주는 역할만을 수행하고 있습니다. 위 두 가지 코드와 비교해보면 컴포넌트 에서 수행하는 연산이 줄어들고, 관리 포인트가 분리되었다는 것을 확인할 수 있습니다.

 

Selector 은 내부에서 get 이라는 메서드를 파라미터로 받고 get 메서드를 활용해 atom 을 사용할 수 있습니다. 여러 atom 을 가공해 사용해야 하는 경우 더욱 효과적으로 사용할 수 있습니다.

 

또한 atom 을 직접 변경하지 않고 불변성을 유지하기 때문에 부수 효과를 확실하게 줄일수 있는 효과를 기대해볼 수 있습니다.

Selector 에서 매개변수가 필요하다?

Selector 를 활용하는데 매개변수가 필요하다면 selectorFamily 를 활용할 수 있습니다. Typescript 를 활용할 때 Generic 으로 <리턴타입, 매개변수타입> 을 지정하고, get 프로퍼티 를 구현할 때 클로저 를 활용해 매개변수(param) 을 활용해 로직 에서 파라미터를 활용할 수 있답니다.

 

컴포넌트 단 에서는 직접 selector 에 매개변수를 넣어줍시다.

const userSelector = useRecoilValue(selector(number));

이어지는 글 살펴보기

  1. Recoil 의 전반적인 모든 것
  2. Recoil with persist
  3. React-Query 도입을 위한 고민 (feat. Recoil)

마치며

오늘 게시글에서는 Selector 을 통해서 부작용을 줄이며 개발을 하는 방법에 대해서 고안해봤습니다.

 

해당 글을 통해서 Selector 에 대한 개념이 명확해 지기를 바라며 글을 줄이도록 하겠습니다.

감사합니다

강동희
Developer

오픈소스컨설팅 Playce Dev Team 소속으로 오늘을 바탕으로 내일이 더 기대되는 프론트엔드 개발자 강동희입니다. 빠르게 변화하는 기술들에 관심이 많으며 지식 공유에 큰 가치를 두고 있습니다.