0. 버그 현황
- 우리는 JWT 토큰을 Context API 로 관리를 하고 있다. 토큰 여부에 따른 페이지 접근 제한을 하고 있다.
- 하지만, 토큰 값을 삭제한 후 signin 페이지에 가서 로그인을 하면 todo 페이지로 이동할 때 토큰값이 로컬스토리지에 저장되어 있음에도 불구하고 Notfound 페이지로 이동한다.
🔸splash 페이지로 접근 후 token 이 있으면 signin 페이지로 이동, 없으면 todo 페이지로 이동
function Splash(){
const navigate = useNavigate();
const token = useContext(UserContext);
useEffect(() => {
setTimeout(() => {
if (!token) {
navigate("/signin");
} else {
navigate('/todo');
}
}, 1500);
}, [token, navigate]);
return (
<div className="bg-main_skyblue flex flex-col justify-center items-center h-screen">
<h1 className="animate-bounce text-6xl font-mono font-bold text-center ">To Do List <br />
for P</h1>
</div>
)
}
🔸라우터 페이지
- 토큰이 있어야지만 todo 와 todo/category 에 접근할 수 있게 했다.
export default function Router() {
const token = useContext(UserContext);
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>
);
}
🔸App.tsx 에서 Context Api 를 통해 모든 값을 토큰값을 전달하고 있다.
function App() {
const access_token: string | null = localStorage.getItem("access_token");
return (
<div className="App">
<UserContext.Provider value={access_token}>
<Router />
</UserContext.Provider>
</div>
);
}
1. 버그 해결과정
- 우선 token 값을 가져와지는지 디버깅을 해보기로 했다.
- signin 페이지에서 토큰값을 인식해 todo 페이지로 넘어가는 로직에서 Context API 에 들어있는 토큰 값 반영이 null 로 되어있다. 어플리케이션에는 값이 담겨있지만, App.tsx 에서 console.log(acces_token) 으로 출력해보니 null 로 나왔다.
- 페이지 이동(signin -> todo 페이지) 할 때 token 이 null 에서 상태 변경이 되지 않는 문제였다.
function App() {
const access_token: string | null = localStorage.getItem("access_token");
console.log(access_token);
return (
<div className="App">
<UserContext.Provider value={access_token}>
<Router />
</UserContext.Provider>
</div>
);
}
- 하지만 새로 고침을 하면 다시 토큰값이 불러와져서 정상적으로 Todo 페이지가 보여진다.
<결론>
- Signin 페이지에서 Todo 페이지로 이동할 때 토큰이 불러와지지 않고(null 상태), 새로 고침을 해야지만이 토큰값이 불러와진다.
즉, null 상태에서 token 값으로 상태 변경이 되지 않는 다는 것
🤔궁금증1) 왜 null 상태에서 token 값으로 상태 변경이 되지 않을까?
- App.tsx 에서 localStorage에서 access_token을 불러와 UserContext를 설정하는 과정과, Router 컴포넌트가 처음 렌더링될 때 useContext(UserContext)를 사용해 token을 불러오는 과정 사이에 동기화 문제가 있을 수 있다.
- App 컴포넌트가 처음 렌더링 되면 localStorage 에서 access_token 을 불러와 UserContext.Provider의 value로 설정한다. 그러나 이 시점에서는 아직 localStorage 에 access_token 이 저장되지 않았을 수 있다. 따라서 UserContext.Provider의 value 는 null 이 된다.
- 그 후 , 사용자가 로그인에 성공하면 localStorage 에 access_token 이 저장되지만, UserContext.Provider의 value가 변경되지 않으므로 Router 컴포넌트가 useContext(UserContext)를 사용해 token을 불러올 때 null을 반환한다.
🤔궁금증2) localStorage.getItem 메서드를 통해 토큰이 있으면 가져오는 건데, access_token 변수가 로그인이 성공하면 null 에서 토큰값으로 왜 자동으로 변경이 안되는거지? 새로고침을 해야만 토큰값이 업데이트가 되지? Context API 는 상태 변경 감지가 안되는건가?
- Context API 는 변경을 감지하는 기능이 포함되어있다. Context.Provider 의 'value' prop 이 변경되면 해당 컨텍스트를 구독하고 있는 모든 컴포넌트들은 다시 렌더링된다. 이 때, 'useContext' Hook 을 사용하는 컴포넌트 들은 새로운 컨텍스트 값을 받아와서 사용하게 된다.
하지만, Context API 는 React 컴포넌트의 렌더링 생명주기에 따라 컨텍스트의 값이 업데이트 되고 반영된다. 상태(state) 변경을 감지하여 컴포넌트를 다시 렌더링 하는 방식으로 작동한다. 상태는 특별한 React 기능인 'useState'나 'useReducer'같은 Hook을 사용해 관리한다. 이런 Hook 을 사용해서 상태를 업데이트 할 때만 React는 변경을 감지하고 컴포넌트를 다시 렌더링한다.
즉, 'localStorage'에 값을 설정한 후에 바로 'useContext'로 그 값을 가져올 수 없다는 것이다. 왜냐하면 'localStorage'의 값이 변경되더라도 state가 변경된 것이 아니기 때문에 React가 인지할 수 없기 때문이다. localStorage가 React의 생명주기와 상태 관리 체계와는 별개로 동작하기 때문이다.
예를 들어, 아래와 같은 코드가 있다.
localStorage.setItem을 호출하여 value를 'new value'로 변경했지만, value 변수는 여전히 'old value'를 가지고 있다. 이는 value 변수에 저장된 값은 변경되지 않았기 때문이다. JavaScript는 value 변수가 localStorage에 저장된 'value' 항목과 연결되어 있음을 자동으로 인지하지 않는다. 이와 마찬가지로, React 컴포넌트는 localStorage의 변화를 자동으로 감지하지 않는다. 이런 경우에는 상태 관리 메커니즘을 통해 localStorage의 변화를 수동으로 감지하고 반영해야 한다. 이 때 useState와 useEffect를 사용할 수 있습니다.
2. 해결책
1) 로그인에 성공할 때 UserContext.Provider의 value를 업데이트해야하지 않을까? 즉, 토큰값이 null 에서 실제 토큰값으로 상태 업데이트를 해주어야 하지 않을까?
- 상태 업데이트를 위해 UserContext 기본값에 useState로 관리하는 token 과 setToken 을 넣었다.
import { createContext, Dispatch, SetStateAction } from "react";
interface UserContextType {
token: string | null;
setToken: Dispatch<SetStateAction<string | null>>;
}
export const UserContext = createContext<UserContextType | null>(null);
// App.tsx
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;
- 로그인 페이지에서 setToken 을 불러와서 로그인이 성공하면 토큰값을 setToken 에 업데이트 하는 로직을 썼다.
function AuthForm() {
const navigate = useNavigate();
const location = useLocation();
const {
register,
handleSubmit,
watch,
formState: { errors, isValid },
} = useForm<MyFormData>({ mode: "onChange" });
const password = watch("password");
const [inputType, setInputType] = useState("password");
const [hasInput, setHasInput] = useState(false);
const togglePasswordVisibility = () => {
setInputType(inputType === "password" ? "text" : "password");
};
const userContext = useContext(UserContext);
if (!userContext) {
// Error handling code here. For example:
throw new Error("UserContext is null");
}
const { setToken } = userContext;
const onSubmitHandler: SubmitHandler<MyFormData> = async (data) => {
try {
const loginRes = await customAxios.post("auth/signin", data);
if (loginRes) {
const login_token = loginRes.data.access_token;
localStorage.setItem("access_token", login_token);
setToken(login_token);
}
navigate("/todo");
} catch (err) {
console.error(err);
}
};
return (
<section className="flex flex-col justify-center items-center h-screen">
<h1 className="mb-10 h-7 font-medium text-2xl">
{location.pathname === "/signin" ? "로그인" : "이메일로 회원가입"}
</h1>
<form onSubmit={handleSubmit(onSubmitHandler)} className="flex flex-col">
{/*중략*/}
</form>
</section>
);
}
export default AuthForm;
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>
);
}
3. 결론
- 기존에 Context 의 value 값에 localStorage 값만 넣었다. 즉, 동적으로 토큰이 있을때, 없을 때를 리액트가 감지하지 못했다.
- Context의 value 에 useState를 활용한 상태값, 함수를 전달 할 수 있다는 것을 알았다.
- 리액트 프로젝트를 하면서 계속 상태 관련한 렌더링에서 버그가 나는데, 상태에 대해 더 깊이 파고들수 있어서 Good
참고자료
https://doubly12f.tistory.com/125
'프로젝트 이모저모' 카테고리의 다른 글
[투두리스트 버그] 전역 상태 값이 바로 변경되어 렌더링 되지 않음 (0) | 2023.08.01 |
---|---|
[투두리스트 페어프로그래밍] recoil 새로 고침 시 전역 데이터 초기화 (0) | 2023.07.24 |
[투두리스트 페어프로그래밍 버그] 로그아웃 시 페이지 이동 안됨(useEffect,Context API) (0) | 2023.07.11 |
[투두리스트 페어프로그래밍] 전역상태관리 VS get API 재호출 (0) | 2023.07.07 |
[투두리스트 페어프로그래밍] 지역변수, useRef, useState 차이 (0) | 2023.07.05 |