[React] 사용자 권한에 따른 렌더링
1. 세팅
1.1 json-server 사용
package.json 파일의 scripts 섹션에 아래 내용 추가하기
"scripts": {
"serve": "json-server --watch db.json --port 4000"
}
아래 명령을 통해 json-sever를 실행
yarn serve
1.2 파일 구조
2. Router 작성
2.1 Router 연결
요구사항
- 라우팅은 “중첩 라우팅”으로 작성할 것!
- 로그인 여부가 상관없는(로그인해도, 하지 않아도 접근할 수 있는) 페이지는
Layout.jsx
를 공통 element로 사용하기- 대상: Home, SearchPage, TestPage
- 로그인 상태가 반드시 아니어야 하는(로그인 한 상태면 접근할 수 없는) 페이지는
NonAuthLayout.jsx
를 공통 element로 사용하기- 대상: LoginPage, SignupPage
- 로그인이 반드시 필요한(로그인 해야만 접근할 수 있는) 페이지는
AuthLayout.jsx
를 공통 element로 사용하기- 대상: UserProfile
- 어느것에도 포함되지 않는 페이지의 경우 NotFound(src > pages > default-set >
NotFound.jsx
)를 렌더링하기
// src/shared/Router.jsx
import AuthLayout from "components/layouts/AuthLayout";
import Layout from "components/layouts/Layout";
import NonAuthLayout from "components/layouts/NonAuthLayout";
import NotFound from "pages/default-set/NotFound";
import HomePage from "pages/protected/HomePage";
import UserProfilePage from "pages/protected/UserProfilePage";
import PublicHomePage from "pages/public/PublicHomePage";
import SearchPage from "pages/public/SearchPage";
import SignInPage from "pages/public/SignInPage";
import SignUpPage from "pages/public/SignUpPage";
import TestPage from "pages/public/TestPage";
import { BrowserRouter, Route, Routes } from "react-router-dom";
const Router = () => {
return (
<BrowserRouter>
<Routes>
{/* 로그인 여부 상관없는 라우터 */}
<Route element={<Layout />}>
<Route path="/" element={<PublicHomePage />} />
<Route path="sign-in" element={<SignInPage />} />
<Route path="sign-up" element={<SignUpPage />} />
<Route path="search" element={<SearchPage />} />
<Route path="test" element={<TestPage />} />
</Route>
{/* 로그인 상태가 반드시 아니어야 하는 라우터 */}
<Route element={<NonAuthLayout />}></Route>
{/* 로그인이 필요한 라우터 */}
<Route element={<AuthLayout />}>
<Route path="/" element={<HomePage />} />
<Route path="/user/:userId" element={<UserProfilePage />} />
</Route>
{/* 404 Not Found */}
<Route path="*" element={<NotFound />} />
{/* 사용자가 잘못된 경로로 이동했을 때 기본적으로 (/)로 리다이렉션 */}
{/* <Route path="*" element={<Navigate replace to="/" />} /> */}
</Routes>
</BrowserRouter>
);
};
export default Router;
2.2 Outlet 연결
각 공통 element(Layout, AuthLayout, NonAuthLayout)는 Outlet을 가지고 있어서 중첩된 컴포넌트가 Layout을 적용받은 상태에서 렌더링 될 수 있도록 하기
2.2.1 Layout.jsx
// src/styles/CommonStyles.jsx
import styled from "styled-components";
export const CommonContainer = styled.div`
display: flex;
max-width: 1200px;
margin: 0 auto;
`;
import { Outlet } from "react-router-dom";
import Navbar from "./Navbar";
import styled from "styled-components";
import { CommonContainer } from "styles/CommonStyles";
const Layout = () => {
return (
<>
<Navbar />
<StLayout>
<StContainer>
<StMessage>
로그인 여부와 상관없이 접근 가능한 페이지입니다.
</StMessage>
<StMain>
<Outlet />
</StMain>
</StContainer>
</StLayout>
</>
);
};
export default Layout;
const StLayout = styled.nav`
background-color: #e2e2e2;
border-bottom: 1px solid #ddd;
`;
const StContainer = styled(CommonContainer)`
flex-direction: column;
min-height: 100vh;
`;
const StMessage = styled.p`
text-align: center;
margin: 20px 0;
font-size: 18px;
color: #333;
`;
const StMain = styled.main`
flex: 1;
padding: 20px;
`;
2.2.2 AuthLayout.jsx
import { Navigate, Outlet } from "react-router-dom";
import Navbar from "./Navbar";
import styled from "styled-components";
import { CommonContainer } from "styles/CommonStyles";
const AuthLayout = () => {
return (
<>
<Navbar />
<StLayout>
<StContainer>
<StMessage>반드시 로그인이 되어있어야 하는 페이지입니다.</StMessage>
<StMain>
<Outlet />
</StMain>
</StContainer>
</StLayout>
</>
);
};
export default AuthLayout;
const StLayout = styled.nav`
background-color: #e2e2e2;
border-bottom: 1px solid #ddd;
`;
const StContainer = styled(CommonContainer)`
flex-direction: column;
min-height: 100vh;
`;
const StMessage = styled.p`
text-align: center;
margin: 20px 0;
font-size: 18px;
color: #333;
`;
const StMain = styled.main`
flex: 1;
padding: 20px;
`;
2.2.3 NonAuthLayout.jsx
import { Navigate, Outlet } from "react-router-dom";
import Navbar from "./Navbar";
import styled from "styled-components";
import { CommonContainer } from "styles/CommonStyles";
const NonAuthLayout = () => {
return (
<>
<Navbar />
<StLayout>
<StContainer>
<StMessage>
로그인이 반드시 안 되어 있어야 하는 페이지입니다.
</StMessage>
<StMain>
<Outlet />
</StMain>
</StContainer>
</StLayout>
</>
);
};
export default NonAuthLayout;
const StLayout = styled.nav`
background-color: #e2e2e2;
border-bottom: 1px solid #ddd;
`;
const StContainer = styled(CommonContainer)`
flex-direction: column;
min-height: 100vh;
`;
const StMessage = styled.p`
text-align: center;
margin: 20px 0;
font-size: 18px;
color: #333;
`;
const StMain = styled.main`
flex: 1;
padding: 20px;
`;
3. custom axios 작성
3.1 auth.js
요구사항
- authApi 생성
- 기본url을
https://moneyfulpublicpolicy.co.kr/
로 설정 «««< HEAD -
haeders는
"Content-Type": "application/json"
로 설정 - haeders는 “Content-Type”: “application/json” 로 설정
93a4303f45c58407b5cb25a65a107bcdc2c39301
- 기본url을
- 요청 interceptor 설정
- request interceptor에서 요청하는 url이 “user”인 경우는 localStorage에 있는 accessToken을 header에 포함시킴
- 이 경우, accessToken이 존재하지 않는다면 아래 코드로 처리하여 실패 동작
alert("인증이 필요합니다."); return Promise.reject("인증이 필요합니다.");
config를 console에 찍으면 사용자 요청에 대한 url이 나온다.
authApi.interceptors.request.use(
(config) => {
console.log(config);
return config;
},
() => {}
);
즉, 이 url을 활용하여 access token check를 할 수 있다!
import axios from "axios";
export const authApi = axios.create({
baseURL: "https://moneyfulpublicpolicy.co.kr",
headers: {
"Content-Type": "application/json",
},
});
// 인터셉터 설정
authApi.interceptors.request.use(
(config) => {
console.log("authApi config:", config);
// 유저가 가지고 있는 accessToken이 지금 요청을 보내려고 하는 이 시점에 맞아?? 정확해?? 유효기간 있어?? 를 파악해야한다!
const token = localStorage.getItem("accessToken");
if (config.url === "/user") {
if (token) {
// Bearer은 소유자라는 뜻인데, "이 토큰의 소유자에게 권한을 부여해줘"라는 의미
// Bearer뒤에 있는 것을 accessToken으로 사용
// ( config.headers.Authorization = )과 동일
config.headers["Authorization"] = `Bearer ${token}`;
} else {
alert("인증이 필요합니다.");
return Promise.reject("인증이 필요합니다! 오류가 발생했어요.");
}
}
// url에 따른 분기
return config;
},
(error) => {
return Promise.reject(error);
}
);
3.2 posts.js
요구사항
⚠️ postsAxios는 권한이 필요 없는 요청이라 가정한다. 따라서 요청 interceptor를 별도로 작성하지 않는다.
- 기본 url을 json-server 주소로 설정 (ex : http://localhost:4001/posts)
- timeout을 1.5초로 세팅
import axios from "axios";
const postsAxios = axios.create({
baseURL: `${process.env.REACT_APP_SERVER_URL}/posts`,
timeout: 1500,
});
export default postsAxios;
3.3 comments.js
요구사항
- commentsAxios 생성
- commentsAxios는 권한이 반드시 필요한 요청이라 가정한다. 따라서 요청 interceptor를 작성하여 사전에 user의 권한을 파악한다.
- 기본 url을 json-server 주소(localhost:3001번 권장)/comments 설정 (ex : http://localhost:3001/comments)
- timeout을 1.5초로 세팅
- 요청 interceptor 설정
- custom으로 만든 authApi에 user 요청(하단 인증 API 명세서 - 회원정보 확인 참고)을 통해 검증을 진행 - 반드시 authApi를 이용
- 응답 interceptor 설정
- 요청이 성공 응답을 받은 경우,
console.log("요청 성공입니다.")
를 출력
- 요청이 성공 응답을 받은 경우,
- 버튼이 눌리는 시점에 권한이 있는지 파악해야한다.
- 이를 위해 onClick을 사용하는 것 보다 commnets 요청에 request 부분에서 로직을 작성해 주는 것이 좋다.
import axios from "axios";
import { authApi } from "./auth";
const commentsAxios = axios.create({
baseURL: `${process.env.REACT_APP_SERVER_URL}/comments`,
timeout: 1500,
});
commentsAxios.interceptors.request.use(
async (config) => {
// 유저가 가지고 있는 accessToken이 지금 요청을 보내려고 하는 이 시점에 맞아?? 정확해?? 유효기간 있어?? 를 파악해야한다!
const ac = localStorage.getItem("accessToken");
try {
// (1) 유저 권한 체크 to 백엔드
await axios.get("/user", {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${ac}`,
},
});
return config; // 성공
} catch (error) {
return Promise.reject(error); // 비동기 작업 중 발생한 에러 반환
}
},
(error) => {
return Promise.reject(error); // 요청 설정 오류 반환
}
);
commentsAxios.interceptors.response.use((response) => {
console.log("요청 성공입니다.");
return response;
});
export default commentsAxios;
리팩토링
- auth.js 와 코드와 중복되니 auth.js에 존재하는 user를 사용하도록 하자
- 이렇게 되면 accessToken과 headers 세팅이 필요 없어진다!
- 아래 처럼 comments.js에서 user을 넘기면 auth.js가 분기를 해준다.
import axios from "axios";
import { authApi } from "./auth";
const commentsAxios = axios.create({
baseURL: `${process.env.REACT_APP_SERVER_URL}/comments`,
timeout: 1500,
});
commentsAxios.interceptors.request.use(
async (config) => {
try {
// (1) 유저 권한 체크 to 백엔드
await authApi.get(`/user`);
return config; // 성공
} catch (error) {
return Promise.reject(error); // 비동기 작업 중 발생한 에러 반환
}
},
(error) => {
return Promise.reject(error); // 요청 설정 오류 반환
}
);
commentsAxios.interceptors.response.use((response) => {
console.log("요청 성공입니다.");
return response;
});
export default commentsAxios;
왼쪽 comment.js가 통과했다는 이야기는 이미 auth.js가 통과했다는 이야기임!
4. 로그인 여부 상관없는 페이지
4.1 회원가입(SignupPage.jsx)
요구사항
- 회원가입 성공 시
- alert로 “회원가입에 성공하였습니다. 로그인 페이지로 이동할게요” 메시지 출력
- login 페이지로 자동 이동
- 회원가입 실패 시
- alert로 백엔드에서 제공받은 오류 메시지 출력
- console.log로 오류 내용 전체 출력
// src/pages/public/SignUpPage.jsx
import { useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { authApi } from "../../axios/auth";
import styled from "styled-components";
const SignUpPage = () => {
const idRef = useRef(null);
const pwRef = useRef(null);
const nickNameRef = useRef(null);
const navigate = useNavigate();
useEffect(() => {
idRef.current.focus();
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
try {
await authApi.post(`/register`, {
id: idRef.current.value,
password: pwRef.current.value,
nickname: nickNameRef.current.value,
});
// 성공 시
alert("회원가입이 완료되었습니다.");
navigate("/login");
} catch (error) {
// 실패 시
const message = error.response.data.message;
alert(message || "회원가입 중 문제가 발생했습니다."); // 사용자 확인
console.log(message); // 개발자 확인
}
};
return (
<StContainer>
<StTitle>회원가입 창</StTitle>
<StForm onSubmit={handleSubmit}>
<StLabel htmlFor="id">아이디</StLabel>
<StInput
type="text"
name="id"
ref={idRef}
placeholder="아이디를 입력하세요."
/>
<StLabel htmlFor="nickname">닉네임</StLabel>
<StInput
type="text"
id="nickname"
ref={nickNameRef}
placeholder="닉네임을 입력하세요."
/>
<StLabel htmlFor="password">비밀번호</StLabel>
<StInput
type="password"
id="password"
ref={pwRef}
placeholder="비밀번호를 입력하세요."
/>
<StButton type="submit">회원가입</StButton>
<StButton
type="button"
onClick={() => {
navigate("/sign-in");
}}
style={{ backgroundColor: "#6c757d", borderColor: "#6c757d" }}
>
로그인하러 가기
</StButton>
</StForm>
</StContainer>
);
};
const StContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
max-width: 500px;
margin: auto;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
background-color: #f9f9f9;
`;
const StTitle = styled.h1`
margin-bottom: 1.5rem;
font-size: 2rem;
color: #333;
`;
const StForm = styled.form`
display: flex;
flex-direction: column;
width: 100%;
`;
const StLabel = styled.label`
font-size: 1rem;
margin-bottom: 0.5rem;
color: #555;
`;
const StInput = styled.input`
padding: 0.8rem;
margin-bottom: 1rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
`;
const StButton = styled.button`
padding: 0.8rem;
margin: 0.5rem 0;
border: none;
border-radius: 4px;
font-size: 1rem;
color: #fff;
background-color: #007bff;
cursor: pointer;
transition: background-color 0.3s ease;
&:hover {
background-color: #0056b3;
}
`;
export default SignUpPage;
4.2 로그인 기능(SignInPage.jsx)
요구사항
- 로그인 성공 시
- alert로 “로그인에 성공하였습니다. 로그인 페이지로 이동할게요” 메시지 출력
- localStorage에 저장(accessToken, userId, nickname)
- 메인 페이지(Home 컴포넌트, 경로상 “/”)로 자동 이동
- 로그인 실패 시
- alert로 백엔드에서 제공받은 오류 메시지 출력
- console.log로 오류 내용 전체 출력
// src/pages/public/SignInPage.jsx
import { useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { authApi } from "../../axios/auth";
import styled from "styled-components";
const SignInPage = () => {
const idRef = useRef(null);
const pwRef = useRef(null);
const navigate = useNavigate();
useEffect(() => {
idRef.current.focus();
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
try {
const res = await authApi.post("/login", {
id: idRef.current.value,
password: pwRef.current.value,
});
const { accessToken, avatar, nickname, userId } = res.data;
// 로그인 성공 시
alert("로그인에 성공하셨습니다. 홈으로 이동합니다.");
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("avatar", avatar);
localStorage.setItem("nickname", nickname);
localStorage.setItem("userId", userId);
navigate("/");
if (!res.data.accessToken) {
alert("토큰이 없습니다. 고객센터에 문의해주세요.");
return;
}
} catch (error) {
// 로그인 실패 시
const message = error.response.data.message;
alert(message || "로그인 중 문제가 발생했습니다."); // 사용자 확인
console.log(message); // 개발자 확인
}
};
return (
<StContainer>
<StTitle>로그인 창</StTitle>
<StForm onSubmit={handleSubmit}>
<StLabel htmlFor="id">아이디: </StLabel>
<StInput
type="text"
id="id"
ref={idRef}
placeholder="아이디를 입력하세요."
/>
<StLabel htmlFor="password">비밀번호</StLabel>
<StInput
type="password"
id="password"
ref={pwRef}
placeholder="비밀번호를 입력하세요."
/>
<StButton type="submit">로그인</StButton>
<StButton
type="button"
onClick={() => {
navigate("/sign-up");
}}
style={{ backgroundColor: "#6c757d", borderColor: "#6c757d" }}
>
회원가입하러 가기
</StButton>
</StForm>
</StContainer>
);
};
const StContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
max-width: 500px;
margin: auto;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
background-color: #f9f9f9;
`;
const StTitle = styled.h1`
margin-bottom: 1.5rem;
font-size: 2rem;
color: #333;
`;
const StForm = styled.form`
display: flex;
flex-direction: column;
width: 100%;
`;
const StLabel = styled.label`
font-size: 1rem;
margin-bottom: 0.5rem;
color: #555;
`;
const StInput = styled.input`
padding: 0.8rem;
margin-bottom: 1rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
`;
const StButton = styled.button`
padding: 0.8rem;
margin: 0.5rem 0;
border: none;
border-radius: 4px;
font-size: 1rem;
color: #fff;
background-color: #007bff;
cursor: pointer;
transition: background-color 0.3s ease;
&:hover {
background-color: #0056b3;
}
`;
export default SignInPage;
4.3 PublicHomePage.jsx
요구사항
- access 토큰이
- 존재하는 경우 “안녕하세요 {닉네임}님!”
- 존재하지 않는 경우 “로그인을 해주세요.”
import { Link } from "react-router-dom";
import styled from "styled-components";
const PublicHomePage = () => {
const at = localStorage.getItem("accessToken");
const nickname = localStorage.getItem("nickname");
return (
<StContainer>
{at ? (
<StGreeting>안녕하세요, {nickname}님!</StGreeting>
) : (
<StGreeting>로그인을 해주세요!</StGreeting>
)}
{/* 로그인 여부가 상관없는 메뉴 */}
<StList>
<StListItem>
<StLink to="/search">검색 페이지로</StLink>
</StListItem>
<StListItem>
<StLink to="/test">권한테스트 페이지로</StLink>
</StListItem>
</StList>
{/* 로그인이 반드시 필요한 메뉴 */}
<StImportant>❗️ 로그인이 반드시 필요한 메뉴</StImportant>
<StList>
<StListItem>
<StLink to="/user/1">1번 유저의 정보</StLink>
</StListItem>
<StListItem>
<StLink to="/user/2">2번 유저의 정보</StLink>
</StListItem>
</StList>
</StContainer>
);
};
export default PublicHomePage;
const StContainer = styled.div`
font-family: Arial, sans-serif;
width: 100%;
`;
const StGreeting = styled.p`
font-size: 1.2em;
color: #333;
`;
const StList = styled.ul`
list-style-type: none;
`;
const StListItem = styled.li`
margin: 10px 0;
`;
const StImportant = styled.p`
font-weight: bold;
color: #e74c3c;
`;
const StLink = styled(Link)`
text-decoration: none;
color: #3498db;
&:hover {
text-decoration: underline;
}
`;
4.4 SearchPage.jsx
요구사항
- useEffect를 활용하여 최초 렌더링 시 postsAxios를 활용하여 전체 데이터를 가져와 state에 세팅
- useSearchParams를 활용하여, searchParams에 따른 posts를 filtering 해서 보여주기
- ex ) http://localhost:3000/search?userId=2 인 경우 ⇒ 전체 post리스트 중, writerUserId가 2인 경우만 filtering 해서 출력
- searchParams는 아래 두 버튼을 통해 변경하도록 updateSearch() 함수를 완성하기
<button onClick={() => updateSearch("1")}>1번유저의 글 보기</button> <button onClick={() => updateSearch("2")}>2번유저의 글 보기</button>
// src/pages/public/SearchPage.jsx
import postsAxios from "../../axios/posts";
import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
const SearchPage = () => {
const [searchParams, setSearchParams] = useSearchParams();
const [posts, setPosts] = useState([]);
const userId = searchParams.get("userId");
// URL의 쿼리 스트링을 변경하는 함수
const updateSearch = (userId) => {
setSearchParams({ userId });
};
useEffect(() => {
const getPosts = async () => {
try {
const res = await postsAxios.get("/");
setPosts(res.data);
} catch (error) {
alert("글 목록을 가져오는 중 오류가 발생하였습니다.");
console.log(error);
}
};
getPosts();
}, []);
const filteredPosts = posts.filter(
(post) => post.writeUserId === Number(userId)
);
return (
<div>
<h1>Posting 정보 보기(검색페이지)</h1>
<div>
{userId ? (
<p>아이디 {userId}님이 쓰신 글</p>
) : (
<p>아래 두 버튼 중 하나를 선택해주세요.</p>
)}
</div>
<button onClick={() => updateSearch(1)}>1번 유저의 글 보기</button>
<button onClick={() => updateSearch(2)}>2번 유저의 글 보기</button>
{filteredPosts?.map((post) => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.author}</p>
<p>{post.body}</p>
</div>
))}
</div>
);
};
export default SearchPage;
4.5 TestPage.jsx
요구사항
⚠️ 세팅한 custom axios로 인해, commentsAxios를 활용하여 값을 가져올 때는 localStorage에 accessToken이 없는 경우 실패해야한다. 별도 코딩은 필요 없으며, commentsAxios 내부에 해당 로직이 있으므로 테스트를 꼭 해보도록 하자.
- handleGetPostButtonClick : postsAxios를 이용하여 전체 포스팅을 가져온 후, post정보의 title과 author를 출력시키도록 하자. 해당 버튼이 클릭됐을 때는 이미 출력된 comments 리스트가 있다면 없애야 한다.
- handleGetCommentsButtonClick : commentsAxios를 활용하여 전체 댓글 리스트를 가져온 후, comment 정보의 body를 출력시키도록 하자. 해당 버튼이 클릭됐을 때는 이미 출력된 posts 리스트가 있다면 없애야 한다.
import commentsAxios from "../axios/comments";
import postsAxios from "../axios/posts";
const TestPage = () => {
const [posts, setPosts] = React.useState([]);
const [comments, setComments] = React.useState([]);
const handleGetPostButtonClick = async () => {
try {
const response = await postsAxios.get("/");
setPosts(response.data);
setComments([]);
} catch (error) {
console.log(error);
alert("포스팅을 가져오는 도중 에러가 발생하였습니다.");
}
};
const handleGetCommentsButtonClick = async () => {
try {
const response = await commentsAxios.get("/");
setComments(response.data);
setPosts([]);
} catch (error) {
console.log(error);
alert("accessToken 유효 시간이 만료되었습니다.");
}
};
return (
<div>
<h1>Test Page</h1>
<p>api 테스트를 진행합니다.</p>
<button onClick={handleGetPostButtonClick}>
posts가져오기 테스트(로그인필요없음)
</button>
<button onClick={handleGetCommentsButtonClick}>
comments가져오기 테스트(로그인필요)
</button>
{posts?.map((post) => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.author}</p>
</div>
))}
{comments?.map((comment) => (
<div key={comment.id}>
<p>{comment.body}</p>
</div>
))}
</div>
);
};
export default TestPage;
5. Layout 작성
5.1 AuthLayout.jsx
요구사항
- 로그인 시 localStorage에 저장했던 accessToken을 기준으로 로그인이 되어있지 않은 경우 alert로 안내한 후, 바로 login 페이지로 이동시키기
- 💡 로컬스토리지는 useEffect에서 가져오지 않아도 된다! 굳이 비동기적으로 작성하지 않아도 컴포넌트가 렌더링되는 시점에 동기적으로 가져오기 때문이다.
import { Navigate, Outlet } from "react-router-dom";
import Navbar from "./Navbar";
import styled from "styled-components";
import { CommonContainer } from "styles/CommonStyles";
const AuthLayout = () => {
const isAuthenticated = localStorage.getItem("accessToken");
if (!isAuthenticated) {
alert("로그인이 필요한 페이지입니다. 로그인 페이지로 이동할게요!");
// replace를 사용하여 이전 페이지로 돌아가는 것을 방지(새로운 히스토리로 덮어씌우기)
return <Navigate to="/sign-in" replace />;
}
return (
<>
<Navbar />
<StLayout>
<StContainer>
<StMessage>반드시 로그인이 되어있어야 하는 페이지입니다.</StMessage>
<StMain>
<Outlet />
</StMain>
</StContainer>
</StLayout>
</>
);
};
export default AuthLayout;
const StLayout = styled.nav`
background-color: #e2e2e2;
border-bottom: 1px solid #ddd;
`;
const StContainer = styled(CommonContainer)`
flex-direction: column;
min-height: 100vh;
`;
const StMessage = styled.p`
text-align: center;
margin: 20px 0;
font-size: 18px;
color: #333;
`;
const StMain = styled.main`
flex: 1;
padding: 20px;
`;
❓useEffect를 사용해야 할까?
- useEffect를 사용하여 로그인 여부를 판단할 경우, 로그인 여부를 판단하기 전에 사용자가 화면을 볼 수 있다는 문제가 발생할 수 있다.
- 이는, useEffect는 컴포넌트가 렌더링된 이후에 실행되기 때문에, 로그인 여부를 판단하기 전에 컴포넌트의 초기 렌더링이 일어나기 때문이다.
- 따라서 렌더링 여부를 판단하는 state를 생성해야 하거나, useEffect를 사용하지 않고 초기 렌더링 단계에서 직접적으로 로그인 여부를 판단하면 이러한 문제를 피할 수 있다.
5.2 NonAuthLayout
요구사항
- 로그인 시 localStorage에 저장했던 accessToken이 존재하면, 해당 Layout에 접근할 수 없음을 의미하기 때문에 alert로 “이미 로그인 상태입니다” 안내한 후, 바로 Home 페이지(컴포넌트 : Home, 경로 : “/”)로 이동
import { Navigate, Outlet } from "react-router-dom";
import Navbar from "./Navbar";
import styled from "styled-components";
import { CommonContainer } from "styles/CommonStyles";
const NonAuthLayout = () => {
const isAuthenticated = localStorage.getItem("accessToken") ? true : false;
if (isAuthenticated) {
alert("이미 로그인 상태입니다.");
return <Navigate to="/" replace />;
}
return (
<>
<Navbar />
<StLayout>
<StContainer>
<StMessage>
로그인이 반드시 안 되어 있어야 하는 페이지입니다.
</StMessage>
<StMain>
<Outlet />
</StMain>
</StContainer>
</StLayout>
</>
);
};
export default NonAuthLayout;
const StLayout = styled.nav`
background-color: #e2e2e2;
border-bottom: 1px solid #ddd;
`;
const StContainer = styled(CommonContainer)`
flex-direction: column;
min-height: 100vh;
`;
const StMessage = styled.p`
text-align: center;
margin: 20px 0;
font-size: 18px;
color: #333;
`;
const StMain = styled.main`
flex: 1;
padding: 20px;
`;
5.3 Navbar.jsx
요구사항
- localStorage의 accessToken 여부에 따라 [로그인하러가기], [로그아웃] 버튼을 다르게 보여주기(조건부 렌더링)
- 로그인하러가기 버튼 클릭 시
- login 페이지로 라우팅
- 로그아웃 버튼 클릭 시
- localStorage에 저장되어있는 accessToken, userId, nickname을 제거
- Home 페이지(컴포넌트 : Home, 경로 : “/”)로 이동
ul
태그 내부의li
태그를 활용하여 각 페이지에 대한 내비게이션을 완성시키기- 1번 유저의 정보 : userId를 1로 params 지정할 수 있게 경로 설정
- 2번 유저의 정보 : userId를 2로 params 지정할 수 있게 경로 설정
// src/components/layouts/Navbar.jsx
import { NavLink, useNavigate } from "react-router-dom";
import styled from "styled-components";
import { CommonContainer } from "styles/CommonStyles";
const Navbar = () => {
const at = localStorage.getItem("accessToken") ? true : false;
const navigate = useNavigate();
const handleLogoutButton = () => {
const logoutConfirm = window.confirm("정말 로그아웃 하시겠습니까?");
if (logoutConfirm) {
localStorage.removeItem("accessToken");
localStorage.removeItem("avatar");
localStorage.removeItem("nickname");
localStorage.removeItem("userId");
navigate("/sign-in");
alert("로그아웃 완료되었습니다!");
}
};
return (
<StNav>
<StNavContainer>
<StNavLink to="/">Home</StNavLink>
{at ? (
<StLogoutButton onClick={handleLogoutButton}>로그아웃</StLogoutButton>
) : (
<StAuthLinks>
<StNavLink to="/sign-in">로그인</StNavLink>
<StNavLink to="/sign-up">회원가입</StNavLink>
</StAuthLinks>
)}
</StNavContainer>
</StNav>
);
};
export default Navbar;
const StNav = styled.nav`
background-color: #f8f9fa;
padding: 10px 20px;
border-bottom: 1px solid #ddd;
`;
const StNavContainer = styled(CommonContainer)`
justify-content: space-between;
align-items: center;
`;
const StNavLink = styled(NavLink)`
color: #333;
text-decoration: none;
padding: 10px 15px;
font-size: 16px;
font-weight: 500;
transition: color 0.3s ease, background-color 0.3s ease;
&.active {
font-weight: bold;
color: #007bff;
background-color: #e9ecef;
border-radius: 5px;
}
&:hover {
color: #0056b3;
}
`;
const StLogoutButton = styled.div`
cursor: pointer;
color: #dc3545;
font-size: 16px;
font-weight: 500;
padding: 10px 15px;
border-radius: 5px;
transition: background-color 0.3s ease;
&:hover {
background-color: #f8d7da;
}
`;
const StAuthLinks = styled.div`
display: flex;
gap: 10px;
`;
댓글남기기