
1,2,3번째 포스팅에서는 프로젝트에 대한 소개와 백엔드의 주요 프로세스에 대해 작성하였다. 이번 포스팅부터는 프론트엔드의 주요 프로세스에 대해 포스팅 할 예정이다. React를 사용하여 개발을 진행하였으며, TypeScript는 사용하지 않았다.
function Home({token, userInfo, onLogout}) {
return (
<CenteredContainer>
<Title>{token ? `환영합니다 ${userInfo.nickname}님` : '어서오세요. 로그인 또는 일반 회원가입을 진행해주세요.'}</Title>
{!token && (
<>
<LinkButton to="/login">로그인</LinkButton>
<LinkButton to="/register">회원가입</LinkButton>
</>
)}
{token && (
<>
<LinkButton to="/myinfo">내 정보</LinkButton>
<Button onClick={onLogout}>로그아웃</Button>
</>
)}
</CenteredContainer>
);
}
export default Home;
token이 존재하면 사용자가 로그인한 상태로 간주하고 환영 메시지와 함께 "내 정보" 및 "로그아웃" 버튼을 표시한다.token이 존재하지 않으면 로그인과 회원가입 버튼을 표시한다.userInfo 객체는 로그인한 사용자의 정보를 포함하며, 여기서는 nickname을 사용한다.onLogout 함수는 로그아웃 버튼 클릭 시 호출된다.const KakaoButton = styled.button`
background: #ffffff;
border: none;
cursor: pointer;
margin-bottom: 10px;
width: 80%;
height: 50px;
`;
function Login({ onLogin }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
const params = new URLSearchParams(location.search);
const emailParam = params.get('email');
const nicknameParam = params.get('nickname');
const accessTokenParam = params.get('accessToken');
const refreshTokenParam = params.get('refreshToken');
if (emailParam && nicknameParam && accessTokenParam && refreshTokenParam) {
const userInfo = { email: emailParam, nickname: nicknameParam };
onLogin(accessTokenParam, refreshTokenParam, userInfo);
navigate('/home');
}
}, [location, onLogin, navigate]);
const handleSubmit = async (e) => {
e.preventDefault();
try {
setError(null);
const response = await api.post('/members/login', { email, password });
const responseHeaders = response.headers;
const accessToken = responseHeaders.get("Authorization");
const refreshToken = responseHeaders.get("Authorization-RefreshToken");
const userInfo = { email: response.data.email, nickname: response.data.nickname };
onLogin(accessToken, refreshToken, userInfo);
navigate('/home');
} catch (error) {
setError('이메일 또는 비밀번호가 잘못되었습니다. 다시 시도해주세요.');
}
};
const handleKakaoLogin = () => {
window.location.href = 'http://localhost:8080/oauth2/authorization/kakao';
};
return (
<CenteredContainer>
<FormContainer>
<Form onSubmit={handleSubmit}>
<Title>로그인</Title>
<Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="이메일" required />
<Input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="비밀번호" required />
<Button type="submit">로그인</Button>
{error && <Error>{error}</Error>}
<KakaoButton type="button" onClick={handleKakaoLogin}>
<img src={process.env.PUBLIC_URL + `/kakao_login_medium_narrow.png`} alt="카카오로 로그인하기" />
</KakaoButton>
<Button onClick={() => navigate('/register')}>회원가입</Button>
<Button onClick={() => navigate(-1)}>돌아가기</Button>
</Form>
</FormContainer>
</CenteredContainer>
);
}
export default Login;
이 컴포넌트는 사용자 로그인 페이지를 구현한 것이다. 사용자가 이메일과 비밀번호를 입력하여 로그인하거나, 카카오 소셜 로그인을 통해 로그인할 수 있도록 구성되어 있다.
상태 및 네비게이션 훅:
useState 훅을 사용하여 이메일, 비밀번호, 오류 메시지 상태를 관리한다.useNavigate 훅을 사용하여 페이지 네비게이션을 처리한다.useLocation 훅을 사용하여 현재 URL의 쿼리 파라미터를 가져온다.useEffect:
onLogin 함수를 호출하여 사용자를 로그인 상태로 만들고, 홈 페이지로 리디렉션한다.handleSubmit:
onLogin 함수를 호출하고 홈 페이지로 이동한다.handleKakaoLogin:
렌더링:
function MyInfo({ token, userInfo, onLogout }) {
const [info, setInfo] = useState(userInfo);
const [showModal, setShowModal] = useState(false);
const navigate = useNavigate();
useEffect(() => {
const fetchData = async () => {
try {
const response = await getMyInfo(token);
setInfo(response.data);
} catch (error) {
console.error('Error fetching user info:', error);
}
};
if (!info && token) {
fetchData();
}
}, [token, info]);
const handleDelete = async () => {
try {
await deleteUser(token);
onLogout();
navigate('/home');
} catch (error) {
console.error('오류가 발생했습니다.', error);
}
};
if (!info) return <p>Loading...</p>;
return (
<Container>
<Title>내 정보</Title>
<p>이메일: {info.email}</p>
<p>닉네임: {info.nickname}</p>
<Button onClick={() => setShowModal(true)}>회원 탈퇴</Button>
<Button onClick={() => navigate('/home')}>돌아가기</Button>
{showModal && (
<ModalContainer>
<Modal>
<p>정말로 회원 탈퇴를 하시겠습니까?</p>
<ModalButton onClick={handleDelete}>예</ModalButton>
<ModalButton onClick={() => setShowModal(false)}>아니오</ModalButton>
</Modal>
</ModalContainer>
)}
</Container>
);
}
export default MyInfo;
이 컴포넌트는 사용자의 개인 정보를 보여주고, 회원 탈퇴 기능을 제공한다.
상태 및 네비게이션 훅:
useState 훅을 사용하여 사용자 정보(info)와 모달 창 표시 여부(showModal)를 관리한다.useNavigate 훅을 사용하여 페이지 네비게이션을 처리한다.useEffect:
token과 info 상태가 변경될 때마다 호출된다.getMyInfo API 호출을 통해 사용자 정보를 가져와 info 상태를 업데이트한다.handleDelete:
deleteUser API 호출을 통해 회원 탈퇴 요청을 보낸다.onLogout 함수를 호출하여 로그아웃 상태로 만들고, 홈 페이지로 리디렉션한다.렌더링:
handleDelete 함수가 호출되고, "아니오" 버튼을 클릭하면 모달 창이 닫힌다.function Oauth2Join() {
const [email, setEmail] = useState('');
const [accessToken, setAccessToken] = useState('');
const [nickname, setNickname] = useState('');
const [message, setMessage] = useState('');
const [error, setError] = useState('');
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
const params = new URLSearchParams(location.search);
const emailParam = params.get('email');
const tokenParam = params.get('accessToken');
if (emailParam && tokenParam) {
setEmail(emailParam);
setAccessToken(tokenParam);
}
}, [location]);
const validateNickname = (nickname) => {
return nickname.length >= 2;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateNickname(nickname)) {
setError('닉네임이 너무 짧습니다. 최소 2글자이어야 합니다.');
return;
}
try {
setError(null);
const response = await api.put('/members/oauth2/update', { email, nickname }, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
setMessage(`회원가입이 완료되었습니다! 환영합니다, ${response.data.nickname}`);
setTimeout(() => navigate('/'), 2000);
} catch (error) {
if (error.response && error.response.status === 600) {
setError('이미 가입된 이메일입니다.');
} else if (error.response && error.response.status === 601) {
setError('이미 존재하는 닉네임입니다.');
} else {
setError('회원가입에 실패했습니다. 다시 시도해주세요.');
}
}
};
return (
<CenteredContainer>
<FormContainer>
<Form onSubmit={handleSubmit}>
<Title>카카오 회원가입</Title>
<Input type="email" value={email} placeholder="이메일" required disabled />
<Input type="text" value={nickname} onChange={(e) => setNickname(e.target.value)} placeholder="닉네임" required />
<Button type="submit">회원가입</Button>
{message && <Message>{message}</Message>}
{error && <Error>{error}</Error>}
</Form>
</FormContainer>
</CenteredContainer>
);
}
export default Oauth2Join;
이 컴포넌트는 사용자가 카카오 OAuth2 로그인을 통해 회원가입을 완료할 수 있도록 하는 페이지를 구현한 것이다.
상태 및 네비게이션 훅:
useState 훅을 사용하여 이메일, 액세스 토큰, 닉네임, 메시지, 오류 메시지 상태를 관리한다.useNavigate 훅을 사용하여 페이지 네비게이션을 처리한다.useLocation 훅을 사용하여 현재 URL의 쿼리 파라미터를 가져온다.useEffect:
validateNickname:
handleSubmit:
렌더링:
disabled 속성을 설정한다.function Register() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [nickname, setNickname] = useState('');
const [message, setMessage] = useState('');
const [error, setError] = useState('');
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
const params = new URLSearchParams(location.search);
const emailParam = params.get('email');
if (emailParam) {
setEmail(emailParam);
}
}, [location]);
const validateEmail = (email) => {
const emailPattern = /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])+[.][a-zA-Z]{2,3}$/;
return emailPattern.test(email);
};
const validatePassword = (password) => {
const passwordPattern = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&?])[A-Za-z\d!@#$%^&?]{8,30}$/;
return passwordPattern.test(password);
};
const validateNickname = (nickname) => {
return nickname.length >= 2;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateEmail(email)) {
setError('이메일 주소 양식을 확인해주세요');
return;
}
if (!validatePassword(password)) {
setError('비밀번호는 8자리 이상이면서 1개 이상의 알파벳, 숫자, 특수문자를 포함해야합니다.');
return;
}
if (!validateNickname(nickname)) {
setError('닉네임이 너무 짧습니다. 최소 2글자이어야 합니다.');
return;
}
try {
await register(email, password, nickname);
setError(null);
setMessage('회원가입이 완료되었습니다! 환영합니다!');
setTimeout(() => navigate('/'), 1000);
} catch (error) {
if (error.response && error.response.data.errorCode === 600) {
setError('이미 가입된 이메일입니다.');
} else if (error.response && error.response.data.errorCode === 601) {
setError('이미 존재하는 닉네임입니다.');
} else {
setError('회원가입에 실패했습니다. 다시 시도해주세요.');
}
}
};
return (
<CenteredContainer>
<FormContainer>
<Form onSubmit={handleSubmit}>
<Title>회원가입</Title>
<Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="이메일" required />
<Input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="비밀번호" required />
<Input type="text" value={nickname} onChange={(e) => setNickname(e.target.value)} placeholder="닉네임" required />
<Button type="submit">회원가입</Button>
<Button onClick={() => navigate(-1)}>돌아가기</Button>
{message && <Message>{message}</Message>}
{error && <Error>{error}</Error>}
</Form>
</FormContainer>
</CenteredContainer>
);
}
export default Register;
이 컴포넌트는 사용자 회원가입을 처리하는 페이지를 구현한다. 사용자는 이메일, 비밀번호, 닉네임을 입력하여 회원가입을 완료할 수 있다.
상태 및 네비게이션 훅:
useState 훅을 사용하여 이메일, 비밀번호, 닉네임, 메시지, 오류 메시지 상태를 관리한다.useNavigate 훅을 사용하여 페이지 네비게이션을 처리한다.useLocation 훅을 사용하여 현재 URL의 쿼리 파라미터를 가져온다.useEffect:
validateEmail:
validatePassword:
validateNickname:
handleSubmit:
렌더링:
import axios from 'axios';
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || 'http://localhost:8080';
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
api.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response && error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const refreshToken = localStorage.getItem('refreshToken');
if (refreshToken) {
try {
originalRequest.headers['Authorization-refresh'] = `Bearer ${refreshToken}`;
const response = await api(originalRequest);
const responseHeaders = response.headers;
const accessToken = responseHeaders.get("Authorization");
const newRefreshToken = responseHeaders.get("Authorization-RefreshToken");
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', newRefreshToken);
api.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
originalRequest.headers['Authorization'] = `Bearer ${accessToken}`;
return api(originalRequest);
} catch (err) {
return Promise.reject(err);
}
}
}
return Promise.reject(error);
}
);
export const register = (email, password, nickname) => {
return api.post('/members/join', { email, password, nickname });
};
export const login = (email, password) => {
return api.post('/members/login', { email, password });
};
export const getMyInfo = (token) => {
return api.get('/members/getMyInfo', {
headers: { Authorization: `Bearer ${token}` },
});
};
export const updateUserInfo = (email, nickname, token) => {
return api.put('/members/oauth2/update', { email, nickname }, {
headers: {
Authorization: `Bearer ${token}`
}
});
};
export const deleteUser = (token) => {
return api.delete('/members', {
headers: { Authorization: `Bearer ${token}` },
});
};
export default api;
이 모듈은 axios를 사용하여 API 요청을 처리하는 기능을 제공한다. 인증 토큰을 자동으로 갱신하는 인터셉터를 포함하며, 회원가입, 로그인, 사용자 정보 조회 및 업데이트, 회원 탈퇴 등의 기능을 제공한다.
API 기본 설정:
axios.create를 사용하여 기본 URL과 헤더를 설정한 api 인스턴스를 생성한다.API_BASE_URL은 환경 변수에서 가져오거나 기본값으로 로컬 호스트를 사용한다.인터셉터 설정:
originalRequest._retry를 사용하여 중복 요청을 방지한다.localStorage에서 리프레시 토큰을 가져와 요청 헤더에 추가하고, 서버에 재요청하여 새로운 액세스 토큰과 리프레시 토큰을 받아온다.localStorage에 저장하고, axios 기본 헤더와 요청 헤더에 설정한다.회원가입 (register):
로그인 (login):
내 정보 조회 (getMyInfo):
사용자 정보 업데이트 (updateUserInfo):
회원 탈퇴 (deleteUser):
function App() {
const [token, setToken] = useState(localStorage.getItem('accessToken'));
const [userInfo, setUserInfo] = useState(JSON.parse(localStorage.getItem('userInfo')));
useEffect(() => {
if (token) {
api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
const storedUserInfo = JSON.parse(localStorage.getItem('userInfo'));
setUserInfo(storedUserInfo);
}
}, [token]);
const handleLogin = (accessToken, refreshToken, userInfo) => {
setToken(accessToken);
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
localStorage.setItem('userInfo', JSON.stringify(userInfo));
setUserInfo(userInfo);
};
const handleLogout = () => {
setToken(null);
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('userInfo');
setUserInfo(null);
};
return (
<Router>
<Container>
<Routes>
<Route path="/" element={<Navigate to="/home" />} />
<Route path="/home" element={<Home token={token} userInfo={userInfo} onLogout={handleLogout} />} />
<Route path="/login" element={<Login onLogin={handleLogin} />} />
<Route path="/register" element={<Register />} />
<Route path="/myinfo" element={<MyInfo token={token} userInfo={userInfo} onLogout={handleLogout} />} />
<Route path="/members/oauth2/join" element={<Oauth2Join />} />
</Routes>
</Container>
</Router>
);
}
export default App;
이 컴포넌트는 애플리케이션의 루트 컴포넌트로, React Router를 사용하여 페이지를 라우팅하며, 사용자의 로그인 상태를 관리한다.
상태 및 초기화:
useState 훅을 사용하여 token과 userInfo 상태를 관리한다.localStorage에서 accessToken과 userInfo를 가져와 초기 상태를 설정한다.useEffect:
token 상태가 변경될 때마다 실행되며, token이 존재하면 api의 기본 헤더에 Authorization 헤더를 설정한다.localStorage에서 userInfo를 가져와 상태를 업데이트한다.handleLogin:
accessToken, refreshToken, userInfo를 받아 상태와 localStorage를 업데이트한다.handleLogout:
token과 userInfo 상태를 초기화하고, localStorage에서 해당 항목들을 제거한다.라우팅 설정:
Router와 Routes를 사용하여 라우트를 설정한다.Home, Login, Register, MyInfo, Oauth2Join 컴포넌트로의 라우트를 설정한다.이렇게 모든 프론트엔드 코드에 대한 설명이 끝났다. 주요 프로세스의 경우 백엔드 기반으로 작성된 코드라 백엔드 코드를 살펴보면 더 이해하기 쉽다고 생각한다. 마지막 포스팅에서는 직접 시현한 이미지와 함께 어떻게 구현되는지 설명할 예정이다.