์ง๋ ์๊ฐ ๋ณต์ต ๋ฐ ์์ต
์ง๋ ์๊ฐ 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์ฌ๊ฐ ๋ง๋ ๊ฑฐ๋ผ ์๋ก ์นํ์ ์ด๋ฉฐ ํธ๋ฆฌํ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
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๋ฅผ ๋ฃ์ด์ค๋ค.
๊ธฐ๋ณธ ์ค์ ๋ฉํํ๊ทธ ๋ถ์ํด๋ณด์
< 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 " />
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(์ ๋ฌ๊ฐ).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 ( '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 ( ํ
์คํธ ๋ฆฌ์คํธ ํญ๋ชฉ์ ๋๋ณํ๋ ๋ ์ด๋ธ , ( ) => {
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์์ ๋ค์๊ณผ ๊ฐ์ ํ
์คํธ์ฝ๋๋ฅผ ์์ฑํด๋ณด์.
๋ฌธ์ ์ ๋ชฉ์ด 'React ์น๊ฐ๋ฐ ํ๊ฒฝ๊ตฌ์ฑ'์ธ๊ฐ?
๋ฌธ์์ '#app' ์์๊ฐ ์กด์ฌํ๋๊ฐ?
'#app' ์์ ์์ ์ ๋ชฉ์์๊ฐ ํฌํจ๋์ด ์๋๊ฐ?
์ ๋ชฉ์์์ ํ
์คํธ๊ฐ 'React ์ฑ ๊ฐ๋ฐ'์ธ๊ฐ?
์ ๋ชฉ์์๊ฐ '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 {
// ์์ ๋ฐ๋์ ๋ก์ง๋ค
}
}
}
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"
$ 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์ด ํจ์ฌ ํธํ๋๊น ๋ค์ ์ผ๋๋ก ํ์.
์ค์นํ๊ธฐ ์ ์ ๋จผ์ 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
)ํด๋ณด๋ฉด ํฐ๋ฏธ๋์์ ํ
์คํธ ๊ฒฐ๊ณผ๋ฅผ ๋ณผ ์ ์๋ค.