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는 뒤로가기가 가능.

 

강의를 들으면서 Ref를 통해 Input태그의 값을 받아오는 걸 보곤 했지만, 무슨 이유로 Ref를 쓰는지는 정확히 몰랐었다.

이번 기회에 정리해보자.


Ref

리액트에서 원하는 시점에 실제 DOM노드에 접근하고 싶을 때 사용하는 놈이다. 다음과 같이 Ref객체를 생성할 수 있다.

import { useRef } from 'react';

// ...생략

const inputRef = useRef();

그리고, 만들어진 Ref객체는 다음과 같이 원하는 DOM요소에 ref속성을 통해 연결시킬 수 있다.

<input type="text" name="title" ref=[inputRef} />

이 Ref라는 객체는 console.log로 찍어보면 알 수 있지만 current라는 프로퍼티를 가지며, current프로퍼티는 ref속성으로 연결된 DOM노드를 가리킨다. 즉 Ref객체의 current속성을 이용해 연결된 DOM노드에 접근하여 value등을 읽어올 수 있는 것!

 

다만 주의할 점은, DOM노드는 렌더링이 끝나야만 생기니까 Ref객체의 current값 역시 컴포넌트가 화면에 렌더링됐을 때만 존재한다는 것. 따라서 Ref를 통해 DOM노드에 접근할 때는 Ref.current를 확인한 다음 사용하도록 하자.

 

그래..이래서 Ref의 current로 인풋 태그의 값을 읽어오고 뭐 그랬던 거구나~으흠~

한마디로 Ref를 사용하면 DOM노드를 직접적으로 건드릴 수 있구나~(포커스를 준다던가..)

 

..? 근데 Ref 왜 쓰는 거지?

자바스크립트에선 querySelector로 그냥 잡아올 수 있는데 말이다.


Ref를 쓰는 이유

id, class를 통해서 바닐라JS로 DOM조작이 가능한 건 맞지만, 컴포넌트가 여러 개 생긴 경우 id나 class로 원하는 DOM요소를 가져오는 건 매우 힘들다. 이 때 Ref를 통해 원하는 DOM요소를 특정하도록 할 수 있다.

 

기본 : 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

 

개발을 하다가 컴포넌트가 렌더링될 때 2번 렌더링되는 현상을 목격했다. 사실 이전에 다른 플젝을 할 때도 렌더링이 두 번되는 경우를 종종 봤었는데(useEffect로 예약걸어둔 함수가 두 번 실행되는..) 이번에 이게 왜 이러지 라는 생각이 들어서 알아봤다.

 

일단 결론부터 말하자면, index.js에서 React.StrictMode가 쓰여서 그렇다. 때문에 StrictMode가 적용된 걸 주석처리해주면 2번 렌더링되는 현상을 해결할 수 있다.

 

https://ko.reactjs.org/docs/strict-mode.html#gatsby-focus-wrapper

 

Strict 모드 – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

이 놈은 개발모드에서만 활성화되는 녀석으로 UI적인 렌더링은 없고, 자식들에 대한 부가적인 검사와 경고를 활성화하는 것을 통해 개발에 도움을 주는 모드라고 한다. 

위 공식문서에 쓰여있다시피, StrictMode가 적용돼있으면 두 번 렌더링을 하여 생명주기 메소드들과 관련된 쉽게 오류들을 찾을 수 있게끔 도와주는거라고 한다.

 

한마디로 두 번 렌더링되는 건 내가 오류없이 잘 코딩하고 있다면 문제될 얘기가 아니라는 뜻인 듯.

+ Recent posts