$ 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)