시작하면서..
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 에서 세션을 다룰 때 사용되는 미들웨어
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 어플리케이션을 만들어봅시다.
