Recoil이란?

React.js를 만든 Facebook에서 만든 전역상태관리 라이브러리. 이 이상의 설명은 필요없을 듯 하다.

 

전역 상태란?

컴포넌트에서 쓰이는 상태값(state)가 아닌, 이곳저것에서 전역적으로 쓰이는 상태(state)를 말한다.

 

전역 상태를 사용하는 이유

굳이 안 써도 되긴 한다. 하지만 그렇게 된다면 서로 다른 컴포넌트끼리 특정한 값(= 상태 = state)를 공유하려면 공통된 상위 컴포넌트로부터 prop을 계속 전달하는 방법을 써야 하는데,

 

  • 중간에 거치는 컴포넌트들마다 하나하나 prop을 전달하게끔 코드 작성하는거 귀찮음
  • 중간에 거치는 컴포넌트들은 전달받은 prop을 하위 컴포넌트로 전달만 하고 사용은 안 할 수도. 즉 비효율적
  • 중간에 거치는 컴포넌트들이 많아지면 나중엔 prop을 추적하기가 힘들어짐

 

이런 문제들이 있다. 이 때 전역적으로 쓰는 상태를 만든다면, 어느 컴포넌트에서나 전역 상태에 접근가능하므로 위에서 말한 불편들이 해소될 수 있다!

 

그럼 수많은 전역관리 툴 중 Recoil을 사용하는 이유?

일단 따지고보면 context API는 전역상태관리를 해주는 기능이라기보다는 단순히 전역적으로 상태를 공유하게끔 해주는 기능이라고 보는게 정확하다. context API를 사용해서도 전역상태값을 변경하는 건 물론 가능하지만, context API를 사용해 상태값을 변경하면 provider로 감싸져있는 컴포넌트들이 죄다 렌더링된다는 치명적 단점이 있다! (provider 하위에서 context를 구독하는 모든 컴포넌트는 provider의 value가 바뀔 때마다 다시 렌더링된다는 말)

 

// ContextTest.jsx

export const myContext = createContext("JOFE");

const ContextTest = (props) => {
    const [name, setName] = useState("JOFE");
    
    return (
        <myContext.Provider value={[name, setName]}>
            <Welcome />
            <Hello />
        </myContext.Provider>
    )
}

 

// Welcome.jsx

const Welcome = (props) => {
    const [name, setName] = useContext(myContext)
    return (
        <div>
            <button onClick={() => {setName("Nunu")}}>이름바꾸기</button>
            {name}
        </div>
    )
}

 

// Hello.jsx

const Hello = (props) => {
    useEffect(() => {
        console.log("Hello 렌더링")
    })

    return (
        <>
        </>
    )
}

 

myContext라는 context의 value로 name이란 상태와 name을 변경해주는 세터함수를 넣어줬다. 그리고 이 context를 Welcome컴포넌트에서 사용하는 모습이다. Hello컴포넌트는 useEffect를 사용해서 렌더링될때마다 console에 Hello 렌더링이란 문구를 찍어준다.

 

 

이름바꾸기를 하면?

 

 

myContext를 사용하지 않는 Hello컴포넌트도 단순히 provider 안에 있다는 죄(?)로 다시 렌더링이 되는 모습을 확인할 수 있다..!

 

바로 여기서 Recoil이 이런 문제들을 해결해줄 수 있기 때문에 사용한다. 하단에서 보여드림.

 

Recoil 기초적인 사용법

일단 상위 컴포넌트에서 RecoilRoot란 컴포넌트로 애들을 감싸주면 된다.

 

import { RecoilRoot } from 'recoil';

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

 

그리고 atoms.js파일을 만들어서 요로코롬 atom들을 만들어준다.

※ atom = Recoil에서 사용하는 상태들의 명칭이자 단위라고 생각하면 됨. 이 atom들의 key들은 각각 고유한 값이어야 함! 중복되면 안 된다는 뜻

 

// atoms.js

import { atom } from "recoil";

export const nameState = atom({
    key: "name",
    default: "JOFE"
});

 

참고로 굳이 atoms.js에 만들 필요는 없는데 이렇게 전역상태들을 한 파일에 관리해주는 게 더 편하다고 한다.

 

// RecoilTest.jsx

const RecoilTest = (props) => {
    return (
        <div>
            <Welcome />
            <Hello />
        </div>
    )
}

 

 

// Welcome.jsx

const Welcome = (props) => {
    const [name, setName] = useRecoilState(nameState)
    return (
        <div>
            <button onClick={() => {setName("Nunu")}}>이름바꾸기</button>
            {name}
        </div>
    )
}

 

요로코롬 useRecoilState를 사용해서 React의 hook마냥 쓸 수 있다! React스럽다는게 최고 장점인 듯. 참고로 세터함수는 필요없고 값만 읽고 싶다면 useRecoilValue를 쓰면 된다.

 

암튼, 이렇게 하면

 

 

 

요로코롬 Hello컴포넌트는 리렌더링되지 않는다! 오직 해당 atom을 사용하는 애들만 바뀐다.

 

 

※ 참고한 글https://hong-jh.tistory.com/entry/Context-API%EB%8A%94-%EC%99%9C-%EC%93%B0%EA%B3%A0-%EA%B7%B8%EB%A0%87%EB%8B%A4%EB%A9%B4-Redux%EB%8A%94-%ED%95%84%EC%9A%94%EC%97%86%EC%9D%84%EA%B9%8C

 

Context API는 왜 쓰고, 그렇다면 Redux는 필요없을까?

안녕하세요 오늘은 제가 평소에 궁금했던 주제로 글을 써보려고 합니다. Context API의 기능과 왜 사용하는지, Context API의 리렌더링 이슈란 무엇인지 그리고 Redux를 대체할 수 있는지. 위 내용을 설

hong-jh.tistory.com

 

firebase와 연계해서 프로젝트를 진행하던 중, 로그인이후 Main page로 이동시킬 때 로그인된 user의 uid를 같이 보내야했다. 이를 어떻게 할 수 있을까?


UseNavigate

react router에서 지원하는 Hook. 지정한 경로로 페이지를 이동시킬 수 있다. 근데 두 번째 인자로 이동시킬 페이지에 함께 보낼 데이터를 지정할 수 있다! 예시는 다음과 같다.

import { useNavigate } from 'react-router-dom'; 

// ...생략

const navigate = useNavigate();

// ...생략

const gotoMain = () => {
  navigate("/main", {
    state: {
      userId: user.uid
    }
  });
};

참고로 이렇게 데이터를 보낼 때는 두 번째 인자의 state라는 속성으로 보내야 한다고 함. 

 

UseLocation

react router에서 지원하는 Hook. useNavigate를 이용해 전송된 데이터를 받을 수 있다. 예시는 다음과 같다.

import { useLocation } from 'react-router-dom'; 

// ...생략

const location = useLocation();

// ...생략

const [userId, setUserId] = useState(
  location.state?.userId
);

location의 state프로퍼티에 내가 보낸 데이터들이 담겨있다고 보면 된다. 

 

 

 

 

참고로, Link태그를 통해서도 데이터를 보낼 수 있다고 한다. 예시는 다음과 같다.

<Link to={`/main`} state={{ test: "hello world" }} >
   test
</Link>

 

또한 참고로, useNavigate의 두 번째 인자로는 state가 아니라 replace라는 것도 보낼 수 있다.

import { useNavigate } from 'react-router-dom'; 

// ...생략

const navigate = useNavigate();

// ...생략

const gotoMain = () => {
  navigate("/main", {replace: true});
};

기본값은 false이고, true로 돼있으면 페이지가 이동된 뒤 뒤로가기를 하더라도 방금 페이지로 돌아올 수 없고, 루트로 돌아오게 된다고 한다. false는 뒤로가기가 가능.

 

기본 : input태그의 value프로퍼티로 input태그의 값을 가져올 수 있다. 


다음과 같이 input태그들의 값을 담을 state를 만든다.

const [values, setValues] = useState({
  title: '',
  writer: '',
  content: '',
});

tite, writer, content는 각 input태그들이 갇는 name프로퍼티다. 편의를 위해 통일시킨 것임.

그리고 다음과 같이 handleChange함수를 만든다.

const handleChange = (e) => {
  setValues(prevValues => {
    const { name, value } = e.target;
    return {
      ...prevValues,
      [name]: value
    };
  };
};

각 input 태그들에 다음과 같이 onChange프로퍼티로 멕여준다.

<form>
  <input type="text" name="title" value={values.title} onChange={handleChange} />
  <input type="text" name="writer" value={values.writer} onChange={handleChange} />
  <input type="text" name="content" value={values.content} onChange={handleChange} />
  <button type="submit">제출</button>
</form>

리액트에서의 onChange에서는 순수JS에서의 oninput이벤트와 같다. 즉 뭔가 입력할 때마다 발생! 이로 인해 input태그의 값들과 values state는 항상 같은 값을 지니게 된다.

 

그리고 제출 버튼을 누르면 동작할 form태그의 submit이벤트에 대해선 고냥 values state의 값들 이용하면 된다. 이 방법을 배우기 전까지는 제출 버튼이 눌리면 그 때 input태그의 값들을 읽어오는 방식을 써야 된다고 생각했는데, 버튼이 눌리기 전에 위에서 한 것처럼 state에 input태그 값들을 onChange를 통해서 일치시키고 제출이 눌리면 바로 state를 활용하는 방법을 쓸 수도 있다는 걸 배웠다.

 

참고로 form태그는 submit이벤트에 대해 기본적으로 GET메소드로 input태그들의 값을 보내니까 이를 방지할 것. 방지하는 방법은 e.preventDefault()를 쓰면 됨.

 

또한 위 코드에서 input태그들에 value 프로퍼티를 딱히 줄 필욘 없음. 저게 없어도 input태그 값 가져오는 건 가능하니까. 다만 저렇게 하면 항상 state값 == input태그값이 됨 즉 input태그의 값이 제어되는 효과를 준다. 

예를 들어, input태그에서 소문자로 입력을 해도 setState에서는 state에 upper를 해서 저장해둔다면 input태그의 값들도 대문자로 알아서 바뀌는 효과를 줄 수 있음. 즉 input태그의 값이 제어되는 것! 이런 컴포넌트를 controlled component라 하고, 쓰나 안 쓰나라면 controlled component로 하는 게 좋다고 한다.

 

 

※ 참조

https://ko.reactjs.org/docs/forms.html

 

폼 – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

순수 JS에서, 

 

  • input 이벤트 : 사용자가 input태그에 뭔가 입력할 때마다 발생하는 이벤트. 빈 input태그에 "abcd"라는 문자열을 입력했다면 a, b, c, d를 입력할 때 각각 input이벤트가 발생한다.
  • change 이벤트 : input태그의 값이 변했을 때 발생하는 이벤트. 입력이 끝나기 전과 후를 비교해 값이 변했을 때 발생한다. input태그에 원래 있던 값이 "a"였다가 "ab"로 바뀔 때 등등. 이 때 "사용자의 입력이 끝났다"로 인식되는 건 인풋 태그의 포커스가 풀릴 때, 사용자가 Enter를 입력했을 때 등등이 있다

 

근데 React에선 사용자가 뭔가 입력할 때마다의 이벤트를 받으려면 onInput으로 하지 말고 onChange로 해야 한다. 리액트 개발자들이 onChange가 좀 더 직관적이라고 생각해서 그렇게 한 거라고 함.

useEffect ?

React 컴포넌트가 렌더링될 때마다 특정 작업(Side effect)을 실행할 수 있도록 하는 리액트 Hook이다. 여기서 side effect란 컴포넌트가 렌더링된 후 처리되어야 하는 부수적인 효과들을 말하며, 컴포넌트가 렌더링된 이후 비동기적으로 처리되는 작업들을 일컫는다. 이를 통해 함수형 컴포넌트에서도 클래스형 컴포넌트에서 쓰던 생명주기 메소드들(ex : componentDidMount)들을 쓸 수 있게 됐다.

useEffect는 실행되면 인자로 받은 콜백을 예약해뒀다가, 컴포넌트가 렌더링되면 예약해둔 콜백을 실행한다.

 

기본적인 형태

useEffect(function, deps);

// ex
useEffect(() => {
  document.title = `총 ${count}개`;
}, [count]);

function : 실행하고자 하는 함수 즉 side effect

deps : 의존성 배열. 이 배열 안의 값이 변경되는 경우에만 side effect가 실행됨. 빈 배열을 줄 경우 해당 컴포넌트가 최초 렌더링될때만(즉 생성될 때만) side effect가 실행되고, 배열 자체를 주지 않을 경우 컴포넌트가 렌더링될때마다 side effect가 수행됨.

 

※ 여기서 말하는 컴포넌트란 useEffect가 작성된 컴포넌트를 말함

 

 

Advanced

1. deps의 종류에 따른 side effect 수행

  • deps로 뭔가 값이 들어있는 배열을 준 경우 : 컴포넌트가 마운트될때(생성될 때)와 deps배열에 작성된 값들이 변경될 때만 side effect가 실행
  • deps로 빈 배열을 준 경우 : 컴포넌트가 마운트될때만 side effect가 실행 (ComponentDidMount만 표현한 느낌)
  • deps로 아무것도 주지 않은 경우 : 컴포넌트가 마운트될때, 컴포넌트가 렌더링될 때마다 side effect가 수행됨(ComponentDidMount와 ComponentDidUpdate를 표현한 느낌)

 

※좀 더 정확히 설명하면..

  1.  컴포넌트가 마운트될 때 콜백을 예약하고 deps 리스트 안의 값들을 기억함. 이후 콜백을 실행.
  2.  state변경 등으로 컴포넌트가 리렌더링되면 useEffect가 다시 실행됨. 이 때 새로 실행되는 useEffect의 deps 리스트 안의 값들을 전에 기억해두고 있던 deps 리스트의 값들과 비교
  3.  달라진 게 있다면 새로 실행된 useEffect의 콜백을 예약하고 렌더링이 끝나면 실행함. 그러나 달라진 게 없으면 콜백을 예약하지 않음(즉 렌더링 끝나도 실행되지 않음).
  4.  때문에 deps를 빈 배열로 주면 항상 전에 기억하던 것과 같은 거니까 이후에 다시 컴포넌트가 렌더링되도 콜백을 수행하지 않음 즉 최초 마운트할 때만 실행하는 꼴이 됨. deps안에 내용물이 뭔가 있을 때, 컴포넌트가 리렌더링되도 deps값이 변한 게 없으면 콜백을 실행하지 않음 즉 deps안의 값이 변경될 경우에만 콜백(side effect)가 실행되는 꼴

 

2. 컴포넌트가 unmount될때 수행할 side effect (clean up)

: side effect로 작성한 함수에서 특정 함수를 return하게 하면 clean up기능을 사용할 수 있다. 덕분에 컴포넌트가 unmount될 시 리턴하는 함수가 실행되게 할 수 있다. useEffect를 통한 side effect는 여러 번 실행될 수 있기 때문에 메모리 누수 등을 막기 위해 이런 clean up이 필요한 경우가 많다. 참고로 컴포넌트가 unmount될 때 실행되므로 컴포넌트가 업데이트될 때(리렌더링될 때)에도 수행된다. 이 경우는 리렌더링이 먼저 된 후 clean up이 수행되고, 새로운 side effect가 실행된다. 즉 useEffect에서 clean up의 동작 순서는

 

  1. props나 state가 update됨
  2. 컴포넌트가 리렌더링됨
  3. 이전 side effect의 clean up이 수행
  4. 새로운 side effect 수행

 

이다.

 

※ clean up을 쓰는 또 다른 이유

: 클로저 때문이다. useEffect에서 side effect로 주는 함수는 자기가 생성될 때의 값을 바라보기 때문. 즉 side effect내에서 참조하는 state가 가장 최신의 state가 아닐 수도 있다. 이 때 clean up이 있다면 과거의 변수 값을 참조하던 effect가 정리되고 새로운 변수 값을 다시 참조할 수 있게 된다. 

 

 

+ Recent posts