사용자의 상태를 담을 user라는 리덕스 모듈을 만들었습니다.
src/modules/user.js
import { createAction, handleActions } from 'redux-actions';
import { takeLatest } from 'redux-saga/effects';
import createRequestSaga, {
createRequestActionTypes,
} from '../lib/api/createRequestSaga';
import * as authAPI from '../lib/api/auth';
// 새로고침 이후 임시 로그인 처리
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);
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,
);
또한 루트 리듀서에 포함시켜 주었습니다.
src/modules/index.js
import { combineReducers } from 'redux';
import { all } from 'redux-saga/effects';
import auth, { authSaga } from './auth';
import loading from './loading';
import user, { userSaga } from './user';
const rootReducer = combineReducers({
auth,
loading,
user,
});
export function* rootSaga() {
yield all([authSaga(), userSaga()]);
}
export default rootReducer;
회원가입 구현 && 에러처리
src/containers/auth/RegisterForm.js
/* eslint-disable no-restricted-globals */
import React, { useEffect, useState } 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';
import { useNavigate } from 'react-router-dom';
const RegisterForm = () => {
const [error, setError] = useState(null);
const navigate = useNavigate();
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;
// 하나라도 비어있다면
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 }));
};
// 회원가입 성공/실패 처리
useEffect(() => {
if (authError) {
// 계정명이 이미 존재할때
if (authError.response.status === 409) {
setError('이미 존재하는 계정명입니다.');
return;
}
// 기타 이유
setError('회원가입 실패');
return;
}
if (auth) {
console.log('회원가입 성공');
console.log(auth);
dispatch(check());
}
}, [auth, authError, dispatch]);
// 컴포넌트가 처음 렌더링될 떄 form을 초기화함
useEffect(() => {
dispatch(initializeForm('register'));
}, [dispatch]);
useEffect(() => {
if (user) {
navigate('/');
}
}, [user, navigate]);
return (
<AuthForm
type="register"
form={form}
onChange={onChange}
onSubmit={onSubmit}
error={error}
/>
);
};
export default RegisterForm;
src/components/auth/AuthForm.js
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;
}
`;
const StyledInput = styled.input`
font-size: 1rem;
border: none;
width: 100%;
border-bottom: 1px solid ${palette.gray[5]};
padding-bottom: 0.5rem;
outline: none;
&:focus {
color: #0ca678;
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="아이디"
value={form.username}
onChange={onChange}
/>
<StyledInput
autoComplete="new-password"
name="password"
placeholder="비밀번호"
type="password"
value={form.password}
onChange={onChange}
/>
{type === 'register' && (
<StyledInput
autoComplete="new-password"
name="passwordConfirm"
placeholder="비밀번호 확인"
type="password"
onChange={onChange}
/>
)}
{error && <ErrorMessage>{error}</ErrorMessage>}
<ButtonWithMarginTop cyan fullWidth>
{text}
</ButtonWithMarginTop>
</form>
<Footer>
{type === 'login' ? (
<Link to="/register">회원가입</Link>
) : (
<Link to="/login">로그인</Link>
)}
</Footer>
</AuthFormBlock>
);
};
export default AuthForm;
회원 인증에 실패 했을 때 띄워줄 컴포넌트를 만들고 각 경우에 다른 문구를 띄워주게 만들었습니다.
로그인 구현 && 에러처리
src/containers/auth/LoginForm.js
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { changeField, initializeForm } from '../../modules/auth';
import AuthForm from '../../components/auth/AuthForm';
import { useNavigate } from 'react-router-dom';
import { login } from '../../modules/auth';
import { check } from '../../modules/user';
const LoginForm = () => {
const [error, setError] = useState(null);
const navigate = useNavigate();
const dispatch = useDispatch();
const { form, auth, authError, user } = useSelector(({ auth, user }) => ({
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) {
console.log('로그인 성공');
dispatch(check());
}
}, [auth, authError, dispatch]);
useEffect(() => {
if (user) {
navigate('/');
}
}, [navigate, user]);
return (
<AuthForm
type="login"
form={form}
onChange={onChange}
onSubmit={onSubmit}
error={error}
/>
);
};
export default LoginForm;
src/components/auth/AuthForm.js
위와 동일
이제 구현된 로그인창과 회원가입 창에서의 결과와 실패에 따른 실패 문구를 보도록 하겠습니다.
또한 만약 계정 인증에 성공하게 되면 바로 '/'로 이동하게 react-router-dom의 navigate('/'); 을 이용하여 구현하였습니다.
헤더 컴포넌트 만들기
우선 반응형 디자인을 더 편하게 작업하기 위해 Responsive 컴포넌트를 만들어서 추후 다른 컴포넌트에서 활용하도록 하겠습니다.
src/components/common/Responsive.js
import React from 'react';
import styled from 'styled-components';
const ResponsiveBlock = styled.div`
padding-left: 1rem;
padding-right: 1rem;
width: 1024px;
margin: 0 auto;
/* 브라우저 크기에 따라 가로 크기 변경 */
@media (max-width: 1024px) {
width: 768px;
}
@media (max-width: 768px) {
width: 100%;
}
`;
const Responsive = ({ children, ...rest }) => {
// style, className, onClick, onMouseMove등의 props를 사용할 수 있도록
// ...rest를 사용하여 ResponsiveBlock에 설정
return <ResponsiveBlock {...rest}>{children}</ResponsiveBlock>;
};
export default Responsive;
src/components/common/Header.js
import React from 'react';
import styled from 'styled-components';
import Responsive from './Responsive';
import Button from './Button';
const HeaderBlock = styled.div`
width: 100%;
background: white;
position: fixed;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
`;
/**
* Responsive 컴포넌트의 속성에 스타일을 추가해서 새로운 컴포넌트 생성
*/
const Wrapper = styled(Responsive)`
height: 4rem;
display: flex;
align-items: center;
justify-content: space-between;
.logo {
font-size: 1.125rem;
font-weight: 800;
letter-spacing: 2px;
}
.right {
display: flex;
align-items: center;
}
`;
/**
* 헤더가 fixed로 되어 있기 때문에 페이지의 콘텐츠가 4rem아래에 나타나도록 해 주는 콘텐츠
*/
const Spacer = styled.div`
height: 4rem;
`;
const Header = () => {
return (
<>
<HeaderBlock>
<Wrapper>
<div className="logo">REACTERS</div>
<div className="right">
<Button>로그인</Button>
</div>
</Wrapper>
</HeaderBlock>
<Spacer />
</>
);
};
export default Header;
헤더 컴포넌트가 언제나 페이지 상단에 떠 있도록 poisition값을 fixed로 설정했습니다. 또한 화면 콘텐츠를 그 헤더 아래에 배치해 주기 위해 Spacer라는 컴포넌트를 만들어서 헤더 크기만큼 공간을 차지하도록 했습니다.
src/pages/PostListPage.js
import React from 'react';
import Header from '../components/common/Header';
const PostListPage = () => {
return (
<div>
<Header />
<div>안녕하세요!</div>
</div>
);
};
export default PostListPage;
또한 로그인을 누르면 '/login'으로 REACTERS를 클릭하면 '/'으로 이동하게끔 만들어 주어야 합니다.
src/components/common/Header.js
import React from 'react';
import styled from 'styled-components';
import Responsive from './Responsive';
import Button from './Button';
import { useNavigate, Link } from 'react-router-dom';
const HeaderBlock = styled.div`
width: 100%;
background: white;
position: fixed;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
`;
/**
* Responsive 컴포넌트의 속성에 스타일을 추가해서 새로운 컴포넌트 생성
*/
const Wrapper = styled(Responsive)`
height: 4rem;
display: flex;
align-items: center;
justify-content: space-between;
.logo {
font-size: 1.125rem;
font-weight: 800;
letter-spacing: 2px;
}
.right {
display: flex;
align-items: center;
}
`;
/**
* 헤더가 fixed로 되어 있기 때문에 페이지의 콘텐츠가 4rem아래에 나타나도록 해 주는 콘텐츠
*/
const Spacer = styled.div`
height: 4rem;
`;
const Header = () => {
const navigate = useNavigate();
return (
<>
<HeaderBlock>
<Wrapper>
<Link to="/" className="logo">
REACTERS
</Link>
<div className="right">
<Button to="/login" navigate={navigate}>
로그인
</Button>
</div>
</Wrapper>
</HeaderBlock>
<Spacer />
</>
);
};
export default Header;
src/components/common/Button.js
import React from 'react';
import styled, { css } from 'styled-components';
import palette from '../../lib/styles/palette';
import { Link } from 'react-router-dom';
const buttonStyle = css`
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: bold;
padding: 0.25rem 1rem;
color: white;
outline: none;
cursor: pointer;
background: ${palette.gray[8]};
&:hover {
background: ${palette.gray[6]};
}
${(props) =>
props.cyan &&
css`
background: ${palette.cyan[5]};
&:hover {
background: ${palette.cyan[4]};
}
`}
${(props) =>
props.fullWidth &&
css`
padding-top: 0.75rem;
padding-bottom: 0.75rem;
width: 100%;
font-size: 1.125rem;
`}
`;
const StyledButton = styled.button`
${buttonStyle}
`;
const StyledLink = styled(Link)`
${buttonStyle}
`;
const Button = (props) => {
return props.to ? (
<StyledLink {...props} cyan={props.cyan ? 1 : 0} />
) : (
<StyledButton {...props} />
);
};
export default Button;
위 코드에서는 styledLini라는 컴포넌트를 새로 만들었습니다. StyledButton과 똑같은 스타일을 사용하므로, 기존에 사용하던 스타일을 buttonStyle이라는 값에 담에서 재사용했습니다. 그리고 Button컴포넌트 내부에서 props.to값에 따라서 StyledLink를 사용할지, StyledButton을 사용할지 설정하도록 설정했습니다.
StyledLink를 사용하는 과정에서는 props.cyan값을 숫자 1과 0으로 변환해주었습니다. 이렇게 한 이유는 styled()함수로 감싸서 만든 컴포넌트의 경우에는 임의 props가 필터링되지 않기 때문입니다.(styled.button으로 만든 컴포넌트의 경우에는 cyan과 같은 임의 props가 자동으로 필터링되어 스타일을 만드는 용도로만 사용되고, 실제 button엘리먼트에게 속성이 전달되지 않습니다.) 필터링이 되지 않으면 cyan={true}라는 값이 Link에서 사용하는 a태그에 그대로 전달되는데, a태그는 boolean값이 임의 props로 설정되는 것을 허용하지 않습니다. 숫자/문자열만 허용하기 때문에 삼항 연산자를 사용하여 boolean을 숫자로 변환해 준 것입니다.
로그인 상태를 보여주고 유지하기
로그인 상태를 유지하기 위해 브라우저에 내장되어 있는 localStorage를 사용하겠습니다.
src/containers/auth/RegisterForm.js
/* eslint-disable no-restricted-globals */
import React, { useEffect, useState } 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';
import { useNavigate } from 'react-router-dom';
const RegisterForm = () => {
const [error, setError] = useState(null);
const navigate = useNavigate();
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;
// 하나라도 비어있다면
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 }));
};
// 회원가입 성공/실패 처리
useEffect(() => {
if (authError) {
// 계정명이 이미 존재할때
if (authError.response.status === 409) {
setError('이미 존재하는 계정명입니다.');
return;
}
// 기타 이유
setError('회원가입 실패');
return;
}
if (auth) {
console.log('회원가입 성공');
console.log(auth);
dispatch(check());
}
}, [auth, authError, dispatch]);
// 컴포넌트가 처음 렌더링될 떄 form을 초기화함
useEffect(() => {
dispatch(initializeForm('register'));
}, [dispatch]);
useEffect(() => {
if (user) {
navigate('/');
try {
localStorage.setItem('user', JSON.stringify(user));
} catch (e) {
console.log('localStorage is not working');
}
}
}, [user, navigate]);
return (
<AuthForm
type="register"
form={form}
onChange={onChange}
onSubmit={onSubmit}
error={error}
/>
);
};
export default RegisterForm;
src/containers/auth/LoginForm.js
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { changeField, initializeForm } from '../../modules/auth';
import AuthForm from '../../components/auth/AuthForm';
import { useNavigate } from 'react-router-dom';
import { login } from '../../modules/auth';
import { check } from '../../modules/user';
const LoginForm = () => {
const [error, setError] = useState(null);
const navigate = useNavigate();
const dispatch = useDispatch();
const { form, auth, authError, user } = useSelector(({ auth, user }) => ({
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) {
console.log('로그인 성공');
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;
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { BrowserRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer, { rootSaga } from './modules';
import createSagaMiddleware from '@redux-saga/core';
import { tempSetUser, check } from './modules/user';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(sagaMiddleware)),
);
function loadUser() {
try {
const user = localStorage.getItem('user');
if (!user) return;
store.dispatch(tempSetUser(JSON.parse(user)));
store.dispatch(check());
} catch (e) {
console.log('localStorage is not working');
}
}
sagaMiddleware.run(rootSaga);
loadUser();
ReactDOM.render(
<Provider store={store}>
<Router>
<App />
</Router>
</Provider>,
document.getElementById('root'),
);
회원가입 및 로그인을 하면 사용자 정보를 localStorage에 저장하도록 작업해 주었습니다. 페이지를 새로고침했을 때도 로그인 상태를 유지하려면, 리액트 앱이 브라우저에서 맨 처음 렌더링될 때 localStorage에서 값을 불러와 리덕스 스토어 안에 넣도록 구현해 주어야 합니다.
이 작업은 App컴포넌트에서 useEffect를 사용하여 처리하거나, App컴포넌트를 클래스형 컴포넌트로 변환하여 componentDidMound메서드를 만들고 그 안에서 처리해도 됩니다. 하지만 여기서는 프로젝트의 엔트리 파일인 index.js에서 처리해 주었습니다,.
왜냐하면, componentDidMound와 useEffect는 컴포넌트가 한 번 렌더링된 이후에 실행되기 때문입니다. 이 경우에는 사용자가 아주 짧은 깜빡임 현상(로그인이 나타났다가 로그아웃이 나타나는 현상)을 경험할 수도 있습니다.
로그인 검증 실패 시 정보 초기화
로그인 정보가 만료되었을 때를 대비하여 사용자 정보를 초기화하는 작업을 해 보겠습니다.
src/modules/user.js
import { createAction, handleActions } from 'redux-actions';
import { takeLatest } from 'redux-saga/effects';
import createRequestSaga, {
createRequestActionTypes,
} from '../lib/api/createRequestSaga';
import * as authAPI from '../lib/api/auth';
// 새로고침 이후 임시 로그인 처리
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);
export const check = createAction(CHECK);
const checkSaga = createRequestSaga(CHECK, authAPI.check);
function checkFailureSaga() {
try {
localStorage.removeItem('user');
} catch (e) {
console.log('localStorage is not working');
}
}
export function* userSaga() {
yield takeLatest(CHECK, checkSaga);
yield takeLatest(CHECK_FAILURE, checkFailureSaga);
}
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,
);
CHECK_FAILURE액션이 발생할 때 checkFailureSaga함수가 호출되도록 설정했습니다. 또한 checkFailureSaga함수에서는 yield를 사용하지 않으므로 function*를 사용하여 제너레이터 함수 형태로 만들어 주지 않아도 괜찮습니다.
로그아웃 구현
로그아웃 기능을 구현하는 것은 간단합니다. 그냥 로그아웃API를 호출해주고 localStorage안의 값을 삭제해 주면 됩니다.
src/lib/api/auth.js
import client from './client';
// 로그인
export const login = ({ username, password }) =>
client.post('/api/auth/login', { username, password });
// 회원가입
export const register = ({ username, password }) =>
client.post('/api/auth/register', { username, password });
// 로그인 상태 확인
export const check = () => client.get('/api/auth/check');
// 로그아웃
export const logout = () => client.post('/api/auth/logout');
src/modules/user.js
import { createAction, handleActions } from 'redux-actions';
import { call, takeLatest } from 'redux-saga/effects';
import createRequestSaga, {
createRequestActionTypes,
} from '../lib/api/createRequestSaga';
import * as authAPI from '../lib/api/auth';
// 새로고침 이후 임시 로그인 처리
const TEMP_SET_USER = 'user/TEMP_SET_USER';
// 회원 정보 확인
const [CHECK, CHECK_SUCCESS, CHECK_FAILURE] =
createRequestActionTypes('user/CHECK');
const LOGOUT = 'user/LOGOUT';
export const tempSetUser = createAction(TEMP_SET_USER, (user) => user);
export const check = createAction(CHECK);
export const logout = createAction(LOGOUT);
const checkSaga = createRequestSaga(CHECK, authAPI.check);
function* logoutSaga() {
try {
yield call(authAPI.logout);
localStorage.removeItem('user');
} catch (e) {
console.log(e);
}
}
function checkFailureSaga() {
try {
localStorage.removeItem('user');
} catch (e) {
console.log('localStorage is not working');
}
}
export function* userSaga() {
yield takeLatest(CHECK, checkSaga);
yield takeLatest(CHECK_FAILURE, checkFailureSaga);
yield takeLatest(LOGOUT, logoutSaga);
}
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,
}),
[LOGOUT]: (state) => ({
...state,
user: null,
}),
},
initialState,
);
src/containers/common/HeaderContainer.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Header from '../../components/common/Header';
import { logout } from '../../modules/user';
const HeaderContainer = () => {
const dispatch = useDispatch();
const { user } = useSelector(({ user }) => ({ user: user.user }));
const onLogout = () => {
dispatch(logout());
};
return <Header user={user} onLogout={onLogout} />;
};
export default HeaderContainer;
src/components/common/Header.js
import React from 'react';
import styled from 'styled-components';
import Responsive from './Responsive';
import Button from './Button';
import { Link } from 'react-router-dom';
const HeaderBlock = styled.div`
width: 100%;
background: white;
position: fixed;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
`;
/**
* Responsive 컴포넌트의 속성에 스타일을 추가해서 새로운 컴포넌트 생성
*/
const Wrapper = styled(Responsive)`
height: 4rem;
display: flex;
align-items: center;
justify-content: space-between;
.logo {
font-size: 1.125rem;
font-weight: 800;
letter-spacing: 2px;
color: black;
}
.right {
display: flex;
align-items: center;
}
`;
/**
* 헤더가 fixed로 되어 있기 때문에 페이지의 콘텐츠가 4rem아래에 나타나도록 해 주는 콘텐츠
*/
const Spacer = styled.div`
height: 4rem;
`;
const UserInfo = styled.div`
font-weight: 800;
margin-right: 1rem;
`;
const Header = ({ user, onLogout }) => {
return (
<>
<HeaderBlock>
<Wrapper>
<Link to="/" className="logo">
REACTERS
</Link>
{user ? (
<div className="right">
<UserInfo>{user.username}</UserInfo>
<Button onClick={onLogout}>로그아웃</Button>
</div>
) : (
<div className="right">
<Button to="/login">로그인</Button>
</div>
)}
</Wrapper>
</HeaderBlock>
<Spacer />
</>
);
};
export default Header;
위와같이 로그아웃 버튼을 누르면 'user/LOGOUT'액션이 dispatch되고 localStorage값이 없어지면서 로그아웃이 됨을 확인할 수 있습니다.
'React > ReactJs' 카테고리의 다른 글
React - FrontEnd Project - 4 (0) | 2022.03.20 |
---|---|
React - React Router v6 (0) | 2022.03.18 |
React - FrontEnd project - 2 (0) | 2022.03.15 |
React - FrontEnd Project - 1 (0) | 2022.03.15 |
React - JWT (0) | 2022.03.13 |