๐Ÿ”Ž๋…ธ๋“œ ์„œ๋น„์Šค ํ…Œ์ŠคํŠธํ•˜๊ธฐ

์„œ๊ฐ€ํฌยท2021๋…„ 11์›” 20์ผ
0

Node.js

๋ชฉ๋ก ๋ณด๊ธฐ
10/15
post-thumbnail

ํ…Œ์ŠคํŠธ ์ค€๋น„ํ•˜๊ธฐ

1. ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋Š” ์ด์œ 

์ž์‹ ์ด ๋งŒ๋“  ์„œ๋น„์Šค๊ฐ€ ์ œ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•จ

  • ๊ธฐ๋Šฅ์ด ๋งŽ๋‹ค๋ฉด ์ˆ˜์ž‘์—…์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ํž˜๋“ฆ
  • ํ”„๋กœ๊ทธ๋žจ์ด ํ”„๋กœ๊ทธ๋žจ์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋„๋ก ์ž๋™ํ™”ํ•จ
  • ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ์ตœ๋Œ€ํ•œ ์‹ค์ œ ํ™˜๊ฒฝ๊ณผ ๋น„์Šทํ•˜๊ฒŒ ํ‰๋‚ด๋ƒ„
  • ์•„๋ฌด๋ฆฌ ์ฒ ์ €ํ•˜๊ฒŒ ํ…Œ์ŠคํŠธํ•ด๋„ ์—๋Ÿฌ๋ฅผ ์™„์ „ํžˆ ๋ง‰์„ ์ˆ˜๋Š” ์—†์Œ
    ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋ฉด ์ข‹์€ ์ 
  • ํ•˜์ง€๋งŒ ํ—ˆ๋ฌดํ•œ ์—๋Ÿฌ๋กœ ์ธํ•ด ํ”„๋กœ๊ทธ๋žจ์ด ๊ณ ์žฅ๋‚˜๋Š” ๊ฒƒ์€ ๋ง‰์„ ์ˆ˜ ์žˆ์Œ
  • ํ•œ ๋ฒˆ ๋ฐœ์ƒํ•œ ์—๋Ÿฌ๋Š” ํ…Œ์ŠคํŠธ๋กœ ๋งŒ๋“ค์–ด๋‘๋ฉด ๊ฐ™์€ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๊ฒŒ ๋ง‰์„ ์ˆ˜ ์žˆ์Œ
  • ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•  ๋•Œ ํ”„๋กœ๊ทธ๋žจ์ด ์ž๋™์œผ๋กœ ์–ด๋–ค ๋ถ€๋ถ„์ด ๊ณ ์žฅ๋‚˜๋Š” ์ง€ ์•Œ๋ ค์คŒ

2. Jest ์„ค์น˜ํ•˜๊ธฐ

npm i โ€“D jest

  • Nodebird ํ”„๋กœ์ ํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•จ
...
{
  "name": "nodebird",
  "version": "0.0.1",
  "description": "์ต์Šคํ”„๋ ˆ์Šค๋กœ ๋งŒ๋“œ๋Š” SNS ์„œ๋น„์Šค",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app",
    "test": "jest"
  },
  ...

3. ํ…Œ์ŠคํŠธ ์‹คํ–‰ํ•ด๋ณด๊ธฐ

routes ํด๋” ์•ˆ์— middlewares.test.js ์ž‘์„ฑ

  • ํ…Œ์ŠคํŠธ์šฉ ํŒŒ์ผ์€ ํŒŒ์ผ๋ช…์— test๋‚˜ spec์ด ์žˆ์œผ๋ฉด ๋จ
  • npm test๋กœ test๋‚˜ spec ํŒŒ์ผ๋“ค์„ ํ…Œ์ŠคํŠธํ•จ.
  • ํ…Œ์ŠคํŠธ๋ฅผ ์•„๋ฌด๊ฒƒ๋„ ์ž‘์„ฑํ•˜์ง€ ์•Š์•˜์œผ๋ฏ€๋กœ ์—๋Ÿฌ ๋ฐœ์ƒ(ํ…Œ์ŠคํŠธ ์‹คํŒจ)

4. ์ฒซ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑํ•˜๊ธฐ

middlewares.test.js ์ž‘์„ฑํ•˜๊ธฐ

  • test ํ•จ์ˆ˜์˜ ์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•œ ์„ค๋ช…
  • ๋‘ ๋ฒˆ์งธ ์ธ์ˆ˜์ธ ํ•จ์ˆ˜์—๋Š” ํ…Œ์ŠคํŠธ ๋‚ด์šฉ์„ ์ ์Œ
  • expect ํ•จ์ˆ˜์˜ ์ธ์ˆ˜๋กœ ์‹ค์ œ ์ฝ”๋“œ๋ฅผ, toEqual ํ•จ์ˆ˜์˜ ์ธ์ˆ˜๋กœ๋Š” ์˜ˆ์ƒ๋˜๋Š” ๊ฒฐ๊ด๊ฐ’์„
  • expect์™€ toEqual์˜ ์ธ์ˆ˜๊ฐ€ ์ผ์น˜ํ•˜๋ฉด ํ…Œ์ŠคํŠธ ํ†ต๊ณผ

๐Ÿ”ปroutes/middlewares.test.js

test('1 + 1์€ 2์ž…๋‹ˆ๋‹ค.', () => {
  expect(1 + 1).toEqual(2);
});
  • expect์™€ toEqual์˜ ์ธ์ˆ˜๊ฐ€ ์ผ์น˜ํ•˜๋ฉด ํ…Œ์ŠคํŠธ ํ†ต๊ณผ

5. ์‹คํŒจํ•˜๋Š” ๊ฒฝ์šฐ

๋‘ ์ธ์ˆ˜๋ฅผ ๋‹ค๋ฅด๊ฒŒ ์ž‘์„ฑํ•˜๋ฉด ์‹คํŒจ(๋ฉ”์‹œ์ง€๋ฅผ ์‚ดํŽด๋ณผ ๊ฒƒ)
๐Ÿ”ปroutes/middlewares.test.js

test('1 + 1์€ 2์ž…๋‹ˆ๋‹ค.', () => {
  expect(1 + 1).toEqual(2);
});

์œ ๋‹› ํ…Œ์ŠคํŠธ

1. middlewares ํ…Œ์ŠคํŠธํ•˜๊ธฐ

middlewares.test.js ์ž‘์„ฑํ•˜๊ธฐ

  • ํ…Œ์ŠคํŠธ ํ‹€ ์žก๊ธฐ
  • describe๋กœ ํ…Œ์ŠคํŠธ ๊ทธ๋ฃนํ™” ๊ฐ€๋Šฅ

๐Ÿ”ปroutes/middlewares.test.js

const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
//๊ทธ๋ฃนํ™” -> describe
describe('isLoggedIn', () => {
  const res = {
    status: jest.fn(() => res),
    send: jest.fn(),
  };
  const next = jest.fn();

  test('๋กœ๊ทธ์ธ ๋˜์–ด์žˆ์œผ๋ฉด isLoggedIn์ด next๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•จ', () => {
    const req = {
      isAuthenticated: jest.fn(() => true),
    };
    isLoggedIn(req, res, next);
    expect(next).toBeCalledTimes(1);
  });

  test('๋กœ๊ทธ์ธ ๋˜์–ด์žˆ์ง€ ์•Š์œผ๋ฉด isLoggedIn์ด ์—๋Ÿฌ๋ฅผ ์‘๋‹ตํ•ด์•ผ ํ•จ', () => {
    const req = {
      isAuthenticated: jest.fn(() => false),
    };
    isLoggedIn(req, res, next);
    expect(res.status).toBeCalledWith(403);
    expect(res.send).toBeCalledWith('๋กœ๊ทธ์ธ ํ•„์š”');
  });
});

describe('isNotLoggedIn', () => {
  const res = {
    redirect: jest.fn(),
  };
  const next = jest.fn();

  test('๋กœ๊ทธ์ธ ๋˜์–ด์žˆ์œผ๋ฉด isNotLoggedIn์ด ์—๋Ÿฌ๋ฅผ ์‘๋‹ตํ•ด์•ผ ํ•จ', () => {
    const req = {
      isAuthenticated: jest.fn(() => true),
    };
    isNotLoggedIn(req, res, next);
    const message = encodeURIComponent('๋กœ๊ทธ์ธํ•œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค.');
    expect(res.redirect).toBeCalledWith(`/?error=${message}`);
  });

  test('๋กœ๊ทธ์ธ ๋˜์–ด์žˆ์ง€ ์•Š์œผ๋ฉด isNotLoggedIn์ด next๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•จ', () => {
    const req = {
      isAuthenticated: jest.fn(() => false),
    };
    isNotLoggedIn(req, res, next);
    expect(next).toHaveBeenCalledTimes(1);
  });
});

2. req, res ๋ชจํ‚นํ•˜๊ธฐ

๋ฏธ๋“ค์›จ์–ด ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด req์™€ res๋ฅผ ๊ฐ€์งœ๋กœ ๋งŒ๋“ค์–ด์ฃผ์–ด์•ผ ํ•จ
jest.fn์œผ๋กœ ํ•จ์ˆ˜ ๋ชจํ‚น ๊ฐ€๋Šฅ

๐Ÿ”ปroutes/middlewares.test.js

const { isLoggedIn, isNotLoggedIn } = require('./middlewares');

describe('isLoggedIn', () => {
  const res = {
    status: jest.fn(() => res),
    send: jest.fn(),
  };
  const next = jest.fn();

  test('๋กœ๊ทธ์ธ ๋˜์–ด์žˆ์œผ๋ฉด isLoggedIn์ด next๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•จ', () => {
    const req = {
      isAuthenticated: jest.fn(() => true),
    };
    isLoggedIn(req, res, next);
    expect(next).toBeCalledTimes(1);
  });

  test('๋กœ๊ทธ์ธ ๋˜์–ด์žˆ์ง€ ์•Š์œผ๋ฉด isLoggedIn์ด ์—๋Ÿฌ๋ฅผ ์‘๋‹ตํ•ด์•ผ ํ•จ', () => {
    const req = {
      isAuthenticated: jest.fn(() => false),
    };
    isLoggedIn(req, res, next);
    expect(res.status).toBeCalledWith(403);
    expect(res.send).toBeCalledWith('๋กœ๊ทธ์ธ ํ•„์š”');
  });
});

describe('isNotLoggedIn', () => {
  const res = {
    redirect: jest.fn(),
  };
  const next = jest.fn();

  test('๋กœ๊ทธ์ธ ๋˜์–ด์žˆ์œผ๋ฉด isNotLoggedIn์ด ์—๋Ÿฌ๋ฅผ ์‘๋‹ตํ•ด์•ผ ํ•จ', () => {
    const req = {
      isAuthenticated: jest.fn(() => true),
    };
    isNotLoggedIn(req, res, next);
    const message = encodeURIComponent('๋กœ๊ทธ์ธํ•œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค.');
    expect(res.redirect).toBeCalledWith(`/?error=${message}`);
  });

  test('๋กœ๊ทธ์ธ ๋˜์–ด์žˆ์ง€ ์•Š์œผ๋ฉด isNotLoggedIn์ด next๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•จ', () => {
    const req = {
      isAuthenticated: jest.fn(() => false),
    };
    isNotLoggedIn(req, res, next);
    expect(next).toHaveBeenCalledTimes(1);
  });
});

3. expect ๋ฉ”์„œ๋“œ

expect์—๋Š” toEqual ๋ง๊ณ ๋„ ๋งŽ์€ ๋ฉ”์„œ๋“œ ์ง€์›

  • toBeCalledWith๋กœ ์ธ์ˆ˜ ์ฒดํฌ
  • toBeCalledTimes๋กœ ํ˜ธ์ถœ ํšŒ์ˆ˜ ์ฒดํฌ

๐Ÿ”ปroutes.middlewares.test.js

...
describe('isNotLoggedIn', () => {
  const res = {
    redirect: jest.fn(),
  };
  const next = jest.fn();

  test('๋กœ๊ทธ์ธ ๋˜์–ด์žˆ์œผ๋ฉด isNotLoggedIn์ด ์—๋Ÿฌ๋ฅผ ์‘๋‹ตํ•ด์•ผ ํ•จ', () => {
    const req = {
      isAuthenticated: jest.fn(() => true),
    };
    isNotLoggedIn(req, res, next);
    const message = encodeURIComponent('๋กœ๊ทธ์ธํ•œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค.');
    expect(res.redirect).toBeCalledWith(`/?error=${message}`);
  });

  test('๋กœ๊ทธ์ธ ๋˜์–ด์žˆ์ง€ ์•Š์œผ๋ฉด isNotLoggedIn์ด next๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•จ', () => {
    const req = {
      isAuthenticated: jest.fn(() => false),
    };
    isNotLoggedIn(req, res, next);
    expect(next).toHaveBeenCalledTimes(1);
  });
});

4. ๋ผ์šฐํ„ฐ ํ…Œ์ŠคํŠธ ์œ„ํ•ด ๋ถ„๋ฆฌํ•˜๊ธฐ

๋ผ์šฐํ„ฐ๋„ ๋ฏธ๋“ค์›จ์–ด์ด๋ฏ€๋กœ ๋ถ„๋ฆฌํ•ด์„œ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ

๐Ÿ”ปcontrollers/user.js


const User = require('../models/user');

exports.addFollowing = async (req, res, next) => {
  try {
    const user = await User.findOne({ where: { id: req.user.id } });
    if (user) {
      await user.addFollowing(parseInt(req.params.id, 10));
      res.send('success');
    } else {
      res.status(404).send('no user');
    }
  } catch (error) {
    console.error(error);
    next(error);
  }
};

๐Ÿ”ปroutes/user.js

const express = require('express');

const { isLoggedIn } = require('./middlewares');
const { addFollowing } = require('../controllers/user');

const router = express.Router();

router.post('/:id/follow', isLoggedIn, addFollowing);

module.exports = router;

5. ๋ผ์šฐํ„ฐ ํ…Œ์ŠคํŠธ

Controllers/user.test.js ์ž‘์„ฑํ•˜๊ธฐ

๐Ÿ”ปControllers/user.test.js

jest.mock('../models/user');
const User = require('../models/user');
const { addFollowing } = require('../controllers/user');

describe('addFollowing', () => {
  const req = {
    user: { id: 1 },
    params: { id: 2 },
  };
  const res = {
    send: jest.fn(),
  };
  const next = jest.fn();

  test('์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์•„ ํŒ”๋กœ์ž‰์„ ์ถ”๊ฐ€ํ•˜๊ณ  success๋ฅผ ์‘๋‹ตํ•ด์•ผ ํ•จ', async () => {
    User.findOne.mockReturnValue(Promise.resolve({
      addFollowing(id) {
        return Promise.resolve(true);
      }
    }));
    await addFollowing(req, res, next);
    expect(res.send).toBeCalledWith('success');
  });

  test('์‚ฌ์šฉ์ž๋ฅผ ๋ชป ์ฐพ์œผ๋ฉด next(error)๋ฅผ ํ˜ธ์ถœํ•จ', async () => {
    const error = '์‚ฌ์šฉ์ž ๋ชป ์ฐพ์Œ';
    User.findOne.mockReturnValue(Promise.reject(error));
    await addFollowing(req, res, next);
    expect(next).toBeCalledWith(error);
  });
});

6. DB ๋ชจํ‚นํ•˜๊ธฐ

Jest๋ฅผ ์‚ฌ์šฉํ•ด ๋ชจ๋“ˆ ๋ชจํ‚น ๊ฐ€๋Šฅ(jest.mock)

  • ๋ฉ”์„œ๋“œ์— mockReturnValue ๋ฉ”์„œ๋“œ๊ฐ€ ์ถ”๊ฐ€๋˜์–ด ๋ฆฌํ„ด๊ฐ’ ๋ชจํ‚น ๊ฐ€๋Šฅ

๐Ÿ”ปcontroller/user.test.js


jest.mock('../models/user');
const User = require('../models/user');
const { addFollowing } = require('../controllers/user');

describe('addFollowing', () => {
  const req = {
    user: { id: 1 },
    params: { id: 2 },
  };
  const res = {
    send: jest.fn(),
  };
  const next = jest.fn();

  test('์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์•„ ํŒ”๋กœ์ž‰์„ ์ถ”๊ฐ€ํ•˜๊ณ  success๋ฅผ ์‘๋‹ตํ•ด์•ผ ํ•จ', async () => {
    User.findOne.mockReturnValue(Promise.resolve({
      addFollowing(id) {
        return Promise.resolve(true);
      }
    }));
    await addFollowing(req, res, next);
    expect(res.send).toBeCalledWith('success');
  });

  test('์‚ฌ์šฉ์ž๋ฅผ ๋ชป ์ฐพ์œผ๋ฉด next(error)๋ฅผ ํ˜ธ์ถœํ•จ', async () => {
    const error = '์‚ฌ์šฉ์ž ๋ชป ์ฐพ์Œ';
    User.findOne.mockReturnValue(Promise.reject(error));
    await addFollowing(req, res, next);
    expect(next).toBeCalledWith(error);
  });
});

ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€

1. ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋ž€

์ „์ฒด ์ฝ”๋“œ ์ค‘์—์„œ ํ…Œ์ŠคํŠธ๋˜๊ณ  ์žˆ๋Š” ์ฝ”๋“œ์˜ ๋น„์œจ

  • ํ…Œ์ŠคํŠธ๋˜์ง€ ์•Š๋Š” ์ฝ”๋“œ์˜ ์œ„์น˜๋„ ์•Œ๋ ค์คŒ
  • jest โ€“coverage
  • Stmts: ๊ตฌ๋ฌธ
  • Branch: ๋ถ„๊ธฐ์ 
  • Funcs: ํ•จ์ˆ˜
  • Lines: ์ค„ ์ˆ˜

๐Ÿ”ปpackage.json

{
  "name": "nodebird",
  "version": "0.0.1",
  "description": "์ต์Šคํ”„๋ ˆ์Šค๋กœ ๋งŒ๋“œ๋Š” SNS ์„œ๋น„์Šค",
  "main": "app.js",
  "scripts": {
    "start": "nodemon server",
    "test": "jest"
  },
  "author": "seokahi",
  "license": "MIT",
  "dependencies": {
    "bcrypt": "^3.0.7",
    "cookie-parser": "^1.4.3",
    "dotenv": "^8.2.0",
    "express": "^4.16.3",
    "express-session": "^1.15.6",
    "morgan": "^1.9.1",
    "multer": "^1.4.2",
    "mysql2": "^2.0.2",
    "nunjucks": "^3.2.0",
    "passport": "^0.4.0",
    "passport-kakao": "1.0.0",
    "passport-local": "^1.0.0",
    "sequelize": "^5.21.3",
    "sequelize-cli": "^5.5.1"
  },
  "devDependencies": {
    "jest": "^24.9.0",
    "nodemon": "^2.0.2",
    "supertest": "^4.0.2"
  }
}

2. ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ์˜ฌ๋ฆฌ๊ธฐ

models/users.js์˜ 5, 41, 42, 47์ค„ ํ™•์ธ

๐Ÿ”ปmodels/user.js

const Sequelize = require('sequelize');

module.exports = class User extends Sequelize.Model {
  static init(sequelize) {
    return super.init({
      email: {
        type: Sequelize.STRING(40),
        allowNull: true,
        unique: true,
      },
      nick: {
        type: Sequelize.STRING(15),
        allowNull: false,
      },
      password: {
        type: Sequelize.STRING(100),
        allowNull: true,
      },
      provider: {
        type: Sequelize.STRING(10),
        allowNull: false,
        defaultValue: 'local',
      },
      snsId: {
        type: Sequelize.STRING(30),
        allowNull: true,
      },
    }, {
      sequelize,
      timestamps: true,
      underscored: false,
      modelName: 'User',
      tableName: 'users',
      paranoid: true,
      charset: 'utf8',
      collate: 'utf8_general_ci',
    });
  }

  static associate(db) {
    db.User.hasMany(db.Post);
    db.User.belongsToMany(db.User, {
      foreignKey: 'followingId',
      as: 'Followers',
      through: 'Follow',
    });
    db.User.belongsToMany(db.User, {
      foreignKey: 'followerId',
      as: 'Followings',
      through: 'Follow',
    });
  }
};
  1. ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ์˜ฌ๋ฆฌ๊ธฐ

models/users.test.js์ž‘์„ฑ

๐Ÿ”ปmodels/users.test.js

const Sequelize = require('sequelize');
const User = require('./user');
const config = require('../config/config')['test'];
const sequelize = new Sequelize(
  config.database, config.username, config.password, config,
);

describe('User ๋ชจ๋ธ', () => {
  test('static init ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ', () => {
    expect(User.init(sequelize)).toBe(User);
  });
  test('static associate ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ', () => {
    const db = {
      User: {
        hasMany: jest.fn(),
        belongsToMany: jest.fn(),
      },
      Post: {},
    };
    User.associate(db);
    expect(db.User.hasMany).toHaveBeenCalledWith(db.Post);
    expect(db.User.belongsToMany).toHaveBeenCalledTimes(2);
  });
});


4. ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ์ฃผ์˜์ 

๋ชจ๋“  ์ฝ”๋“œ๊ฐ€ ํ…Œ์ŠคํŠธ๋˜์ง€ ์•Š๋Š”๋ฐ๋„ ์ปค๋ฒ„๋ฆฌ์ง€๊ฐ€ 100%์ž„

  • ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๋งน์‹ ํ•  ํ•„์š”๊ฐ€ ์—†์Œ
  • ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๋†’์ด๋Š” ๊ฒƒ์ด ์˜๋ฏธ๋Š” ์žˆ์ง€๋งŒ ๋†’์ด๋Š” ๋ฐ ๋„ˆ๋ฌด ์ง‘์ฐฉํ•  ํ•„์š”๋Š” ์—†์Œ
  • ํ•„์š”ํ•œ ๋ถ€๋ถ„ ์œ„์ฃผ๋กœ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Œ

ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

1. ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ํ•ด๋ณด๊ธฐ

๋ผ์šฐํ„ฐ ํ•˜๋‚˜๋ฅผ ํ†ต์งธ๋กœ ํ…Œ์ŠคํŠธํ•ด ๋ด„(์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋ฏธ๋“ค์›จ์–ด, ๋ชจ๋“ˆ์„ ํ•œ ๋ฒˆ์— ํ…Œ์ŠคํŠธ).

  • app.js ๋ถ„๋ฆฌํ•˜๊ธฐ
  • Supertest ์‚ฌ์šฉ(npm i โ€“D supertest)

๐Ÿ”ปapp.js

const express = require('express');
const cookieParser = require('cookie-parser');
const morgan = require('morgan');
const path = require('path');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
const passport = require('passport');

dotenv.config();
const pageRouter = require('./routes/page');
const authRouter = require('./routes/auth');
const postRouter = require('./routes/post');
const userRouter = require('./routes/user');
const { sequelize } = require('./models');
const passportConfig = require('./passport');

const app = express();
passportConfig(); // ํŒจ์ŠคํฌํŠธ ์„ค์ •
app.set('port', process.env.PORT || 8001);
app.set('view engine', 'html');
nunjucks.configure('views', {
  express: app,
  watch: true,
});

sequelize.sync({ force: false })
  .then(() => {
    console.log('๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์„ฑ๊ณต');
  })
  .catch((err) => {
    console.error(err);
  });

app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/img', express.static(path.join(__dirname, 'uploads')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
  resave: false,
  saveUninitialized: false,
  secret: process.env.COOKIE_SECRET,
  cookie: {
    httpOnly: true,
    secure: false,
  },
}));
app.use(passport.initialize());
app.use(passport.session());

app.use('/', pageRouter);
app.use('/auth', authRouter);
app.use('/post', postRouter);
app.use('/user', userRouter);

app.use((req, res, next) => {
  const error =  new Error(`${req.method} ${req.url} ๋ผ์šฐํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.`);
  error.status = 404;
  next(error);
});

app.use((err, req, res, next) => {
  console.error(err);
  res.locals.message = err.message;
  res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

server.js

const app = require('./app');

app.listen(app.get('port'), () => {
  console.log(app.get('port'), '๋ฒˆ ํฌํŠธ์—์„œ ๋Œ€๊ธฐ์ค‘');
});

๐Ÿ”ปpackage.json

{
  "name": "nodebird",
  "version": "0.0.1",
  "description": "์ต์Šคํ”„๋ ˆ์Šค๋กœ ๋งŒ๋“œ๋Š” SNS ์„œ๋น„์Šค",
  "main": "server.js",
  "scripts": {
    "start": "nodemon server",
    "test": "jest"
  },
  "author": "seokahi",
  "license": "MIT",
  "dependencies": {
    "bcrypt": "^3.0.7",
    "cookie-parser": "^1.4.3",
    "dotenv": "^8.2.0",
    "express": "^4.16.3",
    "express-session": "^1.15.6",
    "morgan": "^1.9.1",
    "multer": "^1.4.2",
    "mysql2": "^2.0.2",
    "nunjucks": "^3.2.0",
    "passport": "^0.4.0",
    "passport-kakao": "1.0.0",
    "passport-local": "^1.0.0",
    "sequelize": "^5.21.3",
    "sequelize-cli": "^5.5.1"
  },
  "devDependencies": {
    "jest": "^24.9.0",
    "nodemon": "^2.0.2",
    "supertest": "^4.0.2"
  }
}

2. ํ…Œ์ŠคํŠธ์šฉ DB ์„ค์ •ํ•˜๊ธฐ

๊ฐœ๋ฐœ/๋ฐฐํฌ์šฉ DB๋ž‘ ๋ณ„๋„๋กœ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Œ

  • config/config.json์˜ test ์†์„ฑ

๐Ÿ”ปconfig/config.json

{
  "development": {
    "username": "root",
    "password": "nodejsbook",
    "database": "nodebird",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "test": {
    "username": "root",
    "password": "nodejsbook",
    "database": "nodebird_test",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "production": {
    "username": "root",
    "password": null,
    "database": "database_production",
    "host": "127.0.0.1",
    "dialect": "mysql"
  }
}

3. ๋ผ์šฐํ„ฐ ํ…Œ์ŠคํŠธ

routes/auth.test.js ์ž‘์„ฑ

  • beforeAll: ๋ชจ๋“  ํ…Œ์ŠคํŠธ ์ „์— ์‹คํ–‰
  • request(app).post(์ฃผ์†Œ)๋กœ ์š”์ฒญ
  • send๋กœ data ์ „์†ก

๐Ÿ”ปroutes/auth.test.js

const request = require('supertest');
const { sequelize } = require('../models');
const app = require('../app');

beforeAll(async () => {
  await sequelize.sync();
});

describe('POST /join', () => {
  test('๋กœ๊ทธ์ธ ์•ˆ ํ–ˆ์œผ๋ฉด ๊ฐ€์ž…', (done) => {
    request(app)
      .post('/auth/join')
      .send({
        email: 'zerohch0@gmail.com',
        nick: 'zerocho',
        password: 'nodejsbook',
      })
      .expect('Location', '/')
      .expect(302, done);
  });
});

describe('POST /login', () => {
  const agent = request.agent(app);
  beforeEach((done) => {
    agent
      .post('/auth/login')
      .send({
        email: 'zerohch0@gmail.com',
        password: 'nodejsbook',
      })
      .end(done);
  });

  test('์ด๋ฏธ ๋กœ๊ทธ์ธํ–ˆ์œผ๋ฉด redirect /', (done) => {
    const message = encodeURIComponent('๋กœ๊ทธ์ธํ•œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค.');
    agent
      .post('/auth/join')
      .send({
        email: 'zerohch0@gmail.com',
        nick: 'zerocho',
        password: 'nodejsbook',
      })
      .expect('Location', `/?error=${message}`)
      .expect(302, done);
  });
});

describe('POST /login', () => {
  test('๊ฐ€์ž…๋˜์ง€ ์•Š์€ ํšŒ์›', (done) => {
    const message = encodeURIComponent('๊ฐ€์ž…๋˜์ง€ ์•Š์€ ํšŒ์›์ž…๋‹ˆ๋‹ค.');
    request(app)
      .post('/auth/login')
      .send({
        email: 'zerohch1@gmail.com',
        password: 'nodejsbook',
      })
      .expect('Location', `/?loginError=${message}`)
      .expect(302, done);
  });

  test('๋กœ๊ทธ์ธ ์ˆ˜ํ–‰', (done) => {
    request(app)
      .post('/auth/login')
      .send({
        email: 'zerohch0@gmail.com',
        password: 'nodejsbook',
      })
      .expect('Location', '/')
      .expect(302, done);
  });

  test('๋น„๋ฐ€๋ฒˆํ˜ธ ํ‹€๋ฆผ', (done) => {
    const message = encodeURIComponent('๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.');
    request(app)
      .post('/auth/login')
      .send({
        email: 'zerohch0@gmail.com',
        password: 'wrong',
      })
      .expect('Location', `/?loginError=${message}`)
      .expect(302, done);
  });
});

describe('GET /logout', () => {
  test('๋กœ๊ทธ์ธ ๋˜์–ด์žˆ์ง€ ์•Š์œผ๋ฉด 403', (done) => {
    request(app)
      .get('/auth/logout')
      .expect(403, done);
  });

  const agent = request.agent(app);
  beforeEach((done) => {
    agent
      .post('/auth/login')
      .send({
        email: 'zerohch0@gmail.com',
        password: 'nodejsbook',
      })
      .end(done);
  });

  test('๋กœ๊ทธ์•„์›ƒ ์ˆ˜ํ–‰', (done) => {
    agent
      .get('/auth/logout')
      .expect('Location', `/`)
      .expect(302, done);
  });
});

afterAll(async () => {
  await sequelize.sync({ force: true });
});

4. ํšŒ์› ์ •๋ณด๋ถ€ํ„ฐ ๋งŒ๋“ค๊ธฐ

routes/auth.test.js ์ˆ˜์ •

  • ํ…Œ์ŠคํŠธ ์‹คํ–‰ํ•˜๋ฉด ์„ฑ๊ณตํ•จ
  • ์žฌ์ฐจ ์‹คํ–‰ํ•˜๋ฉด ์‹คํŒจํ•จ

๐Ÿ”ปroutes/auth.test.js

const request = require('supertest');
const { sequelize } = require('../models');
const app = require('../app');

beforeAll(async () => {
  await sequelize.sync();
});

describe('POST /join', () => {
  test('๋กœ๊ทธ์ธ ์•ˆ ํ–ˆ์œผ๋ฉด ๊ฐ€์ž…', (done) => {
    request(app)
      .post('/auth/join')
      .send({
        email: 'zerohch0@gmail.com',
        nick: 'zerocho',
        password: 'nodejsbook',
      })
      .expect('Location', '/')
      .expect(302, done);
  });
});

describe('POST /login', () => {
  const agent = request.agent(app);
  beforeEach((done) => {
    agent
      .post('/auth/login')
      .send({
        email: 'zerohch0@gmail.com',
        password: 'nodejsbook',
      })
      .end(done);
  });

  test('์ด๋ฏธ ๋กœ๊ทธ์ธํ–ˆ์œผ๋ฉด redirect /', (done) => {
    const message = encodeURIComponent('๋กœ๊ทธ์ธํ•œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค.');
    agent
      .post('/auth/join')
      .send({
        email: 'zerohch0@gmail.com',
        nick: 'zerocho',
        password: 'nodejsbook',
      })
      .expect('Location', `/?error=${message}`)
      .expect(302, done);
  });
});

describe('POST /login', () => {
  test('๊ฐ€์ž…๋˜์ง€ ์•Š์€ ํšŒ์›', (done) => {
    const message = encodeURIComponent('๊ฐ€์ž…๋˜์ง€ ์•Š์€ ํšŒ์›์ž…๋‹ˆ๋‹ค.');
    request(app)
      .post('/auth/login')
      .send({
        email: 'zerohch1@gmail.com',
        password: 'nodejsbook',
      })
      .expect('Location', `/?loginError=${message}`)
      .expect(302, done);
  });

  test('๋กœ๊ทธ์ธ ์ˆ˜ํ–‰', (done) => {
    request(app)
      .post('/auth/login')
      .send({
        email: 'zerohch0@gmail.com',
        password: 'nodejsbook',
      })
      .expect('Location', '/')
      .expect(302, done);
  });

  test('๋น„๋ฐ€๋ฒˆํ˜ธ ํ‹€๋ฆผ', (done) => {
    const message = encodeURIComponent('๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.');
    request(app)
      .post('/auth/login')
      .send({
        email: 'zerohch0@gmail.com',
        password: 'wrong',
      })
      .expect('Location', `/?loginError=${message}`)
      .expect(302, done);
  });
});

describe('GET /logout', () => {
  test('๋กœ๊ทธ์ธ ๋˜์–ด์žˆ์ง€ ์•Š์œผ๋ฉด 403', (done) => {
    request(app)
      .get('/auth/logout')
      .expect(403, done);
  });

  const agent = request.agent(app);
  beforeEach((done) => {
    agent
      .post('/auth/login')
      .send({
        email: 'zerohch0@gmail.com',
        password: 'nodejsbook',
      })
      .end(done);
  });

  test('๋กœ๊ทธ์•„์›ƒ ์ˆ˜ํ–‰', (done) => {
    agent
      .get('/auth/logout')
      .expect('Location', `/`)
      .expect(302, done);
  });
});

afterAll(async () => {
  await sequelize.sync({ force: true });
});


5. afterAll๋กœ ์ •๋ฆฌํ•˜๊ธฐ

routes/auth.test.js ์ˆ˜์ •

  • afterAll์€ ํ…Œ์ŠคํŠธ๊ฐ€ ์ข…๋ฃŒ๋œ ํ›„์— ์‹คํ–‰๋จ
  • DB ์ดˆ๊ธฐํ™”ํ•˜๊ธฐ

๐Ÿ”ปauth.test.js

const request = require('supertest');
const { sequelize } = require('../models');
const app = require('../app');

beforeAll(async () => {
  await sequelize.sync();
});

describe('POST /join', () => {
  test('๋กœ๊ทธ์ธ ์•ˆ ํ–ˆ์œผ๋ฉด ๊ฐ€์ž…', (done) => {
    request(app)
      .post('/auth/join')
      .send({
        email: 'zerohch0@gmail.com',
        nick: 'zerocho',
        password: 'nodejsbook',
      })
      .expect('Location', '/')
      .expect(302, done);
  });
});

describe('POST /login', () => {
  const agent = request.agent(app);
  beforeEach((done) => {
    agent
      .post('/auth/login')
      .send({
        email: 'zerohch0@gmail.com',
        password: 'nodejsbook',
      })
      .end(done);
  });

  test('์ด๋ฏธ ๋กœ๊ทธ์ธํ–ˆ์œผ๋ฉด redirect /', (done) => {
    const message = encodeURIComponent('๋กœ๊ทธ์ธํ•œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค.');
    agent
      .post('/auth/join')
      .send({
        email: 'zerohch0@gmail.com',
        nick: 'zerocho',
        password: 'nodejsbook',
      })
      .expect('Location', `/?error=${message}`)
      .expect(302, done);
  });
});

describe('POST /login', () => {
  test('๊ฐ€์ž…๋˜์ง€ ์•Š์€ ํšŒ์›', (done) => {
    const message = encodeURIComponent('๊ฐ€์ž…๋˜์ง€ ์•Š์€ ํšŒ์›์ž…๋‹ˆ๋‹ค.');
    request(app)
      .post('/auth/login')
      .send({
        email: 'zerohch1@gmail.com',
        password: 'nodejsbook',
      })
      .expect('Location', `/?loginError=${message}`)
      .expect(302, done);
  });

  test('๋กœ๊ทธ์ธ ์ˆ˜ํ–‰', (done) => {
    request(app)
      .post('/auth/login')
      .send({
        email: 'zerohch0@gmail.com',
        password: 'nodejsbook',
      })
      .expect('Location', '/')
      .expect(302, done);
  });

  test('๋น„๋ฐ€๋ฒˆํ˜ธ ํ‹€๋ฆผ', (done) => {
    const message = encodeURIComponent('๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.');
    request(app)
      .post('/auth/login')
      .send({
        email: 'zerohch0@gmail.com',
        password: 'wrong',
      })
      .expect('Location', `/?loginError=${message}`)
      .expect(302, done);
  });
});

describe('GET /logout', () => {
  test('๋กœ๊ทธ์ธ ๋˜์–ด์žˆ์ง€ ์•Š์œผ๋ฉด 403', (done) => {
    request(app)
      .get('/auth/logout')
      .expect(403, done);
  });

  const agent = request.agent(app);
  beforeEach((done) => {
    agent
      .post('/auth/login')
      .send({
        email: 'zerohch0@gmail.com',
        password: 'nodejsbook',
      })
      .end(done);
  });

  test('๋กœ๊ทธ์•„์›ƒ ์ˆ˜ํ–‰', (done) => {
    agent
      .get('/auth/logout')
      .expect('Location', `/`)
      .expect(302, done);
  });
});

afterAll(async () => {
  await sequelize.sync({ force: true });
});

6. ๋กœ๊ทธ์•„์›ƒ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

routes/auth.test.js ์ˆ˜์ •

...
describe('GET /logout', () => {
  test('๋กœ๊ทธ์ธ ๋˜์–ด์žˆ์ง€ ์•Š์œผ๋ฉด 403', (done) => {
    request(app)
      .get('/auth/logout')
      .expect(403, done);
  });

  const agent = request.agent(app);
  beforeEach((done) => {
    agent
      .post('/auth/login')
      .send({
        email: 'zerohch0@gmail.com',
        password: 'nodejsbook',
      })
      .end(done);
  });

  test('๋กœ๊ทธ์•„์›ƒ ์ˆ˜ํ–‰', (done) => {
    agent
      .get('/auth/logout')
      .expect('Location', `/`)
      .expect(302, done);
  });
});

afterAll(async () => {
  await sequelize.sync({ force: true });
});

๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ

1. ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ๋ž€

์„œ๋ฒ„๊ฐ€ ์–ผ๋งˆ๋งŒํผ์˜ ์š”์ฒญ์„ ๊ฒฌ๋”œ ์ˆ˜ ์žˆ๋Š”์ง€ ํ…Œ์ŠคํŠธ

  • ์„œ๋ฒ„๊ฐ€ ๋ช‡ ๋ช…์˜ ๋™์‹œ ์ ‘์†์ž๋ฅผ ์ˆ˜์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์˜ˆ์ธกํ•˜๊ธฐ ๋งค์šฐ ์–ด๋ ค์›€
  • ์‹ค์ œ ์„œ๋น„์Šค ์ค‘์ด ์•„๋‹ˆ๋ผ ๊ฐœ๋ฐœ ์ค‘์ผ ๋•Œ๋Š” ๋” ์–ด๋ ค์›€
  • ์ฝ”๋“œ์— ๋ฌธ์ œ๊ฐ€ ์—†๋”๋ผ๋„ ์„œ๋ฒ„ ํ•˜๋“œ์›จ์–ด ๋•Œ๋ฌธ์— ์„œ๋น„์Šค๊ฐ€ ์ค‘๋‹จ๋  ์ˆ˜ ์žˆ์Œ(๋ฉ”๋ชจ๋ฆฌ ๋ถ€์กฑ ๋ฌธ์ œ ๋“ฑ)
  • ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ๋ฏธ๋ฆฌ ์˜ˆ์ธกํ•  ์ˆ˜ ์žˆ์Œ

2. Artillery ์‚ฌ์šฉํ•˜๊ธฐ

์ƒˆ ์ฝ˜์†”์—์„œ ๋‹ค์Œ ๋ช…๋ น์–ด ์ž…๋ ฅ

  • Count ์˜ต์…˜์€ ๊ฐ€์ƒ์˜ ์‚ฌ์šฉ์ž ์ˆ˜
  • N ์˜ต์…˜์€ ํšŸ์ˆ˜
  • 100๋ช…์˜ ์‚ฌ์šฉ์ž๊ฐ€ 50๋ฒˆ์”ฉ ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ์ƒํ™ฉ
    ๊ฒฐ๊ณผ ๋ณด๊ณ ์„œ
  • ์‚ฌ์šฉ์ž ์ƒ์„ฑ(scenarios launched)
  • ํ…Œ์ŠคํŠธ ์„ฑ๊ณต(scenarios completed)
  • ์š”์ฒญ ์„ฑ๊ณต ํšŸ์ˆ˜(requests completed)
  • ์ดˆ๋‹น ์š”์ฒญ ์ฒ˜๋ฆฌ ํšŸ์ˆ˜(RPS sent)
  • ์‘๋‹ต ์ง€์—ฐ ์†๋„(Request latency)
  • Min: ์ตœ์†Œ, Max: ์ตœ๋Œ€, median: ์ค‘์•™๊ฐ’
  • P95: ํ•˜์œ„ 95%, P99: ํ•˜์œ„ 99%
    • ํ•˜์œ„๋Š” ์†๋„ ์ˆœ์„œ๋ฅผ ๋งํ•จ
  • Median๊ณผ P95๊ฐ€ ๋งŽ์ด ์ฐจ์ด๋‚˜์ง€ ์•Š๋Š” ๊ฒŒ ์ข‹์Œ

3. ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€ ์š”์ฒญ ์‹œ๋‚˜๋ฆฌ์˜ค

loadtest.json์— ์‚ฌ์šฉ์ž์˜ ํ–‰๋™ ํ๋ฆ„ ์ž‘์„ฑ ๊ฐ€๋Šฅ

  • target: ์š”์ฒญ ๋„๋ฉ”์ธ
  • Phases์—์„œ duration: ๋ช‡ ์ดˆ ๋™์•ˆ(60์ดˆ)
  • arrivalRate: ๋งค ์ดˆ ๋ช‡ ๋ช…(30๋ช…)
  • flow: ์‚ฌ์šฉ์ž์˜ ์ด๋™
  • get, post ๋“ฑ์˜ ๋ฉ”์„œ๋“œ๋ฅผ ๋‚˜ํƒ€๋ƒ„
  • url์€ ์ด๋™ํ•œ url
  • json์€ ์„œ๋ฒ„๋กœ ์ „์†กํ•œ ๋ฐ์ดํ„ฐ
  • ํ˜„์žฌ GET /, POST /auth/login, GET /hashtag ์ˆœ

๐Ÿ”ปloadtest.json

{
  "config":{
    "target": "http://localhost:8001",
    "phases": [
      {
        "duration": 60,
        "arrivalRate": 30
      }
    ]
  },
  "scenarios": [{
    "flow": [{
      "get": {
        "url": "/"
      }
    }, {
      "post": {
        "url": "/auth/login",
        "json": {
          "email": "zerohch0@naver.com",
          "password": "nodejsbook"
        }
      }
    }, {
      "get": {
        "url": "/hashtag?hashtag=nodebird"
      }
    }]
  }]
}

4. ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€ ์š”์ฒญ ์‹œ๋‚˜๋ฆฌ์˜ค(์‹คํ–‰)

๋ฌธ์ œ์  ๋ฐœ๊ฒฌ

  • ์š”์ฒญ ํ›„๋ฐ˜๋ถ€๊ฐ€ ๋  ์ˆ˜๋ก ์‘๋‹ต ์‹œ๊ฐ„์ด ๊ธธ์–ด์ง

  • ์ฒซ ์‘๋‹ต์€ 4.7๋ฐ€๋ฆฌ์ดˆ, ๋งˆ์ง€๋ง‰ ์‘๋‹ต์€ 51์ดˆ

  • 5400๊ฐœ์˜ ์š”์ฒญ์€ 200 ์‘๋‹ต์ฝ”๋“œ, 1800๊ฐœ๋Š” 302

  • ์„œ๋ฒ„๊ฐ€ ์ง€๊ธˆ ์ •๋„์˜ ์š”์ฒญ์„ ๊ฐ๋‹นํ•˜์ง€ ๋ชปํ•จ

  • ์„œ๋ฒ„ ์‚ฌ์–‘์„ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜๊ฑฐ๋‚˜, ์—ฌ๋Ÿฌ ๊ฐœ ๋‘๊ฑฐ๋‚˜

  • ์ฝ”๋“œ๋ฅผ ๋” ํšจ์œจ์ ์œผ๋กœ ๊ฐœ์„ ํ•˜๋Š” ๋ฐฉ๋ฒ• ๋“ฑ.

  • ํ˜„์žฌ๋Š” ์‹ฑ๊ธ€์ฝ”์–ด๋งŒ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, ํด๋Ÿฌ์Šคํ„ฐ๋ง ๊ธฐ๋ฒ• ๋„์ž…์„ ์‹œ๋„ํ•ด๋ณผ๋งŒ ํ•จ

  • arrivalRate๋ฅผ ์ค„์ด๊ฑฐ๋‚˜ ๋Š˜๋ ค์„œ ์–ด๋Š ์ •๋„ ์ˆ˜์šฉ ๊ฐ€๋Šฅํ•œ์ง€ ์ฒดํฌํ•ด๋ณด๋Š” ๊ฒƒ์ด ์ข‹์Œ

  • ์—ฌ๋Ÿฌ ๋ฒˆ ํ…Œ์ŠคํŠธํ•˜์—ฌ ํ‰๊ท ์น˜๋ฅผ ๋‚ด๋ณด๋Š” ๊ฒŒ ์ข‹์Œ

5. ํ…Œ์ŠคํŠธ ๋ฒ”์œ„

์–ด๋–ค ๊ฒƒ์„ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์–ด๋–ค ๊ฒƒ์„ ํ…Œ์ŠคํŠธ ์•ˆ ํ•  ์ง€ ๊ณ ๋ฏผ๋จ.

  • ์ž์‹ ์ด ์ง  ์ฝ”๋“œ๋Š” ์ตœ๋Œ€ํ•œ ๋งŽ์ด ํ…Œ์ŠคํŠธํ•˜๊ธฐ

  • npm์„ ํ†ตํ•ด ์„ค์น˜ํ•œ ํŒจํ‚ค์ง€๋Š” ํ…Œ์ŠคํŠธํ•˜์ง€ ์•Š์Œ(๊ทธ๊ฑธ ๋งŒ๋“  ์‚ฌ๋žŒ์˜ ๋ชซ์ž„)

  • ์šฐ๋ฆฌ๋Š” ๊ทธ ํŒจํ‚ค์ง€/๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ถ€๋ถ„๋งŒ ํ…Œ์ŠคํŠธ

  • ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์–ด๋ ค์šด ํŒจํ‚ค์ง€๋Š” ๋ชจํ‚น

  • ๋ชจํ‚นํ•ด์„œ ํ†ต๊ณผํ•˜๋”๋ผ๋„ ์‹ค์ œ ์ƒํ™ฉ์—์„œ๋Š” ์—๋Ÿฌ๋‚  ์ˆ˜ ์žˆ์Œ์„ ์—ผ๋‘์— ๋‘์–ด์•ผ ํ•จ

  • ์‹œ์Šคํ…œ ํ…Œ์ŠคํŠธ: QA์ฒ˜๋Ÿผ ํ…Œ์ŠคํŠธ ๋ชฉ๋ก์„ ๋‘๊ณ  ์ฒดํฌํ•ด๋‚˜๊ฐ€๋ฉด์„œ ์ง„ํ–‰ํ•˜๋Š” ํ…Œ์ŠคํŠธ

  • ์ธ์ˆ˜ ํ…Œ์ŠคํŠธ: ์•ŒํŒŒ ํ…Œ์ŠคํŠธ/๋ฒ ํƒ€ ํ…Œ์ŠคํŠธ์ฒ˜๋Ÿผ ํŠน์ • ์‚ฌ์šฉ์ž ์ง‘๋‹จ์ด ์‹ค์ œ๋กœ ํ…Œ์ŠคํŠธ

  • ๋‹ค์–‘ํ•œ ์ข…๋ฅ˜์˜ ํ…Œ์ŠคํŠธ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ด ์„œ๋น„์Šค๋ฅผ ์•ˆ์ •์ ์œผ๋กœ ์œ ์ง€ํ•˜๋Š” ๊ฒŒ ์ข‹์Œ

๐Ÿ˜ƒ์ถœ์ฒ˜๐Ÿ˜ƒ
Node.js ๊ต๊ณผ์„œ - ๊ธฐ๋ณธ๋ถ€ํ„ฐ ํ”„๋กœ์ ํŠธ ์‹ค์Šต๊นŒ์ง€
https://www.inflearn.com/course/%EB%85%B8%EB%93%9C-%EA%B5%90%EA%B3%BC%EC%84%9C/dashboard

0๊ฐœ์˜ ๋Œ“๊ธ€