Bundle에 관하여

Bundle에 관하여
Photo by Eduardo Soares / Unsplash

들어가며

안녕하세요 Noah입니다.
Bundle Class는 Bundle directory를 추상화해 내부 구조를 몰라도 편하게 접근할 수 있도록 해주는데요,
오늘은 이 번들 디렉터리의 구조와 개념, Bundle class의 사용법에 대해 예제와 문서를 보며 함께 알아보도록 하겠습니다.

Bundle

Apple Developer Documentation에서는 Bundle Class를 아래와 같이 소개하고 있어요

A representation of the code and resources stored in a bundle directory on disk.

Bundle Class는 디스크의 번들 디렉터리에 저장된 코드 및 리소스를 나타낸다고 하는데요,
먼저 Bundle Class의 설명에 나오는 번들 디렉터리가 어떤 의미인지 조금 더 자세히 알아봅시다 🏊‍♂️

Bundle Directory

Bundle은 한국말로 '묶음'인데요, 슈퍼마켓에서 파는 맥주 번들, 과자 번들을 떠올리며 아래의 설명을 읽어보시면 좋을 것 같습니다!

맥주, 과자 번들

Apple에서 정의하는 Bundle은 실행 코드와 해당 코드에서 사용하는 리소스(이미지, 음악, 스토리보드 등등) 를 포함하는 계층 구조를 가진 표준화된 디렉터리를 말하는데요,

Bundle을 사용해 application, app extension, framework, plug-in을 나타내며, Bundle이 다른 Bundle을 포함할 수도 있습니다.

예를들면 애플리케이션 번들 이 app extension 번들을 포함하는 것처럼 말이죠

애플리케이션 번들 디렉터리의 구조에 대해 알아볼까요?

Application

macOS Application Bundle

앞서 말한 것 처럼 Bundle은 표준화된 디렉터리 이기 때문에 Bundle에는 콘텐츠를 보관할 수 있는 위치가 정해져 있습니다.

가령 app extension은 plug-in용으로 예약된 위치에, 스토리보드는 리소스용으로 예약된 위치에 배치해야해요

번들의 구조는 번들의 타입과 플랫폼에 따라 다릅니다.

한번 확인해볼까요?

음악 애플리케이션을 우클릭한 후에 Show Package Contents를 클릭해 봅시다.

Bundle을 확인해 보려고 하는데 왜 Package를 열어봐야할까요?
Package의 정의를 먼저 읽어봅시다.

A package is any directory that the Finder presents to the user as if it were a single file.
- Apple Developer Bundle Programming Guide

패키지는 Finder가 사용자에게 단일 파일로 보여주는 디렉터리를 말합니다.
패키지는 macOS에서 디렉터리를 추상화하는 기본적인 방법 중 하나인데요, Package는 디렉터리이지만 Finder에서는 이를 단일 파일로 인식합니다. 따라서 일반적인 디렉터리를 파인더에서 더블클릭하면 해당 디렉터리 내부로 이동하지만, 패키지를 더블클릭하면 해당 파일이 실행됩니다.

따라서 우리가 mac에서 사용하고 있는 application도 모두 패키지입니다.

위에서 우클릭으로 확인하려고 했던 application도 여러 패키지 종류 중 하나인데요, Finder가 패키지로 인식하는 것은 .app, .bundle, .framework, .plugin 등이 있습니다.

그렇다면 우리가 알아보려고 하는 번들과 패키지는 어떤 관계일까요?

많은 타입의 번들이 패키지이기도 한데요,

패키지가 사용자에게 디렉터리를 단일 파일로 보여줌으로써 사용자 경험을 개선하기 위해 존재하는 반면
번들은 개발자가 코드를 패키징하고 운영체제에서 해당 코드에 액세스할 수 있도록 돕는데 더 중점을 둡니다.

번들은 표준화된 구조를 가지고 있는 반면, 패키지는 내부에 어떤 폴더 구조도 가질 수 있기 때문에 모든 패키지가 번들인 것은 아닙니다.

따라서 우리가 보려고 하는 application은 finder에서 단일 파일로 보여지는 패키지이면서, 실행 코드와 해당 코드에서 사용하는 리소스(이미지, 음악, 스토리보드 등등)을 포함하는 번들입니다.

다시 돌아와서 show package contents를 클릭해 패키지이면서, 실행 코드와 해당 코드에서 사용하는 리소스를 포함하는 번들인, application 번들을 확인해 봅시다.

macOS 애플리케이션 번들에 접근해 보니 가장 먼저 Contents라는 디렉터리가 있네요, 내부로 한 번 더들어가 볼까요?

Contents 디렉터리 내부를 보니,여러 디렉터리와 파일을 확인할 수 있는데요,
그 중 Resources 디렉터리 내부를 들여다보도록 해보겠습니다.

Resources 디렉터리에 들어가 보니 앱에서 사용하는 아이콘, 로컬라이제이션과 관련된 파일, WelcomeWindowController.nib 이라는 스토리보드 파일(리소스 관련 파일)을 확인할 수 있네요!

앞서 번들은 계층 구조를 가진 표준화된 디렉터리를 가리킨다고 했는데요,
카카오톡의 애플리케이션 번들도 Music 앱처럼 Resources 디렉터리를 포함하고 있을까요?
한번 살펴봅시다!

마찬가지로 Contents/Resources 에 들어가보면 스토리보드 파일과, 앱 아이콘과 앱 내에서 사용되는 여러 이미지 파일과 음악파일을 확인할 수 있어요

번들의 구조는 앞서 말한 바와 같이 번들의 타입과 플랫폼에 따라 다르기 때문에 iOS나 watchOS, tvOS에서는 다른 구조일 수 있어요

iOS에서의 애플리케이션 번들은 밑에서 확인해보도록 하겠습니다.

macOS에서의 애플리케이션 번들은 어떤 표준화된 계층 구조를 가지고 있을까요?
macOS에서의 애플리케이션 번들은 아래와 같은 파일들을 포함하고 있어요

File

Description

Info.plist file

(Required) The information property list file is a structured file that contains configuration information for the application. The system relies on the presence of this file to identify relevant information about your application and any related files.

Executable

(Required) Every application must have an executable file. This file contains the application’s main entry point and any code that was statically linked to the application target.

Resource files

Resources are data files that live outside your application’s executable file. Resources typically consist of things like images, icons, sounds, nib files, strings files, configuration files, and data files (among others). Most resource files can be localized for a particular language or region or shared by all localizations.

The placement of resource files in the bundle directory structure depends on whether you are developing an iOS or Mac app.

Other support files

Mac apps can embed additional high-level resources such as private frameworks, plug-ins, document templates, and other custom data resources that are integral to the application. Although you can include custom data resources in your iOS application bundles, you cannot include custom frameworks or plug-ins.

Info.plist를 통해 애플리케이션 및 관련 파일에 대한 관련 정보를 식별하기 때문에 애플리케이션 번들은 Info.plist를 꼭 포함하고 있어야 합니다.

모든 애플리케이션 번들은 실행파일(executable file)을 포함하고 있는데요, 이 파일에는 애플리케이션의 main entry point와 애플리케이션 타겟에 정적으로 링크된 코드가 포함되어 있어요

Resources는 애플리케이션의 실행파일 외부에 있는 데이터 파일 입니다.
위에서 본 것처럼 이미지, 아이콘, 음악파일, 스토리보드 파일, 로컬라이제이션과 관련된 파일로 구성되어 있어요

대부분의 Resources 파일은 특정 언어, 지역에 맞게 로컬라이제이션 할 수 있습니다.

이제 iOS에서의 애플리케이션 번들의 구조를 살펴볼까요?

iOS Application Bundle

iOS 애플리케이션 번들의 구조는 모바일 디바이스 요구사항에 맞게 디스크 공간을 절약하고 파일에 대한 액세스를 단순화하기 위해 최상위 번들 디렉터리에 애플리케이션 실행파일과 애플리케이션에서 사용하는 모든 리소스를 포함하고 있어 macOS 애플리케이션 번들과 비교했을 때 비교적 평평한 구조로 되어있어요

대부분의 Xcode 프로젝트 템플릿으로 만든 프로젝트는 실행 파일을 빌드할 때 빌드하려고 하는 target에 대한 번들을 생성하는데요,
iOS 애플리케이션을 빌드할 때 만들어지는 애플리케이션 번들을 확인해 보며 iOS 애플리케이션 번들의 구조를 살펴봅시다.

iOS App 프로젝트에서 Product를 클릭한 후에

Show Build Folder in Finder를 클릭합니다.

Build/Products/Dev-iphonesimulator 에 접근하게 되면
아래와 같이 애플리케이션 번들을 확인할 수 있는데요,

애플리케이션 번들 내부로 가볼까요?

앞서 말한 것처럼 macOS 애플리케이션 번들과 달리 iOS 애플리케이션 번들은 최상위 번들 디렉터리에 애플리케이션 실행파일과 애플리케이션에서 사용하는 모든 리소스가 포함되어 있네요

iOS 애플리케이션 번들도 역시 Info.plist를 포함하고 있어야 하는데요, Info.plist에는 번들의 ID, 버전 번호, display name등 애플리케이션에 대한 정보가 들어있어요

앞서 대부분의 Xcode 프로젝트 템플릿으로 만든 프로젝트는 실행 파일을 빌드할 때 번들을 생성한다고 했는데요, 우리가 Xcode에서 iOS App 템플릿을 이용해 프로젝트를 시작할 때 아래와 같은 창이 뜨는 것을 확인할 수 있습니다.

이곳에서 개발자가 입력하는 정보에 따라 Bundle Identifier가 정해지는 것을 확인할 수 있어요

Plug-ins

Settings.bundle은 설정 애플리케이션에 추가하려는 정보가 담긴 특수한 타입의 플러그인 번들이에요
제가 참여했던 프로젝트인 Havit에서는 애플리케이션에서 사용한 오픈소스 라이브러리 정보를 Settings.bundle을 이용해 설정 애플리케이션에서 확인할 수 있도록 해주었어요

여기까지 자주 쓰는 iOS, macOS의 애플리케이션 번들 디렉터리의 구조를 잠시 살펴보았습니다.

앞서 번들은 개발자가 코드를 패키징하고 운영체제에서 해당 코드에 액세스할 수 있도록 돕는데 더 중점을 둔다고 했는데요,

어떻게 그리고 어떤 도움을 줄까요?

Bundle class

번들 디렉터리를 추상화한 클래스인 Bundle class를 살펴봅시다.
추상화를 통해 얻을 수 있는 이점은 무엇일까요?

추상화를 통해 얻을 수 있는 이점 중 하나는 사용하는 쪽에서 구현 디테일을 몰라도 인터페이스를 통해 필요한 결과를 얻을 수 있는 것이라고 생각해요

그럼, 위에서 살펴본 번들 디렉터리를 추상화하면 어떨까요?
번들 디렉터리를 사용해야 하는 개발자 입장에서는 파일구조에 대한 세부 디테일을 몰라도 Bundle의 리소스에 쉽게 접근할 수 있게 됩니다.

위에서 살펴본 번들 디렉터리를 추상화한 class가 바로 Bundle class입니다.

Bundle class의 인터페이스를 살펴보며 어떻게 사용해야 할지 알아봅시다.

번들 객체를 사용하는 일반적인 패턴은 다음과 같습니다.

먼저 의도한 번들 디렉터리에 대한 번들 객체를 만든 후, 해당 번들 객체의 메서드를 사용해 필요한 리소스를 찾거나 로드합니다.

자주 사용하는 일부 리소스 타입은 번들 없이도 로드할 수 있는데요,
예를 들어 아래 처럼 이미지를 로드할 때 이미지를 에셋 카탈로그에 저장하고, UIImage의 init(named:) 을 이용해 이미지 리소스에 접근하는 것이 그 예시입니다.

먼저 리소스를 찾기 위해서는 먼저 찾으려고 하는 리소스가 포함되어있는 번들을 지정해야 하는데요,
가장 자주 쓰게 되는 번들은 현재 실행 중인 애플리케이션의 코드와 리소스가 포함되어있는 메인 번들입니다.
이 메인 번들 객체를 통해 앱과 함께 제공된 리소스에 액세스할 수 있습니다. 아래의 코드처럼 말이죠

let mainBundle = Bundle.main

이 메인 번들 객체를 통해 아래와 같이 현재 실행 중인 코드와 리소스가 포함되어있는 애플리케이션 번들 디렉터리를 확인해 볼 수 있습니다.

let mainBundleDirectoryFilePath = Bundle.main.bundleURL

메인 번들 이외에 다른 번들에 접근하기 위해서는 어떻게 해야할까요?

Apple의 Bundle Class문서의 설명에 따르면 앱이 플러그인, 프레임워크 또는 기타 번들 콘텐츠와 상호작용하는 경우 번들 객체를 통해 앱이 사용하고 있는 번들에 접근할 수 있다고 되어있는데요,

셸 스크립트, 정적 라이브러리, 및 별도의 동적 라이브러리는 번들 구조를 사용하지 않습니다.

예시를 만들어 보며, 프로젝트에서 사용 중인 프레임워크 번들에 접근하는 방법을 살펴보도록 하겠습니다.

먼저 사용할 프레임워크를 하나 생성한 후에

프레임워크의 Product Name은 Example로 하도록 하겠습니다.

Bundle Identifier에 noah.Example이라고 쓰여 있는 게 보이시나요?

이곳에서 사용할 클래스를 하나 생성해 보도록 하겠습니다.

모듈 외부에서 사용할 것이므로 public 접근제어자를 달아주도록 하겠습니다.

Something이라는 클래스를 생성하고 빌드를 한 후에

iOS App 프로젝트에서 방금 만든 프레임워크를 사용해 보도록 하겠습니다.

이곳에서 바로 앞에 만든 프레임워크를 추가해 보도록 하겠습니다.
사용할 모듈을 import 한 후에

mainBundleIdentifier를 확인해 보도록 하겠습니다.

mainBundleIdentifiernoah.iOSExample이네요
iOS App 프로젝트를 생성할 때 만들어진 Bundle Identifier와 동일한 것으로 미루어 보아 mainBundleIdentifier가 올바르게 불러와진 것을 확인할 수 있어요

다음 브레이크 포인트로 넘어가 봅시다.

frameworkBundleIdentifier의 값은 noah.Example입니다.
역시 위에서 프레임워크를 만들 때 생성한 프레임워크의 Bundle Identifier와 동일한 것으로 미루어보아
우리가 생성한 프레임워크의 번들의 identifier를 올바르게 불러와진 것을 확인할 수 있어요

이처럼 메인 번들에서 로드하는 리소스 대신 프레임워크의 번들 객체에 대한 참조를 통해 프레임워크에 있는 번들 리소스에 접근해 해당 파일을 불러올 수 있는데요,
이를테면 프레임워크에 있는 이미지 혹은 Color Asset을 사용할 수 있습니다.

이를 위해 UIImage에는 UIImage(named:in:with:)와 같은 생성자를 제공하고 있으며 번들 객체를 넣는 파라미터 자리에 이미지를 가져오려고 하는 번들 객체를 전달하면 됩니다.

또한 UIColor에도 역시 UIColor(named:in:compatibleWith:)생성자를 통해 Color Asset을 가져오려고 하는 번들 객체를 전달하면 해당 번들 디렉터리에서 Color Asset을 가져올 수 있어요

이를 잘 활용하면 프레임워크가 독립적인 리소스를 갖도록 할 수 있겠네요 :)
image와 colorAsset과 같이 번들에 접근하기 위해서는 framework의 linking option에서 mach-o type을 dynamic library로 설정해 주어야 하는데요, 이 이유에 대해서는 다음 포스트에서 알아보도록 하겠습니다.

또한 번들 객체에 대한 참조를 얻기 위해서는 앞서 언급한 Bundle과 연관된 클래스 이름을 적는 것 말고도 아래의 생성자를 이용해
Bundle의 identifier를 통해, file URL을 통해, directory의 path를 언급하는 것을 통해 번들 객체에 대한 참조를 얻을 수 있어요

init?(identifier: String)

Returns the NSBundle instance that has the specified bundle identifier.

init?(url: URL)

Returns an NSBundle object initialized to correspond to the specified file URL.

init?(path: String)

Returns an NSBundle object initialized to correspond to the specified directory.

번들에 리소스가 들어가는 시점에 관하여

그렇다면 위에서 본 애플리케이션 번들에 리소스는 어느 시점에 들어가게 될까요?
Xcode에서 iOS App 프로젝트 템플릿을 이용하면 컴파일할 소스 파일, 번들에 리소스를 복사하는 과정 등 애플리케이션 번들이 만들어지기 위해 필요한 규칙이 미리 정의되어있는 것을 확인할 수 있는데요,

프로젝트의 App Target의 Build Phase를 보면

Copy Bundle Resources 라는 Phase를 확인할 수 있어요
바로 이 시점에 리소스가 애플리케이션 번들에 들어가게 됩니다.

빌드 결과로 나오게 된 애플리케이션 번들을 확인해 볼까요?

Assets.xcassets은 Assets.car로 Something.storyboard 파일은 Somthing.storyboardc로 컴파일 되어 애플리케이션 번들에 들어가 있음을 확인할 수 있어요

xib파일은 nib파일로 컴파일됩니다.

마치며

Bundle Class가 아무리 추상화가 잘 되어있어도 번들 디렉터리의 구조와 그 안에 있는 코드 및 리소스에 액세스하는 방법을 이해하는 것이 중요하다고 생각해요

따라서 오늘은 번들 디렉터리와, 번들 클래스에 대한 개념과 자주 쓰이는, 혹은 쓰게될 macOS 애플리케이션 번들, iOS 애플리케이션 번들의 구조에 대해 자세히 알아보고 Bundle class의 사용법에 대해 알아보았습니다.

이번 글이 번들에 대한 이해를 높이는 계기가 되었으면 좋겠습니다.
긴 글 읽어주셔서 감사합니다.

아직 모르는 것이 많고 알아가는 과정입니다. 잘못된 것이 있다면 댓글로 남겨주신다면 감사하겠습니다! 😊

참고