프로젝트 이모저모

[투두리스트 페어프로그래밍 버그] 로그아웃 시 페이지 이동 안됨(useEffect,Context API)

Ella Seon 2023. 7. 11. 10:59

✅한 줄 요약 : useEffect 는 React 컴포넌트 렌더링이 완료된 직후에 비동기로 호출된다.

 

0. 버그 현황

- 로그아웃 버튼을 누르면 / 페이지로 리다이렉트 기능을 만들려고 했다. 

- 로그아웃 버튼을 누르면 토큰 값이 없어져서  / 페이지로 이동해야하는데 Notfound 페이지로 뜸

- 아래 코드를 보면 중간에 useEffect 로 token 값이 없으면 / 페이지로 이동하게 해놨다. 

function Todo() {
  const [showInp, setShowInp] = useState<boolean>(false);
  const [todoList, setTodoList] = useState<TodoItem[]>([]);

  const userContext = useContext(UserContext);
  const navigate = useNavigate();

  if (!userContext) {
    throw new Error("UserContext is null");
  }
  const { token, setToken } = userContext;

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


  const handleRefresh = () => {
    location.reload();
  };

  const handleLogout = () => {
    localStorage.removeItem("access_token");
    setToken(null);
  };

  return (
    <div className="bg-main_skyblue flex flex-col justify-center items-center h-screen">
      <aside className="w-98 text-right mr-5 mb-5">
        <FontAwesomeIcon
          icon={faHouse}
          className="cursor-pointer mr-3 "
          style={{ color: "#50b4fc" }}
          size="xl"
          onClick={handleRefresh}
        />
        <img
          src={Logout}
          alt="로그아웃"
          className="w-6 cursor-pointer inline"
          onClick={handleLogout}
        />
      </aside>
    {/*중략*/}
    </div>
  );
}

export default Todo;

- 하지만 어쩐일인지 NotFound 페이지로 이동을 한다. 

export default function Router() {
  const token = useContext(UserContext)?.token;

  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Splash />} />

        {token ? (
          <>
            <Route path="/todo" element={<Todo />} />
            <Route path="/todo/category" element={<Category />} />
          </>
        ) : (
          <>
            <Route path="/signin" element={<SignIn />} />
            <Route path="/signup" element={<SignUp />} />
          </>
        )}
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}
function App() {
  const [token, setToken] = useState<string | null>(null);

  useEffect(() => {
    const access_token = localStorage.getItem("access_token");
    if (access_token) {
      setToken(access_token);
    }
  }, []);

  return (
    <div className="App">
      <UserContext.Provider value={{ token, setToken }}>
        <Router />
      </UserContext.Provider>
    </div>
  );
}

export default App;

 

1. 버그 해결 과정

- 로그아웃 버튼을 누르고 handleLogout 함수가 실행이 되면, 토큰이 제거되고, setToken에 의해 null 이된다. 

  const handleLogout = () => {
    localStorage.removeItem("access_token");
    setToken(null);
  };

- 즉, Context Api 에 있는 token 이 null 이 되어버렸다. setToken(null) 이 되었는데 Todo 컴포넌트 내에 useEffect 가 먼저 동작하는게 아니고, Router 코드가 먼저 동작을 했다. 왜그럴까??

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

어쨌든, Context API 에 있는 token 상태가 null 이 되었으면 마찬가지로 token 이 null 이니 / 페이지로 이동해야하는 것이 아닌가? 왜 Router 페이지부터 적용이 되는걸까?

 

useEffect 와 라우팅 처리에 대해서 알아보자

 

🔸 useEffect
useEffect 함수는 React 컴포넌트 렌더링이 완료된 직후에 비동기로 호출된다.
따라서, Todo 컴포넌트에서 useEffect 훅을 사용하여 토큰의 유무를 확인하고 있지만, 로그아웃 버튼을 눌러서 토큰이 null 이 된 직후에 바로 이 'useEffect'가 호출되는 건 아니다. 즉, Todo 컴포넌트의 useEffect 는 다음 렌더링 사이클에서야 비로소 호출된다.

🔸Router 컴포넌트
Context API 에 의해 state가 갱신되면 하위 컴포넌트들이 전부 렌더링이 된다.  
 setToken에 의해 토큰이 null 이 되어버리면 'Router' 컴포넌트가 리렌더링되어 적절한 라우트를 활성화 하게 된다. 
기존에 token 이 있을 때는 /todo 라우터가 활성화되었지만 setToken(null) 로 인해 토큰이 없어짐 -> 재 렌더링 -> 토큰이 없는데 todo 페이지에 있다면 /notfound 라우트 활성화하게 됨

결론적으로, 로그아웃 버튼을 눌렀을 때에 먼저 라우터가 업데이트 되고(Context API 의 SetToken 이 null 이 되니까 모든 하위값 렌더링 때문에) 그 후에 'Todo' 컴포넌트의 'useEffect' 가 호출이 된다. 이때문에 토큰이 null 이 되면 /페이지로 리디렉션이 되는게 아니라 'Router' 컴포넌트의 라우팅 설정에 따라서 처리된다. 

 

2. 해결책

- useEffect 를 사용하는 것이 아니라 로그아웃 버튼을 눌렀을 때, 바로 '/' 로 이동하는 로직을 추가해준다.

 

1) 첫번째 해결책

const handleLogout = () => {
  localStorage.removeItem("access_token");
  setToken(null);
  navigate("/");
};

- 이코드는 JavaScript 의 단일 스레드의 특징에 의해 동기적으로 실행이된다. 따라서 코드가 순차적으로 실행이 된다. 

- 하지만 setToken(null) , 즉 React 의 상태 설정 함수가 비동기적으로 동작하기 때문에 상태의 변경이 즉시 일어나지 않을 수 있다. 변경사항은 예약되어있고, 실제 반영은 나중에 React 에 의해 배치 처리된다.

- 결국, 이 코드는 동기적으로 실행되지만 'setToken' 함수에 의한 상태변경은 비동기적으로 처리된다. navigate 함수는 동기적으로 호출되고 즉시 페이지 이동을 수행하기에 'setToken' 에 의한 상태 변경이 완료되기 전에 페이지 이동이 일어날 수 있다. 

- 만약, 상태 업데이트 후에 특정 작업을 수행하려면 'useEffect' 훅으로 상태 변경 후에 작업을 하도록 할 수 있다. 

'useEffect' 는 상태가 실제로 업데이트 된 후에 호출된다. 하지만, 우리코드에서는 리액트 라우터가 렌더링 된 이후에 useEffect가 실행되기 때문에 useEffect로 처리할 수 없다. 

 

2) 두번째 해결책

  const handleLogout = () => {
    localStorage.removeItem("access_token");
    setToken(() => {
      navigate("/");
      return null;
    });
  };

 

- 이 코드도 마찬가지로 'setToken' 내부에서 'navigate'를 호출하는 건 상태 변경이 완료되었다는 것을 보장할 수 없다. 

 

 

🤔 setToken(null) 이 확정이 되고 나서 / 페이지로 이동하게끔. 동.기.적 으로 확실하게 보장하는 로직의 해결책은 없을까...? 우선 1차적인 해결방법으로 기능 구현을 하고 동기적으로 실행되게끔 하는 기능은 추후에 살펴보도록 해야겠다