2019. 9. 17. 10:20

1. 기본개념

1-1. 쿠키, 세션

- 쿠키

'키-값'의 쌍의 형식으로 웹사이트에 접속할 때 자동적으로 만들어지는 임시 파일.

1. 서버는 요청에 대한 응답으로 유저가 본 내용, 세션아이디(ID),  IP 주소 등의 정보를 쿠키에 담아 클라이언트에게 보냄

2. 서버로 부터 쿠키가 오면 웹 브라우저는 쿠키를 저장하고 클라이언트가 요청할 때마다 쿠키를 동봉해서 보냄

3. 서버는 요청에 들어있는 쿠키를 읽어서 사용자가 누구인지 파악

 

- 세션

방문자가 웹 브라우저를 통해 웹 서버에 접속한 시점으로부터 웹 브라우저를 종료함으로써 연결을 끝내는 시점동안
같은 사용자(정확하게는 브라우저)로 부터 들어오는 
일련의 요구를 하나의 상태로 보고 그 상태를 일정하게 유지시키는 기술.

즉, 방문자가 웹서버에 접속해 있는 상태를 하나의 단위로 보고 세션이라고 칭함.

 

- 세션 기반 인증 시스템

: 서버가 세션저장소(서버의 메모리, 디스크, 데이터베이스)에 사용자 정보를 저장함으로써 사용자가 로그인 중임을 기억하는 방식.

1. 유저가 처음 로그인하면 서버는 세션 저장소에 사용자의 정보를 조회하고 response-header field인 set-cookie 값으로 클라이언트 식별자인 session-id(임의의 긴 문자열)를 발행

2. 발행된 session-id는 세션 저장소와 쿠키에 저장

3. 이후 유저가 다른 요청을 보낼 때마다 쿠키에 담긴  session-id를 가지고 서버의 세션 저장소에서 세션을 조회한후 로그인 여부를 결정하여 작업을 처리하고 응답함

 

- 세션기반인증시스템 단점

1. 서버를 확장하기가 번거로워짐

: 서버의 인스턴스가 여러개가 되면, 모든 서버끼리 같은 세션을 공유해야 하므로 세션 전용 DB를 만들어야 하고 신경 쓸 것이 많아짐

 

1-2. 토큰

- 토큰 

: 로그인 이후 서버가 만들어 주는 문자열. 

문자열 안에는 사용자의 로그인 정보와 토큰이 서버에서 발급 되었음을 증명하는 서명이 들어있음.

서명 데이터는 해싱 알고리즘으로 만들어지고 서명이 있음으로 토큰의 무결성(정보가 변경되거나 위조되지 않음)을 보장.

 

- JWT

: Json Web Token, 데이터가 JSON형식으로 이루어진 토큰

 

- 토큰 기반 인증 시스템

1. 클라이언트가 로그인을 하면 서버에서 클라이언트에게 해당 사용자의 정보를 지니고 있는 토큰을 발급받음

2. 추후 클라이언트가 다른 API를 요청할 때 마다 발급받은 토큰을 포함

3. 서버는 토큰이 유효한지 검사하고 결과에 따라 작업을 처리하고 응답

4. 기한이 만료되면 토큰을 지우고 재로그인 하게 함

 

- 토큰 기반 인증 시스템 장점

클라이언트가 로그인 상태를 지닌 토큰을 가지고 있으므로

1. 서버에서 사용자 로그인 정보를 기억하기 위해 사용하는 리소스가 적다

2. 서버의 인스턴스가 늘어나도 서버끼리 사용자의 로그인 상태를 공유할 필요가 없으므로 서버의 확장성이 높음

 

-토큰 기반 인증 시스템 단점

1. 토큰 수명이 짧으면 토큰기한이 만료될 때마다 다시 로그인 해야됨

2. 토큰 수명이 길면 클라이언트의 토큰이 해독되어 악용되는 보안의 문제

1-3. 세션기반인증시스템과 토큰기반인증시스템 공통점과 차이점

- 공통점

1. 클라이언트가 고유 식별자(세션아이디, 토큰)을 갖고 그 식별자를 바탕으로 인증을 처리

 

- 차이점

1. 세션기반인증시스템에서는 식별자에 대한 정보를 서버의 세션저장소에 저장

 

2. User 스키마/모델

2-1. 모델

 

 

2-2

 

Posted by yongminLEE
2019. 9. 11. 20:49

- async 함수에서 발생한 오류는 async 함수안에서 호출 된 또다른 asynce 함수의 try/catch로 예외처리가 가능

-> wrapping함수 구현

const wrapAsync = asyncFn => {
    if (asyncFn.length <= 3) {
        return (async (req, res, next) => {
            try {
                return await asyncFn(req, res, next)
            } catch (error) {
                return next(error)
            }
        })
    }
    else {
        return (async (err, req, res, next) => {
            try {
                return await asyncFn(err, req, res, next)
            } catch (error) {
                return next(error)
            }
        })
    }; 
};

module.exports = wrapAsync;

try-catch를 이용해 상위 래퍼 메소드에서 에러 핸들링을 해주기 때문에 모든 메소드에 try-catch를 사용할 필요x

- async — await가 필요한 부분에만 wrapwrapAsync 메소드를 사용

 

-사용예시

//routing middleware
const fakeWait = () => new Promise((resolve) => setTimeout(resolve, 100));
 
async function asyncRoute(req, res) {
  await fakeWait();
  res.send({ ok: true });
}
router.get('/async', wrapAsync(asyncRoute));

//error handling
app.use((err, req, res, next) => {
  res.status(500);
  res.json({
    error: err.message,
  });
});

 

 

참고

https://www.npmjs.com/package/express-wrap-async

https://medium.com/@changjoopark/express-%EB%9D%BC%EC%9A%B0%ED%8A%B8%EC%97%90%EC%84%9C-async-await%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%A0%A4%EB%A9%B4-7e8ffe0fcc84

Posted by yongminLEE
2019. 9. 5. 10:59

1. mongodb , mongoose 모듈

1-1. mongodb

- Nosql 특성상 문서 객체 안에 들어간느 속성을 마음대로 바꿀 수 있으므로 매우 유연하게 데이터를 저장할 수 있다. 

- 하지만 컬렉션 안에 들어 있는 여러개의 객체를 조회할 때는 제약이 생길 수도 있음

ex) 같은 user 컬렉션 안에 들어 있는 문서 객체라도 어떤 문서객체는 age속성이 있는데, 다른 문서 객체에는 age속성이 없을 수 있음

-> 조회 조건을 공통 적용하기 어려움

 

1-2. mongoose

MongoDB 기반 ODM (object data modelling) 라이브러리 

- 스키마를 만들어, 이에 맞는정 자바스크립트 객체 그대로 db에 저장하거나 db의 문서 객체를 그대로 자바스크립트 객체로 바꿀 수 있게 함

 

2. mongoose 적용 

 

2-1. .env환경변수파일 생성

//.env
PORT=3000
MONGO_URI=mongodb://localhost:27017/blog

- MongoDB에 접속할 때, 서버주소, 포트번호, 계정, 비밀번호들의 민감하고 환경별로 달라지는 값은 코드에 작성하지 않고 환경변수로 설정하여 .env 환경변수 파일에 저장

- github등의 서비스에 올릴 때는 .gitignore를 작성하여 환경변수 파일 제외

- blog는 프로젝트에서 사용할 데이터베이스 이름

 

yarn add dotenv

 

//src/index.js
require('dotenv').config();

//...

const { PORT, MONGO_URI} = process.env;

//...

//웹 서버의 port속성 설정
http.createServer(app).listen(PORT, () => {
    console.log('express server starts : ' + PORT);
});

- dotenv는 환경변수들을 파일에 넣고 사용할 수 있게함

- require('dotenv').config() 호출 후 환경변수는 process.env값을 통해 조회 가능

 

2-2. mongoose로 서버와 데이터베이스 연결

//src/index.js

//...
const { PORT, MONGO_URI} = process.env;

mongoose.connect(MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true }).then(() => {
    console.log('connected to mongodb');
}).catch((e) => {
    console.error(e);
});

//...

 

2-3. import/export구문과 Node.js

- 리액트 프로젝트에서 쓰던 ES모듈 import/export 문법은 Node.js에서 정식으로 지원되지 않음

- require(), exports, module.exports 문법 사용

- Node.js v12부터는 정식 지원

 

3. 스키마와 모델

3.1 스키마

- 컬렉션에 들어가는 문서 내부의 각 필드가 어떤 형식으로 되어 있는지 정의하는 객체

//src/models/post.js
const mongoose = require('mongoose');

//스키마 :컬렉션에 들어가는 문서 내부의 각 필드가 어떤 형식으로 되어 있는지 정의하는 객체
// 스키마 생성 
const {Schema} = mongoose;
const Post = new Schema({
  title: String,
  body:String,
  tags:[String], // 문자열 배열
  publishedDate:{
      type:Date,
      default:new Date() // 현재 날짜를 기본 갑 으로 설정
  }
});

 

3.2 모델

- 스키마를 사용하여 만드는 인스턴스

- 데이터베이스에서 실제 작업을 처리할 수 있는 함수들을 지니고 있는 객체

//src/modes/post.js
const mongoose = require('mongoose');

//스키마 :컬렉션에 들어가는 문서 내부의 각 필드가 어떤 형식으로 되어 있는지 정의하는 객체
// 스키마 생성 
const {Schema} = mongoose;
const Post = new Schema({
  title: String,
  body:String,
  tags:[String], // 문자열 배열
  publishedDate:{
      type:Date,
      default:new Date() // 현재 날짜를 기본 갑 으로 설정
  }
});

//모델 : 스키마를 사용하여 만든 인스턴스
//모델 생성 
module.exports = mongoose.model('Post', Post);

- mongoose.model()의 첫번째 파라미터 : 스키마 이름 -> 데이터베이스는 스키마 이름의 소문자, 복수형태로 데이터베이스에 컬련섹 생성

- mongoose.model()의 두번째 파라미터 : 스키마 객체

 

 

4. 데이터 생성, 조회, 삭제, 수정

4-1. 컨트롤러 수정

//src/api/posts/posts.ctrl.js
const Post = require('models/post');

//wrapAsync 메소드는 앞으로 async-await가 필요한 라우트 핸들러에 사용.
//try-catch를 이용해 상위 래퍼 메소드에서 에러 핸들링을 해주기 때문에 
//모든 메소드에 try-catch를 사용할 필요가 없어진다.
const wrapAsync = require('lib/wrapAsync');

express 에서 async/await 적용 : https://yongmin0000.tistory.com/category/Node.js/express

 

4-2. 데이터 생성

//src/api/posts/posts.ctrl.js
const Post = require('models/post');
const wrapAsync = require('lib/wrapAsync');

//데이터생성
//POST /api/posts
exports.write = wrapAsync(async (req, res, next) => {

    const { title, body, tags } = req.body;
    const post = new Post({
        title, body, tags
    });
    await post.save();  // 데이터베이스등록
    res.send(post);     //저장된 결과 반환
});

4-3. 데이터 조회

//src/api/posts/posts.ctrl.js

// ....

// 데이터 조회
//GET /api/posts
exports.list = wrapAsync(async (req, res, next) => {

    const posts = await Post.find().exec();
    
    res.send(posts);
});

4-4. 특정 데이터 조회

//src/api/posts/posts.ctrl.js

// ....

//특정포스트 조회 
//GET /api/posts/:id 
exports.read = wrapAsync(async (req, res, next) => {
    const { id } = req.params;
    const post = await Post.findById(id).exec();
    //특정포스트 존재x
    if (!post) {
        res.status(404).send("<h1>404 ERROR : 데이터 조회 실패, 해당 id의 데이터 존재 x</h1>");
        return;
    }
    res.send(post);
});

 

4-5. 데이터 삭제

//src/api/posts/posts.ctrl.js

// ....

//데이터삭제
// DELETE /api/posts/:id
exports.remove = wrapAsync(async (req, res) => {
    const { id } = req.params;
    await Post.findByIdAndRemove(id).exec();
    console.log("DELETE 성공");
    res.status(204).send("<h1>삭제 성공</h1>");
});

 

4-6. 데이터 수정

//src/api/posts/posts.ctrl.js

// ....

//데이터 수정
//PATCH /api/posts/:id
exports.update = wrapAsync(async (req, res) => {
    const { id } = req.params;
    const post = await Post.findByIdAndUpdate(id, req.body, {
        new: true   //이 갑을 설정해야 업데이트 된 객체 반환, 설정안하면 업데이트 전 객체 반환
    }).exec();
    if (!post) {
        res.status(404).send("<h1>404 ERROR : 데이터수정실패, 해당 id의 데이터 존재x</h1>")
        return;
    }
    res.send(post);
}); 

 

5. 요청 검증

5.1 ObjectId

- id가 올바른 ObjectId형식인지 아닌지 검증함으로써 형식이 올바르지 않을 때 에러를 서버에서 처리할 수 있도록 한다

//src/api/posts/posts.ctrl.js

//...

//ObjectId 검증
const { ObjectId } = require('mongoose').Types;
exports.checkObjectId = (req, res, next) => {
    const { id } = req.params;

    //검증실패
    if (!ObjectId.isValid(id)) {
        console.log("ObjectId 실패")
        res.status(400).send("400 error : ObjectId 검증 실패");
        return null;
    }
    return next();
}

 

//src//api/posts/index.js
//POSTS API
const express = require('express');
const posts = express.Router();
const postsCtrl = require('./posts.ctrl');

posts.get('/', postsCtrl.list);
posts.post('/', postsCtrl.write);
posts.get('/:id', postsCtrl.checkObjectId, postsCtrl.read);
posts.delete('/:id', postsCtrl.checkObjectId, postsCtrl.remove);
posts.patch('/:id', postsCtrl.checkObjectId, postsCtrl.update);

module.exports = posts;

 

5.2 Request Body

- write, update API에서 전달받은 요청 내용에 title, body, tags값이 모두 들어있는지 검증

- 객체 검증 라이브러리 @hapi/joi 이용

yarn add @hapi/joi

 

//src/api/posts/posts.ctrl.js

const Post = require('models/post');
const wrapAsync = require('lib/wrapAsync');
const Joi = require('@hapi/joi');

//Request body 검증
exports.checkRequestBody = (req,res,next) => {

    //객체가 지닌 갑들을 검증
    const schema = Joi.object({
        title: Joi.string().required(),
        body: Joi.string().required(),
        tags:Joi.array().items(Joi.string()).required()
    });

    const result = schema.validate(req.body);
    //오류가 발생하면 오류내용응답
    if (result.error) {
        console.log("checkRequestBody 실패")
        res.status(400).send(result.error);
        return;
    }
    return next();
}

 

//src//api/posts/index.js
//POSTS API
const express = require('express');
const posts = express.Router();
const postsCtrl = require('./posts.ctrl');

posts.get('/', postsCtrl.list);
posts.post('/',postsCtrl.checkRequestBody, postsCtrl.write);
posts.get('/:id', postsCtrl.checkObjectId, postsCtrl.read);
posts.delete('/:id', postsCtrl.checkObjectId, postsCtrl.remove);
posts.patch('/:id', postsCtrl.checkObjectId, postsCtrl.update);

module.exports = posts;

 

6. Pagination

6-1. 포스트 역순으로 불러오기 

- 최근 작성된 포스트를 먼저 보여주기

//src/api/posts/posts.ctrl.js

// 데이터 조회
//GET /api/posts
exports.list = wrapAsync(async (req, res, next) => {

    const posts = await Post.find()
    .sort({_id:-1})     //포스트 역순으로 불러오기
    .exec();
    
    res.send(posts);
});

 

6-2. 보이는 개수 제한

//src/api/posts/posts.ctrl.js

// 데이터 조회
//GET /api/posts
exports.list = wrapAsync(async (req, res, next) => {


    const posts = await Post.find()
    .sort({_id:-1})     //포스트 역순으로 불러오기
    .limit(10)          //보이는 개수 제한
    .exec();
    
    
    res.send(posts);
});

 

6-3. 페이지 기능 구현

//src/api/posts/posts.ctrl.js

// 데이터 조회
//GET /api/posts
exports.list = wrapAsync(async (req, res, next) => {
    // 페이지 기능
    const page = parseInt(req.query.page||1,10); //10 진수
    if(page<1){                                 //잘못된 페이지 주어지면 오류
        res.status(400).send("<h1>wrong page</h1>");
        return;
    }

    const posts = await Post.find()
    .sort({_id:-1})     //포스트 역순으로 불러오기
    .limit(10)          //보이는 개수 제한
    .skip((page-1)*10)  //파라미터만큼의 데이터를 맨처음에서 제외하고 그 다음 데이터 불러옴
    .exec();
    
    res.send(posts);
});

 

6-4. 마지막 페이지 번호 알려 주기

//src/api/posts/posts.ctrl.js

// 데이터 조회
//GET /api/posts
exports.list = wrapAsync(async (req, res, next) => {
    // 페이지 기능
    const page = parseInt(req.query.page||1,10); //10 진수
    if(page<1){                                 //잘못된 페이지 주어지면 오류
        res.status(400).send("<h1>wrong page</h1>");
        return;
    }

    const posts = await Post.find()
    .sort({_id:-1})     //포스트 역순으로 불러오기
    .limit(10)          //보이는 개수 제한
    .skip((page-1)*10)  //파라미터만큼의 데이터를 맨처음에서 제외하고 그 다음 데이터 불러옴
    .exec();
    
    //마지막 페이지 번호 알려주기
    const postCount = await Post.countDocuments().exec();
    res.set('Last-Page',Math.ceil(postCount/10)); //응답 헤더에 Last-Page라는 커스텀http헤더 설정

    res.send(posts);
});

 

6-5. 내용 길이 제한

//src/api/posts/posts.ctrl.js

// 데이터 조회
//GET /api/posts
exports.list = wrapAsync(async (req, res, next) => {
    // 페이지 기능
    const page = parseInt(req.query.page||1,10); //10 진수
    if(page<1){                                 //잘못된 페이지 주어지면 오류
        res.status(400).send("<h1>wrong page</h1>");
        return;
    }

    const posts = await Post.find()
    .sort({_id:-1})     //포스트 역순으로 불러오기
    .limit(10)          //보이는 개수 제한
    .skip((page-1)*10)  //파라미터만큼의 데이터를 맨처음에서 제외하고 그 다음 데이터 불러옴
    .lean()             //반환형식을 JSON형태로 (내용길이 제한기능을 위해)
    .exec();
    
    //마지막 페이지 번호 알려주기
    const postCount = await Post.countDocuments().exec();
    res.set('Last-Page',Math.ceil(postCount/10)); //응답 헤더에 Last-Page라는 커스텀http헤더 설정

    // 내용길이 제한
    const limitBodyLength = (post)=>({
        ...post,
        body:post.body.length <160 ? post.body : `${post.body.slice(0,160)}...`
    });
    res.send(posts.map(limitBodyLength));
});
Posted by yongminLEE
2019. 9. 5. 10:58

1. 요청과 응답

- 클라이언트에서 서버로 request를 보내고, 서버에서는 request내용을 읽고 처리한뒤 클라이언트에게 response를 보낸다.

- 클라이언트로부터 요청이 왔을 때 어떤 작업을 수행할지 listen 메서드에 콜백함수를 넣거나, 서버에 listening 이벤트 리스너를 추가한다.

 

2. 쿠키와 세션

- 쿠키 : '키-값'의 쌍, reques와 response의 header를 통해 오고 감

- 서버는 요청에 대한 응답을 할 때 쿠키라는 것을 클라이언트에게 보냄 -> 서버로 부터 쿠키가 오면 '웹 브라우저'는 쿠키를 저장해 두었다가 요청할 때마다 쿠키를 동봉해서 보냄 -> 서버는 요청에 들어있는 쿠키를 읽어서 사용자가 누구인지 파악

- 브라우저는 쿠키가 있으면 자동으로 동봉해서 보내므로 따로 처리할 필요 없음, 서버에서 브라우저로 쿠키를 보낼 때만 코드 작성

 

- 세션 : 서버에 사용자 정보를 저장하고 클라이언트와는 세션 아이디로만 소통하는 방식

- 세션 아이디는 꼭 쿠키를 사용해서 주고 받을 필요는 없지만, 대부분의 서비스는 쿠키를 사용

 

 

3. REST API와 라우팅

 

 REST (Representational State Transfer) 정의

1. 자원을 이름(자원의 표현)으로 구분하여 해당 자원의 상태(정보)를 주고 받는 모든 것을 의미한다. -> 즉, 자원(resource)의 표현(representation) 에 의한 상태 전달

- 자원: 해당 소프트웨어가 관리하는 모든 것 -> Ex) 문서, 그림, 데이터, 해당 소프트웨어 자체 등
- 자원의 표현: 그 자원을 표현하기 위한 이름 -> Ex) DB의 학생 정보가 자원일 때, ‘students’를 자원의 표현으로 정한다.
- state(상태) 전달 : 데이터가 요청되어지는 시점에서 자원의 상태(정보)를 전달한다. -> JSON 혹은 XML를 통해 데이터를 주고 받는 것이 일반적

 

2. 월드 와이드 웹(www)과 같은 분산 하이퍼미디어 시스템을 위한 소프트웨어 개발 아키텍처의 한 형식
- REST는 기본적으로 웹의 기존 기술과 HTTP 프로토콜을 그대로 활용하기 때문에 웹의 장점을 최대한 활용할 수 있는 아키텍처 스타일이다.
- REST는 네트워크 상에서 Client와 Server 사이의 통신 방식 중 하나이다.


REST의 구체적인 개념

- HTTP URI(Uniform Resource Identifier)를 통해 자원(Resource)을 명시하고, HTTP Method(POST, GET, PUT, DELETE, PATCH)를 통해 해당 자원에 대한 CRUD Operation을 적용하는 것을 의미
-> 즉, REST는 자원 기반의 구조 (ROA, Resource Oriented Architecture) 설계의 중심에 Resource가 있고 HTTP Method를 통해 Resource를 처리하도록 설계된 아키텍쳐를 의미한다.

- 웹 사이트의 이미지, 텍스트, DB 내용 등의 모든 자원에 고유한 ID인 HTTP URI를 부여한다.
- CRUD Operation
1. Create : 생성(POST)
2. Read : 조회(GET)
3. Update : 수정(PUT)
4. Delete : 삭제(DELETE)
HEAD: header 정보 조회(HEAD)


REST의 장단점

- 장점
1. HTTP 프로토콜의 인프라를 그대로 사용하므로 REST API 사용을 위한 별도의 인프라를 구출할 필요가 없다.
2. HTTP 프로토콜의 표준을 최대한 활용하여 여러 추가적인 장점을 함께 가져갈 수 있게 해준다.
3. HTTP 표준 프로토콜에 따르는 모든 플랫폼에서 사용이 가능하다.
4. Hypermedia API의 기본을 충실히 지키면서 범용성을 보장한다.
5. REST API 메시지가 의도하는 바를 명확하게 나타내므로 의도하는 바를 쉽게 파악할 수 있다.
6. 여러가지 서비스 디자인에서 생길 수 있는 문제를 최소화한다.
7. 서버와 클라이언트의 역할을 명확하게 분리한다.


- 단점
1. 표준이 존재하지 않는다.
2. HTTP Method 형태가 제한적이다.
3. 브라우저를 통해 테스트할 일이 많은 서비스라면 쉽게 고칠 수 있는 URL보다 Header 값이 왠지 더 어렵게 느껴진다.
4. 구형 브라우저가 아직 제대로 지원해주지 못하는 부분이 존재한다. -> PUT, DELETE 사용불가, pushState 지원x


REST가 필요한 이유

1.‘애플리케이션 분리 및 통합’
2. ‘다양한 클라이언트의 등장’
3. 최근의 서버 프로그램은 다양한 브라우저와 안드로이폰, 아이폰과 같은 모바일 디바이스에서도 통신을 할 수 있어야 한다.
4. 이러한 멀티 플랫폼에 대한 지원을 위해 서비스 자원에 대한 아키텍처를 세우고 이용하는 방법을 모색한 결과, REST에 관심을 가지게 되었다.


REST 구성 요소

1. 자원(Resource): URI
  - 모든 자원에 고유한 ID가 존재하고, 이 자원은 Server에 존재한다.
  - 자원을 구별하는 ID는 ‘/groups/:group_id’와 같은 HTTP URI 다.
  - Client는 URI를 이용해서 자원을 지정하고 해당 자원의 상태(정보)에 대한 조작을 Server에 요청한다.
2. 행위(Verb): HTTP Method
  - HTTP 프로토콜의 Method를 사용한다.
  - HTTP 프로토콜은 GET, POST, PUT, DELETE 와 같은 메서드를 제공한다.
3. 표현(Representation of Resource)
  - Client가 자원의 상태(정보)에 대한 조작을 요청하면 Server는 이에 적절한 응답(Representation)을 보낸다.
  - REST에서 하나의 자원은 JSON, XML, TEXT, RSS 등 여러 형태의 Representation으로 나타내어 질 수 있다.
  - JSON 혹은 XML를 통해 데이터를 주고 받는 것이 일반적이다.


REST 특징

1. Server-Client(서버-클라이언트 구조)
  - 자원이 있는 쪽이 Server, 자원을 요청하는 쪽이 Client가 된다.
  - REST Server: API를 제공하고 비즈니스 로직 처리 및 저장을 책임진다.
  - Client: 사용자 인증이나 context(세션, 로그인 정보) 등을 직접 관리하고 책임진다.
    => 서로 간 의존성이 줄어든다.
2. Stateless(무상태)
  - HTTP 프로토콜은 Stateless Protocol이므로 REST 역시 무상태성을 갖는다.
  - Client의 context를 Server에 저장하지 않는다. -> 세션과 쿠키와 같은 context 정보를 신경쓰지 않아도 되므로 구현 단순
  - Server는 각각의 요청을 완전히 별개의 것으로 인식하고 처리한다. -> 각 API 서버는 Client의 요청만을 단순 처리한다.
  - 즉, 이전 요청이 다음 요청의 처리에 연관되어서는 안된다.
  - 물론 이전 요청이 DB를 수정하여 DB에 의해 바뀌는 것은 허용한다.
  - Server의 처리 방식에 일관성을 부여하고 부담이 줄어들며, 서비스의 자유도가 높아진다.
3. Cacheable(캐시 처리 가능)
  - 웹 표준 HTTP 프로토콜을 그대로 사용하므로 웹에서 사용하는 기존의 인프라를 그대로 활용할 수 있다.
  - 즉, HTTP가 가진 가장 강력한 특징 중 하나인 캐싱 기능을 적용할 수 있다.
  - HTTP 프로토콜 표준에서 사용하는 Last-Modified 태그나 E-Tag를 이용하면 캐싱 구현이 가능하다.
  - 대량의 요청을 효율적으로 처리하기 위해 캐시가 요구된다.
  - 캐시 사용을 통해 응답시간이 빨라지고 REST Server 트랜잭션이 발생하지 않기 때문에 전체 응답시간, 성능, 서버의 자원 이용률을 향상시킬 수 있다.
4. Layered System(계층화)
  - Client는 REST API Server만 호출한다.
  - REST Server는 다중 계층으로 구성될 수 있다.
  - API Server는 순수 비즈니스 로직을 수행하고 그 앞단에 보안, 로드밸런싱, 암호화, 사용자 인증 등을 추가하여 구조상의 유연성을 줄 수 있다.
  - 또한 로드밸런싱, 공유 캐시 등을 통해 확장성과 보안성을 향상시킬 수 있다.
  - PROXY, 게이트웨이 같은 네트워크 기반의 중간 매체를 사용할 수 있다.
5. Code-On-Demand(optional)
  - Server로부터 스크립트를 받아서 Client에서 실행한다.
  - 반드시 충족할 필요는 없다.
6. Uniform Interface(인터페이스 일관성)
  - URI로 지정한 Resource에 대한 조작을 통일되고 한정적인 인터페이스로 수행한다.
  - HTTP 표준 프로토콜에 따르는 모든 플랫폼에서 사용이 가능하다.
  - 특정 언어나 기술에 종속되지 않는다


REST API의 개념

REST API의 정의

1. 네트워크 구조의 한 형식. 서버의 자원을 정의하고, 자원에 대한 주소를 지정하는 방법을 가리킨다.
2. REST 기반으로 서비스 API를 구현한 것

  - 최근 OpenAPI(누구나 사용할 수 있도록 공개된 API: 구글 맵, 공공 데이터 등), 마이크로 서비스(하나의 큰 애플리케이션을 여러 개의 작은 애플리케이션으로 쪼개어 변경과 조합이 가능하도록 만든 아키텍처) 등을 제공하는 업체 대부분은 REST API를 제공한다.

 

REST API의 특징

  - 사내 시스템들도 REST 기반으로 시스템을 분산해 확장성과 재사용성을 높여 유지보수 및 운용을 편리하게 할 수 있다.
  - REST는 HTTP 표준을 기반으로 구현하므로, HTTP를 지원하는 프로그램 언어로 클라이언트, 서버를 구현할 수 있다.
  - 즉, REST API를 제작하면 델파이 클라이언트 뿐 아니라, 자바, C#, 웹 등을 이용해 클라이언트를 제작할 수 있다.


RESTful의 개념

  - RESTful이란?
    : RESTful은 일반적으로 REST라는 아키텍처를 구현하는 웹 서비스를 나타내기 위해 사용되는 용어이다.
  - ‘REST API’를 제공하는 웹 서비스를 ‘RESTful’하다고 할 수 있다.
  - RESTful은 REST를 REST답게 쓰기 위한 방법으로, 누군가가 공식적으로 발표한 것이 아니다.
  - 즉, REST 원리를 따르는 시스템은 RESTful이란 용어로 지칭된다.


RESTful의 목적

  - 이해하기 쉽고 사용하기 쉬운 REST API를 만드는 것
  - RESTful한 API를 구현하는 근본적인 목적이 성능 향상에 있는 것이 아니라 일관적인 컨벤션을 통한 API의 이해도 및 호환성을 높이는 것이 주 동기이니, 성능이 중요한 상황에서는 굳이 RESTful한 API를 구현할 필요는 없다.
  - RESTful 하지 못한 경우
Ex1) CRUD 기능을 모두 POST로만 처리하는 API

Ex2) route에 resource, id 외의 정보가 들어가는 경우(/students/updateName)

 

참고
https://gmlwjd9405.github.io/2018/09/21/rest-and-restful.html

Posted by yongminLEE
2019. 9. 5. 10:58

1. 웹서버 생성

yarn add express

 

//src/index.js
const http = require('http');
const express = require('express');

//익스프레스 서버 객체
const app = express();

//웹 서버의 port속성 설정
app.set('port', 3000);

http.createServer(app).listen(app.get('port'), ()=>{
    console.log('express server starts : ' + app.get('port'));
});

express server object의 메소드

- set(name, value) : 서버 설정을 위한 속성 지정

- get(name) : 지정한 속성 반환

- use([path], function) : 미들웨어함수 등록

- get([path], function) : 특정 패스로 요청된 정보 처리

 

 

2. 미들웨어

2-1. 미들웨어등록

//src/index.js
const http = require('http');
const express = require('express');

//익스프레스 서버 객체
const app = express();

//미들웨어 등록
app.use((req,res,next)=>{
    console.log('first middleware');

    //req객체에 user속성을 추가하고 문자열 할당
    req.user = 'mike';

    //다음 미들웨어 호출
    next();
});
app.use('/',(req,res,next)=>{
    console.log('second middleware');
    res.writeHead('200', {'content-type':'text/html;charset=utf-8'});
    res.write('<p>express server 응답 결과 : ' + req.user + '</p>');
    res.end();
})

//웹 서버의 port속성 설정
app.set('port', 3000);

http.createServer(app).listen(app.get('port'), ()=>{
    console.log('express server starts : ' + app.get('port'));
});

- app.use파라미터로 등록된 함수가 하나의 미들웨어

- 미들웨어 함수는 req, res, next 객체를 파라미터로 받음

- req : 요청객체

- res : 응답객체

- next() : 현재처리중인 미들웨어의 다음 미들웨어를 호출하는 함수

 

2-2. 응답객체 메소드

- writeHead(statusCode, statusMessage, headers)

- write(chunk, encoding, callback) : 여러번 작성가능

- end(data, encoding, callback)

+

express에서 추가로 사용 가능한 메소드

- send() : 클라이언트에 모든 종류의 응답데이터를 보낸다 (html, buffer객체, json객체, json배열 등등 -> 만능메서드)

//app.use('/',(req,res,next)=>{
//   res.writeHead('200', {content-type':'text/html;charset=utf-8'});
//	 res.write("<p>server response</p>");
//	 res.end();
//});

app.use('/',(req,res,next)=>{
    res.send('<p>express server response</p>');
});

- status(code) : http 상태코드 반환 -> send()메소드를 연속으로 호출해야 전송

- sendStatus(code) = status(code).send();

app.use('/',(req,res,next)=>{
    res.status(404).send();
})

//app.use('/',(req,res,next)=>{
//    res.sendStatus(404);
//})

- redirect([status], path) : 웹페이지 경로를 강제로 이동

app.use('/',(req,res,next)=>{
	//디폴트 상태값 : 302
    res.redirect(302,'http://www.naver.com');
});

 

2-3. 요청객체의 속성

- req.query : 클라이언트에서 get방식으로 전송한 데이터(querystring) 확인, url query로 전송한 데이터(querystring) 확인

- req.body : 클라이언트에서 post방식으로 전송한 데이터(querystring) 확인, (body-parser 외장모듈 필요)

- req.params : 라우트 파라미터를 확인할 수 있는 객체, 이 객체의 속성을 "토큰"이라고 한다.

- header(name) : header를 확인

 

2-4. static middleware

: 특정 폴더의 파일들을 특정패스로 접근할 수 있도록 만들어준다.

yarn add server-static

public 디렉토리를 만들고 그 안에 index.html파일을 만든다.

app.use('/public', static("/home/yongmin/Desktop/React/blog/blog-backend/public"));

-> http://localhost:3000/public/index.html로 index.html 접근 가능

 

2-5. body-parser middleware

: 클라이언트가 post방식으로 요청할 때 본문 영역에 들어 있는 요청파라미터(querystring)들을 파싱하여

요청객체(req)의 "body" 속성에 넣어준다. ex) req.body.key

<!-- public/index.htrml -->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>

<body>
    <h1>test page</h1>
    <!-- post방식 -->
    <form method="post">   
        <table>
            <tr>
                <td><label>아이디</label></td>
                <td><input type="text" name="id"></td>
            </tr>
            <tr>
                <td><label>비밀번호</label></td>
                <td><input type="password" name="pw"></td>
            </tr>
        </table>
        <input type="submit" value="submit" name="">
    </form>
</body>

</html>

 

//src/index.js
const http = require('http');
const express = require('express');
const static = require('serve-static');
const path = require('path');
const bodyParser = require('body-parser');

//익스프레스 서버 객체
const app = express();
app.set('port', 3000);

//미들웨어 등록

//body-parser를 사용해 application/x-www-from-urlencoded 파싱
app.use(bodyParser.urlencoded({extended:false}));

//body-parser를 사용해 application/json 파싱
app.use(bodyParser.json());

app.use('/public', static("/home/yongmin/Desktop/React/blog/blog-backend/public"));

app.use((req,res)=>{
    const paramId = req.body.id;
    const paramPw = req.body.pw;

    res.send("id : " + paramId + "<br>" + "pw : " + paramPw);
    // res.writeHead('200', {'content-type':'text/html;charset=utf-8'});
    // res.write("<h1>response</h1>");
    // res.write("<p>id : " + paramId + "</p>");
    // res.write("<p>pw : " + paramPw + "</p>");
    // res.end();
})

//웹 서버의 port속성 설정
http.createServer(app).listen(app.get('port'), ()=>{
    console.log('express server starts : ' + app.get('port'));
});

 

3.  라우팅

: 다른주소로 요청이 들어올 때, 다른 작업을 처리할수 있도록 라우터 사용 -> 사용자가 요청한 기능이 무엇인지 패스를 기준으로 구별

3-1. 라우터 설정 및 등록

<!-- public/index.htrml -->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>

<body>
    <!-- GET방식 -->
    <h1>GET</h1>
    <form method="GET" action="../src/index.js">
        <table>
            <tr>
                <td><label>아이디</label></td>
                <td><input type="text" name="gid"></td>
            </tr>
            <tr>
                <td><label>비밀번호</label></td>
                <td><input type="password" name="gpw"></td>
            </tr>
        </table>
        <input type="submit" value="submit" name="">
    </form>
    <hr>
    <!-- POST방식 -->
    <h1>POST</h1>
    <form method="POST">
        <table>
            <tr>
                <td><label>아이디</label></td>
                <td><input type="text" name="pid"></td>
            </tr>
            <tr>
                <td><label>비밀번호</label></td>
                <td><input type="password" name="ppw"></td>
            </tr>
        </table>
        <input type="submit" value="submit" name="">
    </form>
</body>

</html>

 

// src/index.js
const http = require('http');
const express = require('express');
const bodyParser = require('body-parser');

//익스프레스 서버 객체
const app = express();
//라우터 객체 참조
const router = express.Router();
//포트설정
app.set('port', 3000);

//미들웨어 등
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// static 미들웨어 
app.use('/public', express.static("/home/yongmin/Desktop/React/blog/blog-backend/public"));

//라우팅 함수 설정
router.get('/',(req, res) => {
    console.log("/ get방식으로 요청들어옴");
    res.send("<p>HOME</p>");
});
router.get('/src/index.js',(req, res) => {
    console.log("/src/index.js 에서 get방식으로 요청들어옴");
    const paramgId = req.query.gid;
    const paramgPw = req.query.gpw;
    res.writeHead('200', { 'content-type': 'text/html;charset=utf-8' });
    res.write("<h1>GET</h1>");
    res.write("<p>id : " + paramgId + "</p>");
    res.write("<p>pw : " + paramgPw + "</p>");
    res.end();
});
router.post('/public/index.html',(req, res) => {
    console.log("/public/index.html 에서 post방식으로 요청들어옴");
    const paramId = req.body.pid;
    const paramPw = req.body.ppw;
    res.writeHead('200', { 'content-type': 'text/html;charset=utf-8' });
    res.write("<h1>POST</h1>");
    res.write("<p>id : " + paramId + "</p>");
    res.write("<p>pw : " + paramPw + "</p>");
    res.end();
});

//라우터 등록
app.use('/', router);

//웹 서버의 port속성 설정
http.createServer(app).listen(app.get('port'), () => {
    console.log('express server starts : ' + app.get('port'));
});

- 라우팅 함수 설정 (19~43행)

  - GET 방식으로 request 발생시 req.query 객체에서 속성조회가능

  - POST 방식으로 request 발생시 req.body 객체에서 속성조회가능 (body-parser 미들웨어 필수)

- 라우터 등록 (46행)

 

3-2. 클라이언트->서버로 데이터(querystring) 전송 방법 : URL query와 라우트 파라미터, form태그

- URL query(url parameter) : url 뒤에 ?를 붙이고 querysting을 추가하여 보내면 자동으로 객체 형태로 파싱되어 req.query에서 확인 가능

- 라우트파라미터 : 라우트경로 설정 시 맨뒤에 /:parameter? 와 같은 형식으로 라우터 경로 설정하면 req.params 객체에서 속성(토큰)으로 확인 가능. 이 속성을 '토큰'이라고 부른다.

- form 태그 : method속성으로 데이터를 보내는 방식을 설정 가능 (get, post, etc) 

=> get 방식으로 보낸 데이터는 req.query, post방식으로 보낸 데이터는 req.body에서 확인 가능

// src/index.js
//...
router.get('/src/index.js/:name?',(req, res) => {
    console.log("/src/index.js 에서 get방식으로 요청들어옴");
    const {name} = req.params;
    const paramgId = req.query.gid;
    const paramgPw = req.query.gpw;
    res.writeHead('200', { 'content-type': 'text/html;charset=utf-8' });
    res.write("<h1>GET</h1>");
    res.write("<p>id : " + paramgId + "</p>");
    res.write("<p>pw : " + paramgPw + "</p>");
    res.write("<p>name : " + name + "</p>");
    res.end();
});
/...

=> url :  http://localhost:3000/src/index.js/John?gid=123&gpw=456 

=> 결과 : id : 123 pw = 456 name : John (John은 토큰)

 

3-3. request path를 router 객체에 설정시 사용하는 메소드

- get('request path', callback) : get 방식으로 request path 에서 요청이 발생이 콜백함수 실행

- post('request path', callback) : post 방식으로 request path 에서 요청이 발생이 콜백함수 실행

- put('request path', callback) : put 방식으로 request path 에서 요청이 발생이 콜백함수 실행

- delete('request path', callback) : delete 방식으로 request path 에서 요청이 발생이 콜백함수 실행

- patch('request path', callback) : patch 방식으로 request path 에서 요청이 발생이 콜백함수 실행펭

- all('request path', callback) : 모든방식으로 request path 에서 요청이 발생이 콜백함수 실행 - > 페이지 오류 응답 처리하는데 사용

// src/index.js
//...
router.all('*', (req,res)=>{
    res.status(404).send("<h1>404 ERROR - page not found</h1>");
});
/...

 

 

3-4. 라우트 모듈화

- 모듈화 전

// src/index.js
const http = require('http');
const express = require('express');
const bodyParser = require('body-parser');

//익스프레스 서버 객체
const app = express();
//라우터 객체 참조
const router = express.Router();

//포트설정
app.set('port', 3000);

//미들웨어 등
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// static 미들웨어 
app.use('/public', express.static("/home/yongmin/Desktop/React/blog/blog-backend/public"));

//라우팅 함수 
router.get('/',(req, res) => {
    console.log("/ get방식으로 요청들어옴");
    res.send("<h1>HOME</h1>");
});
router.get('/about/:name?', (req,res)=>{
    const {name} = req.params;
    if(name){
        res.send(`${name} 소개`);
    }else{
        res.send("소게");
    }
});
router.get('/posts',(req,res)=>{
    const {id} = req.query;
    if(id){
        res.send(`포스트 #${id}`);
    }else{
        res.send("포스트id 없음");
    }
});
router.all('*', (req,res)=>{
    res.status(404).send("<h1>404 ERROR - page not found</h1>");
});

//라우터 등록
app.use('/', router);

//웹 서버의 port속성 설정
http.createServer(app)
.listen(app.get('port'), () => {
    console.log('express server starts : ' + app.get('port'));
});

 

- 모듈화 후

// src/index.js
const http = require('http');
const express = require('express');
const bodyParser = require('body-parser');

//익스프레스 서버 객체
const app = express();

//라우터 설정
const router = require('./router');

//포트설정
app.set('port', 3000);

//미들웨어 등
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// static 미들웨어 
app.use('/public', express.static("/home/yongmin/Desktop/React/blog/blog-backend/public"));

//라우터 등록
app.use('/', router);

//웹 서버의 port속성 설정
http.createServer(app)
.listen(app.get('port'), () => {
    console.log('express server starts : ' + app.get('port'));
});

 

//라우트 모듈화
// src/router/index.js
const express = require('express');

const router = express.Router();


router.get('/',(req, res) => {
    console.log("/ get방식으로 요청들어옴");
    res.send("<h1>HOME</h1>");
});
router.get('/about/:name?', (req,res)=>{
    const {name} = req.params;
    if(name){
        res.send(`${name} 소개`);
    }else{
        res.send("소게");
    }
});
router.get('/posts',(req,res)=>{
    const {id} = req.query;
    if(id){
        res.send(`포스트 #${id}`);
    }else{
        res.send("포스트id 없음");
    }
});
router.all('*', (req,res)=>{
    res.status(404).send("<h1>404 ERROR - page not found</h1>");
});


module.exports = router;

 

- posts 라우트

//src/api/posts/index.js
const express = require('express');
const posts = express.Router();

const printInfo = (req,res)=> {
    res.send(req.body = {
        method:req.method,
        path : req.originalUrl,
        params: req.params
    })
};

posts.get('/', printInfo);
posts.post('/', printInfo);
posts.get('/:id', printInfo);
posts.delete('/:id', printInfo);
posts.put('/:id', printInfo);
posts.patch('/:id', printInfo);

module.exports = posts;

 

//src/api
const express = require('express');
const api = express.Router();
const posts = require('./posts');

api.use('/posts', posts);

module.exports = api;

 

//src/index.js
const http = require('http');
const express = require('express');
// const static = require('serve-static');
// const path = require('path');
const bodyParser = require('body-parser');

//익스프레스 서버 객체
const app = express();

//라우터 설정
const api = require('./api');

//포트설정
app.set('port', 3000);

//미들웨어 등
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// static 미들웨어 
app.use('/public', express.static("/home/yongmin/Desktop/React/blog/blog-backend/public"));

//라우터 등록

app.use('/api', api);

//웹 서버의 port속성 설정
http.createServer(app)
.listen(app.get('port'), () => {
    console.log('express server starts : ' + app.get('port'));
});

 

3-5. 컨트롤러 파일

- 컨트롤러 : 라우트 처리 함수만 모아놓은 파일

- 컨트롤러에서는 백엔드 기능을 구현

 

//src/api/posts/posts.ctrl.js
//컨트롤러 파일
let postId = 1; // id 초깃값

const posts = [{ id: 1, title: '제목', body: '내용' }];
//목록조회 GET /api/posts 
exports.list = (req, res) => {
    res.send(posts);
};

//특정포스트 조회 GET /api/posts/:id 
exports.read = (req, res) => {
    const { id } = req.params;

    const post = posts.find(p => p.id.toString() === id);

    //포스트 없으면 오류 반환
    if (!post) {
        res.status(404).send("포스트없음");
        return;
    }
    res.send(post);
};

//포스트 작성 POST /api/posts
exports.write = (req, res) => {
    const { title, body } = req.body;
    postId += 1;
    const post = { id: postId, title, body };
    posts.push(post);
    res.send(post);
};

//특정포스트 제거 DELETE /api/posts/:id
exports.remove = (req, res) => {
    const { id } = req.params;

    const index = posts.findIndex(p => p.id.toString() === id);
    if (index === -1) {
        res.status(404).send("포스트없음");
        return;
    }
    posts.splice(index, 1);
    res.status(204).send("콘텐츠 없음, no content");
};

// 포스트 수정 PUT /api/posts/:id
//put 메소드는 전체 포스트 정보를 입력하여 데이터를 통째로 교체할 때 사용
exports.replace = (req, res) => {
    const { id } = req.params;
    const index = posts.findIndex(p => p.id.toString() === id);
    if (index === -1) {
        res.status(404).send("포스트없음");
        return;
    }
    posts[index] = {
        id,
        ...req.body
    };
    res.send(posts[index]);
};

//포스트 수정 PATCH /api/posts/:id
exports.update = (req, res) => {
    const { id } = req.params;
    const index = posts.findIndex(p => p.id.toString() === id);
    if (index === -1) {
        res.status(404).send("포스트없음");
        return;
    }
    posts[index] = {
        ...posts[index],
        ...req.body
    }
    res.send(posts[index]);
};

 

//src/api/posts/index.js
// posts 라우터
const express = require('express');
const posts = express.Router();
const postsCtrl = require('./posts.ctrl');

posts.get('/', postsCtrl.list);
posts.post('/', postsCtrl.write);
posts.get('/:id', postsCtrl.read);
posts.delete('/:id', postsCtrl.remove);
posts.put('/:id', postsCtrl.replace);
posts.patch('/:id', postsCtrl.update);

module.exports = posts;

 

//src/api/index.js
//라우트 모듈화
const express = require('express');
const api = express.Router();
const posts = require('./posts');

api.use('/posts', posts);

module.exports = api;

 

//src/index.js
// 백엔드 프로그래밍
const http = require('http');
const express = require('express');
// const static = require('serve-static');
// const path = require('path');
const bodyParser = require('body-parser');

//익스프레스 서버 객체
const app = express();

//라우터 설정
const api = require('./api');

//포트설정
app.set('port', 3000);

//미들웨어 등
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// static 미들웨어 
app.use('/public', express.static("/home/yongmin/Desktop/React/blog/blog-backend/public"));

//라우터 등록

app.use('/api', api);

//웹 서버의 port속성 설정
http.createServer(app)
.listen(app.get('port'), () => {
    console.log('express server starts : ' + app.get('port'));
});
Posted by yongminLEE