Leptos 완벽 가이드 — Rust 풀스택 웹 프레임워크·Signals·SSR·Server Functions

Leptos 완벽 가이드 — Rust 풀스택 웹 프레임워크·Signals·SSR·Server Functions

이 글의 핵심

Leptos는 Rust로 UI·상태·서버 로직을 한 언어에서 다루는 풀스택 웹 프레임워크입니다. 이 글은 Signals 기반 반응성, 컴포넌트, Server Functions, 라우팅, SSR·하이드레이션, 실전 앱 구조까지 한 흐름으로 연결합니다.

이 글의 핵심

LeptosRust반응형 UI서버 로직을 함께 설계하기 위한 풀스택 웹 프레임워크입니다. 브라우저에서는 주로 WebAssembly(WASM) 로 컴포넌트를 실행하고, 서버에서는 SSR(서버 사이드 렌더링)하이드레이션(hydration) 으로 초기 HTML을 빠르게 제공한 뒤, 클라이언트에서 동일한 반응성 그래프를 이어 붙입니다.

이 글에서는 다음을 실무 관점에서 연결합니다.

  • 핵심 개념: view!·IntoView, 반응성 그래프, 렌더링 모델
  • Signals: 읽기·쓰기·파생 값·부수 효과
  • 컴포넌트와 Props: #[component], 자식 슬롯, 타입 안전한 속성
  • Server Functions: 서버 전용 RPC, 직렬화·에러 타입
  • 라우팅: leptos_routerRoute·중첩·링크
  • 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는 대개 다음과 같은 흐름을 가집니다.

  1. 컴포넌트 함수는 “한 번 실행되어” 반응성 그래프의 노드를 설치합니다.
  2. Signals가 변경되면, 그 신호에 구독한 뷰 조각만 다시 계산합니다.
  3. 가상 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. 데이터 흐름 패턴

  1. 서버 함수로 권한 있는 변경·민감 조회를 처리합니다.
  2. 클라이언트 시그널UI 상태(모달 열림, 폼 입력)에 집중합니다.
  3. 서버와의 동기화가 필요하면 리소스/페칭 패턴(프로젝트 템플릿과 문서의 create_resource 계열)을 사용합니다.

안티패턴은 시그널에 서버가 아닌 곳의 비밀을 넣거나, 서버 함수 없이 공개 API 키를 브라우저에 노출하는 것입니다.

8-3. 배포와 관측 가능성

  • 로그: 구조화 로그(요청 ID 상관관계)를 서버에 남깁니다.
  • 에러: 사용자 메시지와 내부 스택을 분리합니다.
  • 성능: WASM 번들 크기·초기 로드·서버 TTFB를 각각 측정합니다.

9. 모범 사례와 흔한 실수

9-1. 시그널 설계

  • 하나의 진실 공급원: 동일 개념을 여러 시그널에 중복 저장하지 않습니다.
  • 파생은 메모로: 계산 가능한 값은 create_memo유도합니다.
  • 이펙트 남용 금지: 데이터 로딩은 리소스/서버 함수 경로로 보냅니다.

9-2. 컴포넌트 경계

  • Props 폭발을 피하기 위해 컨텍스트작은 도메인 모듈을 도입합니다.
  • 거대 단일 컴포넌트는 테스트·리뷰 모두 어려워집니다.

9-3. 보안

  • 서버 함수는 인증 세션·권한을 항상 검증합니다.
  • CSRF·쿠키 정책은 프록시·도메인 설정과 함께 설계합니다.

10. 정리

Leptos는 Rust의 타입 시스템세밀한 반응성, 서버 함수 경계를 한 축에 모은 풀스택 웹 프레임워크입니다. Signals로 UI 상태를 안전하게 쪼개고, leptos_router로 화면을 나누며, Server Functions로 서버 신뢰 경계를 명확히 하면, SSR·하이드레이션까지 포함한 실전 앱을 일관된 추상화로 유지할 수 있습니다.

다음 단계로는 공식 북의 프로젝트 템플릿을 기반으로 인증·데이터베이스·에러 바운더리를 붙이며, 팀 규모에 맞는 모듈 경계를 실험해 보시기 바랍니다.


배포 전 git add·git commit·git pushnpm run deploy를 실행하는 것을 권장합니다(프로젝트 배포 규칙).