Skip to main content
Summary

이제는 페이지가 필요하다

지금까지는 주로 하나의 컴포넌트 안에서 상태를 만들고, 조건과 반복으로 화면을 바꾸고, 폼과 이벤트를 다뤘다. 이것만으로도 작은 위젯이나 단일 화면 앱은 만들 수 있다. 하지만 실제 웹사이트에는 보통 여러 페이지가 있다.

예를 들어 쇼핑몰을 만든다고 생각해 보자.

  • /는 첫 화면

  • /about은 서비스 소개

  • /products는 상품 목록

  • /products/1은 1번 상품 상세

  • /products/2은 2번 상품 상세

이때 필요한 것이 라우팅이다. 라우팅은 "어떤 URL에 어떤 화면을 보여 줄 것인가"를 정하는 규칙이다. SvelteKit은 이 규칙을 JavaScript 객체나 별도 설정 파일에 길게 쓰지 않는다. 대신 src/routes 폴더 구조를 보고 URL을 만든다. 이런 방식을 파일 기반 라우팅이라고 한다.

SvelteKit 라우팅과 레이아웃 시작하기AI 생성 이미지

src/routes 폴더가 URL이 된다

SvelteKit 프로젝트에서 가장 중요한 라우팅 폴더는 src/routes다. 이 폴더 안의 구조가 그대로 URL 경로가 된다. 파일 기반 라우팅의 기본 규칙은 단순하다. src/routes가 사이트의 루트 주소 /에 해당하고, 그 안의 폴더 이름이 URL 조각이 된다1.

src/routes/
  +page.svelte
  about/
    +page.svelte
  products/
    +page.svelte

위 구조는 다음 URL과 연결된다.

src/routes/+page.svelte              -> /
src/routes/about/+page.svelte        -> /about
src/routes/products/+page.svelte     -> /products

여기서 눈여겨볼 파일 이름은 +page.svelte다. SvelteKit에서 +page.svelte는 "이 경로에서 실제로 보여 줄 페이지 컴포넌트"를 뜻한다. 폴더 이름은 URL이 되지만, +page.svelte라는 파일명 자체는 URL에 나타나지 않는다.

처음에는 이렇게 이해하면 충분하다.

폴더 경로 = URL 경로
+page.svelte = 그 URL에서 보이는 화면

예를 들어 src/routes/about/+page.svelte를 만들고 다음처럼 작성하면 /about 주소에서 이 화면이 보인다.

<h2>서비스 소개</h2>
<p>Svelte로 만든 작은 쇼핑몰 예제입니다.</p>

따로 라우터 설정을 추가하지 않아도 된다. 파일을 올바른 위치에 만들면 SvelteKit이 경로를 알아서 연결한다. 여기서 "알아서"라는 말은 마법이 아니다. SvelteKit이 프로젝트를 빌드할 때 src/routes 폴더를 분석해 URL과 페이지 파일의 관계를 구성하는 것이다.

+page.svelte는 한 주소의 본문이다

+page.svelte는 일반 Svelte 컴포넌트처럼 작성한다. <script>, 마크업, <style>을 사용할 수 있고, $state, $derived, 이벤트, 반복문도 그대로 쓸 수 있다.

첫 화면을 다음처럼 만들어 보자.

<!-- src/routes/+page.svelte -->
<script>
  const featured = ['키보드', '마우스', '모니터'];
</script>

<section>
  <h2>오늘의 추천 상품</h2>

  <ul>
    {#each featured as item}
      <li>{item}</li>
    {/each}
  </ul>
</section>

이 파일은 / 주소의 페이지다. 이번에는 상품 목록 페이지를 만든다.

<!-- src/routes/products/+page.svelte -->
<script>
  const products = [
    { id: 1, name: '기계식 키보드', price: 89000 },
    { id: 2, name: '무선 마우스', price: 42000 },
    { id: 3, name: '27인치 모니터', price: 239000 }
  ];
</script>

<h2>상품 목록</h2>

<ul class="products">
  {#each products as product}
    <li>
      <a href={`/products/${product.id}`}>
        {product.name} - {product.price.toLocaleString()}원
      </a>
    </li>
  {/each}
</ul>

여기서도 링크는 특별한 컴포넌트가 아니라 일반 <a> 태그다. SvelteKit에서는 내부 링크를 보통 <a href="/products">처럼 작성한다. 브라우저의 기본 링크 의미를 유지하면서, 앱 안에서는 필요한 경우 빠른 클라이언트 내비게이션으로 처리할 수 있다.

동적 라우트: products/[id]

상품이 3개라면 상세 페이지 파일을 3개 만들 수도 있다.

products/1/+page.svelte
products/2/+page.svelte
products/3/+page.svelte

하지만 상품이 1,000개라면 이렇게 만들 수 없다. 이럴 때 동적 라우트를 사용한다. 동적 라우트는 URL의 일부가 고정되어 있지 않고 값으로 들어오는 경로다.

SvelteKit에서는 폴더 이름을 대괄호로 감싸면 동적 경로가 된다.

src/routes/products/[id]/+page.svelte

이 파일은 다음 주소들을 모두 처리한다.

/products/1
/products/2
/products/keyboard
/products/anything

[id]는 "이 위치에 들어온 값을 id라는 이름으로 다루겠다"는 뜻이다. 페이지 안에서 현재 URL의 파라미터를 읽으려면 SvelteKit이 제공하는 page 객체를 사용할 수 있다.

<!-- src/routes/products/[id]/+page.svelte -->
<script>
  import { page } from '$app/state';

  let productId = $derived(page.params.id);
</script>

<h2>상품 상세</h2>
<p>현재 상품 ID: {productId}</p>

/products/1로 들어오면 productId는 1이 되고, /products/2로 들어오면 2가 된다. 이 값은 숫자가 아니라 URL에서 온 문자열이라는 점에 주의하자. 실제 상품 데이터를 찾을 때는 필요한 경우 숫자로 변환하거나, 문자열 ID 그대로 비교해야 한다.

이 장에서는 동적 경로를 만드는 데 집중한다. 상품 ID로 서버에서 데이터를 가져오거나, 없는 상품일 때 404를 보내는 방식은 다음 장의 데이터 로딩에서 더 정확히 다룬다.

+layout.svelte로 공통 UI 분리하기

페이지가 여러 개 생기면 곧 중복이 생긴다. 모든 페이지 위에 같은 헤더가 있고, 아래에 같은 푸터가 있다면 각 +page.svelte마다 이 코드를 반복하고 싶지 않을 것이다.

나쁜 구조는 다음과 같다.

<header>...</header>
<main>각 페이지 내용</main>
<footer>...</footer>

이 코드를 모든 페이지에 복사하면, 메뉴 이름을 하나 바꿀 때마다 여러 파일을 수정해야 한다. 이 문제를 해결하는 파일이 +layout.svelte다.

+layout.svelte는 같은 폴더와 그 아래 경로의 페이지를 감싸는 공통 틀이다. 최상위에 만들면 앱 전체에 적용된다.

src/routes/
  +layout.svelte
  +page.svelte
  about/
    +page.svelte
  products/
    +page.svelte
    [id]/
      +page.svelte

이제 src/routes/+layout.svelte를 만든다.

<!-- src/routes/+layout.svelte -->
<script>
  let { children } = $props();
</script>

<header class="site-header">
  <a href="/" class="logo">Svelte Shop</a>

  <nav>
    <a href="/">홈</a>
    <a href="/about">소개</a>
    <a href="/products">상품</a>
  </nav>
</header>

<main class="content">
  {@render children()}
</main>

<footer class="site-footer">
  <small>© 2026 Svelte Shop</small>
</footer>

Svelte 5 기반의 레이아웃에서는 자식 페이지 내용을 children으로 받고, {@render children()} 위치에 렌더링한다2. 즉 위 코드에서 각 페이지의 내용은 <main> 안에 들어간다.

예를 들어 /about으로 이동하면 전체 구조는 개념적으로 이렇게 된다.

<header>공통 헤더</header>
<main>
  <!-- about/+page.svelte 내용 -->
</main>
<footer>공통 푸터</footer>

/products로 이동하면 <main> 안의 내용만 상품 목록 페이지로 바뀐다. 헤더와 푸터는 +layout.svelte에 한 번만 작성했기 때문에 모든 페이지에서 공유된다.

레이아웃도 폴더별로 나눌 수 있다

최상위 +layout.svelte는 앱 전체를 감싼다. 그런데 특정 구역에만 필요한 레이아웃도 있다. 예를 들어 상품 페이지들에만 왼쪽 카테고리 메뉴를 보여 주고 싶다면 products 폴더 안에 +layout.svelte를 만들 수 있다.

src/routes/
  +layout.svelte
  products/
    +layout.svelte
    +page.svelte
    [id]/
      +page.svelte
<!-- src/routes/products/+layout.svelte -->
<script>
  let { children } = $props();
</script>

<div class="products-layout">
  <aside>
    <h3>상품 카테고리</h3>
    <a href="/products">전체</a>
  </aside>

  <section>
    {@render children()}
  </section>
</div>

이 레이아웃은 /products와 /products/1 같은 하위 경로에만 적용된다. /about에는 적용되지 않는다.

레이아웃은 겹쳐서 적용될 수 있다. /products/1 페이지는 먼저 최상위 src/routes/+layout.svelte의 헤더와 푸터 안에 들어가고, 그 안에서 다시 src/routes/products/+layout.svelte의 상품 전용 구조 안에 들어간다.

처음부터 레이아웃을 많이 나눌 필요는 없다. 중복이 눈에 보일 때 분리하면 된다. 기준은 단순하다.

  • 모든 페이지에 필요하면 src/routes/+layout.svelte

  • 특정 구역에만 필요하면 그 폴더의 +layout.svelte

  • 한 페이지에서만 필요하면 해당 +page.svelte 안에 작성

+error.svelte는 라우트의 오류 화면이다

웹사이트에는 정상 화면만 있는 것이 아니다. 없는 주소로 들어올 수도 있고, 데이터를 불러오다가 문제가 생길 수도 있다. 이때 보여 줄 오류 화면을 담당하는 파일이 +error.svelte다.

가장 기본적인 오류 화면은 최상위에 만들 수 있다.

src/routes/
  +layout.svelte
  +error.svelte
  +page.svelte
<!-- src/routes/+error.svelte -->
<script>
  import { page } from '$app/state';
</script>

<section class="error-page">
  <h2>{page.status}</h2>
  <p>{page.error?.message ?? '페이지를 표시할 수 없습니다.'}</p>
  <a href="/">홈으로 돌아가기</a>
</section>

page.status는 404, 500 같은 HTTP 상태 코드다. page.error.message는 사용자에게 보여 줄 오류 메시지다. 사용자가 존재하지 않는 페이지에 들어오거나, 페이지 데이터를 준비하는 과정에서 오류가 발생하면 SvelteKit은 가장 가까운 +error.svelte를 찾아 보여 준다3.

여기서 "가장 가까운"이라는 말도 레이아웃과 비슷하게 폴더 구조를 기준으로 이해하면 된다. 예를 들어 상품 구역에만 다른 오류 화면을 보여 주고 싶다면 src/routes/products/+error.svelte를 둘 수 있다. 그러면 /products 아래에서 발생한 라우트 오류는 상품 구역의 오류 화면을 먼저 사용하고, 그 파일이 없으면 상위 폴더의 오류 화면을 찾아 올라간다.

단, +error.svelte를 모든 작은 실수에 쓰지는 않는다. 입력 폼에서 "이름을 입력하세요" 같은 검증 메시지는 페이지 안의 상태로 보여 주는 편이 자연스럽다. +error.svelte는 페이지 자체를 정상적으로 보여 줄 수 없을 때 사용하는 라우트 단위 오류 화면이라고 이해하면 된다.

$lib alias로 공통 코드를 깔끔하게 가져오기

페이지가 늘어나면 공통 컴포넌트도 늘어난다. 헤더, 푸터, 상품 카드, 가격 포맷 함수 같은 것들은 여러 페이지에서 재사용된다. 이런 파일은 보통 src/lib에 둔다.

src/lib/
  components/
    SiteHeader.svelte
    SiteFooter.svelte
    ProductCard.svelte
  utils/
    formatPrice.js

src/lib에 있는 파일은 $lib alias로 가져올 수 있다.

<script>
  import SiteHeader from '$lib/components/SiteHeader.svelte';
  import SiteFooter from '$lib/components/SiteFooter.svelte';
</script>

상대 경로로 쓰면 이렇게 길어질 수 있다.

import ProductCard from '../../../lib/components/ProductCard.svelte';

파일 위치가 깊어질수록 ../가 많아지고, 나중에 파일을 옮길 때 깨지기 쉽다. $lib를 사용하면 현재 파일이 어디에 있든 src/lib를 기준으로 가져올 수 있다.

예를 들어 가격 포맷 함수를 만든다.

// src/lib/utils/formatPrice.js
export function formatPrice(value) {
  return `${value.toLocaleString()}원`;
}

상품 목록 페이지에서는 이렇게 사용한다.

<script>
  import { formatPrice } from '$lib/utils/formatPrice.js';

  const products = [
    { id: 1, name: '기계식 키보드', price: 89000 },
    { id: 2, name: '무선 마우스', price: 42000 }
  ];
</script>

<ul>
  {#each products as product}
    <li>
      <a href={`/products/${product.id}`}>
        {product.name} - {formatPrice(product.price)}
      </a>
    </li>
  {/each}
</ul>

$lib는 URL 경로가 아니다. 브라우저 주소창의 /lib와도 관계가 없다. 개발할 때 모듈을 가져오기 쉽게 해 주는 import 별칭이다.

작은 사이트 구조로 정리하기

지금까지 만든 구조를 하나로 모으면 다음과 같다.

src/
  lib/
    components/
      SiteHeader.svelte
      SiteFooter.svelte
      ProductCard.svelte
    utils/
      formatPrice.js
  routes/
    +layout.svelte
    +error.svelte
    +page.svelte
    about/
      +page.svelte
    products/
      +layout.svelte
      +page.svelte
      [id]/
        +page.svelte

이 구조만으로도 여러 페이지를 가진 작은 웹사이트를 만들 수 있다.

  • /는 첫 화면이다.

  • /about은 소개 페이지다.

  • /products는 상품 목록 페이지다.

  • /products/1은 동적 상품 상세 페이지다.

  • +layout.svelte는 공통 헤더와 푸터를 담당한다.

  • products/+layout.svelte는 상품 구역에만 필요한 구조를 담당한다.

  • +error.svelte는 라우트 오류 화면을 담당한다.

  • $lib는 공통 컴포넌트와 유틸 함수를 깔끔하게 가져오게 해 준다.

SvelteKit 라우팅의 핵심은 URL을 코드로 억지로 조립하지 않는다는 점이다. 폴더 구조를 페이지 구조와 비슷하게 만들면 된다. URL이 앱의 뼈대라면, +page.svelte는 각 방의 내용이고, +layout.svelte는 방들을 감싸는 복도와 공통 시설이다.

다음 장에서는 이 페이지들이 실제 데이터를 어떻게 가져오는지 배운다. 지금은 상품 ID를 URL에서 읽기만 했다. 다음에는 그 ID로 데이터를 찾고, 서버에서 필요한 정보를 준비하고, 페이지에 안전하게 전달하는 흐름을 다룰 것이다.

참고

1SvelteKit Docs: Routing

2Svelte Tutorial: Layouts

3SvelteKit Docs: Errors

책 목록

쉽게 배우는 Svelte 개발: Svelte 5와 SvelteKit으로 시작하는 프론트엔드

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기