Mern-Boiler-Plate

Minยท2020๋…„ 12์›” 30์ผ
1

Project

๋ชฉ๋ก ๋ณด๊ธฐ
1/6
post-thumbnail

Mern-Boiler-Plate

๐Ÿ“ ํ”„๋กœ์ ํŠธ ์„ค๋ช…

  • John Ahn๋‹˜์˜ ๊ธฐ๋ณธ ๊ฐ•์˜๋ฅผ ์ˆ˜๊ฐ•ํ•˜๋ฉฐ ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ, Auth, ๋กœ๊ทธ์•„์›ƒ ๊ธฐ๋Šฅ์„ ํ•™์Šตํ•œ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•œ ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค.

๐Ÿ’ก ๊ธฐ์ˆ ์Šคํƒ:

  • ํ”„๋ก ํŠธ์—”๋“œ : React, Redux, Ant Design
  • ๋ฐฑ์—”๋“œ: Express, MongoDB

๐Ÿ’ก ์‚ฌ์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

  • ํ”„๋ก ํŠธ์—”๋“œ : nodemon, concurrently, react-router-dom, axios, http-proxy-middleware, redux, react-redux, redux-promise, redux-thunk
  • ๋ฐฑ์—”๋“œ : express, mongoose, body-parser, jsonwebtoken, cookie-parser, body-parser

0. ์ดˆ๊ธฐ ์„ธํŒ…

๋ฆฌ์•กํŠธ ์„ค์น˜

$ npx create-react-app .

Nodemon

$ npm install nodemon --save-dev
package.json
script :"backend": "nodemon index,js"

Concurrently

$ 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

axios

$ npm install axios --save

CORS(Cross-Origin Resource Sharing)

: ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„๊ฐ€ ๋‘ ๊ฐœ์˜ ๋‹ค๋ฅธ ํฌํŠธ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋กœ 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,
    })
  )
}

Ant Design

: 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"

MongoDB

ํด๋Ÿฌ์Šคํ„ฐ, user ์ƒ์„ฑ

Mongoose ๋น„๋ฐ€๋ฒˆํ˜ธ ์ •๋ณด ๊ด€๋ฆฌ

โญ 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))

SSH๋ฅผ ํ†ตํ•œ GitHub ์—ฐ๊ฒฐ

.gitignore

node_modules
dev.js


1. ํšŒ์›๊ฐ€์ž…

ํšŒ์›๊ฐ€์ž…-๋ชจ๋ธ

Bctypt ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”

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()
  }
})

ํšŒ์›๊ฐ€์ž…-์„œ๋ฒ„

BodyParser

ํด๋ผ์ด์–ธํŠธ 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)

2. ๋กœ๊ทธ์ธ

  • ์š”์ฒญ๋œ ์ด๋ฉ”์ผ์„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์žˆ๋Š”์ง€ ์ฐพ์€ํ›„ ์žˆ๋‹ค๋ฉด ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋งž๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ์ธ์ง€ ํ™•์ธ ํ›„ ๋น„๋ฐ€๋ฒˆํ˜ธ๊นŒ์ง€ ๋งž๋‹ค๋ฉด ํ† ํฐ์„ ์ƒ์„ฑ

๋กœ๊ทธ์ธ-๋ชจ๋ธ

ํ† ํฐ ์ƒ์„ฑ

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

3. Auth

HOC(Higer-Order Component)

: function์ด๋ฉฐ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐ›์•„์„œ ์ƒˆ๋กœ์šด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฆฌํ„ดํ•˜๊ณ  ๋ฐฑ์—”๋“œ์— Request๋ฅผ ๋‚ ๋ ค์„œ ์ƒํƒœ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.

  • ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„์˜ Token์„ ๋น„๊ตํ•ด์„œ Auth๋ฅผ ๊ด€๋ฆฌ

SpecificComponent

: LandingPage

  • null : ์•„๋ฌด๋‚˜ ์ง„์ž… ๊ฐ€๋Šฅํ•œ ํŽ˜์ด์ง€ ex) Landing Page
  • true : ๋กœ๊ทธ์ธํ•œ ํšŒ์›๋งŒ ์ง„์ž… ๊ฐ€๋Šฅํ•œ ํŽ˜์ด์ง€ ex) Detail Page
  • false : ๋กœ๊ทธ์ธ ํ•œ ํšŒ์›์€ ์ง„์ž… ๋ชปํ•˜๋Š” ํŽ˜์ด์ง€ ex) Register, Login Page
  • ๊ด€๋ฆฌ์ž๋งŒ ์ง„์ž… ๊ฐ€๋Šฅํ•œ ํŽ˜์ด์ง€ ex) Admin Page

Auth-๋ชจ๋ธ

โญ 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)
    })
  })
}

Auth-์„œ๋ฒ„

โญ 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,
  })
})

Auth-๋ฆฌ๋•์Šค

โญ _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
    }
}

Auth-ํด๋ผ์ด์–ธํŠธ

โญ 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
}

Auth-๋ผ์šฐํŒ…

โญ 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

4. ๋กœ๊ทธ์•„์›ƒ

๋กœ๊ทธ์•„์›ƒ ํ•˜๋ ค๋Š” ์œ ์ €๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์ฐพ์•„์„œ ๊ทธ ์œ ์ €์˜ ํ† ํฐ์„ ์ง€์›Œ์ค€๋‹ค.

๋กœ๊ทธ์•„์›ƒ-์„œ๋ฒ„

โญ 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)
profile
slowly but surely

0๊ฐœ์˜ ๋Œ“๊ธ€