Fresh 프로젝트를 처음 만들면 routes, islands, Preact, Vite 같은 여러 요소가
한꺼번에 등장합니다.
각각의 사용법부터 익히기 시작할 수도 있지만, Fresh에서는 그 전에 한 가지를 이해해 두는 편이 좋습니다.
왜 일반 컴포넌트와 Island를 굳이 나누는가?
이 질문을 따라가면 Fresh가 말하는 Server-first와 Islands Architecture가 자연스럽게 연결됩니다.
Fresh는 Server-first 프레임워크입니다
Fresh 공식 문서는 Fresh를 Server-first web framework라고 설명합니다.
말 그대로 페이지를 만들 때 브라우저보다 서버가 먼저 일을 합니다.
예를 들어 다음과 같은 페이지가 있다고 해보겠습니다.
export default function Home() {
return (
<main>
<h1>Fresh Notes</h1>
<p>Fresh와 Deno를 기록하는 기술 블로그입니다.</p>
</main>
);
}이 페이지를 보기 위해 브라우저에서 Preact가 먼저 실행될 필요는 없습니다.
Fresh가 서버에서 HTML을 만들어 브라우저에 전달합니다.
단순화하면 이런 흐름입니다.
브라우저
│
│ GET /
▼
Fresh Server
│
│ Preact로 HTML 렌더링
▼
HTML Response
│
▼
브라우저가 바로 표시여기까지라면 JavaScript가 없어도 글과 링크를 읽을 수 있습니다.
블로그 글, 문서, 소개 페이지처럼 대부분의 내용이 읽기 위한 콘텐츠라면 꽤 자연스러운 구조입니다.
그렇다면 Preact는 서버에서만 쓰는 걸까요?
그렇지는 않습니다.
Fresh는 Preact를 서버 렌더링에도 사용하고, 필요한 경우 브라우저의 인터랙션에도 사용합니다.
중요한 것은 어디까지 브라우저에서 실행할 것인지 개발자가 구분한다는 점입니다.
예를 들어 블로그 화면을 생각해보겠습니다.
Header
제목
본문
코드 예제
태그
Footer이 중 대부분은 사용자가 클릭할 때 상태가 변하지 않습니다.
HTML만 있어도 됩니다.
그런데 여기에 테마 변경 버튼이 있다면 이야기가 달라집니다.
Header
├─ Logo
├─ Navigation
└─ Theme Toggle ← JavaScript 필요
Article
├─ 제목
├─ 본문
└─ 코드
Footer페이지 전체 때문에 JavaScript를 보낼 필요는 없습니다.
Theme Toggle만 브라우저에서 동작하면 됩니다.
Fresh에서는 이런 부분을 Island로 만듭니다.
Island는 페이지 속의 작은 대화형 영역입니다
예를 들어 우리가 만든 블로그에는 ThemeToggle이 있습니다.
대략 이런 컴포넌트입니다.
export default function ThemeToggle() {
function toggleTheme() {
const root = document.documentElement;
const nextTheme = root.dataset.theme === "freshblog"
? "freshblog-dark"
: "freshblog";
root.dataset.theme = nextTheme;
localStorage.setItem("theme", nextTheme);
}
return (
<button type="button" onClick={toggleTheme}>
Theme
</button>
);
}이 컴포넌트는 document, localStorage, onClick 같은 브라우저 기능이
필요합니다.
따라서 다음처럼 Island로 둡니다.
islands/
└── ThemeToggle.tsx반대로 단순히 글 제목을 표시하는 컴포넌트라면 굳이 Island가 될 이유가 없습니다.
export function ArticleTitle(
{ title }: { title: string },
) {
return <h1>{title}</h1>;
}이런 컴포넌트는 서버에서 HTML로 렌더링하면 끝입니다.
여기서 Fresh의 기준이 꽤 명확해집니다.
브라우저에서 동작할 필요가 없다
→ 일반 컴포넌트
브라우저에서 상태 변경이나 이벤트 처리가 필요하다
→ Island모든 컴포넌트를 Island로 만드는 것이 아니라 JavaScript가 필요한 경계를 직접 표시하는 것에 가깝습니다.
Island도 처음부터 브라우저에서만 그려지는 것은 아닙니다
여기서 처음에는 조금 헷갈릴 수 있습니다.
Island라고 해서 빈 공간을 먼저 보내고 브라우저가 나중에 전부 그리는 것은 아닙니다.
Fresh의 Island는 서버에서도 렌더링됩니다.
예를 들어 ThemeToggle이 포함된 페이지가 있다면 처음 요청에서는 서버가 버튼을 포함한 HTML을 만듭니다.
그 뒤 브라우저에서 해당 Island에 필요한 JavaScript가 로드되고 인터랙션이 연결됩니다.
이 과정을 보통 hydration이라고 부릅니다.
단순화하면 다음과 같습니다.
1. 서버
↓
페이지 전체를 HTML로 렌더링
2. 브라우저
↓
HTML 표시
3. Island가 있다면
↓
해당 Island의 JavaScript 로드
4. hydration
↓
버튼 클릭 등의 인터랙션 활성화즉 페이지 전체를 다시 JavaScript 애플리케이션으로 만드는 것이 아니라 필요한 구역만 활성화합니다.
이것이 Islands Architecture의 핵심입니다.
이름이 왜 Island일까요?
페이지를 바다라고 생각하면 이해하기 쉽습니다.
대부분의 페이지는 서버가 만들어준 HTML입니다.
그 사이에 브라우저에서 실제로 동작해야 하는 작은 영역들이 떨어져 있습니다.
────────────────────────────────
HTML
[ Island ]
HTML
[ Island ] [ Island ]
HTML
────────────────────────────────JavaScript가 필요한 부분만 HTML 속에 섬처럼 존재합니다.
그래서 Islands Architecture라는 이름이 꽤 직관적입니다.
Fresh에서 Island는 단순한 코드 정리 규칙이 아니라 클라이언트 JavaScript의 경계이기도 합니다.
일반적인 SSR과는 무엇이 다를까요?
여기서 "SSR을 쓰는 다른 프레임워크도 서버에서 HTML을 만들지 않나?"라는 의문이 생길 수 있습니다.
맞습니다.
SSR 자체가 Fresh만의 개념은 아닙니다.
차이를 이해할 때는 서버에서 HTML을 만들었는가만 볼 게 아니라, 그 뒤에 얼마나 많은 JavaScript를 브라우저에서 다시 실행하는가를 같이 보는 편이 좋습니다.
전통적인 전체 hydration 구조를 아주 단순화하면 다음과 같이 생각할 수 있습니다.
서버
↓
전체 HTML 생성
브라우저
↓
전체 애플리케이션 JavaScript 로드
↓
페이지 전체 hydrationIsland 방식에서는 범위가 달라집니다.
서버
↓
전체 HTML 생성
브라우저
↓
Island에 필요한 JavaScript만 로드
↓
Island만 hydration따라서 "SSR이냐 아니냐"보다 hydration의 경계를 어디에 두느냐가 Fresh를 이해하는 데 더 중요한 질문입니다.
Fresh의 Zero JS는 여기에서 나옵니다
Island가 하나도 없는 페이지라면 어떻게 될까요?
브라우저에서 실행할 컴포넌트가 없습니다.
그렇다면 클라이언트 JavaScript 자체가 필요하지 않습니다.
Fresh는 처음부터 이를 중요한 원칙으로 내세웠습니다.
다만 여기에는 재미있는 역사 하나가 있습니다.
Fresh는 오래전부터 "JavaScript를 기본적으로 보내지 않는다"고 설명해왔지만, Fresh
2.2까지는 Island가 없는 페이지에도 Island와 Partial 시스템을 시작하기 위한 작은
client-entry 스크립트가 들어갔습니다.
Fresh 2.3에서 이 부분까지 제거됐습니다.
현재는 페이지에 Island나 client navigation용 Partial이 없다면 다음 항목도 생성되지 않습니다.
client-side bundle
<script>
module preload즉 Fresh 2.3부터는 조건을 만족하는 페이지에서 말 그대로 0 KB의 클라이언트 JavaScript가 가능합니다.
이 차이는 우리가 블로그를 만들면서 우연히 직접 확인할 수 있었습니다.
Counter를 지웠더니 HMR도 이상해졌습니다
Fresh 기본 프로젝트에는 Counter Island가 있었습니다.
블로그에는 필요하지 않아 삭제했습니다.
그러자 흥미로운 변화가 생겼습니다.
터미널에서는 Vite가 CSS 변경을 감지하고 있었습니다.
[vite] (client) hmr update /assets/styles.css
[vite] (ssr) hmr update /@id/fresh:server_entry그런데 브라우저에서는 Vite client가 연결되지 않았고 CSS 수정도 자동 반영되지 않았습니다.
삭제했던 Counter Island를 다시 추가하자 브라우저 client가 다시 연결됐습니다.
이 경험 덕분에 Fresh의 Zero JS가 단순한 홍보 문구가 아니라 실제 페이지에 어떤 차이를 만드는지 확인할 수 있었습니다.
물론 블로그에 HMR을 유지하겠다고 의미 없는 Counter를 남길 이유는 없습니다.
어차피 실제로 필요한 ThemeToggle이 있었기 때문에 이를 Island로 만들었습니다.
결과적으로 구조도 자연스러워졌습니다.
Header Server
Navigation Server
Article Server
Markdown Server
Footer Server
ThemeToggle IslandJavaScript를 무조건 없애는 것이 목표가 아니라 필요한 곳에서만 사용하는 것이 핵심입니다.
Island가 많으면 무조건 나쁜 걸까요?
그렇지도 않습니다.
Fresh의 목적은 JavaScript 사용을 금지하는 것이 아닙니다.
필요한 인터랙션이라면 Island를 만드는 것이 맞습니다.
예를 들어 다음 기능들은 자연스럽게 Island 후보가 될 수 있습니다.
Theme Toggle
검색 자동완성
댓글 입력 UI
장바구니
이미지 Carousel
실시간 Dashboard
복잡한 Form UI반대로 다음과 같은 요소까지 습관적으로 Island로 만들 필요는 없습니다.
제목
본문
Footer
정적인 카드
단순 링크
이미지결국 질문은 하나입니다.
이 컴포넌트가 브라우저에서 JavaScript를 실행해야 하는가?
아니라면 서버에 남겨두면 됩니다.
Server-first는 Server-only가 아닙니다
Fresh를 Server-first라고 부른다고 해서 모든 기능을 서버에 넣어야 한다는 뜻도 아닙니다.
Server-first의 first가 중요합니다.
기본 출발점을 서버에 두고, 필요한 인터랙션만 클라이언트로 확장하는 방식입니다.
Server first
HTML
HTML
HTML
↓ 필요한 부분만 확장
HTML
Island
HTML
Island
HTML그래서 Fresh는 정적인 웹사이트만을 위한 프레임워크도 아닙니다.
서버에서는 데이터베이스를 조회하거나 API를 만들 수 있고, 브라우저에서는 Island를 통해 상태를 가진 UI를 구현할 수 있습니다.
다만 처음부터 모든 페이지를 클라이언트 애플리케이션으로 취급하지 않을 뿐입니다.
블로그를 만들면서 Islands가 이해되기 시작했습니다
문서에서 "필요한 JavaScript만 보낸다"라는 설명을 읽는 것과 실제로 프로젝트를 만드는 것은 조금 달랐습니다.
Counter를 지우고,
JavaScript가 없는 페이지를 확인하고,
ThemeToggle이 필요해져 다시 Island를 추가해보니 경계가 명확해졌습니다.
Fresh에서 컴포넌트를 만들 때는 이런 순서로 생각해볼 수 있습니다.
이 UI는 서버에서 HTML로 만들 수 있는가?
│
├─ Yes
│ ↓
│ 일반 컴포넌트
│
└─ 브라우저 인터랙션이 필요한가?
↓
Island이 기준 하나만 알고 있어도 Fresh 프로젝트의 components/와 islands/를 왜
구분하는지 이해하기 훨씬 쉬워집니다.
다음 글에서는 이 구조를 한 단계 더 직접 확인해보겠습니다.
Island가 없는 페이지와 있는 페이지에서 Fresh 2.3이 실제로 어떤 HTML과 JavaScript를 보내는지 개발자 도구를 이용해 비교해보겠습니다.