팀 라이빗은 Tuist를 사용해서 모듈화 아키텍처를 구성하고 있어요.
기능별로 모듈을 분리하면 빌드 시간 단축, 의존성 관리, 코드 재사용성 측면에서 장점도 많은데요!
그러나 새로운 기능을 개발할 때마다 모듈을 추가해야 해서 해당 과정이 꽤 번거롭다는 단점도 있었습니다. 😅
아래 사진은 현재 라이빗의 프로젝트 구조를 나타낸 다이어그램인데요. 한눈에 봐도 꽤 모듈이 많은 것을 알 수 있습니다.

위와 같은 모듈 구조를 유지하며 새 모듈을 추가하려면 4개의 파일을 수동으로 수정해야 했어요.
모듈의 하위 타겟(Data, Domain, Feature)을 정의하는 enum을 추가하는 단계입니다.
코드로는 아래와 같이 작성해요!
// MARK: - User Module
public enum UserModule: String {
case userData = "UserData"
case userDomain = "UserDomain"
case userFeature = "UserFeature"
}
TargetID enum에 새 case를 추가하고, name, sourcesPath 등의 computed property에도 case를 추가해야 합니다.
public enum TargetID {
case app
case core(CoreModule)
case login(LoginModule)
case search(SearchModule)
case user(UserModule) // 추가
public var name: String {
switch self {
// ...
case .user(let module): return module.rawValue // 추가
}
}
public var sourcesPath: SourceFilesList {
switch self {
// ...
case .user(let module): // 추가
return ["\(module.rawValue)/Sources/**"]
}
}
}
프로젝트 레벨의 case를 추가합니다.
public enum ProjectID: String, CaseIterable {
case app = "App"
case core = "Core"
case dsKit = "DSKit"
case search = "Search"
case login = "Login"
case user = "User" // 추가
public var name: String { rawValue }
public var path: Path {
.relativeToRoot("Projects/\(rawValue)")
}
}
다른 모듈에서 의존성을 추가할 때 사용할 헬퍼 함수를 작성합니다.
extension TargetDependency {
// ...
public static func user(_ module: UserModule) -> TargetDependency {
return .project(target: module.rawValue, path: ProjectID.user.path)
}
}
모듈 하나 추가하는데 4개 파일, 10군데 이상을 수정해야 한다는 점에서 휴먼 에러가 발생하기도 쉽고, 무엇보다 귀찮은 게 컸습니다.
위와 같은 과정을 명령어 하나만으로 딸깍! 하고 자동화할 수 있다면 작업 시간은 얼마나 감소할까요?
기존에는 한눈에 봐도 복잡한 수정 과정과 파일을 찾는 과정, 작성하고 휴먼 에러가 났을 경우 해당 에러를 해결하는 시간까지 족히 10분은 걸렸던 것 같습니다. (에러가 날 경우 더 오랜 시간이 걸리는 경우도 있었어요... ㅜㅜ)
그래서 대화형 Shell Script를 만들어서 이 과정을 자동화해보자! 라는 생각을 했고, 실행에 옮겼습니다.
먼저 모듈 생성 스크립트를 생성하고, 대화형 인터페이스를 구성해 다른 팀원들도 편하게 사용할 수 있도록 처리했는데요.
완성한 모듈 생성 스크립트
#!/bin/bash
# Tuist 모듈 자동 생성 스크립트 (대화형)
# 사용법: ./Scripts/create_module.sh
set -e
# 색상 정의
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
터미널에서 색상을 사용해 시각적으로 보기 좋게 만들었어요. 사용자 경험도 중요하니까요! 😎
# 모듈 이름 입력
echo -e "${BOLD}${BLUE}📦 모듈 이름을 입력하세요${NC}"
echo -e "${YELLOW} (예: User, Concert, Ticket)${NC}"
read -p " 모듈 이름: " MODULE_INPUT
# 입력 검증
if [ -z "$MODULE_INPUT" ]; then
echo -e "${RED}❌ 모듈 이름을 입력해주세요.${NC}"
exit 1
fi
# 이름 정규화 (첫 글자 대문자)
MODULE_NAME_UPPER="$(echo "${MODULE_INPUT:0:1}" | tr '[:lower:]' '[:upper:]')${MODULE_INPUT:1}"
MODULE_NAME_LOWER="$(echo "${MODULE_INPUT:0:1}" | tr '[:upper:]' '[:lower:]')${MODULE_INPUT:1}"
사용자가 user라고 입력하든 User라고 입력하든 자동으로 대소문자를 정규화하는 코드를 추가해 휴먼 에러를 방지하고 편리성을 높이려고 했어요.
여러 명이 함께 사용하는 스크립트이기 때문에 한명에게 맞춰진 스크립트 구조보다는 모두가 함께 사용할 수 있는 범용적인 스크립트를 만들기 위해 노력했던 것 같습니다.
Swift 파일의 특정 위치에 코드를 삽입하기 위해서는 awk라는 키워드를 사용해야 했는데요.
아래와 같이 작업해 적절한 위치에 마크 주석과 새로운 enum을 모두 삽입할 수 있었습니다.
awk -v upper="$MODULE_NAME_UPPER" -v lower="$MODULE_NAME_LOWER" \
-v data="$CREATE_DATA" -v domain="$CREATE_DOMAIN" -v feature="$CREATE_FEATURE" '
/\/\/ MARK: - External Dependency/ {
print ""
print "// MARK: - " upper " Module"
print ""
print "public enum " upper "Module: String {"
if (data == "true") print " case " lower "Data = \"" upper "Data\""
if (domain == "true") print " case " lower "Domain = \"" upper "Domain\""
if (feature == "true") print " case " lower "Feature = \"" upper "Feature\""
print "}"
print ""
}
{ print }
' "$CONSTANT_FILE" > "$TEMP_FILE"
mv "$TEMP_FILE" "$CONSTANT_FILE"
이번 작업에서 가장 중요하게 다뤘던 부분은 모듈이 잘못 삭제되거나 생성될 경우 복구할 수 있는 기능이 필요하다는 점이었습니다.
이를 구현하기 위해 작업 시 백업을 생성하고, 롤백 함수를 구현해 스크립트 실행 시 예전 백업 여부를 확인하고 롤백 유무를 확인하도록 했습니다.
# 백업 디렉토리
BACKUP_DIR="$PROJECT_ROOT/.module_backup"
# 백업 생성
echo -e " ${YELLOW}[0/5]${NC} 백업 생성 중..."
mkdir -p "$BACKUP_DIR"
cp "$CONSTANT_FILE" "$BACKUP_DIR/"
cp "$TARGET_ID_FILE" "$BACKUP_DIR/"
cp "$PROJECT_ID_FILE" "$BACKUP_DIR/"
cp "$DEPENDENCY_FILE" "$BACKUP_DIR/"
echo "$MODULE_NAME_UPPER" > "$BACKUP_DIR/module_name.txt"
# Projects 디렉토리도 백업
if [ -d "$MODULE_DIR" ]; then
cp -r "$MODULE_DIR" "$BACKUP_DIR/Projects_${MODULE_NAME_UPPER}"
fi
위와 같이 백업 디렉토리를 생성하고 백업을 생성, Projects 디렉토리를 백업하는 함수도 만들어두었습니다.
rollback() {
echo -e "${YELLOW}⏪ 롤백 중...${NC}"
if [ -d "$BACKUP_DIR" ]; then
cp "$BACKUP_DIR/Module+Constant.swift" "$CONSTANT_FILE"
cp "$BACKUP_DIR/Module+TargetID.swift" "$TARGET_ID_FILE"
cp "$BACKUP_DIR/Module+ProjectID.swift" "$PROJECT_ID_FILE"
cp "$BACKUP_DIR/TargetDependency+Extension.swift" "$DEPENDENCY_FILE"
# Projects 디렉토리 복원
if [ -f "$BACKUP_DIR/module_name.txt" ]; then
MODULE_NAME=$(cat "$BACKUP_DIR/module_name.txt")
if [ -d "$BACKUP_DIR/Projects_$MODULE_NAME" ]; then
cp -r "$BACKUP_DIR/Projects_$MODULE_NAME" "$PROJECT_ROOT/Projects/$MODULE_NAME"
fi
fi
rm -rf "$BACKUP_DIR"
echo -e "${GREEN}✅ 롤백 완료!${NC}"
else
echo -e "${RED}❌ 백업 파일을 찾을 수 없습니다.${NC}"
fi
}
열심히 만들었지만 실행을 위해서는 매번 터미널에서 ./Scripts/create_module.sh 와 같이 파일 경로를 모두 작성해야 했는데요!
이런 귀찮음과 복잡함을 해결하기 위해 Makefile을 추가해서 make module이라는 간단한 명령어로 실행할 수 있게 했습니다.
.PHONY: module module-delete module-rollback generate clean
# 모듈 생성
module:
@./Scripts/create_module.sh
# 모듈 삭제
module-delete:
@./Scripts/delete_module.sh
# Tuist generate
generate:
@tuist generate
# Tuist clean
clean:
@tuist clean
파일 이름을 찾는 시간 없이 빠르게 make module 명령어 하나면 모듈 스크립트를 생성할 수 있었습니다.
직접 모듈 생성 테스트를 진행한 결과 모듈 생성 시간을 기존 10분에서 30초로 줄이는 성과도 얻을 수 있었습니다!
프로젝트에 스크립트를 통한 자동화를 도입해 본 경험은 처음이었는데요.
반복적인 작업을 자동화하면 실수도 줄이고, 개발에 더 집중할 수 있다는 점을 깨닫게 되었습니다 ㅎㅎ
프로젝트에 스크립트를 통한 자동화를 도입하실 예정이시라면 이 글이 참고가 되었으면 좋겠습니다! 🙌
참고하실 수 있는 제 PR 링크를 함께 전달드리며 글을 마치겠습니다. 감사합니다!