시작하는 중

react-redux의 dispatch의 return은 무엇일까 본문

react

react-redux의 dispatch의 return은 무엇일까

싱욱 2023. 3. 27. 22:13

네비게이션 바에서 로그아웃 버튼을 누르면 redux로 관리하고 있는 토큰을 지우면서 로그인 페이지로 리다이렉트 되게 하고 싶었다. 하지만 됐다면 글을 안썼을 것이다 ㅋㅋ

문제는 안된다는 것! 일단 로그아웃 하고서 다시 해당 페이지로 돌아와지고

다시 로그아웃을 하면, 그제서야 로그인 페이지로 이동한다..

 

logoutHandler는 다음과 같은 redux thunk 함수와 login 컴포넌트의 일부분이다.

// In auth.js
export const logoutHandler = () => {
  console.log("logout handler start");
  return async (dispatch) => {
    try {
      const response = await logoutAPI();
      ...
      // 응답이 정상이라면 토큰이 지워지는 로직
      
// In loginComponent
...
  useEffect(() => {
    if (token) {
      navigate("/");
    }
  }, [token, navigate]);
  ...

왜 안될까? 그 전에, dispatch의 리턴 값은 어떻게 된 것인지를 생각해야할 것 같다.

함수인 이상 리턴은 분명히 존재한다. 심지어 undefined라도 존재할 것이다.

그래서 dispatch를 들여다 보기로 했다.


https://react-redux.js.org/api/hooks#usedispatch

 

Hooks | React Redux

API > Hooks: the `useSelector` and `useDispatch` hooks`

react-redux.js.org

dispatch는 useDispatch의 return 값이다. 근데 docs를 아무리 찾아봐도 dispatch가 어떻게 동작하는지 전혀 모르겠어서,

module안의 dispatch로 다가가 보기로 했다.

다가가다 보니까

이런게 나왔다. 마지막 구문을 해석해보면, 미들웨어로 감싸진 dispatch 함수는 비동기 action도 허락될 수 있다고 한다.

그렇다면, dispatch에 들어가는 인자로써의 action은 비동기를 구분가능하다는 것 아닐까?


그렇다면 dispatch는 특정 리턴값을 반환하지 않을까? 싶어서 redux thunk action 함수에 return을 주고 dispatch실행 구문에 할당하였다. 그리고 console.log로 확인해봤다.

 

logoutHandler에 return 1을 주었고 dispatch 함수를 통해, redux의 action을 불러오는 코드를

 const logout = () => {
    const reduxActionResult = dispatch(logoutHandler());
    console.log(reduxActionResult);
    navigate("/login");
  };

이렇게 하니까, 결과가

return 1을 했으나 undefined가 나왔으나, 어쨌든 promise가 fulfilled 되어 나온 모습!

그러면 dispatch는 미들웨어단에서 항상 비동기적으로 작동할까?? 하고 thunk함수를 async가 아닌 일반 동기함수로 바꾸고, ajax 요청을 await가 아닌 then chaining으로 처리했다.

내 생각과는 다르게 1이 나왔다?? 근데 나는 await같은 비동기 로직을 NavBar에서 지정하지 않았다.

 

이것은 그저 숫자 1이다... 그렇다면, dispatch는 인자로 오는 함수를 처리한 결과를 주는 것이 아닐까? 싶었다.

 

내 생각이 맞다면

1. dispatch할 action에 async를 붙이지 않는다면, NavBar에서 then chaining이 불가능 할 것이고

2. dispatch할 action에 async를 붙인다면, NavBar에서 async, await를 사용하여 지금은 promise 객체로 나오지만, 1을 기대할 수 있을 것이다.

1번은 역시 then chaining이 불가능한 모습..

그렇다면 2번도?

역시 잘 나와주는 모습..


그렇다면, dispatch의 과정을 내가 공부했던 이벤트 루프를 기반으로 살펴보기 결정했다.

먼저, 내 코드가 왜 안되는지를 생각해야 했기에 어떻게 동작할지 생각했다.

1. 로그아웃 버튼을 누름으로 인해서 onClick event가 트리거 되어 logout이 실행된다.

2. 이 함수는 promise나, promise에 따른 then 체이닝이 존재하지 않으니 dispatch와 navigate를 순차적으로 실행시킨다.

const logout = () => {
    dispatch(logoutHandler());
    navigate("/login");
  };

3. logoutHandler는 즉시 return으로 async 함수를 실행시키기에 promise를 생성한다.

4. async 함수의 약속에 따라서, await전까지는 동기적으로 실행된다. 이 글의 설명에 따라서, promise안의 콜백 함수는 순차적으로 실행되며 이 코드의 실행 순서에 따라, 실행되는 함수는 콜 스택을 차지하게 된다.

5. 때문에,

export const logoutHandler = () => {
  return async (dispatch) => {
    try {
      const response = await axios(...);
      if (response.status !== 200) {
        throw new Error(response.data);
      }

      dispatch(authActions.logout()); // 토큰 값을 초기화하는 action함수를 dispatch

      return 1;
    } catch (error) {
      console.log("logout error: ", error);
    }
  };
};

이 코드에 따라서, 브라우저 환경에서 axios에 의해 ajax요청이 실행되게 된다.

await이기에 기다리게 된다. 사실 근데 여긴 중요하지 않다 ㅋㅋㅋ

6. 중요한 것은 코드가 함수를 따라 실행되다가, 어떤 함수가 브라우저 환경에 맡겨지고 await를 통해 이를 기다려야되는 순간, js는 async함수를 나와서 나머지 코드를 실행시킨다.

 

// auth.js
export const logoutHandler = () => {
  console.log("logout handler start");
  return async (dispatch) => {
    try {
      console.log(1);
      const response = await setTimeout(() => console.log("setTimeout"), 100);
      console.log(2);
      if (response.status !== 200) {
        throw new Error(response.data.message);
      }
      // 토큰이 redux에서 지워지는 로직
      ...
      
// NavBar.jsx
...
const logout = () => {
    dispatch(logoutHandler());
    console.log("a");
    navigate("/login");
    console.log("b");
  };
...

setTimeout에 원래 axios 요청이 들어가는데! 뭘 넣던 똑같다. 중요한 것은 실행 순서인 것이다.

message에러는, setTimeout 대신에 원래 ajax요청이라서 try, catch에서 catch로 빠지는 것을 핸들링 한 결과이니까 신경쓰지 않고,

중요한 것은 1, a, b, 2, setTimeout 역시나 생각대로다.

console.log(1)까지는 동기적으로 실행됐으나, async, await 특성상 (promise, then chaining과 같다) 브라우저 환경에서 실행되는 함수를 만나게 되면, 이벤트 루프에 따라, async 함수 밖의 함수를 실행하게 된다.

그렇기 때문에, async 함수는 reponse가 기다리는 환경에서 멈추게 되고

dispatch()로 돌아가 console.log("a")부터 실행하게 되는 것!

 

여기까지 이해했다면, 일단 박수!!!

 


그렇다면, 왜 navigate는 작동하지 않고, 다시 원래 페이지를 렌더링하게 되는가?를 의심해야 한다.

여기서, 처음에 나왔던 코드인 login 컴포넌트의 useEffect함수를 봐야한다.

  useEffect(() => {
    if (token) {
      navigate("/");
    }
  }, [token, navigate]);

보면, 토큰이 있다면 로그인 페이지로 이동시키고 있다.

때문에, navigate는 정상 작동하지만, loginpage의 navigate로 인해 다시 "/"로 이동시키는 것이다.

ㅠㅠㅠ

 

여기까지 왔다면 .. 해결할 수 있는 방안이 두개가 있다.

1. thunk action 함수를 동기함수로 만들어서 ajax요청만 비동기로 처리하는 것

2. thunk action 함수의 async를 유지하고, dispatch를 await를 통해서 기다린 뒤에, navigate를 보내는 것이다.


근데 여기서 또 의문이 생긴다. useEffect는 의존성 배열이 변경되었을 경우 실행되는 것이다.

첫 렌더링을 제외하더라도, 다시 렌더링하는 과정에서는 의존성 배열의 원소들이 변화하지 않는 한! 변하지 않는다!

고 생각했다 ㅋㅋ

 

그렇다면 어디서 변화가 생길까? 를 생각해본다면, useEffect가 실행되는 조건을 다시 생각해야한다.

1. useEffect는 처음 렌더링될 때 실행된다. -> 마운트 될 때 실행됨

2. 종속성 배열에 추가된 요소들이 변경될 때 실행된다.

 

1번에서 나는 원래 처음 렌더링될 때만 생각해서, 리액트 프로젝트의 첫 렌더링에서만 실행된다고 생각했다. 하지만, 처음 마운트 될 때도 실행되는 것이니까 .. 당연했던 것이니까 ..

 

reference?

https://react-redux.js.org/api/hooks#usedispatch

'react' 카테고리의 다른 글

내 코드 리팩토링 하기  (0) 2023.04.14
react router v6.4에 생긴 기능들  (0) 2023.04.12
DOM과 React의 가상 돔  (0) 2023.03.22
리액트 파이버  (0) 2023.03.21
리액트의 개념적 모델  (0) 2023.03.19