$ npx create-react-app .
$ npm install nodemon --save-dev
package.json
script :"backend": "nodemon index,js"
$ npm install concurrently --save
package.json
script : "dev": "concurrently \"npm run backend\" \"npm run start --prefix client\""
App.js
$ npm install react-router-dom --save
$ npm install axios --save
: ํด๋ผ์ด์ธํธ์ ์๋ฒ๊ฐ ๋ ๊ฐ์ ๋ค๋ฅธ ํฌํธ๋ฅผ ๊ฐ์ง๊ณ ์์ ๋ ๋ฐ์ํ๋ ๋ฌธ์ ๋ก Proxy๋ก ๋ฌธ์ ํด๊ฒฐ
$ npm intall http-proxy-middleware --save
โญ client/src/setupProxy
const { createProxyMiddleware } = require('http-proxy-middleware')
module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:5000',
changeOrigin: true,
})
)
}
: CSS ํ๋ ์์ํฌ
$ npm install antd --save
$ npm install redux react-redux redux-promise redux-thunk --save
โญ client/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { Provider } from 'react-redux'
import { applyMiddleware, createStore } from 'redux'
import promiseMiddleware from 'redux-promise'
import ReduxThunk from 'redux-thunk'
import Reducer from './_reducers'
const createStoreWithMiddleware =
applyMiddleware(promiseMiddleware, ReduxThunk)(createStore)
ReactDOM.render(
<Provider
store={createStoreWithMiddleware(Reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__() )}>
<App />
</Provider>
, document.getElementById('root'))
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister()
โญ _reducers/index.js
import { combineReducers } from 'redux'
import user from './user_reducer'
const rootReducer = combineReducers({
user // user_reducer์ ํจ์๋ฅผ ๋ฐ์ผ๋ฉฐ ๋ฆฌ๋์ค๋ฅผ ๋ถ๋ฆฌํด์ ์ธ๋ ์ฌ์ฉ
})
export default rootReducer
$ npm init -y
package.json
script : "start": "node index.js"
ํด๋ฌ์คํฐ, user ์์ฑ
โญ server/index.js
const config = require('./config/key')
mongoose.connect(config.mongoURI, ...)
โญ server/config/key.js
if (process.env.NODE_ENV === 'production') {
module.exports = require('./prod')
} else {
module.exports = require('./dev')
}
โญ server/config/prod.js
module.exports = {
// MONGO_URI๋ ๋ฐฐํฌ์ ์ด๋ฆ๊ณผ ๋์ผํ๊ฒ
mongoURI: process.env.MONGO_URI
}
โญ server/config/dev.js
module.exports = {
mongoURI: 'Mongo Connect URI'
}
mongoose ๋ผ์ด๋ธ๋ฌ๋ฆฌ, Model ์์ Schema ์์ฑ
$ npm install express mongoose --save
โญ server/model/User.js
const mongoose = require('mongoose')
const userSchema = mongoose.Schema({
name: {
type: String,
maxlength: 50,
},
email: {
type: String,
trim: true,
unique: 1,
},
password: {
type: String,
minlength: 5,
},
lastname: {
type: String,
maxlength: 50,
},
role: {
type: Number,
default: 0,
},
image: String,
token: {
type: String,
},
tokenExp: {
type: Number,
},
})
const User = mongoose.model('User', userSchema)
module.exports = { User }
โญ server/index.js
const mongoose = require('mongoose')
mongoose.connect(config.mongoURI, {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
useFindAndModify: false
}).then(() => console.log('MongoDB Connected !!'))
.catch(err => console.log(err))
node_modules
dev.js
DB์ ์ฅ์ ์ ์ ๋ฌ๋ฐ์ ๋น๋ฐ๋ฒํธ๋ฅผ Salt๋ฅผ ์ด์ฉํด ์ํธํ
saltRounds : Salt๊ฐ ๋ช ๊ธ์์ธ์ง
$ npm install bcrypt --save
โญ models/User.js
const bcrypt = require('bcrypt')
const saltRounds = 10
// DB์ ์ ์ฅํ๊ธฐ ์ ์ ๋ฌด์์ ํ๋ ๊ฒ (index.js)
// next()๋ฅผ ํด์ค์ผ index.js์ user.save() ๋ถ๋ถ์ด ์คํ๋๋ค.
userSchema.pre('save', function (next) {
// user์ userSchema๋ฅผ ๊ฐ๋ฆฌํค๊ณ ์๋ค.
// index.js์ const user = new User(req.body)
var user = this
// ๋น๋ฐ๋ฒํธ๋ฅผ ๋ฐ๊ฟ๋๋ง ์ํธํ
if (user.isModified('password')) {
// ๋น๋ฐ๋ฒํธ๋ฅผ ์ํธํ ์ํจ๋ค.
bcrypt.genSalt(saltRounds, function (err, salt) {
if (err) return next(err)
bcrypt.hash(user.password, salt, function (err, hash) {
if (err) return next(err)
user.password = hash
next()
})
})
} else {
next()
}
})
ํด๋ผ์ด์ธํธ POST request data์ body๋ก๋ถํฐ ํ๋ผ๋ฏธํฐ๋ฅผ ํธ๋ฆฌํ๊ฒ ์ถ์ถ
$ npm install body-parser --save
โญ server/index.js
const bodyParser = require('body-parser')
const { User } = require('./models/User')
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())
app.post('/api/users/register', (req, res) => {
const user = new User(req.body)
user.save((err, userInfo) => {
if (err) return res.json({ success: false, err})
return res.status(200).json({
success: true
})
})
})
โญ _actions/types.js
export const REGISTER_USER = 'register_user'
โญ _actions/user_action.js
import axios from 'axios'
import {
REGISTER_USER
} from './types'
export const registerUser = (dataToSubmit) => {
const request = axios.post('/api/users/register', dataToSubmit)
.then(response => response.data)
return {
type: REGISTER_USER,
payload: request
}
}
โญ _reducers/user_reducer.js
import {
REGISTER_USER
} from '../_actions/types'
export default (state={}, action) => {
switch (action.type) {
case REGISTER_USER:
return {...state, register: action.payload}
break
default:
return state
}
}
โญ RegisterPage.js
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
import { registerUser } from '../../../_actions/user_action'
import { withRouter } from 'react-router-dom'
import { Form, Input, Button} from 'antd'
const RegisterPage = (props) => {
const dispatch = useDispatch()
const [Email, setEmail] = useState('')
const [Name, setName] = useState('')
const [Password, setPassword] = useState('')
const [ConfirmPassword, setConfirmPassword] = useState('')
const onEmailHandler = (e) => {
setEmail(e.currentTarget.value)
}
const onNameHandler = (e) => {
setName(e.currentTarget.value)
}
const onPasswordHandler = (e) => {
setPassword(e.currentTarget.value)
}
const onConfirmPasswordHandler = (e) => {
setConfirmPassword(e.currentTarget.value)
}
const onSubmitHandler = (e) => {
e.preventDefault()
if (Password.length < 5) {
return alert('๋น๋ฐ๋ฒํธ๋ 5์๋ฆฌ ์ด์์ด์ฌ์ผ ํฉ๋๋ค.')
}
if (Password !== ConfirmPassword) {
return alert('๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์ต๋๋ค.')
}
let body = {
email: Email,
password: Password,
name: Name
}
dispatch(registerUser(body))
.then(res => {
if (res.payload.success) {
props.history.push('/login')
} else {
alert("ํ์๊ฐ์
์ ์คํจํ์์ต๋๋ค.")
}
})
}
return (
<div style={{
display: 'flex', justifyContent: 'center', alignItems: 'center',
width: '100%', height: '100vh' }}>
<Form style={{ display: 'flex', flexDirection: 'column' }}
onSubmit={onSubmitHandler}>
<label>E-mail</label>
<Input type="email" value={Email} onChange={onEmailHandler}/>
<label>Name</label>
<Input type="text" value={Name} onChange={onNameHandler}/>
<label>Password</label>
<Input type="password" value={Password} onChange={onPasswordHandler}/>
<label>Confirm Password</label>
<Input type="password" value={ConfirmPassword} onChange={onConfirmPasswordHandler}/>
<br/>
<Button type="submit" style={{ background: '#1890ff', color: '#fff'}}>
ํ์๊ฐ์
</Button>
</Form>
</div>
)
}
export default withRouter(RegisterPage)
$ npm install jsonwebtoken --save
โญ models/User.js
const jwt = require('jsonwebtoken')
// ํจ์ ๊ตฌํ
// 2. ์์ฒญ๋ ์ด๋ฉ์ผ์ด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์๋ค๋ฉด ๋น๋ฐ๋ฒํธ๊ฐ ๋ง๋ ๋น๋ฐ๋ฒํธ์ธ์ง ํ์ธ
userSchema.methods.comparePassword = function (plainPassword, cb) {
// plainPassword : 1234567
// ์ํธํ๋ ๋น๋ฐ๋ฒํธ : $2b$10$kqEZbclUfOIFSnkgUZsnxurUt3ugTNAeunLyC6IudjXu.1bGg0Osa
// ์ํธํ๋ ๋น๋ฐ๋ฒํธ๋ ๋ณตํธํ๊ฐ ๋์ง์์ plainPassword๋ฅผ ์ํธํํด์ ๋น๊ต
bcrypt.compare(plainPassword, this.password, function (err, isMatch) {
if (err) return cb(err)
cb(null, isMatch) // isMatch ์ ๋ณด๋ index.js์ comparePassword ํ๋ผ๋ฏธํฐ๋ก ๋ค์ด๊ฐ๋ค.
})
}
// 3. ๋น๋ฐ๋ฒํธ๊น์ง ๋ง๋ค๋ฉด ํ ํฐ์ ์์ฑํ๊ธฐ
userSchema.methods.generateToken = function (cb) {
// user์ userSchema๋ฅผ ๊ฐ๋ฆฌํค๊ณ ์๋ค.
// index.js์ const user = new User(req.body)
var user = this
// jsonwebtoken์ ์ด์ฉํด์ token์ ์์ฑํ๊ธฐ
// _id๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ id
// user._id + 'secretToken = token ------> 'secretToken'์ ๋ฃ์ผ๋ฉด user._id๊ฐ ๋์จ๋ค.
// user.id๋ plain object์ฌ์ผ ๋๊ธฐ ๋๋ฌธ์ toHexString
var token = jwt.sign(user._id.toHexString(), 'secretToken')
user.token = token
user.save(function (err, user) {
if (err) return cb(err)
cb(null, user) // user ์ ๋ณด๋ index.js์ getnerateToken ํ๋ผ๋ฏธํฐ๋ก ๋ค์ด๊ฐ๋ค.
})
}
$ npm install cookie-parser --save
โญ server/index.js
const cookieParser = require('cookie-parser')
app.use(cookieParser())
app.post('/api/users/login', (req, res) => {
// 1. ์์ฒญ๋ ์ด๋ฉ์ผ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์๋์ง ์ฐพ๋๋ค.
User.findOne({ email: req.body.email }, (err, user) => {
if (!user) {
return res.json({
loginSuccess: false,
message: '์ ๊ณต๋ ์ด๋ฉ์ผ์ ํด๋นํ๋ ์ ์ ๊ฐ ์์ต๋๋ค.'
})
}
// 2. ์์ฒญ๋ ์ด๋ฉ์ผ์ด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์๋ค๋ฉด ๋น๋ฐ๋ฒํธ๊ฐ ๋ง๋ ๋น๋ฐ๋ฒํธ์ธ์ง ํ์ธ
user.comparePassword(req.body.password, (err, isMatch) => {
if (!isMatch)
return res.json({ loginSuccess: false, message: '๋น๋ฐ๋ฒํธ๊ฐ ํ๋ ธ์ต๋๋ค.'})
// 3. ๋น๋ฐ๋ฒํธ๊น์ง ๋ง๋ค๋ฉด ํ ํฐ์ ์์ฑํ๊ธฐ
user.generateToken((err, user) => {
if (err) return res.status(400).send(err)
// (*์ฟ ํค*, ์ธ์
, ๋ก์ปฌ์คํ ๋ฆฌ์ง)์ ํ ํฐ์ ์ ์ฅํ๋ค.
res.cookie('x_auth', user.token)
.status(200)
.json({ loginSuccess: true, userId: user._id })
})
})
})
})
โญ _actions/types.js
export const LOGIN_USER = 'login_user'
โญ _actions/user_action.js
import axios from 'axios'
import {
LOGIN_USER
} from './types'
export const loginUser = (dataToSubmit) => {
const request = axios.post('/api/users/login', dataToSubmit)
.then(response => response.data)
return {
type: LOGIN_USER,
payload: request
}
}
โญ _reducers/user_reducer.js
import {
LOGIN_USER
} from '../_actions/types'
export default (state={}, action) => {
switch (action.type) {
case LOGIN_USER:
return {...state, loginSuccess: action.payload}
break
default:
return state
}
}
โญ LoginPage.js
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
import { loginUser } from '../../../_actions/user_action'
import { withRouter, Link } from 'react-router-dom'
import { Form, Input, Button} from 'antd'
const LoginPage = (props) => {
const dispatch = useDispatch()
const [Email, setEmail] = useState('')
const [Password, setPassword] = useState('')
const onEmailHandler = (e) => {
setEmail(e.currentTarget.value)
}
const onPasswordHandler = (e) => {
setPassword(e.currentTarget.value)
}
const onSubmitHandler = (e) => {
e.preventDefault()
let body = {
email: Email,
password: Password
}
dispatch(loginUser(body))
.then(res => {
if (res.payload.loginSuccess) {
props.history.push('/')
} else {
alert('Error')
}
})
}
return (
<div style={{
display: 'flex', justifyContent: 'center', alignItems: 'center',
flexDirection: 'column', width: '100%', height: '100vh' }}>
<Form style={{ display: 'flex', flexDirection: 'column' }}
onSubmit={onSubmitHandler} >
<label>E-mail</label>
<Input type='email' value={Email} onChange={onEmailHandler}/>
<label>Password</label>
<Input type='password' value={Password} onChange={onPasswordHandler}/>
<br/>
<Button type='submit' style={{ background: '#1890ff', color: '#fff'}}>
Login
</Button>
</Form>
<br/>
<Link to='/register' >
<Button style={{ display: 'block', background: '#1890ff', color: '#fff'}}>
Register
</Button>
</Link>
</div>
)
}
export default withRouter(LoginPage)
: function์ด๋ฉฐ ๋ค๋ฅธ ์ปดํฌ๋ํธ๋ฅผ ๋ฐ์์ ์๋ก์ด ์ปดํฌ๋ํธ๋ฅผ ๋ฆฌํดํ๊ณ ๋ฐฑ์๋์ Request๋ฅผ ๋ ๋ ค์ ์ํ๋ฅผ ๊ฐ์ ธ์จ๋ค.
ํด๋ผ์ด์ธํธ
์ ์๋ฒ
์ Token
์ ๋น๊ตํด์ Auth
๋ฅผ ๊ด๋ฆฌ: LandingPage
โญ models/User.js
userSchema.statics.findByToken = function (token, cb) {
// user์ userSchema๋ฅผ ๊ฐ๋ฆฌํค๊ณ ์๋ค.
// index.js์ const user = new User(req.body)
var user = this
// ํ ํฐ์ decode ํ๋ค.
// decoded = user id
jwt.verify(token, 'secretToken', function (err, decoded) {
// ์ ์ ์์ด๋๋ฅผ ์ด์ฉํด์ ์ ์ ๋ฅผ ์ฐพ์ ๋ค์์
// ํด๋ผ์ด์ธํธ์์ ๊ฐ์ ธ์จ token๊ณผ DB์ ๋ณด๊ด๋ ํ ํฐ์ด ์ผ์นํ๋์ง ํ์ธ
// findOne : ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ฐพ๋๋ค.
user.findOne({ _id: decoded, token: token }, function (err, user) {
if (err) return cb(err)
cb(null, user)
})
})
}
โญ server/middleware/auth.js
const { User } = require('../models/User')
let auth = (req, res, next) => {
// ์ธ์ฆ ์ฒ๋ฆฌ๋ฅผ ํ๋ ๊ณณ
// 1. ํด๋ผ์ด์ธํธ ์ฟ ํค์์ ํ ํฐ์ ๊ฐ์ ธ์จ๋ค. (Cookie-parser์ด์ฉ)
let token = req.cookies.x_auth
// ํ ํฐ์ ๋ณตํธํ(decode) ํํ ์ ์ (USER ID)๋ฅผ ์ฐพ๋๋ค.
User.findByToken(token, (err, user) => {
if (err) throw err
if (!user) return res.json({ isAuth: false, error: true })
// req์ token๊ณผ user๋ฅผ ๋ฃ์ด์ฃผ๋ ์ด์ ๋
// index.js์์ req ์ ๋ณด(token, user)๋ฅผ ๋ฐ์ ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅ
req.token = token
req.user = user
// next()๋ฅผ ์ฌ์ฉํ์ง ์์ผ๋ฉด ๋ฏธ๋ค์จ์ด ๊ฐํ๋ฒ๋ฆฐ๋ค.
next()
})
}
module.exports = { auth }
โญ server/index.js
// auth๋ผ๋ ๋ฏธ๋ค์จ์ด(auth.js)๋ req๋ฅผ ๋ฐ๊ณ ์ฝ๋ฐฑ function์ ํ๊ธฐ ์ ์ ์ด๋ค ์ผ์ ์ฒ๋ฆฌ
app.get('/api/users/auth', auth, (req, res) => {
// ์ฌ๊ธฐ๊น์ง ๋ฏธ๋ค์จ์ด๋ฅผ ํต๊ณผํด ์๋ค๋ ์๊ธฐ๋ Authentication์ด True
res.status(200).json({
// auth.js์์ user์ ๋ณด๋ฅผ ๋ฃ์๊ธฐ ๋๋ฌธ์ user._id๊ฐ ๊ฐ๋ฅ
_id: req.user._id,
// cf) role์ด 0 ์ด๋ฉด ์ผ๋ฐ์ ์ , role์ด ์๋๋ฉด ๊ด๋ฆฌ์
isAdmin: req.user.role === 0 ? false : true,
isAuth: true,
email: req.user.email,
name: req.user.name,
lastname: req.user.lastname,
role: req.user.role,
image: req.user.image,
})
})
โญ _actions/types.js
export const AUTH_USER = 'auth_user'
โญ _actions/user_action.js
import axios from 'axios'
import {
AUTH_USER
} from './types'
export const auth = () => {
// get์ด๋ผ body๋ถ๋ถ์ด ํ์๊ฐ ์๋ค.
const request = axios.get('/api/users/auth')
.then(response => response.data)
return {
type: AUTH_USER,
payload: request
}
}
โญ _reducers/user_reducer.js
import {
AUTH_USER
} from '../_actions/types'
export default (state={}, action) => {
switch (action.type) {
case AUTH_USER:
return {...state, userData: action.payload}
break
default:
return state
}
}
โญ src/hoc/auth.js
import React, { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { auth } from '../_actions/user_action'
export default function (SpecificComponent, option, adminRoute = null) {
const AuthenticationCheck = (props) => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(auth()).then(res => {
// ๋ก๊ทธ์ธ ํ์ง ์์ ์ํ
if (!res.payload.isAuth) {
if (option) {
props.history.push('/login')
}
} else {
// ๋ก๊ทธ์ธํ ์ํ
if (adminRoute && !res.payload.isAdmin) {
props.history.push('/')
} else {
if (option === false)
props.history.push('/')
}
}
})
}, [])
return (
<SpecificComponent />
)
}
return AuthenticationCheck
}
โญ App.js
import React from 'react'
import {
BrowserRouter as Router, Switch, Route
} from "react-router-dom"
import LandingPage from './components/views/LandingPage/LandingPage'
import LoginPage from './components/views/LoginPage/LoginPage'
import RegisterPage from './components/views/RegisterPage/RegisterPage'
import Auth from './hoc/auth'
const App = () => {
return (
<Router>
<div>
<Switch>
<Route exact path="/" component={Auth(LandingPage, null, true)} />
<Route exact path="/login" component={Auth(LoginPage, false)} />
<Route exact path="/register" component={Auth(RegisterPage, false)} />
</Switch>
</div>
</Router>
)
}
export default App
๋ก๊ทธ์์ ํ๋ ค๋ ์ ์ ๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ฐพ์์ ๊ทธ ์ ์ ์ ํ ํฐ์ ์ง์์ค๋ค.
โญ server/index.js
// auth๋ฅผ ๋ฃ๋ ์ด์ ๋ login์ด ๋์ด์๋ ์ํ์ด๊ธฐ ๋๋ฌธ์
app.get('/api/users/logout', auth, (req, res) => {
User.findOneAndUpdate({ _id: req.user._id }, { token: '' }, (err, user) => {
if (err) return res.json({ success: false, err })
return res.status(200).send({
success: true,
})
})
})
โญ LandingPage.js
import React from 'react'
import axios from 'axios'
import { withRouter, Link } from 'react-router-dom'
import { Button } from 'antd'
const LandingPage = (props) => {
const onClickHandler = () => {
axios.get('/api/users/logout')
.then(res => {
if (res.data.success) {
props.history.push('/login')
} else {
alert('๋ก๊ทธ์์ ํ๋๋ฐ ์คํจํ์ต๋๋ค.')
}
})
}
return (
<div style={{
display: 'flex', justifyContent: 'center', alignItems: 'center',
width: '100%', height: '100vh'
}}>
<Link to='/login'>
<Button style={{ background: '#1890ff', color: '#fff'}}>๋ก๊ทธ์ธ</Button>
</Link>
<Button style={{ background: '#1890ff', color: '#fff'}}
onClick={onClickHandler}>
๋ก๊ทธ์์
</Button>
</div>
)
}
export default withRouter(LandingPage)