본문 바로가기

WINK-(Web & App)/Express.js (Node.js) 스터디

[2024-2 Node.js 스터디] 김민재 #6주차

반응형

API 서버 이해하기


API: 다른 애플리케이션에서 현재 프로그램의 기능을 사용할 수 있게 허용하는 접점을 의미

  • 웹 API는 다른 웹 서비스의 기능을 사용하거나 자원을 가져올 수 있는 창구이다
  • 크롤링: 표면저그올 보이는 웹 사이트의 정보를 일정 주기로 수집해 자체적으로 가공하는 기술
    • 자체적으로 제공하는 API가 없거나 API이용에 제한이 있는 경우 사용

프로젝트 구조 갖추기


다른 서비스가 nodebird의 데이터나 서비스를 이용할 수 있도록 만들 것이다.

{
  "name": "nodebird-api",
  "version": "0.0.1",
  "description": "NodeBird API 서버",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app",
    "test": "echo \\"Error: no test specified\\" && exit 1"
  },
  "author": "Zero Cho",
  "license": "ISC",
  "dependencies": {
    "bcrypt": "^5.0.0",
    "cookie-parser": "^1.4.5",
    "dotenv": "^16.0.1",
    "express": "^4.17.1",
    "express-session": "^1.17.1",
    "morgan": "^1.10.0",
    "mysql2": "^2.1.0",
    "nunjucks": "^3.2.1",
    "passport": "^0.6.0",
    "passport-kakao": "1.0.0",
    "passport-local": "^1.0.0",
    "sequelize": "^6.19.2",
    "uuid": "^8.3.2"
  },
  "devDependencies": {
    "nodemon": "^2.0.16"
  }
}
package.json을 위와 같이 구성해 패키지를 설치한다
  • 도메인을 등록하는 기능이 새로 생겼으므로 도메인 모델을 추가해보자
const Sequelize = require('sequelize');

class Domain extends Sequelize.Model {
  static initiate(sequelize) {
    Domain.init({
      host: {
        type: Sequelize.STRING(80),
        allowNull: false,
      },
      type: {
        type: Sequelize.ENUM('free', 'premium'),
        allowNull: false,
      },
      clientSecret: {
        type: Sequelize.UUID,
        allowNull: false,
      },
    }, {
      sequelize,
      timestamps: true,
      paranoid: true,
      modelName: 'Domain',
      tableName: 'domains',
    });
  }

  static associate(db) {
    db.Domain.belongsTo(db.User);
  }
};

module.exports = Domain;

도메인 모델에는 인터넷 주소, 도메인 종류, 클라이언트 비밀 키가 들어간다

const express = require('express');
const { renderLogin, createDomain } = require('../controllers');
const { isLoggedIn } = require('../middlewares');

const router = express.Router();

router.get('/', renderLogin);

router.post('/domain', isLoggedIn, createDomain);

module.exports = router;

GET / 라우터와 도메인 등록 라우터를 만들어준다

JWT 토큰으로 인증하기


NodeBird 앱이 아닌 다른 클라이언트가 NodeBird의 데이터를 가져갈 수 있게 해야 하는 만큼 별도의 인증 과정이 필요하다. JWT 토큰을 사용해 인증하는 방법을 사용하자

  • JWT(JSON Web Token): JSON 형식의 데이터를 저장하는 토큰이다.
  • JWT의 구성
    • 헤더: 토큰 종류와 해시 알고리즘 정보가 들어있다
    • 페이로드: 토큰의 내용물이 인코딩된 부분이다
    • 시그니처: 일련의 문자열로, 시그니처를 통해 토큰이 변조되었는지 여부를 확인할 수 있다
JWT_SECRET = jwtSecret
.env에서 설정해주고

exports.verifyToken = (req, res, next) => {
  try {
    res.locals.decoded = jwt.verify(req.headers.authorization, process.env.JWT_SECRET);
    return next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') { // 유효기간 초과
      return res.status(419).json({
        code: 419,
        message: '토큰이 만료되었습니다',
      });
    }
    return res.status(401).json({
      code: 401,
      message: '유효하지 않은 토큰입니다',
    });
  }
};

미들웨어의 index.js에서 요청 헤더에 저장된 토큰을 사용해 검증을 할 수 있다.
const express = require('express');

const { verifyToken } = require('../middlewares');
const { createToken, tokenTest } = require('../controllers/v1');

const router = express.Router();

// POST /v1/token
router.post('/token', createToken);

// POST /v1/test
router.get('/test', verifyToken, tokenTest);

module.exports = router;

토큰을 발급하는 라우터(POST)와 사용자가 토큰을 테스트해볼 수 있는 라우터(GET)를 만들었다

다른 서비스에서 호출하기


  • API 제공 서버를 만들었으니, API 사용하는 서비스도 만들어봅시다.
    • 이 서비스는 다른 서버에 요청을 보내므로 클라이언트 역할을 하고, API 사용자의 입장에서 진행
  • 이 서버의 주 목적은 nodebird-api의 API를 통해 데이터를 가져오는 것이다
{
  "name": "nodecat",
  "version": "0.0.1",
  "description": "NodeBird 2차 서비스",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app"
  },
  "author": "Zero Cho",
  "license": "ISC",
  "dependencies": {
    "axios": "^0.27.2",
    "cookie-parser": "^1.4.6",
    "dotenv": "^16.0.1",
    "express": "^4.18.1",
    "express-session": "^1.17.3",
    "morgan": "^1.10.0",
    "nunjucks": "^3.2.3"
  },
  "devDependencies": {
    "nodemon": "^2.0.16"
  }
}

  • .env 파일에 client_secret 키를 넣고
const express = require('express');
const { test } = require('../controllers');

const router = express.Router();

// POST /test
router.get('/test', test);

module.exports = router;

const axios = require('axios');
exports.test = async (req, res, next) => { // 토큰 테스트 라우터
  try {
    if (!req.session.jwt) { // 세션에 토큰이 없으면 토큰 발급 시도
      const tokenResult = await axios.post('<http://localhost:8002/v1/token>', {
        clientSecret: process.env.CLIENT_SECRET,
      });
      if (tokenResult.data?.code === 200) { // 토큰 발급 성공
        req.session.jwt = tokenResult.data.token; // 세션에 토큰 저장
      } else { // 토큰 발급 실패
        return res.json(tokenResult.data); // 발급 실패 사유 응답
      }
    }
    // 발급받은 토큰 테스트
    const result = await axios.get('<http://localhost:8002/v1/test>', {
      headers: { authorization: req.session.jwt },
    });
    return res.json(result.data);
  } catch (error) {
    console.error(error);
    if (error.response?.status === 419) { // 토큰 만료 시
      return res.json(error.response.data);
    }
    return next(error);
  }
};

index.js를 만들어 nodecat 서비스가 토큰 인증 과정을 테스트해보는 라우터를 동작해본다
요청이 왔을 때 토큰이 없으면 v1에서 token 라우터로부터 토큰을 발급받고
성고하면 토큰을 저장하며 발급받은 토큰을 테스트 해보도록 구성했다.

SNS API 서버 만들기


  • 다시 nodebird-api의 입장으로 돌아와서 나머지 api 라우터를 완성시킬 것이다
const express = require('express');

const { verifyToken } = require('../middlewares');
const { createToken, tokenTest, getMyPosts, getPostsByHashtag } = require('../controllers/v1');

const router = express.Router();

// POST /v1/token
router.post('/token', createToken);

// POST /v1/test
router.get('/test', verifyToken, tokenTest);

// GET /v1/posts/my
router.get('/posts/my', verifyToken, getMyPosts);

// GET /v1/posts/hashtag/:title
router.get('/posts/hashtag/:title', verifyToken, getPostsByHashtag);

module.exports = router;

get /posts/my - 내가 올린 포스트 가져오는 라우터
get /posts/hashtag/:title - 해시태그 검색 결과를 가져오는 라우터

사용량 제한 구현하기


  • 일차적으로 인증된 사용자만 API를 사용할 수 있게 필터를 두긴 했지만, 아직 충분하지는 않다
  • 일정 기간 내에 API를 사용할 수 있는 횟수를 제한해 서버의 트래픽을 줄이는 것이 좋다
npm i express-rate-limit

const jwt = require('jsonwebtoken');
const rateLimit = require('express-rate-limit');

exports.isLoggedIn = (req, res, next) => {
  if (req.isAuthenticated()) {
    next();
  } else {
    res.status(403).send('로그인 필요');
  }
};

exports.isNotLoggedIn = (req, res, next) => {
  if (!req.isAuthenticated()) {
    next();
  } else {
    const message = encodeURIComponent('로그인한 상태입니다.');
    res.redirect(`/?error=${message}`);
  }
};

exports.verifyToken = (req, res, next) => {
  try {
    res.locals.decoded = jwt.verify(req.headers.authorization, process.env.JWT_SECRET);
    return next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') { // 유효기간 초과
      return res.status(419).json({
        code: 419,
        message: '토큰이 만료되었습니다',
      });
    }
    return res.status(401).json({
      code: 401,
      message: '유효하지 않은 토큰입니다',
    });
  }
};

exports.apiLimiter = rateLimit({
  windowMs: 60 * 1000, // 1분
  max: 10,
  handler(req, res) {
    res.status(this.statusCode).json({
      code: this.statusCode, // 기본값 429
      message: '1분에 열 번만 요청할 수 있습니다.',
    });
  },
});

exports.deprecated = (req, res) => {
  res.status(410).json({
    code: 410,
    message: '새로운 버전이 나왔습니다. 새로운 버전을 사용하세요.',
  });
};

미들웨어도 추가해 요청을 조절할 수 있다

API 응답 목록
200: json 데이터
401: 유효하지 않은 토큰
410: 새로운 버전
419: 토큰 만료
429: 1분에 한번만
500~: 기타 서버 에러
const express = require('express');

const { verifyToken, apiLimiter } = require('../middlewares');
const { createToken, tokenTest, getMyPosts, getPostsByHashtag } = require('../controllers/v2');

const router = express.Router();

// POST /v2/token
router.post('/token', apiLimiter, createToken);

// POST /v2/test
router.get('/test', apiLimiter, verifyToken, tokenTest);

// GET /v2/posts/my
router.get('/posts/my', apiLimiter, verifyToken, getMyPosts);

// GET /v2/posts/hashtag/:title
router.get('/posts/hashtag/:title', apiLimiter, verifyToken, getPostsByHashtag);

module.exports = router;

사용량 제한이 생겼으므로 새로운 v2를 만들어 라우터에 추가한다

CORS 이해하기


  • 이전 절에서 nodecat이 nodebird-api를 호출하는 것은 서버에서 서버로 API를 호출한 것이다. 만약 nodecat의 프런트에서 서버 API를 호출하면 어떻게 될까?

→ 브라우저와 서버의 도메인이 일치하지 않으면, 기본적으로 요청이 차단된다.

  • 이와 같이 현재 요청을 보내는 클라이언트와 요청을 받는 서버의 도메인이 다르기때문에 생기는 문제를 CORS 문제라고 부른다.
반응형