October 23, 2023
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์ ๋์
ํด ํ์
๊ณผ์ ์์ ์ผ๊ด๋ ์์กด์ฑ ๋ฒ์ ๊ด๋ฆฌ๋ฅผ ๋ณด์ฅํ๊ณ , ๊น์ด์ง๋ ์์กด์ฑ ํธ๋ฆฌ์ ๋ฐ๋ผ ์ค๋ณต ์ค์น๋๋ ์์กด์ฑ๋ค์ ํธ์ด์คํ
์์ผ ํ๋ซํ๊ฒ
๊ด๋ฆฌํ๋ ์ปจ์
์ ๊ฐ์ง๊ณ ์๋ ๊ฒ์ด ํน์ง์ด๋ค.
์์กด์ฑ ํธ์ด์คํ ์ ํตํด ๋์คํฌ ๊ณต๊ฐ์ ์ธ์ด๋ธํ ์ ์๋ค๋ ์ฅ์ ์ด ์์ง๋ง, ํน์ ์์กด์ฑ ํจํค์ง์ ์์กด์ฑ์ด ํธ์ด์คํ ๋์ด ํ๋ก์ ํธ์ ์ง์ ์ถ๊ฐํ์ง ์์ ์์กด์ฑ์ด ๋ฌธ์ ์์ด ๋ถ๋ฌ์ฌ ์ ์๊ฒ ๋๋ ์ ๋ น ์์กด์ฑ (Phantom Dependency) ํ์์ด ๋ฐ์ํ๊ธฐ๋ ํ๋ค.
์ธ๋๋ ์ด์ผ๊ธฐ์ง๋ง, ํ์ฌ๋ pnpm
๋ฅผ ์ฃผ๋ก ์ฌ์ฉํ๋ค. pnpm
์ ์ฌ๋งํฌ, ํ๋๋งํฌ๋ฅผ ํตํด ํ๋ก์ ํธ์์ ์์กด์ฑ์ ๋ก์ปฌ ๋์คํฌ์ ์์ ์คํ ์ด์ ์ค์น๋ ์์กด์ฑ๊ณผ ์ฐ๊ฒฐ์์ผ
์ฌ์ฉํ๋ Content-addressable-storage
์ ๋ต์ ํตํด ์ฌ๋ฌ ํ๋ก์ ํธ์์ ์ค๋ณต๋๊ฒ ์ฌ์ฉ๋๋ ์์กด์ฑ์ ๋ํ ์ค๋ณต ์ค์น๋ฅผ ๋ฐฉ์งํ๊ณ ์๋ค. ์ด ์ปจ์
์ด ์ ์๊ฒ ์ถฉ๊ฒฉ์ด์๋ค.
๋ชจ๋ ์์คํ ์ ๊ธฐ๋ฐ์ผ๋ก ํ์ฑ๋ ๊ฑฐ๋ํ ์ฝ๋ ๋ฒ ์ด์ค์ CSS, ์ด๋ฏธ์ง ๋ฑ ์น์ ๊ตฌ์ฑํ๊ธฐ ์ํ ๋ชจ๋ ์์์ ๋ชจ๋์ด๋ผ ํ๋ค. ๋ฒ๋ค๋ฌ๋ ์คํฌ๋ฆฝํธ ๋ชจ๋ ๋ด์ ํ์ผ ๋จ์์ ๋ณ์ ์ ํจ ๋ฒ์๋ฅผ ์ ์งํ ์ฑ ์น์ ๊ตฌ์ฑํ๋ ๋ชจ๋ ๋ชจ๋๋ค์ ํ๋์ ๋ฒ๋ค๋ก ์์ฑํ๋ค.
ํ๋๋ก ๋ฌถ์ธ ๋ฒ๋ค์ ์ ๋์ ์ธ ์์ฒญ ํ์๋ฅผ ์ค์ฌ ๋ธ๋ผ์ฐ์ ๋ณ๋ก ์์ดํ ์์ฒญ ํ์ ์ ํ์์ ์์ ํ ์ ์์ผ๋ฉฐ, ์ํ๋ค๋ฉด ์ฝ๋ ์คํ๋ฆฌํ ์ ํตํด ์ํ๋ ๋ ๋ถ๋ฆฌ๋ ๋ฒ๋ค์ ๋ก๋ํ ์ ์๊ณ , ์ฌ์ฉํ์ง ์์ ์ฝ๋๋ค์ ํธ๋ฆฌ ์์ดํนํด ๋ฒ๋ค๋ง ๊ณผ์ ์์ ์ ๊ฑฐํ ์ ์๋ค.
์ต๊ทผ webpack
๊ณผ ๋น๊ตํ์ ๋ ๋น๋ ์๋๊ฐ ํ์ ํ ๊ฐ์ ๋ esbuild
, vite
์ ๊ฐ์ ๋ฒ๋ค๋ฌ๋ค์ด ๋ง์ ๊ด์ฌ์ ๋ฐ๊ณ ์๊ณ , ์ต๊ทผ ์ค๋ฌด์์๋ vite
๋ฅผ ์ฃผ๋ก
์ฌ์ฉํ๋๋ฐ ์ญ์๋ ๋ฒ๋ค๋ง ์ค์ ์ด ๋งค์ฐ ๊ฐ๊ฒฐํ๊ณ ๊ฐ๋ฐ์ ์นํ์ ์ด๋ค.
ํ์ง๋ง ๊ฐ์ ๋ ๋ฒ๋ค๋ฌ๋ฅผ ์ ๋๊ฒ ์นญ์ฐฌํ๊ณ ์ webpack
์ ์ฌ์ฉํ๊ธฐ๋ก ๊ฒฐ์ ํ๋ค. ์์ง ๋ฒ๋ค๋ฌ ์์ฅ์์ ์ ์ ์จ์ด ๊ฒฌ๊ณ ํ ๋ฟ๋ง ์๋๋ผ, ์ค๋ฌด์์ ๋ฒ๋ค๋ฌ์ ๋ฌธ์ ๊ฐ ์๊ฒจ
๋๊ตฐ๊ฐ ํด๊ฒฐํด์ผ ํ๋ค๋ฉด ๊ทธ ๋์์ด webpack
์ผ ๊ฐ๋ฅ์ฑ์ด ๋์ง ์์๊น ์ถ์ด ์ค์ ์์ ๊ณผ ์ ์ฉ์ ์์ด ๋ถ๋ด์ด ์ ์ ๊ฐ์ธ ํ๋ก์ ํธ์์ ๋ค๋ฃจ์ด๋ณด๋ ๊ฒ๋ ์ฆ๊ฒ์ง ์์๊น
์ถ์ด์๋ค.
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: {
login: './src/LoginPage.js',
main: './src/MainPage.js'
}
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'],
},
];
}
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
์ ์ถ๊ฐํด ๋ณด๋ ๊ฒ๋
๊ทธ ์ญํ ์ ๊นจ๋ซ๊ธฐ ์ข์ ๊ฒฝํ์ด์๋ค.
์๋กญ๊ฒ ๋น๋๋ฅผ ์งํํ๊ธฐ ์ ์ output
๋๋ ํ ๋ฆฌ๋ฅผ ์ ๋ฆฌํด์ค๋ค. ๋น๋ ๊ฒฐ๊ณผ๋ฌผ๋ค์ด ์์ฌ ๊ตฌ๋ถ์ด ์ด๋ ค์์ง๊ฑฐ๋ ์ง์ ์ ๊ฑฐํด์ฃผ์ด์ผ ํ๋ ๋๊ฐํ ์ํฉ์ ๋ฐฉ์งํ๋ค.
new CleanWebpackPlugin();
๋ฒ๋ค๋ง์ด ์๋ฃ๋ ์๋ฐ์คํฌ๋ฆฝํธ ํ์ผ์ 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>
์ํ๋ ๋ชจ๋์ ๋ณต์ฌํด์ ๋น๋ ๊ฒฐ๊ณผ๋ฌผ์ ํฌํจ์์ผ์ค๋ค. ์ฃผ๋ก 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
๋ช
๋ น์ด๋ฅผ ๋ฐ๋ก ์คํํ์ง ์์๋ ๋ณ๊ฒฝ๋ ๊ฒฐ๊ณผ๋ฌผ์ ์๋กญ๊ฒ ์ ์ฉํด์ค๋ค.
~$ yarn add --dev webpack-dev-server
~$ webpack serve
// webpack.config.js
devServer: {
host: 'localhost',
port: PORT,
open: true,
hot: true,
compress: true,
historyApiFallback: true,
}
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
์ ๋ธ๋ผ์ฐ์ ์ ๋ฒ์ ์ ์ ๊ณตํด ํด๋น ๋ธ๋ผ์ฐ์ ์์ ๋์ ๊ฐ๋ฅํ ์ฝ๋๋ฅผ ์์ฐํ๊ณ , ๊ณ ์คํ์ ์ฝ๋์ ๊ฒฝ์ฐ 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();
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;
ํ์ ์คํฌ๋ฆฝํธ๋ฅผ ํด์ํ๊ธฐ ์ํ ํ๋ฆฌ์ ๋ํ ์ ๊ณตํ๋ค.
const foo: number = 1;
const foo = 1;
์์์ ์ดํด๋ณธ babel
์ ์ผ๋ฐ์ ์ผ๋ก webpack
๊ณผ ํจ๊ป ์ฌ์ฉํ๋ค. ๋น๋ ๊ณผ์ ์์ ์ ์ ํ ์คํ์ ์๋ฐ์คํฌ๋ฆฝํธ๋ก ํธ๋์คํ์ผ๋ง ํ ๋ค ์ด๋ฅผ ๋ฒ๋ค๋งํ๊ธฐ ์ํจ์ธ๋ฐ,
์ด๋ babel-loader
๋ฅผ ํตํด ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
// webpack.config.js
module.exports = {
...
module: {
rules: [
{
test: /\.(ts|tsx|js|jsx)$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
],
},
..
};
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
);
});
});
๋ค์์ผ๋ก ์ปค๋ฒํ ํ ์คํธ๋ ์น์ ๋ณ ํตํฉ ํ ์คํธ์ด๋ค. ๋ณธ ํ๋ก์ ํธ์์๋ ๊ฐ๊ฐ์ ๋ ๋ฆฝ์ ์ธ ๊ธฐ๋ฅ์ ๋ด๊ณ ์๋ ์น์ (์๋ ์ด๋ฏธ์ง์ ๋นจ๊ฐ ๋ฐ์ค)์ ํตํฉ ํ ์คํธ ๋จ์๋ก ์ก์๊ณ , ์๋ฒ์์ ๊ฐ์ ธ์จ ๋ฐ์ดํฐ๋ผ ๊ฐ์ ํ ๊ฐ์ง ๋ฐ์ดํฐ๋ค๊ณผ ์ ์ญ ์ํ๋ฅผ ์ฃผ์ ํด ์์กด์ฑ์ ํด๊ฒฐํด ์ฃผ์๋ค.
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 v.18
์ ๋์์ฑ ๊ธฐ๋ฅ์ ๋ด์๋ด๊ธฐ ์ํด 5๋
์ ๊ฐ๊น์ด ์๊ฐ์ ๋ค์ฌ ๊ธฐ์ ์ ์ํคํ
์ฒ๋ถํฐ ๋ฉํ ๋ชจ๋ธ์ ๊ณ ์ํด ๋ง๋ ๋๊ท๋ชจ ์
๋ฐ์ดํธ๋ฅผ ํฌํจํ๊ณ ์๋ค. ์ถฉ๋ถํ
๊ณต๋ถ๊ฐ ํ์ํ๊ณ ๋ฉ์ปค๋์ฆ ๊ตฌํ์ฒด์ ๋ํด ์ด์ฌํ ํบ์๋ณธ ๊ฒฐ๊ณผ๋ฌผ์ ๊ธ๋ก ์ ๋ฆฌํด ๋ณด๊ธฐ๋ ํ๋ค.
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์ ๋ํ ๋ด์ฉ์ ๋ฐ๋ก ์์ธํ ์ ๋ฆฌํ ์์ ์ผ๋ก ๋ณธ ๊ธ์์๋ ์ถ์ฝํด ์ ๋ฆฌํ์ต๋๋ค.
์๋ฐ์คํฌ๋ฆฝํธ๋ก๋ถํฐ ๋ฐ์ํ๋ ์๋ฌ๊ฐ ๋ฆฌ์ํธ ๋ด๋ถ ์ํ๋ค์ ํผ์ํ๊ณ ์ ํ ์ ์ฒด์ ๋๋๋ง ๋ฌธ์ ๋ฅผ ๋ฐ์์์ผฐ์ง๋ง, ๋ฆฌ์ํธ๋ ์ด๋ฅผ ๋์ํ ๋งํ ์ ์ ํ ๋ฐฉ๋ฒ์ ์ ๊ณตํ์ง ์์์๋ค.
๊ทธ๋ฌ๋ค 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
๋ ๋ทฐ๋ฅผ ๊ตฌ์ฑํ๋ ๋ฐ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด ์์ฒญ ์์ฑ๊ณผ ์๋ต์ ๋ฐ์ ์งํ ๋ฐ์ดํฐ ์ฐธ์กฐ ๋ฐ ์์ธ ์ฒ๋ฆฌ๋ฅผ ํด์ฃผ์ด์ผ ํ๋ ๊ฐ๋ฐ์ ์ฑ
์์ ์์ํ ์ ์๊ฒ ํ๋ค.
์ฟผ๋ฆฌ๋ผ๋ ๊ฐ๊ฐ์ ์ธ์คํด์ค๊ฐ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์์์ ์บ์ฑ, ๋ฆฌํจ์นญํด ์์ฒญ ์์ ์ด ๋ฐ์ดํฐ ์ฐธ์กฐ์ ์ง์ ์ด ์๋๋๋ผ๋ ๋ทฐ์์ ํ์๋ก ํ๋ ์ต์ ํ๋ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ ์ ์๋๋ก ๋ณด์ฅํด ์ค๋ค. ๊ดํ์ฒ๋ผ ๋ฆฌ๋์ค ๋ฏธ๋ค์จ์ด์ ๋น๋๊ธฐ ๋ก์ง์ ๋ด์ ์๋ฒ ์ํ ๊ฐ์ ๋งค๋ฒ ์ต์ ํํด ์ฃผ์ด์ผ ํ๋ ์ฑ ์์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์๊ฒ ์์ํ๋ค๋ ๊ฒ์ ์ถฉ๋ถํ ์ข์ ๊ฒฝํ์ด์๋ค.
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
์ 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')],
});