조건, 반복, 이벤트로 동적인 화면 만들기
동적인 화면은 상태에서 시작한다
정적인 화면은 항상 같은 내용을 보여 준다. 동적인 화면은 다르다. 사용자가 버튼을 누르거나, 입력창에 글자를 쓰거나, 데이터가 비어 있거나, 로그인 상태가 바뀌면 화면도 함께 바뀐다.
Svelte에서 이런 흐름은 보통 세 가지 문법으로 표현한다.
조건: 지금 어떤 화면을 보여 줄지 결정한다.
반복: 배열 데이터를 목록으로 펼친다.
이벤트: 사용자의 행동을 받아 상태를 바꾼다.
짧은 로그인 상태 예제로 먼저 감을 잡아 보자.
<script>
let user = $state(null);
let error = $state('');
function login() {
user = { name: '지민' };
error = '';
}
function failLogin() {
user = null;
error = '아이디 또는 비밀번호를 확인하세요.';
}
function logout() {
user = null;
error = '';
}
</script>
{#if error}
<p class='error'>{error}</p>
{:else if user}
<p>{user.name}님, 환영합니다.</p>
<button onclick={logout}>로그아웃</button>
{:else}
<p>로그인이 필요합니다.</p>
<button onclick={login}>로그인</button>
<button onclick={failLogin}>실패 예시</button>
{/if}{#if}는 조건이 참일 때만 해당 마크업을 보여 준다. {:else if}는 앞 조건이 거짓일 때 다음 조건을 검사한다. {:else}는 어떤 조건도 만족하지 않을 때 보여 줄 기본 화면이다.
여기서 중요한 점은 DOM을 직접 숨기거나 지우지 않는다는 것이다. 우리는 user와 error 상태만 바꾼다. 그러면 Svelte가 현재 상태에 맞는 마크업을 판단하고, 실제로 바뀌어야 하는 부분만 갱신한다.
AI 생성 이미지
빈 목록도 조건이다
웹 앱에는 목록이 많다. 할 일 목록, 상품 목록, 댓글 목록, 장바구니 목록이 모두 배열 데이터에서 출발한다. Svelte에서는 {#each} 블록으로 배열을 화면에 펼친다.
<script>
let books = $state([
{ id: 1, title: 'Svelte 시작하기' },
{ id: 2, title: '컴포넌트 설계 연습' }
]);
</script>
<ul>
{#each books as book, index}
<li>{index + 1}. {book.title}</li>
{/each}
</ul>books as book은 배열의 각 항목을 하나씩 book이라는 이름으로 꺼내겠다는 뜻이다. index는 현재 항목의 순서다. 화면에는 0부터 시작하는 값보다 1부터 시작하는 번호가 자연스러우므로 {index + 1}처럼 표시했다.
목록이 비었을 때도 처리해야 한다. 별도의 {#if}를 써도 되지만, {#each}에는 빈 목록을 위한 {:else}가 있다.
<ul>
{#each books as book}
<li>{book.title}</li>
{:else}
<li>아직 등록한 책이 없습니다.</li>
{/each}
</ul>이 패턴은 실무에서 자주 쓴다. 데이터를 불러오기 전, 검색 결과가 없을 때, 장바구니가 비었을 때 모두 같은 방식으로 빈 상태 메시지를 보여 줄 수 있다.
keyed each로 안정적인 목록 만들기
목록을 단순히 보여 주기만 한다면 {#each books as book}으로도 충분해 보인다. 하지만 항목을 추가하거나 삭제하거나 순서를 바꾸는 화면에서는 각 항목을 구분할 안정적인 기준이 필요하다.
이때 사용하는 것이 keyed each다.
<ul>
{#each books as book (book.id)}
<li>{book.title}</li>
{/each}
</ul>마지막의 (book.id)가 key다. key는 이 항목이 누구인지 알려 주는 고유한 값이다. Svelte는 이 key를 보고 어떤 항목이 추가됐고, 어떤 항목이 삭제됐고, 어떤 항목이 이동했는지 더 안정적으로 판단한다. Svelte 문서에서도 key는 각 항목을 유일하게 식별해야 하며, 문자열이나 숫자를 권장한다고 설명한다1.
가능하면 배열의 순서인 index를 key로 쓰지 않는 편이 좋다. 항목이 중간에서 삭제되면 뒤에 있던 항목들의 index가 모두 바뀐다. 그러면 Svelte가 항목의 정체성을 헷갈릴 수 있다. 서버에서 받은 id가 있으면 그것을 쓰고, 직접 만든 데이터라면 새 항목을 추가할 때 고유한 id를 함께 만들어 주자.
이벤트는 화면에서 상태로 들어오는 통로다
조건과 반복은 상태를 화면으로 보내는 문법이다. 이벤트는 반대로 사용자의 행동을 상태로 가져오는 통로다.
Svelte 5에서는 DOM 이벤트를 onclick, oninput, onsubmit처럼 on으로 시작하는 속성으로 연결한다. 예전 Svelte 자료에서는 on:click 문법을 볼 수 있는데, 이 책에서는 Svelte 5 방식인 onclick을 기준으로 설명한다. 이벤트 속성은 대소문자를 구분하므로 일반 클릭 이벤트에는 onclick처럼 소문자로 써야 한다2.
가장 작은 예시는 버튼 클릭이다.
<script>
let count = $state(0);
</script>
<button onclick={() => count += 1}>
{count}번 클릭
</button>버튼을 누르면 화살표 함수가 실행되고 count가 1 증가한다. count는 $state로 만든 상태이므로, 값이 바뀌면 버튼 안의 {count}도 함께 갱신된다.
입력 이벤트는 사용자가 타이핑할 때 발생한다.
<script>
let keyword = $state('');
</script>
<input
value={keyword}
oninput={(event) => keyword = event.currentTarget.value}
/>
<p>검색어: {keyword}</p>여기서는 입력창의 현재 값을 keyword에 넣었다. 다음 장에서 배울 바인딩을 사용하면 이 코드를 더 짧게 만들 수 있지만, 지금은 이벤트가 상태를 바꾸는 흐름을 직접 보는 것이 중요하다.
폼 제출은 조금 다르다. 브라우저의 기본 폼 제출 동작은 페이지를 새로 불러올 수 있다. Svelte 앱에서는 보통 이 기본 동작을 막고, JavaScript 함수 안에서 상태를 바꾼다.
<script>
let title = $state('');
function submit(event) {
event.preventDefault();
console.log(title);
}
</script>
<form onsubmit={submit}>
<input value={title} oninput={(event) => title = event.currentTarget.value} />
<button type='submit'>저장</button>
</form>event.preventDefault()는 브라우저의 기본 제출 동작을 막는다. 그다음 필요한 검증, 저장, 상태 변경을 함수 안에서 처리한다.
inline handler와 함수 handler 선택하기
이벤트 핸들러는 두 방식으로 자주 쓴다.
첫째, 짧은 동작은 inline handler로 바로 쓴다.
<button onclick={() => count += 1}>증가</button>
<button onclick={() => filter = 'done'}>완료만 보기</button>한 줄로 읽히고 의미가 분명하다면 inline handler가 편하다.
둘째, 여러 단계가 있거나 이름을 붙이고 싶다면 별도 함수로 뺀다.
<script>
function addBook(event) {
event.preventDefault();
const title = newTitle.trim();
if (!title) return;
books.push({ id: nextId, title, done: false });
nextId += 1;
newTitle = '';
}
</script>
<form onsubmit={addBook}>...</form>검증이 있거나, 여러 상태를 바꾸거나, 같은 동작을 여러 곳에서 재사용한다면 함수 이름을 붙이는 편이 읽기 쉽다. addBook이라는 이름만 봐도 이 함수가 무엇을 하는지 알 수 있다.
주의할 점도 있다.
<!-- 잘못된 예: 렌더링 중에 바로 실행된다 -->
<button onclick={removeBook(book.id)}>삭제</button>
<!-- 올바른 예: 클릭했을 때 실행된다 -->
<button onclick={() => removeBook(book.id)}>삭제</button>인자가 필요한 함수를 이벤트에 연결할 때는 화살표 함수로 감싸야 한다. 그래야 컴포넌트가 그려지는 순간이 아니라, 버튼을 클릭하는 순간에 실행된다.
작은 CRUD 예제: 읽을 책 목록
이제 조건, 반복, 이벤트를 합쳐 작은 예제를 만들어 보자. 읽을 책을 추가하고, 읽음 처리하고, 삭제하는 목록이다. CRUD라는 말은 만들기 Create, 읽기 Read, 수정하기 Update, 삭제하기 Delete를 묶어 부르는 표현이다. 대부분의 앱 화면은 이 네 가지 동작의 조합으로 만들어진다. MDN의 Svelte 할 일 목록 예제도 이와 비슷하게 추가, 완료 처리, 삭제 흐름을 중심으로 동적인 UI를 설명한다3.
<script>
let nextId = 3;
let newTitle = $state('');
let error = $state('');
let books = $state([
{ id: 1, title: 'Svelte 시작하기', done: true },
{ id: 2, title: '상품 카드 만들기', done: false }
]);
let doneCount = $derived(
books.filter((book) => book.done).length
);
function addBook(event) {
event.preventDefault();
const title = newTitle.trim();
if (!title) {
error = '책 제목을 입력하세요.';
return;
}
books.push({
id: nextId,
title,
done: false
});
nextId += 1;
newTitle = '';
error = '';
}
function toggleBook(book) {
book.done = !book.done;
}
function removeBook(id) {
books = books.filter((book) => book.id !== id);
}
</script>
<form onsubmit={addBook}>
<input
value={newTitle}
oninput={(event) => newTitle = event.currentTarget.value}
placeholder='읽을 책 제목'
/>
<button type='submit'>추가</button>
</form>
{#if error}
<p class='error'>{error}</p>
{/if}
{#if books.length > 0}
<p>{doneCount} / {books.length}권 읽음</p>
{/if}
<ul>
{#each books as book (book.id)}
<li>
<span class={book.done ? 'done' : ''}>{book.title}</span>
<button type='button' onclick={() => toggleBook(book)}>
{book.done ? '읽음 취소' : '읽음'}
</button>
<button type='button' onclick={() => removeBook(book.id)}>
삭제
</button>
</li>
{:else}
<li>아직 등록한 책이 없습니다.</li>
{/each}
</ul>
<style>
.done {
color: #777;
text-decoration: line-through;
}
.error {
color: crimson;
}
</style>이 예제에서 Create는 폼 제출로 새 책을 추가하는 부분이다. Read는 {#each}로 books 배열을 보여 주는 부분이다. Update는 book.done을 바꿔 읽음 상태를 토글하는 부분이다. Delete는 filter로 삭제할 항목을 제외한 새 배열을 만드는 부분이다.
doneCount는 $derived로 만들었다. 직접 값을 고치는 대신 books에서 계산한다. 책을 추가하거나 삭제하거나 읽음 상태를 바꾸면 books가 바뀌고, 그 결과 doneCount도 자동으로 다시 계산된다. 원본 상태는 books, 계산된 상태는 doneCount라는 구분이 분명하다.
또 하나 볼 점은 $state 배열이다. Svelte 5에서는 배열 안의 객체 속성을 바꾸는 book.done = !book.done 같은 코드도 화면 갱신으로 이어진다. 그래서 완료 처리처럼 작은 수정은 자연스러운 JavaScript 코드로 작성할 수 있다.
오래된 Svelte 자료를 읽을 때
조건문과 반복문은 오래된 자료를 봐도 크게 낯설지 않다. {#if}, {:else}, {#each} 같은 블록 문법은 계속 자주 등장한다.
차이가 크게 보이는 곳은 이벤트다. 예전 자료에는 다음과 같은 코드가 많다.
<button on:click={add}>추가</button>
<form on:submit|preventDefault={submit}>...</form>이 책에서 사용하는 Svelte 5 방식으로는 다음처럼 읽으면 된다.
<button onclick={add}>추가</button>
<form onsubmit={submit}>...</form>그리고 기본 제출 동작을 막아야 한다면 submit 함수 안에서 event.preventDefault()를 호출한다. 오래된 문법을 모두 외울 필요는 없다. 다만 검색하다가 on:click을 만나면 Svelte 5의 onclick과 비슷한 역할을 하던 문법이라고 이해하면 된다.
정리
조건, 반복, 이벤트는 Svelte 화면을 움직이게 만드는 기본 도구다.
{#if}는 상태에 따라 다른 메시지나 화면을 보여 준다. {#each}는 배열을 목록으로 렌더링하고, keyed each는 추가·삭제·정렬이 있는 목록을 안정적으로 다룬다. onclick, oninput, onsubmit 같은 이벤트는 사용자의 행동을 받아 상태를 바꾼다.
이 세 가지를 조합하면 이미 많은 UI를 만들 수 있다. 로그인 메시지, 빈 목록 안내, 상품 카드 목록, 장바구니 추가, 할 일 완료 처리도 같은 원리로 움직인다. 다음 장에서는 입력 폼을 더 편하게 다루는 바인딩을 배우면서, 입력값과 상태를 연결하는 코드를 한 단계 더 간단하게 만들어 보겠다.
참고
3MDN Dynamic behavior in Svelte: working with variables and props
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.