- 로그인 상태: 회원가입, 로그인 페이지 접근 불가
- 로그아웃 상태: 마이페이지 접근 불가
- AppRoute.jsx로 라우터 분리
- Protected Router, Public Router 설정
- 설정한 Route 적용
const {isAuthenticated, isLoading} = useContext(AuthContext)
const ProtectedRoute = () => {
if(isLoading) {
return <div>Loading</div>
}
if(!isAuthenticated) {
return <Navigate to="/login"replace />
}
return children
}
const PublicRoute = ({children}) => {
if(isLoading) {
return <div>Loading</div>
}
if(isAuthenticated) {
return <Navigate to="/" replace/>
}
return children
}
이렇게 구현했고
여기서 useNavigate hook을 쓰지 않고 <Navigate />을 사용한 이유는
1. useNavigate hook: 특정한 코드의 실행이 끝나고 나서 페이지를 이동하고 싶을 때 사용
함수이기 때문에 어떠한 이벤트나 조건을 만족하면 해당 페이지로 이동할 수 있도록 조절할 수 있다.
예시
- 쇼핑몰에서 장바구니에 담기 버튼을 눌렀을 때 request를 보낸 후 장바구니 페이지로 이동,
- 로그인 페이지에서 로그인을 완료한 후 메인페이지로 이동 등
2. Navigate컴포넌트: 특정 경로에서 렌더링 시점에 다른 페이지로 이동시키고 싶을 때 사용
- 쇼핑몰의 회원전용 페이지에 로그인 없이 들어올 경우, 로그인 페이지로 리다이렉트,
- 쇼핑 상세 페이지에서 제품이 품절되었거나 삭제되어서 다른 페이지로 리다이렉트 시키는 경우
이러한 차이점이 있다.
따라서 내가 사용할 용도는 2번 경우이기 때문에 Navigate컴포넌트를 사용했다.
replace를 쓴 이유는 뒤로가기 했을 때 이전 보호된 페이지로 돌아가지 않는다.
주로 인증/인가 관련 리다이렉트에서 사용된다.
만약 로그인이 성공하여 마이페이지로 이동했는데
다시 로그인 페이지로 이동하는것을 막기 위해 사용했다.
라우트를 적용하는 방법도 두 가지가 있다.
<Routes>
<Route path="/" element={<PublicRoute><Home /></PublicRoute>} />
<Route path="/signin" element={<PublicRoute><SignIn /></PublicRoute>} />
<Route path="/signup" element={<PublicRoute><SignUp /></PublicRoute>} />
</Routes>
<Routes>
// Public Routes
<Route element={<PublicRoute />}>
<Route path="/" element={<Home />} />
<Route path="/signin" element={<SignIn />} />
<Route path="/signup" element={<SignUp />} />
</Route>
// Protected Routes
<Route element={<ProtectedRoute />}>
<Route path="/mypage" element={<MyPage />} />
</Route>
</Routes>
첫 번째 방식의 장점은
- 각 라우트마다 독립적인 제어 가능
두 번째 방식의 장점은- 라우트 그룹 관리가 용이함
- 중첩 라우팅이 필요할 때 더 유연함
나는 두 번째 방법이 더 깔끔한 것 같아 두 번째 방법을 사용했다.
하지만 두 번째 방법을 사용하려면 위에서 children으로 구현했던것을 Outlet으로 수정해야한다.
이유는 React Router의 동작 방식 때문이다.
Outlet은 중첩된 라우트의 컴포넌트를 렌더링하기위한 특별한 컴포넌트이다.
라우트가 정상적으로 작동하는것을 확인했다.
[원인]
- AuthContext에서 isAuthenticated상태가 동기화 되지 않음
- 로그인 시에만 setIsAuthenticated(true)로 설정되어있어 페이지를 새로고침 하거나 재방문할 때 인증 상태가 초기화됨
[해결]
- useEffect를 추가하여 초기 인증 상태 확인
- auth상태 변경을 구독하여 실시간으로 상태 업데이트
useEffect(() => {
// 초기 인증 상태 확인
const checkUser = async () => {
try {
const {
data: { session },
} = await supabase.auth.getSession();
setIsAuthenticated(!!session);
} catch (error) {
console.error("session체크 오류", error);
} finally {
setIsLoading(false);
}
};
// auth 상태 변경 구독
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
setIsAuthenticated(!!session);
});
checkUser();
// 클린업
return () => {
subscription?.unsubscribe();
};
}, []);
[원인]
const [isLoading, setIsLoading] = useState(false);
이렇게 초기 세션 체크가 완료되기 전에 isLoading이 false이고 insAuthenticated도 false인 상태가 되어 리다이렉트가 발생했다.
[해결]
const [isLoading, setIsLoading] = useState(true);
변경하니 실제 인증 상태에 따라 적절한 라우팅이 이루어졌다.
생각해보니 로그인 여부만 확인하고있지, 유저 정보를 가져오는 로직이 없다.
useEffect(() => {
const checkUser = async () => {
try {
const {
data: { session },
} = await supabase.auth.getSession();
setIsAuthenticated(!!session);
setUser(session?.user ?? null); // 추가
} catch (error) {
console.error("session체크 오류", error);
} finally {
setIsLoading(false);
}
};
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
setIsAuthenticated(!!session);
setUser(session?.user ?? null); // 추가
if (event === "SIGNED_OUT") { // 추가
setUser(null);
}
});
checkUser();
return () => {
subscription?.unsubscribe();
};
}, []);
추가해주어 추후 마이페이지나 헤더에서 사용할 수 있도록 했다.
[문제]
바로위 코드를 보면 user정보를 session에서 가져오고있는데
이러면 유저 정보를 수정했을 때
마이페이지에서 정보를 정상적으로 뿌려줄 수 없다.
[해결]
session에서 가져오던 user정보를 user테이블에서 가져오는 로직으로 수정했다.
const checkSession = async (session) => {
try {
if (session) {
const { data } = await supabase
.from("users")
.select("*")
.eq("id", session.user.id)
.single();
setUser(data);
} else {
setUser(null);
}
} catch (error) {
console.error("session체크 오류", error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
// 초기 세션 체크
supabase.auth.getSession().then((res) => {
const session = res.data.session;
setIsAuthenticated(!!session);
checkSession(session);
});
// 인증 상태 변경 감지
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
setIsAuthenticated(!!session);
checkSession(session);
});
return () => {
subscription?.unsubscribe();
};
}, []);
인증 상태 변경 감지에
setIsAuthenticated 와 checkSession을 추가하여
로그인/로그아웃 시 실시간으로 사용자 정보가 업데이트 되도록 하였다.
추가로 여기서 eq('id'userId)
의 id는 supabase의 테이블 컬럼명이고, userId는 session.user.id에서 가져온 값이다.
실제 SQL로 변환하면
SELECT * FROM users WHERE id = "userId"
single()은 supabase쿼리에서 단일 레코드만을 반환하도록 지정하는 메서드이다.
1. 결과가 없으면 null을 반환
2. 결과가 여러 개면 에러를 발생
3. 결과가 정확히 하나면 그 객체를 반환
우당탕탕 로그인 및 미들웨어 구현을 완료했다.
처음부터 설계를 잘 했다면, 같은 코드를 여러번 짜는 경우는 없었을텐데..
아직 미숙하다보니 짰던 코드를 다시 짜고 다시 짜게되는 것 같다.
다음 로그인 및 미들웨어 구현 시
같은 실수는 반복하지 않도록 이번 기회에 확실히 익혀야겠다.
또 이런식으로 짜는게 맞는지 여러번 확인하고 체크해서
다음에는 더 나은 방식으로 구현해보도록 하겠다.