Buck으로 iOS앱 빌드해보자!

David Ha (Hyeonsu)
15 min readDec 19, 2018

--

화장실 갔다와 커피를 타고 자리에 다시 돌아와도 Xcode는 여전히 빌드를 돌리고 있었습니다.

Xcode 빌드 속도는 왜 이렇게 느린걸까?

앱의 규모가 커지면 커질 수록 느려지는 빌드속도…

작년 부터 회사 앱에서 xib, storyboard를 99%박멸해서 그나마 2년전에 비하면 지금이 양반이다.

빌드환경 인프라와 최적화, 생산성을 고민한 끝에 나는 회사에서 여유시간 있을 때 마다 짬짬이 예광탄을 쏘아본 결과 Buck으로 수렴한다는 사실을 알게 되었다.

Facebook Buck

발음 조심하자, **ck이 아니라 Buck이다.

Buck is a build system developed and used by Facebook. It encourages the creation of small, reusable modules consisting of code and resources, and supports a variety of languages on many platforms.

Buck을 왜 써야하는지는 아래의 링크들이 잘 설명해준다.

대표적으로 Facebook, Airbnb, Uber, Pinterest(Texture) 와 같은 글로벌 인싸기업들이 사용하고 있다는점!

사실 빌드 셋팅 한다는 것이 머나먼 옛날 개발자분들에게는 그저 밥상 차리는 수준이지만, 21세기를 바라보는 우리 세대에겐 대학시절 Makefile 간단히 실습해보고 그냥 MS사 IDE에 녹색 화살표만 누르면 모든게 해결되기 때문인거 같다.

내가 내린 결론은 녹색버튼이 개발자를 멍청하게 만든다는것..

물론 편리 하지만 내부적인 빌드 문제가 생기면 어디를 구체적으로 건드려야 될지, 어떤 부분을 신경쓰면 Build Speed Optimization 이나 Application Size를 더 줄일 수 있는지에 대해서 무감각해진다.

Build Setting 정교하게 하는 것도 결국 개발자의 실력에 비례한다는 사실.

Buck! 생각보다 신경써야 할 께 많다보니 러닝커브가 만만치는 않았다.

우선 장단점 간단히 알아보자

장점

  • 기존 Xcode Build System에 영향 X (있으면 쓰고 말고 할 수 있음)
  • Build 속도 2배 이상 증가 (엄청 작은 어플리케이션은 도찐개찐..)
  • Unit Test 속도 20% 증가
  • 확실한 Build! (Xcode build system bug 피할 수 있음, 나도 모르겠다 가끔 바보같이 Build하는 경우를 종종 경험한적이 있다.)
  • 개발자는 GUI보단 CLI 이지! commend option이 풍부하고 깔끔하다
  • 3rd party의 다른 3rd party의 의존성 및 구조에 대해서 확실히 이해하면서 쓸 수 있음. (이건 오히려 독일 수도 있음 나중에 설명나옴)
  • 불필요한 Pod의 기능을 줄여가면서 앱 사이즈를 줄일 수 있음
  • 빌드 과정에 대한 모든 log를 그래프로 볼 수 있음
브라우져를 통해서 본 build trace 예시

단점

  • 좀 전에도 언급되었지만 초보가 사용하기엔 러닝 커브가 높다. (기존 Xcode Build Setting에 대한 개념이 어느정도 있어야함)
  • 새로운 3rd party cocoapod으로 다운하기 전에 해당 pod에 대한 BUCK script작성필요. (이거 작업하면서 Buck이 **ck이 되는 경험을 하실 수가 있습니다.)
  • apple_asset_catalog, apple_resource, apple_binary 등등 초기 설정하는데 시간 많이 소요 (마찬가지)
  • LLDB로 디버깅밖에 못합니다. (Xcode로 열어서 볼 방법없나 찾아봐도 없네요. 예외로 Atom + Nuclide Task runner로 돌리는 방법있슴)

자 이게 한번 Buck을 구축해봅시다.

저는 기존에 있는 프로젝트에 Buck을 Embeded하는 방향으로 설명하겠습니다.

자세한건 : https://buckbuild.com/setup/getting_started.html

  1. 설치ㄱㄱ
brew tap facebook/fb
brew install buck
brew tap caskroom/cask
brew tap caskroom/versions
brew cask install java8
brew install ant python git watchman

2. .buckconfig를 만듭니다.

[cxx]
default_platform = iphonesimulator-x86_64
combined_preprocess_and_compile = true

[apple]
iphonesimulator_target_sdk_version = 9.0
iphoneos_target_sdk_version = 9.0
xctool_default_destination_specifier = platform=iOS Simulator, name=iPhone 7, OS=12.0

[alias]
app = //iOSBuckExample:iOSBuckExampleBundle

[httpserver]
port = 8000

[parser]
polyglot_parsing_enabled = true
default_build_file_syntax = SKYLARK
[project]
ide = xcode
ignore = .buckd, \
.hg, \
.git, \
buck-out, \

아마 여기서 부터 처음 경험하시는 분들은 멘붕과 함께 귀차니즘이 몰려오실 껍니다. 전 이런거에 희열을 느끼기 때문에 하나하나 다 분석해보고 docs까지 읽어보았습니다.

위에는 Xcode IDE기준으로 iPhone Application을 빌드하기 위한 기본적인 환경 셋팅이라 보시면 되겠습니다.

  • [cxx] : 욕아님. 어떤 플랫폼 환경에서 빌드를 수행하는지 명시합니다.
  • [apple]: 말그대로 apple platform 빌드 타켓을 셋팅합니다. (iPod은 사이트가서 직접보십시오.)
  • [alias]: Shortcut개념이라 보시면 됩니다.
  • [httpserver]: 이건 좀 전에 장점에 언급되었던 localhost build trace 를 보여주기 위한 서버환경을 의미합니다. 그냥 port정도 정의해주면 충분합니다. 이걸로 빌드가 얼마나 최적화 되었는지 개발자들이 모여서 회고하기 딱 좋습니다.
  • [project]: ide나 프로젝트 범위내에 해당하지 않는 dir, file을 필터링 합니다.
  • [parser]: 이건 눈치 채신분들은 아실 껍니다. Buck의 부모님은 사실 Bazel이였습니다. python에서 좀 더 bazel에 맞겠끔 사용하기 위해서 만들어진게 skylark(starlark)입니다. 이왕이면 해주는게 좋습니다. 제 주변에도 이런친구가 있습니다.

3. 매크로 파일좀 빌립시다.

사실 Airbnb에서 잘 만들어 놓긴 했습니다. 뭔가 제가 원하는 상은 아니지만 Bazel 메크로파일은 잘 맹글어놨길래 아래와 같이 퍼와서 수정하면됩니다.

4. Project대한 BUCK을 만듭시다. (여기서 **ck이 될 수도 있음 주의!)

# 여기선 좀 전에 퍼온 bazel configs 파일과 필요한 method를 호출합니다.
load("//Config:configs.bzl", "binary_configs", "library_configs", "pod_library_configs", "info_plist_substitutions")

# apple_asset_catalog: Asset을 담당합니다.
apple_asset_catalog(
name = "iOSBuckExampleAssets",
visibility = ["PUBLIC"],
dirs = ["Assets.xcassets"],
)

# xib, storyboard, launchscreen과 같은 리소스를 담당합니다.
apple_resource(
name = "iOSBuckExampleResource",
visibility = ["PUBLIC"],
files = glob(["**/*.storyboard"]),
)

# 실질적으로 Compile해야 될 source파일과 3rd-party를 정의해줍니다.
apple_binary(
name = "iOSBuckExampleBinary", <-- 앱이름
visibility = ["PUBLIC"],
swift_version = "4.2", <---------- Swift Version
configs = binary_configs("iOSBuckExample"),
srcs = glob([
"main.m",
"**/*.swift"
]),
deps = [ <---- 의존성은 나중에 자세히 설명하겠습니다.
":iOSBuckExampleAssets",
":iOSBuckExampleResource",
"//Pods/RxAtomic:RxAtomic",
"//Pods/RxSwift:RxSwift"
],
frameworks = [
'$SDKROOT/System/Library/Frameworks/Foundation.framework',
'$SDKROOT/System/Library/Frameworks/UIKit.framework'
],
)

# workspace project file 을 설정합니다.
xcode_workspace_config(
name = "workspace",
visibility = ["PUBLIC"],
workspace_name = "iOSBuckExample",
src_target = ":iOSBuckExampleBundle",
additional_scheme_actions = {
"Build": {
"PRE_SCHEME_ACTIONS": ["echo 'Started Building'"],
"POST_SCHEME_ACTIONS": ["echo 'Finished Building'"],
},
},
action_config_names = {"profile": "Profile"},
)

# Bundle을 설정 합니다. info_plist도 같이 다룹니다.
apple_bundle(
name = "iOSBuckExampleBundle",
visibility = ["PUBLIC"],
extension = "app",
binary = ":iOSBuckExampleBinary",
product_name = "iOSBuckExample",
info_plist = "Info.plist",
info_plist_substitutions = info_plist_substitutions("iOSBuckExample"),
)

# Packgag입니다.
apple_package(
name = "iOSBuckExamplePackage",
bundle = ":iOSBuckExampleBundle",
)

아주 자세한건 Buck사이트가면 제가 설명한거 보다 더 잘되어 있지만 예시에 대한 응집도가 떨어져서 위에 처럼 복붙했습니다.

5. 사실 이 부분이 중요하다고 말씀드리고 싶습니다.

여러분 cocoapod 사랑하는거 잘압니다. 저도 좋아하구요.

사실 이건 최악이지만 Buck은 좀 전 단점에서 말씀 드렸다 싶이 의존성이나 이런저런 것들을 일일히 해줘야합니다.

네.. Pod도 마찬가지로 해줘야합니다.

RxSwift를 예로 들어보죠

apple_library(
name = “RxSwift”,
modular = True,
preprocessor_flags = [
“-fobjc-arc”
],
visibility = [“PUBLIC”],
swift_version = “4.2”,
srcs = glob([
“**/*.m”,
“**/*.mm”,
“**/*.swift”,
]),
exported_headers = glob([
“**/*.h”,
]),
deps = [
“//Pods/RxAtomic:RxAtomic”
],
frameworks = [
“$SDKROOT/System/Library/Frameworks/Foundation.framework”,
“$SDKROOT/System/Library/Frameworks/UIKit.framework”
]
)

Buck의 apple_libary 를 이용해서 정의를 해줘야합니다.

이름, 모듈러, ….. 등등등

가히 충적적인건 frameworks와 해당 3rd-party가 어떤 3rd-party에 의존성을 갖는지 까지 정의해줘야되는거죠

쇗! 뭐같지만 막상 BUCK파일 쓰다보면 왠지모를 희열이 느껴집니다. (코드성애자인게 틀림없는듯..)

그렇다면 RxCocoa를 같이 보면

apple_library(
name = “RxCocoa”,
preprocessor_flags = [
“-fobjc-arc”
],
visibility = [“PUBLIC”],
bridging_header = “RxCocoa-Bridging-Header.h”,
swift_version = “4.2”,
srcs = glob([
“**/*.m”,
“**/*.mm”,
“**/*.swift”,
]),
exported_headers = glob([
“**/*.h”,
]),
deps = [
“//Pods/RxSwift:RxSwift”
],
frameworks = [
“$SDKROOT/System/Library/Frameworks/Foundation.framework”,
“$SDKROOT/System/Library/Frameworks/UIKit.framework”
]
)

마찬가지로 RxCocoa는 RxSwift에 의존성을 가지고 있는건 Rx를 쓰시는 분들은 누구나 다 아는 사실! 하지만 주의할 점이 있습니다.

RxCocoa는 objective-c file과 header를 포함하기 때문에 Swift Compiler에서 compile되기 위해선 Bridge-header가 필요합니다.

bridging_header = “RxCocoa-Bridging-Header.h”,

RxCocoa-Bridging-Header.h를 만들기 위해선 RxCocoa에 있는 header파일을 알아야 합니다. 이리쿵 저러쿵 정리해서 만들면 아래와 같습니다.

#import “RxCocoa/Runtime/include/_RX.h”
#import “RxCocoa/Runtime/include/_RXDelegateProxy.h”
#import “RxCocoa/Runtime/include/_RXKVOObserver.h”
#import “RxCocoa/Runtime/include/_RXObjCRuntime.h”

이렇게 힘겹게 Pod에 대해서 BUCK과 필요에 따른 Bridge header file을 만들었으면 각 pod dir에 옮겨줘야합니다.

그거 마저 귀찮은게 사실이기에 Podfile에 file utility를 이용해서 옮겨줍시다.

Podfile

post_install 부터해서가 buck과 bridge header file을 알아서 해당 pod의 dir에 옮겨주는 스크립트라 보시면 되겠습니다.

6. 실행을 해봅시다!

좀전에 alias에서 설정해둔 아래 스크립트를 기억하실겁니다.

app = //iOSBuckExample:iOSBuckExampleBundle

그냥 buck build //iOSBuckExample:iOSBuckExampleBundle 이렇게 해줘도 되지만

저렇게 alias로 설정해두면 buck build app 이렇게 호출해주면 끝이납니다.

Airbnb성님들께선 이거마저 귀차나서 Makefile을 만들어서 처리하셨다 보시면 되겠습니다.

필자 개인적이고 주관적으로 판단을 내리자면

CI Build랑 Unit Test 처리하는데는 최고인거 같습니다. 그 이외 디버깅은 Xcode에서 제공해주는 거 만큼 기대하시면 안되겠습니다.

또한, 굳이 이렇게까지 해야 되나 싶습니다만… 하고나면 실보단 득이 많았던거 같습니다.

결론: 존잼

--

--