diff --git a/.env b/.env new file mode 100644 index 000000000..c83d04810 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +BUILD_PATH=docs \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..bebdb7ce4 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,59 @@ +module.exports = { + env: { + browser: true, + es2021: true, + node: true, + }, + extends: [ + 'plugin:@typescript-eslint/recommended', + 'airbnb/hooks', + 'airbnb-typescript', + 'prettier', + 'plugin:storybook/recommended', + ], + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 'latest', + sourceType: 'module', + }, + plugins: [ + 'react', + '@typescript-eslint', + 'react-hooks', + 'json-format', + 'simple-import-sort', + '@emotion', + 'prettier', + ], + rules: { + 'react/react-in-jsx-scope': 'off', + 'simple-import-sort/imports': 'error', + 'simple-import-sort/exports': 'error', + '@typescript-eslint/consistent-type-imports': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + }, + ], + 'import/extensions': ['off'], + 'import/no-extraneous-dependencies': ['off'], + 'react/jsx-filename-extension': [ + 'warn', + { + extensions: ['.tsx', '.js', '.jsx'], + }, + ], + '@typescript-eslint/no-use-before-define': ['off'], + }, + ignorePatterns: ['**/build/**/*', '.eslintrc.js', 'craco.config.js'], + settings: { + 'import/resolver': { + typescript: {}, + }, + }, +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..b1ea2e6c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +.yaml +node_modules/.cache/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..81fd20220 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +/.vscode +/node_modules + diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..1b0be5244 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "singleQuote": true, + "semi": true, + "tabWidth": 2, + "trailingComma": "all", + "printWidth": 100, + "arrowParens": "always" +} diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 000000000..1c0360091 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,36 @@ +import type { StorybookConfig } from '@storybook/react-webpack5'; +import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'; +import path from 'path'; + +const config: StorybookConfig = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: [ + '@storybook/addon-links', + '@storybook/addon-essentials', + '@storybook/preset-create-react-app', + '@storybook/addon-onboarding', + '@storybook/addon-interactions', + ], + webpackFinal: async (config) => { + config.resolve?.plugins?.push( + new TsconfigPathsPlugin({ + configFile: path.resolve(__dirname, '../tsconfig.json'), + }), + ); + + return config; + }, + framework: { + name: '@storybook/react-webpack5', + options: { + builder: { + useSWC: true, + }, + }, + }, + docs: { + autodocs: 'tag', + }, + staticDirs: ['../public'], +}; +export default config; diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 000000000..f0b8e7bca --- /dev/null +++ b/.storybook/preview.ts @@ -0,0 +1,16 @@ +import type { Preview } from '@storybook/react'; +import '@/styles'; + +const preview: Preview = { + parameters: { + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, +}; + +export default preview; diff --git a/README.md b/README.md index 3eaeec280..28b8fb21c 100644 --- a/README.md +++ b/README.md @@ -1 +1,96 @@ -# react-deploy \ No newline at end of file +# ๐Ÿš€ 1๋‹จ๊ณ„ - API ๋ช…์„ธ ํ˜‘์˜ & ๋ฐ˜์˜ + +## ๊ธฐ๋Šฅ ์š”๊ตฌ ์‚ฌํ•ญ + +### ์ž‘์„ฑํ•œ API ๋ฌธ์„œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํŒ€ ๋‚ด์—์„œ ์ง€๊ธˆ๊นŒ์ง€ ๋งŒ๋“  API๋ฅผ ๊ฒ€ํ† ํ•˜๊ณ  ํ†ต์ผํ•˜์—ฌ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๋ฐ˜์˜ํ•œ๋‹ค. + +- [] ํŒ€ ๋‚ด์—์„œ ์ผ๊ด€๋œ ๊ธฐ์ค€์„ ์ •ํ•˜์—ฌ API ๋ช…์„ธ๋ฅผ ๊ฒฐ์ •ํ•œ๋‹ค. +- [] ๋•Œ๋กœ๋Š” ํด๋ผ์ด์–ธํŠธ์˜ ํŽธ์˜๋ฅผ ์œ„ํ•ด API ๋ช…์„ธ๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค. +- [] ํŒ€ ๋‚ด์— ๋ฐฐํฌ ๋  API๊ฐ€ ์—ฌ๋Ÿฌ๊ฐœ ์ผ ๊ฒฝ์šฐ ์ƒ๋‹จ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฐ”์—์„œ ์„ ํƒ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•œ๋‹ค. +- [] ํ”„๋ก ํŠธ์—”๋“œ์˜ ๊ฒฝ์šฐ ๋ฐฐํฌ์™€ ์‚ฌ์šฉ์ž๊ฐ€ ํŒ€ ๋‚ด ์—ฌ๋Ÿฌ ์„œ๋ฒ„ ์ค‘ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•˜์—ฌ ์„œ๋น„์Šค๋ฅผ ์ด์šฉ +- [] ํŒ€๋‚ด ๋ฐฑ์—”๋“œ ์—”์ง€๋‹ˆ์–ด์˜ ์ด๋ฆ„์„ ๋„ฃ๊ณ , ์ด๋ฆ„์„ ์„ ํƒํ•˜๋ฉด ํ•ด๋‹น ์—”์ง€๋‹ˆ์–ด์˜ API๋กœ APIํ†ต์‹ ์„ ํ•˜๊ฒŒ ํ•œ๋‹ค. +- [] ๊ธฐ๋ณธ ์„ ํƒ์€ ์ œ์ผ ์ฒซ๋ฒˆ์งธ ์ด๋ฆ„์œผ๋กœ ํ•œ๋‹ค. + +# ๐Ÿš€ 2๋‹จ๊ณ„ - ๋ฐฐํฌํ•˜๊ธฐ + +## ๊ธฐ๋Šฅ ์š”๊ตฌ ์‚ฌํ•ญ + +### ์„ธ๊ฐ€์ง€ ๋ฐฉ๋ฒ• ์ค‘ ๋ณธ์ธ์ด ์›ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ฐฐํฌํ•œ๋‹ค. + +### (๋‹จ, ๊ฐ€๋Šฅํ•˜๋ฉด ์ตœ๋Œ€ํ•œ ๋ฐฉ๋ฒ• 1, 3๋ฒˆ์œผ๋กœ ์ง„ํ–‰ํ•˜๊ณ  CI/CD๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ด์š”) + +#### ๋ฐฉ๋ฒ•1. + +- github action์„ ์‚ฌ์šฉํ•˜์—ฌ ci/cd๋ฅผ ๊ตฌ์„ฑํ•œ๋‹ค. +- cloudflare์˜ pages์— ๋ฐฐํฌํ•œ๋‹ค. + +#### ๋ฐฉ๋ฒ•2. + +- vercel์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐฐํฌํ•œ๋‹ค. + +#### ๋ฐฉ๋ฒ•3. + +- github pages๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐฐํฌํ•œ๋‹ค. +- ์„œ๋ฒ„ API๊ฐ€ ์˜๋„๋Œ€๋กœ ์ž˜ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค๋ฉด ํ•ด๊ฒฐํ•œ๋‹ค. + +# ๐Ÿš€ 3๋‹จ๊ณ„ - ํฌ์ธํŠธ + +## ๊ธฐ๋Šฅ ์š”๊ตฌ ์‚ฌํ•ญ + +### ์ƒํ’ˆ ๊ตฌ๋งค์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ํฌ์ธํŠธ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ๋‹ค. + +- [] ํฌ์ธํŠธ๋Š” ์‚ฌ์šฉ์ž๋ณ„๋กœ ๋ณด์œ ํ•œ๋‹ค. +- [] ํฌ์ธํŠธ ์ฐจ๊ฐ ๋ฐฉ๋ฒ• ๋“ฑ ๋‚˜๋จธ์ง€ ๊ธฐ๋Šฅ์— ๋Œ€ํ•ด์„œ๋Š” ํŒ€๊ณผ ๋…ผ์˜ํ•˜์—ฌ ์ •์ฑ…์„ ๊ฒฐ์ •ํ•˜๊ณ  ๊ตฌํ˜„ํ•œ๋‹ค. + - e.g. + - 5๋งŒ ์› ์ด์ƒ ์ฃผ๋ฌธ ์‹œ ์ด ๊ธˆ์•ก์˜ 10%๊ฐ€ ํ• ์ธ๋œ๋‹ค. + - ํ˜„๊ธˆ ์˜์ˆ˜์ฆ์„ ๋ฐ›์œผ๋ ค๋ฉด ํœด๋Œ€์ „ํ™” ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์•ผ ํ•œ๋‹ค. +- [] API ๋ช…์„ธ๋Š” ํŒ€๊ณผ ํ˜‘์˜ํ•˜์—ฌ ๊ฒฐ์ •ํ•˜๊ณ  ๊ตฌํ˜„ํ•œ๋‹ค. + +# ๐Ÿš€ 4๋‹จ๊ณ„ - ์งˆ๋ฌธ์˜ ๋‹ต๋ณ€์„ README์— ์ž‘์„ฑ + +## ์•„๋ž˜ ์งˆ๋ฌธ์— ๋Œ€ํ•œ ๋‹ต๋ณ€์„ README์— ์ถ”๊ฐ€ํ•˜์—ฌ ๊ณผ์ œ ์ œ์ถœ์„ ํ•ด์š”. + +## ๐Ÿ“ Requirements + +### 6์ฃผ์ฐจ ์งˆ๋ฌธ + +### ์งˆ๋ฌธ 1. SPA ํŽ˜์ด์ง€๋ฅผ ์ •์  ๋ฐฐํฌ๋ฅผ ํ•˜๋ ค๊ณ  ํ•  ๋•Œ Vercel์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ํ•œ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•  ์ˆ˜ ์žˆ์„๊นŒ์š”? + +1. GitHub Pages: + โ—ฆ GitHub Pages๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ •์  ์‚ฌ์ดํŠธ๋ฅผ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ๋‹ค. ํ”„๋กœ์ ํŠธ์˜ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ GitHub์— ์˜ฌ๋ฆฌ๊ณ , ํ•ด๋‹น ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์˜ ์„ค์ •์—์„œ GitHub Pages๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋ฉด ๋œ๋‹ค. +2. Netlify: + โ—ฆ Netlify๋Š” ์ •์  ์‚ฌ์ดํŠธ ๋ฐฐํฌ๋ฅผ ์‰ฝ๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋Š” ํ”Œ๋žซํผ์ด๋‹ค. Git ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์™€ ์—ฐ๋™ํ•˜์—ฌ ์ž๋™ ๋ฐฐํฌ๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋‹ค์–‘ํ•œ ๋นŒ๋“œ ์˜ต์…˜์„ ์ œ๊ณตํ•œ๋‹ค. +3. AWS S3: + โ—ฆ AWS S3๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ •์  ์›น์‚ฌ์ดํŠธ ํ˜ธ์ŠคํŒ…์„ ํ•  ์ˆ˜ ์žˆ๋‹ค. S3 ๋ฒ„ํ‚ท์„ ์ƒ์„ฑํ•˜๊ณ , ์ •์  ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•œ ํ›„, ๋ฒ„ํ‚ท์˜ ํผ๋ธ”๋ฆญ ์ ‘๊ทผ ๊ถŒํ•œ์„ ์„ค์ •ํ•˜๋ฉด ๋œ๋‹ค. CloudFront๋ฅผ ์ด์šฉํ•˜์—ฌ CDN ์„ค์ •๋„ ๊ฐ€๋Šฅํ•˜๋‹ค. +4. Firebase Hosting: + โ—ฆ Firebase Hosting์€ ๋น ๋ฅด๊ณ  ์•ˆ์ „ํ•œ ์ •์  ํ˜ธ์ŠคํŒ…์„ ์ œ๊ณตํ•œ๋‹ค. Firebase CLI๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, SSL ์ธ์ฆ์„œ์™€ CDN๋„ ์ž๋™์œผ๋กœ ์„ค์ •๋œ๋‹ค. +5. Surge: + โ—ฆ Surge๋Š” ๊ฐ„๋‹จํ•œ CLI ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด ์ •์  ์‚ฌ์ดํŠธ๋ฅผ ์‰ฝ๊ฒŒ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ๋Š” ์„œ๋น„์Šค์ด๋‹ค. ํ”„๋กœ์ ํŠธ ๋””๋ ‰ํ† ๋ฆฌ์—์„œ surge ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ๋œ๋‹ค. +6. DigitalOcean: + โ—ฆ DigitalOcean์˜ Droplet์„ ์‚ฌ์šฉํ•˜์—ฌ ์›น ์„œ๋ฒ„๋ฅผ ์„ค์ •ํ•˜๊ณ , Nginx๋‚˜ Apache์™€ ๊ฐ™์€ ์›น ์„œ๋ฒ„๋ฅผ ํ†ตํ•ด ์ •์  ํŒŒ์ผ์„ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ๋‹ค. ์„œ๋ฒ„ ์„ค์ •๊ณผ ๊ด€๋ฆฌ๋ฅผ ์ง์ ‘ ํ•ด์•ผ ํ•œ๋‹ค๋Š” ์ ์—์„œ ๋‹ค์†Œ ๋ณต์žกํ•  ์ˆ˜ ์žˆ๋‹ค. + +### ์งˆ๋ฌธ 2. CSRF๋‚˜ XSS ๊ณต๊ฒฉ์„ ๋ง‰๋Š” ๋ฐฉ๋ฒ•์€ ๋ฌด์—‡์ผ๊นŒ์š”? + +1. CSRF (Cross-Site Request Forgery) ๋ฐฉ์ง€ ๋ฐฉ๋ฒ•: + โ—ฆ CSRF ํ† ํฐ ์‚ฌ์šฉ: ์„œ๋ฒ„๊ฐ€ ๊ฐ ์š”์ฒญ์— ๋Œ€ํ•ด ๊ณ ์œ ํ•œ CSRF ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•˜๊ณ , ์ด๋ฅผ ํผ์ด๋‚˜ AJAX ์š”์ฒญ์— ํฌํ•จ์‹œํ‚จ๋‹ค. ์„œ๋ฒ„๋Š” ์š”์ฒญ์ด ๋“ค์–ด์˜ฌ ๋•Œ ์ด ํ† ํฐ์„ ๊ฒ€์ฆํ•œ๋‹ค. + โ—ฆ SameSite ์ฟ ํ‚ค ์†์„ฑ ์„ค์ •: ์ฟ ํ‚ค์˜ SameSite ์†์„ฑ์„ 'Strict' ๋˜๋Š” 'Lax'๋กœ ์„ค์ •ํ•˜์—ฌ, ๋™์ผ ์‚ฌ์ดํŠธ ๋‚ด์—์„œ๋งŒ ์ฟ ํ‚ค๊ฐ€ ์ „์†ก๋˜๋„๋ก ํ•œ๋‹ค. + โ—ฆ CORS ์„ค์ •: CORS(Cross-Origin Resource Sharing) ์„ค์ •์„ ํ†ตํ•ด ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ๋„๋ฉ”์ธ์—์„œ๋งŒ ์š”์ฒญ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค. +2. XSS (Cross-Site Scripting) ๋ฐฉ์ง€ ๋ฐฉ๋ฒ•: + โ—ฆ ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ ๋ฐ ์ธ์ฝ”๋”ฉ: ์‚ฌ์šฉ์ž๋กœ๋ถ€ํ„ฐ ์ž…๋ ฅ๋ฐ›์€ ๋ฐ์ดํ„ฐ๋Š” ๋ฐ˜๋“œ์‹œ ๊ฒ€์ฆํ•˜๊ณ , ์ถœ๋ ฅ ์‹œ์—๋Š” HTML ์—”ํ‹ฐํ‹ฐ๋กœ ์ธ์ฝ”๋”ฉํ•˜์—ฌ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š๋„๋ก ํ•œ๋‹ค. + โ—ฆ CSP (Content Security Policy) ์„ค์ •: CSP ํ—ค๋”๋ฅผ ์„ค์ •ํ•˜์—ฌ, ์Šคํฌ๋ฆฝํŠธ ์†Œ์Šค๋ฅผ ์ œํ•œํ•จ์œผ๋กœ์จ ์•…์„ฑ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š๋„๋ก ํ•œ๋‹ค. + โ—ฆ ์ถœ๋ ฅ ์‹œ ์•ˆ์ „ํ•œ ๋ฉ”์„œ๋“œ ์‚ฌ์šฉ: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋‚˜ ์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ HTML์— ์ถœ๋ ฅํ•  ๋•Œ๋Š” ์•ˆ์ „ํ•œ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ XSS ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•œ๋‹ค. + +### ์งˆ๋ฌธ 3. ๋ธŒ๋ผ์šฐ์ € ๋ Œ๋”๋ง ์›๋ฆฌ์—๋Œ€ํ•ด ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”. + +1. DOM (Document Object Model) ์ƒ์„ฑ: + โ—ฆ HTML ๋ฌธ์„œ๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ DOM ํŠธ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. DOM ํŠธ๋ฆฌ๋Š” ๋ฌธ์„œ์˜ ๊ตฌ์กฐ๋ฅผ ๊ณ„์ธต์ ์œผ๋กœ ํ‘œํ˜„ํ•œ ํŠธ๋ฆฌ ๊ตฌ์กฐ์ด๋‹ค. +2. CSSOM (CSS Object Model) ์ƒ์„ฑ: + โ—ฆ CSS ํŒŒ์ผ์„ ํŒŒ์‹ฑํ•˜์—ฌ CSSOM ํŠธ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. CSSOM ํŠธ๋ฆฌ๋Š” ์Šคํƒ€์ผ ์ •๋ณด๋ฅผ ๊ณ„์ธต์ ์œผ๋กœ ํ‘œํ˜„ํ•œ ํŠธ๋ฆฌ ๊ตฌ์กฐ์ด๋‹ค. +3. ๋ Œ๋” ํŠธ๋ฆฌ ์ƒ์„ฑ: + โ—ฆ DOM ํŠธ๋ฆฌ์™€ CSSOM ํŠธ๋ฆฌ๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ๋ Œ๋” ํŠธ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ๋ Œ๋” ํŠธ๋ฆฌ๋Š” ์‹ค์ œ๋กœ ํ™”๋ฉด์— ๊ทธ๋ ค์งˆ ๋…ธ๋“œ๋งŒ ํฌํ•จํ•œ๋‹ค. +4. ๋ ˆ์ด์•„์›ƒ: + โ—ฆ ๋ Œ๋” ํŠธ๋ฆฌ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฐ ๋…ธ๋“œ์˜ ํฌ๊ธฐ์™€ ์œ„์น˜๋ฅผ ๊ณ„์‚ฐํ•œ๋‹ค. ์ด๋ฅผ ๋ ˆ์ด์•„์›ƒ ๋˜๋Š” ๋ฆฌํ”Œ๋กœ์šฐ๋ผ๊ณ  ํ•œ๋‹ค. +5. ํŽ˜์ธํŒ…: + โ—ฆ ๊ณ„์‚ฐ๋œ ๋ ˆ์ด์•„์›ƒ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๊ฐ ๋…ธ๋“œ๋ฅผ ํ™”๋ฉด์— ํ”ฝ์…€ ๋‹จ์œ„๋กœ ๊ทธ๋ฆฐ๋‹ค. ์ด ๊ณผ์ •์„ ํŽ˜์ธํŒ… ๋˜๋Š” ๋ž˜์Šคํ„ฐ๋ผ์ด์ œ์ด์…˜์ด๋ผ๊ณ  ํ•œ๋‹ค. +6. ์ปดํฌ์ง€ํŒ…: + โ—ฆ ํŽ˜์ธํŒ…๋œ ๋ ˆ์ด์–ด๋“ค์„ ์กฐํ•ฉํ•˜์—ฌ ์ตœ์ข…์ ์œผ๋กœ ํ™”๋ฉด์— ์ถœ๋ ฅํ•œ๋‹ค. ์ด ๊ณผ์ •์—์„œ๋Š” GPU๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค. diff --git a/craco.config.js b/craco.config.js new file mode 100644 index 000000000..bba205376 --- /dev/null +++ b/craco.config.js @@ -0,0 +1,13 @@ +const CracoAlias = require('craco-alias'); + +module.exports = { + plugins: [ + { + plugin: CracoAlias, + options: { + source: 'tsconfig', + tsConfigPath: 'tsconfig.paths.json', + }, + }, + ], +}; diff --git a/docs/asset-manifest.json b/docs/asset-manifest.json new file mode 100644 index 000000000..a20de68a0 --- /dev/null +++ b/docs/asset-manifest.json @@ -0,0 +1,14 @@ +{ + "files": { + "main.css": "/react-deploy/static/css/main.689b7bff.css", + "main.js": "/react-deploy/static/js/main.bd5a99db.js", + "static/media/kakao_logo.svg": "/react-deploy/static/media/kakao_logo.976fd2de0dc47a40dc4cad638b9b69e1.svg", + "index.html": "/react-deploy/index.html", + "main.689b7bff.css.map": "/react-deploy/static/css/main.689b7bff.css.map", + "main.bd5a99db.js.map": "/react-deploy/static/js/main.bd5a99db.js.map" + }, + "entrypoints": [ + "static/css/main.689b7bff.css", + "static/js/main.bd5a99db.js" + ] +} \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 000000000..a4d5bb338 --- /dev/null +++ b/docs/index.html @@ -0,0 +1 @@ +Kakao Tech
\ No newline at end of file diff --git a/docs/mockServiceWorker.js b/docs/mockServiceWorker.js new file mode 100644 index 000000000..c88cfe025 --- /dev/null +++ b/docs/mockServiceWorker.js @@ -0,0 +1,303 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker (1.3.3). + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70' +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: INTEGRITY_CHECKSUM, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: true, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + const accept = request.headers.get('accept') || '' + + // Bypass server-sent events. + if (accept.includes('text/event-stream')) { + return + } + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = Math.random().toString(16).slice(2) + + event.respondWith( + handleRequest(event, requestId).catch((error) => { + if (error.name === 'NetworkError') { + console.warn( + '[MSW] Successfully emulated a network error for the "%s %s" request.', + request.method, + request.url, + ) + return + } + + // At this point, any exception indicates an issue with the original request/response. + console.error( + `\ +[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, + request.method, + request.url, + `${error.name}: ${error.message}`, + ) + }), + ) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const clonedResponse = response.clone() + sendToClient(client, { + type: 'RESPONSE', + payload: { + requestId, + type: clonedResponse.type, + ok: clonedResponse.ok, + status: clonedResponse.status, + statusText: clonedResponse.statusText, + body: + clonedResponse.body === null ? null : await clonedResponse.text(), + headers: Object.fromEntries(clonedResponse.headers.entries()), + redirected: clonedResponse.redirected, + }, + }) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + const clonedRequest = request.clone() + + function passthrough() { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const headers = Object.fromEntries(clonedRequest.headers.entries()) + + // Remove MSW-specific request headers so the bypassed requests + // comply with the server's CORS preflight check. + // Operate with the headers as an object because request "Headers" + // are immutable. + delete headers['x-msw-bypass'] + + return fetch(clonedRequest, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Bypass requests with the explicit bypass header. + // Such requests can be issued by "ctx.fetch()". + if (request.headers.get('x-msw-bypass') === 'true') { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const clientMessage = await sendToClient(client, { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + mode: request.mode, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.text(), + bodyUsed: request.bodyUsed, + keepalive: request.keepalive, + }, + }) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'MOCK_NOT_FOUND': { + return passthrough() + } + + case 'NETWORK_ERROR': { + const { name, message } = clientMessage.data + const networkError = new Error(message) + networkError.name = name + + // Rejecting a "respondWith" promise emulates a network error. + throw networkError + } + } + + return passthrough() +} + +function sendToClient(client, message) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [channel.port2]) + }) +} + +function sleep(timeMs) { + return new Promise((resolve) => { + setTimeout(resolve, timeMs) + }) +} + +async function respondWithMock(response) { + await sleep(response.delay) + return new Response(response.body, response) +} diff --git a/docs/robots.txt b/docs/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/docs/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/docs/static/css/main.689b7bff.css b/docs/static/css/main.689b7bff.css new file mode 100644 index 000000000..7b8a07241 --- /dev/null +++ b/docs/static/css/main.689b7bff.css @@ -0,0 +1,2 @@ +html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}*{box-sizing:border-box}abbr,address,blockquote,body,button,caption,code,dd,dl,dt,fieldset,figcaption,figure,form,h1,h2,h3,h4,h5,h6,hgroup,input,legend,li,ol,p,pre,th,ul{font-style:normal;font-weight:400;margin:0;padding:0}fieldset,iframe{border:0}caption,th{text-align:left}table{border-collapse:collapse;border-spacing:0}details,main,summary{display:block}audio,canvas,progress,video{vertical-align:initial}button{background:none;border:0;box-sizing:initial;color:inherit;cursor:pointer;font:inherit;line-height:inherit;overflow:visible;vertical-align:inherit}button:disabled{cursor:default}:focus{outline:4px solid #007dfa99;outline-offset:1px}:focus[data-focus-method=mouse]:not(input):not(textarea):not(select),:focus[data-focus-method=touch]:not(input):not(textarea):not(select){outline:none}::-moz-focus-inner{border:0;padding:0}a{text-decoration:none}a:focus{outline:none} +/*# sourceMappingURL=main.689b7bff.css.map*/ \ No newline at end of file diff --git a/docs/static/css/main.689b7bff.css.map b/docs/static/css/main.689b7bff.css.map new file mode 100644 index 000000000..7ad81a15a --- /dev/null +++ b/docs/static/css/main.689b7bff.css.map @@ -0,0 +1 @@ +{"version":3,"file":"static/css/main.689b7bff.css","mappings":"AAAA,KACE,yBAA0B,CAC1B,6BACF,CACA,EACE,qBACF,CACA,kJAiCE,iBAAkB,CADlB,eAAgB,CAFhB,QAAS,CACT,SAGF,CAEA,gBAEE,QACF,CACA,WAEE,eACF,CACA,MACE,wBAAyB,CACzB,gBACF,CACA,qBAGE,aACF,CACA,4BAIE,sBACF,CACA,OACE,eAAgB,CAChB,QAAS,CACT,kBAAmB,CACnB,aAAc,CACd,cAAe,CACf,YAAa,CACb,mBAAoB,CACpB,gBAAiB,CACjB,sBACF,CACA,gBACE,cACF,CACA,OACE,2BAAyC,CACzC,kBACF,CACA,0IAEE,YACF,CACA,mBACE,QAAS,CACT,SACF,CAEA,EACE,oBACF,CACA,QACE,YACF","sources":["styles/reset.css"],"sourcesContent":["html {\r\n -ms-text-size-adjust: 100%;\r\n -webkit-text-size-adjust: 100%;\r\n}\r\n* {\r\n box-sizing: border-box;\r\n}\r\nabbr,\r\nblockquote,\r\nbody,\r\nbutton,\r\ndd,\r\ndl,\r\ndt,\r\nfieldset,\r\nfigure,\r\nform,\r\nh1,\r\nh2,\r\nh3,\r\nh4,\r\nh5,\r\nh6,\r\nhgroup,\r\ninput,\r\nlegend,\r\nli,\r\nol,\r\np,\r\npre,\r\naddress,\r\ncaption,\r\ncode,\r\nfigcaption,\r\npre,\r\nth,\r\nul {\r\n margin: 0;\r\n padding: 0;\r\n font-weight: 400;\r\n font-style: normal;\r\n}\r\n\r\nfieldset,\r\niframe {\r\n border: 0;\r\n}\r\ncaption,\r\nth {\r\n text-align: left;\r\n}\r\ntable {\r\n border-collapse: collapse;\r\n border-spacing: 0;\r\n}\r\ndetails,\r\nmain,\r\nsummary {\r\n display: block;\r\n}\r\naudio,\r\ncanvas,\r\nprogress,\r\nvideo {\r\n vertical-align: initial;\r\n}\r\nbutton {\r\n background: none;\r\n border: 0;\r\n box-sizing: initial;\r\n color: inherit;\r\n cursor: pointer;\r\n font: inherit;\r\n line-height: inherit;\r\n overflow: visible;\r\n vertical-align: inherit;\r\n}\r\nbutton:disabled {\r\n cursor: default;\r\n}\r\n:focus {\r\n outline: 4px solid rgba(0, 125, 250, 0.6);\r\n outline-offset: 1px;\r\n}\r\n:focus[data-focus-method='mouse']:not(input):not(textarea):not(select),\r\n:focus[data-focus-method='touch']:not(input):not(textarea):not(select) {\r\n outline: none;\r\n}\r\n::-moz-focus-inner {\r\n border: 0;\r\n padding: 0;\r\n}\r\n\r\na {\r\n text-decoration: none;\r\n}\r\na:focus {\r\n outline: none;\r\n}\r\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/docs/static/js/main.bd5a99db.js b/docs/static/js/main.bd5a99db.js new file mode 100644 index 000000000..e613934b5 --- /dev/null +++ b/docs/static/js/main.bd5a99db.js @@ -0,0 +1,3 @@ +/*! For license information please see main.bd5a99db.js.LICENSE.txt */ +(()=>{var e={315:(e,t,n)=>{"use strict";var r=n(974),o={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},i={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},a={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},s={};function l(e){return r.isMemo(e)?a:s[e.$$typeof]||o}s[r.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},s[r.Memo]=a;var c=Object.defineProperty,u=Object.getOwnPropertyNames,d=Object.getOwnPropertySymbols,h=Object.getOwnPropertyDescriptor,f=Object.getPrototypeOf,p=Object.prototype;e.exports=function e(t,n,r){if("string"!==typeof n){if(p){var o=f(n);o&&o!==p&&e(t,o,r)}var a=u(n);d&&(a=a.concat(d(n)));for(var s=l(t),m=l(n),g=0;g{e=n.nmd(e);var r="__lodash_hash_undefined__",o=9007199254740991,i="[object Arguments]",a="[object AsyncFunction]",s="[object Function]",l="[object GeneratorFunction]",c="[object Null]",u="[object Object]",d="[object Proxy]",h="[object Undefined]",f=/^\[object .+?Constructor\]$/,p=/^(?:0|[1-9]\d*)$/,m={};m["[object Float32Array]"]=m["[object Float64Array]"]=m["[object Int8Array]"]=m["[object Int16Array]"]=m["[object Int32Array]"]=m["[object Uint8Array]"]=m["[object Uint8ClampedArray]"]=m["[object Uint16Array]"]=m["[object Uint32Array]"]=!0,m[i]=m["[object Array]"]=m["[object ArrayBuffer]"]=m["[object Boolean]"]=m["[object DataView]"]=m["[object Date]"]=m["[object Error]"]=m[s]=m["[object Map]"]=m["[object Number]"]=m[u]=m["[object RegExp]"]=m["[object Set]"]=m["[object String]"]=m["[object WeakMap]"]=!1;var g="object"==typeof n.g&&n.g&&n.g.Object===Object&&n.g,v="object"==typeof self&&self&&self.Object===Object&&self,y=g||v||Function("return this")(),b=t&&!t.nodeType&&t,x=b&&e&&!e.nodeType&&e,w=x&&x.exports===b,S=w&&g.process,k=function(){try{var e=x&&x.require&&x.require("util").types;return e||S&&S.binding&&S.binding("util")}catch(t){}}(),C=k&&k.isTypedArray;var E,_,P=Array.prototype,T=Function.prototype,j=Object.prototype,R=y["__core-js_shared__"],A=T.toString,O=j.hasOwnProperty,D=function(){var e=/[^.]+$/.exec(R&&R.keys&&R.keys.IE_PROTO||"");return e?"Symbol(src)_1."+e:""}(),M=j.toString,z=A.call(Object),F=RegExp("^"+A.call(O).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),L=w?y.Buffer:void 0,B=y.Symbol,I=y.Uint8Array,N=L?L.allocUnsafe:void 0,V=(E=Object.getPrototypeOf,_=Object,function(e){return E(_(e))}),U=Object.create,W=j.propertyIsEnumerable,H=P.splice,q=B?B.toStringTag:void 0,$=function(){try{var e=ye(Object,"defineProperty");return e({},"",{}),e}catch(t){}}(),K=L?L.isBuffer:void 0,Q=Math.max,G=Date.now,Y=ye(y,"Map"),X=ye(Object,"create"),J=function(){function e(){}return function(t){if(!Re(t))return{};if(U)return U(t);e.prototype=t;var n=new e;return e.prototype=void 0,n}}();function Z(e){var t=-1,n=null==e?0:e.length;for(this.clear();++t-1},ee.prototype.set=function(e,t){var n=this.__data__,r=ae(n,e);return r<0?(++this.size,n.push([e,t])):n[r][1]=t,this},te.prototype.clear=function(){this.size=0,this.__data__={hash:new Z,map:new(Y||ee),string:new Z}},te.prototype.delete=function(e){var t=ve(this,e).delete(e);return this.size-=t?1:0,t},te.prototype.get=function(e){return ve(this,e).get(e)},te.prototype.has=function(e){return ve(this,e).has(e)},te.prototype.set=function(e,t){var n=ve(this,e),r=n.size;return n.set(e,t),this.size+=n.size==r?0:1,this},ne.prototype.clear=function(){this.__data__=new ee,this.size=0},ne.prototype.delete=function(e){var t=this.__data__,n=t.delete(e);return this.size=t.size,n},ne.prototype.get=function(e){return this.__data__.get(e)},ne.prototype.has=function(e){return this.__data__.has(e)},ne.prototype.set=function(e,t){var n=this.__data__;if(n instanceof ee){var r=n.__data__;if(!Y||r.length<199)return r.push([e,t]),this.size=++n.size,this;n=this.__data__=new te(r)}return n.set(e,t),this.size=n.size,this};var le,ce=function(e,t,n){for(var r=-1,o=Object(e),i=n(e),a=i.length;a--;){var s=i[le?a:++r];if(!1===t(o[s],s,o))break}return e};function ue(e){return null==e?void 0===e?h:c:q&&q in Object(e)?function(e){var t=O.call(e,q),n=e[q];try{e[q]=void 0;var r=!0}catch(i){}var o=M.call(e);r&&(t?e[q]=n:delete e[q]);return o}(e):function(e){return M.call(e)}(e)}function de(e){return Ae(e)&&ue(e)==i}function he(e){return!(!Re(e)||function(e){return!!D&&D in e}(e))&&(Te(e)?F:f).test(function(e){if(null!=e){try{return A.call(e)}catch(t){}try{return e+""}catch(t){}}return""}(e))}function fe(e){if(!Re(e))return function(e){var t=[];if(null!=e)for(var n in Object(e))t.push(n);return t}(e);var t=xe(e),n=[];for(var r in e)("constructor"!=r||!t&&O.call(e,r))&&n.push(r);return n}function pe(e,t,n,r,o){e!==t&&ce(t,(function(i,a){if(o||(o=new ne),Re(i))!function(e,t,n,r,o,i,a){var s=we(e,n),l=we(t,n),c=a.get(l);if(c)return void oe(e,n,c);var d=i?i(s,l,n+"",e,t,a):void 0,h=void 0===d;if(h){var f=Ee(l),p=!f&&Pe(l),m=!f&&!p&&Oe(l);d=l,f||p||m?Ee(s)?d=s:Ae(g=s)&&_e(g)?d=function(e,t){var n=-1,r=e.length;t||(t=Array(r));for(;++n-1&&e%1==0&&e0){if(++t>=800)return arguments[0]}else t=0;return e.apply(void 0,arguments)}}(ge);function ke(e,t){return e===t||e!==e&&t!==t}var Ce=de(function(){return arguments}())?de:function(e){return Ae(e)&&O.call(e,"callee")&&!W.call(e,"callee")},Ee=Array.isArray;function _e(e){return null!=e&&je(e.length)&&!Te(e)}var Pe=K||function(){return!1};function Te(e){if(!Re(e))return!1;var t=ue(e);return t==s||t==l||t==a||t==d}function je(e){return"number"==typeof e&&e>-1&&e%1==0&&e<=o}function Re(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}function Ae(e){return null!=e&&"object"==typeof e}var Oe=C?function(e){return function(t){return e(t)}}(C):function(e){return Ae(e)&&je(e.length)&&!!m[ue(e)]};function De(e){return _e(e)?re(e,!0):fe(e)}var Me,ze=(Me=function(e,t,n,r){pe(e,t,n,r)},me((function(e,t){var n=-1,r=t.length,o=r>1?t[r-1]:void 0,i=r>2?t[2]:void 0;for(o=Me.length>3&&"function"==typeof o?(r--,o):void 0,i&&function(e,t,n){if(!Re(n))return!1;var r=typeof t;return!!("number"==r?_e(n)&&be(t,n.length):"string"==r&&t in n)&&ke(n[t],e)}(t[0],t[1],i)&&(o=r<3?void 0:o,r=1),e=Object(e);++n{"use strict";var r=n(483),o=n(557);function i(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n