SwiftGen으로 알아보는 선언형 프로그래밍

안녕하세요! 오늘은 SwiftGen을 통해 선언형 프로그래밍에 대해 알아보도록 하겠습니다!

bg

문제 발생

SwiftImage Literal이나 Color Literal에 대해 아시나요?

지금 다니고 있는 회사는 리소스(이미지, 컬러)를 에셋 카탈로그(xcasset)로 관리하고 Image LiteralColor Literal을 통해 리소스를 간접적으로 접근해서 사용하고 있습니다.

그런데 얼마 전부터 알 수 없는 이유로 리터럴이 제대로 작동하지 않는 것을 발견했습니다.

제대로 작동하는 코드
제대로 작동하지 않는 코드

원인을 찾을 수 없어서 결국 대안을 찾기로 했고, 이전에 적용해본 경험이 있는 SwiftGen을 써보기로 했습니다.

써드 파티 도구까지 써야 하나요?

물론 Image LiteralUIImage(named:)로, Color LiteralUIColor로 대체할 수 있습니다.

하지만 후자의 방식은 리터럴 방식보다 단점이 있습니다.

  1. 에셋 카탈로그(이미지, 컬러)가 바뀌거나 삭제되어도 알 방법이 없습니다.
  2. 선언적이지 않습니다.

이러한 이유로 기존의 장점을 유지하면서 안정적인 기술을 찾게 되었고, SwiftGen이 제격이라 판단했습니다.

다른 대안으로는 R.Swift도 있는 것 같은데 사용해본 적이 없지만, 사용 방식은 SwiftGen과 비슷할 것으로 예상됩니다.

SwiftGen을 소개합니다

SwiftGen은 프로젝트에 있는 리소스를 Swift 코드로 자동 생성해서 타입 안정성을 부여하는 도구입니다.

장점

다음은 제가 생각하는 SwiftGen의 장점입니다.

  1. OS, 프레임워크에 상관없이 리소스를 선언적으로 사용할 수 있습니다.
  2. 에셋 카탈로그(이미지, 컬러)를 포함해서 폰트, 스트링 파일 등 다양한 리소스를 타입 안정성을 가지고 관리할 수 있습니다.

사용법

SwiftGen을 사용하는 방법을 알아볼까요?

첫째, swiftgen.yml 파일을 생성하고 관리하려는 리소스 정보를 입력합니다.

xcassets:
  inputs:
    - PROJECT_NAME/Resources/Assets.xcassets
  outputs:
    - templateName: swift4
      output: PROJECT_NAME/Generated/Assets.swift
      params:
        forceProvidesNamespaces: true

둘째, SwiftGen을 실행합니다.

swiftgen

결과물(예: Asset.swift, L10n.swift 등)을 프로젝트에 추가하고 다음과 같이 사용하면 됩니다.

여기까지만 해도 리소스에 타입 안정성이 부여돼서 문제는 해결됐지만 다른 문제가 생깁니다. 지금처럼 SwiftGen을 사용하면 리소스가 변경될 때마다 터미널을 켜서 swiftgen 명령어를 입력해야 하는 귀찮은 상황이 발생합니다.

위와 같은 귀찮은 상황을 제거하기 위해 스크립트를 만들고 프로젝트의 Build Phase에 등록합니다.

swiftgen.sh

#!/bin/sh
RESOURCES_DIR_PATH="../PROJECT_NAME/Resources"
HASH_FILE_PATH="$RESOURCES_DIR_PATH/checksum.md5"

current_resources_hash=`find $RESOURCES_DIR_PATH -type f ! -path "$HASH_FILE_PATH" -print0 | sort -z | xargs -0 md5 | md5`

resources_hash_changed() {
  touch $HASH_FILE_PATH
  old_resources_hash=`cat $HASH_FILE_PATH`
  [ "$old_resources_hash" != "$current_resources_hash" ]
}

if resources_hash_changed
then
  (cd .. && swiftgen)
  echo $current_resources_hash >| $HASH_FILE_PATH
fi
BuildPhase에 SwiftGen 적용

그러면 빌드할 때마다 체크섬 검사 후 리소스에 변경이 있었으면 SwiftGen을 실행합니다.

성공!

제 개인 프로젝트인 FREEZY의 경우엔 이미지와 컬러의 에셋 카탈로그, 다국어화 스트링 파일을 SwiftGen을 통해 관리하고 있습니다.

여기까지 SwiftGen에 대해 알아보았습니다. 여기부턴 앞에서 계속 말씀드렸던 선언적이다의 의미를 알아보도록 하겠습니다.

선언적이다

‘선언적’이라는 것은 코드가 선언형 프로그래밍을 따르고 있다는 의미로, 명령형 프로그래밍과는 대비되는 개념입니다.

프로그램이 어떤 방법으로 해야 하는지를 나타내기보다 무엇과 같은지를 설명하는 경우에 "선언형"이라고 한다. 예를 들어, 웹 페이지는 선언형인데 웹페이지는 제목, 글꼴, 본문, 그림과 같이 "무엇"이 나타나야 하는지를 묘사하는 것이지 "어떤 방법으로" 컴퓨터 화면에 페이지를 나타내야 하는지를 묘사하는 것이 아니기 때문이다.

위 내용은 위키피디아에서 인용했습니다. 선언적인 것은 설명 그대로 “어떻게” 보다는 “무엇을”에 집중하는 것입니다.

왜 선언적이어야 할까요?

이미지를 그리는 방식에 비유해보겠습니다.

명령형으로 이미지를 그리는 방식이라면 우리는 hello.png라는 이름을 가진 이미지 파일을 UIKit인지 AppKit인지에 따라 UIImage, NSImage를 사용하는 코드를 모두 작성해야 합니다. 하지만 SwiftGen을 사용하면 Asset.hello.image라고만 작성하면 됩니다.

이렇게 “어떻게”를 숨기고 실제 사용하는 곳에서는 “무엇을”에만 집중하는 것은 우리에게 타입 안정성, 코드의 간결함 같은 이로운 결과를 많이 제공합니다.

실제로 선언적인 기술들

우리 주변의 선언적인 기술은 뭐가 있을까요?

iOS 개발자에게 가장 익숙한 선언적인 기술은 SwiftUI일 것 같습니다. SwiftUI는 뷰를 그리기 위한 하나의 코드로 기기별(아이폰, 아이패드, 애플 워치, 아이맥 등) 상황에 맞는 뷰를 그려줍니다. 사용자에게 뷰를 어떻게 그릴지보다는 무슨 뷰를 그릴지에 더 집중할 수 있게 만들기 때문에 선언적이라고 생각합니다.

코드로 비교해볼까요? 빨갛고 동그란 X 버튼을 UIKitSwiftUI로 만들어보겠습니다.

UIKit

import UIKit

let button: UIButton = UIButton(type: .system)
button.setTitle("X", for: .normal)
button.setTitleColor(UIColor.white, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 20, weight: .bold)
button.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
button.backgroundColor = UIColor.red
button.layer.cornerRadius = 15
button.addTarget(self, action: #selector(), for: .touchUpInside)
SwiftUI

Button("X") { }
	.foregroundColor(Color.white)
	.font(Font.system(size: 20, weight: .bold))
	.frame(width: 30, height: 30)
	.background(Color.red)
	.cornerRadius(15)

어떤 코드가 더 선언적일까요?

UIKit이라고 생각하는 분이 계실 수도 있습니다.

하지만 AppKit까지 고려했을 때 두 배가 되는 UIKit 코드와 비교해 SwiftUI는 그대로일 것으로 예상이 됩니다.

그런 면에서 SwiftUI가 선언적이라 할 수 있겠습니다.

SwiftUI가 어색하신 분들에겐 Storyboard를 대입해서 생각해보시는 것도 좋겠네요!

마치며

오늘은 iOS 개발 중 만나게 되는 리소스를 SwiftGen을 통해서 관리하는 방법에 대해 알아보고, 선언형 프로그래밍을 맛보는 시간을 가져보았습니다.

아마 선언형 프로그래밍을 처음 접하시는 분은 많이 헷갈리실 것 같습니다. 저도 마찬가지로 처음 접했을 때는 무엇이 선언적인가에 대해 고민을 많이 했었습니다. 답은 많겠지만 단순하게 생각하는 것이 가장 쉬운 이해 방법이라고 생각합니다!

읽어주셔서 감사하고 다음 글에서 뵙겠습니다!