react.js

[react, recoil] 리코일 전역상태관리 (예제: 로그인 페이지)

코복이 2023. 7. 31. 17:40
728x90

 

1. Recoil 이란? 

리액트의 전역 상태관리 라이브러리로 리액트를 만든 메타(구페이스북)에서 제작
*유사품: Redux, useContext 등..

 

 

2. 설치 

Recoil 패키지는 npm에 존재한다. 안정한 최신 버전을 설치하기 위해서는 아래의 명령어를 실행

npm install recoil

yarn

yarn add recoil

 

 

 

3. Root

recoil 상태를 사용하기 위해선 컴포넌트가 <RecoilRoot> 로 감싸져있어야 한다. 

최상단에서 감싸주자.

import React from 'react';
import {
  RecoilRoot,
} from 'recoil';

function App() {
  return (
    <RecoilRoot>
      <CharacterCounter />
    </RecoilRoot>
  );
}

 

 

리코일은 크게 상태를 나타내는 Atom과 상태의 변화를 나타내는 Selector로 구성

 

 

4. Atom

Atom 상태(state)를 나타나며 어떤 컴포넌트에서나 읽고 쓸 수 있다.

atom의 값을 읽는 컴포넌트들은 암묵적으로 atom을 구독한다.

그래서 atom에 어떤 변화가 있으면 그 atom을 구독하는 모든 컴포넌트가 재 렌더링 되는 결과가 발생할 것이다.

import { atom } from "recoil"

export const textState = atom({
  key: 'textState', // unique ID (with respect to other atoms/selectors)
  default: '', // default value (aka initial value)
});

*atom은 key와 default 값으로 구성되어 있고 key는 다른 atom/selector 와 겹치지 않는 고유한 이름이어야 한다.

*default는 useState(***) ***여기에 넣는 것과 동일

 

 

4-1. 사용법

useState와 사용법이 비슷하다.

 

컴포넌트가 atom을 읽고 쓰게 하기 위해서는 useRecoilState()를 아래와 같이 사용하면 된다.

 

컴포넌트에서 state만 또는 setter 함수만 필요하다면 따로 불러올 수도 있다. (훅 이름이 다르다)

const state = useRecoilValue(atom)

const setState = useSetRecoilState(atom)

function CharacterCounter() {
  return (
    <div>
      <TextInput />
      <CharacterCount />
    </div>
  );
}

function TextInput() {
  const [text, setText] = useRecoilState(textState);

  const onChange = (event) => {
    setText(event.target.value);
  };

  return (
    <div>
      <input type="text" value={text} onChange={onChange} />
      <br />
      Echo: {text}
    </div>
  );
}

 

 

 

5. Selector

Selector는 상태를 순수 함수에 전달하여 처리하고 그 결과 값을 리턴한다.

import { selector } from "recoil"

export const charCountState = selector({
  key: 'charCountState', // unique ID (with respect to other atoms/selectors)
  get: ({get}) => {
    const text = get(textState);

    return text.length;
  },
});

 

 

 

5-1. 사용법

컴포넌트에 useRecoilValue() 훅 파라미터에 원하는 selector를 넣어 값을 받는다.

function CharacterCount() {
  const count = useRecoilValue(charCountState);

  return <>Character Count: {count}</>;
}

 

 

 

 

6. 예제: 로그인 페이지

* 사실 auth나 회원 관리는 firebase로 처리했고 로그인 컴포넌트에서 남은 상태는 이메일, 패스워드 validation 인데 이게 다른 컴포넌트에서 재사용될일도 없고.. 무튼 전역 상태관리를 위한 recoil의 예제 컴포넌트로는 적합하지 않지만 state를 대체하고 selector 사용을 연습하기 위한 예제인 점을 감안하자. 

 

 

로그인 컴포넌트

import { createUser, signInUser } from "../fireUtil";
import styles from "./LoginForm.module.css";
import { useRecoilState, useRecoilValue } from "recoil";
import {
  emailAtom,
  emailValidationAtom,
  pwAtom,
  pwValidationAtom,
} from "./recoil/Atoms";

const LoginForm = ({ haveAccount }) => {
  const [email, setEmail] = useRecoilState(emailAtom);
  const isValidEmail = useRecoilValue(emailValidationAtom);
  const [pw, setPw] = useRecoilState(pwAtom);
  const isValidPw = useRecoilValue(pwValidationAtom);

  const handleEmailInput = (e) => {
    setEmail(e.target.value);
  };

  const handlePwInput = (e) => {
    setPw(e.target.value);
  };

  /* 회원 생성 */
  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!isValidEmail || !isValidPw) {
      window.alert("이메일 또는 비밀번호가 올바르지 않습니다.");
    } else {
      await createUser(email, pw);
      setEmail("");
      setPw("");
    }
  };

  /* 회원 로그인 */
  const handleLogin = async (e) => {
    e.preventDefault();
    await signInUser(email, pw);
    setEmail("");
    setPw("");
  };

  return (
    <div className={styles.wrap}>
      {/* 로그인 폼 */}
      <form onSubmit={haveAccount ? handleLogin : handleSubmit}>
        {/* 이메일 */}
        <input
          className={styles.infoInput}
          name="email"
          onChange={handleEmailInput}
          value={email}
          placeholder="이메일"
          type="email"
        />
        {!isValidEmail && email.length > 1 && (
          <span className={styles.alert}>올바른 이메일을 입력해주세요.</span>
        )}

        {/* 패스워드 */}
        <input
          className={styles.infoInput}
          name="password"
          onChange={handlePwInput}
          value={pw}
          placeholder="비밀번호"
          type="password"
        />
        {!isValidPw && pw.length > 1 && (
          <span className={styles.alert}>
            영문, 숫자 조합 8자리 이상 입력해주세요.
          </span>
        )}

        {/* 제출 버튼 */}
        <input
          className={haveAccount ? styles.login : styles.signup}
          id={styles.submit}
          type="submit"
          value={haveAccount ? "로그인" : "계정 생성"}
        />
      </form>
    </div>
  );
};
export default LoginForm;

 

 

상태 저장소

import { atom, selector } from "recoil";

export const emailAtom = atom({
  key: "emailAtom", // 이름이며 전역적으로 유일한 이름
  default: "", // 초기값
});

export const emailValidationAtom = selector({
  key: "emailValidationAtom",
  get: ({ get }) => {
    const inputEmail = get(emailAtom);
    const regex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
    return regex.test(inputEmail); // true or false 반환
  },
});

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

export const pwValidationAtom = selector({
  key: "pwValidationAtom",
  get: ({ get }) => {
    const inputPw = get(pwAtom);
    const regex = /^(?=.*[a-zA-Z])(?=.*[0-9]).{8,25}$/;
    return regex.test(inputPw); // true or false 반환
  },
});

 

 

 

7. 느낀점

개인 프로젝트를 하며 prop chain에 대한 불편함을 몸소 느꼈다.

만들 때야 생각없이 줄줄 작성하기 좋지만 추후 수정/관리가 매우 불편했다.

전역 상태관리에 대한 필요성을 배웠고 REDUX도 숙지해보고 프로젝트에 따라

적절한 라이브러리를 선택해야겠다.

728x90