개발 플랫폼
68fe9166 · 2026-10-23 GitHub 기준.Tuist는 프로젝트 생성, 빌드 캐시, 테스트 선택, 실행 기록 분석을 연결해 팀의 반복 작업을 줄이는 도구 모음입니다. README는 Xcode·Gradle·Bazel 지원을 설명합니다. 실제 저장소에도 Swift CLI, Kotlin Gradle 플러그인, Elixir 웹 서버, Rust 캐시 서비스가 각각 존재합니다.
작은 앱에서는 빌드 버튼을 누르고 기다리면 됩니다. 모듈과 개발자가 늘어나면 같은 라이브러리를 개발자마다, CI 작업마다 다시 컴파일합니다. 설정 파일 충돌, 오래 걸리는 테스트, “어제보다 왜 느려졌지?”라는 질문도 함께 늘어납니다. Tuist가 다루는 대상은 이런 개발 과정 자체의 비용입니다.
이 저장소의 핵심은 빌드 명령 하나를 빠르게 만드는 데서 끝나지 않고, 재사용과 관찰을 하나의 흐름으로 묶는 설계입니다.
각 개발자가 완제품을 조립하는 작업자라면, 빌드 캐시는 이미 검사한 부품을 보관하는 창고입니다. 같은 규격의 부품이 있으면 가져다 쓰고, 규격이 달라졌으면 새로 만듭니다.
여기서 끝나면 “창고에 부품이 많다”는 사실만 압니다. Tuist의 서버는 어떤 빌드가 어떤 부품을 썼고 얼마나 걸렸는지도 기록합니다. 그래서 재사용의 조건을 정하는 일과 그 효과를 측정하는 일이 함께 중요합니다.
| 사용자가 보는 기능 | 내부에서 찾아볼 자리 | 배울 개념 |
|---|---|---|
| 프로젝트 생성 | cli/Sources/TuistKit/Generator/Generator.swift | 설정 → 그래프 → 검증 → 파일 생성 |
| 모듈 캐시 | cli/Sources/TuistCache/CacheGraphContentHasher.swift | 입력 지문과 의존성 추적 |
| 빌드 분석 | server/lib/tuist/builds/workers/process_build_worker.ex | 비동기 작업과 재시도 |
| 기록 대시보드 | server/lib/tuist_web/live/generate_runs_live.ex | 서버 상태와 실시간 화면 연결 |
모든 언어를 먼저 익힐 필요는 없습니다. 함수의 입력·출력, 외부 시스템과 만나는 경계, 실패했을 때의 반환값부터 표시하면 큰 저장소도 읽을 수 있습니다. 첫 목표는 기능 목록을 외우는 것이 아니라, 빌드 한 번의 데이터 흐름을 설명하는 것입니다.
Tuist는 2026-10-23 GitHub Trending Elixir Daily #8에 선정됐습니다. 다만 일일 순위와 스타 5,845개만으로 “어떤 발표 때문에 올랐다”거나 “오늘 몇 개의 스타가 늘었다”고 설명할 수는 없습니다. 아래는 README와 구현 범위를 바탕으로 해석한 기술적 관심 요인입니다.
gradle/과 Kura의 여러 캐시 API가 이를 뒷받침합니다.| 도구 | 공식 자료의 중심 역할 | Tuist에서 더 살펴볼 지점 |
|---|---|---|
| XcodeGen | YAML·JSON 명세와 폴더 구조로 Xcode 프로젝트 생성 | Swift 기반 선언과 생성 과정이 캐시·실행 기록으로 이어지는 연결 |
| fastlane | 앱 서명·스크린샷·베타 배포·출시 자동화 | 출시 절차 앞단의 빌드 결과 재사용과 빌드·테스트 분석 |
| Bazel | 빌드 작업을 명시하고 원격 캐시에 작업 결과와 CAS 객체 저장 | Bazel도 지원 대상으로 두는 캐시·관찰 서비스의 운영 계층 |
| Tuist | 생성·캐시·선택적 테스트·분석·미리보기의 플랫폼 | 여러 기능이 공유하는 인증·저장소·API 계약과 언어별 경계 |
비교 근거: XcodeGen 공식 저장소, fastlane 공식 문서, Bazel 원격 캐시 문서. 표는 각 도구의 중심 역할을 비교합니다. 성능 우열을 측정한 결과는 아니며, fastlane·Bazel과 Tuist를 함께 구성하는 경우도 생각할 수 있습니다.
Tuist의 차별점은 개별 기능의 수보다, 프로젝트 구조·재사용된 결과·실행 기록을 연결해 팀 단위로 다룬다는 데 있습니다.
캐시에 없는 최초 빌드, 자주 바뀌는 모듈, 내려받는 비용이 큰 산출물은 효과가 다릅니다. 선택적 테스트도 입력과 의존성을 제대로 알아야 안전합니다. 이 저장소만 읽고 특정 배수의 속도 향상이나 장애 감소를 보장할 수는 없습니다.
mix.exs와 package.json은 허용할 버전 조건을 적고, mix.lock과 Package.resolved는 선택된 버전을 기록합니다. Elixir의 ~> 1.7은 1.7 계열만 허용하는 표기가 아닙니다. ~> 1.7.0보다 범위가 넓어 1.8도 들어갈 수 있습니다.| 요소 | 실제 버전 근거 | 이 저장소에서의 역할 |
|---|---|---|
| Elixir / Erlang | mise.toml: 1.20.2 / 29.0.3server/mix.exs: Elixir ~> 1.19 | 실행 도구의 고정값과 프로젝트의 허용 범위가 다름 |
| Phoenix | ~> 1.7 → 잠금 1.8.16 | 웹 라우팅·컨트롤러·엔드포인트 |
| LiveView | ~> 1.2.0 → 잠금 1.2.13 | 서버에 상태를 두는 대시보드 |
| Ecto SQL | ~> 3.12 → 잠금 3.13.5 | 관계형 데이터 접근과 마이그레이션 |
| Postgrex | 잠금 0.22.3 | PostgreSQL 연결 드라이버 |
| Oban | ~> 2.21.0 → 잠금 2.21.1 | PostgreSQL 기반 백그라운드 작업 |
| Bandit / Plug | 잠금 1.12.5 / 1.19.5 | HTTP 서버와 요청 처리 파이프라인 |
| ClickHouse 연결 | ch·ecto_ch는 Git 참조잠금 커밋 0acf8c2 / 4554ec2 | 실행 이벤트·분석 데이터 조회 |
| 공유 모듈 | tuist_common·tuist_ex는 로컬 경로 의존성 | 서비스 공통 기능과 Elixir 빌드·테스트 계측 |
버전 출처는 server/mix.exs, server/mix.lock, mise.toml입니다. ch와 ecto_ch는 일반 배포 버전 대신 특정 Git 커밋이 선택되어 있으므로, 버전 번호만으로 재현 환경을 설명하면 빠지는 정보가 생깁니다.
server/lib/tuist/command_events/event.ex는 Ch 타입으로 실행 이벤트를 정의하고, CommandEvents.list_command_events/2는 ClickHouseFlop.validate_and_run!/3을 호출합니다. 따라서 실행 기록을 전부 PostgreSQL에 저장한다고 이해하면 실제 코드와 어긋납니다. PostgreSQL과 분석용 ClickHouse를 구분해서 읽어야 합니다.
| 요소 | 파일의 값 | 읽을 때의 포인트 |
|---|---|---|
| Noora | noora/mix.exs·noora/package.json: 0.81.4 | 서버는 path: "../noora"를 참조하는 컴포넌트 라이브러리 |
| ECharts | server/package.json: ^6.1.0 | 차트 표현에 쓰는 JavaScript 의존성 |
| Cytoscape | 3.33.4 / cytoscape-dagre 4.0.0 | 그래프 표시와 배치에 필요한 의존성 |
| noVNC | @novnc/novnc: 1.5.0 | 브라우저에서 원격 화면을 다루는 기능의 기반 |
| Faro / Scalar | ^2.11.0 / ^1.52.0 | 브라우저 관측 도구 / API 문서 UI 의존성 |
| Node / aube | server/mise.toml: 24.11.0 / 1.6.2 | 자산을 만드는 개발 도구 |
server/assets/app/app.js는 Socket, LiveSocket, Noora를 가져오고 차트·클립보드·터미널 등에 사용할 훅을 등록합니다. 화면의 중심은 HEEx 템플릿과 LiveView이며, JavaScript가 브라우저 상호작용을 보완합니다. 다른 하위 앱에 React 등이 있더라도 주 대시보드의 구조와 섞어서 설명하지 않아야 합니다.
여기에도 흥미로운 차이가 있습니다. server/mix.lock에는 Noora 0.76.0 항목이 남아 있지만, 서버의 명세는 로컬 경로를 가리키고 그 소스 버전은 0.81.4입니다. 잠금 파일의 한 줄을 곧바로 현재 로컬 의존성의 버전으로 읽으면 안 되는 사례입니다.
| 구성 | 버전·설정 근거 | 역할 |
|---|---|---|
| Swift CLI | 루트 Package.swift: tools 6.1, macOS 15 선언 | 설정 로딩·그래프 구성·프로젝트 생성·서버 통신 |
| CLI 라이브러리 | Package.resolved: ArgumentParser 1.8.2, XcodeProj 9.17.5, OpenAPIRuntime 1.12.1 | 명령 파싱·Xcode 파일 모델·API 클라이언트 기반 |
| Kura | kura/Cargo.toml: 0.8.0, Rust edition 2024 | 여러 빌드 도구의 캐시 요청을 처리하는 Rust 서비스 |
| Kura 의존성 | 명세: axum 0.8.9, tokio 1.48.0, rocksdb 0.24.0, tonic 0.14.2 | HTTP·비동기 실행·메타데이터 저장·gRPC |
| CAS 플러그인 | cas-plugin/Cargo.toml: Rust edition 2021, cdylib·rlib, proxy 실행 파일 | Xcode 컴파일 캐시 연동용 라이브러리와 프록시의 빌드 정의 |
| Gradle 연동 | gradle/build.gradle.kts: Gson 2.10.1, OkHttp 4.12.0, Retrofit 2.11.0 | Kotlin으로 작성한 플러그인의 JSON 처리와 HTTP 통신 |
| 네이티브 앱 | app/ Swift, android/ Kotlin | iOS·macOS·Android 클라이언트 |
Cargo의 위 숫자는 명세에 적힌 버전 요구사항입니다. Rust edition 2024도 컴파일러의 정확한 버전 번호를 뜻하지 않습니다. 언어 규칙의 세대와 설치할 도구 버전을 구분하는 습관을 들이면 환경 문제를 줄일 수 있습니다.
server/docker-compose.yml은 postgres:16과 DB 관리 화면을 정의합니다. 서버 컨테이너에는 S3·OAuth·라이선스 관련 설정 자리도 있습니다.server/mise.toml의 ClickHouse는 26.1.2.11-stable입니다. Tuist.ClickHouseVersions는 최소 주 버전 25와 가장 오래된 테스트 대상 25.7.7.68-stable을 따로 둡니다.server/Dockerfile은 Elixir 1.20.2·OTP 29.0.3, Swift 6.2, Node 22 이미지를 나누어 사용합니다. 개발용 Node 24.11.0과 이미지 안의 Node 22는 다른 값입니다.USER nobody로 실행하도록 정의합니다. 이미지에는 Chromium 등 부가 기능용 패키지도 들어갑니다.infra/helm/tuist/values.yaml은 웹 서버와 파서를 소비하는 processor를 구분합니다. processor는 기본 비활성이고 큐 동시성 설정이 별도입니다.infra/runners-controller/go.mod은 Go 1.25.0, controller-runtime v0.20.0, k8s.io/api v0.32.1을 선언합니다. 웹 서비스 바깥의 실행 자원 제어도 별도 모듈로 관리합니다.handbook/package.json은 VitePress ^1.6.4, esbuild ^0.25.10, Wrangler ^4.40.3을 선언합니다. 주 대시보드와 문서 사이트의 프론트엔드 구성은 다릅니다..github/workflows/server.yml에는 PostgreSQL 16, ClickHouse 현재·하한 버전 테스트, Credo·포맷 검사·JavaScript 테스트가 있습니다. cli.yml은 macOS와 Linux 경로를 나누어 다룹니다.2026-10-23의 최신 GitHub 릴리스 태그 helm@0.228.1, 루트의 개발용 Tuist 4.215.0-canary.12, 서버 Mix 프로젝트 버전 0.1.0, Helm Chart.yaml의 0.2.0은 서로 다른 위치에 있는 값입니다. 하나를 골라 “저장소 전체의 버전”이라고 부르면 오해가 생깁니다.
처음에는 산출물을 전달하는 경로와 계정·실행 기록을 관리하는 경로를 나누어 보세요. 실제 배치는 호스팅 방식과 기능 설정에 따라 달라지지만, 아래 지도는 주요 코드 경계를 이해하는 출발점이 됩니다.
Kura의 kura/src/http.rs에 있는 public_routes()는 Xcode CAS, 모듈 업로드·다운로드, Gradle 등 여러 요청을 라우터에 등록합니다. kura/src/main.rs는 Tokio 런타임을 만든 뒤 kura::run()을 실행합니다. 한편 Elixir의 cache/도 별도 서비스로 남아 있으므로, 모든 캐시 기능을 한 서버가 같은 방식으로 처리한다고 가정해서는 안 됩니다.
생성 기능의 진입 흐름은 GenerateService.run(...) → generatorFactory.generation(...) → Generator.generateWithGraph(...)입니다. 서비스는 설정과 캐시 프로필을 정하고, 생성기는 그래프와 출력 파일을 다룹니다.
Generator.swift의 generateWithGraph는 load로 그래프·부수효과·환경을 받고, DescriptorGenerator.generateWorkspace와 writer.write를 호출합니다. 개념 모델을 만드는 단계와 디스크에 쓰는 단계가 드러나므로, 함수가 무엇을 책임지는지 추적하기 쉽습니다.
검증 시에는 environment.initialGraphWithSources ?? graph를 사용합니다. 소스 모듈을 캐시된 프레임워크로 교체한 뒤에는 중복 링크 경고가 생길 수 있어, 소스 기준의 초기 그래프를 검증하려는 의도가 주석과 코드에 함께 나타납니다. 변환 전후의 모델을 모두 보존하는 이유를 읽을 수 있는 대목입니다.
GenerateService가 ConfigLoading, GeneratorFactorying, Opening 같은 프로토콜을 받는 점도 보세요. 파일 로딩·생성·앱 열기라는 외부 동작을 교체할 수 있는 경계로 둡니다. 초보자는 이런 이름을 보며 “구체 구현을 바꾸면 테스트에서 무엇을 흉내 낼 수 있을까?”를 질문하면 좋습니다.
주문서를 곧바로 공장 기계에 전달하지 않고, 먼저 조립도인 그래프로 바꿉니다. 조립도를 검사하고 필요한 부품을 교체한 뒤 최종 작업 지시서를 만듭니다. 프로젝트 생성 도구도 작은 컴파일러처럼 읽으면 구조가 잘 보입니다.
TargetContentHasher.contentHash는 소스뿐 아니라 리소스, 설정, 스크립트, 의존성, 추가 입력 등을 조합합니다. GraphContentHasher는 타깃 간 의존성을 따라 각 타깃의 해시를 계산하는 상위 계층입니다.
바깥의 CacheGraphContentHasher.contentHashes는 선택된 구성에 맞게 그래프 설정을 좁히고, 아래 입력도 더합니다. 다음은 실제 additionalStrings 구성의 발췌입니다.
// cli/Sources/TuistCache/CacheGraphContentHasher.swift
let additionalStrings = [
resolvedConfiguration,
try await SwiftVersionProvider.current.swiftlangVersion(),
try await XcodeController.current.selectedBuildVersion(),
version.rawValue,
]
Xcode 릴리스가 같은 컴파일러를 포함해도 SDK는 달라질 수 있다는 설명이 이 코드의 주석에 있습니다. 그래서 Swift 버전과 Xcode 빌드 버전을 함께 담습니다. 또한 저장소 소유 타깃의 커버리지 조건을 별도 차원으로 반영하는 코드가 있으며, Release 구성과 외부 패키지에는 구분 규칙이 다릅니다.
캐시 설계에서 가장 먼저 지켜야 하는 조건은, 결과를 바꾸는 입력이 달라졌을 때 이전 결과를 잘못 재사용하지 않는 것입니다.
확인할 세부 사항은 순서 안정성입니다. TargetContentHasher는 목적지, 포함 제품 참조, 일부 파일 목록을 정렬합니다. 같은 집합을 다른 순서로 읽었다는 이유만으로 다른 해시가 나오면 불필요한 캐시 실패가 생기기 때문입니다. 반대로 입력을 빠뜨리면 낡은 결과를 재사용하는 오류가 됩니다.
Tuist.Application.start/2는 Supervisor.start_link에 strategy: :one_for_one을 넘깁니다. 이는 감독 대상 자식 프로세스 하나가 실패하면 그 자식을 재시작하는 전략입니다. 다만 네이티브 코드가 OS 프로세스 전체를 종료시키면 이런 BEAM 내부의 감독만으로는 보호할 수 없습니다.
# server/lib/tuist/builds/workers/process_build_worker.ex
use Oban.Worker,
queue: :process_build,
max_attempts: 5,
unique: [
keys: [:build_id],
states: :incomplete,
period: :infinity
]
ProcessBuildWorker.perform/1은 업로드된 아카이브를 읽어 빌드 정보를 저장합니다. 미완료 작업에서는 build_id 기준으로 중복 등록을 제한합니다. 작업 중복 방지 설정이 저장 동작 전체의 멱등성을 자동 보장하지는 않습니다. 함수 내부의 교체·저장 경계도 별도로 확인해야 합니다.
| 문제 | 실제 대응 | 설계에서 배울 것 |
|---|---|---|
| 업로드 직후 객체를 못 찾음 | 초기 두 번은 15초 snooze | 잠시 기다리면 나아질 실패와 영구 실패 구분 |
| 잘못된 아카이브 | {:discard, :corrupt_archive} | 재시도해도 달라지지 않을 입력을 구분 |
| 끝나지 않는 처리 | 작업 제한 5분, 파서 제한 4분 | 안쪽 제한이 먼저 구조화된 오류를 돌려주게 설계 |
| Swift 파서의 런타임 충돌 | 별도 OS 프로세스로 실행 | BEAM 전체의 종료로 번지는 것을 막는 경계 |
XCActivityLogParser.parse/5는 MuonTrap을 거쳐 Swift 실행 파일을 호출하고, Task.yield와 Task.shutdown으로 제한 시간을 적용합니다. 종료 상태가 128보다 크면 {:parser_crashed, status, output}으로 구분합니다. 디렉토리 이름에 nif가 남아 있어도 이 경로의 실제 실행 방식은 외부 실행 파일입니다.
parse/5는 빌드 단계별 기록이 담긴 JSONL을 File.stream!으로 읽고 소비 함수에 넘긴 뒤 after에서 임시 파일을 지웁니다. 따라서 스트림을 나중에 읽으려고 밖으로 가져가면 수명 조건이 깨집니다. 메모리 효율을 위해 지연 처리할 때도 “언제까지 읽을 수 있는가”를 API 계약으로 정해야 합니다.
호스팅 배치에서는 TUIST_MODE=processor로 같은 릴리스를 작업 소비용으로 띄웁니다. RuntimeChildren.open_graph_image_renderer/1은 웹 역할에서만 렌더러를 만들고, Oban.RuntimeConfig.peer_eligible?/1도 :web만 리더 후보로 허용합니다. 역할을 추가할 때 권한과 부가 프로세스를 자동으로 물려주지 않는 명시적 허용 방식입니다.
최신 커밋의 주제는 runs 목록의 페이지 크기를 builds와 맞추는 수정입니다. TuistWeb.API.RunsController.index/2를 보면 기본 크기는 20, 최댓값은 1,000입니다. API 스키마만 바꿨는지, 실제 조회 경로와 테스트까지 연결되는지 함께 읽는 연습에 좋습니다.
# runs_controller.ex에서 확인할 계약
@cursor_order_fields [:ran_at, :id]
# page_size의 스키마 값 발췌
default: 20,
minimum: 1,
maximum: 1000
# 개념 SQL: 실제 생성되는 쿼리를 그대로 옮긴 것은 아님
SELECT * FROM command_events
WHERE project_id = :project_id
AND (ran_at, id) < (:last_time, :last_id)
ORDER BY ran_at DESC, id DESC
LIMIT :page_size;
ran_at만 정렬에 쓰면 같은 시각의 실행이 여러 개일 때 일부를 건너뛸 수 있습니다. 그래서 id를 함께 사용합니다. 컨트롤러는 first/after 또는 last/before를 구성하고, CommandEvents.list_command_events/2에 프로젝트 필터와 내림차순 정렬을 넘깁니다.
server/test/tuist_web/controllers/api/runs_controller_test.exs에는 기본값 20, 101개 기록에 대해 page_size=1000을 허용하는 경우, 1,001을 거부하는 경우가 있습니다. page 매개변수는 여전히 받아들이지만 무시하는 것으로 문서화되어 있어, 이전 클라이언트와의 호환성도 볼 수 있습니다.
고유한 보조 키는 같은 시각의 기록을 구분합니다. 하지만 페이지를 넘기는 동안 데이터가 수정되거나 삭제되는 문제까지 모두 해결하지는 않습니다. 커서 값 자체가 프로젝트 접근 권한을 대신하지도 않습니다. 이 코드에서도 선택된 프로젝트 필터와 인증·인가 플러그를 별도로 확인해야 합니다.
커밋 68fe9166의 Git 추적 경로 19,149개에서, 아래 확장자의 일반 파일을 직접 합산한 물리적 줄 수입니다. .git, 심볼릭 링크, 경로 구성 요소가 third_party·vendor인 파일은 제외했습니다. 테스트·픽스처·생성 코드·주석·빈 줄은 포함하므로, 직접 작성한 제품 코드만의 규모와는 다릅니다.
| 언어·형식 | 파일 수 | 물리적 줄 수 |
|---|---|---|
| Elixir (.ex / .exs) | 5,511 | 957,282줄 · 약 95.7만 |
| Swift | 3,780 | 612,843줄 · 약 61.3만 |
| Go | 636 | 192,045줄 · 약 19.2만 |
| Rust | 175 | 156,330줄 · 약 15.6만 |
| CSS | 249 | 62,364줄 |
| HEEx | 237 | 59,474줄 |
| JavaScript (.js / .mjs) | 272 | 45,271줄 |
| Kotlin (.kt / .kts) | 195 | 30,138줄 |
| TypeScript (.ts / .tsx) | 38 | 8,399줄 |
| 위 확장자 합계 | 11,093 | 2,124,146줄 |
영역을 좁히면 server/의 Elixir가 620,915줄, cli/의 Swift가 539,279줄, kura/의 Rust가 118,056줄입니다. 같은 확장자의 테스트도 포함한 숫자입니다. GitHub의 주 언어 분류와 이 단순 줄 수 집계는 계산 방법이 다르므로, 이 표로 GitHub 언어 비율을 재현했다고 해석하지는 않습니다.
GenerateService.swift, Generator.swift, CacheGraphContentHasher.swift, process_build_worker.ex, runs_controller.ex를 먼저 읽으세요. 다른 모듈이 궁금해질 때 호출된 함수를 따라가면 읽기 범위가 자연스럽게 넓어집니다.
RuntimeChildren는 역할을 받아 필요한 자식 명세 목록을 반환합니다. open_graph_image_renderer(:web)에는 렌더러가 있고, 다른 역할에는 빈 목록이 돌아옵니다. 조건문을 거대한 시작 함수에 모으는 대신, 설정 판단을 독립적인 함수로 만든 형태입니다.
# server/lib/tuist/application/runtime_children.ex 발췌
def marketing_stats(:web), do: [Tuist.Marketing.Stats]
def marketing_stats(_), do: []
# server/lib/tuist/oban/runtime_config.ex 발췌
def peer_eligible?(:web), do: true
def peer_eligible?(_), do: false
실습 아이디어: :web, :processor, :xcresult_processor, 가상의 새 역할을 행으로 둔 표를 만들고, 각 함수가 무엇을 반환할지 손으로 채워 보세요. 새 역할에 기능이 자동으로 켜지는지 확인하면 허용 목록의 장점을 체감할 수 있습니다.
GenerateRunsLive.mount/3는 연결된 소켓에서 프로젝트의 PubSub 채널을 구독합니다. handle_params/3는 URL에서 필터와 정렬 상태를 읽습니다. handle_event("update_filter", ...)는 커서를 초기화하고 push_patch로 URL을 바꾸며, 새 실행 이벤트는 handle_info/2에서 처리합니다.
이미 다음 페이지를 보고 있다면 새 기록이 왔다고 목록을 덮어쓰지 않는 분기가 있습니다. 실시간성이 좋아 보여도 사용자가 읽는 위치를 보존하는 편이 더 나을 수 있다는 판단입니다. 실습 아이디어: “첫 페이지”와 “과거 페이지”에서 이벤트 처리 결과를 각각 그려 보세요.
GenerateService는 async throws로 로딩·생성 실패를 위로 전달합니다. TargetContentHash는 최종 해시뿐 아니라 hashedPaths와 subhashes도 보관합니다. 결과를 숫자 하나로만 남기지 않고 “무엇 때문에 달라졌는지”를 설명할 재료를 함께 두는 모델입니다.
실습 아이디어: contentHash에서 입력 종류를 목록으로 추출하고, 각 입력이 바뀔 때 해시가 바뀌어야 하는 이유를 적어 보세요. 커버리지 설정, 컴파일러 버전, 리소스 파일처럼 소스 코드 바깥의 입력을 빼먹지 않는 것이 목표입니다.
PostgreSQL 기반 Oban 작업과 ClickHouse 기반 실행 이벤트는 쓰임새가 다릅니다. 특히 Tuist.CommandEvents.Event가 Ch 필드로 정의되어 있다는 사실을 Ecto의 문법 모양만 보고 놓치지 마세요. 같은 질의 도구를 쓴다고 같은 데이터베이스라는 뜻은 아닙니다.
실습 아이디어: “프로젝트 생성”, “작업 등록”, “실행 시간 집계”, “아카이브 다운로드”를 네 줄로 적고, 각각 필요한 저장소와 실패 조건을 연결해 보세요. 두 DB를 오가는 작업을 하나의 트랜잭션이라고 가정해도 되는지도 질문해 보세요.
kura/src/http.rs의 public_routes()는 API별 진입점을 한눈에 보여줍니다. kura/src/main.rs에서는 Tokio 런타임의 작업 스레드를 구성하고, Linux용 jemalloc 설정도 검증합니다. 고급 단계에서는 “요청을 처리하는 코드”와 “메모리·디스크 사용량을 제어하는 코드”를 나누어 읽어 보세요.
실습 아이디어: 모듈 캐시 업로드의 시작·부분 업로드·완료 라우트를 찾아 순서도를 만드세요. 업로드가 중단됐을 때 임시 데이터가 누구의 책임인지 kura/src/store.rs와 관련 처리 함수로 따라가는 것이 다음 단계입니다.
언어별 문법을 따로 외우기보다, 입력·상태·외부 효과·실패의 네 가지를 같은 질문으로 읽으면 여러 기술이 하나의 시스템으로 보입니다.
함수 하나마다 “받는 것 / 돌려주는 것 / 바깥에 바꾸는 것 / 실패하면 하는 것” 네 칸만 적어 보세요. 함수 이름을 모두 기억하지 못해도, 어느 경계에서 문제가 생겼는지는 설명할 수 있게 됩니다.
Tuist 전체의 단일 최소 사양을 찾기보다, 읽고 싶은 기능과 실행하려는 구성 요소부터 정하는 편이 정확합니다.
| 목적 | 코드·설정에서 확인되는 조건 | 학습용 자원 제안 |
|---|---|---|
| 소스 탐독 | Git, UTF-8 편집기, 검색 도구면 시작 가능 | 일반 노트북으로 가능. 언어 서버의 전체 색인은 필요한 폴더만 사용 |
| Elixir 서버 개발 | Elixir 1.20.2·OTP 29.0.3, Node 24.11.0·aube 1.6.2, PostgreSQL·ClickHouse와 기능별 저장소 | 4코어·RAM 16GB·SSD 여유 30GB부터 계획. 동시 빌드와 분석 DB가 크면 증설 |
| 전체 이미지 빌드 | Elixir·Swift·Node 빌드 단계와 시스템 라이브러리가 필요 | RAM 32GB·SSD 여유 50GB를 우선 계획. 실제 캐시와 이미지 크기로 재조정 |
| Xcode 프로젝트 기능 | 루트 Swift 패키지는 macOS 15 선언, 저장소의 Xcode 선택값은 27.0 | 해당 Xcode를 설치할 수 있는 Mac. 시뮬레이터 여러 개면 RAM 16GB 이상을 우선 계획 |
| Linux CLI 경로 | cli.yml에 Linux용 빌드 경로가 있음 | 명령별 플랫폼 제한 확인. Xcode 컴파일과 iOS 시뮬레이터는 Mac이 필요 |
| Kura 심화 학습 | Rust edition 2024, RocksDB 및 네이티브 빌드 의존성 | SSD 용량·입출력과 메모리 예산을 함께 고려. 부하 수준별 실측 필요 |
오른쪽 사양은 학습 계획을 위한 추정이며 공식 최소 사양이나 벤치마크 결과가 아닙니다. 정확한 요구량은 데이터 보존 기간, 캐시 크기, 작업 동시성, 실행 기능에 따라 달라집니다. 운영 용량은 개발용 표를 그대로 적용할 수 없습니다.
기준 커밋의 셀프호스팅 문서는 유효한 유료 라이선스가 필요하고 Enterprise 조직 대상이라고 명시합니다. Tuist.Application.start/2에도 Tuist.License.assert_valid!() 호출이 있습니다. 서버 배포를 계획한다면 하드웨어 표와 함께 이 조건도 확인해야 합니다.
mise.toml을 함께 봅니다. 예를 들어 루트 Helm은 4.2.0, 서버 쪽 설정에는 3.16.3이 있습니다.redis_url() 설정이 있을 때 추가되는 분기가 있습니다.GenerateRunCommand.swift는 #if os(macOS)로 감싸져 있습니다. “CLI가 Linux에서도 빌드됨”과 “모든 명령이 Linux에서 동작함”은 다른 주장입니다.server/Dockerfile은 빌드 단계에서 ELIXIR_ERL_OPTIONS="+S 2"를 설정합니다. 주석은 병렬 빌드 단계가 겹치며 16GB 실행 환경에서 메모리가 부족했던 상황을 설명합니다. 이는 전체 서비스의 최소 RAM이 16GB라는 뜻이 아니라, 병렬 작업 수가 메모리 최고점을 바꾼다는 근거입니다.
infra/helm/tuist/values.yaml의 processor 기본 설정은 replicaCount: 2, queueConcurrency: 5, bazelTestQueueConcurrency: 1입니다. 해당 기능은 기본 비활성이며 resources: {}라서 이 파일만으로 CPU·메모리 최솟값을 계산할 수는 없습니다.
# 큐 슬롯 수를 생각하는 계산 예시
process_build 동시 처리 슬롯 = processor 복제본 수 × 큐 동시성
= 2 × 5 = 10
# 슬롯 10개가 CPU 10개를 뜻하지는 않음
메모리 최고점 ≈ 작업별 사용량 × 실행 중 작업 수 + 런타임·캐시 비용
로그 압축 해제, 외부 파서, DB 연결, 임시 파일을 동시에 사용하므로 CPU만 보고 병렬도를 높이면 다른 자원이 먼저 고갈될 수 있습니다. 학습 단계에서도 “빠르게”와 함께 “동시에 몇 개까지”를 질문해 보세요.
아래 과제는 소스 탐독과 독립적인 작은 모델만으로 시작할 수 있습니다. 각 과제의 완료 조건까지 남기면 “읽어 봤다”에서 “설명할 수 있다”로 넘어갈 수 있습니다.
목표: tuist generate가 명령 인자를 받아 프로젝트 파일을 기록하기까지 호출 경로를 그립니다.
GenerateRunCommand와 GenerateService.run을 찾습니다.Generator.generateWithGraph에서 로딩·검증·출력 함수를 표시합니다.완료 조건: 화살표마다 실제 함수명 하나와 입력·출력 모델 하나를 쓴 그림. noOpen이 생성 자체와 생성 후 앱 열기 중 어느 동작에 영향을 주는지도 설명합니다.
# 저장소 루트에서 읽기 전용으로 경로 찾기
rg -n 'func run|GenerateService' cli/Sources/TuistGenerateCommand
rg -n 'generateWithGraph|writer.write' cli/Sources/TuistKit
sed -n '55,105p' cli/Sources/TuistKit/Generator/Generator.swift
목표: “이 변경은 캐시를 무효화해야 하는가?”를 소스의 입력 목록으로 설명합니다.
TargetContentHasher.contentHash에서 입력을 다섯 종류 이상 찾습니다.완료 조건: 다섯 행 이상의 판단표. 특히 집합처럼 취급하는 파일 목록은 단순 순서 변경만으로 해시가 바뀌지 않아야 한다는 이유를 설명합니다.
# 설명용 모델의 입력 예시. 실제 Tuist 키 형식은 아님
입력 A: 구성=Debug, 소스=H1, 의존성=H2, Xcode=버전A
입력 B: 구성=Debug, 소스=H1, 의존성=H2, Xcode=버전B
질문: 소스가 같아도 A의 결과를 B에서 재사용해도 될까?
근거: CacheGraphContentHasher의 additionalStrings
목표: 정렬 키 하나와 복합 키의 차이를 종이 또는 별도 배열 모델로 확인합니다.
ran_at에 ID가 다른 기록 세 개를 만듭니다.(ran_at, id)를 함께 비교하고 누락 여부를 다시 확인합니다.완료 조건: 누락이 생기는 예시 하나와 해결된 예시 하나. 이어서 runs 컨트롤러 테스트의 20·1,000·1,001이 각각 어떤 계약을 검증하는지 적습니다.
목표: ProcessBuildWorker.perform/1과 XCActivityLogParser의 실패 경계를 연결합니다.
완료 조건: 실패 유형 다섯 개 이상의 표와 “큐 중복 방지만으로 충분하지 않은 이유” 한 문단. 네이티브 파서의 OS 프로세스 종료와 BEAM 프로세스 실패도 구분합니다.
목표: GenerateRunsLive에서 사용자의 탐색 위치를 보존하는 규칙을 설명합니다.
완료 조건: 네 단계 이상의 상태표와 두 개의 반례. 화면이 최신이어야 하는 순간과 사용자의 읽기 위치를 지켜야 하는 순간을 구분합니다.
좋은 실습의 결과물은 기능을 실행한 화면만이 아니라, 입력이 바뀌거나 실패했을 때 어떤 일이 일어나야 하는지 설명하는 근거입니다.
| 주차 | 배울 내용 | 읽을 소스 | 남길 결과물 |
|---|---|---|---|
| 1주차 | Git 탐색, 명세·잠금 파일, 함수 입력·출력 | README.md, server/mix.exs, server/mix.lock, Package.resolved | 선언 범위·선택 버전·도구 버전 비교표 |
| 2주차 | Swift 기본 모델, 프로토콜, 그래프, 비동기 호출 | GenerateService.swift, Generator.swift | 명령 → 그래프 → 출력 호출 지도 |
| 3주차 | 해시, 결정성, 의존성 전파, CAS | TargetContentHasher.swift, CacheGraphContentHasher.swift | 캐시 입력 목록과 변경 판단표 |
| 4주차 | Elixir 패턴 매칭, OTP 감독, Oban 작업 | application.ex, runtime_children.ex, process_build_worker.ex | 역할별 자식 프로세스 표와 실패 행렬 |
| 5주차 | HTTP 계약, Ecto·ClickHouse, 커서 조회, LiveView | runs_controller.ex, command_events.ex, generate_runs_live.ex | 복합 커서 예시와 URL 상태 전이표 |
| 6주차 | Rust 비동기 처리, 컨테이너, 큐 동시성과 자원 | kura/src/main.rs, kura/src/http.rs, server/Dockerfile, Helm values | 배치도와 자원 병목 가설 3개 |
로드맵의 마지막 목표는 모든 기술을 사용해 보는 것이 아니라, 하나의 변경이 캐시·서버·화면에 미치는 영향을 근거로 설명하는 것입니다.
같은 소스인데 Xcode가 달라지면 왜 캐시 키가 바뀌어야 할까요? Swift 파서를 별도 프로세스로 실행하는 이유는 무엇일까요? 같은 시각의 실행 세 개를 페이지 크기 두 개로 어떻게 빠짐없이 넘길까요? 각 답에 함수명 하나를 붙여 보세요.
| 용어 | 쉬운 설명 |
|---|---|
| 모노레포 | 여러 앱·서비스·라이브러리를 한 저장소에서 관리하는 방식. Tuist의 CLI·서버·Kura가 함께 있습니다. |
| 매니페스트 | 만들고 싶은 프로젝트의 구조와 설정을 적은 선언. 여기서는 Project.swift 등이 출발점입니다. |
| DSL | 특정 문제를 편하게 표현하기 위한 언어·표현 규칙. Swift 문법으로 프로젝트 구조를 적는 것도 이런 접근입니다. |
| 타깃 | 빌드해 얻으려는 앱·프레임워크·테스트 같은 단위입니다. |
| 의존성 그래프 | 어떤 타깃이 어떤 타깃을 필요로 하는지 화살표로 나타낸 관계도입니다. |
| 해시 | 내용을 일정한 길이의 지문으로 요약한 값. 입력 변화 감지와 객체 식별에 사용합니다. |
| 결정성 | 같은 입력에 같은 결과가 나오는 성질. 파일 목록 정렬과 도구 버전 관리가 중요해집니다. |
| 캐시 무효화 | 입력이 바뀌어 이전 결과를 더 이상 재사용하면 안 된다고 판단하는 일입니다. |
| CAS | 내용의 해시를 주소처럼 쓰는 저장소. 파일 이름과 별개로 같은 내용을 식별합니다. |
| Action cache | 빌드 작업의 입력 지문과 작업 결과 메타데이터를 연결하는 표. CAS와 구분됩니다. |
| 선택적 테스트 | 변경 내용과 의존 관계를 바탕으로 실행할 테스트를 고르는 방법입니다. |
| BEAM | Elixir·Erlang 코드가 실행되는 가상 머신. 내부에 많은 경량 프로세스를 둘 수 있습니다. |
| OTP | 프로세스 구성·감독·재시작 등 장기 실행 시스템을 만드는 Erlang 생태계의 기반입니다. |
| Supervisor | 자식 프로세스를 감시하고 정해진 전략으로 재시작하는 감독자입니다. |
| Oban | PostgreSQL에 작업을 기록하고 소비·재시도하는 Elixir 작업 큐 라이브러리입니다. |
| 멱등성 | 같은 작업을 반복해도 의도한 최종 상태가 달라지지 않는 성질. 작업 중복 방지와는 별도입니다. |
| 재시도 / snooze | 실패 뒤 다시 처리하거나 잠시 뒤로 미루는 동작. 원인에 따라 기다릴지 포기할지 정합니다. |
| NIF | BEAM에서 네이티브 함수를 부르는 인터페이스. 이 글의 xcactivitylog 파서는 이름과 달리 외부 실행 파일로 분리되어 있습니다. |
| LiveView / HEEx | 서버 상태로 화면을 갱신하는 Phoenix 방식과 그 HTML 템플릿 형식입니다. |
| PubSub | 발행된 이벤트를 구독자에게 전달하는 방식. 새 실행을 화면에 알릴 때 사용합니다. |
| Ecto | Elixir에서 데이터 모델·검증·질의를 표현하는 도구. 어댑터에 따라 연결하는 DB가 달라집니다. |
| ClickHouse | 많은 이벤트를 모아 집계·분석하는 데 사용하는 열 지향 데이터베이스입니다. |
| 커서 페이지네이션 | “몇 번째 행부터” 대신 “마지막으로 본 정렬 값 다음부터” 조회하는 방식입니다. |
| 보조 정렬 키 | 시간처럼 중복될 수 있는 값이 같을 때 순서를 확정하는 추가 키. runs API는 ID를 함께 씁니다. |
| OpenAPI | 요청·응답·매개변수를 기계가 읽을 수 있게 표현하는 API 명세입니다. |
| 잠금 파일 | 해결된 의존성 버전·참조를 기록하는 파일. 명세의 허용 범위와 비교해 읽습니다. |
| Tokio / Axum | Rust의 비동기 실행 기반과 그 위에서 HTTP 요청을 다루는 웹 프레임워크입니다. |
| RocksDB | 프로세스에 포함해 사용할 수 있는 키·값 저장 엔진. Kura의 저장 계층 의존성입니다. |
| Helm / Pod | Kubernetes 배포 설정을 묶는 도구 / 컨테이너가 실행되는 기본 배치 단위입니다. |
| 관찰 가능성 | 로그·지표·추적·이벤트로 시스템 안에서 일어난 일을 설명할 수 있는 정도입니다. |
| 항목 | 확인 내용 |
|---|---|
| 저장소 | tuist/tuist · 설명: “Your platform team, as a service” (플랫폼 팀을 서비스로 제공) |
| 선정 | GitHub Trending Elixir Daily #8 · 2026-10-23 |
| 스타 / 포크 | 5,845 / 780 · 2026-10-23 GitHub 기준 |
| 주 언어 / 생성일 | Elixir / 2018-04-16 |
| 최신 릴리스 | helm@0.228.1 · 2026-10-23 · Helm 배포 태그이며 CLI 버전과 구분 |
| 최근 push | 2026-10-23 · GitHub 기준 |
| 최근 커밋 | 68fe9166 · 2026-10-23fix(server): align runs page size limit with builds (#14682)뜻: runs 페이지 크기 제한을 builds와 맞춤 |
| 코드 규모 | 선택한 9개 언어·형식 합계 약 212.4만 줄, 11,093개 파일. 테스트·생성 코드 포함. 전체 추적 경로는 19,149개 |
| GitHub 라이선스 필드 | NOASSERTION · 단일 라이선스로 해석하지 않고 하위 파일을 확인해야 함 |
| 라이선스 파일 | 루트의 기본 안내는 MIT. server/LICENSE.md 머리말은 FCL-1.0-MIT, kura/LICENSE.md는 AGPL v3, kura/Cargo.toml은 AGPL-3.0-only, atlas/LICENSE.md는 MPL-2.0 |
| 분석 범위 | 기준 커밋의 소스·의존성·빌드·CI 설정. 실제 처리량·캐시 절감률·설치 성공 여부는 검증 범위 밖 |
라이선스의 경우 루트 LICENSE.md의 앞부분은 server·kura·atlas를 MPL-2.0 예외로 설명하지만, 뒤쪽 Kura 안내는 AGPL v3 only라고 적습니다. 실제 서버 라이선스 파일 머리말도 FCL-1.0-MIT여서 루트 개요와 다릅니다. Kura 파일과 Atlas 파일까지 경로별로 구분해야 하는 상태입니다.
이 저장소에서 오래 가져갈 지식은 특정 도구의 사용법보다, 재사용의 정확성을 지키고 실패의 영향을 좁히며 결과를 설명하는 설계 방식입니다.
일일 트렌딩 선정 사실은 급상승의 원인이나 장기 인기도를 증명하지 않습니다. 기능 소개는 README의 설명과 소스에서 확인되는 구조를 구분해 읽어야 하며, 캐시 속도 향상 수치와 운영 안정성은 실제 측정이 필요합니다.
여기 인용한 함수와 설정은 커밋 68fe9166 기준입니다. 공개 문서의 현재 내용은 이후 바뀔 수 있고, 잠금 파일·로컬 경로 의존성·개발 도구·컨테이너의 버전도 서로 다를 수 있습니다. 코드에 테스트가 있다는 사실과 그 환경에서 테스트가 통과한다는 사실은 구분해야 합니다.
라이선스는 루트 안내와 일부 하위 파일이 일치하지 않습니다. 이 글의 표는 파일에 적힌 내용을 비교한 것이며, 저장소 전체에 동일한 이용 조건이 적용된다는 의미가 아닙니다. 줄 수 역시 코드 품질이나 학습 난이도의 점수로 읽지 마세요.