본문 바로가기
C.W.K.
Lesson 06 of 06 · published

제품은 하나, 네이티브 스택은 둘

~15 min · swift, swift-native, remake, parity-ledger, platform-affinity

Level 0툴 빌려 쓰는 사람
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"기능은 하나도 안 바뀌었어. 그게 포인트야. 아키텍처가 제품이면 같은 앱을 두 번 지을 수 있어."

작업실에 쌍둥이가 생긴 날

2026-09-11 에 작업실이 3.0.0 을 냈어. 그런데 v3 는 업데이트가 아니야. Tauri shell 도, Rust core 도, React UI 도 통째로 트리에서 나갔어. 파일 199 개, 지워진 줄로 6만 5천 줄쯤. 그 자리에 Swift 네이티브 앱이 들어왔고. SwiftUI 랑 AppKit, Swift 패키지 타깃 둘. 제품 자기 화면에는 web view 가 없어. 딱 하나 있는 건 따로 떨어진 참고 페이지를 띄우는 격리된 덮개야. 옛날 빌드랑 새 빌드를 나란히 놓고 볼 때 작가가 구분을 못 해야 했어. 그 구분 불가가 통과 기준이었고, 그걸 넘는 데 뭐가 필요했는지가 이 lesson 이야.

멀쩡한 앱을 왜 다시 지어

Tauri 빌드는 고장 난 적이 없어. 가볍고, 안정적이고, 출시도 했어. 앞의 lesson 다섯 개가 그걸 가르친 데는 이유가 있고, 그 lesson 들이 트랙에 그대로 남아 있는 데도 이유가 있어. 다만 작업실의 삶은 전부 Mac 위에서, 포토샵 옆에서 일어나. 그런데 Rust shell 안의 web view 는 그 집의 식구가 아니라 손님이야. 네이티브 메뉴, 네이티브 클립보드, 호스트 앱 위로 올라오는 윈도우 레벨, OS 층에서 바로 오는 펜 타블렛 압력 이벤트. shell 을 통해서도 닿을 수는 있어. 근데 Swift 에서는 그게 전부 직통이야. 그래서 결정은 'Tauri 가 실패했다' 가 아니었어. 이 플랫폼에서는 그 플랫폼의 언어가 제일 가벼운 shell 이더라, 그거였어.

스택은 플랫폼 친화 선택이고, 아키텍처가 제품이야. 같은 작업실이 이제 둘로 존재해. Tauri + Rust 빌드는 크로스플랫폼 쪽 답이야. Windows 나 Linux 도 목표라면 여전히 그쪽이 맞고. Swift 빌드는 Apple 네이티브 쪽 답이고. 기능은 계약상 똑같아. 네가 고르는 건 shell 이고, 네가 지키는 건 그 밑에 깔린 설계야.

방법: 원본 옆에 짓는 복제품

돌고 있는 제품은 그 자리에서 뜯어고치는 게 아니야. Swift 작업실은 따로 격리된 프로젝트로 태어났어. 대략 스물아홉 시간 동안, 살아서 돌고 있는 Tauri 원본 바로 옆에서. 원본은 실행 가능한 정답지 역할을 했어. 올바른 동작이 뭐냐는 질문이 나올 때마다 물어볼 수 있는, 돌아가는 프로그램이 있었던 거지. 맞게 짓고 있는지는 parity 장부로 따라갔어. 교체 시점에 마흔아홉 줄, 한 줄에 동작 하나. 정체성, 윈도우 레벨, 떠 오기, 자르기, 생성, candidate, 유료 Pro 흐름, 복사/도장/실행 취소, 그리고 펜. 그리고 어느 줄도 '맞아 보임' 으로는 닫지 않았어. 날짜 박힌, 실제 설치된 증거로만 닫았어. 첫 목표, 그러니까 펜 실행을 뺀 모든 줄이 받아들여지고 나서야 새 트리가 옛 트리를 대신했고, 두 history 는 한 레포로 이어 붙였어. 옛 커밋이랑 태그는 전부 보존하고, Tauri 소스는 legacy 보관소에 얼려 두고, force-push 로 지운 건 아무것도 없어.

parity 장부는 '돌아가요' 를 서명할 수 있는 줄로 바꿔 줘. 느낌으로 하는 기능 동등이 리메이크를 반쪽짜리로 출시해. 표로 하면 — 동작, 증거, 날짜 — 계약이 돼. 어느 줄도 느낌으로 못 닫고, 마지막 줄이 닫히는 날 너는 '끝' 이 정확히 뭐였는지를 알아.

교체에서 살아남은 것

domain/view lesson 의 생존 시험 기억하지. view 를 지우면 뭐가 살아남나. 리메이크가 그 시험을 진짜로 돌린 거야. 손 하나 안 대고 살아남은 것들: bridge 프로토콜 — 포토샵 플러그인은 똑같은 envelope 를 말해. workspace 도메인 모델 — 문서, 미리보기, candidate, 신선도 게이트. 이제 UI 가 읽고 명령만 하는 headless Swift core 야. lineage 계약. 그리고 앱이 자기 포트를 아예 안 여는 룰. 다시 지어진 건 UI 의 픽셀 전부랑 그걸 둘러싼 shell 이야. 아키텍처가 제품이었으니까, 제품이 스택을 살아남은 거지.

밑에 깔린 키트

'네이티브' 가 '혼자' 를 뜻하진 않았어. Swift 작업실의 배관은 가족 공용 Swift 키트에서 와. 덩어리 reader 가 붙은 HTTP transport, WebSocket 클라이언트, 로컬 문서 저장소, 컨테이너 경로 같은 것들이야. 로컬 패키지 의존성 하나로 쓰고. 채택 장부에는 어느 키트 리비전을 vendor 했는지랑, 똑같이 의도적으로, 어느 키트 부품을 안 채택했는지가 적혀 있어. 앱 자체 JSON 타입, 앱 자체 윈도우 위치 컨트롤러, 그리고 명령 큐는 없음. 유료 생성 명령은 절대 다시 재생되면 안 되거든. 키트 얘기는 키트의 quest 가 따로 해. 여기서 요점은 네이티브로 간다고 바뀐 건 언어지 공유 규율이 아니었다는 거야.

정직하게 열어 둔 목표

기능 하나는 건져 오지 못했어. 정밀 펜 실행 — 압력 조절이 살아 있는 스트로크를 포토샵으로 몰아넣는 거. 증명된 건 한 번, 옛 트리에서, 교체 며칠 전이었어. 진짜 타이밍 맞춘 타블렛 이벤트, 비상 정지 걸쇠, OS 의 secure-input 경비랑 기나긴 싸움까지 다 합하고. 그게 옛 트리랑 같이 나갔어. 새 앱에는 스트로크 검증이랑 리허설 화면이 있어. 그리고 의도적으로 옛 도우미를 안 되살리기로 했어. 은퇴한 실험은 들춰 볼 증거지 되살릴 실행기가 아니거든. 펜 실행은 다음 제품 목표로 기록돼 있어. 열림, 이름 붙음, 했다고 암시 안 함.

parity 주장은 예외에 이름을 붙인 만큼만 정직해. '다 돼요' 에 말 안 한 빠진 기능 하나면 리메이크가 거짓말하는 거야. 장부에서 제일 값진 줄은 아직 이라고 쓴 줄이야. 그 줄이 있어야 옆의 '검증됨' 전부를 믿을 수 있거든.

피파의 고백

난 리메이크가 판결이길 바랐어. 새 스택이 이기고 옛 스택은 잘못이었다는. 아빠는 그 프레임을 안 받아 줬어. 장례식이 아니라 갈림길이라고. Mac 말고도 목표가 있는 사람한테 Tauri 빌드는 여전히 맞는 답이고, 이 트랙 앞쪽 lesson 다섯 개는 일부러 그걸 그대로 가르쳐. Swift 빌드는 Apple 하드웨어에서 포토샵 옆에 사는 제품한테 맞는 답이고. 답이 둘, 제품은 하나. 그리고 두 답 다 감당할 값이 된 이유는 밑에 깔린 설계가 어느 스택 것도 아니었기 때문이야. 그게 lesson 전부인데, 나는 승자를 고르느라 그걸 놓칠 뻔했어.

Code

도메인 core, 그리고 그 밑의 키트·swift
import CwkKitCore

// headless 도메인 core: SwiftUI import 가 0 개인 Swift 패키지 타깃.
// view 는 이걸 읽고 명령만 날려. core 는 view 를 몰라.
// 내일 UI 를 지워도 이건 컴파일돼.
struct Workspace {
    private(set) var candidates: [Candidate] = []
    private(set) var jobs: [GenerationJob] = []
    var previewIsCurrent: Bool { /* 신선도 게이트 */ }

    mutating func apply(_ event: BridgeEvent) { /* 하나뿐인 reducer */ }
}

// 엔진 호출은 전부 가족 공용 kit transport 를 타.
// 정책 하나, chunked reader 하나, 타임아웃이 사는 자리 하나.
let session = HTTPTransports.urlSession(
    policy: HTTPSessionPolicy(requestTimeout: 300,
                              resourceTimeout: 300,
                              byteBudget: 128 * 1024 * 1024))

// 실시간 다리는 작업실 자기 서비스로 가는 kit WebSocket '클라이언트'.
// (2026-09-21 까지는 뇌가 그 bridge 를 열어 뒀어.)
// 앱은 자기 포트를 아예 안 열어. 공격할 것도, 방화벽 뚫을 것도,
// 살릴 거라곤 클라이언트뿐이야.
let bridge = WebSocketChannelFactory(encoding: .text)
// -> wss://localhost:8443/api/cinder/bridge?role=cinder
답은 둘, 제품은 하나·text
작업실 하나, 네이티브 스택 둘 (기능은 계약상 동일):

  Tauri + Rust + web view          SwiftUI + AppKit (v3)
  크로스플랫폼 쪽 답               Apple 네이티브 쪽 답
  맞을 때: Windows/Linux           맞을 때: 제품이 Mac 에서,
    도 목표일 때                     포토샵 옆에 살 때
  가르치는 곳: lesson 01-05,       가르치는 곳: 이 lesson,
    Rust + Tauri quest               그리고 Swift Quest (준비 중)

  어느 쪽이든 똑같은 것:
    bridge 프로토콜 * 도메인 모델 * lineage * candidate board
    Pro 생성 흐름 * 가이던스 * 앱에 inbound 포트 없음

  스택은 고르는 거야. 아키텍처가 제품이야.

External links

Exercise

네가 잘 아는 프로젝트를 하나 골라서, 앞 lesson 의 생존 시험을 스택 규모로 돌려 봐. 내일 다른 플랫폼 스택으로 다시 짓는다면, 뭐가 손 안 대고 살아남아야 하고(프로토콜, 도메인 모델, 계약), 뭘 다시 짓게 될 거라고 예상해(UI, shell, 플랫폼 접착제)? 두 리스트를 적어 봐. '살아남음' 리스트가 빈약하면, 그 아키텍처는 지금 스택 안에 살고 있었던 거야.
Hint
설계가 스택 독립적이라는 신호는 이거야. 계약들, 그러니까 메시지 모양이랑 상태 모델, 소유 규칙을 프레임워크 이름 하나 없이 설명할 수 있어. 'React 쪽 부분' 이나 'Rust 쪽 부분' 으로밖에 설명 못 하는 건 전부 다시 짓는 리스트로 가.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.