새로운 상태관리 라이브러리의 탄생, Recoiljs

Juno Cho·2020년 5월 18일
7
post-thumbnail

서문

얼마 전, 페이스북에서 따끈따끈한 상태관리 라이브러리가 하나 더 탄생했다. 바로 Recoiljs다.

https://recoiljs.org/

지금까지 가장 유명한 상태관리 라이브러리는 크게 세 가지 정도 분류할 수 있을 듯 하다. Context API, Redux, MobX.

특히 Redux는 압도적인 플러그인을 무기로 상당히 높은 점유율을 보이고 있는데... 주변 사람들의 Redux를 보는 시선은 그렇게 좋지만은 않다. 약간 적폐...?로 보는 느낌?

나는 대부분 규모가 크지 않은 서비스를 주로 만들어 왔기 때문에 상태관리 라이브러리에 대한 필요성이 그렇게 크진 않았는데 그럼에도 불구하고 이런 경우에는 정말 눈물난다.

import React, { useState } from 'react';
import ChildComponent from './ChildComponent';

export default function func() {
  const [parentState, setParentState] = useState(null);
  const [secondParentState, setSecondParentState] = useState(null);
  const [thirdParentState, setThirdParentState] = useState(null);
  ...
  const [tenthParentState, setTenthParentState] = useState(null);
  
  return (
    <>
      <ChildComponent
        parentState={parentState}
        secondParentState={secondParentState}
        thirdParentState={thirdParentState}
	...
        tenthParentState={tenthParentState}
      />
    </>

딱히 과장한 건 아니다. 정말로 저런 경우가 꽤 많다. 아래 열 개의 props를 필요로 하는 SecondChildComponent가 하나 더 존재해도 무리가 없다.

상태관리 라이브러리가 필요한 시점이다.


터줏대감 Redux

"상태관리 라이브러리 뭐 쓸까요?" 라는 질문에 보편적인 대답은 "규모가 크지 않으면 Context 쓰시고 규모가 크면 Redux 쓰세요." 라는 것인데 사실 좀 애매모호한 답변이라고 생각한다. 차라리 다 한 번쯤 써보고 자기한테 맞는 걸 고르는 편이 나을 지도 모른다.

한번 더 후술할 이야기지만 일단 Redux는 명성에 비해 비교적 복잡한 편이다. 이 글은 Recoil을 소개할 글이니 Redux는 간단하게만 짚고 넘어가도록 하겠다. (Next.js 적용기에 Redux 관련 글이 올라갈 예정이다.)

Redux는 크게 아래 세 가지로 이루어진다.
(1) Action: 단순 문자열로 구성된 행위. 아무 능력도 없다.
(2) Reducer: Action 행위에 따른 실질적인 행동을 수행.
(3) Store: 각 컴포넌트에 다이렉트로 상태를 주입시켜 주기 위해 존재하는 저장 공간.

자, 요즘은 redux에도 hook이 등장해서 (useSelector, useDispatch) 기존의 connect를 활용한 mapStateToProps, mapDispatchToProps를 쓰지 않아도 좀 더 간단하게 Redux를 사용할 수 있다.

또한 2019년에 Dan Abramov가 컨테이너 컴포넌트와 프레젠테이셔널 컴포넌트의 분류가 필수적이지 않다고 선언한 후로는 custom hook을 통해 보다 모던한 구조를 만들 수도 있다.

관련 내용은 프론트엔드 강의와 리액트 책 출간으로 유명하신 Velopert님의 블로그에 잘 정리되어 있다.

Dan Abramov의 원본 글
https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0
Velopert 님의 React + Typescript + Redux 글
https://velog.io/@velopert/use-typescript-and-redux-like-a-pro

어쨌든 요즘은 액션과 리듀서를 한 파일에 작성하는 Ducks 패턴을 많이 사용하므로
(1) 액션과 리듀서를 정의하는 파일을 생성하고
(2) 각 리듀서들을 combineReducers를 통해 하나로 규합한 뒤
(3) useSelector, useDispatch를 활용하여 custom hook을 만들고
(4) store를 만들어 index.js에 Provider를 통해 적용시키고
(5) 필요한 컴포넌트에서 import 해서 사용한다.

음... 사실 익숙해지면 자연스러운 흐름인데 뭔가 할 게 많아보이기도 한다.


Recoiljs

Redux에 Action과 Reducer가 있다면 Recoil에는 Atom과 Selector가 있다.

프로젝트를 하나 만들어서 직접 사용해 보도록 하자.

npx create-react-app my-recoil-app
cd my-recoil-app
npm start

다음은 Recoil을 설치하자.

npm install recoil
# or
yarn add recoil

그 다음 RecoilRoot를 root component에 적용시켜 주는 것만으로 기본적인 셋팅은 끝난다.

나는 css를 신경 쓰기 싫기 때문에 reactstrap도 같이 적용시켜 주도록 하겠다.

npm install --save bootstrap reactstrap
// index.js

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { RecoilRoot } from "recoil";

// reactstrap 적용을 위한 import
import 'bootstrap/dist/css/bootstrap.min.css';

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

이제 Recoil에서 사용할 수 있는 세 가지 훅을 모두 사용할 수 있도록 공식 홈페이지 데모를 응용해보도록 하자.

input을 통해 임의의 문자열을 받고 해당 문자열과 문자열의 길이를 반환하는 컴포넌트. 그리고 문자열을 뒤집는 버튼을 하나 추가해 보자.

Recoil은 나온 지 얼마 안 되었기 때문에 Redux처럼 딱히 정형화 된 패턴이 없다.
아래 방식은 순전히 본인의 스타일대로 짠 것이기 때문에 꼭 이대로 할 필요는 없다.

// src/modules/appRecoil.js

import { atom, selector } from "recoil";

export const textState = atom({
  key: "app/textState",
  default: ""
});

// get을 통해 textState 값을 이끌어 내 length를 반환한다.
export const textLength = selector({
  key: "app/textLength",
  get: ({ get }) => {
    const text = get(textState);

    return text.length;
  }
});

// get을 통해 뒤집힌 문자열을 반환하고 set을 통해 뒤집힌 문자열을 textState에 set 시켜준다.
export const reverseText = selector({
  key: "app/reverseText",
  get: ({ get }) =>
    get(textState).split("").reverse().join(""),
  set: ({ set }, newValue) =>
    set(
      textState,
      newValue.split("").reverse().join("")
    )
});

앞서 말한 것처럼 Recoil은 Atom과 Selector 두 가지 대표 요소를 가지고 있다. Atom은 단순히 고유한 키 값과 초기 값을 가지며, Selector는 다른 곳에서 얻어진 값(derived state)을 토대로 하는 요소다.

textState는 빈 문자열을 초기 값으로 갖는 요소이며, textLength는 textState의 값을 get을 통해 이끌어 내 해당 값의 length를 반환하는 요소이다.

만들어 진 Atom이나 Selector를 가져다 사용하는 방법은 크게 세 가지가 있다. useState와 비슷하다는 느낌을 많이 받았다.
(1) useRecoilState: 값도 필요하고 값을 set하는 것도 필요할 때. (state + setState)
(2) useRecoilValue: 값을 사용하기만 할 때. (state)
(3) useSetRecoilState: 값을 set하기만 할 때. (setState)

상기 세 가지 Hook 이외에도 더 존재한다. 자세한 것은 공식 홈페이지 참고
https://recoiljs.org/docs/api-reference/core/useRecoilState

// App.js

import React from "react";
import { Form, FormGroup, Label, Input } from "reactstrap";
import { useSetRecoilState } from "recoil";

import { textState } from "./modules/appRecoil";
import TextInput from "./components/TextInput";
import TextLength from "./components/TextLength";

function App() {
  // 해당 컴포넌트에서는 input에 onChange가 발생할 때마다 text의 값을 set할 것이다.
  // 값 자체를 사용할 일이 없으니 useSetRecoilState를 사용하자.
  const setText = useSetRecoilState(textState);

  const changeInput = e => {
    const inputTarget = e.target.value;

    // useState의 setState 사용하듯 사용하면 된다.
    setText(inputTarget);
  };

  return (
    <>
      <Form>
        <FormGroup>
          <Label>Your Input</Label>
          <Input
            type="text"
            name="customInput"
            id="customInput"
            placeholder="아무거나 입력하세요."
            onChange={changeInput}
          />
        </FormGroup>
      </Form>
      <TextInput />
      <TextLength />
    </>
  );
}

export default App;
// src/components/TextInput.js

import React from "react";
import { useRecoilValue } from "recoil";

import { textState } from "../modules/appRecoil";

export default function TextInput() {
  // App.js에 있는 input의 값을 출력할 공간이다.
  // 값만 사용할 것이므로 useRecoilValue를 사용하자.
  const text = useRecoilValue(textState);

  return (
    <>
      <p>Your Input is {text}</p>
    </>
  );
}
// src/components/TextLength.js

import React from "react";
import { useRecoilValue } from "recoil";

import { textLength } from "../modules/appRecoil";
import ReverseTextButton from "./ReverseTextButton";

export default function TextLength() {
  // 마찬가지로 값만 사용할 것이다. Selector도 Atom과 호출 방식은 동일하다.
  const length = useRecoilValue(textLength);

  return (
    <>
      <p>Input String's length is {length}</p>
      <ReverseTextButton />
    </>
  );
}
// src/components/ReverseTextButton.js

import React, { useState } from "react";
import { useRecoilValue, useRecoilState } from "recoil";
import { Button } from "reactstrap";

import { textState, reverseText } from "../modules/appRecoil";

export default function ReverseTextButton() {
  const [isClick, setIsClick] = useState(false);

  const text = useRecoilValue(textState);
  // useRecoilState는 형태가 useState와 굉장히 유사하다.
  const [convertedText, setConvertedText] = useRecoilState(reverseText);

  const clickReverseString = () => {
    setIsClick(true);
    // 뒤집힌 문자열을 textState에 set 시킨다.
    setConvertedText(text);
  };

  return (
    <>
      <Button onClick={clickReverseString}>Reverse String</Button>
      // 단순히 textState의 값을 get을 통해 이끌어 낸 후, 뒤집은 값을 반환한다.
      {!isClick && <p>Converted String is {convertedText}</p>}
    </>
  );
}

동작해보면 아래와 같이 나타난다.


Redux vs. Recoil

나는 이번 소규모 프로젝트에 Recoil을 도입해보고 있는데 처음엔 좀 갸우뚱 싶다가도 확실히 Redux에 비해 가벼우면서도 '상태관리'라는 본연의 기능에 집중하고 있다는 생각이 들었다.

반면 Redux처럼 컴포넌트 코드의 state 코드를 완전히 분리해낼 수는 없기 때문에 변수 관련 코드를 별개 파일로 떨어뜨려내고 싶은 사람의 경우에는 조금 불만족스러울 수도 있겠다는 생각도 든다.

아직 Recoil은 따끈따끈한 신상이기 때문에 앞으로 더 많은 기능이 나타나거나 보강될 것이다. 지금 이 글에서도 언급하지 않은 내용들이 공식 홈페이지에 나와 있으니 관심 있으신 분들은 한 번 둘러보길 바란다.

profile
FE Developer && Vocal && Alcohol Enthusiast

0개의 댓글