(λ²ˆμ—­) SSR μ„±λŠ₯ λŒ€κ²°

2024-09-242024-09-24
  • SSR

원문 : An SSR Performance Showdown

πŸ’‘ λ…ΈνŠΈ: 첫 번째 λ²„μ „μ˜ λ²€μΉ˜λ§ˆν¬μ—μ„œ λͺ‡ 가지 μ‹€μˆ˜κ°€ μžˆμ—ˆμœΌλ©°, μ΄λŠ” Theo Browne의 이 νŠΈμœ—μ— μš”μ•½λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. λ¨Όμ € μ΄λŸ¬ν•œ 였λ₯˜μ— λŒ€ν•΄ μ‚¬κ³Όλ“œλ¦¬λ©°, Rich Harris, Ryan Carniato, Dan Abramov, BalΓ‘zs NΓ©meth, Dominic Gannaway의 μˆ˜μ •μ— λŒ€ν•΄ μ§„μ‹¬μœΌλ‘œ κ°μ‚¬μ˜ 말씀을 μ „ν•©λ‹ˆλ‹€. λ˜ν•œ Preact 버전에 κΈ°μ—¬ν•΄μ£Όμ‹  Jovi De Croockμ—κ²Œλ„ 감사λ₯Ό ν‘œν•©λ‹ˆλ‹€.

μ„œλ²„ μ‚¬μ΄λ“œ λ Œλ”λ§(SSR)은 Node.js둜 κ³ μ„±λŠ₯ μ›Ή μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ ꡬ좕할 λ•Œ μ’…μ’… κ°„κ³Όλ˜λŠ” μΈ‘λ©΄μž…λ‹ˆλ‹€.

μ»¨μ„€νŒ…μ„ ν•˜λ©΄μ„œ Node.js μ„±λŠ₯ 문제λ₯Ό λ””λ²„κΉ…ν•˜λŠ” 데 쀑점을 λ‘λŠ” κ²½μš°κ°€ λ§Žμ•˜μŠ΅λ‹ˆλ‹€. μ΄λŸ¬ν•œ μƒν™©μ—μ„œ 문제의 원인은 거의 항상 SSRμ΄μ—ˆμŠ΅λ‹ˆλ‹€. SSR은 CPU에 큰 뢀담을 μ£ΌλŠ” μž‘μ—…μœΌλ‘œ, Node.js의 이벀트 루프λ₯Ό μ°¨λ‹¨ν•˜λŠ” μ£Όμš” 원인이 될 수 μžˆμŠ΅λ‹ˆλ‹€. λ”°λΌμ„œ ν”„λŸ°νŠΈμ—”λ“œ μŠ€νƒμ„ 선택할 λ•Œ 이 점을 λ°˜λ“œμ‹œ κ³ λ €ν•΄μ•Ό ν•©λ‹ˆλ‹€.

μ €ν¬λŠ” μ˜€λŠ˜λ‚  κ°€μž₯ 인기 μžˆλŠ” λΌμ΄λΈŒλŸ¬λ¦¬μ—μ„œ SSR μ„±λŠ₯이 μ–΄λ–€ μƒνƒœμΈμ§€ μ•Œμ•„λ³΄κΈ°λ‘œ ν–ˆμŠ΅λ‹ˆλ‹€. 특히 Fastify와 κΉ”λ”ν•˜κ²Œ 톡합할 수 μžˆλŠ” 라이브러리λ₯Ό μ§‘μ€‘μ μœΌλ‘œ μ‚΄νŽ΄λ³΄μ•˜μŠ΅λ‹ˆλ‹€.

이λ₯Ό μœ„ν•΄ λ§Žμ€ μš”μ†Œλ₯Ό ν¬ν•¨ν•œ λ³΅μž‘ν•œ μƒ˜ν”Œ λ¬Έμ„œλ₯Ό μƒμ„±ν•˜μ—¬ ν…ŒμŠ€νŠΈ νŽ˜μ΄μ§€λ₯Ό 맀우 크게 λ§Œλ“€κ³ , 각 라이브러리의 μ„±λŠ₯을 μ •ν™•ν•˜κ²Œ μΈ‘μ •ν•  수 μžˆλŠ” μΆ©λΆ„ν•œ μ‹€ν–‰ μ‹œκ°„μ„ 확보해야 ν–ˆμŠ΅λ‹ˆλ‹€.

κ·Έλž˜μ„œ LLMμ—κ²Œ 10x10px νƒ€μΌλ‘œ 된 divλ₯Ό μ‚¬μš©ν•˜μ—¬ μ»¨ν…Œμ΄λ„ˆμ— λ‚˜μ„ ν˜•μ„ κ·Έλ¦¬λŠ” μ½”λ“œλ₯Ό μž‘μ„±ν•΄ 달라고 μš”μ²­ν–ˆμŠ΅λ‹ˆλ‹€.

<script>
const wrapper = document.getElementById('wrapper')
const width = 960
const height = 720
const cellSize = 5

function drawSpiral() {
  let centerX = width / 2
  let centerY = height / 2
  let angle = 0
  let radius = 0
  const step = cellSize

  while (radius < Math.min(width, height) / 2) {
    let x = centerX + Math.cos(angle) * radius
    let y = centerY + Math.sin(angle) * radius

    if (x >= 0 && x <= width - cellSize && y >= 0 && y <= height - cellSize)
    {
      const tile = document.createElement('div')
      tile.className = 'tile'
      tile.style.left = `${x}px`
      tile.style.top = `${y}px`
      wrapper.appendChild(tile)
    }

    angle += 0.2
    radius += step * 0.015
  }
}
drawSpiral()
</script>

이후, ν…ŒμŠ€νŠΈν•˜λ €λŠ” λͺ¨λ“  라이브러리λ₯Ό μ‚¬μš©ν•œ 버전을 μƒμ„±ν•˜λ„λ‘ μš”μ²­ν–ˆμœΌλ©°, μ›λž˜ 예제의 DOM λ©”μ„œλ“œμ— μ˜μ‘΄ν•˜μ§€ μ•Šκ³  각 라이브러리의 λ Œλ”λ§ 엔진을 μ‚¬μš©ν•˜λ„λ‘ κ΅¬ν˜„μ„ μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€.

λ‹€μŒμ€ 2398개의 <div> μš”μ†Œλ₯Ό ν¬ν•¨ν•œ μƒ˜ν”Œ λ¬Έμ„œμ˜ λͺ¨μŠ΅μž…λ‹ˆλ‹€.

1

Fastify의 Vite 톡합 섀정은 λ‹€μ–‘ν•œ ν”„λ ˆμž„μ›Œν¬μ˜ SSR μ„±λŠ₯을 μ‘°μ‚¬ν•˜κΈ° μœ„ν•œ μ™„λ²½ν•œ ν…ŒμŠ€νŠΈ ν™˜κ²½μ„ μ œκ³΅ν•©λ‹ˆλ‹€.

이 κΈ€μ—μ„œλŠ” SSR을 μˆ˜ν–‰ν•˜λŠ” 데 ν•„μš”ν•œ μ΅œμ†Œν•œμ˜ 보일러 ν”Œλ ˆμ΄νŠΈλ₯Ό μ‚΄νŽ΄λ³΄κ³  5가지 μ£Όμš” ν”„λŸ°νŠΈμ—”λ“œ 라이브러리의(React, Vue, Solid, Svelte, Preact) μ„±λŠ₯을 비ꡐ해 λ³΄μ•˜μŠ΅λ‹ˆλ‹€. λ˜ν•œ 더 κ°„λ‹¨ν•œ λŒ€μ•ˆμœΌλ‘œ fastify-html(ghtml을 Fastify둜 λž©ν•‘ν•œ 버전) 및 @fastify/viewλ₯Ό ν†΅ν•œ ejs도 μ‚΄νŽ΄λ΄€μŠ΅λ‹ˆλ‹€.

μš°λ¦¬λŠ” Next.js, Astro, Qwik 와 같은 도ꡬ듀, 그리고 λ‹€λ₯Έ μ™„μ „ν•œ ν”„λ ˆμž„μ›Œν¬λ“€μ„ κ³ λ €ν•˜μ§€ μ•ŠκΈ°λ‘œ ν–ˆμŠ΅λ‹ˆλ‹€. κ·Έ μ΄μœ λŠ” 이듀이 독립적인 λ Œλ”λ§ λ©”μ„œλ“œλ₯Ό μ œκ³΅ν•˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.

@fastify/vite 기반 ν…ŒμŠ€νŠΈμ˜ 경우 λ‹€μŒκ³Ό 같은 보일러 ν”Œλ ˆμ΄νŠΈλ₯Ό μ‚¬μš©ν–ˆμŠ΅λ‹ˆλ‹€.

import Fastify from 'fastify';
import FastifyVite from '@fastify/vite';

const server = Fastify();
await server.register(FastifyVite /* options */);

await server.vite.ready();
await server.listen({ port: 3000 });

λͺ¨λ“  ν…ŒμŠ€νŠΈλŠ” ν”„λ‘œλ•μ…˜ λΉŒλ“œ, 즉 vite buildλ₯Ό μ‹€ν–‰ν•œ 후에 μ‹€ν–‰λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

μ˜ˆμ™Έκ°€ μžˆλ‹€λ©΄ Viteκ°€ ν•„μš”ν•˜μ§€ μ•Šμ€ fastify-html 및 ejs ν…ŒμŠ€νŠΈμž…λ‹ˆλ‹€.

λͺ¨λ“  μ˜ˆμ œκ°€ ν¬ν•¨λœ μ €μž₯μ†Œλ₯Ό ν™•μΈν•˜μ„Έμš”.

일관성 보μž₯

λͺ¨λ“  μ˜ˆμ‹œκ°€ λ™μΌν•œ νŠΉμ„±μ„ κ³΅μœ ν•˜λ„λ‘ ν–ˆμŠ΅λ‹ˆλ‹€.

  • ν΄λΌμ΄μ–ΈνŠΈ μΈ‘ λ°˜μ‘μ„± κΈ°λŠ₯을 μ‚¬μš©ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

  • React 및 Solid의 경우처럼 ν•΄λ‹Ή ν”„λ ˆμž„μ›Œν¬μ— λΆ€μ μ ˆν•œ 경우λ₯Ό μ œμ™Έν•˜κ³  λͺ¨λ“  μŠ€νƒ€μΌ 바인딩은 ν…œν”Œλ¦Ώ λ¦¬ν„°λŸ΄μ„ μ‚¬μš©ν•˜μ—¬ μˆ˜ν–‰λ©λ‹ˆλ‹€.

  • x 및 y 값은 toFixed(2)둜 λ§Œλ“€μ–΄μ§‘λ‹ˆλ‹€.

  • <style> νƒœκ·ΈλŠ” document μ…€μ—λ§Œ μ‘΄μž¬ν•©λ‹ˆλ‹€.

ν…ŒμŠ€νŠΈλŠ” 2020λ…„ν˜• λ§₯뢁 에어 M1, 8GB RAM, λ§₯OS λ²€μΈ„λΌμ—μ„œμ˜ λ…Έλ“œ v22μ—μ„œ μ‹€ν–‰λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

fastify-html

이상값(outlier)λΆ€ν„° μ‹œμž‘ν•˜κ² μŠ΅λ‹ˆλ‹€. ghtmlλ₯Ό λž˜ν•‘ν•˜λŠ” Fastify ν”ŒλŸ¬κ·ΈμΈ fastify-htmlλŠ” μ΄ˆλ‹Ή 1088개의 μš”μ²­μ„ μ „λ‹¬ν•©λ‹ˆλ‹€. μ•žμ„œ μ–ΈκΈ‰ν–ˆλ“―μ΄ 이 섀정은 νŠΉλ³„ν•œ κ΅¬λ¬Έμ΄λ‚˜ λ³€ν™˜μ΄ ν•„μš”ν•˜μ§€ μ•ŠμœΌλ―€λ‘œ Viteκ°€ ν•„μš”ν•˜μ§€ μ•Šλ‹€λŠ” μ μ—μ„œ λ‹€λ₯Έ λͺ¨λ“  μ„€μ •κ³Ό λ‹€λ¦…λ‹ˆλ‹€.

2

fastify-html이 이 ν…ŒμŠ€νŠΈμ— κΈ°μ€€μ μœΌλ‘œ μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€. 이 λΌμ΄λΈŒλŸ¬λ¦¬λŠ” λ‹€λ₯Έ 라이브러리의 κ³ κΈ‰ κΈ°λŠ₯이 μ—†λŠ” λ‹¨μˆœν•œ HTML ν…œν”Œλ¦Ώ 라이브러리의 λž˜νΌμ— λΆˆκ³Όν•˜κΈ° λ•Œλ¬Έμ— λ‹€λ₯Έ λͺ¨λ“  λΌμ΄λΈŒλŸ¬λ¦¬μ™€ μ œλŒ€λ‘œ λΉ„κ΅ν•˜κΈ°λŠ” μ–΄λ ΅μŠ΅λ‹ˆλ‹€. 더 λ‹¨μˆœν•˜κΈ° λ•Œλ¬Έμ— 이미 더 λ‚˜μ€ μ„±λŠ₯을 λ°œνœ˜ν•  κ²ƒμœΌλ‘œ μ˜ˆμƒν–ˆκ³ , κ·Έμ € λ‹€λ₯Έ λͺ¨λ“  κΈ°λŠ₯을 κ°–μΆ˜ λΌμ΄λΈŒλŸ¬λ¦¬λ“€μ΄ μ–Όλ§ˆλ‚˜ λ’€μ²˜μ§€λŠ”μ§€λ₯Ό μ•Œμ•„λ³΄κ³  μ‹Άμ—ˆμŠ΅λ‹ˆλ‹€.

μ‚¬μš©λœ 보일러 ν”Œλ ˆμ΄νŠΈλŠ” μ•„λž˜μ—μ„œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. document 셸을 λ Œλ”λ§ν•˜λŠ” λ ˆμ΄μ•„μ›ƒ ν•¨μˆ˜λ₯Ό λ“±λ‘ν•˜λŠ” 데 createHtmlFunction(@fastify/vite λͺ¨λ°©)κ°€ μ‚¬μš©λ˜λŠ” 것을 μ£Όλͺ©ν•˜μ„Έμš”.

import Fastify from 'fastify';
import fastifyHtml from 'fastify-html';
import { createHtmlFunction } from './client/index.js';

const server = Fastify();
await server.register(fastifyHtml);

server.addLayout(createHtmlFunction(server));

참고둜, ꡬ식 EJS(@fastify/view 기반)λ₯Ό μ‚¬μš©ν•œ ν…ŒμŠ€νŠΈλ„ μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€. μ΄ˆλ‹Ή 443개의 μš”μ²­μ„ μ²˜λ¦¬ν•  수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

Vue

2μœ„λ₯Ό μ°¨μ§€ν•œ VueλŠ” μ΄ˆλ‹Ή 1028건의 μš”μ²­μ„ μ²˜λ¦¬ν•˜λ©°, λ›°μ–΄λ‚œ SSR μ„±λŠ₯κ³Ό μ™„μ „ν•œ 라이브러리 μƒνƒœκ³„λ₯Ό μ›ν•˜λŠ” 경우 졜고의 선택일 κ²ƒμž…λ‹ˆλ‹€.

3

동기식 μ„œλ²„ μ‚¬μ΄λ“œ λ Œλ”λ§μ„ μœ„ν•΄ μ‚¬μš©λœ Vue APIλŠ” renderToString()μž…λ‹ˆλ‹€.

import { renderToString } from 'vue/server-renderer';

// ...

await server.register(FastifyVite, {
  async createRenderFunction({ createApp }) {
    return async () => ({
      element: await renderToString(createApp()),
    });
  },
});

Svelte

3μœ„λŠ” Svelte 5(아직 μΆœμ‹œ μ „)둜, μ΄ˆλ‹Ή 968건의 μš”μ²­μ„ μ²˜λ¦¬ν•˜μ—¬ 맀우 인상적인 μ„±λŠ₯을 λ³΄μ—¬μ£Όμ—ˆμŠ΅λ‹ˆλ‹€. 특히 ν’λΆ€ν•œ κΈ°λŠ₯을 κ³ λ €ν–ˆμ„ λ•Œ λ”μš± λ†€λΌμš΄ κ²°κ³Όμž…λ‹ˆλ‹€.

SvelteλŠ” 자체적인 non-JSX templating ꡬ문이 있으며 엔진이 맀우 νš¨μœ¨μ μ΄μ–΄μ„œ μ„±μˆ™ν•œ 라이브러리 μ—μ½”μ‹œμŠ€ν…œμ„ κ°–μΆ˜ ν”„λ ˆμž„μ›Œν¬κ°€ ν•„μš”ν•˜κ³  SSR μ„±λŠ₯ μ €ν•˜λ₯Ό μ›ν•˜μ§€ μ•ŠλŠ” 경우 νƒμ›”ν•œ 선택이 될 수 μžˆμŠ΅λ‹ˆλ‹€.

4

μ„œλ²„ μ‚¬μ΄λ“œ λ Œλ”λ§μ„ μœ„ν•΄ μ‚¬μš©λœ Svelte APIλŠ” Svelte 5의 render()μž…λ‹ˆλ‹€.

await server.register(FastifyVite, {
  root: import.meta.url,
  createRenderFunction({ Page }) {
    return () => {
      const { body: element } = render(Page);
      return { element };
    };
  },
});

render() ν•¨μˆ˜λŠ” head 및 body의 속성도 λ°˜ν™˜ν•©λ‹ˆλ‹€.

Solid

4μœ„λŠ” μ΄ˆλ‹Ή 907건의 μš”μ²­μ„ μ²˜λ¦¬ν•˜λŠ” SolidJSκ°€ μ°¨μ§€ν–ˆμŠ΅λ‹ˆλ‹€. Svelte보닀 맀우 κ·Όμ†Œν•œ 차이둜 λ’€μ²˜μ§‘λ‹ˆλ‹€. SolidλŠ” React의 맀우 μœ λ§ν•œ λŒ€μ•ˆμ΄μ§€λ§Œ, 아직 μƒνƒœκ³„κ°€ μ„±μˆ™ν•˜μ§€ μ•Šμ€ μƒνƒœμž…λ‹ˆλ‹€.

μš°λ¦¬κ°€ μ£Όλͺ©ν•œ ν•œ κ°€μ§€λŠ” SolidJSκ°€ μ‹€μ œλ‘œ μˆ˜ν™”(Hydration) κ³Όμ •μ˜ μΌλΆ€λ‘œ IDλ₯Ό μ‚¬μš©ν•˜λŠ” 데 어렀움을 κ²ͺκ³  μžˆλ‹€λŠ” κ²ƒμž…λ‹ˆλ‹€. Vue와 Solid κ°κ°μ—μ„œ μƒμ„±λœ λ§ˆν¬μ—…μ„ 비ꡐ해 λ³΄μ„Έμš”.

<div class="tile" style="left: 196.42px; top: 581.77px">
<div data-hk=1c2397 class="tile" style="left: 196.42px; top: 581.77px">

μ΄λŠ” μ„±λŠ₯ μ €ν•˜μ˜ 상당 뢀뢄이 μœ μ„ μœΌλ‘œ 전솑해야 ν•˜λŠ” 이 μΆ”κ°€ μ‘°κ°μ—μ„œ λ°œμƒν•œλ‹€λŠ” 것을 μ˜λ―Έν•©λ‹ˆλ‹€. ν•˜μ§€λ§Œ μ €ν¬λŠ” μˆ˜ν™” λ“±μ˜ ν΄λΌμ΄μ–ΈνŠΈ κΈ°λŠ₯을 ν™œμ„±ν™”ν•œ 정상적인 μ‹€μ œ μƒν™©μ—μ„œ ν”„λ ˆμž„μ›Œν¬κ°€ μ–΄λ–»κ²Œ μž‘λ™ν•˜λŠ”μ§€ μ •ν™•νžˆ κ²€μ¦ν•˜κ³  μ‹Άμ—ˆμŠ΅λ‹ˆλ‹€.

5

보일러 ν”Œλ ˆμ΄νŠΈμ˜ 경우, @fastify/vite의 createRenderFunction 훅을 μ‚¬μš©ν•˜μ—¬ Solid μ»΄ν¬λ„ŒνŠΈ ν•¨μˆ˜(createApp)λ₯Ό μΊ‘μ²˜ν–ˆμŠ΅λ‹ˆλ‹€.

import { renderToString } from 'solid-js/web';

// ...

await server.register(FastifyVite, {
  root: import.meta.url,
  createRenderFunction({ createApp }) {
    return () => {
      return {
        element: await renderToString(createApp),
      };
    };
  },
});

Preact

React의 인기 λ™μƒμœΌλ‘œ 5μœ„λ₯Ό μ°¨μ§€ν•œ PreactλŠ” μ΄ˆλ‹Ή 717건의 μš”μ²­μ„ μ²˜λ¦¬ν•©λ‹ˆλ‹€. PreactλŠ” React와 맀우 μœ μ‚¬ν•˜μ§€λ§Œ, 더 λΉ λ₯΄κ³  κ°€λ³κ²Œ λ§Œλ“œλŠ” λ§Žμ€ 차이점이 μžˆμŠ΅λ‹ˆλ‹€.

6

동기식 μ„œλ²„ μ‚¬μ΄λ“œ λ Œλ”λ§μ— μ‚¬μš©λ˜λŠ” Preact APIλŠ” renderToString()μž…λ‹ˆλ‹€.

import { renderToString } from 'preact-render-to-string';

// ...

await server.register(FastifyVite, {
  root: import.meta.url,
  createRenderFunction({ createApp }) {
    return () => {
      return {
        element: renderToString(createApp()),
      };
    };
  },
});

React

React 19 RCλŠ” μ΄ˆλ‹Ή 572건의 μš”μ²­μ„ μ²˜λ¦¬ν•˜λ©° 6μœ„λ₯Ό μ°¨μ§€ν–ˆμŠ΅λ‹ˆλ‹€.

7

동기식 μ„œλ²„ μ‚¬μ΄λ“œ λ Œλ”λ§μ— μ‚¬μš©λ˜λŠ” React APIλŠ” renderToString()μž…λ‹ˆλ‹€.

import { renderToString } from 'react-dom/server';

// ...

await server.register(FastifyVite, {
  root: import.meta.url,
  createRenderFunction({ createApp }) {
    return () => {
      return {
        element: renderToString(createApp()),
      };
    };
  },
});

마무리

πŸ’‘ κ·Έλ ‡λ‹€λ©΄ 이 κ²°κ³ΌλŠ” 무엇을 μ˜λ―Έν• κΉŒμš”?

맨 μœ„μ—λŠ” fastify-htmlκ³Ό Vueκ°€ 있고 κ·Έ λ’€λ₯Ό Svelte와 Solidκ°€ 바짝 λ’€μ«“κ³  μžˆμŠ΅λ‹ˆλ‹€. μ•„λ§ˆλ„ Vue와 Svelteκ°€ SSR μ„±λŠ₯κ³Ό μƒνƒœκ³„ μ„±μˆ™λ„ μ‚¬μ΄μ—μ„œ κ°€μž₯ 쒋은 μ ˆμΆ©μ•ˆμ„ μ œμ‹œν•  κ²ƒμž…λ‹ˆλ‹€.

μ•žμ„œ μ–ΈκΈ‰ν–ˆλ“―μ΄, 본격적인 ν”„λŸ°νŠΈμ—”λ“œ ν”„λ ˆμž„μ›Œν¬λ₯Ό μ—†μ• κ³  μ΅œμ†Œν•œμ˜ ν…œν”Œλ¦Ώμ„ κ³ μˆ˜ν•¨μœΌλ‘œμ¨ μ„±λŠ₯μ—μ„œ μ–΄λ–€ 이점을 얻을 수 μžˆλŠ”μ§€ 보여주기 μœ„ν•΄ fastify-html ν…ŒμŠ€νŠΈκ°€ κΈ°μ€€μ„ μœΌλ‘œ μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

πŸš€ ν•œκ΅­μ–΄λ‘œ 된 ν”„λŸ°νŠΈμ—”λ“œ 아티클을 λΉ λ₯΄κ²Œ 받아보고 μ‹Άλ‹€λ©΄ Korean FE Article을 κ΅¬λ…ν•΄μ£Όμ„Έμš”!

Profile picture

emewjin

Frontend Developer

잘λͺ»λœ λ‚΄μš© ν˜Ήμ€ 더 쒋은 방법이 있으면 μ–Έμ œλ“ μ§€ μ•Œλ €μ£Όμ„Έμš” XD