해당 시리즈는 제목 그대로 김민준님의 '리액트를 다루는 기술'을 요약한 글입니다.
목차
24장 프런트엔드 프로젝트: 시작 및 회원 인증 구현
24장 프런트엔드 프로젝트: 시작 및 회원 인증 구현
앞으로 개발할 기능
- 회원가입/로그인
- 글쓰기
- 포스트 목록 보기/포스트 읽기
- 포스트 수정 및 삭제
프로젝트를 처음 만들고 나서 설계를 시작할 때 가장 먼저 무엇을 하면 좋을까요? 바로 리액트 라우터를 프로젝트에 설치하고 적용하는 것입니다.
앞으로 만들 라우트 컴포넌트
- LoginPage
- RegisterPage
- WritePage
- PostPage
- PostListPage
라우터 적용
// src/pages/LoginPage.js
import React from 'react';
const LoginPage = () => {
return <div>로그인</div>
};
export default LoginPage;
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
document.getElementById('root'),
);
// src/App.js
import React from 'react';
import { Route } from 'react-router-dom';
import PostListPage from './pages/PostListPage';
import LoginPage from './pages/LoginPage';
import RegisterPage from './pages/RegisterPage';
import WritePage from './pages/WritePage';
import PostPage from './pages/PostPage';
function App() {
return (
<>
<Route component={PostListPage} path={['/@:username', '/']} exact />
<Route component={LoginPage} path="/login" exact />
<Route component={RegisterPage} path="/register" exact />
<Route component={WritePage} path="/write" exact />
<Route component={PostPage} path="/@:username/:postId" exact />
</>
);
}
export default App;
path에 '/@:username'이라고 입력하면 http://localhost:3000/@velopert 같은 경로에서 velopert를 username 파라미터로 읽을 수 있게 해줌. (Medium, 브런치 같은 서비스에서도 계정명을 주소 경로 안에 넣을 때 @을 넣는 방식을 사용)
스타일 설정
// lib/styles/palette.js
const palette = {
gray: [
'#f8f9fa',
'#f1f3f5',
'#e9ecef',
'#dee2e6',
'#ced4da',
'#adb5bd',
'#868e96',
'#495057',
'#343a40',
'#212529',
],
cyan: [
'#e3fafc',
'#c5f6fa',
'#99e9f2',
'#66d9e8',
'#3bc9db',
'#22b8cf',
'#15aabf',
'#1098ad',
'#0c8599',
'#0b7285',
],
};
export default palette;
Button 컴포넌트 만들기
// components/common/Button.js
import React from 'react';
import styled, { css } from 'styled-components';
import palette from '../../lib/styles/palette';
const StyledButton = styled.button`
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]};
}
`;
const Button = (props) => <StyledButton {...props} />;
export default Button;
{ ...props }는 Button이 받아 오는 props를 모두 StyledButton에 전달한다는 의미.
리덕스 적용
// src/modules/auth.js
import { createAction, handleActions } from 'redux-actions';
import produce from 'immer';
const SAMPLE_ACTION = 'auth/SAMPLE_ACTION';
export const sampleAction = createAction(SAMPLE_ACTION);
const initialState = {};
const auth = handleActions(
{
[SAMPLE_ACTION]: (state, action) => state,
},
initialState,
);
export default auth;
// src/modules/index.js
import { combineReducers } from 'redux';
import auth from './auth';
const rootReducer = combineReducers({ auth });
export default rootReducer;
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import rootReducer from './modules';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension';
const store = createStore(rootReducer, composeWithDevTools());
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>,
document.getElementById('root'),
);
회원가입과 로그인 구현
UI 준비하기
// src/components/auth/AuthForm.js
import React from 'react';
import styled from 'styled-components';
const AuthFormBlock = styled.div``;
const AuthForm = () => {
const text = textMap[type];
return (
<AuthFormBlock>
AuthForm
</AuthFormBlock>
);
};
export default AuthForm;
// src/components/auth/AuthTemplate.js
import React from 'react';
import styled from 'styled-components';
const AuthTemplateBlock = styled.div``;
const AuthTemplate = ({ children }) => {
return (
<AuthTemplateBlock>
{children}
</AuthTemplateBlock>
);
};
export default AuthTemplate;
// pages/LoginPage.js
import React from 'react';
import AuthTemplate from '../components/auth/AuthTemplate';
import AuthForm from '../components/auth/AuthForm';
const LoginPage = () => {
return (
<AuthTemplate>
<AuthForm />
</AuthTemplate>
);
};
export default LoginPage;
// pages/RegisterPage.js
import React from 'react';
import AuthTemplate from '../components/auth/AuthTemplate';
import AuthForm from '../components/auth/AuthForm';
const RegisterPage = () => {
return (
<AuthTemplate>
<AuthForm />
</AuthTemplate>
);
};
export default RegisterPage;
AuthTemplate 완성하기
// components/auth/AuthTemplate.js
import React from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import palette from '../../lib/styles/palette';
const AuthTemplateBlock = styled.div`
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
background: ${palette.gray[2]};
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`;
const WhiteBox = styled.div`
.logo-area {
padding-bottom: 2rem;
text-align: center;
font-weight: bold;
letter-spacing: 2px;
}
box-shadow: 0 0 8px rgba(0, 0, 0, 0.025);
padding: 2rem;
width: 360px;
background: white;
border-radius: 2px;
`;
const AuthTemplate = ({ children }) => {
return (
<AuthTemplateBlock>
<WhiteBox>
<div className="logo-area">
<Link to="/">REACTERS</Link>
</div>
{children}
</WhiteBox>
</AuthTemplateBlock>
);
};
export default AuthTemplate;
AuthForm 완성하기
// components/auth/AuthForm.js
import React from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import palette from '../../lib/styles/palette';
import Button from '../common/Button';
const AuthFormBlock = styled.div`
h2 {
margin: 0 0 2rem 0;
color: ${palette.gray[8]};
}
`;
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: 1.4rem;
}
`;
const Footer = styled.footer`
margin-top: 2rem;
text-align: right;
a {
color: ${palette.gray[6]};
text-decoration: underline;
&:hover {
color: ${palette.gray[9]};
}
}
`;
const AuthForm = () => {
return (
<AuthFormBlock>
<h2>로그인</h2>
<form>
<StyledInput
autoComplete="username"
name="username"
placeholder="아이디"
/>
<StyledInput
autoComplete="new-password"
name="password"
placeholder="비밀번호"
type="password"
/>
<Button>
로그인
</Button>
</form>
<Footer>
<Link to="/register">회원가입</Link>
</Footer>
</AuthFormBlock>
);
};
export default AuthForm;
로그인 버튼 디자인 + type props에 따라 내용 달라지게 하기
// components/common/Button.js
import React from 'react';
import styled, { css } from 'styled-components';
import palette from '../../lib/styles/palette';
const StyledButton = styled.button`
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.fullWidth &&
css`
padding: 0.75rem 0;
width: 100%;
font-size: 1.125rem;
`}
${(props) =>
props.cyan &&
css`
background: ${palette.cyan[5]};
&:hover {
background: ${palette.cyan[4]};
}
`}
`;
const Button = (props) => <StyledButton {...props} />;
export default Button;
import React from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import palette from '../../lib/styles/palette';
import Button from '../common/Button';
const AuthFormBlock = styled.div`
h2 {
margin: 0 0 2rem 0;
color: ${palette.gray[8]};
}
`;
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: 1.4rem;
}
`;
const Footer = styled.footer`
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 AuthForm = ({ type }) => {
const text = textMap[type];
return (
<AuthFormBlock>
<h2>{text}</h2>
<form onSubmit={onSubmit}>
<StyledInput
autoComplete="username"
name="username"
placeholder="아이디"
/>
<StyledInput
autoComplete="new-password"
name="password"
placeholder="비밀번호"
type="password"
/>
{type === 'register' && (
<StyledInput
autoComplete="new-password"
name="passwordConfirm"
placeholder="비밀번호 확인"
type="password"
onChange={onChange}
value={form.passwordConfirm}
/>
)}
<ButtonWithMarginTop cyan fullWidth>
{text}
</ButtonWithMarginTop>
</form>
<Footer>
{type === 'login' ? (
<Link to="/register">회원가입</Link>
) : (
<Link to="/login">로그인</Link>
)}
</Footer>
</AuthFormBlock>
);
};
export default AuthForm;
// pages/LoginPage.js
import React from 'react';
import AuthTemplate from '../components/auth/AuthTemplate';
import AuthForm from '../components/auth/AuthForm';
const LoginPage = () => {
return (
<AuthTemplate>
<AuthForm type="login" />
</AuthTemplate>
);
};
export default LoginPage;
// pages/RegisterPage.js
import React from 'react';
import AuthTemplate from '../components/auth/AuthTemplate';
import AuthForm from '../components/auth/AuthForm';
const RegisterPage = () => {
return (
<AuthTemplate>
<AuthForm type="register" />
</AuthTemplate>
);
};
export default RegisterPage;
리덕스로 폼 상태 관리하기
// modules/auth.js
import { createAction, handleActions } from 'redux-actions';
import produce from 'immer';
const CHANGE_FIELD = 'auth/CHANGE_FIELD';
const INITIALIZE_FORM = 'auth/INITIALIZE_FORM';
export const changeField = createAction(
CHANGE_FIELD,
({ form, key, value }) => ({
form,
key,
value,
}),
);
export const initializeForm = createAction(INITIALIZE_FORM, (form) => form);
const initialState = {
register: {
username: '',
password: '',
passwordConfirm: '',
},
login: {
username: '',
password: '',
},
};
const auth = handleActions(
{
[CHANGE_FIELD]: (state, { payload: { form, key, value } }) =>
produce(state, (draft) => {
draft[form][key] = value;
}),
[INITIALIZE_FORM]: (state, { payload: form }) => ({
...state,
[form]: initialState[form],
}),
},
initialState,
);
export default auth;
컨테이너 컴포넌트 만들기
// containers/LoginForm.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AuthForm from '../../components/auth/AuthForm';
import { changeField, initializeForm } from '../../modules/auth';
const LoginForm = () => {
const dispatch = useDispatch();
const { form } = useSelector(({ auth }) => ({ form: auth.login }));
const onChange = (e) => {
const { value, name } = e.target;
dispatch(
changeField({
form: 'login',
key: name,
value,
}),
);
};
const onSubmit = (e) => {
e.preventDefault();
};
useEffect(() => {
dispatch(initializeForm('login'));
}, [dispatch]);
return (
<AuthForm
type="login"
form={form}
onChange={onChange}
onSubmit={onSubmit}
/>
);
};
export default LoginForm;
// pages/LoginPage.js
import React from 'react';
import AuthTemplate from '../components/auth/AuthTemplate';
import LoginForm from '../containers/auth/LoginForm';
const LoginPage = () => {
return (
<AuthTemplate>
<LoginForm />
</AuthTemplate>
);
};
export default LoginPage;
// containers/RegisterForm.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AuthForm from '../../components/auth/AuthForm';
import { changeField, initializeForm } from '../../modules/auth';
const RegisterForm = () => {
const dispatch = useDispatch();
const { form } = useSelector(({ auth }) => ({ form: auth.register }));
const onChange = (e) => {
const { value, name } = e.target;
dispatch(
changeField({
form: 'register',
key: name,
value,
}),
);
};
const onSubmit = (e) => {
e.preventDefault();
};
useEffect(() => {
dispatch(initializeForm('register'));
}, [dispatch]);
return (
<AuthForm
type="register"
form={form}
onChange={onChange}
onSubmit={onSubmit}
/>
);
};
export default RegisterForm;
// pages/RegisterPage.js
import React from 'react';
import AuthTemplate from '../components/auth/AuthTemplate';
import RegisterForm from '../containers/auth/RegisterForm';
const RegisterPage = () => {
return (
<AuthTemplate>
<RegisterForm />
</AuthTemplate>
);
};
export default RegisterPage;
// components/auth/AuthForm.js
import React from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import palette from '../../lib/styles/palette';
import Button from '../common/Button';
( ... )
const AuthForm = ({ type, form, onChange, onSubmit }) => {
const text = textMap[type];
return (
<AuthFormBlock>
<h2>{text}</h2>
<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}
/>
)}
<ButtonWithMarginTop cyan fullWidth>
{text}
</ButtonWithMarginTop>
</form>
<Footer>
{type === 'login' ? (
<Link to="/register">회원가입</Link>
) : (
<Link to="/login">로그인</Link>
)}
</Footer>
</AuthFormBlock>
);
};
export default AuthForm;
'WEB🔨 > React' 카테고리의 다른 글
리액트에서 프록시 여러 개 설정하는 방법 (0) | 2021.08.02 |
---|---|
리액트를 다루는 기술 요약#14 (0) | 2021.07.12 |
리액트를 다루는 기술 요약#13 (0) | 2021.07.11 |
리액트를 다루는 기술 요약#12 (0) | 2021.07.11 |
리액트를 다루는 기술 요약#11 (0) | 2021.06.29 |