🌱 Blogrow 란 ?
blog
+grow
를 합친 말로,
내적인 성장을 위한 블로그 라는 의미로 타이틀을 지어본 프로젝트임.
📝 DAY 01 - 220518
- 기획
- 컴포넌트 / 페이지 구성 (UI)
- styled-components
- 리덕스 설정 (module, store, provider)
Back-end Part
- 회원가입 / 로그인 + 로그아웃 (미들웨어 / koa-bodyparser)
- 비밀번호 해싱 (bcrypt)
- 회원정보, 포스트 (mongoDB + mongoose)
- 서버 구축 (Koa) / 라우터 (koa-router)
- 환경변수 (dotenv)
Front-end Part
- CRA(create-react-app) 기반
- react-router-dom
- 각 routes에 대한 UI 개발
- 회원 인증 UI 구현
- 데이터 관리 -
Redux
- 글쓰기 (posts/write) - WYSIWYG 에디터 라이브러리
- 스타일링 -
styled-components
$ yarn create react-app blog-frontend
blog-backend 를 연 상태에서 우클릭 - [작업 영역에 폴더 추가] 클릭.
[파일]-[작업 영역을 다른 이름으로 저장] 클릭시 나중에도 이렇게 열 수 있게 저장됨.
blog-frontend 폴더에 설정 파일 생성.
{
"singleQuote": true,
"semi": true,
"useTabs": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80
}
{
"compilerOptions": {
"target": "es6"
}
}
react-router-dom
설치$ yarn add react-router-dom
페이지 | 설명 |
---|---|
LoginPage | 로그인 |
RegisterPage | 회원가입 |
WritePage | 글쓰기 |
PostPage | 포스트 읽기 |
PostListPage | 포스트 목록 |
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>,
);
import { Routes, Route } from 'react-router-dom';
import LoginPage from './pages/LoginPage';
import WritePage from './pages/WritePage';
import PostListPage from './pages/PostListPage';
import PostPage from './pages/PostPage';
import RegisterPage from './pages/RegisterPage';
function App() {
return (
<Routes>
<Route path="/" element={<PostListPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/write" element={<WritePage />} />
<Route path="/@:username">
<Route index element={<PostListPage />} />
<Route path=":postId" element={<PostPage />} />
</Route>
</Routes>
);
}
export default App;
index
props를 주면, path="/"와 같은 의미이다.<Route path="/@:username" element={<PostListPage />} />
<Route path="/@:username/:postId" element={<PostPage />} />
/@:username
은 해당 username에 들어간 값을 파라미터로 읽을 수 있게 해줌.-> velog도 마찬가지임. 계정명을 주소 경로안에 넣을 때, 경로에 @를 넣는 방식.
velog.io/@thisisyjin
$ yarn add styled-components
src/lib/styles 디렉터리를 생성하고, 그 안에 palette.js
파일을 작성.
// src: https://yeun.github.io/open-color
const palette = {
gray: [
'#f8f9fa',
'#f1f3f5',
'#e9ecef',
'#dee2e6',
'#ced4da',
'#adb5bd',
'#868e96',
'#495057',
'#343a40',
'#212529',
],
teal: [
'#e6fcf5',
'#c3fae8',
'#96f2d7',
'#63e6be',
'#38d9a9',
'#20c997',
'#12b886',
'#0ca678',
'#099268',
'#087f5b',
],
};
export default palette;
open-color 라이브러리를 직접 설치해서 사용해도 됨.
(나는 일부 색상만 사용할 것이고, 자동으로. import 되도록 하기 위해 파일을 생성했음.)
import styled from 'styled-components';
import palette from '../lib/styles/palette';
const StyledButton = styled.button`
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: 700;
padding: 0.5rem 1rem;
color: #fff;
outline: none;
cursor: pointer;
background: ${palette.gray[8]};
&:hover {
background: ${palette.gray[6]};
}
`;
const Button = (props) => {
return <StyledButton {...props} />;
};
export default Button;
StyledButton을 바로 export 해도 되지만, 추후 자동으로 import 할 수 없기 때문에
Button으로 한번 감싸주고 렌더링 해준 것.
import Button from '../components/Button';
const PostListPage = () => {
return (
<div>
<Button>버튼테스트</Button>
</div>
);
};
export default PostListPage;
index.css
를 수정함.* {
margin: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100%;
}
#root {
min-height: 100%;
}
html {
height: 100%;
}
/* reset CSS */
a {
color: inherit;
text-decoration: none;
}
code {
font-family: 'Courier New', Courier, monospace;
}
yarn add redux react-redux redux-actions immer redux-devtools-extension
❗️ 참고 -
redux-toolkit.js
을 사용하면immer
,redux
, redux-devtools-extension
이 내장되어 있다!
우선 프로젝트 진행한 후, 추후 수정과정에서 redux-toolkit으로 바꿔보자.
src/modules 디렉터리 생성 후, auth.js
모듈 생성.
import { createAction, handleActions } from 'redux-actions';
const SAMPLE_ACTION = 'auth/SAMPLE_ACTION';
export const sampleAction = createAction(SAMPLE_ACTION);
/* 🔻 redux-actions 미사용시
export const sampleAction = () => ({type: SAMPLE_ACTION});
*/
const initialState = {};
const auth = handleActions(
{
[SAMPLE_ACTION]: (state, action) => state,
},
initialState,
);
/* 🔻 redux-actions 미사용시
const auth = (state = initialState, action) => {
switch (action.type) {
case SAMPLE_ACTION:
return {
...state,
}
}
} */
export default auth;
1. createAction
필요한 추가 데이터 (type필드 외에)는
payload
라는 이름을 사용함.2. handleActions
첫번째 인자로는
{[type]: state 업데이트 함수}
형태의 객체를 넣어주고, (액션 개수대로)
두번째 인자로는 초기값인initialState
를 넣어준다.
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/client';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from './modules';
const store = createStore(rootReducer, composeWithDevTools());
// 추후 redux-toolkit의 configureStore로 바꿀 예정.
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
);
🔻 redux devtool의 state에 auth
가 있는지 확인.
-> 여기서 auth의 값 = state임. (지금은 initialState)
/components
에 작성하고,import styled from 'styled-components';
const AuthFormBlock = styled.div``;
const AuthForm = () => {
return <AuthFormBlock>AuthForm</AuthFormBlock>;
};
export default AuthForm;
import styled from 'styled-components';
const AuthTemplateBlock = styled.div``;
const AuthTemplate = () => {
return <AuthTemplateBlock></AuthTemplateBlock>;
};
export default AuthTemplate;
우선, 템플릿을 성한다.
import styled from 'styled-components';
const AuthTemplateBlock = styled.div``;
const AuthTemplate = () => {
return (
<AuthTemplateBlock>
</AuthTemplateBlock>
);
};
export default AuthTemplate;
snippet-generator.app에 접속해서 좌측에 코드를 붙여넣고,
AuthTemplate
을 모두 ${TM_FILENAME_BASE}
로 바꺼줌.
-> 확장자를 제외한 파일명.
코드를 복사하고, VSCODE 설정 > 사용자 코드 조각 메뉴를 클릭.
입력창에 javascriptreact
를 입력함.
그리고 아까 복사한 snippet을 붙여넣고 저장함. (주석 전부 지우고)
{
"Styled React Functional Component": {
"prefix": "srfc",
"body": [
"import styled from 'styled-components';",
"",
"const ${TM_FILENAME_BASE}Block = styled.div``;",
"",
"const ${TM_FILENAME_BASE} = () => {",
" return (",
" <${TM_FILENAME_BASE}Block>",
"",
" </${TM_FILENAME_BASE}Block>",
" );",
"};",
"",
"export default ${TM_FILENAME_BASE};",
""
],
"description": "Styled React Functional Component"
}
}
하단바에 JavaScript 라고 적힌 부분을 클릭하여 javaScriptReact
로 바꿔준 후,
srfc
를 입력하고 엔터를 누르면 위와 같이 스니펫을 사용 가능함.
나중에 제작할 컴포넌트임.
스니펫 테스트를 위해 생성했으므로, 다시 아까 생성한 AuthTemplate 컴포넌트로 가기.
import styled from 'styled-components';
const AuthTemplateBlock = styled.div``;
const AuthTemplate = ({ children }) => {
return <AuthTemplateBlock>{children}</AuthTemplateBlock>;
};
export default AuthTemplate;
import AuthForm from '../components/auth/AuthForm';
import AuthTemplate from '../components/auth/AuthTemplate';
const LoginPage = () => {
return (
<AuthTemplate>
<AuthForm />
</AuthTemplate>
);
};
export default LoginPage;
import AuthForm from '../components/auth/AuthForm';
import AuthTemplate from '../components/auth/AuthTemplate';
const RegisterPage = () => {
return (
<AuthTemplate>
<AuthForm />
</AuthTemplate>
);
};
export default RegisterPage;
-> AuthTemplate의 props.children이 AuthForm 이므로 렌더링됨.
import styled from 'styled-components';
import palette from '../../lib/styles/palette';
import { Link } from 'react-router-dom';
const AuthTemplateBlock = styled.div`
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: ${palette.gray[2]};
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`;
const WhiteBox = styled.div`
.logo-area {
display: block;
padding-bottom: 2rem;
text-align: center;
font-weight: 700;
letter-spacing: 0.25em;
}
box-shadow: 0 0 8px rgba(0, 0, 0, 0.025);
padding: 2rem;
width: 360px;
background: #fff;
border-radius: 4px;
`;
const AuthTemplate = ({ children }) => {
return (
<AuthTemplateBlock>
<WhiteBox>
<div className="logo-area">
<Link to="/">BLOGROW</Link>
</div>
{children}
</WhiteBox>
</AuthTemplateBlock>
);
};
export default AuthTemplate;
import styled from 'styled-components';
import palette from '../../lib/styles/palette';
import { Link } from 'react-router-dom';
import Button from '../common/Button';
const AuthFormBlock = styled.div`
h3 {
margin: 0;
color: ${palette.gray[8]};
margin-bottom: 2rem;
}
`;
const StyledInput = styled.input`
font-size: 1rem;
border: none;
border-bottom: 1px solid ${palette.gray[5]};
padding-bottom: 0.5rem;
outline: none;
width: 100%;
margin-bottom: 0.8rem;
&:focus {
color: ${palette.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 AuthForm = () => {
return (
<AuthFormBlock>
<h3>로그인</h3>
<form>
<StyledInput
autoComplete="username"
name="username"
placeholder="아이디"
/>
<StyledInput
type="password"
autoComplete="new-password"
name="password"
placeholder="비밀번호"
/>
<Button>로그인</Button>
</form>
<Footer>
<Link to="/register">회원가입</Link>
</Footer>
</AuthFormBlock>
);
};
export default AuthForm;
AuthFormBlock : 컨테이너. (h3 스타일링)
-> StyledInput : input
--> Footer : 회원가입 페이지로 이동하는 Link 존재. (/register)
Button 컴포넌트에 teal
과 fullWidth
라는 props를 줄때,
styled-components의 css() 함수
를 이용하여 조건부 스타일링을 적용하도록.
<Button teal fullWidth>로그인</Button>
common/Button.js 수정
import styled, { css } from 'styled-components';
import palette from '../../lib/styles/palette';
const StyledButton = styled.button`
...
${(props) =>
props.fullWidth &&
css`
padding-top: 0.75rem;
padding-bottom: 0.75rem;
width: 100%;
font-size: 1.2rem;
`}
${(props) =>
props.teal &&
css`
background: ${palette.teal[7]};
&:hover {
background: ${palette.teal[6]};
}
`}
`;
...
방법 1. 컴포넌트에 style props 주기
<Button teal fullWidth style={{marginTop: '1rem'}}>
방법 2. styled 함수로 새 컴포넌트로 정의.
import styled from 'styled-components';
import palette from '../../lib/styles/palette';
import { Link } from 'react-router-dom';
import Button from '../common/Button';
...
// 컴포넌트를 꾸밀때는 ()안에 넣어줘야함.
const ButtonWithMarginTop = styled(Button)`
margin-top: 1rem;
`;
const AuthForm = () => {
return (
<AuthFormBlock>
...
<ButtonWithMarginTop teal fullWidth>
로그인
</ButtonWithMarginTop>
...
</AuthFormBlock>
);
};
export default AuthForm;
-> props(teal, fullWidth)는 자동으로 Button 컴포넌트로 전달됨.
(styled.Button은 Button이기 때문)
AuthForm.js 수정
import styled from 'styled-components';
import palette from '../../lib/styles/palette';
import { Link } from 'react-router-dom';
import Button from '../common/Button';
const AuthFormBlock = styled.div`
h3 {
margin: 0;
color: ${palette.gray[8]};
margin-bottom: 2rem;
}
`;
const StyledInput = styled.input`
font-size: 1rem;
border: none;
border-bottom: 1px solid ${palette.gray[5]};
padding-bottom: 0.5rem;
outline: none;
width: 100%;
margin-bottom: 0.8rem;
&:focus {
color: ${palette.teal[7]};
border-bottom: 1px solid ${palette.gray[7]};
}
& + & {
margin-top: 1rem;
}
`;
const ButtonWithMarginTop = styled(Button)`
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 textMap = {
login: '로그인',
register: '회원가입',
};
const AuthForm = ({ type }) => {
const text = textMap[type];
return (
<AuthFormBlock>
<h3>{text}</h3>
<form>
<StyledInput
autoComplete="username"
name="username"
placeholder="아이디"
/>
<StyledInput
type="password"
autoComplete="new-password"
name="password"
placeholder="비밀번호"
/>
{type === 'register' && (
<StyledInput
autoComplete="new-password"
name="passwordConfirm"
placeholder="비밀번호 확인"
type="password"
/>
)}
<ButtonWithMarginTop teal fullWidth>
{text}
</ButtonWithMarginTop>
</form>
<Footer>
{type === 'login' ? (
<Link to="/register">회원가입</Link>
) : (
<Link to="/login">로그인</Link>
)}
</Footer>
</AuthFormBlock>
);
};
export default AuthForm;
AuthForm의 props로 type
값을 받고,
LoginPage에서의 AuthForm과 RegisterPage에서의 AuthForm의 UI를 다르게 구분함.
로그인 창 | 회원가입 창 |
---|---|
h3와 버튼의 텍스트가 '로그인' 임. 비밀번호 창만 존재. footer에 회원가입 Link. | h3와 버튼의 텍스트가 '회원가입' 임. 비밀번호 확인 창도 존재. footer에 로그인 Link. |
-> 삼항연산자 또는 단축평가(&&)로 type===register
인지 확인하기.
LoginPage.js
에서는 AuthForm에 props로 type='login'을 전달해줌.import AuthForm from '../components/auth/AuthForm';
import AuthTemplate from '../components/auth/AuthTemplate';
const LoginPage = () => {
return (
<AuthTemplate>
<AuthForm type="login" />
</AuthTemplate>
);
};
export default LoginPage;
RegisterPage.js
에서는 AuthForm에 props로 type='register'을 전달해줌.import AuthForm from '../components/auth/AuthForm';
import AuthTemplate from '../components/auth/AuthTemplate';
const RegisterPage = () => {
return (
<AuthTemplate>
<AuthForm type="register" />
</AuthTemplate>
);
};
export default RegisterPage;