시작하면서..
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 어플리케이션을 만들어봅시다.