본문 바로가기

TIL/React

useState(useRecoilState)를 동기적으로 처리하는 방법

문제 상황

아래와 같은 코드를 통해

 

(1)로그인 성공 → (2)setIsLogin 함수 실행(isLogin = true) → (3)navigate 함수 실행

 

의 순서로 진행되는 로직을 구현하려 했다.

...
const [isLogin, setIsLogin] = useRecoilState(isLoginState);

const getToken = async () => {
    await axios({
      method: "GET",
      url: `${process.env.REACT_APP_BASE_URL}/auth/kakao/callback`,
      params: {
        authCode,
      },
      withCredentials: true,
    })
      .then((res) => {
		// (1) 로그인 성공
        onLoginSuccess(res).then(() => {
			// (2)setIsLogin 함수 실행(isLogin = true)
          setIsLogin(true);
          const requestPath = requestUrl.split(
            process.env.REACT_APP_CLIENT_URL
          )[1];
			// "false"
          alert(isLogin);
			// (3)navigate 함수 실행
          navigate(requestPath);
        });
      })
      .catch(onLoginFail);
  };

  useEffect(() => {
    getToken();
  }, []);

 

 

이는 ‘사용자의 로그인 여부에 따라 라우터의 접근 가능 여부를 설정’하는 로직을 구현하기 위함으로,

 

이를 위해 로그인 여부 확인이 필요한 페이지들의 상위 라우터로 다음과 같이 PrivateRoute를 설정해둔 상태이다.

 

// PrivateRoute.jsx
function PrivateRoute() {
  const isLogin = useRecoilValue(isLoginState);

  console.log(isLogin);

  const requestUrl = window.location.href;

	// "true"
  alert(isLogin);

  if (isLogin) {
    return <Outlet />;
  } else {
    alert("로그인 유효 시간이 만료되었습니다. 다시 로그인해주세요.");
    if (requestUrl) {
      return <Navigate to={`/login?requestUrl=${requestUrl}`} />;
    } else {
      return <Navigate to={`/login`} />;
    }
  }
}

// atom.js
const { persistAtom } = recoilPersist();

export const isLoginState = atom({
  key: "isLogin",
  default: false,
  effects_UNSTABLE: [persistAtom],
});
...

 

위와 같이 작성하여 실행한 결과, 필자가 의도한 ‘로그인 여부에 따른 라우팅 차단’은 정상적으로 작동하였다.

 

하지만 setIsLogin이 정확히 어느 시점에 실행되는지 파악하기 위해 isLogin 값에 대해 alert를 찍어본 결과,

실제 작동 순서는 (1) → (2) → (3)이 아닌, (1) → (3) → (2)였다. (alert : ”false” → “true”)

 

setIsLogin 직후의 alert(isLogin)은 아직 state변경이 반영되지 않아 “false”로 찍혔고,

 

navigate를 통해 페이지를 이동하며 호출된 PrivateRoute.jsx의 alert(isLogin)은 state 변경이 반영되어 “true”로 찍힌 것이다.

 

 

 

 

해결

이는 setState의 비동기적 실행 방식 때문에 발생한 문제이다.

더보기

useState(setState)의 비동기적 작동 방식

간단히 말해, state 변경에 따른 과도한 리렌더링을 방지하기 위해 useState는 비동기적으로 작동한다.

자세한 내용은 다음 글에 잘 정리되어 있으니 참고하면 될 듯 하다.

 

[React] useState의 비동기적 동작

 

[React] useState의 비동기적 동작

1. setState의 비동기적 동작 함수형 컴포넌트로 간단한 카운터를 만들었습니다. import React, {useState} from 'react'; const Counter2 = () => { const [count, setCount] = useState(0); const onClick = () => { setCount(count+1); consol

bamtory29.tistory.com

 

 

setState를 동기적으로 사용하려면, useEffect의 의존성 배열을 이용하면 된다.

 

useEffect의 의존성 배열에 state를 넣으면, ‘해당 state 값이 변경 될때’ useEffect 내의 함수를 실행하게 되므로,

 

다음과 같이 isLogin이 변경될때(setIsLogin이 실행될때) navigate가 실행되도록 할 수 있다.

 

useEffect(() => {
    const requestPath = requestUrl.split(process.env.REACT_APP_CLIENT_URL)[1];
    alert(isLogin);
    navigate(requestPath);
  }, [isLogin]);

 

그러나 여기서 끝이 아니다.

 

useEffect의 의존성 배열에 state 값을 넣더라도 useEffect는 state의 변경 시 뿐 아니라 ‘첫 렌더링 시’에도 실행된다.

 

따라서 위와 같이 작성하면 첫 렌더링 시 navigate 함수가 가장 먼저 호출되어, 의도치 않게 동작하게 된다.

 

의도대로 작동하게 하기 위해서는, useEffect가 첫 렌더링 시에는 실행되지 않도록 설정해야 한다.

 

⇒ 다음과 같이 useRef를 활용하여 custom Hook을 생성하여 해결하였다.

 

Ref(reference)는 useRef 함수를 통해 생성하며,

‘컴포넌트가 리렌더링되어도 값이 초기화되지 않는’ 특징을 가진다.

더보기

useRef란?

보통 react에서 DOM 요소에 직접 접근하기 위해 사용되는 Hook이다.

  • 컴포넌트가 리렌더링 될 때에도 Ref 값은 변경되지 않고,
  • Ref값이 변경될 때에도 컴포넌트는 리렌더링되지 않는

특징을 가진다.

자세한 내용은 아래 링크 참고.

 

[React] useRef 사용법 및 예제

 

[React] useRef 사용법 및 예제

[useRef] useRef는 저장공간(변수 관리), DOM 요소에 접근을 위해 사용이 되는 React hooks입니다. Ref는 'reference'의 약자로, '참조'라는 뜻입니다. DOM 요소를 참조해서 이렇게 지었을까요? 1. 저장공간(변수

itprogramming119.tistory.com

 

이를 이용하여 다음과 같이 커스텀 훅을 생성하여 최종 해결하였다.

 

// hooks/useDidMountEffect.js
...
export const useDidMountEffect = (func, deps) => {
  // useRef는 컴포넌트가 리렌더링되어도 값이 유지됨
  const didMount = useRef(false);

  useEffect(() => {
    // 첫 렌더링이 아닐 경우(Ref값이 true) => 함수 실행
    if (didMount.current) func();
    // 첫 렌더링일 경우 => Ref값을 true로 변경. 함수 실행 x
    else didMount.current = true;
  }, deps);
};

// KakaoCallback.jsx
...
const getToken = async () => {
  ...
    .then((res) => {
      onLoginSuccess(res).then(() => {
        setIsLogin(true);
      });
    })
    .catch(onLoginFail);
};

...

useDidMountEffect(() => {
  const requestPath = requestUrl.split(process.env.REACT_APP_CLIENT_URL)[1];
  navigate(requestPath);
}, [isLogin]);

 

 

 

 

References

https://codingapple.com/unit/react-setstate-async-problems/

 

state 변경함수 사용할 때 주의점 : async - 코딩애플 온라인 강좌

(짧아서 글로 진행합니다) 자바스크립트의 sync / async 관련 상식 자바스크립트는 일반적인 코드를 작성하면 synchronous 하게 처리됩니다. 번역하면 동기방식 이런데..  뭔소리냐면 코드 적은 순서

codingapple.com

https://velog.io/@rootcho/useEffect-%EC%B2%AB-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%8B%9C-%ED%95%A8%EC%88%98-%ED%98%B8%EC%B6%9C-%EB%A7%89%EA%B8%B0useDidMountEffect-hook

 

useEffect 첫 렌더링 시 함수 호출 막기(useDidMountEffect hook)

 

velog.io