โ† ๋ชฉ๋ก์œผ๋กœ
Remix and Qwik Exploration

Next.js๋Š” React ๊ธฐ๋ฐ˜ SSR์˜ ๋Œ€ํ‘œ๋กœ ์ธ์ง€๋˜๊ณ  ์žˆ์ง€๋งŒ ๋ณต์žก์„ฑ๊ณผ ๋ฌด๊ฑฐ์šด ๋ฒˆ๋“ค ํฌ๊ธฐ ๋“ฑ์˜ ๋ฌธ์ œ์ ๋„ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.
์ด๋Ÿฌํ•œ ์ƒํ™ฉ์—์„œ Remix์™€ Qwik์€ ๋” ๋‹จ์ˆœํ•˜๊ณ  ํšจ์œจ์ ์ธ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

Next.js๋Š” ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜์ง€๋งŒ, ๊ทธ๋งŒํผ ๋ณต์žกํ•œ API์™€ ์„ค์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
๋˜ํ•œ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์งˆ์ˆ˜๋ก ๋ฒˆ๋“ค ํฌ๊ธฐ๊ฐ€ ์ฆ๊ฐ€ํ•˜๊ณ  ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๋น„์šฉ์ด ๋†’์•„์ง€๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค๋ฅธ ์ ‘๊ทผ ๋ฐฉ์‹์„ ์ทจํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ๋“ค์ด ๋“ฑ์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.

Remix์™€ Qwik์€ ๊ฐ๊ฐ ๋‹ค๋ฅธ ๋ฐฉ์‹์œผ๋กœ ์ด๋Ÿฌํ•œ ๋ฌธ์ œ์— ์ ‘๊ทผํ•ฉ๋‹ˆ๋‹ค.
Remix๋Š” ์›น ํ‘œ์ค€์„ ์ตœ๋Œ€ํ•œ ํ™œ์šฉํ•˜์—ฌ ๋‹จ์ˆœํ•จ์„ ์ถ”๊ตฌํ•˜๊ณ , Qwik์€ ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๊ณผ์ •์„ ๊ทผ๋ณธ์ ์œผ๋กœ ์žฌ์„ค๊ณ„ํ•˜์—ฌ ์ดˆ๊ธฐ ๋กœ๋”ฉ ์†๋„๋ฅผ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค.

Remix: ์›น ํ‘œ์ค€์„ ํ™œ์šฉํ•œ ๋‹จ์ˆœํ•จ

Remix๋Š” React Router์˜ ์ฐฝ์‹œ์ž๋“ค์ด ๋งŒ๋“  ํ”„๋ ˆ์ž„์›Œํฌ๋กœ, ์›น ํ‘œ์ค€์„ ์ตœ๋Œ€ํ•œ ํ™œ์šฉํ•˜๋Š” ์ฒ ํ•™์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

Next.js์™€์˜ ์ฃผ์š” ์ฐจ์ด์ 

Next.js๋Š” ๋‹ค์–‘ํ•œ ๋ Œ๋”๋ง ์ „๋žต(SSR, SSG, ISR)๊ณผ API ๋ผ์šฐํŠธ ๋“ฑ ๋งŽ์€ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜์ง€๋งŒ,
์ด๋กœ ์ธํ•ด API๊ฐ€ ๋ณต์žกํ•ด์ง€๊ณ  ํ•™์Šต ๊ณก์„ ์ด ๋†’์•„์ง‘๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด Remix๋Š” ์›น ํ‘œ์ค€์— ๊ธฐ๋ฐ˜ํ•œ ๋‹จ์ˆœํ•œ ์ ‘๊ทผ ๋ฐฉ์‹์„ ์ทจํ•ฉ๋‹ˆ๋‹ค.

์บ์‹ฑ ๋ฉ”์ปค๋‹ˆ์ฆ˜

Next.js๋Š” ์ž์ฒด์ ์ธ ์บ์‹ฑ ์‹œ์Šคํ…œ์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด ๋ณต์žกํ•œ ์„ค์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด Remix๋Š” HTTP ํ‘œ์ค€์ธ Cache-Control ํ—ค๋”๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ง๊ด€์ ์ธ ์บ์‹ฑ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

// Remix์˜ ๊ฐ„๋‹จํ•œ ์บ์‹ฑ ์˜ˆ์‹œ
export async function loader({ request }) {
  const data = await fetchData();
  return json(data, {
    headers: {
      "Cache-Control": "max-age=300, s-maxage=3600"
    }
  });
}

๋ผ์šฐํŒ… ์‹œ์Šคํ…œ

Next.js๋Š” ํŒŒ์ผ ์‹œ์Šคํ…œ ๊ธฐ๋ฐ˜ ๋ผ์šฐํŒ…์„ ์‚ฌ์šฉํ•˜๋ฉฐ, API ๋ผ์šฐํŠธ๋ฅผ ๋ณ„๋„๋กœ ๊ตฌ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Remix๋Š” ์ค‘์ฒฉ ๋ผ์šฐํŒ…์„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ง€์›ํ•˜๋ฉฐ, ๊ฐ ๋ผ์šฐํŠธ๊ฐ€ ์ž์ฒด ๋กœ๋”์™€ ์•ก์…˜์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค.

// Remix์˜ ์ค‘์ฒฉ ๋ผ์šฐํŒ… ์˜ˆ์‹œ
// routes/dashboard.tsx (๋ถ€๋ชจ ๋ผ์šฐํŠธ)
export default function Dashboard() {
  return (
    <div>
      <h1>๋Œ€์‹œ๋ณด๋“œ</h1>
      <Outlet /> {/* ์ž์‹ ๋ผ์šฐํŠธ๊ฐ€ ๋ Œ๋”๋ง๋˜๋Š” ์œ„์น˜ */}
    </div>
  );
}

// routes/dashboard.stats.tsx (์ž์‹ ๋ผ์šฐํŠธ)
export default function Stats() {
  return <div>ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ</div>;
}

๋ฐ์ดํ„ฐ ๋กœ๋”ฉ

Next.js๋Š” getServerSideProps, getStaticProps ๋“ฑ ๋‹ค์–‘ํ•œ ๋ฐ์ดํ„ฐ ํŽ˜์นญ ๋ฐฉ์‹์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. Remix๋Š” ๋‹จ์ผํ•œ loader/action ํŒจํ„ด์œผ๋กœ ์ผ๊ด€์„ฑ ์žˆ๋Š” ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

// Remix์˜ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์˜ˆ์‹œ
export async function loader({ params }) {
  const product = await getProduct(params.id);
  return json(product);
}

export default function ProductPage() {
  const product = useLoaderData();
  return <div>{/* ์ œํ’ˆ ์ •๋ณด ๋ Œ๋”๋ง */}</div>;
}

Remix์˜ ์žฅ์ 

  1. ์—๋Ÿฌ ์ฒ˜๋ฆฌ: ๊ฐ ๋ผ์šฐํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ ErrorBoundary๋ฅผ ์ •์˜ํ•˜์—ฌ ์„ธ๋ถ„ํ™”๋œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
  2. ํ”„๋กœ๊ทธ๋ ˆ์‹œ๋ธŒ ์ธํ•ธ์Šค๋จผํŠธ: ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์—†์ด๋„ ๊ธฐ๋ณธ ๊ธฐ๋Šฅ์ด ์ž‘๋™ํ•˜๋„๋ก ์„ค๊ณ„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
  3. ๋” ์ ์€ ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ: ํ•„์š”ํ•œ ์ฝ”๋“œ๋งŒ ํด๋ผ์ด์–ธํŠธ๋กœ ์ „์†กํ•˜์—ฌ ๋ฒˆ๋“ค ํฌ๊ธฐ๋ฅผ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค.

Qwik: ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ์ ‘๊ทผ๋ฒ•

Qwik์€ Builder.io ํŒ€์ด ๊ฐœ๋ฐœํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ, ํŠนํžˆ ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๊ณผ์ •์„ ๊ทผ๋ณธ์ ์œผ๋กœ ์žฌ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๋ฌธ์ œ

Next.js์™€ Remix๋ฅผ ํฌํ•จํ•œ ๋Œ€๋ถ€๋ถ„์˜ SSR ํ”„๋ ˆ์ž„์›Œํฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๊ณผ์ •์„ ๊ฑฐ์นฉ๋‹ˆ๋‹ค:

  1. HTML์„ ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋งํ•˜์—ฌ ์ „์†ก
  2. ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฒˆ๋“ค ๋‹ค์šด๋กœ๋“œ
  3. ์ „์ฒด ์ปดํฌ๋„ŒํŠธ ํŠธ๋ฆฌ ์žฌ๊ตฌ์„ฑ
  4. ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์—ฐ๊ฒฐ ๋ฐ ์ƒํƒœ ๋ณต์›

์ด ๊ณผ์ •์€ ํŠนํžˆ ๋ณต์žกํ•œ ์•ฑ์—์„œ ์ƒ๋‹นํ•œ ์‹œ๊ฐ„์ด ์†Œ์š”๋˜๋ฉฐ "ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๋น„์šฉ"์ด๋ผ๊ณ  ๋ถˆ๋ฆฝ๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด TTI(Time to Interactive)๊ฐ€ ๋Šฆ์–ด์ง€๊ณ  ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ์ €ํ•˜๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Qwik์˜ ์ ‘๊ทผ๋ฒ•: ๋ฆฌ์คŒ์–ด๋ธ”(Resumable)

Qwik์€ ์ด ๋ฌธ์ œ๋ฅผ "๋ฆฌ์คŒ์–ด๋ธ”(Resumable)" ์ ‘๊ทผ๋ฒ•์œผ๋กœ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์•ฑ์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ ์‹œ์ž‘ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์„œ๋ฒ„์—์„œ ์ค‘๋‹จ๋œ ์ง€์ ๋ถ€ํ„ฐ ์ด์–ด์„œ ์‹คํ–‰ํ•˜๋Š” ๊ฐœ๋…์ž…๋‹ˆ๋‹ค.

// Qwik ์ปดํฌ๋„ŒํŠธ ์˜ˆ์‹œ
export const Counter = component$(() => {
  const count = useSignal(0);
  
  return (
    <div>
      <p>Count: {count.value}</p>
      <button onClick$={() => count.value++}>์ฆ๊ฐ€</button>
    </div>
  );
});

์ŠคํŠธ๋ฆฌ๋ฐ ํ•˜์ด๋“œ๋ ˆ์ด์…˜์˜ ์ž‘๋™ ๋ฐฉ์‹

Qwik์˜ ์ ‘๊ทผ๋ฒ•์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํŠน์ง•์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค:

1. ์ง€์—ฐ ๋กœ๋”ฉ(Lazy Loading)

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

// ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ๋ณ„๋„์˜ ์ฒญํฌ๋กœ ๋ถ„๋ฆฌ๋จ
export const MyButton = component$(() => {
  return (
    <button onClick$={() => {
      // ์ด ์ฝ”๋“œ๋Š” ๋ฒ„ํŠผ์ด ํด๋ฆญ๋  ๋•Œ๋งŒ ๋กœ๋“œ๋จ
      console.log('๋ฒ„ํŠผ์ด ํด๋ฆญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค');
    }}>
      ํด๋ฆญ
    </button>
  );
});

2. ์ง๋ ฌํ™”๋œ ์ƒํƒœ

๋ชจ๋“  ์ƒํƒœ์™€ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ HTML์— ์ง๋ ฌํ™”ํ•˜์—ฌ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋กœ๋“œ๋˜๊ธฐ ์ „์—๋„ ์ƒํƒœ ์ •๋ณด๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

<!-- Qwik์ด ์ƒ์„ฑํ•œ HTML ์˜ˆ์‹œ -->
<button on:click="./chunks/button_click.js#handler" q:obj="123">ํด๋ฆญ</button>

3. ์ ์ง„์  ์‹คํ–‰

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

์ด๋Ÿฌํ•œ ์ ‘๊ทผ๋ฒ•์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ด์ ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

  • ์ดˆ๊ธฐ ๋กœ๋”ฉ ์‹œ๊ฐ„ ๋‹จ์ถ•: ํ•„์ˆ˜์ ์ธ HTML๋งŒ ๋จผ์ € ๋กœ๋“œํ•˜์—ฌ FCP(First Contentful Paint)๋ฅผ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.
  • ์ตœ์†Œํ•œ์˜ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ: ์ƒํ˜ธ์ž‘์šฉ์ด ํ•„์š”ํ•œ ๋ถ€๋ถ„๋งŒ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋กœ๋“œํ•˜์—ฌ TTI(Time to Interactive)๋ฅผ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.
  • ์ฆ‰๊ฐ์ ์ธ ์ƒํ˜ธ๋™์ž‘: ์ „์ฒด ์•ฑ์ด ํ•˜์ด๋“œ๋ ˆ์ด์…˜๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆด ํ•„์š” ์—†์ด ๊ฐ ๋ถ€๋ถ„์ด ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

Qwik City

Qwik์˜ ๋ฉ”ํƒ€ ํ”„๋ ˆ์ž„์›Œํฌ์ธ Qwik City๋Š” ๋ผ์šฐํŒ…, ๋ฏธ๋“ค์›จ์–ด, ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ๋“ฑ์˜ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

// Qwik City์˜ ๋ผ์šฐํŠธ ์ปดํฌ๋„ŒํŠธ ์˜ˆ์‹œ
export const onGet = async ({ params, response }) => {
  const data = await fetchData(params.id);
  response.headers.set('Cache-Control', 'max-age=3600');
  return { data };
};

export default component$(() => {
  const { data } = useEndpoint();
  return <div>{/* ๋ฐ์ดํ„ฐ ๋ Œ๋”๋ง */}</div>;
});

Remix๋Š” ์›น ํ‘œ์ค€์„ ํ™œ์šฉํ•œ ๋‹จ์ˆœํ•จ์„, Qwik์€ ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ์ž์ฒด๋ฅผ ์žฌ์„ค๊ณ„ํ•œ ์ ‘๊ทผ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
ํ”„๋กœ์ ํŠธ์˜ ์š”๊ตฌ์‚ฌํ•ญ์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์„ ํƒํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.