함께 성장하는 프로독학러

Memo_app 03. 인증(authentication) - 회원가입 구현 본문

Programming/tutorials

Memo_app 03. 인증(authentication) - 회원가입 구현

프로독학러 2018. 6. 23. 15:31

안녕하세요, 프로독학러 입니다.


이번 포스팅에서는 인증의 기능 중 회원가입을 구현해 보도록 하겠습니다. 

(인증에는 회원가입, 로그인, 로그아웃, 로그인 확인의 4가지 기능이 있습니다)


*본 튜토리얼은 Velopert 님의 'React.js Codelab 2016' 을 기반으로 되었습니다.

(여러 모듈들의 버전업에 따라 작성방법이 조금씩 달라진 코드를 버전에 맞게 수정하고, 제가 튜토리얼을 따라함에 있어 이해가 쉽지 않았던 부분에 설명을 추가하는 방식으로 진행합니다.)

* Velopert 님의 원본 튜토리얼을 보고싶으신 분들은 아래의 링크를 참고해 주세요.

<React.js codelab 2016 - Velopert>


1. BACK-END 환경설정


기능을 구현하기에 앞서 BACK-END 에 필요한 파일을 먼저 생성해 보도록 하겠습니다.

account 라우터, api 루트 라우터, account 모델 총 세 가지 파일을 생성하도록 하겠습니다.


1-1) account 라우터


(./server/routes/account.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import express from 'express';
 
const router = express.Router();
 
router.post('/signup', (req, res) => {
    /* to be implemented */
    res.json({ success: true });
});
 
router.post('/signin', (req, res) => {
    /* to be implemented */
    res.json({ success: true });
});
 
router.get('/getinfo', (req, res) => {
    res.json({ info: null });
});
 
router.post('/logout', (req, res) => {
    return res.json({ success: true });
});
 
export default router;
 
cs


account 라우터는 BACK-END API 로, 데이터베이스와 통신해 결과를 리스폰스하는 역할을 합니다.

기능의 구현은 차차 하기로 하고 우선은 틀만 갖추어 두었습니다.


1-2) api 루트 라우터 생성


어떤 디렉토리에서 index.js 파일은 해당 디렉토리를 대표하는 역할을 합니다.

즉, 디렉토리 경로까지 import 하면 자동으로 index.js 파일이 import 되는것 입니다.


(./server/routes/index.js)

1
2
3
4
5
6
7
import express from 'express';
import account from './account';
 
const router = express.Router();
router.use('/account', account);
 
export default router;
cs


5번째 줄에서 /account 로 들어오는 접근을 account(1-1에서 생성한)로 위임했습니다.

추후에 memo 라우터를 생성하게되면 memo 라우터도 임포트 시켜줄 예정입니다.


1-3) api 라우터 불러와서 사용


1-2 에서 생성한 루트 라우터를 server/main.js 에서 사용하는 코드입니다.


(./server/main.js)

1
2
3
4
...
import api from './routes';
app.use('/api', api);
...
cs


위의 코드를 통해 http://URL/api/account/signup 의 경로로 접근할 수 있게 되었습니다.


1-4) mongoose 를 통한 account 모델링


(./server/models/account.js)

1
2
3
4
5
6
7
8
9
10
11
12
import mongoose from 'mongoose';
 
const Schema = mongoose.Schema;
 
const Account = new Schema({
    username: String,
    password: String,
    salt: String,
    created: { type: DatedefaultDate.now }
});
 
export default mongoose.model('account', Account);
cs


account Schema 를 통해 도큐먼트의 형태를 만들고, model 로 export 합니다.

export 한 model 을 통해서 데이터베이스에 접근할 수 있습니다.

* mongoose 에 익숙하지 않으신 분들은 아래의 링크를 참조해 주세요.

<mongoose 사용법>


(./routes/account.js)

1
2
3
...
import Account from '../models/account';
...
cs


account 라우터에서 model 을 사용하기 위해 위의 코드를 추가합니다.

이제부터 Account 를 통해 DB 에 접근할 수 있습니다.


1-5) pbkdf2-password 설치


npm install --save pbkdf2-password


비밀번호 보안 모듈인 pbkdf2-password 를 설치합니다.

* pbkdf2-password 의 사용법에 익숙치 않으신 분들은 아래의 링크를 참고해 주세요.

<비밀번호 보안 모듈 - pbkdf2-password >


모듈의 사용은 API 안에서 하도록 하겠습니다. (회원가입, 로그인 API 내에서 사용)

API 에서 사용하도록 import 해줍니다.


(./routes/account.js)

1
2
3
4
...
import bkfd2Password from 'pbkdf2-password';
const hasher = bkfd2Password();
...
cs


2. FRONT-END 환경설정


클라이언트 사이드에서 필요한 모듈과 설정을 해 보도록 하겠습니다.


2-1) webpack 에 resolve 설정 추가하기


(webpack.config.js & webpack.dev.config.js)

1
2
3
4
5
6
import path from 'path';
...
resolve: {
        modules: [path.resolve(__dirname, "src"), "node_modules"]
    },
...
cs


webpack.config.js 와 webpack.dev.config.js 에 resolve 필드에 modules 속성을 추가합니다.

위의 코드를 통해 src 디렉토리를 루트 디렉토리로 인식시킬 수 있습니다.

(./components 혹은 ../components 와 같은 상대적 경로가 아닌 components 와 같이 절대적 경로로 디렉토리에 접근할 수 있습니다)

* 이전 코드에서는 resolve.root 속성이었지만 버전업되면서 resolve.root 속성이 사라졌습니다.


2-2) 모듈 설치 및 적용


클라이언트 사이드에서 필요한 모듈들을 설치합니다.


npm install --save axios react-router-dom react-timeago redux react-redux redux-thunk


axios : HTTP 요청을 할 수 있도록 도와주는 모듈

react-router-dom : 클라이언트 사이드 라우터 (* 이전에는 react-router 로 설치했지만 버전업 되면서 브라우저용은 react-router-dom 으로 설치)

react-timeago : 시간을 얼마나 지났는지 계산해서 나타내는 React 컴포넌트 (3 seconds ago 와 같이)

redux, react-redux : 전역 상태관리를 위한 라이브러리 (FLUX), 뷰 레이어 바인딩

redux-thunk : dispatch 함수에서 결과에 따라 (비동기) 다른 dispatch 함수를 리듀서에 전달하도록 하는 미들웨어


2-3) webpack css-loader 와 style-loader 설치 및 적용


npm install --save-dev style-loader css-loader


위의 로더들을 통하여 프로젝트안에서 css 파일을 import 하여 사용할 수 있습니다.


(webpack.config.js & webpack.dev.config.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
...
    module: {
      rules: [
        {
          ...
        },
        {
          test: /\.css$/,
          use: [ 'style-loader''css-loader' ]
        }
      ]
    },
...
cs


module.rules 에 loader 규칙을 추가해 주세요.

* webpack 설정파일을 수정하면 서버를 재시작해야 적용됩니다.


src/style.css 파일을 생성해 봅시다.

(배경색을 회색으로 하는 스타일입니다)


(./src/style.css)

1
2
3
body {
    background-color: #ECEFF1;
}
cs


webpack 설정파일의 entry 에 style.css 를 추가해주도록 합시다.


(webpack.config.js & webpack.dev.config.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//webpack.config.js
...,
entry: [
    'react-hot-loader/patch',
    './src/index.js',
    './src/style.css'
  ],
...
 
//webpack.dev.config.js
...,
entry: [
        './src/index.js',
        'webpack-dev-server/client?http://0.0.0.0:4000'
        'webpack/hot/only-dev-server',
        './src/style.css'
    ],
...
cs


style 로더를 통해 적용한 CSS 스타일이 제대로 작동하는지 확인해 보도록 하겠습니다.


서버를 종료하고 다시 실행해봅시다. (npm run win_development)

* MongoDB 서버도 켜져 있어야합니다.



배경색이 설정한대로 변경된 것을 알 수 있습니다.


2-4) Materializecss


프로젝트를 예쁘게 만들기 위한 CSS 프레임워크입니다.

* 자세한 사용법은 홈페이지를 참조하세요.

<Materializecss 홈페이지>


index.html 에 관련 파일을 CDN 형식으로 불러와주세요.


(./dist/index.html)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>
 
   <head>
      <meta charset="UTF-8">
      <!--Import Google Icon Font-->
      <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
      <!--Import materialize.css-->
      <link type="text/css" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.6/css/materialize.min.css"  media="screen,projection"/ />
      <!--Let browser know website is optimized for mobile-->
      <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
 
      <title>MEMOPAD</title>
   </head>
 
   <body>
      <div id="root"></div>
      <script type="text/javascript" src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
      <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.6/js/materialize.min.js"></script>
      <script src="/bundle.js"></script>
   </body>
 
</html>
cs


Materializecss 는 jQuery 도 필요하므로 같이 불러왔습니다.


3. 회원가입


위의 과정을 통해 이제 정말 본격적으로 프로젝트를 시작할 수 있게 되었습니다.

그럼 시작해 보도록하겠습니다!


가장 먼저 구현할 기능은 '회원가입'입니다.


회원가입은 어떤 뷰를 가지고 있어야하며, 어떤 방식으로 작동해야 할까요?

이를 그림으로 나타내면 다음과 같습니다.



복잡해 보이지만, 천천히 살펴보도록 하겠습니다.


가장 먼저 BACK-END 의 API 를 살펴보도록 하겠습니다.

API 의 역할은 req.body 로 들어온 데이터를 DB에 조회해 입력받은 아이디와 비밀번호가 유효한지 검사하는 역할을 수행합니다.

(검사 결과에 따라 다른 값을 res.body 로 전달합니다.)

두 번째로 살펴볼 것은 FRONT-END 의 Redux 입니다.

리덕스는 액션타입을 가진 액션 객체를 dispatch 함수로 전달받아 액션타입에 따라 리듀서가 state를 변경합니다.

이 때, thunk 라는 미들웨어를 사용하는데, 이 thunk 함수는 axios 를 이용하여 API 와 통신을 주고받고, 그 결과에 따라 다른 dispatch 함수를 실행합니다. 

세 번째로 살펴볼 것은 FRONT-END 의 컨테이너 컴포넌트인 Register 입니다.

Register 는 리덕스와 연결되어 thunk 함수와 리덕스 state를 사용할 수 있습니다.

그리고 내부적으로 thunk 함수를 사용하는 메소드를 정의하여 Authentication 컴포넌트로 전달합니다.

네 번째로 살펴볼 것은 FRONT-END 의 Authentication 컴포넌트입니다.

이 컴포넌트는 회원가입 ,로그인 뷰를 렌더링하는 컴포넌트입니다.

또한 username 과 password 를 결정하여 thunk 함수를 실행시킵니다.


데이터가 흐르는 과정은 위의 그림의 숫자와 관련이 있습니다.


1. Redux 에서 정의한 thunk 함수와 state 값을 컨테이너 컴포넌트(Register)에 전달(연결) 합니다.

2. Register 컴포넌트 내에서 thunk 함수를 실행하는 메소드를 정의하여 Authentication 컴포넌트로 전달합니다.

3. Authentication 컴포넌트에서 username 값과 password 값을 결정하여 실행합니다.

4. API 로 데이터를 전달합니다.

5. API 에서 DB 의 데이터에 접근하여 오류가 있는지 확인하고 결과에 따라 다른 JSON 파일을 response 합니다.

6. 5로 부터 전달받은 데이터를 통해 리덕스 state 를 변경하고, 변경된 값을 Register 컨테이너 컴포넌트에 전달합니다.


3-1) BACK-END API 구현


그럼 API 부터 구현해 보도록하겠습니다.


(./server/routes/account.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/*
    ACCOUNT SIGNUP: POST /api/account/signup
    BODY SAMPLE: { "username": "test", "password": "test" }
    ERROR CODES:
        1: BAD USERNAME
        2: BAD PASSWORD
        3: USERNAM EXISTS
*/
router.post('/signup', (req, res) => {
    // CHECK USERNAME FORMAT
    // 유저네임으로 사용할 수 있는 문자는 영어와 숫자 뿐
    let usernameRegex = /^[a-z0-9]+$/;
 
    if(!usernameRegex.test(req.body.username)) {
        return res.status(400).json({ // HTTP 요청에 대한 리스폰스 (json 형식으로)
            error: "BAD USERNAME",
            code: 1
        });
    }
 
    // CHECK PASS LENGTH
    // 비밀번호 유형 검사 (4보다 작거나, 들어온 비밀번호의 값이 문자열이 아닐 경우)
    if(req.body.password.length < 4 || typeof req.body.password !== "string") {
        return res.status(400).json({
            error: "BAD PASSWORD",
            code: 2
        });
    }
 
    // CHECK USER EXISTANCE
    // 기존에 존재하는 username 이 있는지 DB 에서 확인
    Account.findOne({ username: req.body.username }, (err, exists) => { //Model.findOne 메소드
        if (err) throw err;
        if(exists){
            return res.status(409).json({
                error: "USERNAME EXISTS",
                code: 3
            });
        }
 
        // CREATE ACCOUNT
        // 위의 코드 1~3 의 결격 사항이 없을 경우 db에 저장
        // hasher 를 이용해 비밀번호 보안
        hasher({password:req.body.password}, function(err, pass, salt, hash){
          let account = new Account({
              username: req.body.username,
              password: hash,
              salt: salt
          });
          account.save( err => {
              if(err) throw err;
              return res.json({ success: true });
          });
        });
    });
});
cs


router.post('/signup', ...) 부분의 코드를 위와 같이 수정합니다.

해당 라우터는 POST 방식으로 '/api/account/signup' 로 들어오는 접근에 걸리는 라우터로, req.body 로 들어오는 데이터는 

{ "username": "...", "password": "..." }

의 형식입니다.


코드의 12 번째 줄에서 정규표현식 패턴을 만들었습니다. 해당 패턴은 시작부분에서 소문자 a-z, 숫자 0-9 까지의 문자만을 의미합니다.

14 번째 줄에서 req.body.username 을 위에서 만든 정규표현식 패턴으로 test 했습니다.

test 메소드는 해당 표현식에 해당하는 문자가 있으면 true 를, 없으면 false 를 반환하므로 리턴값이 false 이면 400 status 와 함께 에러객체를 리스폰스합니다.

* HTTP status 400은 잘못된 요청이라는 의미입니다. (서버가 요청의 구문을 인식하지 못했다)


코드의 23 번째 줄에서는 비밀번호의 유효성을 검사합니다.

비밀번호의 길이가 4보다 작거나 들어온 비밀번호의 값이 문자열이 아닌 데이터 형식일 때 에러객체를 리스폰스합니다.


코드의 32 번째 줄은 입력(요청)한 username 값이 DB 안에 존재하는지 확인합니다.

확인하여 이미 존재한다면 409 status 와 함께 에러객체를 리스폰스합니다.

* HTTP status 409는 충돌을 의미합니다. (요청을 수행하는 중 충돌이 발생했다)


코드의 44 번째 줄은 hasher 함수를 이용하여 salt, hash 값을 생성하고 새 모델을 만들어 DB에 저장하는 코드입니다.

DB 에 저장이 정상적으로 완료되면 { "success": ture } 를 리스폰스합니다.

* pbkdf2-password 의 사용법에 익숙하지 않으신 분들은 아래의 링크를 참조해 주세요.

<pbkdf2-password 보안 모듈>


3-2) Redux 구현


두 번째로 구현해 볼 것은 FRONT-END 의 Redux 입니다.


Redux 에서는 ActionType 과 actionc 생성자함수, 리듀서를 구현하고 리덕스를 사용하기 위한 설정을 진행합니다.


3-2-1) ActionTypes.js


ActionTypes 는 action 객체의 타입(type) 값을 의미하며, 문자열을 리턴합니다.


(./src/actions/ActionTypes.js)

1
2
3
4
5
/* AUTHENTICATION */
// Register
export const AUTH_REGISTER = "AUTH_REGISTER";
export const AUTH_REGISTER_SUCCESS = "AUTH_REGISTER_SUCCESS";
export const AUTH_REGISTER_FAILURE = "AUTH_REGISTER_FAILURE";
cs


3-2-2) authentication.js 


액션생성자 함수와 thunk 를 정의하는 파일입니다.


(./src/actions/authentication.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import axios from 'axios';
import {
    AUTH_REGISTER,
    AUTH_REGISTER_SUCCESS,
    AUTH_REGISTER_FAILURE
} from './ActionTypes';
 
/* REGISTER */
export function registerRequest(username, password) {
    return (dispatch) => {
        // Inform Register API is starting
        dispatch(register());
 
        return axios.post('/api/account/signup', { username, password })
        .then((response) => {
            dispatch(registerSuccess());
        }).catch((error) => {
            dispatch(registerFailure(error.response.data.code));
        });
    };
}
 
export function register() {
    return {
        type: AUTH_REGISTER
    };
}
 
export function registerSuccess() {
    return {
        type: AUTH_REGISTER_SUCCESS,
    };
}
 
export function registerFailure(error) {
    return {
        type: AUTH_REGISTER_FAILURE,
        error
    };
}
cs


코드의 23 번째 줄은 액션생성자 함수로 액션객체를 리턴합니다. 

action 객체의 type 값은 "AUTH_REGISTER" 입니다.


29 번째 줄 역시 액션생성자로, action.type 값이 "AUTH_REGISTER_SUCCESS"인 객체를 리턴합니다.


35 번째 줄은 action.type 값이 "AUTH_REGISTER_FAILURE"인 객체를 리턴하는 액션생성자입니다.


코드의 9번째 줄은 thunk 입니다. 

registerRequest 함수가 실행되면 먼저 action.type 이 "AUTH_REGISTER" 인 액션객체를 리듀서로 보냅니다. (12 번째 줄) 이는 회원가입 요청을 시작했다는 의미입니다.

그 뒤에 axios 를 이용하여 3-1) 에서 구현한 API 와 통신합니다. req.body 로 전달하는 값은 { username: "...", password: "..." } 형태의 JSON 객체입니다. 통신이 끝나면 API 에서는 JSON 데이터를 리스폰스(리턴) 하는데, 성공하면 { "success": ture } 를, 실패하면 에러객체를 리턴합니다.

통신이 끝나면 API 에서 리스폰스하는 데이터에 따라 action.type 값이 "AUTH_REGISTER_SUCCESS" 인 객체를 리듀서로 보내거나(성공), action.type 값이 "AUTH_REGISTER_FAILURE" 인 객체를 리듀서로 보냅니다.


3-2-3) reducer (authentication.js) 


리듀서는 dispatch 함수로 부터 전달받은 action 객체의 type 값에 따라 state 를 변경하는 함수입니다.


(./src/reducers/authentication.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import * as types from 'actions/ActionTypes';
 
const initialState = {
    login: {
        status: 'INIT'
    },
    register: {
        status: 'INIT',
        error: -1
    },
    status: {
        valid: false,
        isLoggedIn: false,
        currentUser: ''
    }
};
 
export default function authentication(state = initialState, action) {
  switch(action.type) {
    case types.AUTH_REGISTER:
      return {
        ...state,
        register: {
          status: 'WAITING',
          error: -1
        }
      }
    case types.AUTH_REGISTER_SUCCESS:
      return {
        ...state,
        register: {
          ...state.register,
          status: 'SUCCESS'
        }
      }
    case types.AUTH_REGISTER_FAILURE:
      return {
        ...state,
        register:{
          status: 'FAILURE',
          error: action.error
        }
      }
    default:
      return state;
  }
};
cs


리듀서 함수에서는 state 를 변경합니다. 이 state 는 state.authentication 으로 접근할 수 있으며, 해당 state 를 사용하고자하는 컴포넌트에 connect 하여 사용합니다. (Register 컨테이너에서 연결하여 사용할 예정)


코드의 세 번째 줄에서 initialState 를 정의했습니다. 그리고 18번째 줄에서 리듀서 함수를 사용할 때 ES6 의 default parameter 를 사용하여 state 값에 아무것도 들어오지 않았을 때 initialState 를 사용하도록 설정하였습니다.

(리듀서함수는 이전상태(prevState)와 action 객체를 인자로 받음)

(현재 initialState 는 register 이외에 다른 기능에 필요한 값들도 들어가 있는 상태입니다)

* ES6 의 default parameter 가 익숙하지 않으신 분들은 아래의 링크를 참조해 주세요.

<ES6, default parameter>


리듀서 함수는 switch 조건문으로 작성되었습니다. 인자로 들어온 action 객체의 type 값에 따라 state 를 다르게 변경합니다.

* JS 의 switch 조건문에 익숙하지 않으신 분들은 아래의 링크를 참조해 주세요.

<javascript switch 조건문>


action.type 값에 따라 state 를 변경할 떄, 원래 state 를 손상시키지 않고 새로운 state 를 리턴해야하기 때문에 ES6의 spread operator 가 사용되었습니다.

* ES6 의 spread operator 에 익숙하지 않으신 분들은 아래의 링크를 참조해 주세요.

<ES6, spread operator>


3-2-4) reduers 디렉토리의 index


combineReducers 를 이용해 여러개의 리듀서를 합치는 역할을 하는 파일입니다.


(./src/reducers/index.js)

1
2
3
4
5
6
7
import authentication from './authentication';
 
import { combineReducers } from 'redux';
 
export default combineReducers({
    authentication
});
cs


코드를 작성하면서 authentication 이외의 리듀서도 index.js 에 import 할 예정입니다.


3-2-5) 리덕스 사용 설정 - Provider 컴포넌트


리덕스를 사용하기 위해 src/index.js 에서 렌더링할 컴포넌트를 Provider 컴포넌트로 감싸주어야합니다.

추가적으로 Provider 의 store 속성에는 리듀서를 활용하여 만든 리덕스 스토어를 연결해줍니다.

(리듀서와 사용할 미들웨어-thunk 를 이용하여 리덕스 스토어 생성)


(./src/index.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from 'react';
import ReactDOM from 'react-dom';
 
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import reducers from 'reducers';
import thunk from 'redux-thunk';
 
const store = createStore(reducers, applyMiddleware(thunk));
 
const title = 'Memo_App';
 
ReactDOM.render(
  <Provider store={store}>
      <div>{title}</div>
  </Provider>
  ,
  document.getElementById('root')
);
 
module.hot.accept();
cs



3-3) Register 컴포넌트 구현 (컨테이너 컴포넌트)


이제 컨테이너 컴포넌트를 구현해 보도록 하겠습니다.

컨테이너 컴포넌트는 클라이언트 사이드 라우터에 따라 렌더링 되는 컴포넌트입니다.


어떤 뷰가 보여야하는지 먼저 완성본을 통해서 살펴보겠습니다.



위 그림과 같이 username 과 password 를 입력하는 창이 있고 CREATE 버튼이 있습니다.

이는 회원가입 창이지만 로그인 화면의 구성도 위와 비슷할 것입니다. (input 태그와 전송버튼으로 이루어진)

따라서 비슷한 레이아웃인 인풋창과 버튼이 포함된 부분을 Authentication 컴포넌트로 처리하고, Register 컴포넌트나 Login 컴포넌트로 Authentication 컴포넌트를 렌더링하겠습니다.


먼저 containers 디렉토리에 Register.js 파일을 생성하도록 하겠습니다.


(./src/containers/Register.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Component } from 'react';
 
class Register extends Component {
    render() {
        return (
            <div>
                register
            </div>
        );
    }
}
 
export default Register;
cs


containers 디렉토리의 index 도 생성해 주도록 합시다.


(./src/containers/index.js)

1
2
3
import Register from './Register';
 
export { Register };
cs


Register 컨테이너 컴포넌트를 클라이언트사이드 라우팅을 통해 '/register' 경로로 들어왔을 때 렌더링하도록 설정하겠습니다.


(./src/index.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import { Register } from 'containers'
 
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import reducers from 'reducers';
import thunk from 'redux-thunk';
 
const store = createStore(reducers, applyMiddleware(thunk));
 
const title = 'Memo_App!';
 
ReactDOM.render(
  <Provider store={store}>
    <Router>
      <div>
        <Route path="/register" component={Register}/>
      </div>
    </Router>
  </Provider>
  ,
  document.getElementById('root')
);
 
module.hot.accept();
cs


코드의 19번째 줄에서 '/register' 경로(url)로 접근했을 때 Register 컴포넌트가 렌더링 되도록 설정했습니다.

* 클라이언트 사이드 라우터인 react-router-dom 에 대해서 익숙치 않으신 분들은 아래 링크로 첨부된 카테고리의 글을 참고해주세요.

<react-router_v4 강좌>


3-4) Authentication 컴포넌트 구현


Register 컨테이너 컴포넌트에서 렌더링할 Authentication 컴포넌트를 만들어 보도록 하겠습니다.

Authentication 컴포넌트에서는 Materializecss 를 이용하여 view 를 만들어 렌더링 할 것입니다.


(./src/components/Authentication.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
 
class Authentication extends Component {
    render() {
        const inputBoxes = (
            <div>
                <div className="input-field col s12 username">
                    <label>Username</label>
                    <input
                    name="username"
                    type="text"
                    className="validate"/>
                </div>
                <div className="input-field col s12">
                    <label>Password</label>
                    <input
                    name="password"
                    type="password"
                    className="validate"/>
                </div>
            </div>
        );
 
        const loginView = (
            <div>
                <div className="card-content">
                    <div className="row">
                        {inputBoxes}
                        <a className="waves-effect waves-light btn">SUBMIT</a>
                    </div>
                </div>
 
 
                <div className="footer">
                    <div className="card-content">
                        <div className="right" >
                        New Here? <Link to="/register">Create an account</Link>
                        </div>
                    </div>
                </div>
 
            </div>
        );
 
        const registerView = (
            <div className="card-content">
                <div className="row">
                    {inputBoxes}
                    <a className="waves-effect waves-light btn">CREATE</a>
                </div>
            </div>
        );
        return (
          <div className="container auth">
              <Link className="logo" to="/">MEMOPAD</Link>
              <div className="card">
                  <div className="header blue white-text center">
                      <div className="card-content">{this.props.mode ? "LOGIN" : "REGISTER"}</div>
                  </div>
                  {this.props.mode ? loginView : registerView }
              </div>
          </div>
        );
    }
}
 
Authentication.propTypes = {
    mode: PropTypes.bool
};
 
Authentication.defaultProps = {
    mode: true
};
 
export default Authentication;
cs


Authentication 컴포넌트는 로그인 뷰와 회원가입 뷰를 모두 가진 컴포넌트로, 렌더링하는 컴폰넌트로부터 전달받는 mode props 를 통해 어떤 뷰를 보여줄지 결정합니다. 전달받는 mode props 가 true 라면 로그인 뷰를 렌더링하고, false 라면 회원가입 뷰를 렌더링합니다.

(코드의 62 번째 줄)


* input 창들은 로그인 뷰와 회원가입 뷰 모두 같기 때문에 inputBoxes 로 분리하여 로그인, 회원가입 뷰에서 사용하였습니다.

(코드의 7~24 번째 줄)

* Link 컴포넌트는 SPA 에서 사용되는 컴포넌트로, url을 이동할 때 전체페이지를 새로고침하지 않고 렌더링할 대상만 바꾸는 컴포넌트 입니다.


components 디렉토리의 index 를 생성해 줍시다.


(./src/components/index.js)

1
2
3
import Authentication from './Authentication';
 
export { Authentication };
cs


3-5) Register 컨테이너 컴포넌트에서 Authentication 컴포넌트 렌더링


'/register' url 경로로 접근하면 렌더링되는 컨테이너 컴포넌트인 Register 에 Authentication 컴포넌트를 렌더링하겠습니다.

이 때, Authentication 에 mode props 를 false 로 전달해야 회원가입 뷰가 렌더링 됩니다.

(Authenticaction 컴포넌트에서 그렇게 정함)


(./src/containers/Register.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { Component } from 'react';
import { Authentication } from 'components';
 
class Register extends Component {
    render() {
        return (
            <div>
                <Authentication mode={false}/>
            </div>
        );
    }
}
 
export default Register;
cs


추가적으로 Authentication 컴포넌트를 위한 css 스타일도 추가해 주도록 하겠습니다.


(./src/style.css)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/* Authentication */
.auth {
  margin-top: 50px;
    text-align: center;
}
.logo {
    text-align: center;
    font-weight: 100;
    font-size: 80px;
    -webkit-user-select: none;
    /* Chrome all / Safari all */
    -moz-user-select: none;
    /* Firefox all */
    -ms-user-select: none;
    /* IE 10+ */
    user-select: none;
    /* Likely future */
}
a.logo {
    color: #5B5B5B;
}
{
    cursor: pointer;
}
.auth .card {
    width: 400px;
    margin: 0 auto;
}
@media screen and (max-width: 480px) {
  .auth .card {
    width: 100%;
  }
  .logo {
    font-size: 60px;
  }
}
.auth .header {
    font-size: 18px;
}
.auth .row {
    margin-bottom: 0px;
}
.auth .username {
  margin-top: 0px;
}
.auth .btn {
    width: 90%;
}
.auth .footer {
    border-top: 1px solid #E9E9E9;
    padding-bottom: 21px;
}
cs


개발서버를 실행시켜(프로젝트 경로에서 npm run win_development 명령어를 통해) 제대로 작동하는지 확인해 보겠습니다.

* 개발서버를 실행할 때 MongoDB 서버도 열려있어야 합니다.



위와 같이 /register 경로로 접근했을 때 Register 컴포넌트가 제대로 렌더링 되는것을 알 수 있습니다.

('/' 경로의 클라이언트 사이드 라우팅은 아직 존재하지 않으므로 localhost:4000 으로 접속했을때 배경색만 보이는것은 정상입니다)


3-6) Register 컨테이너 컴포넌트에 Redux 연결


위의 과정까지 잘 따라왔으면, 이제 데이터들을 주고받는 작업을 할 준비가 완료되었습니다.

가장 첫 번째로 데이터를 넘겨주는 것은 Redux state 와 thunk 함수를 컨테이너 컴포넌트에 넘겨주는 것입니다.

이는 connect 를 통해 이루어집니다.


(./src/containers/Register.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React, { Component } from 'react';
import { Authentication } from 'components';
import { connect } from 'react-redux';
import { registerRequest } from 'actions/authentication';
 
class Register extends Component {
    render() {
        return (
            <div>
                <Authentication mode={false}/>
            </div>
        );
    }
}
 
const mapStateToProps = (state) => {
    return {
        status: state.authentication.register.status,
        errorCode: state.authentication.register.error
    };
};
 
const mapDispatchToProps = (dispatch) => {
    return {
        registerRequest: (id, pw) => {
            return dispatch(registerRequest(id, pw));
        }
    };
};
 
export default connect(mapStateToProps, mapDispatchToProps)(Register);
 
cs


react-redux 의 connect 를 이용하여 Register 컴포넌트(컨테이너)와 리덕스를 연결합니다.

이 때, mapStateToProps 와 mapDispatchToProps 를 통하여 리덕스 state 와 thunk 함수를 Register 컴포넌트로 들어온 프롭스처럼 사용할 수 있습니다.

(Register 컴포넌트 내에서 this.state.status / this.props.registerRequest 와 같이)


3-7) Register 컨테이너 컴포넌트에서 thunk 실행 메소드 정의, Authentication 컴포넌트로 전달


Redux 연결을 통해 Register 컴포넌트에서 thunk 를 프롭스처럼 사용할 수 있게 되었습니다. (this.props.registerRequest)

이 thunk 를 실행하는 메소드를 Register 컴폰넌트 안에서 정의하여 Authentication 컴포넌트로 전달해 주도록하겠습니다.


(./src/containers/Register.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import React, { Component } from 'react';
import { Authentication } from 'components';
import { connect } from 'react-redux';
import { registerRequest } from 'actions/authentication';
 
class Register extends Component {
    handleRegister = (id, pw) => {
        return this.props.registerRequest(id, pw).then(
            () => {
                if(this.props.status === "SUCCESS") {
                    Materialize.toast('Success! Please log in.'2000);
                    this.props.history.push('/login');
                    return true;
                } else {
                    /*
                        ERROR CODES:
                            1: BAD USERNAME
                            2: BAD PASSWORD
                            3: USERNAME EXISTS
                    */
                    let errorMessage = [
                        'Invalid Username',
                        'Password is too short',
                        'Username already exists'
                    ];
 
                    let $toastContent = $('<span style="color: #FFB4BA">' + errorMessage[this.props.errorCode - 1+ '</span>');
                    Materialize.toast($toastContent, 2000);
                    return false;
                }
            }
        );
    }
 
    render() {
        return (
            <div>
                <Authentication mode={false}
                  onRegister={this.handleRegister}/>
            </div>
        );
    }
}
 
const mapStateToProps = (state) => {
    return {
        status: state.authentication.register.status,
        errorCode: state.authentication.register.error
    };
};
 
const mapDispatchToProps = (dispatch) => {
    return {
        registerRequest: (id, pw) => {
            return dispatch(registerRequest(id, pw));
        }
    };
};
 
export default connect(mapStateToProps, mapDispatchToProps)(Register);
cs


코드의 7~33 번째 줄에서 thunk 메소드를 실행하는 메소드인 handleRegister 를 정의하고 39 번째 줄에서 Authentication 컴포넌트에 onRegister props 로 전달했습니다.


handleRegister 의 내용은 다음과 같습니다.

먼저 thunk 를 실행합니다. thunk 는 BACK-END 와 통신하여 리덕스 state를 변경합니다.

실행후 .then() 메소드를 통해 다음코드를 실행하는데, 이 시점은 리덕스 state 가 변경된 이후의 시점입니다.

.then() 메소드의 콜백함수는 this.props.status (리덕스로 연결된 register 상태 - WAITING / SUCCESS / FAILURE 중의 하나) 의 값에 따라 다른방식으로 작동하도록 되어 있습니다.

만약 this.props.status 의 값이 SUCCESS 라면 Materializecss 의 알림메소드로 회원가입에 성공했으니 로그인하라는 알림을 주고 로그인 페이지로 이동시킵니다.

* SUCCESS 일때 ture를, SUCCESS 가 아닐때 false 를 리턴하는데, 이는 Authentication 의 state(input 창)를 비우는것과 연관이 있습니다. (3-8 에서 설명 예정)

* react-router v4 에서는 라우트컴포넌트에 기본적으로 history, location, match 객체를 프롭스로 전달합니다. (browserHistory 지원 중단)

만약 this.props.status 의 값이 SUCCESS 가 아니라면 업데이트된 에러코드(state)를 통해 Materializecss 알림을 띄웁니다.


3-8) Authentication 컴포넌트에서 thunk 의 인자 결정하여 실행


Authentication 컴포넌트는 Register 컨테이너로부터 onRegister 속성으로 thunk 실행 메소드를 전달받았습니다.

Authentication 는 thunk 함수의 인자인 username 과 password 값을 결정해 실행하는 역할을 수행합니다.


이를 위해 input 창에 들어오는 값들을 Authentication 의 state(컴포넌트 state)로 설정하고, 이 state 를 통하여 onRegister 함수를 실행시키도록 하겠습니다.


(./src/components/Authentication.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
 
class Authentication extends Component {
    state = {
      username:"",
      password:""
    }
 
    handleChange = (e) => {
        let nextState = {};
        nextState[e.target.name= e.target.value;
        this.setState(nextState);
    }
 
    handleRegister = () => {
        let id = this.state.username;
        let pw = this.state.password;
 
        this.props.onRegister(id, pw).then(
            (result) => {
                if(!result) {
                    this.setState({
                        username: '',
                        password: ''
                    });
                }
            }
        );
    }
 
    render() {
        const inputBoxes = (
            <div>
                <div className="input-field col s12 username">
                    <label>Username</label>
                    <input
                    name="username"
                    type="text"
                    className="validate"
                    onChange={this.handleChange}
                    value={this.state.username}/>
                </div>
                <div className="input-field col s12">
                    <label>Password</label>
                    <input
                    name="password"
                    type="password"
                    className="validate"
                    onChange={this.handleChange}
                    value={this.state.password}/>
                </div>
            </div>
        );
 
        const loginView = (
            <div>
                <div className="card-content">
                    <div className="row">
                        {inputBoxes}
                        <a className="waves-effect waves-light btn">SUBMIT</a>
                    </div>
                </div>
 
 
                <div className="footer">
                    <div className="card-content">
                        <div className="right" >
                        New Here? <Link to="/register">Create an account</Link>
                        </div>
                    </div>
                </div>
 
            </div>
        );
 
        const registerView = (
            <div className="card-content">
                <div className="row">
                    {inputBoxes}
                    <a className="waves-effect waves-light btn"
                      onClick={this.handleRegister}>CREATE</a>
                </div>
            </div>
        );
        return (
          <div className="container auth">
              <Link className="logo" to="/">MEMOPAD</Link>
              <div className="card">
                  <div className="header blue white-text center">
                      <div className="card-content">{this.props.mode ? "LOGIN" : "REGISTER"}</div>
                  </div>
                  {this.props.mode ? loginView : registerView }
              </div>
          </div>
        );
    }
}
 
Authentication.propTypes = {
    mode: PropTypes.bool,
    onRegister: PropTypes.func
};
 
Authentication.defaultProps = {
    mode: true,
    onRegister: (id, pw) => { console.error("register function is not defined"); }
};
 
export default Authentication;
 
cs


코드의 6 번째 줄에서 컴포넌트 state 를 정의 했습니다. 

state 에 접근할 떄는this.state.username / this.state.password 와 같이 접근합니다.


코드의 42 번째 줄과 51 번째 줄을 보면 input 태그에 onChange 이벤트에 this.handleChange 메소드가 등록되어 있는것을 알 수 있습니다.

이는 input 태그에 변화 이벤트 (태그에 값을 입력) 가 발생하면 handleChange 메소드를 실행하겠다는 의미입니다.


handleChange 메소드는 코드의 11 번째 줄에 정의돼 있습니다.

메소드의 내용은 입력된 값으로 this.state 를 변경하는 것입니다. (setState 를 통해)

* 메소드의 파라메터 e 는 이벤드 객체를 의미하며, e.target 은 이벤트가 일어나는 타깃 엘리먼트를 의미합니다. 이 경우에는 입력중인 input 창(name 값이 username 혹은 passowrd)입니다. 

* 또한 input 태그의 value 값이 각각 state로 설정되어 있는데, 이는 handleChange 함수와 실시간으로 연동되는것을 의미합니다.

(handleChange 에 의해 state 값이 변경되고 해당 값이 input 태그의 값이 된다.)


17 번째 줄에서 handleRegister 메소드를 정의 했는데, 이는 Register 로부터 전달받은 thunk 함수를 실행하는 메소드입니다.

this.state 값을 인자로 하여 thunk를 실행하고, 실행이 완료되었을 때 리턴되는 값이 false 라면 인풋창을 비웁니다. (실패했을때 비운다. 성공하면 로그인 페이지로 push 되기 때문에 비울 필요 없음)

handleRegister 메소드는 82 번째 줄에서 CREATE 버튼(a 태그) 에 onClick 이벤트로 지정해 줍니다.


프롭스로 값을 전달받았으므로 propTypes 와 defaultProps 를 지정해 줍니다.

* propTypes 는 리액트가 버전업 되면서 외부 모듈을 설치해 import 하여 사용하는 것으로 변경되었습니다.(2번째 줄)

(npm install --save prop-types)


여기까지 완료했다면 회원가입 기능이 잘 구현되는지 확인해 보도록 하겠습니다.


npm run win_development

* mongoDB 서버도 열려있어야 합니다.


명령어를 프로젝트 파일 경로에서 입령해 dev 서버를 실행시키고, 브라우저를 통해 아래의 주소로 접속합니다. 


localhost:4000/register


원하는 아이디와 비밀번호를 입력하고 CREATE 버튼을 눌러봅시다.

어떤가요? 성공했다는 메시지와 함께 /login 경로로 이동하나요? (해당 경로에는 어떤 화면도 렌더링 되지 않는 것이 정상)

잘 된다면 똑같은 아이디로 회원가입을 한 번 더 시도해 보세요. 

이미 아이디가 있다는 경고 메시지가 뜬다면 잘 따라오신 겁니다.


여기까지...


인증의 회원가입을 통해 본격적인 기능 구현을 시작해보았습니다.

가장 중요한 것은 역시 각각 요소들이 어떤 역할을하고, 어떤 데이터들이 어떻게 이동하는지 이해하는것 입니다.

만약 여기까지 따라왔는데 이해가 잘 되지 않으신 분들은 위쪽으로 올라가 작동원리에 대해 천천히 한 번 더 읽어보시는 것을 추천드립니다.

처음 봤을 때 이해하지 못하더라고 코드를 구현하고 나서 보면 이해가 쉬워질 수 있기 때문이죠.

부족한 설명을 잘 견디며 끝까지 봐 주셔서 감사합니다.


다음 포스팅에서는 나머지 인증 기능(로그인, 로그아웃, 로그인확인)을 마저 구현해 보도록 하겠습니다.


감사합니다.


**참고 자료 (항상 감사드립니다)

<React.js codelab 2016 - Velopert>


*이 포스팅이 도움이 되셨다면 다녀가셨다는 표시로 공감 부탁드릴게요! (로그인 하지 않으셔도 공감은 가능합니다 ㅎㅎ)

Comments