react-router-dom은 React에서 페이지 이동(라우팅)을 구현하기 위한 대표 라이브러리입니다. SPA(단일 페이지 앱)에서 URL이 바뀌어도 전체 페이지를 새로고침하지 않고, URL에 따라 다른 컴포넌트를 렌더링하게 해줍니다.
핵심 개념만 정리하면:
BrowserRouter로 앱을 감싸 라우팅을 활성화합니다.Routes 안에 Route를 두고, path에 맞는 element(컴포넌트)를 보여줍니다.<Link to="/path" />: 새로고침 없이 화면 전환 (권장)useNavigate(): 코드로 이동 (버튼 클릭 후 이동 등)useParams(): /posts/:id 같은 동적 경로 값 읽기useSearchParams(): ?q=... 같은 쿼리스트링 처리Outlet로 구성 가능


import { useState } from "react";
import { Link , useNavigate } from "react-router-dom";
import styled from "styled-components";
import api from '../../../api/axios'
const Container = styled.div
`display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color:
#f2f2f2;`;
const FormWrapper = styled.div
`background-color: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0px 8px 16px
rgba(0,0,0,0.1);
width: 400px;`;
const Title = styled.h2
`text-align: center;
margin-bottom: 20px;
color: #333;`;
const Input = styled.input`
width: 100%;
padding: 12px;
margin-bottom: 15px;
border-radius: 6px;
border: 1px solid #ccc;
font-size: 16px;
&:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 5px rgba(0,123,255,0.3);
}
`;
const Button = styled.button`
width: 100%;
padding: 12px;
background-color: #007bff;
color: white;
border: none;
font-size: 16px;
border-radius: 6px;
cursor: pointer;
margin-top: 10px;
&:hover {
background-color: #0056b3;
}
&:disabled {
background-color: #aaa;
cursor: not-allowed;
}
`;
const TextLink = styled(Link)`
display: block;
text-align: center;
margin-top: 15px;
font-size: 14px;
color: #007bff;
text-decoration: none;
cursor: pointer;
&:hover {
text-decoration: underline;
}`;
const SigninPage = () => {
const navigate = useNavigate();
const [form,setForm] = useState({
email : '',
password : ''
});
const changeHandler = (e) => {
//기존 값을 유지시키고 현재 입력된 필드에 대한 상태변환
const {name, value} =e.target ;
setForm({...form , [name] : value}); //속성에 대한 접근으로 대괄호 사용
}
const submitHandler = async (e) => {
e.preventDefault();
if (!form.email || !form.password) {
alert('이메일과 비밀번호를 모두 입력해주세요');
return;
}
const data = {
email : form.email,
password : form.password
}
try {
// 1. GET 요청으로 입력한 이메일과 일치하는 유저 데이터가 있는지 조회합니다.
// URL 결과 예시: /users?email=test@test.com
const response = await api.get(`/users?email=${form.email}`);
/*권장 방식 : api.get('url',{ parmas :
{ email : xxxx, password : xxxxx}
}
)*/
// json-server는 조건 검색 시 항상 [배열] 형태로 응답합니다.
const userList = response.data;
// 2. 이메일이 일치하는 유저가 존재하고, 비밀번호까지 일치하는지 확인
if (userList.length > 0 && userList[0].password === form.password) {
alert(`${userList[0].name}님, 환영합니다!`);
// 로그인 성공 시 블로그 메인페이지(예: '/')로 이동
navigate('/blog/index');
} else {
// 3. 계정이 없거나 비밀번호가 틀린 경우
alert('이메일 또는 비밀번호가 일치하지 않습니다.');
}
} catch (err) {
console.error("로그인 통신 에러:", err);
alert('서버와의 통신에 실패했습니다.');
}
}
return (
<Container>
<FormWrapper>
<Title>로그인</Title>
<form onSubmit={submitHandler}>
<Input type="text"
name="email"
value={form.email}
onChange={changeHandler}
placeholder="이메일을 입력하세요"/>
<Input type="password"
name="password"
value={form.password}
onChange={changeHandler}
placeholder="비밀번호를 입력하세요"/>
<Button type="submit">Login</Button>
</form>
<TextLink to='/'>아직 회원이 아니라면? 회원가입</TextLink>
</FormWrapper>
</Container>
);
}
export default SigninPage;

import { useState } from "react";
import { Link , useNavigate } from "react-router-dom";
import styled from "styled-components";
import api from '../../../api/axios'
const Container = styled.div
`display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color:
#f2f2f2;`;
const FormWrapper = styled.div
`background-color: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0px 8px 16px
rgba(0,0,0,0.1);
width: 400px;`;
const Title = styled.h2
`text-align: center;
margin-bottom: 20px;
color: #333;`;
const Input = styled.input`
width: 100%;
padding: 12px;
margin-bottom: 15px;
border-radius: 6px;
border: 1px solid #ccc;
font-size: 16px;
&:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 5px rgba(0,123,255,0.3);
}
`;
const Button = styled.button`
width: 100%;
padding: 12px;
background-color: #007bff;
color: white;
border: none;
font-size: 16px;
border-radius: 6px;
cursor: pointer;
margin-top: 10px;
&:hover {
background-color: #0056b3;
}
&:disabled {
background-color: #aaa;
cursor: not-allowed;
}
`;
const TextLink = styled(Link)`
display: block;
text-align: center;
margin-top: 15px;
font-size: 14px;
color: #007bff;
text-decoration: none;
cursor: pointer;
&:hover {
text-decoration: underline;
}`;
const SignUpPage = () => {
const navigate = useNavigate();
const [form,setForm] = useState({
name : '',
email : '',
password : ''
});
const changeHandler = (e) => {
//기존 값을 유지시키고 현재 입력된 필드에 대한 상태변환
const {name, value} =e.target ;
setForm({...form , [name] : value}); //속성에 대한 접근으로 대괄호 사용
}
const submitHandler = async (e) => {
e.preventDefault();
const data = {
name : form.name,
email : form.email,
password : form.password
}
/*Quiz
통신을 통해서 json-server에 데이터를 입력 (완)
통신이 성공했을 때 화면을 SigninPage 페이지로 이동 (완)
SigninPage는 기존 SignUpPage의 이름 입력부분을 제외하고 이메일과 비밀번호만 받음
가입된 계정으로 로그인 시도했을 때 계정이 존재하면
블로그 메인페이지로 이동
*/
await api.post('/users',data)
.then(response => {
navigate('/signin');
})
.catch(err=> {
alert('회원가입에 실패했습니다. 다시 시도해주세요');
});
}
return (
<Container>
<FormWrapper>
<Title>회원가입</Title>
<form onSubmit={submitHandler}>
<Input type="text"
name = "name"
value={form.name}
onChange={changeHandler}
placeholder="이름을 입력하세요"/>
<Input type="text"
name="email"
value={form.email}
onChange={changeHandler}
placeholder="이메일을 입력하세요"/>
<Input type="password"
name="password"
value={form.password}
onChange={changeHandler}
placeholder="비밀번호를 입력하세요"/>
<Button type="submit">SignUp</Button>
</form>
<TextLink to='/signin'>회원이라면? 로그인</TextLink>
</FormWrapper>
</Container>
)
}
export default SignUpPage;
메인페이지와 작성 페이지는 내일 완성될 거 같음 !
그 중에 localstorage 개념이 내 코드엔 포함되지 않아서 강사님 코드와 비교 & 참조 해보았다.
const submitHandler = async (e) => {
e.preventDefault() ;
await api.get('/users', {
params : {
email : form.email,
password : form.password
}
})
.then( response => {
console.log(`debug >>>> axios request success `);
console.log(response);
if(response.status === 200 ) {
// 인증된 사용자 정보(token - session)를 유지할 수 있어야 함.
// 현재 기준으로 sessionStorage , localStorage 사용하여
// 인증된 사용자 정보를 저장하고 공유할 수 있다.
localStorage.setItem("token" , 'jslim9413@naver.com');
moveUrl('/blog/index');
}
})
.catch( err => {
console.log(`debug >>>> axios request error ${err}`);
});
}

local storage에 토큰으로 정보 저장해서 세션 유지
내 코드에 local storage 넣어서 유지시키듯이 수정해달라고 함.
try {
setIsLoading(true); // 로딩 시작 (버튼 비활성화)
// 1. 실제 백엔드는 로그인 검증을 POST 요청으로 처리합니다.
// 백엔드 엔드포인트 예시: /api/v1/auth/login 또는 /users/login
const response = await api.post('/users/login', {
email: form.email,
password: form.password
});
// 2. 백엔드에서 정상적으로 토큰과 유저 정보를 응답받은 경우
// 백엔드 응답 객체 구조 예시: { data: { accessToken: "...", user: { name: "홍길동" } } }
const { accessToken, user } = response.data;
if (accessToken) {
// LocalStorage에 JWT 토큰 저장
localStorage.setItem('token', accessToken);
// 필요 시 사용자의 간단한 프로필 정보도 저장 (메인화면 이름 표시용)
localStorage.setItem('userName', user.name);
alert(`${user.name}님, 환영합니다!`);
// 로그인 성공 후 메인 페이지로 리다이렉트
navigate('/blog/index');
} else {
alert('인증 토큰을 받아오지 못했습니다. 관리자에게 문의하세요.');
}
} catch (err) {
console.error("로그인 통신 에러:", err);
// 3. 백엔드가 보낸 에러 메시지가 있다면 에러 메시지를 출력하고, 없다면 기본 메시지 출력
// 예: 백엔드에서 401 Unauthorized와 함께 { message: "비밀번호가 일치하지 않습니다." }를 보냈을 때 처리
const errorMessage = err.response?.data?.message || '이메일 또는 비밀번호가 일치하지 않거나 서버 오류가 발생했습니다.';
alert(errorMessage);
} finally {
setIsLoading(false); // 로딩 종료 (버튼 재활성화)
}

navigate에서 arrow함수 적용을 안 해서 즉시 호출해버려서 페이지가 변하지 않는다는 문제가 있었음 ! 그래서 익명함수로 연결했더니 정상적으로 작동함.
일단 내일 벨로그에 어제 오늘 리포트 다시 올릴 예정 😂
이제 troble shooting할 때는 문제 상황 캡쳐 -> 문제 상황 해결 캡쳐까지 작성하는 걸로 노력해봐야겠음 !

이제 진짜 졸업프로젝트에서 백엔드 연동할 수 있을 거 같은데? 주말에 시도해보는 걸로 !