회원가입을 구현했으니, 이제는 회원가입한 데이터베이스를 토대로 로그인을 구현해보자.
복습 !
1. 해당 파일은 사용자의 상태를 담을 user라는 리덕스 모듈 기능이다
리덕스 모듈이란 다음 항목들이 액션 타입, 액션 생성함수, 리듀서 를 모두 포함하는 자바스크립트 파일을 의미한다. 여러 리덕스 모듈을 루트리듀서를 통해 combine한다.
import { createAction, handleActions } from 'redux-actions';
import { takeLatest } from 'redux-saga/effects';
import * as authAPI from '../lib/api/auth';
import createRequestSaga, {
createRequestActionTypes
} from '../lib/createRequestSaga';
const TEMP_SET_USER = 'user/TEMP_SET_USER'; // 새로고침 이후 임시 로그인 처리
// 회원 정보 확인
const [CHECK, CHECK_SUCCESS, CHECK_FAILURE] = createRequestActionTypes(
'user/CHECK'
);
export const tempSetUser = createAction(TEMP_SET_USER, user => user); //파라미터 받는것 명시하는 것뿐이다. (user라는 parameter를 받는다는 뜻)
export const check = createAction(CHECK);
const checkSaga = createRequestSaga(CHECK, authAPI.check);
export function* userSaga() {
yield takeLatest(CHECK, checkSaga);
}
const initialState = {
user: null,
checkError: null
};
export default handleActions(
{
[TEMP_SET_USER]: (state, { payload: user }) => ({
...state,
user
}),
[CHECK_SUCCESS]: (state, { payload: user }) => ({
...state,
user,
checkError: null
}),
[CHECK_FAILURE]: (state, { payload: error }) => ({
...state,
user: null,
checkError: error
})
},
initialState
);
당연히 해당 리덕스 모듈을 루트리듀서에 포함시킨다.
다음으로, RegisterForm 파일을 수정하여, 회원 가입 성공 후 check를 호출하여서 현재 사용자가 로그인 상태가 되었는지 확인하는 과정을 거쳐보자.
복습 !
useSelector의 인자는 리덕스모듈로 등록해 놓은 상태들이다. 이 상태를 가져와서 가공 후에, form,auth,authError,user 변수로 넘긴다.
또한 현재 인자로 {auth,user} 가 들어가 있는데, 이는 루트리듀서에서 등록한 리듀서들 중, auth,user라는 상태(리덕스 모듈)를 호출하는 것이다.
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { changeField, initializeForm, register } from '../modules/auth';
import AuthForm from '../components/auth/AuthForm';
import {check} from '../modules/user';
const RegisterForm = () => {
const dispatch = useDispatch(); //스토어에서 컨테이너 컴포넌트를 가져옴
const { form,auth,authError,user } = useSelector(({auth,user})=> ({
form: auth.register,
auth:auth.auth,
authError:auth.authError,
user:user.user
}));
// 인풋 변경 이벤트 핸들러
const onChange = (e) => {
const { value, name } = e.target;
dispatch(
changeField({
form: 'register',
key: name,
value,
}),
);
};
// 폼 등록 이벤트 핸들러
const onSubmit = (e) => {
e.preventDefault();
const {username,password,passwordConfirm} = form; // state 에 존재하는 form에 따른 username,password,passwordconfirm key-value를 객체비구조화로 나타냄.
if (password!== passwordConfirm){
//오류처리 해야함
return;
}
dispatch(register({username,password}));
};
//컴포넌트가 처음 렌더링될 때 form 을 초기화함
useEffect(()=>{
dispatch(initializeForm('register'));},[dispatch]);
//회원가입 성공/실패 처리
useEffect(()=>{
if (authError){
console.log('오류 발생');
console.log(authError);
return;
}
if (auth){
console.log('회원가입 성공');
console.log(auth);
dispatch(check());
}
},[auth,authError,dispatch]);
//user잘 설정됨?
useEffect(()=>{
if (user){
console.log('check API 성공');
console.log(user);
}
},[user]);
return (
<AuthForm
type="register"
form={form}
onChange={onChange}
onSubmit={onSubmit}
/>
);
};
export default RegisterForm;
axios.get 메소드로 auth/check로 API를 쏘지만, 스프링에서는 해당 작업을 진행할 수 없었다.(내가 무지한걸 수도...) 그래서 회원가입을 하고 성공하면, 로그인 폼으로 넘어가고, 로그인폼에서 로그인까지 진행하는 것으로 변경하였다. 이를 위해서 'user'라는 모듈을 사용하지 않기로 했다. 하지만 지금은 공부하는 단계니까 일단은 다 지우지는 말고, 루트리듀서에 해당 모듈을 지우고 진행하였다.
import React, { useEffect } from 'react';
import {useNavigate} from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { changeField, initializeForm, register } from '../modules/auth';
import AuthForm from '../components/auth/AuthForm';
const RegisterForm = () => {
const navigate= useNavigate();
const dispatch = useDispatch(); //스토어에서 컨테이너 컴포넌트를 가져옴
const { form, auth, authError} = useSelector(({ auth}) => ({
form: auth.register,
auth: auth.auth,
authError: auth.authError,
// user: user.user
}));
// 인풋 변경 이벤트 핸들러
const onChange = (e) => {
const { value, name } = e.target;
dispatch(
changeField({
form: 'register',
key: name,
value,
}),
);
};
// 폼 등록 이벤트 핸들러
const onSubmit = (e) => {
e.preventDefault();
const { username, password, passwordConfirm } = form; // state 에 존재하는 form에 따른 username,password,passwordconfirm key-value를 객체비구조화로 나타냄.
if (password !== passwordConfirm) {
//오류처리 해야함
return;
}
dispatch(register({ username, password }));
};
//컴포넌트가 처음 렌더링될 때 form 을 초기화함
useEffect(() => {
dispatch(initializeForm('register'));
}, [dispatch]);
//회원가입 성공/실패 처리
useEffect(() => {
if (authError) {
console.log('오류 발생');
console.log(authError);
return;
}
if (auth){
window.alert('회원가입 성공!');
console.log('회원가입 성공');
dispatch(initializeForm('register'));
console.log(auth);
navigate('/login');
// dispatch(check());
}
}, [auth, authError,dispatch]);
//user잘 설정됨?
// useEffect(()=>{
// if (user){
// console.log('check API 성공');
// console.log(user);
// }
// },[user]);
return (
<AuthForm
type="register"
form={form}
onChange={onChange}
onSubmit={onSubmit}
/>
);
};
export default RegisterForm;
회원가입 성공/실패 부분을 보면, initializeForm 상태 함수를 dispatch하는 것을 볼 수 있다. 해당 부분은 'auth'모듈에서 InitializeForm 상태함수에 약간의 변형을 주어, auth부분을 초기화 하였다. 코드는 다음과 같다.
const auth = handleActions(// createAction생성시, 인자가 존재하면 payload를 추가로 붙여줘야함. { [CHANGE_FIELD]: (state, { payload: { form, key, value } }) => produce(state, draft => { draft[form][key] = value; // 예: state.register.username을 바꾼다 }), [INITIALIZE_FORM]: (state, { payload: form }) => ({ //이 payload는 이미 createAction할 때 건내준 인자임. ...state, [form]: initialState[form], auth:null, //이렇게 정의해야 로그인화면으로 넘어가지 않는다!! authError: null // 폼 전환 시 회원 인증 에러 초기화 }), // 회원가입 성공 [REGISTER_SUCCESS]: (state, { payload: auth }) => ({ //saga에서 정의한 register임 따라서 payload는 saga내 비동기 함수 response.data로 들어감.(api 결과) // 따로 인자를 주지 않아도, saga에서 이미 정의가 되어진 것임. ...state, authError: null, auth //스프링에서 쏜 member객체가 auth로 들어감 }), // 회원가입 실패 [REGISTER_FAILURE]: (state, { payload: error }) => ({ ...state, authError: error }), // 로그인 성공 [LOGIN_SUCCESS]: (state, { payload: auth }) => ({ ...state, authError: null, auth }), // 로그인 실패 [LOGIN_FAILURE]: (state, { payload: error }) => ({ ...state, authError: error }) }, initialState );
하나 더 말하자면,
handleActions에서, payload부분은, createAction할 때, 파라미터로 준 것과 일치한다.
하지만 RegisterSuccess,LoginSuccess등, 회원가입, 로그인 상태함수들은 모두, createRequestSaga로 비동기화 처리를 진행한 상태이다.(즉, API 서버에 쏘고, 받는 상태함수이다) 따라서, 해당 함수들은 서버에서 받는 response가 곧 payload로 넘어간다. 나는 스프링에서 Controller측에서, Member라는 객체를 클라이언트로 전송하였기때문에, 해당 함수들의 payload는 서버에서 받은 Member 객체가 된다.
먼저 회원인증 에러를 처리하는 메세지를 보여주기 위한 UI를 준비하자
AuthForm 컴포넌트를 먼저 수정하자.
import React from 'react';
import styled from 'styled-components';
import { Link } from 'react-router-dom';
import palette from '../../lib/styles/palette';
import Button from '../common/button';
/**
* 회원가입 또는 로그인 폼을 보여줍니다.
*/
const AuthFormBlock = styled.div`
h3 {
margin: 0;
color: ${palette.gray[8]};
margin-bottom: 1rem;
}
`;
/**
* 스타일링된 input
*/
const StyledInput = styled.input`
font-size: 1rem;
border: none;
border-bottom: 1px solid ${palette.gray[5]};
padding-bottom: 0.5rem;
outline: none;
width: 100%;
&:focus {
color: $oc-teal-7;
border-bottom: 1px solid ${palette.gray[7]};
}
& + & {
margin-top: 1rem;
}
`;
/**
* 폼 하단에 로그인 혹은 회원가입 링크를 보여줌
*/
const Footer = styled.div`
margin-top: 2rem;
text-align: right;
a {
color: ${palette.gray[6]};
text-decoration: underline;
&:hover {
color: ${palette.gray[9]};
}
}
`;
const ButtonWithMarginTop = styled(Button)`
margin-top: 1rem;
`;
const textMap = {
login: '로그인',
register: '회원가입'
};
/**
* 에러를 보여줍니다
*/
const ErrorMessage = styled.div`
color: red;
text-align: center;
font-size: 0.875rem;
margin-top: 1rem;
`;
const AuthForm = ({ type, form, onChange, onSubmit, error }) => {
const text = textMap[type];
return (
<AuthFormBlock>
<h3>{text}</h3>
<form onSubmit={onSubmit}>
<StyledInput
autoComplete="username"
name="username"
placeholder="아이디"
onChange={onChange}
value={form.username}
/>
<StyledInput
autoComplete="new-password"
name="password"
placeholder="비밀번호"
type="password"
onChange={onChange}
value={form.password}
/>
{type === 'register' && (
<StyledInput
autoComplete="new-password"
name="passwordConfirm"
placeholder="비밀번호 확인"
type="password"
onChange={onChange}
value={form.passwordConfirm}
/>
)}
{error && <ErrorMessage>{error}</ErrorMessage>}
<ButtonWithMarginTop cyan fullWidth style={{ marginTop: '1rem' }}>
{text}
</ButtonWithMarginTop>
</form>
<Footer>
{type === 'login' ? (
<Link to="/register">회원가입</Link>
) : (
<Link to="/login">로그인</Link>
)}
</Footer>
</AuthFormBlock>
);
};
export default AuthForm;
AuthForm의 함수 파라미터로 error가 들어가있는데, 이는 props로 error값을 받아왔을때 error메세지를 렌더링해주기 위한 역할이다.
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { changeField, initializeForm, login } from '../modules/auth';
import AuthForm from '../components/auth/AuthForm';
// import { check } from '../../modules/user';
import { useNavigate } from 'react-router-dom';
const LoginForm = () => {
const navigate = useNavigate();
const [error, setError] = useState(null);
const dispatch = useDispatch();
const { form, auth, authError} = useSelector(({ auth }) => ({
form: auth.login,
auth: auth.auth,
authError: auth.authError,
// user: user.user,
}));
// 인풋 변경 이벤트 핸들러
const onChange = (e) => {
const { value, name } = e.target;
dispatch(
changeField({
form: 'login',
key: name,
value,
}),
);
};
// 폼 등록 이벤트 핸들러
const onSubmit = (e) => {
e.preventDefault();
const { username, password } = form;
dispatch(login({ username, password }));
};
// 컴포넌트가 처음 렌더링 될 때 form 을 초기화함
useEffect(() => {
dispatch(initializeForm('login'));
}, [dispatch]);
useEffect(() => {
if (authError) {
console.log('오류 발생');
console.log(authError);
setError('로그인 실패');
return;
}
if (auth) {
window.alert('로그인 성공!');
console.log('로그인 성공');
navigate('/');
try {
localStorage.setItem('auth', JSON.stringify(auth));
} catch (e) {
console.log('localStorage is not working');
}
// dispatch(check());
}
}, [auth, authError, dispatch]);
// useEffect(() => {
// if (user) {
// navigate('/');
// try {
// localStorage.setItem('user', JSON.stringify(user));
// } catch (e) {
// console.log('localStorage is not working');
// }
// }
// }, [navigate, user]);
return (
<AuthForm
type="login"
form={form}
onChange={onChange}
onSubmit={onSubmit}
error={error}
/>
);
};
export default LoginForm;
import React, { useEffect,useState } from 'react';
import {useNavigate} from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { changeField, initializeForm, register } from '../modules/auth';
import AuthForm from '../components/auth/AuthForm';
const RegisterForm = () => {
const [error,setError] = useState(null);
const navigate= useNavigate();
const dispatch = useDispatch(); //스토어에서 컨테이너 컴포넌트를 가져옴
const { form, auth, authError} = useSelector(({ auth}) => ({
form: auth.register,
auth: auth.auth,
authError: auth.authError,
// user: user.user
}));
// 인풋 변경 이벤트 핸들러
const onChange = (e) => {
const { value, name } = e.target;
dispatch(
changeField({
form: 'register',
key: name,
value,
}),
);
};
// 폼 등록 이벤트 핸들러
const onSubmit = (e) => {
e.preventDefault();
const { username, password, passwordConfirm } = form; // state 에 존재하는 form에 따른 username,password,passwordconfirm key-value를 객체비구조화로 나타냄.
// 하나라도 비어있다면
if ([username, password, passwordConfirm].includes('')) {
setError('빈 칸을 모두 입력하세요.');
return;
}
// 비밀번호가 일치하지 않는다면
if (password !== passwordConfirm) {
setError('비밀번호가 일치하지 않습니다.');
dispatch(changeField({ form: 'register', key: 'password', value: '' }));
dispatch(
changeField({ form: 'register', key: 'passwordConfirm', value: '' }),
);
return;
}
dispatch(register({ username, password }));
};
//컴포넌트가 처음 렌더링될 때 form 을 초기화함
useEffect(() => {
dispatch(initializeForm('register'));
}, [dispatch]);
//회원가입 성공/실패 처리
useEffect(() => {
if (authError) {
// 계정명이 이미 존재할 때
if (authError.response.status === 409) {
setError('이미 존재하는 계정명입니다.');
return;
}
// 기타 이유
setError('회원가입 실패');
return;
}
if (auth){
window.alert('회원가입 성공!');
console.log('회원가입 성공');
dispatch(initializeForm('register'));
console.log(auth);
navigate('/login');
// dispatch(check());
}
}, [auth, authError,dispatch]);
//user잘 설정됨?
// useEffect(()=>{
// if (user){
// console.log('check API 성공');
// console.log(user);
// }
// },[user]);
return (
<AuthForm
type="register"
form={form}
onChange={onChange}
onSubmit={onSubmit}
error={error}
/>
);
};
export default RegisterForm;
본격적으로 로그인을 구현해보자.
먼저, 로그인을 구현하기 위해서 '세션'을 사용하기로 했다. cookie를 사용하는 방법은 보안상의 이유로 사용할 수 없었다. 하지만 그렇다고 Session을 써보지도 않은 상태에서 벌써부터 SpringSecurity를 다루는 것은 욕심이라고 생각했다.
package com.example.SpringAndReact.Repository;
import com.example.SpringAndReact.Domain.Member;
import java.util.Optional;
public interface MemberRepository {
Member saveMember(Member member);
Optional<Member> findByUserId(String id);
Optional<Member> findMember(String id, String password);
}
package com.example.SpringAndReact.Repository;
import com.example.SpringAndReact.Domain.Member;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long,Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member saveMember(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findByUserId(String id) {
return store.values().stream().filter(member->member.getUsername().equals(id)).findAny();
}
@Override
public Optional<Member> findMember(String id, String password) {
return store.values().stream()
.filter(member -> (member.getUsername().equals(id) && member.getPassword().equals(password)))
.findAny();
}
}
package com.example.SpringAndReact.Repository;
import com.example.SpringAndReact.Domain.Member;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import java.util.List;
import java.util.Optional;
public class JpaMemberRepository implements MemberRepository{
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
@Override
public Member saveMember(Member member) {
em.persist(member);
return member;
}
@Override
public Optional<Member> findByUserId(String id) {
List<Member> result = em.createQuery("select m from Member m where m.username = :name", Member.class)
.setParameter("name", id)
.getResultList();
return result.stream().findAny();
}
@Override
public Optional<Member> findMember(String id, String password) {
if(!findByUserId(id).isPresent()){
try{
throw new IllegalAccessException("세상에 그런 아이디는 등록된 적이 없어요!");
}catch(IllegalAccessException e){
throw new RuntimeException(e);
}
}
TypedQuery<Member> query = em.createQuery("select m from Member m where m.username = :name and m.password = :password", Member.class);
query.setParameter("name",id);
query.setParameter("password",password);
List<Member> resultList = query.getResultList();
if (resultList.isEmpty()){
try{
throw new IllegalAccessException("password를 다시 확인해 주세요!");
} catch(IllegalAccessException e){
throw new RuntimeException(e);
}
}
return resultList.stream().findAny();
};
}
다음과 같이 "JPQL" 쿼리문을 직접 작성해보는 연습을 하는겸, 쿼리를 직접 짜보았다.
package com.example.SpringAndReact.Service;
import com.example.SpringAndReact.Domain.Member;
import java.util.Optional;
public interface MemberService {
Long join(Member member);
void validateDuplicateMember(Member member);
Optional<Member> checkIdPassword(Member member);
}
package com.example.SpringAndReact.Service;
import com.example.SpringAndReact.Domain.Member;
import com.example.SpringAndReact.Repository.MemberRepository;
import jakarta.transaction.Transactional;
import java.util.Optional;
@Transactional
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public Long join(Member member) {
validateDuplicateMember(member);
memberRepository.saveMember(member);
return member.getId(); //고유 id 반환
}
@Override
public void validateDuplicateMember(Member member) {
memberRepository.findByUserId(member.getUsername()).ifPresent(mem -> {
try {
throw new IllegalAccessException("이미 존재하는 회원입니다.");
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
});
}
@Override
public Optional<Member> checkIdPassword(Member member) {
Optional<Member> resultMember = memberRepository.findMember(member.getUsername(), member.getPassword());
if (!resultMember.isPresent()){
try{
throw new IllegalAccessException("아이디, 비밀번호를 다시 확인해주세요.");
} catch(IllegalAccessException e){
throw new RuntimeException(e);
}
}
return resultMember;
}
}