- Planning a REST API
- CURD Operations & Endpoints
- Validation
- Image Upload
- Authentication
- Node + Express App Setup => No changes
- Routing / Endpoints => No changes, more Http methods
- Handling Request & Responses => Parse + Send JSON Data , no Views
- Request Validation => No changes
- Database Communication => No changes
- Files, Uploads, Downloads => No changes (only on client - side)
- Session & Cookies => No Session & Cookie Usage
- Authentication => Different Authentication Approach
-
미리 제작한 프로젝트 가져오기
-
$npm i로 node_modules 설치하기 -
src/pages/Feed/feed.js
-
REST API 서버로 보낼 URL 설정하기
loadPosts = direction => { if (direction) { this.setState({ postsLoading: true, posts: [] }); } let page = this.state.postPage; if (direction === 'next') { page++; this.setState({ postPage: page }); } if (direction === 'previous') { page--; this.setState({ postPage: page }); } fetch('http://localhost:8080/feed/posts') .then(res => { if (res.status !== 200) { throw new Error('Failed to fetch posts.'); } return res.json(); }) .then(resData => { this.setState({ posts: resData.posts, totalPosts: resData.totalItems, postsLoading: false }); }) .catch(this.catchError); };
-
localhost:3000에 접속하면 포스트가 생성된 것을 볼 수 있다.
-
-
react : src/pages/Feed/Feed.js
-
finishEditHandler메소드 URL 수정하기finishEditHandler = postData => { this.setState({ editLoading: true }); // Set up data (with image!) let url = 'http://localhost:8080/feed/post'; let method = 'POST'; if (this.state.editPost) { url = 'URL'; } fetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: postData.title, content: postData.content }) }) .then(res => { if (res.status !== 200 && res.status !== 201) { throw new Error('Creating or editing a post failed!'); } return res.json(); }) ...
-
-
REST-API : controllers/feed.js
-
createPost에 Front Server로 보내줄 데이터 추가하기exports.createPost = (req, res, next) => { const title = req.body.title; const content = req.body.content; console.log(title); // Create post in db // status : 201 새로운 리소스 생성을 성공했다. res.status(201).json({ message: 'Post created successfully!', post: { id: new Date().toISOString(), title: title, content: content, creator: { name: 'kooks7' }, createdAt: new Date() } }); };
-
-
$npm i --save express-validator -
routes/feed.js
-
expressvalidator 가져오고 각 라우터에 검증 로직 넣기exports.getPosts = (req, res, next) => { res.status(200).json({ posts: [ { _id: '1', title: 'First Posts', content: 'This is the first post!', imageUrl: 'images/home4.jpg', creator: { name: 'kooks7' }, createdAt: new Date() } ] }); };
-
-
controllers/feed.js
-
포스트 생성할 때 오류 검증하기
const { validationResult } = require('express-validator/check'); ... exports.createPost = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res .status(422) .json({ message: '올바른 값을 입력해주세요', errors: errors.array() }); } const title = req.body.title; const content = req.body.content; // Create post in db // status : 201 새로운 리소스 생성을 성공했다. res.status(201).json({ message: 'Post created successfully!', post: { id: new Date().toISOString(), title: title, content: content, creator: { name: 'kooks7' }, createdAt: new Date() } }); };
-
-
$npm i --save mongoose -
app.js
-
mongoose 세팅하기
const mongoose = require('mongoose'); const MONGODB_URI = 'mongodb://localhost:27017/test'; ... mongoose .connect(MONGODB_URI) .then(result => { app.listen(8080); }) .catch(err => { console.log(err); });
-
-
models/post.js
-
post 스키마 만들기
const mongoose = require('mongoose'); const Schema = mongoose.Schema; const postSchema = new Schema( { title: { type: String, required: true }, imageUrl: { type: String, required: true }, content: { type: String, required: true }, creator: { type: Object, required: true } }, { timestamps: true } ); module.exports = mongoose.model('Post', postSchema);
-
-
controllers/feed.js
-
createPost와 mongodb 연동하기exports.createPost = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res .status(422) .json({ message: '올바른 값을 입력해주세요', errors: errors.array() }); } const title = req.body.title; const content = req.body.content; const post = new Post({ title: title, content: content, // 파일 업로드 전까지 하드코딩 imageUrl: 'images/home4.jpg', creator: { name: 'kooks7' } }); post .save() .then(result => { console.log(result); res.status(201).json({ message: 'Post created successfully!', post: result }); }) .catch(err => { console.log(err); }); };
-
-
app.js
-
static 폴더 설정하기
const path = require('path'); app.use('/images', express.static());
-
-
controllers/feed.js
exports.createPost = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { const error = new Error('올바른 값을 입력해주세요'); error.statusCode = 422; throw error; } const title = req.body.title; const content = req.body.content; const post = new Post({ title: title, content: content, imageUrl: 'images/home4.jpg', creator: { name: 'kooks7' } }); post .save() .then(result => { console.log(result); res.status(201).json({ message: 'Post created successfully!', post: result }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); };
-
app.js
-
에러 핸들링 라우터 만들기
app.use((error, req, res, next) => { console.log(error); const status = error.statusCode || 500; const message = error.message; res.status(status).json({ message: message }); });
-
-
routes/feed.js
-
getPostrouter 만들기router.get('/post/:postId', feedController.getPost);
-
-
controllers/feed.js
-
getPostcontrollers 만들기exports.getPost = (req, res, next) => { const postId = req.params.postId; Post.findById(postId) .then(post => { if (!post) { const error = new Error('Could not find post.'); error.statusCode = 404; throw error; } res.status(200).json({ message: 'Post fetched', post: post }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); };
-
기존 파일 가져오는 로직 수정하기 (
getPosts)exports.getPosts = (req, res, next) => { Post.find() .then(posts => { res .status(200) .json({ message: 'Fetched posts successfully.', posts: posts }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } }); };
-
-
프론트페이지 수정하기 src/pages/Feed/SinglePost/SinglePost.js
-
componentDidMount()componentDidMount() { const postId = this.props.match.params.postId; fetch('http://localhost:8080/feed/post/' + postId) .then(res => { if (res.status !== 200) { throw new Error('Failed to fetch status'); } return res.json(); }) .then(resData => { this.setState({ title: resData.post.title, author: resData.post.creator.name, image: 'http://localhost:8080/' + resData.post.imageUrl, date: new Date(resData.post.createdAt).toLocaleDateString('en-US'), content: resData.post.content }); }) .catch(err => { console.log(err); }); }
-
windows 운영체제에서 서버를 실행하면 경로처리를 다르게 해줘야 한다.
-
파일 업로드시 발생하는 CORS 오류 해결하기 위해서
uuid설치하기$npm i --save uuid -
multer 설치하기
$npm i --save multer -
app.js
-
multer 로직 구성하기
const multer = require('multer'); const uuidv4 = require('uuid/v4'); const fileStorage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, 'images'); }, filename: (req, file, cb) => { cb(null, uuidv4()); } }); const fileFilter = (req, file, cb) => { if ( file.mimetype === 'image/png' || file.mimetype === 'image/jpg' || file.mimetype === 'image/jpeg' ) { cb(null, true); } else { cb(null, false); } }; app.use(multer({storage: fileStorage, fileFilter: fileFilter}).single('image'))
-
-
controllers/feed.js
-
createPost에 multer로 파일 업로드 기능 넣기exports.createPost = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { const error = new Error('올바른 값을 입력해주세요'); error.statusCode = 422; throw error; } if (!req.file) { const error = new Error('No image provided.'); error.statusCode = 422; throw error; } const imageUrl = req.file.path.replace('\\', '/'); const title = req.body.title; const content = req.body.content; const post = new Post({ title: title, content: content, imageUrl: imageUrl, creator: { name: 'kooks7' } }); post .save() .then(result => { console.log(result); res.status(201).json({ message: 'Post created successfully!', post: result }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); };
-
-
Front page : src/pages/Feed/SinglePost/Feed.js
-
프론트 페이지에
formData넣기finishEditHandler = postData => { this.setState({ editLoading: true }); // const formData = new FormData(); formData.append('title', postData.title); formData.append('content', postData.content); formData.append('image', postData.image); let url = 'http://localhost:8080/feed/post'; let method = 'POST'; if (this.state.editPost) { url = 'URL'; } fetch(url, { method: method, body: formData }) .then(res => { if (res.status !== 200 && res.status !== 201) { throw new Error('Creating or editing a post failed!'); } return res.json(); }) .then(resData => { console.log(resData); const post = { _id: resData.post._id, title: resData.post.title, content: resData.post.content, creator: resData.post.creator, createdAt: resData.post.createdAt }; this.setState(prevState => { let updatedPosts = [...prevState.posts]; if (prevState.editPost) { const postIndex = prevState.posts.findIndex( p => p._id === prevState.editPost._id ); updatedPosts[postIndex] = post; } else if (prevState.posts.length < 2) { updatedPosts = prevState.posts.concat(post); } return { posts: updatedPosts, isEditing: false, editPost: null, editLoading: false }; }); }) .catch(err => { console.log(err); this.setState({ isEditing: false, editPost: null, editLoading: false, error: err }); }); };
-
-
routes/feed.js
-
put 메소드 사용하기
router.put('/post/:postId')
-
-
controllers/feed.js
exports.updatePost = (req, res, next) => { const postId = req.params.postId; const title = req.body.title; const content = req.body.content; let imageUrl = req.body.image; if (req.file) { imageUrl = req.file.path; } if (!imageUrl) { const error = new Error('No file picked.'); error.statusCode = 422; throw error; } };
-
Front page : src/pages/Feed/SinglePost/Feed.js
-
프론트페이지 로직 구성하기
fetch('http://localhost:8080/feed/posts') .then(res => { if (res.status !== 200) { throw new Error('Failed to fetch posts.'); } return res.json(); }) .then(resData => { this.setState({ posts: resData.posts.map(post => { return { ...post, imagePath: post.imageUrl }; }), totalPosts: resData.totalItems, postsLoading: false }); }) .catch(this.catchError); };
-
-
routes/feed.js
-
validator 설정하고
updatePost가져오기router.put( '/post/:postId', [ body('title') .trim() .isLength({ min: 5 }), body('content') .trim() .isLength({ min: 5 }) ], feedController.updatePost );
-
-
controllers/feed.js
-
updatecontroller 작성하기exports.updatePost = (req, res, next) => { const postId = req.params.postId; const errors = validationResult(req); if (!errors.isEmpty()) { const error = new Error('올바른 값을 입력해주세요'); error.statusCode = 422; throw error; } const title = req.body.title; const content = req.body.content; let imageUrl = req.body.image; if (req.file) { imageUrl = req.file.path; } if (!imageUrl) { const error = new Error('No file picked.'); error.statusCode = 422; throw error; } Post.find(postId) .then(post => { if (!post) { const error = new Error('Could not find post.'); error.statusCode = 404; throw err; } // 기존 이미지 지우기 if (imageUrl !== post.imageUrl) { clearImage(post.imageUrl); } post.title = title; post.imageUrl = imageUrl; post.content = content; return post.save(); }) .then(result => { res.status(200).json({ message: 'Post updated!', post: result }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); }; // 파일 path 받아서 해당 파일 삭제하는 Helper Function const clearImage = filePath => { filePath = path.join(__dirname, '..', filePath); fs.unlink(filePath, err => console.log(err)); };
-
-
Front : src/pages/Feed/Feed.js
-
finishEditHandler작성하기finishEditHandler = postData => { this.setState({ editLoading: true }); const formData = new FormData(); formData.append('title', postData.title); formData.append('content', postData.content); formData.append('image', postData.image); let url = 'http://localhost:8080/feed/post'; let method = 'POST'; if (this.state.editPost) { url = 'http://localhost:8080/feed/post/' + this.state.editPost._id; method = 'PUT'; }
-
-
CORS 해결하기
-
cors 모듈 사용하기 기존 방법으로 사용해도 put 메소드로 요청을 보낼 때 cors 오류가 떠서 cors 모듈을 사용
-
$ npm i --save cors -
app.js 모든 라우터에 cors 적용하기
const cors = require('cors'); app.use(cors());
-
-
-
controllers/feed.js
-
controller 작성하기 로직은 post가 있는지 찾는다. => 있으면 저장된 사진을 삭제한다. => 해당 포스터를 DB에서 삭제한다.
exports.deletePost = (req, res, next) => { const postId = req.params.postId; Post.findById(postId) .then(post => { if (!post) { const error = new Error('Could not find post.'); error.statusCode = 404; throw err; } // Check logged in user clearImage(post.imageUrl); return Post.findByIdAndRemove(postId); }) .then(result => { console.log(result); res.status(200).json({ message: 'Delete Post.' }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); };
-
-
routes/feed.js 라우터 만들기
router.delete('/post/:postId', feedController.deletePost);
-
Front: ...../Feed.js 프론트 페이지 수정
deletePostHandler = postId => { this.setState({ postsLoading: true }); fetch('http://localhost:8080/feed/post/' + postId, { method: 'DELETE' }) .then(res => { if (res.status !== 200 && res.status !== 201) { throw new Error('Deleting a post failed!'); } return res.json(); }) .then(resData => { console.log(resData); this.setState(prevState => { const updatedPosts = prevState.posts.filter(p => p._id !== postId); return { posts: updatedPosts, postsLoading: false }; }); }) .catch(err => { console.log(err); this.setState({ postsLoading: false }); }); };
-
React Front Page : .../Feed.js
-
쿼리 파라미터로 페이지 나타내기
loadPosts = direction => { if (direction) { this.setState({ postsLoading: true, posts: [] }); } let page = this.state.postPage; if (direction === 'next') { page++; this.setState({ postPage: page }); } if (direction === 'previous') { page--; this.setState({ postPage: page }); } fetch('http://localhost:8080/feed/posts?page=' + page) .then(res => { if (res.status !== 200) { throw new Error('Failed to fetch posts.'); } return res.json(); }) .then(resData => { this.setState({ posts: resData.posts.map(post => { return { ...post, imagePath: post.imageUrl }; }), totalPosts: resData.totalItems, postsLoading: false }); }) .catch(this.catchError); };
-
-
controllers/feed.js
-
getPosts22강에서 했던 pagination 과 동일한 로직으로 페이지 구성하기exports.getPosts = (req, res, next) => { const currentPage = req.query.page || 1; const perPage = 2; let totalItems; // 총 item 갯수 세기 Post.find() .countDocuments() .then(count => { totalItems = count; return Post.find() .skip((currentPage - 1) * perPage) .limit(perPage); }) .then(posts => { res.status(200).json({ message: 'Fetched posts successfully.', posts: posts, totalItems: totalItems }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } }); };
-
-
models/user.js
-
user 모델 구성하기
const mongoose = require('mongoose'); const Schema = mongoose.Schema; const userSchecma = new Schema({ email: { type: String, required: true }, password: { type: String, required: true }, name: { type: String, required: true }, status: { type: String, default: 'I am new!' }, post: [ { type: Schema.Types.ObjectId, ref: 'Post' } ] }); module.exports = mongoose.model('User', userSchecma);
-
-
routes/auth.js
-
인증을 처리할 라우터 생성하기 express-validator 적용하기
const express = require('express'); const { body } = require('express-validator/check'); const User = require('../models/user'); const authController = require('../controllers/auth'); const router = express.Router(); router.put( '/signup', [ body('email') .isEmail() .withMessage('올바른 이메일을 입력해주세요.') // E-mail 이미 존재하는지 체크 하는 로직 .custom(value => { return User.findOne({ email: value }).then(userDoc => { if (userDoc) { return Promise.reject('이메일이 이미 존재합니다.'); } }); }) .normalizeEmail(), body('password') .trim() .isLength({ min: 5 }), body('name') .trim() .not() .isEmpty() ], authController.signup ); module.exports = router;
-
app.js에 설정
const authRoutes = require('./routes/auth'); app.use('/auth', authRoutes);
-
-
controllers/auth.js
-
signup router 만들기
const { validationResult } = requrie('express-validator/check'); const User = require('../models/user'); exports.signup = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { const error = new Error('Validation failed.'); error.statusCode = 422; error.data = errors.array(); throw error; } const email = req.body.email; const name = req.body.namel; const password = req.body.password; //... encrypt으로 password 로직 짜기 };
-
-
password 암호화 해서 저장하기
-
$npm i --save bcryptjs -
controllers/auth.js에 bcrypt 가져오기
const { validationResult } = require('express-validator/check'); const bcrypt = require('bcryptjs'); const User = require('../models/user'); exports.signup = (req, res, next) => { const errors = validationResult(req); ... const password = req.body.password; bcrypt .hash(password, 12) .then(hashedPw => { const user = new User({ email: email, password: hashedPw, name: name }); return user.save(); }) .then(result => { res .status(201) .json({ message: '유저가 생성되었습니다.', userId: result._id }); }) .catch(err => { if (!err.statusCode) { err.statusCode = 500; } next(err); }); };
-
-
React Front Page : app.js
-
signupHandler에서 보내줄 데이터 사용자한테 받고 서버로 요청 보내기signupHandler = (event, authData) => { event.preventDefault(); this.setState({ authLoading: true }); fetch('http://localhost:8080/auth/signup', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: authData.signupForm.email.value, password: authData.signupForm.password.value, name: authData.signupForm.name.value }) }) .then(res => { if (res.status === 422) { throw new Error( "Validation failed. Make sure the email address isn't used yet!" ); } if (res.status !== 200 && res.status !== 201) { console.log('Error!'); throw new Error('Creating a user failed!'); } return res.json(); }) .then(resData => { console.log(resData); this.setState({ isAuth: false, authLoading: false }); this.props.history.replace('/'); }) .catch(err => { console.log(err); this.setState({ isAuth: false, authLoading: false, error: err }); }); };
-