์›น ์ œํ’ˆ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ๊ณผ ๊ธฐ์ˆ  ์Šคํƒ ๋˜์งš์–ด๋ณด๊ธฐ

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

ํŒจํ‚ค์ง€ ๋งค๋‹ˆ์ €

ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ๋ฅผ ์šฉ์ดํ•˜๊ฒŒ ๋„์™€์ฃผ๋Š” ํŒจํ‚ค์ง€ ๋งค๋‹ˆ์ €๋Š” ํŒจํ‚ค์ง€ ์˜์กด์„ฑ ๊ด€๋ฆฌ์™€ ํ”„๋กœ์ ํŠธ์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ, ์Šคํฌ๋ฆฝํŠธ ๋“ฑ ๊ด€๋ฆฌ ํฌ์ธํŠธ๋ฅผ ๋ฌถ์–ด ํ•œ๊ณณ์—์„œ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค€๋‹ค. npm์„ ์‹œ์ž‘์œผ๋กœ yarn, pnpm ๋“ฑ ์—ฌ๋Ÿฌ ํŒจํ‚ค์ง€๊ฐ€ ์žˆ์ง€๋งŒ, ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•œ ์‹œ์ ์— ๊ฐ€์žฅ ์ž์ฃผ ์‚ฌ์šฉํ•˜๊ณ  ์ต์ˆ™ํ–ˆ๋˜ yarn์„ ์‚ฌ์šฉํ–ˆ๋‹ค.

{ "name": "git-glances", "version": "2.2.2", "main": "index.js", "repository": "git@github.com:youthfulhps/git-glances.git", "author": "youthfulhps <ybh942002@gmail.com>", "license": "MIT", ... }

yarn์€ lockfile์„ ๋„์ž…ํ•ด ํ˜‘์—… ๊ณผ์ •์—์„œ ์ผ๊ด€๋œ ์˜์กด์„ฑ ๋ฒ„์ „ ๊ด€๋ฆฌ๋ฅผ ๋ณด์žฅํ•˜๊ณ , ๊นŠ์–ด์ง€๋Š” ์˜์กด์„ฑ ํŠธ๋ฆฌ์— ๋”ฐ๋ผ ์ค‘๋ณต ์„ค์น˜๋˜๋Š” ์˜์กด์„ฑ๋“ค์„ ํ˜ธ์ด์ŠคํŒ…์‹œ์ผœ ํ”Œ๋žซํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๋Š” ์ปจ์…‰์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๊ฒƒ์ด ํŠน์ง•์ด๋‹ค.

yarn์˜ ์˜์กด์„ฑ ํ˜ธ์ด์ŠคํŒ…

์ด๋ฏธ์ง€ ์ถœ์ฒ˜

์˜์กด์„ฑ ํ˜ธ์ด์ŠคํŒ…์„ ํ†ตํ•ด ๋””์Šคํฌ ๊ณต๊ฐ„์„ ์„ธ์ด๋ธŒํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์ง€๋งŒ, ํŠน์ • ์˜์กด์„ฑ ํŒจํ‚ค์ง€์˜ ์˜์กด์„ฑ์ด ํ˜ธ์ด์ŠคํŒ…๋˜์–ด ํ”„๋กœ์ ํŠธ์— ์ง์ ‘ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š์€ ์˜์กด์„ฑ์ด ๋ฌธ์ œ์—†์ด ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ์œ ๋ น ์˜์กด์„ฑ (Phantom Dependency) ํ˜„์ƒ์ด ๋ฐœ์ƒํ•˜๊ธฐ๋„ ํ•œ๋‹ค.

์™ธ๋žŒ๋œ ์ด์•ผ๊ธฐ์ง€๋งŒ, ํ˜„์žฌ๋Š” pnpm๋ฅผ ์ฃผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค. pnpm์€ ์‹ฌ๋งํฌ, ํ•˜๋“œ๋งํฌ๋ฅผ ํ†ตํ•ด ํ”„๋กœ์ ํŠธ์—์„œ ์˜์กด์„ฑ์„ ๋กœ์ปฌ ๋””์Šคํฌ์˜ ์ƒ์œ„ ์Šคํ† ์–ด์— ์„ค์น˜๋œ ์˜์กด์„ฑ๊ณผ ์—ฐ๊ฒฐ์‹œ์ผœ ์‚ฌ์šฉํ•˜๋Š” Content-addressable-storage ์ „๋žต์„ ํ†ตํ•ด ์—ฌ๋Ÿฌ ํ”„๋กœ์ ํŠธ์—์„œ ์ค‘๋ณต๋˜๊ฒŒ ์‚ฌ์šฉ๋˜๋Š” ์˜์กด์„ฑ์— ๋Œ€ํ•œ ์ค‘๋ณต ์„ค์น˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ  ์žˆ๋‹ค. ์ด ์ปจ์…‰์ด ์ ์ž–๊ฒŒ ์ถฉ๊ฒฉ์ด์—ˆ๋‹ค.

pnpm์˜ node_modules ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ

์ด๋ฏธ์ง€ ์ถœ์ฒ˜

๋ฒˆ๋“ค๋Ÿฌ

๋ชจ๋“ˆ ์‹œ์Šคํ…œ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ˜•์„ฑ๋œ ๊ฑฐ๋Œ€ํ•œ ์ฝ”๋“œ ๋ฒ ์ด์Šค์™€ CSS, ์ด๋ฏธ์ง€ ๋“ฑ ์›น์„ ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ๋ชจ๋“  ์ž์›์„ ๋ชจ๋“ˆ์ด๋ผ ํ•œ๋‹ค. ๋ฒˆ๋“ค๋Ÿฌ๋Š” ์Šคํฌ๋ฆฝํŠธ ๋ชจ๋“ˆ ๋‚ด์˜ ํŒŒ์ผ ๋‹จ์œ„์™€ ๋ณ€์ˆ˜ ์œ ํšจ ๋ฒ”์œ„๋ฅผ ์œ ์ง€ํ•œ ์ฑ„ ์›น์„ ๊ตฌ์„ฑํ•˜๋Š” ๋ชจ๋“  ๋ชจ๋“ˆ๋“ค์„ ํ•˜๋‚˜์˜ ๋ฒˆ๋“ค๋กœ ์ƒ์„ฑํ•œ๋‹ค.

ํ•˜๋‚˜๋กœ ๋ฌถ์ธ ๋ฒˆ๋“ค์€ ์ •๋Ÿ‰์ ์ธ ์š”์ฒญ ํšŸ์ˆ˜๋ฅผ ์ค„์—ฌ ๋ธŒ๋ผ์šฐ์ €๋ณ„๋กœ ์ƒ์ดํ•œ ์š”์ฒญ ํšŸ์ˆ˜ ์ œํ•œ์—์„œ ์•ˆ์ „ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์›ํ•œ๋‹ค๋ฉด ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…์„ ํ†ตํ•ด ์›ํ•˜๋Š” ๋•Œ ๋ถ„๋ฆฌ๋œ ๋ฒˆ๋“ค์„ ๋กœ๋“œํ•  ์ˆ˜ ์žˆ๊ณ , ์‚ฌ์šฉํ•˜์ง€ ์•Š์€ ์ฝ”๋“œ๋“ค์„ ํŠธ๋ฆฌ ์‰์ดํ‚นํ•ด ๋ฒˆ๋“ค๋ง ๊ณผ์ •์—์„œ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ๋‹ค.

์ตœ๊ทผ webpack๊ณผ ๋น„๊ตํ–ˆ์„ ๋•Œ ๋นŒ๋“œ ์†๋„๊ฐ€ ํ˜„์ €ํžˆ ๊ฐœ์„ ๋œ esbuild, vite์™€ ๊ฐ™์€ ๋ฒˆ๋“ค๋Ÿฌ๋“ค์ด ๋งŽ์€ ๊ด€์‹ฌ์„ ๋ฐ›๊ณ  ์žˆ๊ณ , ์ตœ๊ทผ ์‹ค๋ฌด์—์„œ๋Š” vite๋ฅผ ์ฃผ๋กœ ์‚ฌ์šฉํ•˜๋Š”๋ฐ ์—ญ์‹œ๋‚˜ ๋ฒˆ๋“ค๋ง ์„ค์ •์ด ๋งค์šฐ ๊ฐ„๊ฒฐํ•˜๊ณ  ๊ฐœ๋ฐœ์ž ์นœํ™”์ ์ด๋‹ค.

ํ•˜์ง€๋งŒ ๊ฐœ์„ ๋œ ๋ฒˆ๋“ค๋Ÿฌ๋ฅผ ์‹ ๋‚˜๊ฒŒ ์นญ์ฐฌํ•˜๊ณ ์„  webpack์„ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ๋‹ค. ์•„์ง ๋ฒˆ๋“ค๋Ÿฌ ์‹œ์žฅ์—์„œ ์ ์œ ์œจ์ด ๊ฒฌ๊ณ ํ•  ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ์‹ค๋ฌด์—์„œ ๋ฒˆ๋“ค๋Ÿฌ์— ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒจ ๋ˆ„๊ตฐ๊ฐ€ ํ•ด๊ฒฐํ•ด์•ผ ํ•œ๋‹ค๋ฉด ๊ทธ ๋Œ€์ƒ์ด webpack์ผ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์ง€ ์•Š์„๊นŒ ์‹ถ์–ด ์„ค์ • ์ˆ˜์ •๊ณผ ์ ์šฉ์— ์žˆ์–ด ๋ถ€๋‹ด์ด ์ ์€ ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ์—์„œ ๋‹ค๋ฃจ์–ด๋ณด๋Š” ๊ฒƒ๋„ ์ฆ๊ฒ์ง€ ์•Š์„๊นŒ ์‹ถ์–ด์„œ๋‹ค.

npm ๋ฒˆ๋“ค๋Ÿฌ ๋‹ค์šด๋กœ๋“œ ์ˆ˜ ๋น„๊ต

webpack์˜ ์„ค์ • ํŒŒ์ผ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง„๋‹ค. ๋ช‡ ๊ฐ€์ง€ ํ•„์ˆ˜์ ์ธ ์„ค์ •์„ ์‚ดํŽด๋ณด์ž.

var path = require('path'); module.exports = { mode: 'none', entry: './src/index.js', output: { filename: 'main.js', path: path.resolve(__dirname, 'dist'), }, };

entry, output

๋ฒˆ๋“ค๋ง์ด ์‹œ์ž‘๋˜๋Š” ์ง„์ž…์ ์€ entry ํฌ์ธํŠธ๋กœ ์ œ๊ณตํ•˜๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋ฅผ output์— ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์‹ฑ๊ธ€ ํŽ˜์ด์ง€ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ œ์ž‘ํ•  ๊ฒฝ์šฐ ์ง„์ž…์ ์„ ํ•˜๋‚˜๋กœ ๋‘์–ด ๋ชจ๋“  ํŽ˜์ด์ง€๋ฅผ ํ•˜๋‚˜์˜ ๋ฒˆ๋“ค๋กœ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๊ณ , ์ง„์ž…์ ์„ ์—ฌ๋Ÿฌ ๊ฐœ๋กœ ๋‚˜๋ˆ„์–ด ๋ฉ€ํ‹ฐ ํŽ˜์ด์ง€ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์„ฑํ•˜๋Š” ํŽ˜์ด์ง€ ๋ณ„ ๋ฒˆ๋“ค์„ ์ƒ์„ฑํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

entry: { login: './src/LoginPage.js', main: './src/MainPage.js' }

loader

webpack์€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ์ด ์•„๋‹Œ image, css ๋“ฑ๊ณผ ๊ฐ™์€ ์ž์›๋“ค์„ ํ•ด์„ํ•˜๊ณ  ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ๋„๋ก loader๋ฅผ ์ œ๊ณตํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

// ... module: { rules: [ { test: /\.(ts|tsx)$/, loader: 'ts-loader', options: { transpileOnly: true, }, }, { test: /\.css?$/, use: ['style-loader', 'css-loader', 'postcss-loader'], }, { test: /\.(webp|jpg|png|jpeg)$/, loader: 'file-loader', options: { name: '[name].[ext]?[hash]', }, }, { test: /\.(ts|tsx|js|jsx)$/, exclude: /node_modules/, use: 'babel-loader', }, ]; }

loader๋ฅผ ์ ์šฉํ•  ํŒŒ์ผ ์œ ํ˜•์ด ๊ฐ™๋‹ค๋ฉด rules ์—์„œ ๋จผ์ € ์˜ค๋Š” loader๊ฐ€ ์ ์šฉ๋˜๊ณ , ํ•˜๋‚˜์˜ rule์— ์—ฌ๋Ÿฌ ๊ฐœ์˜ loader๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด, use ๋ฐฐ์—ด์˜ ๋’ค์—์„œ๋ถ€ํ„ฐ loader๊ฐ€ ์ ์šฉ๋œ๋‹ค.

// css-loader๊ฐ€ style-loader๋ณด๋‹ค ์šฐ์„  ์ ์šฉ module: { rules: [ { test: /\.css$/, use: ['css-loader'], }, { test: /\.css$/, use: ['style-loader'], }, ]; } // sass-loader -> css-loader -> style-loader ์ˆœ์œผ๋กœ ๋กœ๋” ์ ์šฉ modules: { rules: [ { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'], }, ]; }

plugin

webpack์˜ plugin์€ webpack์˜ ๊ธฐ๋ณธ์ ์ธ ๋ฒˆ๋“ค๋ง ๋™์ž‘์— ์ถ”๊ฐ€์ ์ธ ๋™์ž‘๊ณผ ๊ธฐ๋Šฅ์„ ๋ถ€๊ฐ€ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค. ๊ฐ€๋ น ํ•ด๋‹น ๊ฒฐ๊ณผ๋ฌผ์˜ ํ˜•ํƒœ๋ฅผ ์กฐ์ž‘ํ•˜๊ฑฐ๋‚˜, ์ถ”๊ฐ€์ ์ธ ์ž‘์—…์„ ์ง€์‹œํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.

plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'public', 'index.html'), hash: false, }), new webpack.DefinePlugin({ 'process.env': JSON.stringify(process.env), 'process.env.IS_WEB': JSON.stringify(isWeb), }), new CopyWebpackPlugin({ patterns: [ { from: path.resolve(__dirname, 'public/*.png'), to() { return '[name][ext]'; }, }, { from: path.resolve(__dirname, 'public/icons'), to() { return 'icons/[name][ext]'; }, }, { from: path.resolve(__dirname, 'public/manifest.json'), to: 'manifest.json', }, ...(!isWeb ? [ { from: path.resolve(__dirname, 'public/favicon.png'), to: 'icons/icon16.png', }, ] : []), ], }), ];

ํ”„๋กœ์ ํŠธ๋ฅผ ์ œ์ž‘ํ•  ๋•Œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ plugin๋“ค์„ ์‚ฌ์šฉํ–ˆ๋‹ค. ์ดˆ๊ธฐ ์„ค์ •์„ ๊ตฌ์„ฑํ•  ๋•Œ ์ผ๊ด„์ ์œผ๋กœ ์ ์šฉํ•ด ์ฃผ์—ˆ๋‹ค๋ฉด ์ข‹์•˜๊ฒ ์ง€๋งŒ, ๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฌผ์— ์—์…‹ ํŒŒ์ผ์ด ํฌํ•จ๋˜์–ด ์žˆ์ง€ ์•Š๋‹ค๋˜์ง€, ๋ณด์ผ๋Ÿฌ ํ”Œ๋ ˆ์ดํŠธ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ๋‹น์—ฐํ•˜๊ฒŒ ์ƒ๊ฐํ–ˆ๋˜ index.html ํŒŒ์ผ์ด ์ƒ์„ฑ๋˜์–ด ์žˆ์ง€ ์•Š๋Š” ๋“ฑ ์„์—ฐ์น˜ ์•Š์€ ๋ถˆํŽธํ•จ์„ ํ•˜๋‚˜์”ฉ ๋А๋ผ๋ฉด์„œ plugin์„ ์ถ”๊ฐ€ํ•ด ๋ณด๋Š” ๊ฒƒ๋„ ๊ทธ ์—ญํ• ์„ ๊นจ๋‹ซ๊ธฐ ์ข‹์€ ๊ฒฝํ—˜์ด์—ˆ๋‹ค.

CleanWebpackPlugin

์ƒˆ๋กญ๊ฒŒ ๋นŒ๋“œ๋ฅผ ์ง„ํ–‰ํ•˜๊ธฐ ์ „์— output ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ •๋ฆฌํ•ด์ค€๋‹ค. ๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฌผ๋“ค์ด ์Œ“์—ฌ ๊ตฌ๋ถ„์ด ์–ด๋ ค์›Œ์ง€๊ฑฐ๋‚˜ ์ง์ ‘ ์ œ๊ฑฐํ•ด์ฃผ์–ด์•ผ ํ•˜๋Š” ๋‚œ๊ฐํ•œ ์ƒํ™ฉ์„ ๋ฐฉ์ง€ํ•œ๋‹ค.

new CleanWebpackPlugin();

HtmlWebpackPlugin

๋ฒˆ๋“ค๋ง์ด ์™„๋ฃŒ๋œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ์„ script ํƒœ๊ทธ๋ฅผ ํ†ตํ•ด ๋กœ๋“œํ•˜๋Š” HTML ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด์ค€๋‹ค.

new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'public', 'index.html'), hash: false, }); <!DOCTYPE html> <html> <head></head> <body> <script defer="defer" src="main.js"></script> </body> </html>

CopyWebpackPlugin

์›ํ•˜๋Š” ๋ชจ๋“ˆ์„ ๋ณต์‚ฌํ•ด์„œ ๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฌผ์— ํฌํ•จ์‹œ์ผœ์ค€๋‹ค. ์ฃผ๋กœ image์™€ ๊ฐ™์€ ์—์…‹์„ ๋Œ€์ƒ์œผ๋กœ ์‚ฌ์šฉํ–ˆ๋‹ค.

new CopyWebpackPlugin({ patterns: [ { from: path.resolve(__dirname, 'public/*.png'), to() { return '[name][ext]'; }, }, { from: path.resolve(__dirname, 'public/icons'), to() { return 'icons/[name][ext]'; }, }, ], });

๋ถ€๊ฐ€์ ์ธ ์„ค์ •๋“ค

webpack-dev-server

webpack-dev-server๋Š” ๊ฐœ๋ฐœ ๊ณผ์ •์—์„œ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜๊ณ  webpack ๋ช…๋ น์–ด๋ฅผ ๋”ฐ๋กœ ์‹คํ–‰ํ•˜์ง€ ์•Š์•„๋„ ๋ณ€๊ฒฝ๋œ ๊ฒฐ๊ณผ๋ฌผ์„ ์ƒˆ๋กญ๊ฒŒ ์ ์šฉํ•ด์ค€๋‹ค.

~$ yarn add --dev webpack-dev-server ~$ webpack serve // webpack.config.js devServer: { host: 'localhost', port: PORT, open: true, hot: true, compress: true, historyApiFallback: true, }

experiments

ES2022 ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜์ธ top-level await๋ฅผ ์‚ฌ์šฉํ•ด๋ณด๊ธฐ ์œ„ํ•ด experiments ์†์„ฑ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ๋‹ค.

experiments: { topLevelAwait: true, }

ํŠธ๋žœ์Šค์ปดํŒŒ์ผ๋Ÿฌ

๋ธŒ๋ผ์šฐ์ €๋งˆ๋‹ค ํ•ด์„ ๊ฐ€๋Šฅํ•œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ŠคํŽ™ ํ•œ๊ณ„๊ฐ€ ์กฐ๊ธˆ์”ฉ ๋‹ฌ๋ผ ์ฐธ๊ณ ์„œ์—์„œ ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ ๋ฅผ ์ œ๊ณตํ•ด์ฃผ๊ณค ํ•œ๋‹ค.

babel์€ ์ด๋Ÿฌํ•œ ํฌ๋กœ์Šค ๋ธŒ๋ผ์šฐ์ง• ์ด์Šˆ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋Š” ์ŠคํŽ™์˜ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ํŠธ๋žœ์ŠคํŒŒ์ผ๋ง ํ•ด์ค€๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ ์ฝ”๋“œ๋ฅผ ๋ถ„์„ํ•ด์„œ ์ถ”์ƒ ๊ตฌ๋ฌธ ํŠธ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ์ถ”์ƒ ๊ตฌ๋ฌธ ํŠธ๋ฆฌ๋ฅผ ํ†ตํ•ด ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ•ด์„ํ•  ์ˆ˜ ์žˆ๋Š” ์ ๋‹นํ•œ ์ŠคํŽ™์˜ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ์ฝ”๋“œ๋ฅผ ์žฌ์ƒ์„ฑํ•œ๋‹ค.

babel์˜ ์„ค์ • ํŒŒ์ผ์€ ์ผ๋ฐ˜์ ์œผ๋กœ presets, plugins์„ ๋‚˜์—ดํ•œ๋‹ค. plugin์€ ๊ฐ€๋ น ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ์— ํฌํ•จ๋œ react ๊ฐœ๋ฐœ ๊ตฌ๋ฌธ๊ณผ jsx, ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋กœ ์ž‘์„ฑ๋œ ์ฝ”๋“œ๋ฅผ ํŠธ๋žœ์ŠคํŒŒ์ผ๋ง ๊ณผ์ •์—์„œ ํ•ด์„ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค. plugin์€ ์ข…์ข… ํ•˜๋‚˜์˜ ๋ชฉ์ ์„ ์ด๋ฃจ๊ณ ์ž ์˜์กด์ ์œผ๋กœ ์ถ”๊ฐ€ํ•ด ์ฃผ์–ด์•ผ ํ•˜๋Š” plugin๋“ค์ด ์žˆ๋Š”๋ฐ, ์ด๋“ค์„ ๋ฌถ์–ด๋‘” ๊ฒƒ์ด preset์ด๋‹ค.

module.exports = { presets: [ '@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript', ], plugins: [ 'babel-plugin-macros', 'styled-components', [ '@babel/plugin-transform-runtime', { regenerator: true, }, ], ], };

babel-env

babel-env์€ ๋ธŒ๋ผ์šฐ์ €์™€ ๋ฒ„์ „์„ ์ œ๊ณตํ•ด ํ•ด๋‹น ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋™์ž‘ ๊ฐ€๋Šฅํ•œ ์ฝ”๋“œ๋ฅผ ์ƒ์‚ฐํ•˜๊ณ , ๊ณ ์ŠคํŽ™์˜ ์ฝ”๋“œ์˜ ๊ฒฝ์šฐ polyfill์„ ์ œ๊ณตํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค. ๊ฐ€๋ น ES2015์˜ Promise ๊ฐ์ฒด๋ฅผ ํŠธ๋žœ์ŠคํŒŒ์ผ๋งํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒฐ๊ณผ๋ฅผ ์–ป๊ฒŒ ๋˜๋Š”๋ฐ,

new Promise(); 'use strict'; new Promise();

์—ฌ์ „ํžˆ Promise๋กœ ์œ ์ง€๋˜๋Š” ์ฝ”๋“œ๋Š” ์œˆ๋„์šฐ ์ต์Šคํ”Œ๋กœ์–ด์™€ ๊ฐ™์€ ๋ธŒ๋ผ์šฐ์ €์—์„œ ํ•ด์„์ด ๋ถˆ๊ฐ€๋Šฅํ•ด ๋ ˆํผ๋Ÿฐ์Šค ์—๋Ÿฌ๋ฅผ ๋˜์ง„๋‹ค. babel-env๋Š” ์ด๋Ÿฌํ•œ ์ด์Šˆ์— ๋งž์„œ Promise๋ฅผ ํ•ด์„ํ•  ์ˆ˜ ์žˆ๋„๋ก polyfill์ด๋ผ๋Š” ์ฝ”๋“œ ์กฐ๊ฐ์„ ์ œ๊ณตํ•˜์—ฌ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•œ๋‹ค.

[ "@babel/preset-env", { useBuiltIns: "usage", // ํด๋ฆฌํ•„ ์‚ฌ์šฉ ๋ฐฉ์‹ ์ง€์ • corejs: { // ํด๋ฆฌํ•„ ๋ฒ„์ „ ์ง€์ • version: 2, }, }, ], 'use strict'; require('core-js/modules/es6.promise'); require('core-js/modules/es6.object.to-string'); new Promise();

@babel/preset-react

react๋ฅผ ์œ„ํ•œ preset๋„ ๋ธŒ๋ผ์šฐ์ €์—์„œ ํ•ด์„ ๊ฐ€๋Šฅํ•œ ์ฝ”๋“œ๋กœ ๋ณ€ํ™˜ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋œ๋‹ค.

import React from 'react'; function Component() { return <button>Click me!</button>; } export default Component; import React from 'react'; import { jsx as _jsx } from 'react/jsx-runtime'; function Component() { return /*#__PURE__*/ _jsx('button', { children: 'Click me!', }); } export default Component;

@babel/preset-typescript

ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ•ด์„ํ•˜๊ธฐ ์œ„ํ•œ ํ”„๋ฆฌ์…‹ ๋˜ํ•œ ์ œ๊ณตํ•œ๋‹ค.

const foo: number = 1; const foo = 1;

babel-loader

์œ„์—์„œ ์‚ดํŽด๋ณธ babel์€ ์ผ๋ฐ˜์ ์œผ๋กœ webpack๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•œ๋‹ค. ๋นŒ๋“œ ๊ณผ์ •์—์„œ ์ ์ ˆํ•œ ์ŠคํŽ™์˜ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ํŠธ๋žœ์ŠคํŒŒ์ผ๋ง ํ•œ ๋’ค ์ด๋ฅผ ๋ฒˆ๋“ค๋งํ•˜๊ธฐ ์œ„ํ•จ์ธ๋ฐ, ์ด๋Š” babel-loader๋ฅผ ํ†ตํ•ด ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.

// webpack.config.js module.exports = { ... module: { rules: [ { test: /\.(ts|tsx|js|jsx)$/, exclude: /node_modules/, loader: 'babel-loader', }, ], }, .. };

babel-loader์™€ babel.config.js

webpack์— ์ ์šฉ ๊ฐ€๋Šฅํ•œ babel-loader์˜ ์˜ต์…˜์œผ๋กœ presets, plugins๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์–ด babel ์„ค์ • ํŒŒ์ผ์„ ๋”ฐ๋กœ ๊ตฌ์„ฑํ•  ํ•„์š”๋Š” ์—†์ง€๋งŒ, ๋งŒ์•ฝ ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ ํด๋”์— babel ์„ค์ • ํŒŒ์ผ์ด ์กด์žฌํ•œ๋‹ค๋ฉด babel-loader๊ฐ€ ์ ์šฉ๋  ๋•Œ ํ•ด๋‹น ์„ค์ •์„ ์ฝ์–ด๋“œ๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์— ์„ค์ •์„ ์ œ๊ณตํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์ž์œ ๋กญ๊ฒŒ ์„ ํƒ ๊ฐ€๋Šฅํ•˜๋‹ค.

ํ…Œ์ŠคํŠธ

ํ…Œ์ŠคํŠธ๋Š” jest ํ™˜๊ฒฝ์—์„œ ๋ฆฌ์—‘ํŠธ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด testing-library๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ts-jest preset์„ ํ†ตํ•ด ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ๋ฅผ ์ปค๋ฒ„ํ–ˆ๋‹ค.

import type { JestConfigWithTsJest } from 'ts-jest'; const config: JestConfigWithTsJest = { preset: 'ts-jest', testEnvironment: 'jsdom', setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'], ... }; export default config;

๋งŒ์•ฝ ๋ชจ๋“ˆ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๊ฒฝ๋กœ์— ๋ณ„์นญ์„ ๋ถ€์—ฌํ•ด์„œ ์ถ•์•ฝํ•ด ์‚ฌ์šฉํ–ˆ๋‹ค๋ฉด, jest ํ™˜๊ฒฝ์—๋„ ์•Œ๋ ค์ฃผ์–ด์•ผ ํ•œ๋‹ค. webpack์˜ alias ์†์„ฑ๊ณผ ๋™์ผํ•˜๋‹ค.

const config: JestConfigWithTsJest = { ..., moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', '^@layout(.*)$': '<rootDir>/src/_layout/$1', '^@shared(.*)$': '<rootDir>/src/_shared/$1', }, } }

๋˜ํ•œ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋ณ€ํ™˜ํ•ด ํ…Œ์ŠคํŠธํ•  ๋•Œ ๋ณ€ํ™˜๋œ ๊ฒฐ๊ณผ๊ฐ€ ๊ฐœ๋ฐœ ๊ฒฐ๊ณผ๋ฌผ๊ณผ ๋™์ผํ•˜๋„๋ก ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ๋ฐ babel ์„ค์ • ํŒŒ์ผ์„ ์ œ๊ณตํ–ˆ๋‹ค.

const config: JestConfigWithTsJest = { ..., transform: { '^.+\\.tsx?$': [ 'ts-jest', { tsconfig: '<rootDir>/tsconfig.json', babelConfig: '<rootDir>/babel.config.js', useESM: true, }, ], }, } }

์ˆœ์ˆ˜ํ•œ ํ•จ์ˆ˜์™€ ์„น์…˜ ํ†ตํ•ฉ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ

โ€˜์ •๋ น ๋‚˜๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์˜์ง€ํ•˜๊ณ  ํ•„์š”๋กœ ํ• ๊นŒ, ์•„๋‹ˆ๋ฉด ๊ณต๊ฐœ๋œ ํ”„๋กœ์ ํŠธ๋ผ์„œ ๊ตฌ์ƒ‰ ๋งž์ถ”๊ธฐ ์œ„ํ•จ์ธ ๊ฑธ๊นŒ?โ€™ ๊ฐ•ํ•œ ๊ฒฐํ•ฉ๋“ค๋กœ ์ด๋ฃจ์–ด์ง„ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ๋‚˜์„œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ ํ›„ํšŒํ•˜๋ฉฐ ๋“ค์—ˆ๋˜ ์ƒ๊ฐ์ด๋‹ค. ๋‹ค์‹œ ํ•œ๋ฒˆ ์‹คํŒจํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ ๊ตฌํ˜„ ์ฝ”๋“œ๋ณด๋‹ค ๋จผ์ € ์ž‘์„ฑ๋˜์–ด์•ผ ํ•œ๋‹ค๋Š” ๊ฑธ ๋А๊ผˆ๋‹ค.

100%์˜ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์ž๋ž‘ํ•˜๋Š” ๊ฒƒ๋„ ์ข‹๊ฒ ์ง€๋งŒ, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ ์ด์œ ์— ๋Œ€ํ•ด ๊ฒฝํ—˜์„ ํ†ตํ•ด ๋™์˜ํ•  ์ˆ˜ ์žˆ๋Š” ๋ถ€๋ถ„๋“ค์„ ์œ„์ฃผ๋กœ ์ปค๋ฒ„ํ•˜๋ ค ํ–ˆ๋‹ค. ๊ทธ ์‹œ์ž‘์ ์€ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๊ฐ€ ์—†๋Š” ์ˆœ์ˆ˜ํ•œ ํ•จ์ˆ˜ ํ…Œ์ŠคํŠธ์ด๋‹ค. moment.js์™€ ๊ฐ™์ด ์˜์กด์„ฑ ๋ชจ๋“ˆ์—์„œ ์ œ๊ณตํ•˜๋Š” ํ•จ์ˆ˜๋“ค์„ ์‚ฌ์šฉํ•˜๋Š” ํ•จ์ˆ˜๋“ค์˜ ํ…Œ์ŠคํŠธ๋Š” ํŒจํ‚ค์ง€์—๊ฒŒ ํ…Œ์ŠคํŠธ๋ฅผ ๋งก๊ธธ ์ˆ˜ ์žˆ์„ ๊ฑฐ๋ผ ๊ธฐ๋Œ€ํ•˜๊ณ , ๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ํ•จ์ˆ˜๋“ค์—๋งŒ ์ง‘์ค‘ํ–ˆ๋‹ค.

์ˆœ์ˆ˜ํ•œ ํ•จ์ˆ˜๋“ค์˜ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋‘๋ฉด ๋‚˜์ค‘์— ๋กœ์ง์ด ๊ตฌ๊ตฌ์ ˆ์ ˆ ๋ถˆํŽธํ•˜๊ฒŒ ๋А๊ปด์งˆ ๋•Œ ํ•จ์ˆ˜์˜ ๊ฒฐ๊ณผ๊ฐ€ ์ด์ „๊ณผ ๋™์ผํ•˜๋‹ค๋Š” ๊ฒƒ์„ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์ ์œผ๋กœ ์˜์ง€ํ•œ ์ฑ„ ๋ฆฌํŒฉํ† ๋งํ•ด ๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

export const getSortedLanguageList: SortedLanguageList = mergedLanguageList => { return Object.entries(mergedLanguageList) .sort(([, a], [, b]) => Number(b) - Number(a)) .reduce((r, [k, v]) => ({ ...r, [k]: v }), {}); }; describe('...', () => { it('getSortedLanguageList๋Š” ๋ณ‘ํ•ฉ๋œ ์–ธ์–ด๋ณ„ ์‚ฌ์šฉ๋Ÿ‰์„ ๊ธฐ์ค€์œผ๋กœ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•œ๋‹ค.', () => { expect(getSortedLanguageList({})).toStrictEqual({}); expect(getSortedLanguageList(mockedMergedLanguageList)).toStrictEqual( mockedMergedLanguageList ); }); });

๋‹ค์Œ์œผ๋กœ ์ปค๋ฒ„ํ•œ ํ…Œ์ŠคํŠธ๋Š” ์„น์…˜๋ณ„ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์ด๋‹ค. ๋ณธ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๊ฐ๊ฐ์˜ ๋…๋ฆฝ์ ์ธ ๊ธฐ๋Šฅ์„ ๋‹ด๊ณ  ์žˆ๋Š” ์„น์…˜(์•„๋ž˜ ์ด๋ฏธ์ง€์˜ ๋นจ๊ฐ„ ๋ฐ•์Šค)์„ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ๋‹จ์œ„๋กœ ์žก์•˜๊ณ , ์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ผ ๊ฐ€์ •ํ•œ ๊ฐ€์งœ ๋ฐ์ดํ„ฐ๋“ค๊ณผ ์ „์—ญ ์ƒํƒœ๋ฅผ ์ฃผ์ž…ํ•ด ์˜์กด์„ฑ์„ ํ•ด๊ฒฐํ•ด ์ฃผ์—ˆ๋‹ค.

GitGlances ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ๋ฒ”์œ„

import { render, screen } from '@testing-library/react'; import { ContributionsCollection } from '@shared/apis/contribution'; import Contribution from '../components'; import useContributionsCollectionQuery from '../queries/useContributionsCollectionQuery'; import { mockedContributionCollection } from './mocks'; import { RecoilRoot } from 'recoil'; const mockedUseContributionQuery = useContributionsCollectionQuery as jest.Mock< ContributionsCollection >; jest.mock('../queries/useContributionsCollectionQuery'); describe('Contribution ์ปดํฌ๋„ŒํŠธ๋Š” ์œ ์ €์˜ ์˜ค๋Š˜ ๊ธฐ์—ฌ๋„ ์ •๋ณด๋ฅผ ๋žœ๋”๋งํ•œ๋‹ค.', () => { beforeEach(() => { mockedUseContributionQuery.mockImplementation( () => mockedContributionCollection ); }); afterEach(() => { jest.clearAllMocks(); }); it('์œ ์ €์˜ ์ด ๊ธฐ์—ฌ๋„๋Š” ์„น์…˜ ์š”์•ฝ ์ •๋ณด์—์„œ ์ œ๊ณตํ•œ๋‹ค.', async () => { render( <RecoilRoot> <Contribution /> </RecoilRoot> ); const totalContributions = await screen.findByText('3'); expect(totalContributions).toBeInTheDocument(); }); });

ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

components, hooks, utils ๋ณ„๋กœ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๊ตฌ์„ฑํ•˜๊ณ  ๋กœ์ง ๋ ˆ๋ฒจ๊ณผ UI๋ฅผ ์œตํ•ฉํ•˜๊ธฐ ์œ„ํ•ด ๊ฑฐ๋Œ€ํ•œ ์ฝ”๋“œ ๋ฒ ์ด์Šค๋ฅผ ๋‹ค์‹œ ๊ฑฐ์Šฌ๋Ÿฌ ์˜ฌ๋ผ๊ฐ€๊ธฐ๋„ ํ•˜๊ณ , ์›ํ•˜๋Š” ์˜์กด์„ฑ์ด ์–ด๋””์— ์œ„์น˜์— ํ–ˆ๋Š”์ง€ ๊ด€๋ จ๋œ ์ฝ”๋“œ์˜ ๊ธฐ์–ต์„ ๋”๋“ฌ์–ด ๊ฒ€์ƒ‰ํ•ด๋ณด๊ธฐ๋„ ํ•œ๋‹ค.

import { getSortedLanguageList } from '../../../../utils/language';

๋ณธ ํ”„๋กœ์ ํŠธ๋„ ์ดˆ๊ธฐ์—๋Š” ๋™์ผํ•œ ๊ตฌ์กฐ๋กœ ์ง„ํ–‰ํ•˜๋‹ค๊ฐ€ ๋ ˆํผ๋Ÿฐ์Šค ์‚ผ์•„ ์ด์ „ ํšŒ์‚ฌ ๋ฆฌ๋“œ ๋ถ„์˜ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ๋ฅผ ๋ณด๊ณ ์„  ์นดํ†ก์„ ๋“œ๋ ค ํ•ด๋‹น ํ”„๋กœ์ ํŠธ์˜ ๋ ˆํผ๋Ÿฐ์Šค๋ฅผ ์–ป์–ด๋ƒˆ๋‹ค. ์ง€์—ญ์„ฑ์˜ ์›์น™์„ ๊ณ ๋ คํ•œ ํŒจํ‚ค์ง€ ๊ตฌ์กฐ: ๊ธฐ๋Šฅ๋ณ„๋กœ ๋‚˜๋ˆ„๊ธฐ ๋ฅผ ์ฝ์–ด๋ณด๋ฉด ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ๋ฅผ ์žก๋Š”๋ฐ๋„ ๋งŽ์€ ๊ณ ๋ฏผ์„ ๋‹ด์•„๋‚ด๋Š” ๊ฒƒ์— ๋†€๋ผ์šธ ๋”ฐ๋ฆ„์ด๋‹ค.

๋ฐ›์•„๋“ค์ด๊ธฐ ๋‚˜๋ฆ„์ด์ง€๋งŒ, ์„น์…˜๋ณ„๋กœ ๊ฐ๊ฐ์˜ ๊ธฐ๋Šฅ์„ ๊ตฌ์‚ฌํ•˜๋Š” ์ œํ’ˆ์„ ์ œ์ž‘ํ•˜๊ณ  ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ธ€์—์„œ ์–ธ๊ธ‰ํ•˜๋Š” โ€˜์บ์‹œ ๋ฏธ์Šคโ€™๋ฅผ ์ตœ์†Œํ™”ํ•  ์ˆ˜ ์žˆ๋Š” ์ง€์—ญ์„ฑ์ด ๋ฐœ์ƒํ•˜๋Š” ๊ธฐ๋Šฅ๋ณ„ ๋‹จ์œ„๋กœ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๊ตฌ์„ฑํ–ˆ๊ณ , ์ž‘์—… ์ค‘์ธ ๊ธฐ๋Šฅ์˜ ๋งฅ๋ฝ ๋ธ”๋ก์„ ๊ธฐ๋Šฅ๋ณ„ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ๊ธฐ์ค€ ์‚ผ์•„ ๋จธ๋ฆฟ์†์— ์ ์žฌํ•œ ์ƒํƒœ๋กœ ์ž‘์—…ํ•ด ์ฝ”๋“œ ๋ฒ ์ด์Šค ์†์—์„œ ํ—ค๋งค์ง€ ์•Š๊ณ  ์›ํ•˜๋Š” ์˜์กด์„ฑ์„ ์ฐพ์•„๋‚ผ ์ˆ˜ ์žˆ์—ˆ๋‹ค. ๋”๋ถˆ์–ด ๊ทธ ์ˆ˜์ • ๋ฒ”์œ„ ๋˜ํ•œ ์˜ˆ์ธก ๊ฐ€๋Šฅํ•ด์ง€๋Š” ์žฅ์ ๋„ ์žˆ๋‹ค.

src โ”œโ”€ Contribution โ”‚ โ””โ”€ atoms โ”‚ โ””โ”€ components โ”‚ โ””โ”€ queries โ”‚ โ””โ”€ test โ”‚ โ””โ”€ utils โ”œโ”€ Daily โ”œโ”€ Language โ”œโ”€ Notification โ”œโ”€ Refactor โ”œโ”€ ... โ”œโ”€ _layout โ”œโ”€ _shared

react

react v.18์€ ๋™์‹œ์„ฑ ๊ธฐ๋Šฅ์„ ๋‹ด์•„๋‚ด๊ธฐ ์œ„ํ•ด 5๋…„์— ๊ฐ€๊นŒ์šด ์‹œ๊ฐ„์„ ๋“ค์—ฌ ๊ธฐ์ €์˜ ์•„ํ‚คํ…์ฒ˜๋ถ€ํ„ฐ ๋ฉ˜ํƒˆ ๋ชจ๋ธ์„ ๊ณ ์•ˆํ•ด ๋งŒ๋“  ๋Œ€๊ทœ๋ชจ ์—…๋ฐ์ดํŠธ๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ๋‹ค. ์ถฉ๋ถ„ํ•œ ๊ณต๋ถ€๊ฐ€ ํ•„์š”ํ–ˆ๊ณ  ๋ฉ”์ปค๋‹ˆ์ฆ˜ ๊ตฌํ˜„์ฒด์— ๋Œ€ํ•ด ์—ด์‹ฌํžˆ ํ†บ์•„๋ณธ ๊ฒฐ๊ณผ๋ฌผ์„ ๊ธ€๋กœ ์ •๋ฆฌํ•ด ๋ณด๊ธฐ๋„ ํ–ˆ๋‹ค.

Suspense

Suspense๋Š” ์ž์‹ ์ด ๊ฐ์‹ผ ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ์—์„œ throw๋œ promise๊ฐ€ resolve๋  ๋•Œ๊นŒ์ง€ ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ ์ž‘์—…์„ ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„๋กœ ๋ ˆ์ธ์„ ๋ณ€๊ฒฝํ•ด ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋‹ค๋ฅธ ์ž‘์—…์— ๋„˜๊ฒจ์ฃผ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค. ๊ทธ๋Ÿฌ๊ณค ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ ๋žœ๋”๋ง์„ ๋ณด์žฅ๋ฐ›์„ ์ˆ˜ ์žˆ์„ ๋•Œ๊นŒ์ง€ fallback์— ์ œ๊ณต๋œ ๋…ธ๋“œ๋ฅผ ๋…ธ์ถœ์‹œํ‚จ๋‹ค.

์—ฌ๊ธฐ์„œ promise๋Š” React.lazy๋ฅผ ํ†ตํ•ด ๋™์ ์œผ๋กœ ํ•„์š”ํ•œ ์ž์›์„ ๋กœ๋“œํ•˜๋Š” ๊ฒƒ, ์ปจํ…์ธ  ๋žœ๋”๋ง์— ์‚ฌ์šฉ๋  ๋ฐ์ดํ„ฐ ์š”์ฒญํ•˜๋Š” ๊ฒƒ์„ ํฌํ•จํ•œ ๊ธฐ๋‹ค๋ฆด ์ˆ˜ ์žˆ๋Š” ๋ชจ๋“  ๊ฒƒ์ด ๊ทธ ๋Œ€์ƒ์ด ๋˜๊ณ , ๋•๋ถ„์— ๊ฐœ๋ฐœ์ž๋ฅผ ํ”ผ๊ณคํ•˜๊ฒŒ ํ–ˆ๋˜ ๋ฐ์ดํ„ฐ ํŒจ์นญ์— ๋Œ€ํ•œ ์„ฑ๊ณต ์—ฌ๋ถ€์— ๋”ฐ๋ฅธ UI ๊ตฌ์„ฑ ๋ถ„๊ธฐ๋ฅผ ์„ ์–ธ์ ์œผ๋กœ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.

const resource = fetchProfileData(); function App() { return ( <ErrorBoundary fallback={<Error />}> <Suspense fallback={<Loader />}> <UserProfile /> </Suspense> </ErrorBoundary> ); } function UserProfile() { const user = resource.user.read(); return <div>{user.userName}</div>; }

Suspense์— ๋Œ€ํ•œ ๋‚ด์šฉ์€ ๋”ฐ๋กœ ์ž์„ธํžˆ ์ •๋ฆฌํ•  ์˜ˆ์ •์œผ๋กœ ๋ณธ ๊ธ€์—์„œ๋Š” ์ถ•์•ฝํ•ด ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

Error Boundary

์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ๋ถ€ํ„ฐ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฆฌ์—‘ํŠธ ๋‚ด๋ถ€ ์ƒํƒœ๋“ค์„ ํ›ผ์†ํ•˜๊ณ  ์ œํ’ˆ ์ „์ฒด์˜ ๋žœ๋”๋ง ๋ฌธ์ œ๋ฅผ ๋ฐœ์ƒ์‹œ์ผฐ์ง€๋งŒ, ๋ฆฌ์—‘ํŠธ๋Š” ์ด๋ฅผ ๋Œ€์‘ํ•  ๋งŒํ•œ ์ ์ ˆํ•œ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•˜์ง€ ์•Š์•˜์—ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‹ค react v16์—์„œ ์ƒˆ๋กญ๊ฒŒ ์—๋Ÿฌ ๊ฒฝ๊ณ„๋ผ๋Š” ์ƒˆ๋กœ์šด ๊ฐœ๋…์„ ๋„์ž…ํ•ด ๋ฐœ์ƒํ•œ ์—๋Ÿฌ๊ฐ€ ํผ์ง€๋Š” ๊ฒฝ๊ณ„๋ฅผ ๋‘์–ด ์•ฑ ์ „์—ญ์œผ๋กœ ์—๋Ÿฌ๊ฐ€ ์˜ํ–ฅ์„ ๋ผ์น˜๋Š” ๊ฒƒ์„ ์ œํ•œํ•˜๊ณ , ๋žœ๋”๋ง ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ•œ ์ปดํฌ๋„ŒํŠธ ๋Œ€์‹  fallback์œผ๋กœ ์ „ํ™˜๋  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค.

์—๋Ÿฌ ๊ฒฝ๊ณ„๋Š” static getDerivedStateFromError() ํ˜น์€ componentDidCatch() ๊ฐ€ ์„ ์–ธ๋œ ํด๋ž˜์Šค ์ปดํฌ๋„ŒํŠธ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๊ณ , ์ด ์ปดํฌ๋„ŒํŠธ ์ž์ฒด๊ฐ€ ์—๋Ÿฌ์˜ ๊ฒฝ๊ณ„๊ฐ€ ๋œ๋‹ค.

class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // ๋‹ค์Œ ๋ Œ๋”๋ง์—์„œ ํด๋ฐฑ UI๊ฐ€ ๋ณด์ด๋„๋ก ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•ฉ๋‹ˆ๋‹ค. return { hasError: true }; } componentDidCatch(error, errorInfo) { // ์—๋Ÿฌ ๋ฆฌํฌํŒ… ์„œ๋น„์Šค์— ์—๋Ÿฌ๋ฅผ ๊ธฐ๋กํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. logErrorToMyService(error, errorInfo); } render() { if (this.state.hasError) { // ํด๋ฐฑ UI๋ฅผ ์ปค์Šคํ…€ํ•˜์—ฌ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. return <h1>Something went wrong.</h1>; } return this.props.children; } }

์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด fallback์ด ๋…ธ์ถœ๋  ์ˆ˜ ์žˆ๋„๋ก ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์€ static getDerivedStateFromError()์ด ๋‹ด๋‹นํ•˜๊ณ , ์—๋Ÿฌ ์ •๋ณด๋ฅผ ๊ธฐ๋กํ•˜๊ธฐ ์œ„ํ—ค componentDidCatch()๊ฐ€ ์‚ฌ์šฉ๋œ๋‹ค. ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๊ฐ๊ฐ์˜ ์„น์…˜๋ณ„๋กœ ์—๋Ÿฌ ๊ฒฝ๊ณ„๋ฅผ ๋‘๊ณ  ๋‹ค๋ฅธ ๊ธฐ๋Šฅ์„ ๋‹ด๋‹นํ•˜๋Š” ์„น์…˜์— ์—๋Ÿฌ๊ฐ€ ๋ฒˆ์ ธ๋‚˜๊ฐ€์ง€ ์•Š๋„๋ก ๋ง‰์•˜๋‹ค.

... const initialState: ErrorBoundaryState = { hasError: false, error: null, errorMessage: null, }; class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> { constructor(props: ErrorBoundaryProps) { super(props); this.state = initialState; } static getDerivedStateFromError(error: any) { return { hasError: true, error, errorMessage: typeof error.message === 'string' ? error.message : null, }; } resetErrorBoundaryState = () => { const { reset } = this.props; this.setState({ ...initialState }); if (reset) reset(); }; render() { const { hasError, error, errorMessage } = this.state; const { children, gridArea, hasToken } = this.props; if (!hasToken) { return <PulseSection gridArea={gridArea} />; } if (hasError && error) { return ( <Error errorMessage={errorMessage} reset={this.resetErrorBoundaryState} gridArea={gridArea} /> ); } return children; } } export default ErrorBoundary;

์—๋Ÿฌ ๊ฒฝ๊ณ„ ์ปดํฌ๋„ŒํŠธ๋Š” Suspense์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๊ธฐ ์ข‹๋‹ค. Suspense๋Š” ๋กœ๋”ฉ ์ค‘์˜ fallback์„ ๋‹ด๋‹นํ•˜๊ณ , ErrorBoundary๋ฅผ ํ†ตํ•ด promise์˜ reject์— ๋‹ด๊ธด ์—๋Ÿฌ๋ฅผ ํ•ธ๋“ค๋งํ•œ๋‹ค.

function SuspenseBoundary({ children, gridArea = '' }: SuspenseBoundaryProps) { const { reset } = useQueryErrorResetBoundary(); const gitGlancesTokenValue = useRecoilValue(tokenAtom); return ( <ErrorBoundary reset={reset} gridArea={gridArea} hasToken={!!gitGlancesTokenValue} > <Suspense fallback={<SectionSpinner gridArea={gridArea} />}> {children} </Suspense> </ErrorBoundary> ); } // UserProfile ์„น์…˜์—์„œ ๋ฐœ์ƒํ•œ ์—๋Ÿฌ๋Š” Language ์„น์…˜์— ์˜ํ–ฅ์„ ๋ผ์น˜์ง€ ์•Š๋Š”๋‹ค. <SuspenseBoundary gridArea="Profile"> <UserProfile /> </SuspenseBoundary> <SuspenseBoundary gridArea="Language"> <Language /> </SuspenseBoundary>

react-query

react-query๋Š” ๋ทฐ๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์š”์ฒญ ์ž‘์„ฑ๊ณผ ์‘๋‹ต์„ ๋ฐ›์€ ์งํ›„ ๋ฐ์ดํ„ฐ ์ฐธ์กฐ ๋ฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์–ด์•ผ ํ•˜๋Š” ๊ฐœ๋ฐœ์ž ์ฑ…์ž„์„ ์œ„์ž„ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.

์ฟผ๋ฆฌ๋ผ๋Š” ๊ฐ๊ฐ์˜ ์ธ์Šคํ„ด์Šค๊ฐ€ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์•Œ์•„์„œ ์บ์‹ฑ, ๋ฆฌํŒจ์นญํ•ด ์š”์ฒญ ์‹œ์ ์ด ๋ฐ์ดํ„ฐ ์ฐธ์กฐ์˜ ์ง์ „์ด ์•„๋‹ˆ๋”๋ผ๋„ ๋ทฐ์—์„œ ํ•„์š”๋กœ ํ•˜๋Š” ์ตœ์‹ ํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณด์žฅํ•ด ์ค€๋‹ค. ๊ด€ํ–‰์ฒ˜๋Ÿผ ๋ฆฌ๋•์Šค ๋ฏธ๋“ค์›จ์–ด์— ๋น„๋™๊ธฐ ๋กœ์ง์„ ๋‹ด์•„ ์„œ๋ฒ„ ์ƒํƒœ ๊ฐ’์„ ๋งค๋ฒˆ ์ตœ์‹ ํ™”ํ•ด ์ฃผ์–ด์•ผ ํ–ˆ๋˜ ์ฑ…์ž„์„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—๊ฒŒ ์œ„์ž„ํ•œ๋‹ค๋Š” ๊ฒƒ์€ ์ถฉ๋ถ„ํžˆ ์ข‹์€ ๊ฒฝํ—˜์ด์—ˆ๋‹ค.

const useContributionsCollectionQuery = (from: string, to: string) => { const { data: contributionsCollection } = useQuery< ContributionsCollection, AxiosError >({ queryKey: ['contributionsCollection', from, to], refetchOnWindowFocus: true, queryFn: async () => { const { data } = await getContributionsCollection(from, to); const destructuredContributionsCollection = getDestructuredContributionsCollection( data ); return destructuredContributionsCollection; }, }); return contributionsCollection as ContributionsCollection; }; export default useContributionsCollectionQuery;

react-query์—์„œ๋Š” ์ฟผ๋ฆฌ๋ฅผ staleํ•˜๊ฒŒ ํ˜น์€ inactive ์ƒํƒœ์˜ ์ฟผ๋ฆฌ๋ฅผ ํŠน์ • ๊ธฐ๊ฐ„ ๋™์•ˆ ์บ์‹ฑํ•  ๊ฒƒ์ธ์ง€ ๊ฒฐ์ •ํ•  ์ˆ˜ ์žˆ๋Š” staleTime, cacheTime์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ณธ ํ”„๋กœ์ ํŠธ์˜ ๊ธฐ๋Šฅ์„ ์˜ˆ์‹œ๋กœ ๋“ค๋ฉด, notification์ด๋‚˜ contribution์˜ ๊ฒฝ์šฐ ์•ฑ์ด ์ƒˆ๋กญ๊ฒŒ ๋งˆ์šดํŠธ๋˜๊ฑฐ๋‚˜, ํฌ์ปค์Šค๋ฅผ ๋ฐ›์•˜์„ ๋•Œ ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธํ•ด์ฃผ์–ด์•ผ ํ–ˆ์ง€๋งŒ, ํ•œ๋‹ฌ ๋‹จ์œ„์˜ ํŠธ๋žœ๋“œ ๋ ˆํฌ์ง€ํ† ๋ฆฌ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” trends์„น์…˜์˜ ๊ฒฝ์šฐ ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ๊ฐ€ ๋  ํ•„์š”๊ฐ€ ์—†์–ด ํ•˜๋ฃจ ์ •๋„ ์ฟผ๋ฆฌ๋ฅผ freshํ•˜๊ฒŒ ์œ ์ง€ํ•ด์ค˜๋„ ํฌ๊ฒŒ ๋ฌธ์ œ๋˜์ง€ ์•Š์•˜์„ ๊ฒƒ์ด๋‹ค.

์ •๋ง ์•„์‰ฝ๊ฒŒ๋„ ๋ณธ ํ”„๋กœ์ ํŠธ์—์„œ graphql๋กœ ๊ตฌ์„ฑ๋œ github API๋ฅผ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ, graphql์€ ์ผ๋ฐ˜์ ์œผ๋กœ POST ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด ์ฟผ๋ฆฌ๋ฅผ ๋ฐ”๋””์— ๋‹ด์•„ ์š”์ฒญ์„ ์ „๋‹ฌํ•˜๋Š” ๊ตฌ์กฐ์—ฌ์„œ GET ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์–ป์€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๋Š” ์ฟผ๋ฆฌ์ฒ˜๋Ÿผ ์ƒํƒœ๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์—†์–ด ๊ธฐ๋ฏผํ•˜๊ฒŒ API ์š”์ฒญ์„ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์—†์—ˆ๋‹ค.

recoil

recoil์€ atom ๋‹จ์œ„๋กœ ์ „์—ญ ์ƒํƒœ๋ฅผ ๊ตฌ์„ฑํ•˜๊ณ , selector๋ฅผ ํ†ตํ•ด ์†Œ๋น„์ž์—๊ฒŒ ์ „์—ญ ์ƒํƒœ๋ฅผ ๊ตฌ๋…, ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋Š” API๋ฅผ ์ œ๊ณตํ•œ๋‹ค. ๋ฆฌ๋•์Šค๋ฅผ ๋‹ค์‹œ ์ƒ๊ฐํ•ด ๋ณด๋ฉด ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ์ „๋‹ฌ๋ฐ›์€ ๋ฐ์ดํ„ฐ์™€ ํด๋ผ์ด์–ธํŠธ์˜ ์ „์—ญ ์ƒํƒœ๊ฐ€ ๊ณต์กดํ•˜๋Š” ์Šคํ† ์–ด ๊ตฌ์กฐ์˜€์ง€๋งŒ, ์„œ๋ฒ„ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ react-query์—๊ฒŒ ๋งก๊ธฐ๊ณ  recoil์€ ํด๋ผ์ด์–ธํŠธ์˜ ์ „์—ญ ์ƒํƒœ์— ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ๋„๋ก ์‚ฌ์šฉํ–ˆ๋‹ค.

๋”๋ถˆ์–ด, ๋ธŒ๋ผ์šฐ์ € ์Šคํ† ์–ด์™€ ์ƒํƒœ๋ฅผ ๋™๊ธฐํ™”์‹œ์ผœ persistํ•˜๊ฒŒ ์œ ์ง€์‹œ์ผœ ์ฃผ์–ด์•ผ ํ•˜๋Š” ์ „์—ญ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ์šฉ์ดํ–ˆ๋‹ค. atom effects๋Š” atom์„ ๋™๊ธฐํ™”ํ•˜๊ฑฐ๋‚˜ ์ดˆ๊ธฐํ™”ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋Š”๋ฐ ํŽ˜์ด์ง€๊ฐ€ ์ƒˆ๋กœ๊ณ ์นจ ๋˜์—ˆ์„ ๋•Œ localStorage์™€ ๊ฐ™์€ ๋ธŒ๋ผ์šฐ์ € ์Šคํ† ์–ด์—์„œ ์ €์žฅํ•ด ๋‘์—ˆ๋˜ ์ด์ „ ์ƒํƒœ๋ฅผ ๋ถˆ๋Ÿฌ์™€ atom์„ ์ดˆ๊ธฐํ™”ํ•œ๋‹ค.

import { AtomEffect } from 'recoil'; import { getChromeStorageItem, setChromeStorageItem, } from '@shared/utils/chrome'; const localStorageEffect = <AtomDataType>(key: string) => { const effects: AtomEffect<AtomDataType> = ({ setSelf, onSet }) => { const savedValue = localStorage.getItem(key); if (savedValue != null) { setSelf(JSON.parse(savedValue)); } onSet((newValue, _, isReset) => { if (isReset) { localStorage.removeItem(key); } else { localStorage.setItem(key, JSON.stringify(newValue)); } }); }; return effects; }; export const dailyRepoAtom = atom<AtomRepoState>({ key: 'dailyRepo', default: { prevRepo: null, updatedAt: '', hasTodayContribution: false, }, effects: [storageEffect<AtomRepoState>('dailyRepo')], });

Reference