Skip to main content
Summary

커질수록 중요한 것은 고치기 쉬운 코드다

작은 예제에서는 화면이 잘 보이면 충분해 보인다. 버튼을 누르면 값이 바뀌고, 폼을 제출하면 목록에 항목이 추가된다. 하지만 프로젝트가 커지면 문제가 달라진다.

  • props 이름을 바꿨는데 다른 컴포넌트가 깨진다.

  • API 응답 형태가 바뀌었는데 화면에서는 늦게 발견된다.

  • 장바구니 계산 로직을 고쳤더니 할인 금액이 틀어진다.

  • 로그인 폼은 되는데 회원가입 폼은 새로고침 후 오류가 사라진다.

이때 필요한 도구가 TypeScript, 검사 도구, 테스트, 디버깅 습관이다. 이 장의 목표는 "완벽한 코드"를 만드는 것이 아니다. 실수했을 때 빨리 발견하고, 고칠 때 덜 불안한 구조를 만드는 것이다.

TypeScript, 테스트, 디버깅으로 안정성 높이기AI 생성 이미지

Svelte 컴포넌트에 TypeScript 적용하기

Svelte 컴포넌트에서 TypeScript를 쓰려면 <script>에 lang="ts"를 붙인다.

<script lang="ts">
  let count = $state(0);

  function increase() {
    count += 1;
  }
</script>

<button onclick={increase}>클릭 {count}회</button>

이 코드만 보면 JavaScript와 거의 같다. 차이는 값을 더 명확하게 설명할 수 있다는 점이다.

<script lang="ts">
  let count: number = $state(0);
  let message: string = $state('안녕하세요');
</script>

항상 모든 변수에 타입을 직접 붙일 필요는 없다. 0을 넣으면 TypeScript는 count가 숫자라고 추론할 수 있다. 하지만 props처럼 컴포넌트 바깥에서 들어오는 값은 타입을 분명히 적어 두는 편이 좋다.

상품 카드 컴포넌트를 예로 보자.

<script lang="ts">
  type Props = {
    title: string;
    price: number;
    imageUrl?: string;
    onAdd?: () => void;
  };

  let {
    title,
    price,
    imageUrl = '/placeholder.png',
    onAdd = () => {}
  }: Props = $props();
</script>

<article class="card">
  <img src={imageUrl} alt={title} />
  <h2>{title}</h2>
  <p>{price.toLocaleString()}원</p>
  <button type="button" onclick={onAdd}>담기</button>
</article>

여기서 title은 문자열, price는 숫자여야 한다. imageUrl과 onAdd 뒤에 붙은 ?는 선택 값이라는 뜻이다. 부모가 넘기지 않아도 컴포넌트가 동작할 수 있다. 대신 기본값을 함께 적어 두면, 값이 없을 때도 이미지와 버튼 동작이 어색하게 깨지지 않는다.

이제 부모 컴포넌트에서 실수로 price="10000"처럼 문자열을 넘기면 타입 오류를 확인할 수 있다.

<ProductCard title="키보드" price="10000" />

화면에서는 비슷하게 보일 수도 있지만, 계산을 시작하면 문제가 생긴다. TypeScript는 이런 실수를 실행 전에 알려준다. 이것이 타입의 가장 큰 장점이다.

타입은 코드를 무겁게 만드는 장식이 아니다. 컴포넌트가 어떤 값을 필요로 하는지 적어 둔 작은 문서다.

npx sv check로 프로젝트 전체 확인하기

에디터가 타입 오류를 밑줄로 알려 주더라도, 명령줄 검사는 따로 필요하다. 내 화면에서는 괜찮아 보여도 팀원의 환경이나 배포 환경에서 문제가 드러날 수 있기 때문이다.

Svelte 프로젝트에서는 다음 명령으로 타입 오류와 Svelte 문법 문제를 확인할 수 있다.

npx sv check

sv check는 Svelte CLI에서 제공하는 검사 명령이고, 실제 진단에는 svelte-check 패키지가 필요하다. 최근 SvelteKit 프로젝트라면 보통 준비되어 있지만, 없다면 개발 의존성으로 설치한 뒤 사용하면 된다.

npm i -D svelte-check

또는 프로젝트에 이미 스크립트가 준비되어 있다면 다음처럼 실행한다.

npm run check

검사 도구는 대략 이런 문제를 잡아 준다.

<script lang="ts">
  type Props = {
    count: number;
  };

  let { count }: Props = $props();
</script>

<p>{count.toUpperCase()}</p>

count는 숫자인데 toUpperCase()는 문자열 메서드다. 브라우저에서 사용자가 해당 화면을 보기 전까지 기다릴 필요가 없다. 검사 명령을 실행하면 바로 알 수 있다.

작업 흐름은 단순하게 가져가면 된다.

npm run check
npm run test
npm run build

프로젝트에 따라 테스트 스크립트 이름이 test:unit, test:e2e처럼 나뉘어 있을 수도 있다. 이름은 달라도 목표는 같다. 배포 전에 타입 검사, 테스트, 빌드를 통과시키는 습관이 생기면 수정이 훨씬 안전해진다.

ESLint와 Prettier로 코드 스타일 맞추기

TypeScript와 sv check가 "코드가 맞는가"에 가깝다면, ESLint와 Prettier는 "팀이 같은 방식으로 쓰고 있는가"를 도와준다.

Prettier는 코드 모양을 자동으로 정리한다.

<button     onclick={save}>저장</button>

저장 후에는 다음처럼 일관된 모양이 된다.

<button onclick={save}>저장</button>

ESLint는 단순한 모양보다 규칙에 가깝다. 예를 들어 사용하지 않는 변수, 실수하기 쉬운 Svelte 문법, 보안상 조심해야 하는 HTML 작성 방식 등을 잡는 데 도움을 준다. Svelte용 ESLint 규칙에는 {@html} 사용 주의, target="_blank"의 보안 속성, keyed each 권장, 버튼 타입 명시 같은 실무적인 규칙들이 포함된다.1

팀 프로젝트에서는 "나는 이렇게 쓰는 게 보기 좋다"라는 논쟁이 자주 생긴다. ESLint와 Prettier를 쓰면 논쟁을 줄일 수 있다. 규칙은 도구에 맡기고, 사람은 구조와 사용자 경험에 집중한다.

보통은 다음 명령을 스크립트로 둔다.

{
  "scripts": {
    "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
    "lint": "prettier --check . && eslint .",
    "format": "prettier --write ."
  }
}

check 스크립트는 기존 SvelteKit 프로젝트에서 자주 볼 수 있는 형태다. 새 프로젝트에서는 npx sv check를 직접 쓰거나, 같은 명령을 check 스크립트에 연결해도 된다.

정리 기준은 간단하다.

  • 저장할 때 format으로 모양을 맞춘다.

  • 커밋 전 lint로 규칙 위반을 확인한다.

  • 배포 전 check, test, build를 실행한다.

도구가 많아 보이지만 역할은 다르다. Prettier는 모양, ESLint는 습관과 규칙, TypeScript와 Svelte check는 타입과 문법을 맡는다.

Vitest로 작은 로직 테스트하기

테스트는 모든 화면을 클릭해 보는 것부터 시작하지 않는다. 먼저 순수 함수와 store처럼 작은 단위를 확인하는 편이 쉽다.

가격 포맷 함수를 만들어 보자.

// src/lib/formatPrice.ts
export function formatPrice(price: number) {
  if (price < 0) {
    throw new Error('가격은 0보다 작을 수 없습니다.');
  }

  return `${price.toLocaleString()}원`;
}

Vitest 테스트는 다음처럼 작성한다.

// src/lib/formatPrice.test.ts
import { describe, expect, it } from 'vitest';
import { formatPrice } from './formatPrice';

describe('formatPrice', () => {
  it('숫자를 원화 문자열로 바꾼다', () => {
    expect(formatPrice(12000)).toBe('12,000원');
  });

  it('음수 가격은 오류로 처리한다', () => {
    expect(() => formatPrice(-1)).toThrow();
  });
});

실행은 보통 다음 명령으로 한다.

npm run test

또는 직접 실행할 수도 있다.

npx vitest

store도 테스트할 수 있다. 다만 전역 store 하나를 그대로 테스트하면 이전 테스트의 상태가 다음 테스트에 남을 수 있다. 그래서 store를 만드는 함수를 따로 두면 안전하다.

// src/lib/cart.ts
import { writable } from 'svelte/store';

export type CartItem = {
  id: number;
  name: string;
  price: number;
  quantity: number;
};

export function createCart() {
  const { subscribe, update, set } = writable<CartItem[]>([]);

  return {
    subscribe,
    add(item: Omit<CartItem, 'quantity'>) {
      update((items) => {
        const found = items.find((x) => x.id === item.id);

        if (found) {
          return items.map((x) =>
            x.id === item.id ? { ...x, quantity: x.quantity + 1 } : x
          );
        }

        return [...items, { ...item, quantity: 1 }];
      });
    },
    clear() {
      set([]);
    }
  };
}
// src/lib/cart.test.ts
import { describe, expect, it } from 'vitest';
import { get } from 'svelte/store';
import { createCart } from './cart';

describe('cart', () => {
  it('같은 상품을 두 번 담으면 수량이 증가한다', () => {
    const cart = createCart();

    cart.add({ id: 1, name: '마우스', price: 30000 });
    cart.add({ id: 1, name: '마우스', price: 30000 });

    expect(get(cart)).toEqual([
      { id: 1, name: '마우스', price: 30000, quantity: 2 }
    ]);
  });
});

이런 테스트는 빠르다. 브라우저를 열지 않고도 장바구니의 핵심 규칙이 맞는지 확인한다. 실무에서는 UI 테스트보다 이런 작은 테스트가 버그를 더 빨리 알려 줄 때가 많다.

Playwright로 사용자 흐름 검증하기

Vitest가 작은 로직을 확인한다면, Playwright는 실제 사용자의 흐름을 확인한다. 브라우저를 열고, 페이지로 이동하고, 버튼을 누르고, 결과가 보이는지 검사한다. Playwright는 Chromium, Firefox, WebKit 같은 여러 브라우저 자동화를 지원하고, 사용자에게 보이는 요소를 기준으로 찾는 locator와 자동 대기 기능을 제공한다.2

장바구니 흐름을 테스트한다면 다음처럼 쓸 수 있다.

// tests/cart.spec.ts
import { expect, test } from '@playwright/test';

test('상품을 장바구니에 담는다', async ({ page }) => {
  await page.goto('/products');

  await page
    .getByRole('button', { name: '장바구니 담기' })
    .first()
    .click();

  await expect(page.getByText('장바구니 1')).toBeVisible();
});

page.goto('/products')처럼 상대 경로를 쓰려면 Playwright 설정에 baseURL이 잡혀 있어야 한다. SvelteKit용 테스트 설정에서는 개발 서버 주소를 baseURL로 두는 경우가 많다. 설정이 없다면 http://localhost:5173/products처럼 전체 주소를 써야 한다.

여기서 중요한 점은 CSS 클래스보다 사용자가 인식하는 이름으로 요소를 찾는 것이다. getByRole('button', { name: '장바구니 담기' })는 버튼의 역할과 이름을 기준으로 찾는다. 테스트가 접근성과 함께 좋아진다.

Playwright 테스트는 모든 세부 사항을 확인하는 데 쓰기보다, 중요한 사용자 흐름을 지키는 데 쓰는 편이 좋다.

  • 회원가입할 수 있는가?

  • 로그인 후 내 정보가 보이는가?

  • 상품을 장바구니에 담고 결제 페이지로 갈 수 있는가?

  • 폼 오류가 사용자에게 다시 표시되는가?

작은 로직은 Vitest, 실제 흐름은 Playwright로 나누면 테스트 부담이 줄어든다.

@debug와 개발자 도구로 문제 추적하기

반응성 문제는 눈으로만 보면 헷갈릴 때가 많다. 값은 바뀐 것 같은데 화면이 그대로이거나, $derived 값이 예상과 다르게 계산될 수 있다. 이때 Svelte의 @debug 태그를 잠깐 사용할 수 있다.

<script lang="ts">
  let count = $state(0);
  let doubled = $derived(count * 2);
</script>

{@debug count, doubled}

<button onclick={() => count += 1}>증가</button>
<p>{count} / {doubled}</p>

개발 중 브라우저 콘솔에서 값 변화를 확인할 수 있다. 단, @debug는 임시 도구다. 문제를 찾은 뒤에는 삭제하는 습관을 들이자.

브라우저 개발자 도구도 함께 써야 한다.

  • Console: JavaScript 오류, console.log, @debug 결과를 확인한다.

  • Elements: 실제 DOM 구조와 CSS 적용 상태를 본다.

  • Network: API 요청 URL, 상태 코드, 요청 본문, 응답 데이터를 확인한다.

  • Application: 쿠키, localStorage, sessionStorage 값을 확인한다.

특히 SvelteKit에서 폼 액션이나 API 요청이 예상대로 동작하지 않을 때는 Network 탭이 중요하다. 다음 순서로 확인하면 좋다.

  1. 요청이 실제로 발생했는가?

  2. URL과 HTTP 메서드가 맞는가?

  3. 요청 본문에 입력값이 들어 있는가?

  4. 응답 상태 코드가 200, 303, 400, 422, 500 중 무엇인가?

  5. 서버가 돌려준 JSON이나 HTML에 오류 메시지가 포함되어 있는가?

  6. 쿠키가 설정되거나 삭제되었는가?

화면만 보면 "로그인이 안 된다"로 보이지만, Network 탭에서는 원인이 더 구체적으로 보인다. 쿠키가 안 내려왔는지, 리다이렉트가 잘못됐는지, 서버 검증에서 실패했는지 나눠 볼 수 있다.

안정성을 높이는 기본 루틴

마지막으로 실무에서 반복할 수 있는 루틴을 정리하자.

npm run format
npm run lint
npm run check
npm run test
npm run build

매번 모든 명령을 손으로 실행할 필요는 없다. 저장 시 포맷, 커밋 전 lint, 배포 전 check와 test처럼 단계별로 나눠도 된다. 중요한 것은 오류를 늦게 발견하지 않는 것이다.

Svelte는 작은 코드로 화면을 빠르게 만들 수 있게 도와준다. 하지만 프로젝트가 커질수록 속도만으로는 부족하다. TypeScript는 값의 모양을 지켜 주고, check와 lint는 실수를 줄이며, 테스트는 중요한 동작을 보호하고, 디버깅 도구는 문제가 생겼을 때 원인을 좁혀 준다.

동작하는 코드를 만드는 것에서 한 걸음 더 나아가자. 고치기 쉬운 코드를 만드는 것이 오래 가는 Svelte 프로젝트의 기준이다.

참고

1eslint-plugin-svelte Available Rules

책 목록

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

전체 목차 보기

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

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

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

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

참여하기