Search for a command to run...

DOM이 트리인 것처럼, React도 결국 트리다. 그리고 트리 구조에서 데이터 흐름은 기본적으로 Root → Parent → Child 방향으로 내려간다.
문제는 React(특히 일반적인 CSR 컴포넌트 모델)에서는 MVC가 깔끔히 분리되어 있지 않다는 점이다. “데이터 로딩(모델) + 화면(뷰) + 이벤트 처리(컨트롤)”가 한 파일/한 함수 안에서 한 덩어리로 굴러가는 경우가 많다.
그러다 보니, 아래 같은 상황이 자주 생긴다.
React(Next.js) 자체에서는 다음과 같이 상태를 공유하는 방법밖에 없기 때문이다:
이 문제는 사용자로 하여금 문제를 느끼게 만든다. 짧은 순간이어도, GNB가 바뀐다거나 Breadcrumb가 튀는 것이 보인다.
근본적으로 하위의 함수를 미리 실행해 볼수 없다는데서 구조적 문제가 있다. 저~~~ 아래에 있는 함수를 일단 실행해 봐야 하는데, 이때 이미 최초 tree가 그려진다. 저~~~ 아래에 있는 함수가 실행돼야만 데이터가 준비된다. 그제서야 상위 함수에서 UI 수정을 할 수 있다.
보기 좋게 정리하자면 다음과 같다:
이로 인해 사용자는 깜빡임(flicker)이나 버그 같은 느낌을 받는다. 그러기에 “성능이 안 좋네?” 혹은 “뭔가 사이트가 불안정한데?”가 된다. 특히, 프로그램을 납품하는 입장이라면 고객이 고쳐달라고 분명 클레임을 걸 것이다.
굳이 위로 데이터를 올려보내자면 context에 데이터를 설정하거나 콜백 함수를 던지고 하위에서 호출하는 방식을 쓴다. 이 모든게 re-rendering을 불러 일으킨다. 별도의 상태 관리 시스템을 써도 근본 구조 때문에 발생하는 문제이므로 re-rendering 문제는 동일하다.
스벨트도 기본적으로는 트리구조를 따른다. 하지만, Svelte에서는 Model하고 View가 분리되어 있다. Model 영역은 +page.ts, +page.js 또는 +layout.js, +layout.ts 에서 담당한다. View 부분은 +page.svelte, +layout.svelte를 쓴다.
View하고 Model이 나눠저있기 때문에, React처럼 무조건 Root에서 child로 가는 1방향 방식이 아니라, || 모습의 데이터 로딩이 가능하다. (И 모양이라고 하는게 더 맞을지도 모르겠다.)
다음과 같은 폴더 구조가 있다하자: root > blog > [slug] > article.svelte
그러면 아래 처럼 두 단계에 나눠 코드가 실행된다.
가독성을 위해
+page.ts,+page.svelte대신각 단계.ts또는각 단계.svelte로 표현했다.
이러면 model 순서에서 각 단계의 데이터를 하나로 모을 수 있다. (page.data <- $app/state). 그리고 모든 view에서 이 데이터를 참조할 수 있다. 덕분에 일관된 View 처리가 가능하다.
데이터 계층(Model)과 표현(View) 계층이 분리되어서 최초 Tree 랜더링에서도 정답 화면을 그릴 수 있다.
Svelte도 결국 컴포넌트 트리다. 그런데 SvelteKit을 쓰면 개발 구조가 자연스럽게 이렇게 갈라진다.
+page.ts/js, +layout.ts/js의 load()+page.svelte, +layout.svelte이 분리는 생각보다 크다. 왜냐면 “레이아웃이 뭘 그려야 하는지”를 결정하는 데 필요한 데이터를 렌더링 이전 단계(load 단계)에서 정리할수 있기 때문이다.
여기서 핵심은 이 느낌이다:
그리고 SvelteKit은 현재 라우트에서 발생한 모든 load()의 결과가 합쳐져서 $page.data로 잡힌다. 즉, 레이아웃에서:
를 렌더링 시점에 이미 알고 그릴 수 있다.
엄밀히 말하면 load() 실행 순서는 레이아웃→페이지로 “계층대로” 도는 편에 가깝다.
하지만 최종 렌더링 시점에는 데이터가 합쳐져 있기 때문에, 체감적으로는 “하위가 실어 보낸 정보를 상위가 받아서 그린다”가 된다.
아래와 같은 코드가 있다고 하자
<CommonLayout>
<Blog>
<Article />
</Blog>
</CommonLayout>
CommonLayout 입장에서는 “아래에 뭐가 붙을지” 모른다.
그런데 요구사항이 이럴 수도 있다:
Blog > 게시글 명, Support > 질문명 같은 걸 보여줘야 함<Single>이면 햄버거 버튼을 숨기고, 아래가 <Blog>면 햄버거 버튼을 보여야 함조금 더 살펴보면, 하위의 데이터를 위로 올려줘야 하는 상황이 생길 수도 있다 (엄밀하게는 자식 컴포넌트가 실행돼야 정보가 생기는 구조):
admin 이면 상위 레이아웃에 "공식 인증" 마크를 붙이고 싶음 (또는 가벼운 UI 변화를 원함)물론.. React에서도 상위에서 경로에 따라 데이터를 확정한 뒤 -> root에서 부터 모든 데이터를 구할수도 있겠지만 encapsulation에는 맞지 않다.
그리고 경로가 매우 많을때는 어떻게 대처할 것인가..? 모든 경로를 if, switch-case로 표현할 것인가?
이 경우 next를 쓰더라도 현실적인 문제를 푸는데 도움이 되지 않는다.
React로 하면 보통 이런 선택을 한다:
근데 React에서 쓰는 패턴은 사용자 입장에서 "화면이 바뀌는 경험"을 주게 한다:
만악의 근원은 하위의 정보를 "일단 실행해보기 전까지는" 상위에서 알 수 없다는 것이다. React에서는 MVC가 분리되지 않고, 하나의 함수 내에서 MVC가 이루어진다. 그러다 보니 데이터를 무조건 아래로만 옮길 수 있다. (Props Drilling, ContextProvider류)
이런 제약을 깨고 마개조를 하면 strictmode를 켰을때 double rendering 경고를 먹게 될 것이다!
이건 개발자 눈에는 “정상 동작”인데, 고객사 눈에는 “깜빡임/버그”다. 이것을 싫어하는 사람도 매우 많다. (분명 고쳐달라고 말 나올것이다.) 페이지에 최초 진입할때 생기는 1회성 rerendering도 고객 입장에서는 버그이기 때문이다.
SvelteKit에선 보통 이런 식으로 푼다.
+page.ts에서 데이터를 모두 모음 (이는 다시 view에게도 전달됨. view에 갈 데이터를 위에도 올려주는 개념임.)+layout.svelte(<CommonLayout> 역할)에서 $page.data를 보고 UI를 결정 $page.data는 모든 load 데이터를 병합한 결과를 가지고 있다. (모두가 하나의 객체를 바라본다.)+page.ts/ +layout.ts는 다음과 같은 구조가 된다.
// routes/blog/+layout.ts
export const load = async ({ params }) => {
const lastestArticle = await latestArticle();
return { latestArticle }
};
// routes/blog/[slug]/+page.ts
export const load = async ({ params }) => {
const article = await fetchArticle(params.slug);
return {
article,
layoutHints: {
breadcrumb: ["Blog", article.title],
hideHamburger: false
}
};
};
이렇게 파일을 만들면 모든 경로의 +page.ts가 우선 실행된다. 그리고 모든 계층에서 데이터를 모은다. 이 모아진 데이터로 부터 Root에서 각 컴포넌트가 렌더링 된다. 그렇기에 상위 view에서 하위의 데이터를 조회하는것도 어렵지 않다.
<!-- routes/+layout.svelte -->
<script lang="ts">
import { page } from "$app/state";
let { children } = $props();
// 상위 레이아웃에서 참고할 수 있는 정보가 담겨있음 (2-level nested)
let hints = $page.data.layoutHints;
// 최신 글 데이터가 담겨있음.
let news = $page.data.latestArticle;
// React Router로 생각해보면 "아티클", "시리즈", "질문" 이런식으로 여러 메뉴가 있을수 있음
// 이때, 맨 위 레이아웃에서 현재 들어간 메뉴의 최신글이 몇개인지 등을 표시해야 할 수도 있음 (badge 표시)
// 그렇다고 맨 위에서 path를 참고하여 최신글을 부르는것은 어려울 수 있음. (if-else가 커짐)
// react라면 하위 컴포넌트가 실행되고 나서야 제대로 그릴수 있지만, Svelte에서는 데이터가 하위에서 올라오기에 처음부터 깔끔하게 그릴 수 있음
</script>
<GNB hideHamburger={hints?.hideHamburger ?? false} />
<Breadcrumb items={hints?.breadcrumb ?? []} />
<LatestArticleNotifier article={news} />
{@render children()}

포인트는 단순하다.
load()) 분리해서 먼저 가져온 후 모두에게 공유한다그 결과로 UI가 덜 흔들린다. 처음부터 맞는 모습으로 그려진다.
필자가 자주 쓰는 말이 있다. PL쪽에서 나온 말이지만, 여기서도 적용될 것 같다.
"우아한 구조"라는게 있고, "현실 문제를 풀기에 적합한 구조"라는게 있다. 가장 최고의 언어(도구)는 현실 문제를 풀기에 적합한 구조를 가지면서 우아한 구조가 적용된 것이다. 즉, 현실 문제를 풀지 못하고 외면한다면 좋은 언어나 도구가 아니다.
필자 생각에는 Rust가 이에 해당한다. 우아한 구조를 가지면서도 현실 문제를 충분히 해결한다.
조금의 난이도를 타협봤지만, 그럼에도 충분히 쓸만다.
우아한 구조란 Haskell이나 Lisp쪽을 가르키고, 현실 문제를 풀기 적합한 구조는 주로 C++, Java 등을 가르킨다. 웹 개발 계열에서는 전자는 React (Next.js), 후자는 MVC가 명확한 Django, Spring, Flask가 될 것이다. 그리고 그 중간에 Sveltekit이 존재한다.
SvelteKit은 M-V를 분리함으로써 React가 가진 리랜더링 문제를 수정함과 동시에, React 처럼 선언적으로 Dom Tree 랜더링이 가능하다. (모델은 +page.ts/+layout.ts, 뷰는 .svelte) 그렇기에 현실 문제를 잡으면서도 우아한 코딩이 가능해진다.
개인적으로는 이런 면에서 SvelteKit이 Next.js보다 “기본 구조를 더 잘 깔아놨다”는 느낌을 많이 받는다.
아쉽게도 한국에서는 React (Next.js)만을 사용한다. Next외 Nuxt, Sveltkit인력풀이 없다보니 Next.js 구인 공고를 올리게 되고, 사용자들도 Next만 쓰고, 학교에서도 Next만 보는것 같다.
하지만 보다 웹에 유리한 구조는 Sveltekit이라고 생각한다. 다들 Sveltekit을 찍먹이라도 해보면 좋겠다.
로그인 후 댓글을 작성할 수 있습니다.