Leptos 가이드 — Rust 풀스택 웹 프레임워크·Signals·SSR·Server Functions
이 글의 핵심
Leptos는 Rust로 UI·상태·서버 로직을 한 언어에서 다루는 풀스택 웹 프레임워크입니다. 이 글은 Signals 기반 반응성, 컴포넌트, Server Functions, 라우팅, SSR·하이드레이션, 실전 앱 구조까지 한 흐름으로 연결합니다.
이 글의 핵심
Leptos는 Rust로 반응형 UI와 서버 로직을 함께 설계하기 위한 풀스택 웹 프레임워크입니다. 브라우저에서는 주로 WebAssembly(WASM) 로 컴포넌트를 실행하고, 서버에서는 SSR(서버 사이드 렌더링) 과 하이드레이션(hydration) 으로 초기 HTML을 빠르게 제공한 뒤, 클라이언트에서 동일한 반응성 그래프를 이어 붙입니다.
이 글에서는 다음을 실무 관점에서 연결합니다.
- 핵심 개념:
view!·IntoView, 반응성 그래프, 렌더링 모델 - Signals: 읽기·쓰기·파생 값·부수 효과
- 컴포넌트와 Props:
#[component], 자식 슬롯, 타입 안전한 속성 - Server Functions: 서버 전용 RPC, 직렬화·에러 타입
- 라우팅:
leptos_router의Route·중첩·링크 - SSR·하이드레이션: 초기 HTML·클라이언트 활성화
- 실전 풀스택:
cargo-leptos기반 프로젝트 구조와 배포 시 고려사항
참고: Leptos는 버전에 따라 크레이트·매크로 이름이 달라질 수 있습니다. 운영 전 공식 문서와 프로젝트의
leptos버전을 함께 확인하시기 바랍니다.
1. Leptos를 이해하는 출발점
1-1. 왜 “Rust 풀스택”인가
전통적인 웹 스택은 언어가 둘 이상입니다. 예를 들어 TypeScript로 프론트, Go나 Python으로 API를 나누면, DTO 스키마·검증 규칙·에러 형식이 경계마다 반복됩니다. Leptos는 UI·상태·서버 함수 시그니처를 Rust 타입 시스템 안에 두기 때문에, 컴파일 시점에 불일치를 크게 줄일 수 있습니다.
다만 Rust는 학습 비용과 빌드 파이프라인(WASM·SSR) 이 따르므로, “팀 전체가 Rust에 익숙한가”, “브라우저 WASM 배포를 감당할 수 있는가”를 먼저 판단하는 것이 좋습니다.
1-2. 렌더링과 반응성의 큰 그림
Leptos UI는 대개 다음과 같은 흐름을 가집니다.
- 컴포넌트 함수는 “한 번 실행되어” 반응성 그래프의 노드를 설치합니다.
- Signals가 변경되면, 그 신호에 구독한 뷰 조각만 다시 계산합니다.
- 가상 DOM 전체 diff를 기본으로 삼지 않고, 세밀한 업데이트를 지향합니다.
이 모델은 “컴포넌트 = 매 프레임마다 호출되는 렌더 함수”라는 React식 멘탈모델과 다릅니다. Leptos에서는 설치(install) 와 구독(subscription) 의 이미지가 더 잘 맞습니다.
2. 핵심 개념: view!, IntoView, 반응성 그래프
2-1. view! 매크로
view!는 선언적으로 UI 트리를 구성합니다. HTML과 유사한 문법으로 요소·이벤트·자식을 기술하며, 컴파일 타임에 최적화된 뷰 생성 코드로 전개됩니다.
use leptos::prelude::*;
#[component]
fn Greeting(name: String) -> impl IntoView {
view! {
<section class="card">
<h1>"안녕하세요, " {name} "!"</h1>
</section>
}
}
핵심 포인트는 name이 문자열 그대로 한 번만 끼워 넣어지는 것이 아니라, 반응형 값이면 해당 값이 바뀔 때만 관련 텍스트 노드가 갱신될 수 있다는 점입니다(타입·컨텍스트에 따라 동작이 달라지므로, 프로젝트에서 사용하는 버전의 예제를 기준으로 삼는 것이 안전합니다).
2-2. IntoView와 조합 가능한 뷰
컴포넌트는 반환 타입으로 impl IntoView를 자주 사용합니다. 조각(fragment)·조건부·리스트를 일관되게 합성할 수 있게 해 주는 경계 타입이라고 이해하면 됩니다.
2-3. “반응성 그래프”가 해결하는 문제
전역 상태 스토어에 모든 화면이 의존하면, 작은 변경에도 불필요한 렌더가 번질 수 있습니다. Signals는 의존성을 추적하여, 변경의 원인에 연결된 뷰만 갱신하는 패턴을 취합니다. 이는 성능뿐 아니라 디버깅 시 인과 관계를 좁히는 데도 도움이 됩니다.
3. 반응성 시스템: Signals
3-1. 읽기·쓰기 시그널
가장 기본은 create_signal으로 만든 읽기 핸들과 쓰기 핸들입니다. 읽기는 구독을 만들고, 쓰기는 변경을 알립니다.
use leptos::prelude::*;
#[component]
fn Counter(initial: i32) -> impl IntoView {
let (count, set_count) = create_signal(initial);
view! {
<button
on:click=move |_| set_count.update(|n| *n += 1)
>
{move || count.get()}
</button>
}
}
왜 move 클로저가 많은가에 대한 짧은 설명: 이벤트 핸들러와 반응형 블록은 소유권이 클로저로 넘어가며, 구독이 살아 있는 동안 캡처된 시그널이 안전하게 참조되어야 합니다. Rust의 소유권 모델이 UI 코드에 그대로 드러나는 지점입니다.
3-2. 파생 값: create_memo
여러 시그널을 읽어 파생 상태를 만들 때는 create_memo가 적합합니다. 입력이 바뀔 때만 재계산되도록 메모이제이션됩니다.
use leptos::prelude::*;
#[component]
fn PriceTag(unit_price: f64) -> impl IntoView {
let qty = create_rw_signal(1_i32);
let subtotal = create_memo(move |_| unit_price * qty.get() as f64);
view! {
<p>"소계: " {move || format!("{:.2}", subtotal.get())}</p>
}
}
3-3. 부수 효과: create_effect
외부 세계와 맞닿는 작업(로깅, 브라우저 API 호출 등)은 렌더 결과에 직접 나타나지 않을 수 있습니다. 이때 의존성이 바뀔 때만 실행되는 효과로 분리합니다.
use leptos::prelude::*;
#[component]
fn LogOnChange() -> impl IntoView {
let (value, set_value) = create_signal(0_i32);
create_effect(move |_| {
leptos::logging::log!("value = {}", value.get());
});
view! {
<button on:click=move |_| set_value.update(|v| *v += 1)>"+1"</button>
}
}
주의: 효과 안에서 무거운 네트워크 호출을 무분별하게 넣으면, 의존성 변화에 따라 호출 폭주가 날 수 있습니다. 데이터 페칭은 리소스/서버 함수와 역할을 나누는 편이 운영에 유리합니다.
4. 컴포넌트와 Props
4-1. #[component]와 Props
컴포넌트는 함수에 매크로를 붙여 정의합니다. 함수 인자가 Props가 되며, 기본값·옵션·자식 슬롯 등은 프로젝트에서 채택한 패턴(예: #[prop(optional)])에 따라 확장합니다.
use leptos::prelude::*;
#[component]
pub fn Button(#[prop(optional)] label: Option<String>) -> impl IntoView {
let text = label.unwrap_or_else(|| "확인".to_string());
view! { <button>{text}</button> }
}
4-2. 자식(Children)과 슬롯
레이아웃 컴포넌트는 자식 뷰를 받아 감싸는 형태가 흔합니다. 이는 재사용 가능한 뼈대(헤더·사이드바·메인)를 만들 때 유용합니다.
use leptos::prelude::*;
#[component]
pub fn Shell(children: Children) -> impl IntoView {
view! {
<div class="shell">
<header>"My App"</header>
<main>{children()}</main>
</div>
}
}
핵심은 “자식도 뷰 트리의 일부”라는 점입니다. 부모가 구조를 고정하고, 페이지별 내용만 슬롯으로 주입합니다.
5. Server Functions
5-1. 서버 전용 RPC 형태의 경계
Server Functions는 브라우저 코드에서 함수처럼 호출하지만, 실행은 서버에서 이루어지는 RPC에 가깝습니다. 요청·응답이 직렬화되므로, 타입은 유지되더라도 경계 비용이 존재합니다.
use leptos::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone)]
pub struct TodoInput {
pub title: String,
}
#[server]
pub async fn create_todo(input: TodoInput) -> Result<(), ServerFnError> {
// DB·파일 등 서버 자원 접근
leptos::logging::log!("새 할 일: {}", input.title);
Ok(())
}
운영 관점에서 중요한 것은 인증·권한·입력 검증을 서버에서 반드시 다시 수행한다는 점입니다. 클라이언트 Rust 코드는 조작 가능하므로, 서버 함수는 신뢰 경계입니다.
5-2. 에러 모델과 사용자 메시지
ServerFnError는 경계를 넘는 실패를 표현합니다. 사용자에게 보여 줄 메시지와 내부 원인 로그를 분리하고, 민감 정보가 직렬화되지 않게 주의합니다.
6. 라우팅: leptos_router
6-1. Router·Routes·Route
leptos_router는 클라이언트 내비게이션과 URL ↔ 컴포넌트 매핑을 제공합니다. 앱 루트에 Router를 두고, 경로별로 Route를 정의합니다.
use leptos::prelude::*;
use leptos_router::components::{Route, Router, Routes};
#[component]
fn App() -> impl IntoView {
view! {
<Router>
<nav>
<a href="/">"홈"</a>
<a href="/about">"소개"</a>
</nav>
<main>
<Routes>
<Route path="" view=Home/>
<Route path="about" view=About/>
</Routes>
</main>
</Router>
}
}
#[component]
fn Home() -> impl IntoView { view! { <h1>"홈"</h1> } }
#[component]
fn About() -> impl IntoView { view! { <h1>"소개"</h1> } }
6-2. 중첩 라우트와 레이아웃
관리자 화면처럼 공통 레이아웃 아래에 하위 경로를 두는 경우, 중첩 Route 와 부모 레이아웃 컴포넌트를 조합합니다. 이 패턴은 권한 체크를 레이아웃 한곳에 모을 때 유리합니다.
6-3. A 컴포넌트와 네비게이션
일반 <a href>도 동작하지만, SPA 경험을 위해 프레임워크가 제공하는 링크 컴포넌트(문서에서 A 등으로 안내)를 쓰면 클라이언트 라우팅과 프리페치 전략을 일관되게 가져가기 쉽습니다. 버전별 이름이 다를 수 있으니, 사용 중인 문서의 예제를 따르십시오.
7. SSR과 Hydration
7-1. SSR이 주는 가치
초기 HTML을 서버에서 생성하면 첫 페인트가 빨라지고, 검색 엔진·소셜 미리보기 등 HTML 스냅샷이 필요한 시나리오에 유리합니다. 다만 개인화된 UI는 캐시 전략과 충돌할 수 있으므로, 공개 페이지 vs 로그인 후 페이지를 나눠 설계하는 것이 일반적입니다.
7-2. Hydration이 하는 일
SSR로 내려준 HTML은 정적입니다. Hydration은 브라우저에서 WASM이 로드된 뒤, 동일한 반응성 그래프를 연결해 클릭·입력 같은 상호작용을 살리는 과정입니다. 여기서 서버 HTML과 클라이언트 트리 불일치가 나면 경고나 깨짐이 발생할 수 있으므로, 초기 상태의 단일 출처를 유지하는 것이 중요합니다.
7-3. cargo-leptos와 실행 모드
실전 프로젝트는 cargo-leptos 로 서버·클라이언트 빌드·실행을 묶는 경우가 많습니다. 개발 서버에서 핫 리로드·에셋 처리를 경험한 뒤, 배포 시에는 정적 파일·WASM·서버 바이너리를 각각 CDN·리버스 프록시 뒤에 두는 구성을 검토합니다.
8. 실전 풀스택 앱 구축: 권장 구조
8-1. 디렉터리 관점의 분리
규모가 커지면 다음과 같이 책임을 나누는 것이 유지보수에 유리합니다.
components/: 재사용 UI·디자인 시스템routes/또는pages/: URL 단위 화면server/: DB 모델·리포지토리·외부 API 클라이언트lib.rs/main.rs: 앱 부트스트랩·라우터 조립
8-2. 데이터 흐름 패턴
- 서버 함수로 권한 있는 변경·민감 조회를 처리합니다.
- 클라이언트 시그널은 UI 상태(모달 열림, 폼 입력)에 집중합니다.
- 서버와의 동기화가 필요하면 리소스/페칭 패턴(프로젝트 템플릿과 문서의
create_resource계열)을 사용합니다.
안티패턴은 시그널에 서버가 아닌 곳의 비밀을 넣거나, 서버 함수 없이 공개 API 키를 브라우저에 노출하는 것입니다.
8-3. 배포와 관측 가능성
- 로그: 구조화 로그(요청 ID 상관관계)를 서버에 남깁니다.
- 에러: 사용자 메시지와 내부 스택을 분리합니다.
- 성능: WASM 번들 크기·초기 로드·서버 TTFB를 각각 측정합니다.
9. 모범 사례와 흔한 실수
9-1. 시그널 설계
- 하나의 진실 공급원: 동일 개념을 여러 시그널에 중복 저장하지 않습니다.
- 파생은 메모로: 계산 가능한 값은
create_memo로 유도합니다. - 이펙트 남용 금지: 데이터 로딩은 리소스/서버 함수 경로로 보냅니다.
9-2. 컴포넌트 경계
- Props 폭발을 피하기 위해 컨텍스트나 작은 도메인 모듈을 도입합니다.
- 거대 단일 컴포넌트는 테스트·리뷰 모두 어려워집니다.
9-3. 보안
- 서버 함수는 인증 세션·권한을 항상 검증합니다.
- CSRF·쿠키 정책은 프록시·도메인 설정과 함께 설계합니다.
9.4 반응성 그래프·하이드레이션 내부와 프로덕션 이슈
Leptos는 컴포넌트 실행 시 반응성 그래프를 구축하고, 시그널 읽기가 구독 엣지를 만듭니다. 이는 가상 DOM 전체 diff보다 CPU·메모리를 아끼는 대신, 디버깅 시 “왜 이 조각만 갱신됐지?”를 추적하려면 시그널 의존성을 머릿속에 그려야 함을 뜻합니다. effect 남발은 예상치 못한 연쇄 갱신을 부르니, 데이터는 리소스/서버 함수, UI는 시그널로 역할을 나누는 것이 안전합니다.
하이드레이션 불일치는 서버가 만든 HTML과 클라이언트가 기대하는 초기 시그널 값이 다를 때 발생합니다. 시간·랜덤·localStorage를 렌더 중에 직접 읽으면 흔히 터집니다. 해결책은 초기값의 단일 출처를 서버에서 내려 주고, 클라이언트 전용 로직은 effect 또는 클라이언트 전용 컴포넌트 경계로 미룹니다.
WASM·배포 관점
- 번들 크기: 트리셰이킹과 의존성 최소화가 첫 로드에 직결됩니다.
- 압축·캐시:
wasmMIME·Content-Encoding·CDN 캐시 키를 점검합니다. - 서버 렌더링:
cargo-leptos환경에서 서버 바이너리와 정적 에셋을 분리 배포하는지 확인합니다.
9.5 트러블슈팅
| 증상 | 흔한 원인 | 점검 |
|---|---|---|
| Hydration mismatch | 시간/랜덤/브라우저 전용 API | 서버·클라이언트 초기값 분리 |
| 시그널이 갱신돼도 UI가 그대로 | 잘못된 클로저·읽기 누락 | move 클로저, get/with 경로 확인 |
| WASM 로드 실패 | MIME·경로·CORS | 네트워크 탭·서버 정적 파일 설정 |
| 서버 함수 403/401 | 쿠키·SameSite·프록시 헤더 | 리버스 프록시 X-Forwarded-Proto |
10. 정리
Leptos는 Rust의 타입 시스템과 세밀한 반응성, 서버 함수 경계를 한 축에 모은 풀스택 웹 프레임워크입니다. Signals로 UI 상태를 안전하게 쪼개고, leptos_router로 화면을 나누며, Server Functions로 서버 신뢰 경계를 명확히 하면, SSR·하이드레이션까지 포함한 실전 앱을 일관된 추상화로 유지할 수 있습니다.
다음 단계로는 공식 북의 프로젝트 템플릿을 기반으로 인증·데이터베이스·에러 바운더리를 붙이며, 팀 규모에 맞는 모듈 경계를 실험해 보시기 바랍니다.
배포 전 git add·git commit·git push 후 npm run deploy를 실행하는 것을 권장합니다(프로젝트 배포 규칙).
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「Leptos 완벽 가이드 — Rust 풀스택 웹 프레임워크·Signals·SSR·Server Functions」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「Leptos 완벽 가이드 — Rust 풀스택 웹 프레임워크·Signals·SSR·Server Functions」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Leptos 핵심 개념, Signals, 컴포넌트·Props, Server Functions, leptos_router, SSR·하이드레이션, 풀스택 앱 구축까지 정리한 Rust 웹 프레임워크 실전 가이드입니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- SvelteKit 완벽 가이드 | Full Stack·Routing
- Remix 완벽 가이드 | Full Stack·Loader
- The Complete SvelteKit Guide | Full Stack, Routing, Form Actions, Load, Hooks
이 글에서 다루는 키워드 (관련 검색어)
Leptos, Rust, Web Framework, SSR, Full Stack 등으로 검색하시면 이 글이 도움이 됩니다.