Skip to content

Latest commit

ย 

History

History
486 lines (447 loc) ยท 25.2 KB

20211126.md

File metadata and controls

486 lines (447 loc) ยท 25.2 KB

React

์ง€๋‚œ ์‹œ๊ฐ„ ๋ณต์Šต ๋ฐ ์ˆ˜์Šต

  • ์ง€๋‚œ ์‹œ๊ฐ„ package.json์—์„œ 'start' ์Šคํฌ๋ฆฝํŠธ๋กœ '&'์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ณ‘๋ ฌ ๋ช…๋ น(npm run dev:compile-watch & npm run dev:server-open)์„ ํ–ˆ๋Š”๋ฐ, ์œˆ๋„์šฐ ํ™˜๊ฒฝ์—์„œ๋Š” ์ž˜ ์•ˆ๋˜๋‹ˆ๊นŒ npm-run-all ํŒจํ‚ค์ง€๋ฅผ ๊น”๊ณ  script๋ฅผ run-p dev:compile-watch dev:server-open์œผ๋กœ ๋ฐ”๊ฟ”์ฃผ์—ˆ๋‹ค.
  • ๋ชจ๋“ˆํ™”ํ•˜๋ฉด์„œ ์ƒ๊ธด ์—ฌ๋Ÿฌ ๋ชจ๋“ˆ ํŒŒ์ผ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋‹ค ์ปดํŒŒ์ผํ•ด์ค˜์•ผํ•˜๋ฏ€๋กœ, dev:compile-watch ๋ช…๋ น์–ด๊ฐ€ ์‹คํ–‰ํ•  ์Šคํฌ๋ฆฝํŠธ๋Š” "npm run dev:compile -- -w"์—์„œ dev:compile์ด ์•„๋‹ˆ๋ผ dev:compile-dir๋กœ ๋ฐ”๊ฟ”์ค˜์•ผ. (dev:compile์€ main.js ํŒŒ์ผ๋งŒ ์ปดํŒŒ์ผํ•ด์ฃผ๊ณ  ์žˆ์—ˆ๋‹ค.)
  • index.html ํŒŒ์ผ์—์„œ contents๋ผ๋Š” ํด๋ž˜์Šค๋ฅผ ๊ฐ€์ง„ div์˜ lang ์†์„ฑ๋„ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ ๋•Œ๋งˆ๋‹ค toggle ์‹œ์ผœ์ฃผ์–ด์•ผ๋งŒ ํ•œ๋‹ค.
    • renderUpdatedUI ํ•จ์ˆ˜ ์•ˆ์—์„œ $('.contents').attr('lang', translator.currentMode)๋ฅผ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.
  • ์–ด์ œ ์šฐ๋ฆฌ๋Š” @types/node, @types/live-server๋ฅผ ์„ค์น˜ํ–ˆ๋‹ค. typescript์™€ vscode ๋ชจ๋‘ MS์‚ฌ๊ฐ€ ๋งŒ๋“  ๊ฑฐ๋ผ ์„œ๋กœ ์นœํ™”์ ์ด๋ฉฐ ํŽธ๋ฆฌํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.

preparing ์‹ค์Šต

package.json ๋ฐ server.js์˜ ์„œ๋ฒ„ํ™˜๊ฒฝ์„ commonJS์™€ ESM ๋ฒ„์ „์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ

  • nodejs.dev์—์„œ package.json Guide์„ ์ฝ๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ •ํ•ด์ฃผ์ž
    • "private": true: npm์— ์˜ฌ๋ ค ๊ณต๊ฐœํ•  package๋ผ๋ฉด public์œผ๋กœ ํ•ด๋„ ๋˜์ง€๋งŒ ์šฐ๋ฆฌ๋Š” ์•„๋‹ˆ๋‹ˆ๊นŒ
    • "name": "preparing": package์ด๋ฆ„ ๋ ๊ฑฐ๋‹ˆ๊นŒ ๋Œ€๋ฌธ์ž ์“ฐ์ง€ ๋ง๊ฒƒ
    • "version": "0.0.1"
    • "scripts": { "start": "", "dev": "node server/index", "test": "" }: ์ผ๋‹จ ๋„ฃ์–ด๋‘๊ธฐ
    • "type": "module": server.js์—์„œ commonJS๋ฅผ ์‚ฌ์šฉํ•˜์ง€๋งŒ ๋ธŒ๋ผ์šฐ์ €์˜ ESM๊ณผ ๋™์ผํ•˜๊ฒŒ ํ•˜๋ ค๋ฉด ๋„ฃ์–ด์ฃผ์ž
  • ์ด ์ƒํƒœ์—์„œ "npm run dev"๋กœ server์„ ๋Œ๋ฆฌ๋ฉด live-server์„ importํ•˜๋Š” ๊ตฌ๋ฌธ์ธ require์„ ํ•ด์„ํ•˜์ง€ ๋ชปํ•ด reference error๊ฐ€ ๋‚œ๋‹ค.
    • commonJS๋กœ ๋ฐ”๊พธ์–ด ํ•ด๊ฒฐํ•˜๊ธฐ: server/index.js์˜ ํ™•์žฅ์ž๋ฅผ server/index.cjs๋กœ ๋ฐ”๊พผ๋‹ค.
    • ESM์œผ๋กœ ๋ฐ”๊พธ์–ด ํ•ด๊ฒฐํ•˜๊ธฐ: server/index.js์˜ ํ™•์žฅ์ž๋ฅผ server/index.mjs๋กœ ๋ฐ”๊พธ๊ณ  const liveServer = require('live-server')๋ฅผ import liveServer from 'live-server'๋กœ ๋ฐ”๊พผ๋‹ค.
    • node.jsํ™˜๊ฒฝ์—์„œ ESM์„ ๊ธฐ๋ณธ์œผ๋กœ ์„ค์ •ํ•œ ๊ฒฝ์šฐ, ์ฆ‰ package.json์— "type":"module"๋ฅผ ์ง€์ •ํ•œ ๊ฒฝ์šฐ ๋ช…๋ น์–ด์— ํ™•์žฅ์ž๋ฅผ ์ƒ๋žตํ•˜๋ฉด ์•ˆ๋œ๋‹ค. scripts์˜ "dev" ๋ช…๋ น์–ด์— ๊ผญ ํ™•์žฅ์ž(node server/index.mjs)๋ฅผ ์ง€์ •ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค. ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด 'cannot find module'์—๋Ÿฌ!

server์—์„œ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •ํ•˜๊ธฐ

// server/index.mjs
import liveServer from 'live-server';

const params = {
  host: 'localhost',
  port: 3000,
  open: false
}

liveServer.start(params);
  • ์—ฌ๊ธฐ์„œ port์™€ open์— ์ฃผ์–ด์งˆ params๋ฅผ CLI ๋ช…๋ น์–ด๋กœ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ธํŒ…ํ•ด์ฃผ๋ฉด์„œ server์„ ๊ตฌ๋™์‹œ์ผœ๋ณด์ž
    • ๋จผ์ € const { PORT, OPEN } = process.env;๋กœ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ๋ฐ›๊ณ , null ๋ณ‘ํ•ฉ์—ฐ์‚ฐ์ž๋กœ port์™€ open์— ๊ฐ๊ฐ ๋„ฃ์–ด์ค€๋‹ค.
    • null ๋ณ‘ํ•ฉ ์—ฐ์‚ฐ์ž ๋Œ€์‹  or ๋‹จ์ถ•ํ‰๊ฐ€๋ฒ•(||)์„ ์‚ฌ์šฉํ•˜๊ธฐ๋„ ํ•˜์ง€๋งŒ, ๊ทธ๋Ÿฐ ๊ฒฝ์šฐ '0'์ด๋‚˜ ๋นˆ ๋ฌธ์ž์—ด์ด falsy๋กœ ํ‰๊ฐ€๋˜๋ฏ€๋กœ ๋”์šฑ ์•ˆ์ „ํ•œ null ๋ณ‘ํ•ฉ ์—ฐ์‚ฐ์ž๋ฅผ ์จ์ฃผ๋ฉด ์ข‹๋‹ค.
const { PORT, OPEN } = process.env;
const params = {
  host: 'localhost',
  port: PORT ?? 3000,
  open: OPEN ?? false
}
  • ๊ทธ๋Œ€๋กœ server์„ ๋Œ๋ฆฌ๋ฉด ์•„์ง PORT์™€ OPEN์„ ๋„ฃ์–ด์ฃผ์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์— ๋‘˜๋‹ค undefined ๊ฐ’์„ ๊ฐ€์ง„๋‹ค.
  • bash shell์—์„œ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•  ๋•Œ, (scripts์—) ํ™˜๊ฒฝ๋ณ€์ˆ˜=๊ฐ’์˜ ํ˜•ํƒœ๋กœ ๋„ฃ์–ด์ฃผ๊ธฐ๋งŒ ํ•˜๋ฉด ๋œ๋‹ค. 1ํšŒ์„ฑ์œผ๋กœ ์„ค์ •ํ•ด์ค€ ํ™˜๊ฒฝ ๋ณ€์ˆ˜์ด๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น ํ”„๋กœ์„ธ์Šค๊ฐ€ ์‚ด์•„์žˆ๋Š” ๋™์•ˆ์—๋งŒ ์œ ํšจํ•˜๋ฉฐ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ข…๋ฃŒํ•˜๋ฉด ์‚ฌ๋ผ์ง„๋‹ค.
    • "dev": "PORT=8080 node server/index.mjs"๋กœ, "start": "OPEN='/client/public' npm run dev"์œผ๋กœ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•˜๋ฉฐ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰์‹œํ‚จ๋‹ค.
  • ํ•˜์ง€๋งŒ window ํ™˜๊ฒฝ์—์„œ๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •์ด ์ด๊ฑธ๋กœ ์ž˜ ์•ˆ๋ ๊ฑฐ๋‹ค. ๊ทธ๋Ÿฌ๋‹ˆ๊นŒ cross-env๋ผ๋Š” ํŒจํ‚ค์ง€๋ฅผ ๊น”์•„์„œ ๋ชจ๋“  scripts์˜ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ • CLI ๋ช…๋ น์–ด์— cross-env๋ฅผ ์•ž์— ๋ถ™์—ฌ์ค€๋‹ค.
// bash ๋ช…๋ น์–ด
$ npm i -D cross-env 

// package.json
"scripts": {
    "start": "cross-env OPEN='/client/public' npm run dev",
    "dev": "cross-env PORT=8080 node server/index.mjs",
    // ๊ธฐํƒ€ ์Šคํฌ๋ฆฝํŠธ
},
  • ์ด์ œ server ๊ฑด๋“œ๋ฆด ์ผ ์—†๊ธด ํ•œ๋ฐ ๊ทธ๋ž˜๋„ server ํŒŒ์ผ์— ๋ณ€ํ™”๊ฐ€ ์žˆ์„ ๋•Œ๋งˆ๋‹ค ๊ฐ์ง€ํ•˜์—ฌ ๋‹ค์‹œ ๊ตฌ๋™ํ•ด์ฃผ๋Š” watch ์˜ต์…˜์„ ๊ฐ€์ง„ ํŒจํ‚ค์ง€๋„ ๋‹ค์šด๋ฐ›์•„๋ณด์ž. ์ด์ „์— nodemon์„ ์จ๋ดค์„ํ…Œ๋‹ˆ ์˜ค๋Š˜์€ node-dev๋กœ (๊ทผ๋ฐ window๋Š” node-dev ์•ˆ๋˜๋‹ˆ๊นŒ nodemon์œผ๋กœ)
    • $ npm i -D node-dev๋กœ ์„ค์น˜ํ•˜๊ณ , package.json์—์„œ "dev" ๋ช…๋ น์–ด์— node ๋Œ€์‹  node-dev๋ฅผ ๋„ฃ์–ด์ค€๋‹ค.

client ํŒŒ์ผ ์ž‘์—…

HTML head ํƒœ๊ทธ

  • ๊ธฐ๋ณธ ์„ค์ • ๋ฉ”ํƒ€ํƒœ๊ทธ ๋ถ„์„ํ•ด๋ณด์ž
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  • http-equiv๋Š” ๋ฌธ์„œ์˜ ์ดˆ๊ธฐ์ •๋ณด๋ฅผ ์ง€์ •ํ•˜๋Š” ์†์„ฑ์œผ๋กœ, content๋ฅผ ๊ผญ ๊ธฐ์žฌํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.
    • ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ ์„ค์ •์„ ๋‚˜ํƒ€๋‚ด๋Š” X-UA-Compatible์—์„œ X๋Š” ์‹คํ—˜์ ์ธ ๋‹จ๊ณ„๋ฅผ, UA๋Š” User Agent๋ฅผ, Compatible์€ ํ˜ธํ™˜์„ ๋‚˜ํƒ€๋‚ด๋ฉฐ, content๋กœ ์˜ค๋Š” "IE=edge"๋Š” Internet Explorer์˜ ๊ฐ€์žฅ ์ตœ์‹ (edge) ๋‹จ๊ณ„๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค.
    • ์ด๊ฒƒ์€ ๊ณง ์ด html ๋ฌธ์„œ๋ฅผ ์—ด ๋•Œ ์‚ฌ์šฉํ•  ๋ Œ๋”๋ง ์—”์ง„์„ ์ง€์ •ํ•˜๋Š” ๊ฒƒ์œผ๋กœ, IE์˜ ์ตœ์‹  ๋ Œ๋”๋ง ์—”์ง„์„ ์‚ฌ์šฉํ•˜๋„๋ก ์ง€์ •ํ–ˆ๋‹ค.
    • ํ•œ ๋•Œ IE๊ฐ€ ํ‘œ์ค€์„ ๋„ˆ๋ฌด ์•ˆ ์ง€์ผฐ๊ธฐ ๋•Œ๋ฌธ์—, "IE=9"๋กœ ์ง€์ •๋˜๋Š” ๊ฒฝ์šฐ๋Š” ๋งŒ์ผ IE๋กœ ์—ด๋ฆฌ๋ฉด ์ตœ์†Œํ•œ 9๋ฒ„์ „์œผ๋กœ ๋ Œ๋”๋งํ•˜๊ฒŒ๋” ํ•ด๋‹ฌ๋ผ๋Š” ์š”์ฒญ์ด๋‹ค.
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  • viewport๊ฐ€ device-width๋กœ ์„ค์ •๋˜๋Š” ๊ฒฝ์šฐ, ์‚ฌ์šฉ์ž์˜ ๊ธฐ๊ธฐ์— ๋งž๊ฒŒ ๋งž์ถฐ์ง„๋‹ค.
  • ์ ‘๊ทผ์„ฑ ๊ด€์ ์—์„œ "user-scalable=no"๋Š” ์“ฐ์ง€ ์•Š๋Š” ๊ฒƒ์ด ๊ถŒ์žฅ๋œ๋‹ค. ํ™•๋Œ€ํ•ด์„œ ๋ด์•ผํ•  ๋•Œ๊ฐ€ ์žˆ์œผ๋‹ˆ๊นŒ.
  • ๊ทธ ๋ฐ–์—๋„ head ํƒœ๊ทธ ์•ˆ์— SEO, favicon, webfonts, open-graph info ๋“ฑ์„ ๋„ฃ์–ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.
  • favicon(favorite icon)์€ ํ•ด๋‹น ์‚ฌ์ดํŠธ์˜ ์•„์ด๋ดํ‹ฐํ‹ฐ๋ฅผ ์œ„ํ•ด ํ•„์ˆ˜์ ์ธ๋ฐ, linkํƒœ๊ทธ์—์„œ 'link:icon'์œผ๋กœ emmet ์†๊ธฐ๋ฒ•์„ ์‚ฌ์šฉํ•˜๋ฉด <link rel="shortcut icon">์ด ๋˜๋Š”๋ฐ ์—ฌ๊ธฐ์„œ shortcut์€ ์‚ญ์ œํ•˜๊ณ  ๊ทธ๋ƒฅ icon์œผ๋กœ.
  • webfont๋„ spoqa han sans๋กœ CDN link ํƒœ๊ทธ๋ฅผ ๊ฑธ์–ด์ฃผ์ž.
<link rel="stylesheet" href="//spoqa.github.io/spoqa-han-sans/css/SpoqaHanSansNeo.css" />
<link rel="icon" href="//vectorlogo.zone/logos/reactjs/reactjs-icon.svg" />
<link rel="stylesheet" href="./css/main.css" />

Test Driven Development

  • client/src ์•ˆ์— utils๋ผ๋Š” ํด๋”๋ฅผ ๋งŒ๋“ค๊ณ , tests.js, index.js, throwError.js, getRandom.js, transformText.js ๋ชจ๋“ˆํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์ฃผ์ž.
  • ํ…Œ์ŠคํŠธ ์ฃผ๋„ ๊ฐœ๋ฐœ์ด๋ž€, ๋จผ์ € ์–ด๋–ค ํ•จ์ˆ˜๊ฐ€ ํ•„์š”ํ•œ์ง€ ์ƒ๊ฐ(think)ํ•˜๊ณ , testํ•˜๊ณ , code๋ฅผ ์งœ๊ณ  refactoringํ•œ๋‹ค!
  • ์ฑ…์„ ์“ธ ๋•Œ ๊ธฐํš์ž์™€ ๋ฏธํŒ…ํ•˜๋Š” ๊ฒŒ think, ๋ชฉ์ฐจ๋ฅผ ์งœ๋Š” ๊ฒŒ test, ์‹ค์ œ๋กœ ๊ธ€์„ ์“ฐ๋Š” ๊ฒŒ code, ๋‹ค๋“ฌ๋Š” ๊ฒŒ Refactor์ด๋ผ๊ณ  ๋ณด๋ฉด ๋œ๋‹ค.
  • ๋ณดํ†ต์€ test๋Š” library๋กœ ๋งŽ์ด ํ•˜์ง€๋งŒ, ์ด๋ฒˆ์— ํ•œ ๋ฒˆ ์ง์ ‘ ์งœ๋ณด๋„๋ก ํ•˜์ž.
    • describe: ๊ธฐ์ˆ ํ•˜๋Š” utility
    • test: ํ…Œ์ŠคํŠธ utility
    • expect: ๊ธฐ๋Œ€๊ฐ’์„ ๊ฒ€ํ† ํ•˜๋Š” utility

expect utility

  • expect(์ „๋‹ฌ๊ฐ’).toBe(๊ธฐ๋Œ€๊ฐ’)์œผ๋กœ ๊ฐ™์€์ง€ ๋น„๊ตํ•ด์ฃผ๊ฑฐ๋‚˜, expect(๋…ธ๋“œ).toBeInTheDocument()๋กœ ํ•ด๋‹น ๋…ธ๋“œ๊ฐ€ ๋ฌธ์„œ ์•ˆ์— ์กด์žฌํ•˜๋Š”์ง€๋ฅผ ์•Œ์•„๋ณผ ์ˆ˜ ์žˆ์œผ๋ฉฐ, expect(์ „๋‹ฌ๊ฐ’).not.toBe(๊ธฐ๋Œ€๊ฐ’) ๋“ฑ์œผ๋กœ ๋ฐ˜๋Œ€ ๊ฒฐ๊ณผ๋„ ํ™•์ธ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.
// utils/tests.js
function expect(received) {
  // ์ „๋‹ฌ๊ฐ’๊ณผ ๋น„๊ตํ•  ์ˆ˜ ์žˆ๋Š” utility ๋ชจ์Œ ๊ฐ์ฒด ๋ฐ˜ํ™˜
  return {
    toBe(expected) {
      // ์ „๋‹ฌ๊ฐ’๊ณผ ๊ธฐ๋Œ€๊ฐ’์ด ๊ฐ™์ง€ ์•Š์œผ๋ฉด ์˜ค๋ฅ˜
      if (received !== expected) {
        throwError(`${received}์™€ ${expected}์˜ ๊ฐ’์ด ๋™์ผํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.`);
      }
    }, 
    toBeInTheDocument() {
      if (!document.body.contains(received)){
        throwError(`${received}๋Š” ๋ฌธ์„œ ์•ˆ์— ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.`);
      }
    }
    not: {
      toBe(expected) {
        if (received === expected) {
          throwError(`${received}์™€ ${expected}์˜ ๊ฐ’์ด ๋™์ผํ•ฉ๋‹ˆ๋‹ค.`);
        }
      },
      toBeInTheDocument() {
        if (document.body.contains(received)) {
          throwError(`${received}๋Š” ๋ฌธ์„œ ์•ˆ์— ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.`);
        }
      }
    }
  }
}
  • ์—ฌ๊ธฐ์—์„œ expect๊ฐ€ ๋ฆฌํ„ดํ•˜๋Š” ๊ฐ์ฒด ์† received๋Š” ํ•จ์ˆ˜์™€ ๊ฐ™์ด ํด๋กœ์ €๋กœ expectํ•จ์ˆ˜์—๊ฒŒ ์ฃผ์–ด์ง„ ์ธ์ž received๋ฅผ ์ฐธ์กฐํ•˜๊ณ  ์žˆ๋‹ค.
  • throw new Error์„ ๋งค๋ฒˆ ํ•˜๊ธฐ ๊ท€์ฐฎ์œผ๋‹ˆ๊นŒ utility func๋กœ ๋นผ์ฃผ๊ณ  tests.js์—์„œ importํ•ด์ค€๋‹ค
// utils/throwError.js

export function throwError(errorMsg) {
  throw new Error(errorMsg);
}

test utility

  • ์ฝ”๋“œ ์‚ฌ์šฉ ์˜ˆ์‹œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.
test('1+1=2', () => expect('1+1').toBe(2));
  • ๋งŒ๋“ค์–ด๋ณด์ž
function test(description, callback) {
  try { // ์‚ฌ์šฉ์ž๊ฐ€ ์ „๋‹ฌํ•œ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰
    callback();    
    console.log(`๐Ÿ˜ƒํ…Œ์ŠคํŠธ ์„ฑ๊ณต: ${description}`)
  } catch(error) {
    console.groupCollapsed(`๐Ÿ˜ซํ…Œ์ŠคํŠธ ์‹คํŒจ: ${description}`);
    console.error(error.message);
    console.groupEnd();
  }
}
  • console.groupCollapsed๋Š” console.groupEnd๊ฐ€ ํ˜ธ์ถœ๋˜๊ธฐ ์ „๊นŒ์ง€์˜ ๊ธด ์ฝ˜์†”๋ฉ”์‹œ์ง€๋ฅผ ๋‹ซํž˜์ƒํƒœ๋กœ ์ œ๊ณตํ•œ๋‹ค.

describe utility

  • ์ฝ”๋“œ ์‚ฌ์šฉ ์˜ˆ์‹œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.
describe(ํ…Œ์ŠคํŠธ ๋ฆฌ์ŠคํŠธ ํ•ญ๋ชฉ์„ ๋Œ€๋ณ€ํ•˜๋Š” ๋ ˆ์ด๋ธ”, () => {
  test();
  test();
  test();
  ...
})
  • ๋งŒ๋“ค์–ด๋ณด์ž
function describe(testLabel, callback) {
  console.group(testLabel);
  callback();
  console.groupEnd();
}
  • console.group์€ ์ƒ์œ„์—์„œ ์—ฌ๋Ÿฌ๊ฐœ์˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋ฌถ์–ด์ฃผ๊ธฐ๋งŒ ํ•˜๋Š” ์• ๋‹ค.

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

  • ์—ฌ๊ธฐ๊นŒ์ง€ ์šฐ๋ฆฌ๋Š” describe, test, expect๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ํ…Œ์ŠคํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ง์ ‘ ์ž‘์„ฑํ•ด๋ณด์•˜๋‹ค.
    • ์ด์ œ ์ด ์„ธ๊ฐœ์˜ ํ•จ์ˆ˜๋ฅผ exportํ•ด์ฃผ๊ณ , public/index.html์— scriptํƒœ๊ทธ๋กœ src/main.js๋ฅผ type="module"๋กœ ๋ถˆ๋Ÿฌ์˜ค์ž
<script type="module" src="./src/main.js"></script>
  • main.js์—์„œ ํ…Œ์ŠคํŠธ์ฝ”๋“œ๋ฅผ ์จ๋ณด์ž
import { describe, test, expect } from './utils/tests.js';

describe('์ดˆ๋“ฑ์ˆ˜ํ•™', () => {
  test('10 * 20 - 8 = 192', () => {
    expect(10 * 20 - 8).toBe(192);
  });
  test('1+1=11', () => {
    expect(1+1).toBe(11);
  });
})
  • ์ด์ œ ์„œ๋ฒ„๋ฅผ ๋Œ๋ฆฌ๊ณ  index.html์„ ์š”์ฒญํ•˜๋ฉด ์ฝ˜์†”์— ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ค€๋‹ค.
    • ์ดˆ๋“ฑ์ˆ˜ํ•™ ํ…Œ์ŠคํŠธ๊ฐ€ ์„ฑ๊ณตํ•˜์—ฌ ์ฝ˜์†”์— ์ ์ ˆํ•œ ์„ฑ๊ณต๋ฌธ๊ตฌ์™€ ์‹คํŒจ ๋ฌธ๊ตฌ๊ฐ€ ๋œจ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค!

getRandom ํ•จ์ˆ˜๋ฅผ ํ…Œ์ŠคํŠธ ์ฃผ๋„ ๊ฐœ๋ฐœ๋กœ ๋งŒ๋“ค์–ด๋ณด๊ธฐ

  • ์ด์ œ getRandom ํ•จ์ˆ˜๋ฅผ ์œ„ํ•œ ํ…Œ์ŠคํŠธ ํŒŒ์ผ์„ ๋จผ์ € ๋งŒ๋“ค๋ฉฐ ํ…Œ์ŠคํŠธ์ฃผ๋„๊ฐœ๋ฐœ์„ ์‹ค์Šตํ•ด๋ณด์ž.
// src/utils/getRandom.js์— ํ•จ์ˆ˜ ํ‹€๋งŒ ๋งŒ๋“ค์–ด๋‘๊ณ  export
export const getRandom = () => {};
export const getRandomCount = () => {};

// src/utils/index.js์—์„œ re-export
export { getRandom, getRandomCount } from './getRandom.js';
export * from './tests.js';

// src/utils/getRandom.test.js ํŒŒ์ผ์„ ๋งŒ๋“ค๊ณ  ํ•„์š”ํ•œ ์ž์›์„ importํ•œ ํ›„ ๊ทธ ์•ˆ์—์„œ ํ…Œ์ŠคํŠธ ๋กœ์ง ๋จผ์ € ์ง ๋‹ค.
import { test, expect } from './tests.js';
import { getRandom, getRandomCount } from './getRandom.js';

test('getRandom(10)์€ 10๋ณด๋‹ค ์ž‘๊ฑฐ๋‚˜ ๊ฐ™๊ณ  0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค.', () => {
  let targetCount = getRandom(10);
  console.log(`getRandom(10) = ${targetCount})
  expect(targetCount>10).toBe(false);
  expect(targetCount>=0).toBe(true);
});

// src/main.js
import './utils/getRandom.test.js';
  • ํ˜„ ์ƒํƒœ์—์„œ main.js๋ฅผ ๋กœ๋“œํ•˜๋Š” html์„ ๋ธŒ๋ผ์šฐ์ €์— ๋„์šฐ๋ฉด ํ…Œ์ŠคํŠธ ์‹คํŒจ๊ฐ€ ๋œฌ๋‹ค. ์•„์ง ํ•จ์ˆ˜ ๋กœ์ง์„ ์•ˆ ์งฐ์œผ๋‹ˆ๊นŒ ๋‹น์—ฐํ•œ ๊ฒƒ.
    • getRandom์€ 1๋กœ ์ดˆ๊ธฐํ™”๋˜๋Š” n์„ ๋ฐ›์•„ Math.random()*n์„ ๋ฆฌํ„ดํ•ด์ฃผ์ž.
export const getRandom = (n=1) => Math.random()*n;
  • ์ด๋ฒˆ์—๋Š” ํŠน์ • ๋ฒ”์œ„ ๋‚ด์˜ ์ž„์˜์˜ ์ˆซ์ž๋ฅผ ๋ฆฌํ„ดํ•˜๋Š” getRandomCount๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. ํ…Œ์ŠคํŠธ ๋กœ์ง ๋จผ์ €, ํ•จ์ˆ˜๋ฅผ ๋‚˜์ค‘์— ์ง ๋‹ค.
// src/utils/getRandom.test.js
test('getRandomCount(5, 7)์€ 5 ์ด์ƒ 7 ์ดํ•˜์˜ ๊ฐ’์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.', () => {
  let targetValue = getRandomCount(5, 7);
  console.log(`getRandomCount(5, 7)์€ ${targetValue}`)
  expect(targetValue >= 5).toBe(true);
  expect(targetValue <= 7).toBe(true);
})

// src/utils/getRandom.js
const getRandomCount = (min = 0, max = 10) => Math.floor(getRandom()*(max - min) + min);

transformText ํ•จ์ˆ˜๋ฅผ ํ…Œ์ŠคํŠธ ์ฃผ๋„๊ฐœ๋ฐœ๋กœ ๋งŒ๋“ค์–ด๋ณด๊ธฐ

  • ์ฃผ์–ด์ง„ ๋ฌธ์ž์—ด์„ snake_case, kebab-case, camelCase, TitleCase ๋กœ ๋งŒ๋“ค์–ด์ฃผ๋Š” ํ•จ์ˆ˜๋ฅผ ํ…Œ์ŠคํŠธ์ฃผ๋„๊ฐœ๋ฐœ๋กœ ๋งŒ๋“ค์–ด๋ณด์ž.
// src/utils/transformText.js
export const snakeCase = data => {};
export const kebabCase = data => {};
export const camelCase = data => {};
export const titleCase = data => {};

// src/utils/transformText.test.js
import { test, expect } from './tests.js';

test(`snakeCase('simple is the best') => 'simple_is_the_best'`, () => {
  expect(snakeCase('simple is the best')).toBe('simple_is_the_best');
});

test(`kebabCase('simple is the best') => 'simple-is-the-best'`, () => {
  expect(kebabCase('simple is the best')).toBe('simple-is-the-best');
});

test(`camelCase('simple is the best') => 'simpleIsTheBest'`, () => {
  expect(camelCase('simple is the best')).toBe('simpleIsTheBest');
});

test(`titleCase('simple is the best') => 'SimpleIsTheBest'`, () => {
  expect(titleCase('simple is the best')).toBe('SimpleIsTheBest');
});
  • ์ด์ œ ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์ž. ์ •๊ทœํ‘œํ˜„์‹์„ ์ด์šฉํ•˜์—ฌ ์ž‘์„ฑํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.
    • ๋ฌธ์ž์—ด์„ replace๋ฉ”์„œ๋“œ๋กœ ๋ฐ”๊ฟ”์ฃผ๊ณ  ์‹ถ์„ ๋•Œ, ๋ฐ”๊พธ๊ธฐ ์ „ ๋ฌธ์ž๋ฅผ ํ™œ์šฉํ•˜๋ ค๋ฉด ๋‘๋ฒˆ์งธ ์ธ์ž๋กœ ๋ฐ”๊ฟ€ ๋ฌธ์ž์—ด ๋Œ€์‹  ์ด์ „ ๋ฌธ์ž์—ด์„ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›๋Š” ์ฝœ๋ฐฑ์„ ์ค€๋‹ค.
// src/utils/transformText.js
export const snakeCase = data => data.toString().replace(/\s/g, '_');
export const kebabCase = data => data.toString().replace(/\s/g, '-');
export const camelCase = data => data.toString().replace(/\s\w/g, match => match.toUpperCase().trim());
export const titleCase = data => data.toString().replace(/(^\w|\s\w)/g, match => match.toUpperCase().trim());
  • ์ด์ œ main.js์— importํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ €์—์„œ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋„๋ก ํ•˜์ž.
// src/main.js
import './utils/transformText.test.js';

DOM TEST๋ฅผ ํ…Œ์ŠคํŠธ ์ฃผ๋„ ๊ฐœ๋ฐœ๋กœ ํ•ด๋ณด๊ธฐ

  • client์— domTest๋ผ๋Š” ํด๋”๋ฅผ ๋งŒ๋“ค๊ณ  index.js์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ…Œ์ŠคํŠธ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž.
  1. ๋ฌธ์„œ ์ œ๋ชฉ์ด 'React ์›น๊ฐœ๋ฐœ ํ™˜๊ฒฝ๊ตฌ์„ฑ'์ธ๊ฐ€?
  2. ๋ฌธ์„œ์— '#app' ์š”์†Œ๊ฐ€ ์กด์žฌํ•˜๋Š”๊ฐ€?
  3. '#app' ์š”์†Œ ์•ˆ์— ์ œ๋ชฉ์š”์†Œ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”๊ฐ€?
  4. ์ œ๋ชฉ์š”์†Œ์˜ ํ…์ŠคํŠธ๊ฐ€ 'React ์•ฑ ๊ฐœ๋ฐœ'์ธ๊ฐ€?
  5. ์ œ๋ชฉ์š”์†Œ๊ฐ€ 'headline'์ด๋ผ๋Š” ํด๋ž˜์Šค๋ฅผ ํฌํ•จํ•˜๋Š”๊ฐ€?
  • ์ด์ฒ˜๋Ÿผ ๋ฌด์—‡์ด ํ•„์š”ํ•œ์ง€ ์จ๋†“๊ณ  ์ด๋ฅผ ์ถฉ์กฑ์‹œํ‚ค๋„๋ก ๊ฐœ๋ฐœํ•˜๋Š” ๊ฒƒ์ด ํ…Œ์ŠคํŠธ ์ฃผ๋„ ๊ฐœ๋ฐœ์ด๋‹ค.
  • main.js์—์„œ domTest/index.js๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ณ , domTest/index.js์—์„œ๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์งœ๋ณด์ž.
// src/domTest/index.js
import { describe, test, expect } from '../utils/tests.js';

describe('DOM TEST', () => {
  const $appNode = document.getElementById('app');

  test("๋ฌธ์„œ ์ œ๋ชฉ์ด 'React ์›น๊ฐœ๋ฐœ ํ™˜๊ฒฝ๊ตฌ์„ฑ'์ธ๊ฐ€?", () => {
    expect(document.title === 'React ์›น๊ฐœ๋ฐœ ํ™˜๊ฒฝ๊ตฌ์„ฑ').toBe(true);
  });
  
  test("๋ฌธ์„œ์— '#app' ์š”์†Œ๊ฐ€ ์กด์žฌํ•˜๋Š”๊ฐ€?", () => {
    expect($appNode).toBeInTheDocument();
  });

  const $heading = appNode.querySelector('h1, h2, h3, h4, h5, h6');
  test("'#app' ์š”์†Œ ์•ˆ์— ์ œ๋ชฉ์š”์†Œ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”๊ฐ€?", () => {
    hasHeadline = appNode.querySelector('h1, h2, h3, h4, h5, h6');
    expect(hasHeadline).toBe(true);
  });
  
  test("์ œ๋ชฉ์š”์†Œ์˜ ํ…์ŠคํŠธ๊ฐ€ 'React ์•ฑ ๊ฐœ๋ฐœ'์ธ๊ฐ€?", () => {
    const $heading = appNode.querySelector('h1, h2, h3, h4, h5, h6');
    expect($heading.textContent).toBe('React ์•ฑ ๊ฐœ๋ฐœ');
  });

  test("์ œ๋ชฉ์š”์†Œ๊ฐ€ 'headline'์ด๋ผ๋Š” ํด๋ž˜์Šค๋ฅผ ํฌํ•จํ•˜๋Š”๊ฐ€?", () => {
    expect($heading.classList.contains('headline')).toBe(true);
  });
})
  • ๋ณด๋‹ค ์šฉ์ดํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด expect๊ฐ€ ๋ฆฌํ„ดํ•˜๋Š” ๊ฐ์ฒด์— truthy/falsyํ•œ ๊ฐ’์ธ์ง€ ํŒ๋‹จํ•˜๋Š” ๋ฉ”์„œ๋“œ์™€ ํด๋ž˜์Šค ์ด๋ฆ„ ํ™•์ธํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ๋งŒ๋“ค์–ด์ฃผ์ž
// src/utils/tests.js

const expect = received => {
  return {
    toBe(expected) {
      if (received !== expected) throwError(`${received}๋Š” ${expected}์™€ ๊ฐ™์ง€ ์•Š์Šต๋‹ˆ๋‹ค.`);
    },
    toBeInTheDocument() {
      if (!document.body.contains(received)) throwError(`${received}๋Š” ๋ฌธ์„œ์— ํฌํ•จ๋˜์–ด์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.`);
    },
    toBeTruthy() {
      if (!received) throwEror(`${received}๋Š” truthy ๊ฐ’์ด ์•„๋‹™๋‹ˆ๋‹ค.`);
    },
    toBeFalsy() {
      if (received) throwError(`${received}๋Š” falsy ๊ฐ’์ด ์•„๋‹™๋‹ˆ๋‹ค.`);
    },
    toHaveClass(className) {
      if (!received.classList.contains(className)) throwError(`${received}์š”์†Œ์—๋Š” ${className}๋ผ๋Š” ํด๋ž˜์Šค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.`);
    }
   not {
     // ์œ„์™€ ๋ฐ˜๋Œ€์˜ ๋กœ์ง๋“ค
   }
  }
}

Lint ์„ค์ •

  • Javascript์˜ ๋งน์ ์€ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹คํ–‰์‹œํ‚ค๊ธฐ ์ „๊ฐ€์ง€ ์˜ค๋ฅ˜๋ฅผ ์žก๊ธฐ ์–ด๋ ต๋‹ค๋Š” ์ ์ด๋‹ค. ์ด๋ฅผ ์œ„ํ•ด Lint๊ฐ€ ํ•„์š”ํ•˜๋‹ค.
  • VSCode์—์„œ extension์œผ๋กœ ๊น”์•„๋‘” ESlint์™€ Prettier์„ ๋น„ํ™œ์„ฑํ™”ํ•˜๊ณ  reloadํ•ด๋ณด์ž
  • eslint๊ฐ€ ์ „์—ญ์— ๊น”๋ ค์žˆ๋Š”์ง€ ํ™•์ธํ•ด๋ณด์ž. $ eslint --version์—์„œ command not found๊ฐ€ ๋œจ๋ฉด global์— ์—†๋Š” ๊ฒƒ
  • $ npx eslint --init์œผ๋กœ ์•„๋ž˜์™€ ๊ฐ™์ด ๊ธฐ๋ณธ์„ค์ •์„ ํ•œ๋‹ค.
    • To check syntax and find problems / Javascript modules(import/export) / React / No(Typescript) / all(Browser and node.js) / Javascript (jsํŒŒ์ผ๋กœ config ๊ด€๋ฆฌ) / YES(plugin๋„ ์„ค์น˜)
  • ์ด์ œ package.json ํŒŒ์ผ์— eslint์™€ eslint-plugin-react๊ฐ€ ๊น”๋ ค์žˆ๊ณ  eslintrc.cjs ํŒŒ์ผ๋„ ์ƒ์„ฑ๋˜์–ด์žˆ๋‹ค.
  • eslintrc.cjs ํŒŒ์ผ์—๋Š” parser ์˜ต์…˜์— jsx๊ฐ€ true๋กœ ์„ค์ •๋˜์–ด์žˆ๋Š”๋ฐ ์ด๋Š” React์šฉ์œผ๋กœ lint๋ฅผ ํ–ˆ๊ธฐ ๋•Œ๋ฌธ
    • "rules"๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์ž. "no-unused-vars":"warn"๋กœ ์„ ์–ธ๋งŒ ํ•˜๋Š” ๊ฒฝ์šฐ ๋นจ๊ฐ„ ์ค„ ๋ง๊ณ  ๋…ธ๋ž€ warning์œผ๋กœ๋งŒ ํ‘œ์‹œํ•˜๋„๋ก.
    • client ์•ˆ์— ์žˆ๋Š” ๊ฒƒ(gitignore ์ œ์™ธ)์„ ๊ฒ€์‚ฌํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” $npx eslint ./client/ --ignore-path .gitignore
    • ์ด๋ ‡๊ฒŒ run ํ•˜๋ฉด react version์ด not specified๋ผ๊ณ  ๋œจ๋Š”๋ฐ ์ด๋Š” ์œŒ๊ฐ€ ์•„์ง React๋ฅผ ์•ˆ ๊น”์•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. eslintrc.cjs์—์„œ "plug-in"์— ์žˆ๋Š” "plugin:react/recommended"์™€ "extends"์— ์žˆ๋Š” react๋ฅผ ์ฃผ์„์ฒ˜๋ฆฌํ•ด์ค€๋‹ค.
  • ๋งค๋ฒˆ eslint ๋ช…๋ น์–ด๋ฅผ $npx eslint ./client/ --ignore-path .gitignore๋กœ ์น  ์ˆ˜๋Š” ์—†์œผ๋‹ˆ eslint-watch๋ฅผ ๊น”์•„์ฃผ์ž. $ npm i -D eslint-watch
    • package.json์—์„œ ๋ช…๋ น์–ด๋“ค์„ ๋งŒ๋“ค์–ด์ฃผ์ž.
// package.json์˜ "script"์— ์ถ”๊ฐ€ 
  "lint": "eslint ./client/ --ignore-path .gitignore",
  "watch:lint": "esw ./client/ --watch --color --ignore-path .gitignore"

Prettier ์„ค์ •

  • $ npm i -D prettier๋กœ ์„ค์น˜ํ•˜๊ณ  $ npx prettier -h๋กœ ๋„์›€๋ง์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
  • init์ด ๋”ฐ๋กœ ์—†์–ด์„œ ์ดˆ๊ธฐ์„ค์ •ํŒŒ์ผ์„ ์ž๋™์œผ๋กœ ๋งŒ๋“ค์–ด์ฃผ์ง€ ์•Š์œผ๋‹ˆ prettier.io์—์„œ ํ•„์š”ํ•œ ์„ค์ •ํŒŒ์ผ ๋‚ด์šฉ์„ ๋ณต์‚ฌํ•˜์—ฌ prettierrc.cjs์— ๋ณต์‚ฌ. ์šฐ๋ฆฌ๋Š” ์•ผ๋ฌด๋‹˜์ด ์ค€ ๊ฑธ๋กœ ํ•˜๊ธฐ.
// prettierrc.cjs
module.exports = {
  // ํ™”์‚ดํ‘œ ํ•จ์ˆ˜ ์‹ ๋งค๊ฐœ๋ณ€์ˆ˜ () ์ƒ๋žต ์—ฌ๋ถ€ (ex: (a) => a)
  arrowParens: 'always',
  // ๋‹ซ๋Š” ๊ด„ํ˜ธ(>) ์œ„์น˜ ์„ค์ •
  // ex: <div
  //       id="unique-id"
  //       class="contaienr"
  //     >
  htmlWhitespaceSensitivity: 'css',
  bracketSameLine: false,
  // ๊ฐ์ฒด ํ‘œ๊ธฐ ๊ด„ํ˜ธ ์‚ฌ์ด ๊ณต๋ฐฑ ์ถ”๊ฐ€ ์—ฌ๋ถ€ (ex: { foo: bar })
  bracketSpacing: true,
  // ํ–‰ํญ ์„ค์ • (์ค„ ๊ธธ์ด๊ฐ€ ์„ค์ • ๊ฐ’๋ณด๋‹ค ๊ธธ์–ด์ง€๋ฉด ์ž๋™ ๊ฐœํ–‰)
  printWidth: 80,
  // ์‚ฐ๋ฌธ ๋ž˜ํ•‘ ์„ค์ •
  proseWrap: 'preserve',
  // ๊ฐ์ฒด ์†์„ฑ key ๊ฐ’์— ์ธ์šฉ ๋ถ€ํ˜ธ ์‚ฌ์šฉ ์—ฌ๋ถ€ (ex: { 'key': 'xkieo-xxxx' })
  quoteProps: 'as-needed',
  // ์„ธ๋ฏธ์ฝœ๋ก (;) ์‚ฌ์šฉ ์—ฌ๋ถ€
  semi: true,
  // ์‹ฑ๊ธ€ ์ธ์šฉ ๋ถ€ํ˜ธ(') ์‚ฌ์šฉ ์—ฌ๋ถ€
  singleQuote: true,
  // ํƒญ ๋„ˆ๋น„ ์„ค์ •
  tabWidth: 2,
  // ๊ฐ์ฒด ๋งˆ์ง€๋ง‰ ์†์„ฑ ์„ ์–ธ ๋’ท ๋ถ€๋ถ„์— ์ฝค๋งˆ ์ถ”๊ฐ€ ์—ฌ๋ถ€
  trailingComma: 'es5',
  // ํƒญ ์‚ฌ์šฉ ์—ฌ๋ถ€
  useTabs: false,
};
  • prettier๋ฅผ ํ†ตํ•œ formatting๊ณผ ์‹ค์‹œ๊ฐ„ watch๋ฅผ client ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋Œ€์ƒ์œผ๋กœ ์‹คํ–‰ํ•˜๋Š” ์Šคํฌ๋ฆฝํŠธ๋ฅผ package.json์— ๋„ฃ์–ด์ฃผ์ž.
    • watch๋กœ ๊ตฌ๋™์‹œํ‚ค๋ ค๋ฉด onchange๋ผ๋Š” ํŒจํ‚ค์ง€๋ฅผ ๊น”์•„์ฃผ์ž. $ npm i -D onchange
    • "format": --write ์˜ต์…˜์€ prettier๊ฐ€ formatํ•˜์—ฌ ์“ธ์ˆ˜์žˆ๊ฒŒ ํ•ด์ค€๋‹ค.
    • "watch:format": client ์•ˆ์— ์žˆ๋Š” ํŒŒ์ผ๋“ค์ด ๋ณ€๊ฒฝ๋˜๋ฉด format ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰์‹œํ‚จ๋‹ค.
// package.json์˜ "script"์— ์ถ”๊ฐ€ 
  "format": "prettier --write ./client --ignore-path .gitignore",
  "watch:format": "onchange ./client -- npm run format {{changed}}"
  • ๋‘ ๋ช…๋ น์–ด๋ฅผ ๋™์‹œ์— ์‹คํ–‰์‹œํ‚ค๊ธฐ ์œ„ํ•ด &๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ npm-run-all ํŒจํ‚ค์ง€๋ฅผ ๊น”์•„์„œ lint์™€ format์„ ๋ณ‘๋ ฌ๋กœ(run-p) ๋™์‹œ์— ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค. $ npm i -D npm-run-all
// package.json์˜ "script"์— ์ถ”๊ฐ€ 
  "watch": "npm run watch:lint & npm run watch:format",
// npm-run-all ํŒจํ‚ค์ง€ ์„ค์น˜ ํ›„
  "watch": "run-p watch:lint watch:format"
// "watch": "run-p watch:**"๋กœ ์ง€์ •ํ•˜๋ฉด watch:๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ชจ๋“  ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•ด์ค€๋‹ค
  • ์ด๋Ÿฐ ์‹์œผ๋กœ lint์™€ code format package๋ฅผ ์ง์ ‘ ํ™˜๊ฒฝ์„ค์ •ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ vscode์˜ extension์ด ํ›จ์”ฌ ํŽธํ•˜๋‹ˆ๊นŒ ๋‹ค์‹œ ์ผœ๋„๋ก ํ•˜์ž.

Test Library (Jest)

  • ์„ค์น˜ํ•˜๊ธฐ ์ „์— ๋จผ์ € npx๋กœ Jest๋ฅผ ์‹คํ–‰ํ•ด๋ณด์ž. $ npx jest ./client๋กœ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰์‹œํ‚ค๋ฉด client directory ์•ˆ์—์„œ testํŒŒ์ผ์„ ์ฐพ์•„ ์‹คํ–‰ํ•œ๋‹ค.
  • ํ˜„์žฌ ๋งŒ๋“ค์–ด๋‘” getRandom.test.js์™€ transformText.test.js๋ฅผ ์ฐพ์•„ ์‹คํ–‰ํ•˜๋ ค๋‹ค๊ฐ€ ์‹คํŒจํ•œ๋‹ค.
    • ์šฐ๋ฆฌ๋Š” testํŒŒ์ผ์—์„œ import๋กœ getRandom.js์™€ transformText.js์˜ ํ•จ์ˆ˜๋“ค์„ ๊ฐ€์ ธ์™”๋Š”๋ฐ, nodejs๋กœ ํ…Œ์ŠคํŠธํ•  ์‹œ commonJS๋กœ ํŒŒ์ผ์„ ๋งŒ๋“ค๊ธฐ ๋•Œ๋ฌธ์— ์—๋Ÿฌ๊ฐ€ ๋‚œ๋‹ค.
    • ์šฐ๋ฆฌ๊ฐ€ ์ง  ์ฝ”๋“œ๋ฅผ ์ธ์‹์‹œํ‚ค๋ ค๋ฉด import ๊ตฌ๋ฌธ์„ require๋กœ ๋ฐ”๊พธ๊ฑฐ๋‚˜, ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ๊น”์•„์ฃผ์–ด์•ผ ํ•œ๋‹ค. $ npm i -D babel-jest @babel/core @babel/preset-env
  • $ npm i -D jest๋กœ ์„ค์น˜ํ•˜๊ณ , $ npx jest --init์œผ๋กœ jest ๊ตฌ์„ฑํŒŒ์ผ์„ ์ƒ์„ฑํ•œ๋‹ค.
    • init ํ›„ ์˜ต์…˜์„ค์ •์€ YES(test๋ผ๋Š” ์Šคํฌ๋ฆฝํŠธ๋ฅผ package.json์— ๋„ฃ์–ด์ค„๊ฒƒ) / no(typescript๋กœ configํŒŒ์ผ ๋งŒ๋“ค๊ฒƒ์ธ์ง€) / jsdom(๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ์ฒ˜๋Ÿผ ํ…Œ์ŠคํŠธํ•˜๊ฒ ๋‹ค) / no(coverage reports ๋งŒ๋“ค๊ฒƒ์ธ์ง€) / babel / no (mock call์„ ์ž๋™์œผ๋กœ clear)
  • init์„ ๋งˆ์น˜๋ฉด jest.config.mjs ํŒŒ์ผ ์ƒ๊ธฐ๋Š”๋ฐ, ์—ฌ๊ธฐ์„œ ์•„๋ž˜ ๋ช‡๋ช‡๊ฐœ์˜ ์ฃผ์„์„ ํ’€์–ด์ฃผ์ž
    • coveragePathIgnorePatterns: node_modules ํด๋”๋Š” ๋ฌด์‹œ
    • coverageProvider: babel๋กœ ํ•ด์ฃผ๊ธฐ
    • moduleFileExtensions: ๋Œ€์ƒ ํŒŒ์ผ์˜ ํ™•์žฅ์ž๋ช…
    • testEnvironment: jsdom ๋ธŒ๋ผ์šฐ์ €์™€ ๊ฐ™์€ ํ™˜๊ฒฝ์œผ๋กœ ํ…Œ์ŠคํŠธ
    • testMatch: ์ •๊ทœํ‘œํ˜„์‹์œผ๋กœ ํ‘œํ˜„๋œ ํŒŒ์ผ๋ช…์— ํ•ด๋‹นํ•˜๋Š” ํŒŒ์ผ์„ ๋‹ค ํ…Œ์ŠคํŠธํ•˜๋„๋ก
    • transform: undefined๋กœ ๋˜์–ด์žˆ๋Š” ํŒŒ์ผ๋ช… ์ดˆ๊ธฐ๊ฐ’์„ ์•„๋ž˜์™€ ๊ฐ™์€ ์ •๊ทœํ‘œํ˜„์‹์œผ๋กœ ๋ฐ”๊ฟ” ์ด์— ํ•ด๋‹นํ•˜๋Š” ํŒŒ์ผ์„ babel-jest ํ”Œ๋Ÿฌ๊ทธ์ธ์œผ๋กœ ์—ฐ๊ฒฐํ•˜์—ฌ ๋‹ค ์ปดํŒŒ์ผํ•˜๋„๋ก ํ•ด์ฃผ๋ฉด ์•„๊นŒ ๊น”์•„์ค€ babel-jest๋ฅผ ํ†ตํ•ด์„œ import ๊ตฌ๋ฌธ๋„ jest๊ฐ€ ์ธ์‹ํ•  ์ˆ˜ ์žˆ๋‹ค.
  transform: { 
    '\\.[jt]sx?$': 'babel-jest' 
  }
  • ์ด์ œ .babelrc ํŒŒ์ผ๋„ ๋งŒ๋“ค์–ด ์•„๋ž˜์™€ ๊ฐ™์ด configuration์„ ํ•ด์ฃผ์ž.
{
  "compact": false,
  "comments": false,
  "presets": [
    [
      "@babel/preset-env",
      {
        "loose": true
      }
    ]
  ]
}
  • getRandom ํ•จ์ˆ˜๋“ค์„ jest๋ฅผ ํ†ตํ•ด testํ•ด๋ณด์ž.
    • jest์˜ matcher function์ธ toBeLessThan์œผ๋กœ ๊ธฐ๋Œ€๊ฐ’์— ์ „๋‹ฌ๋œ ๊ฐ’์ด ์ž‘๊ฑฐ๋‚˜ ๊ฐ™์€์ง€ ํ™•์ธํ•ด๋ณด์ž.
    • getRandom.test.js์—์„œ ๊ธฐ์กด ํ…Œ์ŠคํŠธ๋ฅผ ์ฃผ์„์ฒ˜๋ฆฌํ•˜๊ณ , ์•„๋ž˜์™€ ๊ฐ™์ด ๋‹ค์‹œ ํ…Œ์ŠคํŠธ์ฝ”๋“œ๋ฅผ ์จ๋ณด์ž
test('getRandom(10)์€ 10๋ณด๋‹ค ์ž‘๊ฑฐ๋‚˜ ๊ฐ™๊ณ  0๋ณด๋‹ค ํฌ๊ฑฐ๋‚˜ ๊ฐ™์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค', () => {
  let targetCount = getRandom(10);
  expect(targetCount).toBeLessThan(10);
  expect(targetCount).toBeGreaterThan(0);
})
  • ๊ทผ๋ฐ ์ง€๊ธˆ lint๊ฐ€ jest์˜ ํ•จ์ˆ˜๋“ค์„ ์ธ์‹ ๋ชปํ•ด์„œ ๋นจ๊ฐ„ ์ค„์ด ๊ณ„์† ๋œจ๋‹ˆ๊นŒ eslintrc.cjs์—์„œ ์„ค์ • ์ข€ ํ•ด์ฃผ๊ธฐ
    • "env": { "globals/jest": true }์™€ plugin: ["react", "jest"]๋ฅผ ์„ค์ •ํ•ด์ค€๋‹ค.
    • ๋งŒ์•ฝ eslintrcํŒŒ์ผ์—์„œ "globals/jest"๋ฅผ ์ธ์‹ ๋ชปํ•˜๋ฉด "globals": { "jest": true }๋กœ ํ•ด์ฃผ๊ธฐ
    • plugin๋„ ๊น”์•„์ฃผ๊ณ  $ npm i -D eslint-plugin-jest extends์— "plugin:jest/recommended"๋ฅผ ๋„ฃ์–ด์ค€๋‹ค.
    • settings์—๋„ settings : {jest: {version: require('jest/package.json').version}}๋กœ ๋ฒ„์ „ ์„ค์ •ํ•ด์ค€๋‹ค.
  • ์ธํ…”๋ฆฌ์„ผ์Šค์—์„œ ์ •๋ณด ์ž˜ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋„๋ก $ npm i -D @types/jest๊นŒ์ง€ ์„ค์น˜ํ•ด์ฃผ์ž.
  • ์ด์ œ getRandom.test.js์™€ transformText.test.js์—์„œ ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  tests.js๋ฅผ importํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ฃผ์„์ฒ˜๋ฆฌํ•ด๋†“๊ณ , jest๋กœ ํ…Œ์ŠคํŠธ($ npm test)ํ•ด๋ณด๋ฉด ํ„ฐ๋ฏธ๋„์—์„œ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.