๋ณธ๋ฌธ์œผ๋กœ ๋ฐ”๋กœ๊ฐ€๊ธฐ

๐Ÿšจ next 9.4 ์ด์ƒ๋ถ€ํ„ฐ๋Š” ์ƒˆ๋กญ๊ฒŒ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ๋‚˜์™”์Šต๋‹ˆ๋‹ค.

nextjs.org/docs/basic-features/environment-variables

 

dotenv ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•  ํ•„์š” ์—†์ด

.env.local ํŒŒ์ผ์„ ์ตœ์ƒ๋‹จ์— ์ƒ์„ฑํ•œ ํ›„ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์ž‘์„ฑํ•˜์‹œ๊ณ , process.env.[์ด๋ฆ„] ๊ผด๋กœ ์‚ฌ์šฉํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

 

 

๐Ÿšจ custom server๋ฅผ ๋งŒ๋“ค์–ด๋ณด๋ ค๋ฉด ํ”„๋ก ํŠธ ์ตœ์ƒ์œ„ ํŒŒ์ผ์— server.js๋ฅผ ๋งŒ๋“ค๊ณ  ์•„๋ž˜์˜ ์ง€์‹œ๋ฅผ ๋”ฐ๋ฅด๋ฉด๋ฉ๋‹ˆ๋‹ค.

์ด ๋ถ€๋ถ„์€ ์ „์— ๋‹ค๋ฅธ ํฌ์ŠคํŠธ์—์„œ๋„ ๋‹ค๋ฃฌ ๋ฐ” ์žˆ์œผ๋‹ˆ ์ฐธ๊ณ ํ•ฉ์‹œ๋‹ค!

 

nextjs.org/docs/advanced-features/custom-server

darrengwon.tistory.com/539

 

 


 

 

 

1. next.config.js ๊ตฌ์„ฑ๊ณผ bundle-analyzer๋ฅผ ํ†ตํ•œ ๋ฒˆ๋“ค ํฌ๊ธฐ ํ™•์ธ + tree shaking + gzip ์••์ถ•

 

nextjs.org/docs/api-reference/next.config.js/introduction

๊ณต์‹ ๋ฌธ์„œ์—์„œ ์‚ดํŽด๋ณด์‹œ๋ฉด ์•„์‹œ๊ฒ ์ง€๋งŒ ๋‚ด์šฉ์ด ๋งŽ์Šต๋‹ˆ๋‹ค.

Environment Variables, Base Path, Rewirtes, Redeirects, Custom Headers ๋“ฑ๋“ฑ.

 

์ด ๋ถ€๋ถ„์€ ๊ฐ€์ด๋“œ๋ณด๋‹ค๋Š” ์ง์ ‘ ๊ณต์‹ ๋ฌธ์„œ๊ณผ ์›นํŒฉ ๋ฌธ์„œ๋ฅผ ๋ณด๊ณ  ์„œ๋น„์Šค๋งˆ๋‹ค ํ•„์š”ํ•œ ๋ถ€๋ถ„์„ ๊ต์ฒดํ•˜๊ฑฐ๋‚˜ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ž‘์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

์ปค์Šคํ…€ ์›นํŒฉ ์„ธํŒ… ๋ถ€๋ถ„์€ ์ด ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•ฉ์‹œ๋‹ค.

nextjs.org/docs/api-reference/next.config.js/custom-webpack-config

 

์—ฌ๊ธฐ์— ์ €๋Š” bundle-analyzer๋ฅผ ๊ฐ์‹ธ์„œ ์‚ฌ์šฉํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

https://www.npmjs.com/package/@next/bundle-analyzer

 

@next/bundle-analyzer

Use `webpack-bundle-analyzer` in your Next.js project

www.npmjs.com

 

const withBundleAnalyzer = require("@next/bundle-analyzer")({
  enabled: process.env.ANALYZE === "true",
});

module.exports = withBundleAnalyzer({
  compress: true,
  webpack(config) {
    console.log(config);
    let prod = process.env.NODE_ENV === "production";
    return {
      ...config,
      mode: prod ? "production" : "development",
      devtool: prod ? "hidden-source-map" : "eval",
    };
  },
});

 

npm run build๋ฅผ ํ†ตํ•ด์„œ ๋นŒ๋“œ๋ฅผ ํ•˜๊ณ ๋‚˜๋ฉด ์ž๋™์œผ๋กœ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ค์‹œ๋‹ค.

๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฌผ์˜ analyze ์ชฝ์—์„œ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

parsed size๋ฅผ ๋ณด๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๊ฐ ๋ฒˆ๋“ค(ํŒŒ์ผ)๋‹น 500kb ์ดํ•˜๊ฐ€ ๋˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ์ธํ„ฐ๋„ท ์†๋„๊ฐ€ ๋น ๋ฅธ ๊ณณ์—์„œ๋Š” 1mb ์ •๋„๋„ ๊ดœ์ฐฎ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์ € ์œ„์˜ ๊ฒฐ๊ณผ๋ฌผ์—์„œ๋Š” 500kb ์ด์ƒ ๋˜๋Š” ๊ฒƒ์ด ์—†์ง€๋งŒ moment๊ฐ€ ์›Œ๋‚™ ์ปค๋ณด์ด๊ธฐ ๋•Œ๋ฌธ์— ์ค„์—ฌ๋ณด๋„๋กํ•˜๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ณผ์ •์„ tree shaking์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

 

478.59KB

 

๋งŒ์•ฝ ํฐ ํŒŒ์ผ์˜ ๊ฒฝ์šฐ์—๋Š” tree shaking์„ ํ‚ค์›Œ๋“œ๋กœ ๊ฒ€์ƒ‰ํ•ด๋ณด์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.์‹ค์ œ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋ชจ๋“ˆ๋งŒ ๋กœ๋”ฉํ•˜๋Š” ๊ฒƒ์„ ๋งํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด antd ์•„์ด์ฝ˜ ๋ฒˆ๋“ค์ด ๋„ˆ๋ฌด ํฌ๊ธฐ๊ฐ€ ํฌ๋ฏ€๋กœ and icon tree shaking ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์•„๋ณด์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ tree shaking ์ž์ฒด๊ฐ€ ์•ˆ๋˜๋Š” ํŒจํ‚ค์ง€๋„ ๋งŽ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ๊ฐ์•ˆํ•˜๊ณ  ์‚ฌ์šฉํ•˜์‹œ๋˜๊ฐ€, ์•„๋‹ˆ๋ฉด ์•„์˜ˆ ๋“ค์–ด๋‚ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

moment.js ๊ด€๋ จ tree shaking์€ ๊ฒ€์ƒ‰ํ•˜๋‹ˆ๊นŒ ๋ฐ”๋กœ ๋‚˜์˜ค๋„ค์š”. ์šฐ๋ฆฌ๋Š” ContextReplacementPlugin๋ฅผ ์‚ฌ์šฉํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

https://github.com/jmblog/how-to-optimize-momentjs-with-webpack#bonus

https://gist.github.com/iamakulov/59d88d00404259abb83daaf51b70cb07

 

Webpack’s ContextReplacementPlugin examples

Webpack’s ContextReplacementPlugin examples. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

 

์šฐ์„  ์‚ฌ์šฉํ•  locale์„ ์ง€์ •ํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋Š” ์–ธ์–ดํŒฉ๋งŒ ์ง€์ •ํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋Š” ์–‘์„ ์ค„์ด๊ณ 

import moment from "moment";
moment.locale("ko");

์›นํŒฉ์—๋„ ContextReplacementPlugin ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ํ†ตํ•ด ํ•„์š”ํ•œ ๋ถ€๋ถ„๋งŒ ์ง€์ •ํ•ฉ์‹œ๋‹ค.

const withBundleAnalyzer = require("@next/bundle-analyzer")({
  enabled: process.env.ANALYZE === "true",
});
const webpack = require("webpack");

module.exports = withBundleAnalyzer({
  compress: true,
  webpack(config) {
    console.log(config);
    let prod = process.env.NODE_ENV === "production";
    return {
      ...config,
      mode: prod ? "production" : "development",
      devtool: prod ? "hidden-source-map" : "eval",
      plugins: [
        ...config.plugins,
        new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /^\.\/ko$/),
      ],
    };
  },
});

 

์ค„์ธ ๊ฒฐ๊ณผ 57.7KB๋กœ ์ค„์–ด๋“  ๊ฒƒ์„ ๋ณด์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์™ธ์— ๋‹ค๋ฅธ ํŒจํ‚ค์ง€์˜ ์šฉ๋Ÿ‰์ด ๋„ˆ๋ฌด ํฌ๋‹ค๋ฉด tree shaking ํ•ด์ฃผ์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

 

tree-shaking์— ๋Œ€ํ•œ ์ข‹์€ ๊ธ€์„ ์ฐพ์•„ ์—ฌ๊ธฐ์— ๊ณต์œ ํ•ด๋ด…๋‹ˆ๋‹ค.

 

blog.jungbin.kim/web/2019/02/16/js-decreaing-webpack-bundle.html#lodash-%EC%9A%A9%EB%9F%89-%EC%A4%84%EC%9D%B4%EA%B8%B0

 

Decrease webpack bunlding file size

Webpack bundling ํŒŒ์ผ ์‚ฌ์ด์ฆˆ ์ค„์ด๊ธฐ(Tree Shaking) ์ด ๊ธ€์€ ๊ฐœ์ธ์ ์œผ๋กœ ์‹œ๊ฐ„ ๊ณ„์‚ฐ์ด ํ•„์š”ํ•œ ํฌ๋กฌ ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ ์•ฑ์„ ๊ฐœ๋ฐœํ•˜๋‹ค๊ฐ€ ๋นŒ๋“œ ํŒŒ์ผ ํฌ๊ธฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด์„œ ์ž‘์„ฑํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (์‚ฌ์šฉํ™˜๊ฒฝ: We

blog.jungbin.kim

 

 

 

 

gzip์œผ๋กœ ์••์ถ•ํ•˜๊ธฐ

 

1/3 ~ 1/4 ์ •๋„๋กœ ๋ฒˆ๋“ค ์šฉ๋Ÿ‰์„ ์••์ถ•ํ•ด์ค๋‹ˆ๋‹ค. (!)

npm install compression-webpack-plugin --save-dev

 

https://webpack.js.org/plugins/compression-webpack-plugin/

 

CompressionWebpackPlugin | webpack

webpack is a module bundler. Its main purpose is to bundle JavaScript files for usage in a browser, yet it is also capable of transforming, bundling, or packaging just about any resource or asset.

webpack.js.org

 

์••์ถ•ํ•œ ๊ฒฐ๊ณผ ๋นŒ๋“œ ๋‚ด์šฉ๋ฌผ ์ค‘ .gz ๊ฐ€ ๋ถ™์€ ๊ฒƒ์ด ๋ณด์ž…๋‹ˆ๋‹ค.

 

์ตœ์ข…์ ์œผ๋กœ next.config.js๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

const withBundleAnalyzer = require("@next/bundle-analyzer")({
  enabled: process.env.ANALYZE === "true",
});
const webpack = require("webpack");
const CompressionPlugin = require("compression-webpack-plugin");

module.exports = withBundleAnalyzer({
  compress: true,
  webpack(config) {
    console.log(config);

    // ๋ฐฐํฌํ™˜๊ฒฝ์ธ๊ฐ€?
    const prod = process.env.NODE_ENV === "production";

    // ํ”Œ๋Ÿฌ๊ทธ์ธ ๊ด€๋ จ ์„ค์ •
    const plugins = [
      ...config.plugins,
      new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /^\.\/ko$/),
    ];
    if (prod) {
      plugins.push(new CompressionPlugin());
    }
    return {
      ...config,
      mode: prod ? "production" : "development",
      devtool: prod ? "hidden-source-map" : "eval",
      plugins: plugins,
    };
  },
});

 

 

 

2. redux devtools ๋“ฑ redux state๊ฐ’์„ ๋…ธ์ถœํ•˜๋Š” ๋‚ด์šฉ ์ œ๊ฑฐํ•˜๊ธฐ

 

์ง๋ฐฉ ๊ฐ™์ด redux state๊ฐ€ ๋…ธ์ถœ๋˜๋Š” ์ผ์„ ๋ง‰๊ธฐ ์œ„ํ•ด์„œ NODE_ENV๊ฐ€ production์ผ ๋•Œ Devtools๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋„๋ก ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ๋˜, next-redux-wrapper๊ฐ€ ์ƒ๋‹นํžˆ verboseํ•˜๊ธฐ ๋•Œ๋ฌธ์—(์ด๊ฑฐ์•ผ ๊ฐ„๋‹จํ•œ ๋กœ๊ทธ ์ฝ๊ธฐ๋„ ํž˜๋“ค ์ •๋„๋กœ ๋งŽ์Šต๋‹ˆ๋‹ค.) prod์ผ ๋•Œ๋Š” debug๊ฐ€ false๋กœ ์„ค์ •ํ•˜๋„๋ก ํ•˜์—ฌ ์ข€ ์กฐ์šฉํžˆ ๋งŒ๋“ค์–ด์ฃผ๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค. 

const prod = process.env.NODE_ENV === "production";

const configureStore = () => {
  const logger = createLogger();

  const enhancer = prod
    ? compose(applyMiddleware(thunk))
    : compose(composeWithDevTools(applyMiddleware(logger, thunk)));

  const store = createStore(rootReducer, enhancer);
  return store;
};

const wrapper = createWrapper(configureStore, { debug: !prod });

 

npm run build/startํ•˜์‹  ํ›„์— devtools์— ๋ถˆ์ด ๊บผ์กŒ๋Š”์ง€ ํ™•์‹คํžˆ ํ™•์ธํ•ด์ค์‹œ๋‹ค.

 

 

์—ฌ๋‹ด์ธ๋ฐ ํ‹ฐ์Šคํ† ๋ฆฌ๋Š” ๋ฐฐํฌ ํ™˜๊ฒฝ์ธ๋ฐ๋„ ์ด๊ฑธ ์•ˆ ๊บผ๋’€๋„ค์š”. ๋˜‘๋˜‘ํ•˜์‹  ๋ถ„๋“ค์ด๋ผ์„œ ๋ฌธ์ œ ์—†๋Š” ๋ถ€๋ถ„๋งŒ ๋…ธ์ถœ์‹œํ‚ค์‹  ๊ฑด์ง€ ์ž์„ธํ•œ ์ƒํ™ฉ์€ ๋ชจ๋ฅด๊ฒ ์ง€๋งŒ ์ผ๋‹จ ๊บผ์ฃผ๋Š” ๊ฒŒ ์›์น™์ž…๋‹ˆ๋‹ค.

 

 

 

3. ๋นŒ๋“œ ๋ฐ ๋ฐฐํฌ

 

์ฃผ์˜ํ•˜์‹ค ์ ์ด, Next๋Š” SSR์ด๊ธฐ ๋•Œ๋ฌธ์— ํ”„๋ก ํŠธ์—๋„ ์„œ๋ฒ„๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ๋ฐฑ์—”๋“œ๋งŒ pm2๋กœ ๋Œ๋ฆฌ๊ณ , ํ•ด๋‹น ์„œ๋ฒ„ ์ธ์Šคํ„ด์Šค๋ฅผ ์ข…๋ฃŒํ•  ๊ฒฝ์šฐ ์ ‘์†ํ•  ์ˆ˜ ์—†๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

์•™๋Œ€!

 

 

next ํ”„๋ก ํŠธ๋ฅผ ๋ฐฐํฌํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์šฐ์„  ์ปค์Šคํ…€ ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์–ด ๋‘ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

darrengwon.tistory.com/539?category=891029

 

express๋กœ Next ์ปค์Šคํ…€ ํ”„๋ก ํŠธ ์„œ๋ฒ„ ๊ตฌ์ถ•ํ•˜๊ธฐ

Advanced Features: Custom Server | Next.js Start a Next.js app programmatically using a custom server. nextjs.org npm run build && start ์™ธ์— ์ข€ ๋” ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์„ ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ปค์Šคํ…€ ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์–ด์•ผ..

darrengwon.tistory.com

 

์—ฌ๊ธฐ์— ๋งŒ๋“ค์–ด ๋‘์‹  ํ›„ ์ง„ํ–‰ํ•ฉ์‹œ๋‹ค.

 

์‹คํ–‰ script ๋ถ€๋ถ„์—์„œ๋Š”

npm run build => npm run start ๋‘ ๋ช…๋ น์–ด๋ฅผ ์น˜๊ธฐ ๊ท€์ฐฎ๋‹ค๋ฉด "prestart" ๋ฅผ ์ง€์ •ํ•ด๋‘๋ฉด ๋ฉ๋‹ˆ๋‹ค.

(์ €๋Š” ๋งค๋ฒˆ prestart๋ฅผ npm run build๋กœ ์ง€์ •ํ•ด๋‘๊ณ  ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค)

 

ํ”„๋ก ํŠธ ์ชฝ์˜ package.json์˜ script๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธํ•˜์‹ค ๋•Œ๋Š” ๋กœ๊ทธ ๊ผญ ๋‹ฌ์•„์ค์‹œ๋‹ค. ์•ˆ ๋‹ฌ์•„์ฃผ๋ฉด ์™œ ์•ˆ๋˜๋Š”์ง€ ์•Œ ์ˆ˜๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

  "scripts": {
    "dev": "next dev",
    "prepm2-dev": "cross-env ANALYZE=false next build",
    "pm2-dev": "pm2 start server.js -e testLog/err.log -o testLog/out.log --watch",
    "build": "cross-env NODE_ENV=production ANALYZE=true next build",
    "prestart": "npm run build",
    "start": "cross-env NODE_ENV=production pm2 start server.js -i 1 --name \"next-front\""
  },

 

๋ฐฑ์—”๋“œ ์ชฝ์˜ package.json์˜ script๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๋”ฐ๋กœ ์‹คํ–‰ํ•ด์ฃผ๋ฉด ์•„์ฃผ ๋ฒˆ๊ฑฐ๋กœ์šฐ๋‹ˆ concurrently๋ฅผ ์ด์šฉํ•ฉ๋‹ˆ๋‹ค.

dev๋Š” ๋กœ์ปฌ์—์„œ ์†Œ์Šค ์ฝ”๋“œ๋กœ ๊ฐœ๋ฐœํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ์ฝ”๋“œ์ด๊ณ 

test๋Š” ๋กœ์ปฌ์—์„œ ๋ฐฐํฌ ์ง์ „์˜ ํ™˜๊ฒฝ๊ณผ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ํ™˜๊ฒฝ์„ ์กฐ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ํ…Œ์ŠคํŠธ ์šฉ์ž…๋‹ˆ๋‹ค.

prod๋Š” ์‹ค์ œ ๋ฐฐํฌ ํ™˜๊ฒฝ์ž…๋‹ˆ๋‹ค. ๋ฆฌ๋ˆ…์Šค๋ผ์„œ cross-env๋ฅผ ํ•ด์ค˜๋„ ๋˜๊ณ  ์•ˆํ•ด์ค˜๋„ ๋ฉ๋‹ˆ๋‹ค. ์ €๋Š” ๊ทธ๋ƒฅ ๋‘๊ฒ ์Šต๋‹ˆ๋‹ค. ใ…Ž

 

  "scripts": {
    "front": "cd .. && cd front && npm run dev",
    "server": "nodemon --exec node app.js",
    "dev": "concurrently --kill-others-on-fail \"npm run front\" \"npm run server\"",
    "-----test-----": "------------------",
    "testServer": "cross-env NODE_ENV=production pm2 start app.js -i -1 --watch",
    "test": "concurrently --kill-others-on-fail \"npm run testServer\" \"npm run prodFront\"",
    "-----prod-----": "------------------",
    "prodServer": "cross-env NODE_ENV=production pm2 start app.js -i -1 --watch",
    "prodFront": "cd .. && cd front && npm run start",
    "prod": "concurrently --kill-others-on-fail \"npm run prodServer\" \"npm run prodFront\""
  },

 


๋Œ“๊ธ€์„ ๋‹ฌ์•„ ์ฃผ์„ธ์š”

darren, dev blog
๋ธ”๋กœ๊ทธ ์ด๋ฏธ์ง€ DarrenKwonDev ๋‹˜์˜ ๋ธ”๋กœ๊ทธ
VISITOR ์˜ค๋Š˜891 / ์ „์ฒด578,782