Next.js에서 Svelte로 이행한 이야기
자작 웹사이트
프로그래밍 학습과 셀프 브랜딩을 위해 개인 웹사이트를 직접 만들며 수많은 시행착오를 겪고 있습니다. AI가 범람하는 요즘, 개발자 스스로 손으로 웹사이트를 처음부터 끝까지 만드는 일은 점점 줄어들고 있습니다. 그러나 AI를 적극적으로 활용하더라도, 이를 감독하는 사람이 기본을 제대로 이해하지 못한다면 AI에게 정확한 요구를 전달하기 어렵다고 생각했습니다. 그래서 오히려 이런 시대일수록 직접 코드를 작성해 보고, AI에게 구체적이고 적절한 지시를 내릴 수 있는 역량을 기르는 것이 중요하다고 느꼈습니다.
처음에는 여기저기서 들은 이야기만 믿고 무작정 웹사이트를 만들어 보느라 시행착오가 많았습니다. 그럼에도 실제로 서비스를 배포하고 운영해 보면서 웹 서비스 개발의 전 과정을 몸으로 익힐 수 있었고, 매우 값진 경험이 되었습니다. 여가 시간에 코드를 작성하고, 개인 사비로 서버 비용을 감당해야 하는 프로젝트인 만큼 화려한 디자인이나 거대한 인프라를 갖추기는 어려웠습니다. 대신 한정된 자원 안에서 얼마나 효율적으로 시스템을 설계하고 구축할 수 있을지 고민하게 되었고, 자연스럽게 관련 공부에도 더 깊이 몰입하게 되었습니다.
Next.js로 개발 시작
처음에는 ‘개발 공부’ 그 자체도 중요한 목표였기 때문에, 많은 사람들에게 검증된 기술 스택을 중심으로 웹사이트를 만들고자 했습니다. 웹사이트 제작에 널리 사용되는 프레임워크인 Next.js를 선택하면, 관련 정보를 인터넷에서 쉽게 찾아볼 수 있을 것이라고 판단했습니다. 그렇게 별다른 망설임 없이 Next.js를 도입해 웹페이지 개발을 시작했습니다. ChatGPT를 비롯한 AI 도구와 구글 검색이 곁에 있었기에, 당시에는 두려울 것이 없다고 느꼈습니다.
하지만 시간이 지날수록 Next.js 기반 코드가 점점 복잡해진다는 인상을 받았습니다. 로직이 늘어나고 구조가 복잡해질수록 useMemo 같은 최적화 기법을 적절히 사용해야 했고, 기능 구현과 성능 최적화를 동시에 고려하는 일이 점점 큰 부담으로 다가왔습니다. 여기에 React와 Next.js가 빠른 속도로 새로운 기능을 추가하면서, 그 변화를 따라잡기 위해 들여야 하는 시간과 노력도 계속 늘어나는 느낌을 받았습니다. 물론 React 생태계에 대한 이해를 더 깊게 다진다면 충분히 극복 가능한 과제라고 생각했습니다. 그러나 직장에서의 업무가 바빠지며 여가 시간은 점점 줄어들었고, 구현하고 싶은 아이디어와 기능은 오히려 더 많아졌습니다. 결국 제가 투자할 수 있는 시간에는 분명한 한계가 있다는 사실을 뼈저리게 느끼게 되었습니다.
Svelte 영접
그러던 중 우연히 인터넷에서 정보를 찾아보다가 Svelte라는 프레임워크를 접하게 되었습니다. Svelte는 React처럼 Virtual DOM을 사용하는 대신, 컴파일 단계에서 효율적인 자바스크립트 코드로 변환해 준다는 점이 인상적이었습니다. 그 덕분에 프론트엔드 애플리케이션이 가벼워지고, 코드 역시 더 간결해질 수 있다는 설명이 특히 매력적으로 다가왔습니다.
이미 Next.js에 어느 정도 피로감을 느끼고 있던 터라, 자연스럽게 Svelte 공식 문서와 튜토리얼을 빠르게 읽어 내려갔습니다. React와 Next.js에 비해 제공되는 함수나 기능의 수는 확실히 적었지만, 오히려 그 단순함 덕분에 머릿속에 있는 아이디어를 구현하기에는 충분하다고 느꼈습니다. 결국 Next.js로 작성해 두었던 기존 코드베이스를 Svelte 기반으로 전환하기로 마음먹었고, 본격적으로 리팩터링 작업에 착수했습니다.
Svelte의 매력: 간결한 코드
최근 웹 개발에서는 React와 Next.js처럼 컴포넌트 기반 프레임워크가 표준처럼 사용되고 있습니다. 이런 도구들은 강력하지만, 상태 관리나 이벤트 처리만 하더라도 import, 훅(hook), 컴포넌트 함수 선언 등 상당한 양의 보일러플레이트 코드가 따라붙습니다. Svelte는 같은 일을 더 적은 코드와 더 직관적인 문법으로 해결하는 것을 목표로 합니다. 화면에서 보이는 숫자를 하나 증가시키는 간단한 카운터만 예로 들어도 차이가 드러납니다.
아래 예시는 완전히 같은 기능을 하는 카운터를 두 가지 방식으로 구현한 코드입니다. 위쪽은 React + Next.js 예시로, useState를 import하고, 컴포넌트 함수를 만들고, 상태를 업데이트하는 함수를 따로 정의해야 합니다. 아래쪽은 Svelte + SvelteKit 예시로, <script> 안에서 숫자 변수 하나(let count = $state(0))만 선언한 뒤, 버튼에 onclick 이벤트를 연결하고, 화면에는 {count}를 그대로 출력하면 끝입니다. 참고로 Next.js에서 Hook을 사용하려면 CSR 코드와 SSR 코드를 분리해서 따로 작성한 후, 나중에 합치는 식으로 설계를 해야 했지만, Svelte에서는 SSR에서도 Rune (React의 Hook과 유사한 개념)을 자유롭게 사용할 수 있기 때문에, 상태 관리를 위해서 보일러플레이트 코드를 길게 쓰지 않아도 되는 점이 상당히 좋았습니다.
간단한 카운터 (React + Next.js)
"use client"
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prev => prev + 1);
};
return (
<button onClick={handleClick}>
Count: {count}
</button>
);
} 간단한 카운터 (Svelte + SvelteKit)
<script>
let count = $state(0);
const handleClick = () => {
count += 1;
};
</script>
<button onclick={handleClick}>
Count: {count}
</button> 간단한 카운터 예시는 “숫자 하나를 어떻게 관리하느냐”를 보여주는 예제라면, 실제 서비스에서는 사용자의 입력에 따라 다른 값이 자동으로 바뀌는 경우가 훨씬 더 많습니다. 예를 들어, 이름 입력란에 “Sung-eon”이라고 적으면 바로 아래 인삿말이 Hello, Sung-eon!으로 바뀌는 화면을 떠올리면 이해하기 쉽습니다.
이때 화면에는 두 가지 값이 등장합니다. 하나는 사용자가 직접 입력하는 이름(name) 이고, 다른 하나는 그 이름을 바탕으로 만들어지는 인삿말(greeting) 입니다. 인삿말은 항상 이름에 의해 결정되므로, 이런 값을 흔히 파생 상태(derived state) 라고 부를 수 있습니다. “A라는 값이 바뀌면, B라는 값도 그에 맞게 자동으로 바뀌어야 한다”는 관계를 코드로 어떻게 표현하느냐가 관건입니다.
React/Next.js에서는 보통 이름을 useState로 관리하고, 인삿말은 useMemo나 렌더링 함수 안의 계산 로직으로 처리하게 됩니다. 기능적으로는 문제 없지만, 훅을 import하고, 의존성 배열을 관리하고, 입력 이벤트에서 event.target.value를 꺼내는 등 부수적인 코드가 꽤 많이 따라붙는 편입니다. 아래에서는 같은 기능을 Svelte로 구현했을 때 어떤 식으로 달라지는지, 그리고 Svelte 5에서는 $state와 $derived를 이용해 “이 값은 상태이고, 이 값은 그 상태에서 자동으로 계산된다”라는 관계를 더 직접적으로 표현할 수 있는지를 예제로 보여드리겠습니다.
상태 업데이트 (React + Next.js)
'use client';
import { useMemo, useState } from 'react';
export default function Greeting() {
const [name, setName] = useState('');
const greeting = useMemo(() => name ? `Hello, ${name}!` : 'Hello, world!', [name]);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
};
return (
<>
<input
value={name}
onChange={handleChange}
placeholder="Enter your name"
/>
<p>{greeting}</p>
</>
);
} 상태 업데이트 (Svelte + SvelteKit)
<script>
let name = $state('');
let greeting = $derived(name ? `Hello, ${name}!` : 'Hello, world!');
</script>
<input bind:value={name} placeholder="Enter your name" />
<p>{greeting}</p> Svelte 쪽 코드를 보시면, React 예제와 동일하게 “입력값에 따라 인삿말이 바뀐다”는 기능을 유지하면서도 훨씬 적은 요소만으로 같은 흐름을 표현하고 있습니다. 이름은 $state('') 한 줄로 상태임을 드러내고, 인삿말은 $derived(...)로 “이 값은 name에 의해 자동으로 계산된다”라고 선언만 해 두면 됩니다. 별도의 훅 import나 의존성 배열, 이벤트 객체에서 event.target.value를 꺼내는 보일러플레이트 없이도, 입력값과 파생 상태의 관계가 코드 레벨에서 바로 읽히기 때문에, 기능은 그대로지만 무엇이 언제, 무엇에 의해 바뀌는지를 이해하는 데 필요한 부담이 훨씬 줄어듭니다.
Svelte 이행의 장벽: 에코시스템
Svelte를 도입할 때 가장 많이 언급되는 약점 중 하나는 에코시스템의 규모입니다. React에 비해 참고할 수 있는 예제나 라이브러리, 커뮤니티 자료가 적다는 점이 큰 단점처럼 이야기되곤 합니다. 하지만 적어도 이번에 제가 진행한 개인 웹사이트 프로젝트 범위에서는, 이것이 크게 체감되는 단점이라고 느끼지는 않았습니다. Tiptap과 같은 WYSIWYG 에디터나 Auth.js와 같은 인증 라이브러리도 Svelte를 공식 지원하고 있어, “Svelte라서 쓸 수 없는 도구가 많다”는 인상은 생각보다 크지 않았습니다. 오히려 필요한 기능을 구현하는 데에 라이브러리 부족으로 막히는 경험은 거의 없었습니다.
다만, 이번 프로젝트는 어디까지나 제 개인 학습과 포트폴리오 제작을 위한 용도였기 때문에 이런 인상이 가능했다고도 생각합니다. 실제 서비스 운영 환경에서는 팀 내 개발자들의 숙련도, 사내 표준 스택, 장기적인 유지보수 관점에서 Svelte의 에코시스템 규모가 어느 정도 제약으로 다가올지 아직은 쉽게 상상하기 어렵습니다. 현재로서는 “개인 프로젝트와 실험적인 용도에서는 충분히 매력적이지만, 기업 현장에서의 도입은 조금 더 신중한 검토가 필요하다” 정도로 정리할 수 있을 것 같습니다.
마무리
이번 개인 웹사이트 프로젝트를 통해 느낀 점은, 결국 중요한 것은 “React냐 Svelte냐”와 같은 기술 선택 그 자체보다, 도구를 직접 써 보고 장단점을 몸으로 이해하는 과정이었다는 점입니다. Next.js를 사용하면서는 복잡한 구조와 빠르게 변하는 생태계 속에서 시간의 한계를 실감했고, Svelte로 옮기면서는 더 간결한 문법과 가벼운 코드가 코드 작성의 효율화를 체험할 수 있었습니다.
물론, Svelte가 만능의 정답이라고 생각하지는 않습니다. 현업 환경에서는 팀 구성, 기존 코드베이스, 채용 시장, 장기 유지보수 등 개인 프로젝트에서는 마주치지 않는 제약 조건들이 많습니다. 다만 이번 경험을 통해, 어떤 기술이 유행한다는 이유만으로 무조건 따라가기보다는, 직접 만들어 보고 부딪혀 보면서 나와 내가 만들고 싶은 서비스에 더 잘 맞는 도구를 선택할 수 있는 기준이 조금은 생겼다고 느꼈습니다.
AI가 코드를 대신 짜 주는 시대라고 해도, 결국 어떤 아키텍처를 선택할지, 어떤 트레이드오프를 감수할지는 인간이 결정해야 합니다. 앞으로도 저는 개인 웹사이트를 작은 실험장 삼아 새로운 프레임워크와 라이브러리를 계속 시험해 보면서, “AI에게 무엇을 어떻게 시킬 것인가”를 스스로 설계할 수 있는 개발자로 성장해 가고 싶습니다.
Loading reactions and comments…