grep

iOS

한 줄로 끝내는 iOS 화면 생성: Scaffold + Makefile

이건우Dubu(두부) / iOS 개발팀여기어때

2025년 5월 26일

원문에서 보기 ↗

안녕하세요, 여기어때 iOS개발팀 두부입니다.

여기어때 iOS 앱은 1년 전인 2024년 5월 Tuist 도입을 통한 모듈화를 진행했고, 현재는 Micro Feature Architecture 기반의 유연한 Feature 개발을 통해 빠르게 성장해 나가고 있습니다.

최근에는 SwiftUI와 MVI Pattern 을 결합한 UI 로직과 상태 관리가 명확히 분리되는 아키텍처를 채택하며 팀 내부 기술 스택은 한층 견고해졌습니다. 이 덕분에 공통 컴포넌트 재사용은 물론, 기능 개발 시 일관된 규칙 적용이 자연스럽게 뒤따르고 있죠.

Photo by Jack Sloop on Unsplash

여기어때 iOS 앱에서는 SwiftUI 화면을 개발할 때 아래 5가지 파일로 책임을 분리하고 있습니다.

  1. Coordinator
  2. HostingViewController
  3. ReducerView (View)
  4. ObservableReducer
  5. Reducer

이렇게 분리된 레이어 덕분에 명확하게 상태를 관리할 수 있지만, 매번 수작업으로 5개의 파일을 생성하고 기본 코드를 채우는 과정은 매우 번거롭습니다. 하지만 이러한 ‘Boilerplate Code’를 자동으로 생성 할 수 있다면, 앞에서 언급한 기본 코드를 반복적으로 채우는 업무 패턴을 역으로 이용해 생산성을 높이는 구조를 만들 수 있지 않을까요?

바로 이 지점에서 Tuist Scaffold 와 Makefile 을 활용한 자동화가 빛을 발합니다.

반복적이고 구조적인 작업을 손으로 하나하나 처리하는 대신, Template 기반 생성과 명령어 한 줄로 빠르게 개발 환경을 구성할 수 있는 방식은 실무에서 큰 생산성 차이를 만들어냅니다.

이 글에서는 Tuist Scaffold와 Makefile을 활용해, 이러한 문제를 어떻게 효율적으로 해결할 수 있었는지 소개드리려 합니다.

Scaffold?

Scaffold는 건설, 건축 등 산업현장에서 쓰이는 임시로 설치한 가시설물 등을 뜻하는데요. 프로그래밍에서의 Scaffolding은 기본 구조를 빠르게 생성하는 자동화된 코드 생성 기술을 의미합니다.

소프트웨어를 개발하다 보면, 기존 아키텍처가 있는 프로젝트에서 개발자가 프로젝트와 일관성 있는 새로운 컴포넌트나 기능을 부트스트랩하고 싶을 때가 생깁니다. Tuist는 이러한 Scaffolding 기술을 Template으로 관리할 수 있게 제공하고 있습니다. Template은 Tuist에서 기본으로 제공해 주는 템플릿을 사용할 수도 있고, 직접 만들어 사용할 수도 있습니다.

Template을 톺아봅시다

Template 생성

먼저, Template을 생성하는 법에 대해서 알아보겠습니다.

  1. 디렉터리 이름과 동일한 .swift파일을 생성합니다. (Manifest file)

이때 주의할 점은, 파일 및 폴더명을 아래 형식에 맞게 설정해주어야 한다는 것 입니다**.**

...
└─ Tuist
    └── Templates
        └── SwiftUIScreen ✅
            └── SwiftUIScreen.swift ✅

✅ Templates 하위 폴더 이름과 그 폴더 안에 있는 .swift 파일 이름이 동일해야 합니다.

Template 정의

Tuist Template의 핵심은 Manifest 라 불리는 .swift 파일입니다. 이 파일에 Template의 메타데이터와 어떤 파일들이 생성될지를 정의하게 됩니다. Manifest file을 구성하는 요소들을 하나씩 살펴보겠습니다.

  1. Template.Attribute (parameter)

command line에서 --name 과 같은 형태로 입력 될 값입니다. .required()일 경우, command line에서 반드시 받아야 합니다. 반면에 .optional()인 경우, command line에서 입력하지 않아도 되며 default값을 설정해줄 수 있습니다.

// SwiftUIScreen.swift

import Foundation
import ProjectDescription

// command line 입력 --name 파라미터에 들어가는 값
// ex) tuist scaffold SwiftUIScreen --name 여기어때
let nameAttribute: Template.Attribute = .required("name")

// command line 입력 --targetModule 파라미터에 들어가는 값
// camelCase는 kebab-case로 자동 치환됩니다.
// ex) tuist scaffold SwiftUIScreen --target-module MyFeature
let targetModuleAttribute = Template.Attribute.required("targetModule")

// command line 입력하지 않아도 되는 값
// `default`값을 통해 자동으로 현재 날짜로 생성
let dateAttribute = Template.Attribute.optional(
    "date",
    default: {
        let formatter = DateFormatter()
        formatter.dateFormat = "M/d/yy"
        return formatter.string(from: Date())
    }()
)

2. Template

생성될 파일의 구조입니다. 사용될 Attribute와 파일들의 path, fileName을 정의합니다.

// SwiftUIScreen.swift

import Foundation
import ProjectDescription

// ... Attributes ... //

let template = Template(
    description: "Adds Coordinator, HostingVC, ReducerView, ObservableReducer, Reducer for a SwiftUI screen inside an existing module",
    attributes: [
        nameAttribute,
        targetModuleAttribute,
        dateAttribute,
        .optional("platform", default: "iOS")
    ],
    items: [
        // Coordinator ✅
        .file(
            path: ".../\(nameAttribute)Coordinator.swift",
            templatePath: "Coordinator.stencil"
        ),
        // HostingViewController ✅
        .file(
            path: ".../\(nameAttribute)HostingViewController.swift",
            templatePath: "HostingViewController.stencil"
        ),
        // ReducerView ✅
        .file(
            path: ".../\(nameAttribute)ReducerView.swift",
            templatePath: "ReducerView.stencil"
        ),
        // ObservableReducer ✅
        .file(
            path: ".../\(nameAttribute)ObservableReducer.swift",
            templatePath: "ObservableReducer.stencil"
        ),
        // Reducer ✅
        .file(
            path: ".../\(nameAttribute)Reducer.swift",
            templatePath: "Reducer.stencil"
        ),
    ]
)

3. Stencil

file 타입의templatePath에 지정된 .stencil 파일은 실제로 생성될 Swift 파일의 ‘뼈대’가 담겨 있습니다.

// ... //

.file(
    path: ".../\(nameAttribute)Coordinator.swift",
    templatePath: "Coordinator.stencil" // ✅ Stencil file
)

Stencil 템플릿 문법을 이용해, Manifest 에서 정의한 Attribute(ex: nameAttribute, targetModuleAttribute, dateAttribute)를 placeholder 형태로 참조하고, 그 자리에 실제 값이 삽입되도록 구성하는 것이죠.

예를 들어, Coordinator.stencil 파일은 이런 식으로 구성될 수 있습니다.

//
//  {{ name }}Coordinator.swift
//  {{ targetModule }}Feature
//
//  Created by author on {{ date }}.
//

import UIKit

public final class {{ name }}Coordinator: Coordinator {
    private var navigationController: UINavigationController?

    public init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    public func start() {
        // ...
    }
}

Tuist Scaffold vs Xcode Template

많은 iOS 개발자분들이 Xcode의 File → New → File… 메뉴에서 제공하는 Xcode Template을 한 번쯤 사용해 보셨을 텐데요. Xcode Template 기능도 “미리 정의된 틀”에서 새 파일을 생성한다는 점에서 Tuist Scaffold와 유사한 면이 있습니다.

하지만 실제 팀 차원에서는 템플릿의 일관된 공유와 재사용, 장기적인 유지보수 관점에서는 관리의 용이성이 중요한 요소가 됩니다.

그리하여 다음과 같은 이유로 Tuist Scaffold를 채택했습니다.

Xcode Template vs Tuist Scaffold

Makefile을 통해 간편하게

이제부터는 tuist scaffold 명령어를 사용하여 Boilerplate Code를 뚝딱 만들어낼 수 있습니다.

tuist scaffold SwiftUIScreen --name=MyPage --target-module=MyPageFeature
tuist generate

하지만 command line에 scaffold 명령어를 입력하고 기다린 후, 매번 tuist generate 를 해주어야 합니다. 또한 scaffold 명령어 문법이 기억나지 않는 팀원분들은 문서를 다시 찾아 보아야 할 텐데요, 이 또한 상당히 번거로운 일이 될 수 있겠죠. 이러한 불편함을 해소하고, 더욱 간편하고 사용자 친화적인 스크립트를 실행할 수 있도록 Makefile을 활용해 봅시다.

Makefile 생성

먼저 Makefile을 Project 최상단 디렉터리에 생성합니다.

App
├── App.xcodeproj/
├── App.xcworkspace/
├── Tuist/
└── Makefile ✅

Makefile 작성

이제 Makefile을 작성해보겠습니다.

  1. help 명령어를 작성하여 팀원 모두가 사용하기 편한 환경 구축하기

Makefile을 사용하는 팀원 모두가 쉽게 명령어를 파악하고 사용할 수 있도록 help 명령어를 추가할 수 있습니다. help target에는 팀 내부적으로 사용하고 있는 명령어에 대한 간단한 설명을 작성했습니다. 여기서 추가적으로.DEFAULT_GOAL을 설정하면 make만 입력했을 때 help가 실행되어 보다 사용하기에 편리한 스크립트를 작성할 수 있습니다.

# parameter없이 `make`만 입력 시 help가 실행되도록 ✅
.DEFAULT_GOAL := help

# 도움말 ✅
help:
 @echo "Scaffold Commands:"
 @echo "   module             새 모듈 생성"
 @echo "                      Usage: make module name=모듈명"
 @echo ""
 @echo "   swiftui-screen     SwiftUI 화면 생성"
 @echo "                      Usage: make swiftui-screen name=화면명 target-module=모듈명"
 @echo ""
 @echo "   uikit-screen       UIKit 화면 생성"
 @echo "                      Usage: make uikit-screen name=화면명 target-module=모듈명"
 @echo ""
  1. .PHONY 설정으로 충돌 방지하기

만약 Makefile의 target과 동일한 이름의 파일이 존재할 경우 충돌이 발생할 수 있습니다. 이를 방지하기 위해 .PHONY에 target들을 명시하여 실제 파일이 아닌 "가짜" 목표임을 알려주는 작업입니다.

# PHONY 설정 ✅
# Phony는 가짜라는 의미로 실제 파일이름이 아닌 target을 의미합니다. 
# make 명령이 실행되는 디렉터리에 Makefile의 target과 같은 이름의 파일이 존재할 경우에 충돌이 발생하는데 .PHONY에 명시하여 이를 회피할 수 있습니다.
.PHONY: module swiftui-screen uikit-screen help
  1. @if 문법을 활용해 target 작성하기

Makefile은 정말 많은 문법들을 가지고 있습니다. 특히 @if 문법을 활용하면 특정 조건에 따라 더욱 유연한 스크립트를 작성할 수 있는데요. 저희 팀에서는 @if 문법을 활용해, 명령어 실행 시 필수 파라미터가 누락된 경우 누락된 부분을 안내하고 실행을 중단하는 방식 으로 처리하고 있습니다. 예를 들어, SwiftUI Screen을 생성할 때는 name과 target-module 두 개의 파라미터가 반드시 필요하기 때문에 아래처럼 조건 분기 처리를 적용할 수 있습니다.

swiftui-screen:
  @if [ -z "$(name)" ] || [ -z "$(target-module)" ]; then \ # @if 문법 사용 ✅
      echo "Error: name과 target-module을 모두 지정해주세요"; \
      echo "Usage: make swiftui-screen name=화면명 target-module=모듈명"; \
      exit 1; \
  fi
  @tuist scaffold SwiftUIScreen --name "$(name)" --target-module "$(target-module)"
  @tuist generate
  @echo "✅ '$(name)' SwiftUI Screen created successfully!" 
  @echo "🥸 생성된 파일의 author 부분을 수정해주세요!" # 안내 문구 추가 ✅

또한, Command 수행 완료 시 필요한 안내 문구를 추가 하여 편의성을 높일 수 있습니다. Scaffold 명령어 외에도 tuist fetch, tuist generate와 같은 기본 명령어들도 Makefile을 통해 단축할 수 있습니다. 예를 들어, make generate와 같이 입력하면 tuist fetch와 tuist generate가 함께 실행되도록 설정할 수 있습니다.

Makefile 전체 코드

.DEFAULT_GOAL := help

.PHONY: generate regenerate clean reset module swiftui-screen uikit-screen help

# 생략...

swiftui-screen:
  @if [ -z "$(name)" ] || [ -z "$(target-module)" ]; then \
      echo "Error: name과 target-module을 모두 지정해주세요"; \
      echo "Usage: make swiftui-screen name=화면명 target-module=모듈명"; \
      exit 1; \
  fi
  @tuist scaffold SwiftUIScreen --name "$(name)" --target-module "$(target-module)"
  @tuist generate
  @echo "✅ '$(name)' SwiftUI Screen created successfully!" 
  @echo "🥸 생성된 파일의 author 부분을 수정해주세요!"

# 생략...

help:
 @echo ""
 @echo "Basic Commands:"
 @echo "   generate           tuist fetch & tuist generate"
 @echo "   regenerate         .xcworkspace & .xcodeproj 삭제 후 fetch & generate"
 @echo "   clean              .xcworkspace & .xcodeproj 삭제"
 @echo "   reset              tuist cache 포함 전체 파일 삭제"
 @echo ""
 @echo "Scaffold Commands:"
 @echo "   module             새 모듈 생성"
 @echo "                      Usage: make module name=모듈명"
 @echo ""
 @echo "   swiftui-screen     SwiftUI 화면 생성"
 @echo "                      Usage: make swiftui-screen name=화면명 target-module=모듈명"
 @echo ""
 @echo "   uikit-screen       UIKit 화면 생성"
 @echo "                      Usage: make uikit-screen name=화면명 target-module=모듈명"
 @echo ""

이제 위와 같이 Template 및 Makefile 스크립트를 모두 작성했다면, 단 한줄로 화면 개발을 위한 Boilerplate Code 파일들을 생성할 수 있습니다!

make swiftui-screen name=화면명 target-module=모듈명 ✅

마치며

이전에 저희 팀에서는 화면 단위로 반복적인 템플릿을 복사&붙여넣기하는 작업은 업무 피로도를 높이는 주요 원인 이었습니다. 하지만 Scaffolding 도입 이후, 매번 반복되던 5개 파일의 Boilerplate Code를 단 한 줄의 커맨드로 자동 생성해 귀찮은 초기 세팅 대신 비즈니스 로직에 더욱 집중할 수 있게 되었죠.

또한, 팀원 간 코드 스타일이나 구조가 달라지는 일도 자연스럽게 줄어들었습니다. 이처럼 Tuist의 Scaffold는 단순히 “파일 찍어내기”를 넘어서 팀 내부적인 개발 워크플로우를 한층 더 견고하고 일관성 있게 만들어주는 강력한 도구입니다.

반복되는 작업, 휴먼에러가 많은 작업을 자동화해, 생산성을 높이고 싶으신 분들에게 이 글이 조금이라도 도움이 되길 바랍니다. 개선점이나 궁금한 사항이 있다면 언제든 댓글 남겨주세요.

읽어주셔서 감사합니다. 🙇🏻‍♂️

Refer.