React.js Codelab 2016 – Express 와 React.js 를 사용한 웹 어플리케이션 만들기 (1)


시작하면서..

React.js 코드랩에 참여하신 모든 개발자 여러분! 환영합니다!

저희는 이제 배경지식을 어느정도 공부 한 상태이고, 이제 마지막 프로젝트를 진행해볼 차례입니다.

(코드랩 세션을 참석하지 못하셨다면, https://velopert.com/reactjs-tutorials 에서 React 입문을 하고나서 이 강좌를 진행해주세요.)

이전에 만들었던 예제 프로젝트와는 달리 이번에는 조금은 멋진? 웹 어플리케이션을 만들어볼거에요.

미리보기 URL: https://memo.hoah.xyz/

몇몇 분들에게는 간단한 프로젝트가 될 수 도 있겠지만,

초보자들분들에게는 어쩌면 조금은 복잡한 프로젝트가 될 수도 있습니다.

 

만약에 코드랩 세션에서 시간이 부족하거나 도중에 막혀서 완성하지 못할시엔, 이 포스트를 참조하면서 프로젝트를 완성해보시길 바랍니다.

도중에 질문이 생기거나 하면 언제든지 물어봐주세요 (덧글 혹은 이메일)

 

강의를 시작하기전에, 작업환경을 설정해주시구요: https://velopert.com/1980

자, 그러면 강의를 시작하겠습니다 !


1. Express & React.js 설정하기

초기 프로젝트 CLONE 하기

git clone https://github.com/velopert/react-codelab-project.git
cd react-codelab-project.git
git checkout step00

npm install
# npm install 과정이 오래 걸린다면, 다음과 같이 node_modules.zip 을 다운로드 받아서 압축을 해제하세요
# 윈도우라면 직접 받아서 압축해제하세요.
wget https://github.com/velopert/react-codelab-fundamentals/releases/download/1.0/node_modules.zip
unzip node_modules.zip -d node_modules

# webpack 과 webpack-dev-server 가 gloabl install이 안돼있다면 설치하세요
npm install -g webpack webpack-dev-server

Global Dependency 설치

npm install -g babel-cli nodemon cross-env

babel-cli: 콘솔 환경에서 babel 을 사용 할 수 있게 해줍니다 (ES6 transpile)

nodemon: development 환경에서 파일이 수정 될 때마다 서버를 재시작해줍니다

cross-env: 윈도우 / 리눅스 / OSX 에서 환경변수값을 설정합니다.

 

Local Dependency 설치

npm install --save express body-parser

express: Nodejs 웹 프레임워크

body-parser: JSON 형태의 데이터를 HTTP 요청에서 파싱 할 때 사용됩니다

 

Express 코드 작성 server/main.js

import express from 'express';
import path from 'path';

const app = express();
const port = 3000;

app.use('/', express.static(path.join(__dirname, './../public')));

app.get('/hello', (req, res) => {
    return res.send('Hello CodeLab');
});

app.listen(port, () => {
    console.log('Express is listening on port', port);
});

 

NPM 스크립트 수정 package.json

{
  "name": "codelab",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "clean": "rm -rf build public/bundle.js",
    "build": "babel server --out-dir build --presets=es2015 && webpack",
    "start": "cross-env NODE_ENV=production node ./build/main.js",
    "development": "cross-env NODE_ENV=development nodemon --exec babel-node --presets=es2015 ./server/main.js --watch server"
  },
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "body-parser": "^1.15.2",
    "express": "^4.14.0",
    "path": "^0.12.7",
    "react": "^15.1.0",
    "react-dom": "^15.1.0"
  },
  "devDependencies": {
    "babel-core": "^6.9.1",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.9.0",
    "babel-preset-react": "^6.5.0",
    "react-hot-loader": "^1.3.0",
    "webpack": "^1.13.1",
    "webpack-dev-server": "^1.14.1"
  }
}

주의: 윈도우는 명령어가 다릅니다! 윈도우는 win_start / win_development 를 이용해주세요.

npm run build 를 실행하면 서버사이드 스크립트들을 build 폴더에 transpile 하여 저장하고 webpack 을 통해 클라이언트 코드를 build 합니다.

 

Webpack 개발서버용 설정파일 만들기 webpack.dev.config.js

var webpack = require('webpack');

module.exports = {

    /* webpack-dev-server를 콘솔이 아닌 자바스크립트로 실행 할 땐, 
    HotReloadingModule 를 사용하기 위해선 dev-server 클라이언트와 
    핫 모듈을 따로 entry 에 넣어주어야 합니다. */
    
    entry: [
        './src/index.js',
        'webpack-dev-server/client?http://0.0.0.0:4000', // 개발서버의 포트가 이 부분에 입력되어야 제대로 작동합니다
        'webpack/hot/only-dev-server'
    ],

    output: {
        path: '/', // public 이 아니고 /, 이렇게 하면 파일을 메모리에 저장하고 사용합니다
        filename: 'bundle.js'
    },

    // 개발서버 설정입니다
    devServer: {
        hot: true,
        filename: 'bundle.js',
        publicPath: '/',
        historyApiFallback: true,
        contentBase: './public',
        /* 모든 요청을 프록시로 돌려서 express의 응답을 받아오며,
        bundle 파일의 경우엔 우선권을 가져서 devserver 의 스크립트를 사용하게 됩니다 */
        proxy: {
            "**": "http://localhost:3000" // express 서버주소
        },
        stats: {
          // 콘솔 로그를 최소화 합니다
          assets: false,
          colors: true,
          version: false,
          hash: false,
          timings: false,
          chunks: false,
          chunkModules: false
        }
    },


    plugins: [
        new webpack.optimize.OccurenceOrderPlugin(),
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoErrorsPlugin()
    ],

    module: {
        loaders: [
            {
                test: /\.js$/,
                loaders: ['react-hot', 'babel?' + JSON.stringify({
                    cacheDirectory: true,
                    presets: ['es2015', 'react']
                })],
                exclude: /node_modules/,
            }
        ]
    }


};

development 환경과 production 환경에서 bundle.js 의 결과값이 다르기 때문에 개발환경 전용 설정파일을 따로 만듭니다

주의: 최근 react-hot-loader 가 업데이트 되어서, 그냥 설치하시면 “react-hot-loader”: “^3.0.0-beta.3” 가 설치됩니다.

설치 하실 때, npm install –save react-hot-loader@1.3.0 을 하시거나, 버전 3을 쓰고 싶다면 수정을 다음과 같이 하세요:

    module:{
        loaders: [
            {
                test: /.js$/,
                loader: 'babel',
                exclude: /node_modules/,
                query: {
                    cacheDirectory: true,
                    presets: ['es2015', 'react'],
                    plugins: ["react-hot-loader/babel"]
                }
            }
        ]
    },

 

Webpack 설정파일 수정 webpack.config.js

module.exports = {
    entry: './src/index.js',

    output: {
        path: __dirname + '/public/',
        filename: 'bundle.js'
    },

    module: {
        loaders: [
            {
                test: /\.js$/,
                loaders: ['babel?' + JSON.stringify({
                    cacheDirectory: true,
                    presets: ['es2015', 'react']
                })],
                exclude: /node_modules/,
            }
        ]
    }
};

기존의 필요없는 설정을 지웠습니다.

 

server 메인 파일 수정 server/main.js

import WebpackDevServer from 'webpack-dev-server';
import webpack from 'webpack';

const devPort = 4000;

/*
    Express Codes 
*/


if(process.env.NODE_ENV == 'development') {
    console.log('Server is running on development mode');
    const config = require('../webpack.dev.config');
    const compiler = webpack(config);
    const devServer = new WebpackDevServer(compiler, config.devServer);
    devServer.listen(
        devPort, () => {
            console.log('webpack-dev-server is listening on port', devPort);
        }
    );
}

development 환경일때 개발서버를 켜는 코드를 추가하였습니다.

 

체크포인트

앞으로 진행을 하다가 도중에 막히면 (해결을 하고 넘어가는게 좋지만) 계속해서 진행을 하기위하여 체크포인트로 “이동”을 할 수 있습니다.
현 상태를 체크포인트로 넘어가는 방법은 다음과 같습니다:

# 기존에 했던 작업을 일단 커밋
git add .
git commit -m"막힌 부분 기록.."
# checkpoint 로 이동
git checkpoint step[섹션번호]
# 예: git checkpoint step01
# 도중에 막히면 일단 체크포인트로 가고,
# 코드랩이 끝나고 나서 막혔던 checkpoint 로 다시 이동하여 어떤 부분이 잘못됐었나 다시 되짚어보세요.

해당 섹션의 전체 코드를 확인하고자 한다면 https://github.com/velopert/react-codelab-project 에 들어가서

제목 없음

위와 같이 브랜치를 선택해주시면 됩니다.

2. MongoDB 설치

저희 프로젝트에서는 MongoDB 데이터베이스를 사용합니다.
MongoDB 를 설치하는 과정은 이 포스트 를 참고해주세요.

코드랩 세션에선 기본적인 MongoDB 에 대한 소개와 사용법을 설명해드렸었지만, 코드랩 세션에 참여하지 않은 분들은
MongoDB 강좌를 훑어보시길 바랍니다

 

3. 미들웨어 및 기타 모듈 설치

주의: 이 포스트에 나오는 코드 조각들에는 일부 코드(이미 작성된 부분)들이 생략되어있습니다.
필요한 부분만 적혀있으니, 적당한 위치에 코드들을 삽입/수정 해주세요

모듈 설치 및 적용 (i)

npm install --save morgan body-parser

server/main.js

import morgan from 'morgan'; // HTTP REQUEST LOGGER
import bodyParser from 'body-parser'; // PARSE HTML BODY

app.use(morgan('dev'));
app.use(bodyParser.json());

morgan: HTTP 요청을 로그하는 미들웨어

body-parser: 요청에서 JSON을 파싱할때 사용되는 미들웨어

 

모듈 설치 및 적용 (ii)

npm install --save mongoose express-session

server/main.js

import mongoose from 'mongoose';
import session from 'express-session';

/* mongodb connection */
const db = mongoose.connection;
db.on('error', console.error);
db.once('open', () => { console.log('Connected to mongodb server'); });
// mongoose.connect('mongodb://username:password@host:port/database=');
mongoose.connect('mongodb://localhost/codelab');

/* use session */
app.use(session({
    secret: 'CodeLab1$1$234',
    resave: false,
    saveUninitialized: true
}));

mongoose: mongodb 데이터 모델링 툴; MongoDB 에 있는 데이터를 여러분의 Application에서 JavaScript 객체로 사용 할 수 있도록 해줍니다.

참고: https://velopert.com/594

express-session: express 에서 세션을 다룰 때 사용되는 미들웨어

참고: https://velopert.com/406

 

4. Backend – 계정인증 구현하기

디렉토리 구조 이해하기

server
├── main.js
├── models
│   ├── account.js
│   └── memo.js
└── routes
    ├── account.js
    ├── index.js
    └── memo.js

models 디렉토리엔 mongoose로 만든 데이터 모델이 저장되어있고, routes 디렉토리엔 회원인증 / 메모 API 들이 저장되어있습니다.

 

account 라우터 생성 (server/routes/account.js)

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;

회원가입 / 로그인 / 현재세션체크 API 를 담당할 account 라우터 입니다

먼저 틀만 짜놓고 나중에 구현합시다

 

api 루트 라우터 생성 (server/routes/index.js)

import express from 'express';
import account from './account';

const router = express.Router();
router.use('/account', account);

export default router;

지금은 루트 라우터에서 account 라우터만 불러와서 사용하지만

나중에는 메모를 담당하는 memo 라우터도 불러와서 사용하게 됩니다

 

api 라우터 불러와서 사용 (server/main.js)

/* setup routers & static directory */
import api from './routes';
app.use('/api', api);

이렇게 서버 메인 파일에서 api 라우터를 불러오게 되면,

http://URL/api/account/signup 이런식으로 api 를 사용 할 수 있게 됩니다

 

mongoose 를 통한 account 모델링 (server/models/account.js)

import mongoose from 'mongoose';

const Schema = mongoose.Schema;

const Account = new Schema({
    username: String,
    password: String,
    created: { type: Date, default: Date.now }
});

export default mongoose.model('account', Account);

account Schema 를 만들고 model 로 만들어서 export 합니다

Schema 와 Model 의 차이는, Schema 는 그냥 데이터의 ‘틀’ 일 뿐이구요, Model 은, 실제 데이터베이스에 접근 할 수 있게 해주는 클래스입니다

참고: http://mongoosejs.com/docs/guide.html

모델화르 할때 .model() 의 첫번째 인수로 들어가는 account 는 collection 이름이에요. 근데, 이게 복수형으로 설정이됩니다. 예를들어 account의 복수형은 accounts 이니 accounts 라는 컬렉션이 만들어지고 거기에 저장이 되는거죠. 컬렉션 이름을 직접 정하고싶다면 .model(‘my_account’, Account, ‘my_account’) 이런식으로 세번째 인수를 추가하여 전달해주면 됩니다.

 

bcryptjs 해쉬 모듈 설치

npm install --save bcryptjs

게정 인증을 구현하는데, 비밀번호를 그냥 plain text 로 저장하게하면 보안상 좀 허술하겠죠?

bcryptjs 모듈을 사용하여 비밀번호 보안을 강화합시다! 사용법: https://www.npmjs.com/package/bcryptjs#usage—sync

 

bcryptjs 적용하기 (server/models/account.js)

import mongoose from 'mongoose';
import bcrypt from 'bcryptjs';

const Schema = mongoose.Schema;

const Account = new Schema({
    username: String,
    password: String,
    created: { type: Date, default: Date.now }
});

// generates hash
Account.methods.generateHash = function(password) {
    return bcrypt.hashSync(password, 8);
};

// compares the password
Account.methods.validateHash = function(password) {
    return bcrypt.compareSync(password, this.password);
};

export default mongoose.model('account', Account);

여기서, Schema 자체에 임의 메소드 두개를 정의해주었습니다.

이렇게 메소드를 만들어주면 나중에 모델에서 해당 메소드를 실행 할 수 있습니다

주의하실 점은 여기서는 arrow 메소드를 사용하시면 제대로 작동하지 않기 때문에 그냥 일반 함수형으로 작성하셔야합니다 (this binding 오류)

 

회원가입 구현: POST /api/signup (server/routes/account.js)

import express from 'express';
import Account from '../models/account';

const router = express.Router();

/*
    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({
            error: "BAD USERNAME",
            code: 1
        });
    }

    // CHECK PASS LENGTH
    if(req.body.password.length < 4 || typeof req.body.password !== "string") {
        return res.status(400).json({
            error: "BAD PASSWORD",
            code: 2
        });
    }

    // CHECK USER EXISTANCE
    Account.findOne({ username: req.body.username }, (err, exists) => {
        if (err) throw err;
        if(exists){
            return res.status(409).json({
                error: "USERNAME EXISTS",
                code: 3
            });
        }

        // CREATE ACCOUNT
        let account = new Account({
            username: req.body.username,
            password: req.body.password
        });

        account.password = account.generateHash(account.password);

        // SAVE IN THE DATABASE
        account.save( err => {
            if(err) throw err;
            return res.json({ success: true });
        });

    });
});

// more codes..

mongoose 의 사용법은, mongodb 의 명령어와 매우 비슷합니다.

새 모델을 만들때는, 객체를 생성해주고, save 메소드를 통하여 값을 저장합니다.

 

로그인 구현: POST /api/signin (server/routes/account.js)

/*
    ACCOUNT SIGNIN: POST /api/account/signin
    BODY SAMPLE: { "username": "test", "password": "test" }
    ERROR CODES:
        1: LOGIN FAILED
*/
router.post('/signin', (req, res) => {

    if(typeof req.body.password !== "string") {
        return res.status(401).json({
            error: "LOGIN FAILED",
            code: 1
        });
    }

    // FIND THE USER BY USERNAME
    Account.findOne({ username: req.body.username}, (err, account) => {
        if(err) throw err;

        // CHECK ACCOUNT EXISTANCY
        if(!account) {
            return res.status(401).json({
                error: "LOGIN FAILED",
                code: 1
            });
        }

        // CHECK WHETHER THE PASSWORD IS VALID
        if(!account.validateHash(req.body.password)) {
            return res.status(401).json({
                error: "LOGIN FAILED",
                code: 1
            });
        }

        // ALTER SESSION
        let session = req.session;
        session.loginInfo = {
            _id: account._id,
            username: account.username
        };

        // RETURN SUCCESS
        return res.json({
            success: true
        });
    });
});

express session 을 다루는건 매우 간단합니다.

따로 해야 할 건 없고, req.session 을 사용해서 그냥 객체 다루듯이 하면 됩니다

 

세션확인 구현: GET /api/getInfo (server/routes/account.js)

/*
    GET CURRENT USER INFO GET /api/account/getInfo
*/
router.get('/getinfo', (req, res) => {
    if(typeof req.session.loginInfo === "undefined") {
        return res.status(401).json({
            error: 1
        });
    }

    res.json({ info: req.session.loginInfo });
});

세션확인이 필요한 이유는, 클라이언트에서 로그인 시, 로그인 데이터를 쿠키에 담고 사용을 하고 있다가,

만약에 새로고침을 해서 어플리케이션을 처음부터 다시 렌더링 하게 될 때, 지금 갖고 있는 쿠키가 유효한건지 체크를 해야 하기 때문입니다.

 

로그아웃 구현: POST /api/logout (server/routes/account..js)

/*
    LOGOUT: POST /api/account/logout
*/
router.post('/logout', (req, res) => {
    req.session.destroy(err => { if(err) throw err; });
    return res.json({ sucess: true });
});

현재 세션을 파괴 할 때는 req,session.destroy() 를 사용하면 됩니다.

간단하죠.

 

Express 에러처리 (server/main.js)

/* handle error */
app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

라우터에서 throw err 가 실행되면 이 코드가 실행됩니다

 

5. Backend – 메모 작성 / 수정 / 삭제 / 읽기 구현하기

Memo 모델 만들기 (server/models/memo.js)

import mongoose from 'mongoose';

const Schema = mongoose.Schema;

const Memo = new Schema({
    writer: String,
    contents: String,
    starred: [String],
    date: {
        created: { type: Date, default: Date.now },
        edited: { type: Date, default: Date.now }
    },
    is_edited: { type: Boolean, default: false }
});

export default mongoose.model('memo', Memo);

 

Memo 라우터 만들기 (server/routes/memo.js)

import express from 'express';
import Memo from '../models/memo';
import mongoose from 'mongoose';

const router = express.Router();

// WRITE MEMO
router.post('/', (req, res) => {

});

// MODIFY MEMO
router.put('/:id', (req, res) => {

});

// DELETE MEMO
router.delete('/:id', (req, res) => {

});

// GET MEMO LIST
router.get('/', (req, res) => {

});

export default router;

계정인증부분 작성 할 때 처럼, 먼저 틀을 작성해두고 구현은 차차 하도록 하겠습니다.

 

API 라우터에 Memo 라우터 추가 (server/routes/index.js)

import express from 'express';
import account from './account';
import memo from './memo';

const router = express.Router();
router.use('/account', account);
router.use('/memo', memo);

export default router;

memo 라우터를 api 라우터에서 사용하도록 합시다. 이제 /api/memo 에다가 GET / POST / PUT / DELETE 등 메소드로 요청을 할 수 있습니다

 

작성기능 구현하기: POST /api/memo (server/routes/memo.js)

/* 
    WRITE MEMO: POST /api/memo
    BODY SAMPLE: { contents: "sample "}
    ERROR CODES
        1: NOT LOGGED IN
        2: EMPTY CONTENTS
*/
router.post('/', (req, res) => {
    // CHECK LOGIN STATUS
    if(typeof req.session.loginInfo === 'undefined') {
        return res.status(403).json({
            error: "NOT LOGGED IN",
            code: 1
        });
    }
    
    // CHECK CONTENTS VALID
    if(typeof req.body.contents !== 'string') {
        return res.status(400).json({
            error: "EMPTY CONTENTS",
            code: 2
        });
    }

    if(req.body.contents === "") {
        return res.status(400).json({
            error: "EMPTY CONTENTS",
            code: 2
        });   
    }

    // CREATE NEW MEMO
    let memo = new Memo({
        writer: req.session.loginInfo.username,
        contents: req.body.contents
    });

    // SAVE IN DATABASE
    memo.save( err => {
        if(err) throw err;
        return res.json({ success: true });
    });
});

 

읽기기능 구현하기: GET /api/memo (server/routes/memo.js)

/*
    READ MEMO: GET /api/memo
*/
router.get('/', (req, res) => {
    Memo.find()
    .sort({"_id": -1})
    .limit(6)
    .exec((err, memos) => {
        if(err) throw err;
        res.json(memos);
    });
});

지금으로서는, 작성된 메모들을 최신부터 오래된것 순서로 6개만 읽어옵니다.

나중에, 무한 스크롤링을 구현 할 때는, 특정 _id 보다 낮은 메모 6개 읽기,
새로운 메모를 읽어올 때에는, 특정 _id 보다 높은 메모읽기,
그리고 유저를 검색할 때 사용 될 특정 유저의 메모 읽기 기능을 구현 할 것입니다.

 

삭제 기능 구현하기: DELETE /api/memo/:id (server/routes/memo.js)

/*
    DELETE MEMO: DELETE /api/memo/:id
    ERROR CODES
        1: INVALID ID
        2: NOT LOGGED IN
        3: NO RESOURCE
        4: PERMISSION FAILURE
*/
router.delete('/:id', (req, res) => {

    // CHECK MEMO ID VALIDITY
    if(!mongoose.Types.ObjectId.isValid(req.params.id)) {
        return res.status(400).json({
            error: "INVALID ID",
            code: 1
        });
    }

    // CHECK LOGIN STATUS
    if(typeof req.session.loginInfo === 'undefined') {
        return res.status(403).json({
            error: "NOT LOGGED IN",
            code: 2
        });
    }

    // FIND MEMO AND CHECK FOR WRITER
    Memo.findById(req.params.id, (err, memo) => {
        if(err) throw err;

        if(!memo) {
            return res.status(404).json({
                error: "NO RESOURCE",
                code: 3
            });
        }
        if(memo.writer != req.session.loginInfo.username) {
            return res.status(403).json({
                error: "PERMISSION FAILURE",
                code: 4
            });
        }

        // REMOVE THE MEMO
        Memo.remove({ _id: req.params.id }, err => {
            if(err) throw err;
            res.json({ success: true });
        });
    });

});

 

수정 기능 구현하기: PUT /api/memo/:id  (server/routes/memo.js)

/*
    MODIFY MEMO: PUT /api/memo/:id
    BODY SAMPLE: { contents: "sample "}
    ERROR CODES
        1: INVALID ID,
        2: EMPTY CONTENTS
        3: NOT LOGGED IN
        4: NO RESOURCE
        5: PERMISSION FAILURE
*/
router.put('/:id', (req, res) => {

    // CHECK MEMO ID VALIDITY
    if(!mongoose.Types.ObjectId.isValid(req.params.id)) {
        return res.status(400).json({
            error: "INVALID ID",
            code: 1
        });
    }

    // CHECK CONTENTS VALID
    if(typeof req.body.contents !== 'string') {
        return res.status(400).json({
            error: "EMPTY CONTENTS",
            code: 2
        });
    }

    if(req.body.contents === "") {
        return res.status(400).json({
            error: "EMPTY CONTENTS",
            code: 2
        });
    }

    // CHECK LOGIN STATUS
    if(typeof req.session.loginInfo === 'undefined') {
        return res.status(403).json({
            error: "NOT LOGGED IN",
            code: 3
        });
    }

    // FIND MEMO
    Memo.findById(req.params.id, (err, memo) => {
        if(err) throw err;

        // IF MEMO DOES NOT EXIST
        if(!memo) {
            return res.status(404).json({
                error: "NO RESOURCE",
                code: 4
            });
        }

        // IF EXISTS, CHECK WRITER
        if(memo.writer != req.session.loginInfo.username) {
            return res.status(403).json({
                error: "PERMISSION FAILURE",
                code: 5
            });
        }

        // MODIFY AND SAVE IN DATABASE
        memo.contents = req.body.contents;
        memo.date.edited = new Date();
        memo.is_edited = true;

        memo.save((err, memo) => {
            if(err) throw err;
            return res.json({
                success: true,
                memo
            });
        });
        
    });
});

Backend를 작업하는건, NodeJS 를 처음 사용해보시는거라면, 익숙하지 않을수도 있지만,
하면 할수록 편해진답니다.

자, 이제 기본적인 Backend 구현이 끝났습니다.

(단, 아직 Backend 가 완전하게 끝난건 아닙니다, 아직 star 기능, 유저 찾기 기능, 페이징 기능이 남아있습니다)

우리가 지금 만든 Backend 를 토대로 React 어플리케이션을 만들어봅시다.

  • Seongkuk Park

    아직 mongoose 가 익숙치 않아서 복잡해 보이네요;

  • 안녕하세요. 최근 프로젝트에 react를 접목하면서 많은 도움이 되고 있습니다.
    다름 아니라 Webpack-dev-server 설정 파일 (webpack.dev.config.js) 의 프록시 설정이 최근들어 미묘한 변동이 있는 것 같아 댓글을 답니다.
    오늘 개인 프로젝트를 새로 git clone 떠서 npm3 install을 했더니 다음 코드의 프록시가 제대로 작동하지 않았습니다.
    proxy : {
    “*” : “http://localhost:3000”
    },

    그래서 좀더 찾아보니 webpack dev server 공식 github.io의 proxy 부분이 미묘하게 바뀌었더군요.
    // Set this if you want webpack-dev-server to delegate a single path to an arbitrary server.
    // Use “**” to proxy all paths to the specified server.
    // This is useful if you want to get rid of ‘http://localhost:8080/’ in script[src],
    // and has many other use cases (see https://github.com/webpack/webpack-dev-server/pull/127 ).
    proxy: {
    “**”: “http://localhost:9090”
    },

    설정을 **으로 바꾼 뒤로는 다시 정상 작동합니다. 제 개인설정이 잘못 된 것일수도 있지만 혹여 확인해보시고 webpack.config 규격이 바뀐 것이라면 반영하는 것이 좋을 것 같아 알려드립니다.

    • 감사합니다! 이전에는 webpack-dev-middleware 를 구버전을 사용해서 됐었던거였네요.
      수정하겠습니다 🙂

    • 만약에 코드랩 중에 발견했으면 멘붕했을것같습니다…ㅋㅋㅋㅋ

  • 레미프

    음.. 근데 왜 main.js 에서 app 설정하는 내용이 없는거 같은데 제가 잘못안건가요?

    • Express 코드 작성 server/main.js

      에서 작성한 코드를 그대로 유지하면서 진행합니다. 헷갈릴수도있으니 조만간 수정하겠습니다.

  • ggoban

    api 라우터 불러와서 사용 (server/main.js)

    /* setup routers & static directory */
    import api from ‘./routes’;
    app.use(‘/api’, api);
    이렇게 서버 메인 파일에서 api 라우터를 불러오게 되면,

    http://URL/api/signup 이런식으로 api 를 사용 할 수 있게 됩니다.

    위부분은 아래 내용과 테스트 내용하고 비교해보면 http://URL/api/account/signup 이런형태가 맞는거죠??

    따라서 실습중인데 저는 express는 따로 분리하는걸 좋아해서 분리후 진행중입니다. 별 문제는 없겠죠??

    • * 수정되었습니다.

      이전엔 한 프로젝트에 진행 했었는데, 저도 따로 분리해서 관리하는걸 좋아합니다 ㅎㅎㅎ
      별 문제 없습니다 !

  • ggoban

    좀 백엔드에서 둘러봤는데 signin 할때는 session 에 정상적으로 logininfo 가 보이는데
    로그인 상태 확인시의 req 전체를 보면 session에는 비어있더라구요. 대신 req.sessionStore 안에 MemoryStore 쪽 sessions 에 저장이 되던데 ..
    버전의 문젠지 두갤 분리해놓은 문젠지.. 잘 모르겠네요 ㅠㅠ쥬륵

  • 류한경

    velopert 님! mongoose.Types.ObjectId.isValid 이게 디비에 _id 값이 존재하는가? 안하는가?를 알려주는건가요?

    • 해당 명령어는 주어진 id 가 제대로된 mongodb id 인지 형식을 확인해줍니다.
      예를들어 ‘asdf’ 이렇게만 전달해주면 false 가 되겠죠.

      존재유무는 확인해주지 않아요.

  • Sim Isaac

    webpack.dev.config.js에
    변경사항 있습니다. rename되었다고 하네요.
    OccurenceOrderPlugin -> OccurrenceOrderPlugin
    그리고 이마저도 default가 되었기 때문에 할 필요 없대요. 관련 링크는
    https://gist.github.com/sokra/27b24881210b56bbaff7#occurrence-order
    입니다.

    NoErrorPlugin도 마찬가지로 안 되는데,
    ->NoErrorsPlugin으로 바뀐 건지 오타난 것인지는 모르겠습니다. 아무튼 제가 올린 것으로 바꾸셔야 해요. 그리고도 webpack option validation error가 나고 있는데 그 이유는 좀 찾아봐야겠네요.

  • Gen happy

    github 에 있는 react-skeleton 이용해서 공부해도 되겠죠?
    설치 해도 안되어서요…
    css-loader 라은 브랜치는 scss를 사용하는건가요?

  • Time Spot

    status 메시지에서 아래의 경우에 400이 아닌지..
    401은 403과 비슷한 개념이지만..

    router.post(‘/signin’, (req, res) => {

    if(typeof req.body.password !== “string”) {
    return res.status(401).json({
    error: “LOGIN FAILED”,
    code: 1
    });
    }

    아래부분또한 업무적으로 볼때
    404로 구분하는게 더 나은거 아닌가 의견드립니다..

    if(!account.validateHash(req.body.qode)) {
    return res.status(401).json({

  • Yongyeon Yong Kim

    잘보고 갑니다.

  • Help

    안녕하십니까! 글 정말 잘 보고있습니다.
    듀토리얼을 따라하며 필요한 모듈들을 다 설치 한 상황입니다.
    현재 OS는 윈도우를 사용하고 있는데 babel 명령어가 실행이 되지 않는 상황입니다. 그에 따라 build 시에 npm run build를 실행하면 babel부분의 오류가 뜨네요.

    cmd창에서 babel을 실행해보면,

    babel : ‘babel’ 용어가 cmdlet, 함수, 스크립트 파일 또는 실행할
    수 있는 프로그램 이름으로 인식되지 않습니다. 이름이 정확한지 확
    인하고 경로가 포함된 경우 경로가 올바른지 검증한 다음 다시 시도
    하십시오.

    위와 같은 오류가 발생하게 됩니다….! 도와주십시오!!

  • Hunyoung Kim

    안녕하세요. 밑에 분과 같은 맥락의 질문입니다.
    현재 OS를 윈도우로 사용하고 있어서 기존의 npm run build 등의 명령어 사용시 에러가 발생합니다.
    윈도우용 명령어인 win_start 등의 명령어의 정확한 사용법을 알고싶습니다.

  • JIN

    미리보기가.. ㅠㅠ 없군요… 따라가다 보면 미리보기를 만들어서 볼수있겟죠 ㅠㅠ?