Rust와 GPUI로 데스크톱 시간 앱 만들기: 초보자의 여정

발행: (2026년 1월 1일 오후 03:00 GMT+9)
14 min read
원문: Dev.to

Source: Dev.to

러스트 시간 역설: 개발자의 이야기

나는 Rust로 간단한 world‑clock 앱을 만들기로 했습니다. 6주가 지난 뒤에야 드디어 작동시켰는데—다섯 개 도시의 현재 시간을 표시합니다. 아이러니하게도, 시간을 표시하는 데 너무 많은 시간을 투자한 나머지 시간 자체가 멈춘 듯했습니다.

이 프로젝트가 가르치는 내용

마스터하게 될 핵심 Rust 개념

  • Ownership & Borrowing – 모든 타임존 카드는 자체 상태를 관리하는 Entity입니다.
  • Trait Implementation – UI 컴포넌트를 위한 사용자 정의 Render 트레이트.
  • Type Safety – 런타임 타임존 오류를 방지하는 컴파일 타임 보장.
  • Lifetimes&mut Context와 윈도우 참조에 대한 이해.

GPUI 프레임워크 기본

  • impl IntoElement를 이용한 컴포넌트 아키텍처.
  • EntityContext를 통한 상태 관리.
  • cx.observe_window_bounds()를 사용한 반응형 업데이트.
  • 플렉스박스 스타일 API를 활용한 레이아웃 시스템.

프로젝트 빌드 및 실행 방법

필수 조건

# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Verify installation
rustc --version
cargo --version

프로젝트 설정

새 프로젝트 만들기

cargo new world-time-app
cd world-time-app

Cargo.toml에 의존성 추가

[dependencies]
chrono = { version = "0.4" }
gpui = "0.2"
gpui-component = "0.4.0-preview1"
  • src/main.rs에 코드를 복사합니다.
  • 빌드 및 실행:
cargo run --release

일반적인 빌드 문제 및 해결책

  • 문제: “레지스트리에서 gpui를 찾을 수 없습니다”
    해결책: GPUI는 crates.io가 아니라 Git을 통해서만 사용할 수 있습니다. 위와 같이 Git 의존성을 사용하세요.

  • 문제: 컴파일에 10분 이상 걸림
    해결책: 첫 번째 빌드는 느립니다(의존성 다운로드/컴파일). 이후 빌드는 Cargo 캐시를 사용합니다. --release 옵션은 최종 빌드 시에만 사용하세요.

  • 문제: Linux에서 창이 나타나지 않음
    해결책: 필요한 시스템 라이브러리를 설치합니다:

    # Ubuntu/Debian
    sudo apt-get install libxkbcommon-dev libwayland-dev
    
    # Fedora
    sudo dnf install libxkbcommon-devel wayland-devel

코드 아키텍처 분석

WorldTime 엔티티

pub struct WorldTime {
    name: String,
    time: String,        // HH:MM format
    diff_hours: i32,     // offset from home
    is_home: bool,
    timezone_id: String,
}

Key learning: Rust의 구조체는 데이터를 소유하거나 명시적으로 빌려야 합니다. String(소유)과 &str(빌림) 중 선택하는 것이 첫 번째 큰 결정이었습니다.

컴포넌트 렌더링 패턴

impl Render for WorldTime {
    fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement
}

Key learning: &mut self 매개변수가 며칠 동안 저를 혼란스럽게 했습니다. 가변 참조를 통해 컴포넌트는 렌더링 사이클 동안 상태를 업데이트할 수 있습니다.

시간 업데이트

if now.duration_since(self.last_update).as_secs() >= 60 {
    for city in &self.cities {
        city.update(cx, |city, _cx| {
            city.update_time();
        });
    }
}

Key learning: Entityupdate() 메서드는 컨텍스트와 클로저를 받습니다. 클로저는 &mut T(엔티티 내부 상태에 대한 가변 접근)와 컨텍스트를 전달받아 GPUI의 소유권 모델 내에서 안전하게 상태를 변형할 수 있게 합니다.

러스트 초보자를 위한: 내가 알았으면 좋았던 것들

1‑2주차: 컴파일러는 당신의 선생님

도전 – “왜 이게 컴파일되지 않을까?!”

// ❌ This fails
let time = WorldTime::new(...);
render(time);
use_time_again(time); // ERROR: value moved

// ✅ This works
let time = WorldTime::new(...);
render(&time);
use_time_again(&time); // Borrowing, not moving

교훈: 러스트의 오류 메시지는 상세합니다. 주의 깊게 읽으세요—대부분 해결 방법을 포함하고 있습니다.

3‑4주차: 라이프타임은 두렵지 않다

도전 – 트레이트 바운드에서 'a 이해하기.

돌파 순간 – 라이프타임은 컴파일러에게 “이 참조는 이 기간 동안 유효합니다”라고 알려주는 것뿐입니다. 컴파일러가 대부분을 추론합니다.

// GPUI handles this complexity:
fn render(&mut self, _window: &mut Window, _cx: &mut Context)
// You don’t write the lifetimes manually

5‑6주차: Borrow Checker와 싸우기

악명 높은 오류

error[E0502]: cannot borrow `self.cities` as mutable because it is also borrowed as immutable

내 해결책 – 로직을 별도 함수로 분리합니다. Borrow Checker는 스코프 단위로 동작합니다:

// Instead of doing everything in one method:
fn render(&mut self) {
    let cities = &self.cities; // Immutable borrow
    self.update_all(); // ❌ Mutable borrow while immutable exists
}

// Split into logical units:
fn render(&mut self) {
    self.update_if_needed(); // Mutable borrow ends here
    let cities = &self.cities; // Now we can borrow immutably
}

나를 구한 디버깅 전략

  1. 프린트 디버깅은 여전히 유효합니다
dbg!(&self.time); // Rust's debug macro
println!("City: {}, Time: {}", self.name, self.time);

Lessons Learned

1. Keep the Boilerplate Minimal

fn main() {
    // Minimal setup
    let app = App::new();
    app.run();
}

2. Start Simple, Add Complexity

My first version just showed one city. Then two. Then five.
Each step compiled before moving forward.


3. Use cargo check Constantly

cargo check   # Fast syntax checking without building
cargo clippy  # Linting suggestions
cargo fmt     # Auto‑formatting

4. Read the Compiler Suggestions

Rust’s compiler often suggests fixes:

help: consider borrowing here
  |
5 |     render(&time);
  |            +

FAQ: 흔히 묻는 러스트 학습자 질문

왜 러스트는 배우기 어려운가?

러스트는 나중에 충돌을 디버깅하는 대신 메모리 관리를 미리 생각하도록 강요합니다. 학습 곡선이 가파르지만 전체 버그 종류(사용‑후‑해제, 데이터 레이스, 널 포인터 역참조)를 예방합니다.

러스트에서 생산성을 갖추려면 얼마나 걸릴까?

  • 2–3주 기본 문법
  • 2–3개월 편안한 개발

이 프로젝트(6주)는 실제 문제를 해결해야 했기 때문에 어떤 튜토리얼보다도 많은 것을 가르쳐 주었다.

먼저 C++를 배워야 할까?

아니다. 러스트의 소유권 시스템은 독특합니다. C++ 습관은 오히려 러스트를 더 어렵게 만들 수 있습니다. 바로 러스트부터 시작하세요.

코드가 컴파일되지 않으면 어떻게 할까?

초보자에게는 정상 – 나는 전체 시간의 약 70 %를 컴파일러 오류와 싸우는 데 보냈다. 각 오류는 무언가를 가르쳐 줍니다.

언제 .clone()을 사용해야 할까?

여러 개의 소유 복사가 필요할 때. 클론은 성능 비용이 들지만 개발을 진행하게 해줍니다. 나중에 최적화한다:

// Quick solution:
let city_copy = city.clone();

// Optimized later:
let city_ref = &city;

언제 &, &mut, 혹은 참조 없이 사용해야 할까?

SymbolMeaning
&읽기 전용 대여 (가장 일반적)
&mut독점 쓰기 접근
(none)소유권 이전 (값을 소모)

&부터 시작하고, 수정이 필요하면 mut를 추가하며, 필요할 때만 소유권을 이전한다.

이 프로젝트의 성능 인사이트

메모리 사용량

  • 컴파일된 바이너리: ~4 MB (최적화된 릴리스 모드)
  • 런타임 메모리: 5개의 타임존 카드에 대해 ~8 MB
  • 가비지 컬렉터 오버헤드 없음

시작 시간

  • 콜드 스타트: M1 Mac에서 ~200 ms
  • 핫 스타트: ~50 ms

업데이트 효율성

60초마다 업데이트 검사가 매 렌더 프레임에서 실행되지만 필요할 때만 재계산됩니다—Rust의 제로 코스트 추상화를 보여줍니다.

실제 도움이 된 자료

필수 학습 경로

  • The Rust Book (공식) – 1‑10장 먼저 읽기
  • Rust by Example – 실습 코드 스니펫
  • Rustlings – 올바르게 작성하면 컴파일되는 인터랙티브 연습문제
  • This project – 개념의 실제 적용

막혔을 때

  • Rust Users Forum – 초보자를 위한 친절한 커뮤니티
  • r/rust subreddit – 매일 질문 스레드
  • Rust Discord – 실시간 도움
  • Compiler errors – 진지하게, 두 번 읽어보세요

GPUI 전용 자료

  • Zed GitHub repo – 소스 코드 예시
  • crates/gpui/examples/에 있는 GPUI 예시
  • Zed의 자체 코드베이스 – 프로덕션 GPUI 사용

Source:

가공되지 않은 진실: 나의 시간 투자

시간 분배 (예상)

ActivityHours
문서 / 튜토리얼 읽기40
컴파일러 오류와 싸우기60
이해 후 다시 작성하기15
실제 기능 코딩10
“왜 창이 보이지 않지?”8
Total133

그럴 가치가 있었나요?

네.

  • 두 번째 Rust 프로젝트는 3 일 걸렸습니다.
  • 세 번째는 몇 시간 안에 끝났습니다.

첫 주에 몇 시간을 잡아먹던 컴파일러 오류가 이제는 몇 분이면 해결됩니다. Rust의 난이도는 초기 단계에 집중돼 있습니다; 학습 비용을 한 번만 지불하면 영원히 혜택을 누릴 수 있습니다: segfault 없음, 데이터 레이스 없음, 예기치 않은 런타임 패닉 없음.

다음 단계: 프로젝트 확장하기

초보자 친화적인 추가 기능

  • 도시를 더 추가하기 (패턴을 그대로 복사)
  • 색상 / 스타일 변경 (RGB 값을 수정)
  • 날짜 표시 추가 (time을 확장하여 날짜 포함)

중급 과제

  • 설정 파일에서 도시를 읽기 (파일 I/O 학습)
  • 도시를 동적으로 추가 / 제거 (UI 상태 관리 학습)
  • 아날로그 시계 표시 (그리기 API 학습)

고급 기능

  • 시스템 트레이 통합
  • 서머타임 처리
  • 도시당 다중 시간대 지원
  • 네트워크 시간 동기화

실제 교훈: 고난을 받아들여라

Rust 코드가 첫 시도에 바로 컴파일된다면, 당신은 배우는 것이 아니라 베끼는 것입니다.
빌려주기 검사기 오류, 이해되지 않는 수명 주석, “왜 이걸 이동시킬 수 없지?” 라는 디버깅에 쏟는 시간—그것이 바로 학습 과정입니다.

내 시간 앱은 이제 완벽히 동작합니다. 메모리 누수도 없고, 레이스 컨디션도 없으며, 정의되지 않은 동작도 없습니다. 컴파일러가 첫 실행 전에 이를 보장했습니다.

내가 “잃은” 시간은 실제로는 잃은 것이 아니라, 프로덕션에서 당신의 시간을 존중하는 언어를 이해하는 데 투자한 시간입니다.

프로젝트 저장소

직접 빌드해보고 싶나요? 전체 코드는 위 문서에 있습니다. 간단한 버전(한 도시)부터 시작해 컴파일을 성공시키고, 점차 복잡성을 추가하세요.

기억하세요: 모든 Rust 개발자는 컴파일 오류에 몇 시간을 보냈습니다. 초보자와 전문가의 차이는 전문가가 더 많은 실수를 했다는 점입니다.

Rust 1.75+, GPUI(Zed 프레임워크), 결단력, 그리고 빌림 검사기와의 건강한 관계로 구축되었습니다.

“컴파일러는 여러분에게 까다롭지만, 여러분의 사용자는 그럴 필요가 없습니다.” – Rust 커뮤니티 지혜

Back to Blog

관련 글

더 보기 »