[iOS] Apollo - 2

r1verfuture
13 min readFeb 29, 2024

--

드디어 미루고 미루던 Apollo 에 대해서 저번부터 글 쓰기 시작했다 ㅋㅋ 이전 글은 1편이었기 때문에 Apollo 에 대해 소개하는 것 위주로 썼다면 이번 글에서는 실제 프로젝트에 적용하는 것 위주로 써볼 예정이다 !! 이번 글이 이전 글보다 덜 지루하지 않을까 .. 라고 장담 아닌 장담을 하면서 글 시 ~ 작 🐻

Apollo iOS 는 타겟에 파일 형태로 정의되어 있는 GraphQL QueryMutation 을 가지고 코드를 생성한다. 이때 QueryMutation 에 대한 내용이 있는 파일은 .graphql 이라는 확장자를 가지고 있어야 한다. .graphql 이확장자를 가지고 있는 QueryMutation 파일을 가지고 Apollo iOS 가 모델에 관련된 코드를 만드는데, 이 코드가 포함되는 파일은 .graphql.swift 라는 확장자를 가진다. 이미 이전 글에서 언급했지만, 이 일련의 과정을 Code Generation 이라고 한다. .graphql 에 있는 내용을 가지고 .graphql.swift 로 바꾸어 주기 (Code Generation 을 실행하기) 위해서는 Apollo iOS 가 제공하는 Codegen CLI 라는 친구를 사용하면 된다. 이 친구는 어떻게 쓰냐고 ~? 이 친구를 쓰는 방법도 이전 글에 있는 Apollo 설치 방법처럼 Swift Package Manager (SPM), CocoaPods, Package.swift 이렇게 3가지로 나뉜다.

Swift Package Manager (SPM)

Apollo iOS 를 설치했다면 이미 Codegen CLI 를 설치할 준비가 끝났다 ㅋㅋ 그리고 참고로 이때 사용하는 CLI 는 항상 유효한 버전임을 보장한다고 한다. CLI 플러그인을 설치하는 방법은 아주 간단하다 ㅎ Xcode 왼편의 파일 계층 창 (?) 에 있는 타겟에 마우스를 갖다대고 오른쪽 버튼을 누르면

출처 : Apollo 공식 문서

이렇게 여러 가지 항목들이 나타나는데, 그 중에서 Install CLI 를 누르면 끝 !! Install CLI 버튼을 눌러서 CLI 플러그인을 설치하고 나면 프로젝트 폴더에

이렇게 생긴 apollo-ios-cli 라는 이름의 Codegen CLI 가 생긴다. 이 친구가 생기고 나면 이제 ./apollo-ios-cli 명령어를 실행시킬 수 있게 되는데, 이 친구는 컴파일된 CLI 실행 파일이 있어야만 동작한다. 참고로 컴파일된 CLI 실행 파일은 일반적으로 XcodeDerived Data.build 폴더에 있다. 이 2가지 폴더가 비어있다면 컴파일된 CLI 실행 파일이 없다는 뜻인데, 이 경우에는 다시 InstallCLI 플러그인을 실행해서 CLI 실행 파일을 만들면 된다.

Codegen CLIJSON 파일을 사용해서 Code Generation 엔진을 구성하는데, 이때 사용하는 JSON 파일은 Codegen CLIinit 명령어를 통해 만들 수 있다. 정확한 명령어는

./apollo-ios-cli init --schema-name ${MySchemaName} --module-type ${ModuleType}

이런 꼴인데 ${MySchemaName} 에는 만들어 놓은 Schema 파일의 네임스페이스 이름을 쓰면 되고, ${ModuleType} 에는 만들어 놓은 Schema 파일의 형식이 어떻게 포함되도록 할 것인지를 쓰면 된다. Code Generation 을 구성하기 전에 여러 가지 항목을 설정해 주어야 하는데, 그 중의 하나인 ${ModuleType} 은 꽤 중요한 항목이기 때문에 올바른 옵션을 선택해 주어야 한다. 공식 문서에서 이 항목에 쓸 수 있는 옵션으로 embeddedInTarget(name: String) , swiftPackageManager , other 이렇게 3가지를 소개하고 있다.

embeddedInTarget(name: String)

은 생성된 모듈을 앱 타겟에 직접 포함시키고 싶을 때 사용하고, 타겟이 하나인 앱에서는 이 옵션을 사용하면 된다. 이 옵션에는 name 라는 인자값이 있는데 여기에는 Schema 타입이 포함되는 타겟의 이름을 적으면 된다.

swiftPackageManger

Swift Package Manager (SPM) 를 사용해서 프로젝트를 구성한 경우에 Schema 타입을 생성하는 가장 빠르고 편리한 방법이다. 이 옵션을 사용하면 Code Generation 엔진이 Package.swift 파일을 생성해서 Schema 모듈을 SPM 패키지처럼 사용할 수 있게 만들어준다.

other

은 내가 직접 만든 모듈에 Schema 타입을 생성하는 것을 허용하는 옵션이다. 내가 직접 만든 모듈의 예시로는 CocoaPods 와 같은 Third Party Package Manager 를 사용하는 것이 있다. 이때 주의해야할 점은 위의 명령어 중 ${MySchemaName} 에 적어주었던 이름과 그 모듈의 이름이 같아야 한다는 것이다.

이렇게 여러 가지 옵션들이 있기는 하지만 미니 프로젝트처럼 가볍게 해보는 용도라면 embeddedInTarget 옵션을 사용하면 된다. 이 옵션을 쓸 때 명령어에 --target-name 이라는 옵션을 추가해 주어야 하는데, 이 친구는 타겟 이름을 뜻하기 때문에 이 친구 옆에 타겟 이름을 적어주면 된다. 대충

./apollo-ios-cli init --schema-name ${MySchemaName} --module-type embeddedInTarget --target-name "MyTarget"

이런 식으로 명령어를 입력하면 되고, 이 명령어를 실행하면 apollo-ios-cli 라는 이름의 Codegen CLI 가 있는 폴더에

이렇게 생긴 apollo-codegen-config.json 라는 이름의 파일이 생긴다.

이 다음에 실행해야 하는 명령어는

./apollo-ios-cli generate

인데, 이 명령어를 실행하면 Code Generation 엔진은 GraphQL Schema 에 해당하는 파일 (.graphqls 라는 확장자를 가진 파일) 을 찾은 후 이 파일을 가지고 Swift 형태의 파일 (.graphql.swift 라는 확장자를 가진 파일) 을 만들어서 schema-name 에 썼던 디렉토리 안에 넣고, GraphQL QueryMutation 에 해당하는 파일 (.graphql 라는 확장자를 가진 파일) 을 가지고 Swift 형태의 파일 (.graphql.swift 라는 확장자를 가진 파일) 을 만들어서 schema-name 에 썼던 디렉토리의 하위 폴더에 넣는다. Schema 파일들이 포함되어 있는 디렉토리에 MySchemaName 폴더가 생성되는 것이 기본이지만, 프로젝트에 타겟을 더 추가하는 경우에는 해당 파일들의 위치를 그 타겟 안으로 바꾸어 주어야 한다. 새로운 파일을 생성할 때마다 파일의 위치를 바꾸어 주어야 하는데, 이 과정이 꽤 번거롭기 때문에 SPM 으로 프로젝트 타겟을 정의하는 것을 추천한다 ..!

CocoaPods

를 사용할 경우에는 pod install 명령어를 실행하는 동안 Apollo iOSCodegen CLI 를 실행 가능한 어플리케이션으로 컴파일한다. SPM 을 사용할 때 Install CLI 플러그인을 설치하고 나면 ./apollo-ios-cli 명령어를 사용할 수 있었던 것처럼 CocoaPods 를 사용할 때는 Apollo iOS CLI 가 설치되고 나면 ./Pods/Apollo/apollo-ios-cli 명령어를 사용할 수 있게 된다. 명령어는

./Pods/Apollo/apollo-ios-cli ${Command Name} -${Command Arguments}

이런 꼴로 구성할 수 있고, ./apollo-ios-cli init 명령어와 마찬가지로

./Pods/Apollo/apollo-ios-cli init --schema-name ${MySchemaName} --module-type ${ModuleType}

를 실행하면 Code Generation 엔진을 구성할 때 필요한 JSON 파일 (apollo-codegen-config.json) 이 생성된다. 이때의 ${MySchemaName}${ModuleType}./apollo-ios-cli init 명령어 설명할 때 설명했던 것과 동일하다. 위에서 설명했던 SPM 과 다 똑같은데, 명령어 접두사 (?) 만 다를 뿐이다. ./apollo-ios-cli generate 와 같은 역할을 하는 명령어는

./Pods/Apollo/apollo-ios-cli generate

이것이고, 이 명령어를 실행하면 .graphql.swift 확장자를 가진 파일들이 생긴다.

Package.swift

를 사용하는 경우에는 SPM 과 거 ~ 의 같은데, SPM 을 사용했을 때 Install CLI 플러그인을 설치하는 것을

swift package --allow-writing-to-package-directory apollo-cli-install

이 명령어로 대신하고, 이 명령어를 실행하면 apollo-cli-install SPM 플러그인이 설치된다. 이 플러그인을 설치하면 SPM 때와 마찬가지로 apollo-ios-cli 라는 이름의 Codegen CLI 이 프로젝트 폴더에 생기고, ./apollo-ios-cli 명령어를 사용할 수 있게 된다. 이 이후부터는 SPM 과 다 똑같기 때문에 설명은 생략 ㅋㅋ

이렇게 사전 준비가 다 끝나면 이제 본격적으로 Apollo 를 프로젝트 안에서 쓰면 된다. GraphQL QueryMutation 을 앱 안에서 실행하기 전에 ApolloClient 라는 객체를 초기화해 주어야 한다. ApolloClient 객체는 ApolloClientProtocol 을 준수함으로써 Apollo 의 핵심 API 를 실행할 수 있도록 해주는 친구이고,

class ApolloClient

와 같이 정의되어 있다. ApolloClient 객체를 초기화하는 방법에는 2가지가 있는데, 가장 간단한 방법은

import Foundation
import Apollo

let apolloClient = ApolloClient(url: URL(string: "http://localhost:4000/graphql")!)

이런 식으로 url 파라미터에 GraphQL 서버 URL 을 넣는 것이다. 이 초기화 로직 안을 뜯어보면

public convenience init(url: URL) {
let store = ApolloStore(cache: InMemoryNormalizedCache())
let provider = DefaultInterceptorProvider(store: store)
let transport = RequestChainNetworkTransport(
interceptorProvider: provider,
endpointURL: url
)

self.init(networkTransport: transport, store: store)
}

라고 되어 있는데

let store = ApolloStore(cache: InMemoryNormalizedCache())

를 통해 InMemoryNormalizedCache 방식으로 응답값을 캐싱하도록 설정되어 있다는 사실을 알 수 있고,

let transport = RequestChainNetworkTransport(
interceptorProvider: provider,
endpointURL: url
)

를 통해 HTTP 방식으로 GraphQL 서버와 통신하도록 설정되어 있다는 사실을 알 수 있다. (이 아이들에 대해 이 글에서 더 자세히 다룰까 했지만, 그러면 이 글이 너무 길어질 것 같아서 따로 이 내용에 대해 글 써보려고 한다. To be continued .. ㅋㅋ) 마지막으로

let provider = DefaultInterceptorProvider(store: store)

에서 사용한 DefaultInterceptorProvider 에 대해 간략하게 설명하고 넘어가자면 이 친구는 정규화된 캐시로 응답값을 읽기/쓰기 하고, URLSession 을 사용해서 네트워크 요청을 보내고, GraphQL 응답값을 JSON 형태로 바꾼다.

self.init(networkTransport: transport, store: store)

를 보면 알겠지만, url 파라미터를 사용하는 초기화 로직 대신 networkTransportstore 파라미터를 사용해서 ApolloClient 객체를 초기화할 수도 있다. 이 방법은 NetworkTransportApolloStore 를 직접 넣어주어 ApolloClient 객체를 내 입맛대로 만들어 주고 싶은 경우에 사용한다.

public init(networkTransport: NetworkTransport, store: ApolloStore) {
self.networkTransport = networkTransport
self.store = store
}

와 같은 형태로 구성되어 있는데 networkTransport 파라미터에는 서버에게 작업 보내는 역할을 하는 NetworkTransport 를 넣으면 되고, store 파라미터에는 GraphQL 응답값을 위해 쓰이는 로컬 캐시를 담당하는 ApolloStore 를 넣으면 된다. 기본으로 설정되어 있는 캐시는 메모리 안에 응답값을 저장하는데, 이 응답값은 앱이 실행되는 동안 계속 유지되지는 않기 때문에 캐시 데이터가 계속 유지되도록 하거나 캐시를 구성하고 싶다면 ApolloStore 를 커스텀해야 한다. 이때 NetworkTransport 나 의존성 추가되어 있는 것 중에 하나라도 ApolloStore 를 가지고 있다면 그 ApolloStore 가 이 store 파라미터 안에 들어간다.

이렇게 ApolloClient 객체를 초기화하고 나서 QueryMutation 을 실제로 프로젝트에 사용할 수 있도록 하는 작업을 해야 한다. ApolloClient 객체는 이 QueryMutation 을 불러온 후에 안전한 타입의 데이터 모델로 응답값을 반환해 준다. 예를 들어 .graphql 확장자를 가진 파일에

query HeroName {
hero {
name
}
}

이런 형태의 GraphQL Query 를 작성하고 나서

./apollo-ios-cli generate

명령어를 실행하면 Apollo iOSHeroNameQuery.graphql.swift 라는 이름의 파일을 만들 것이고, 이 파일 안에는 HeroNameQuery 라는 이름의 클래스가 작성되어 있을 것이다. 이제 이 클래스를

apolloClient.fetch(query: HeroNameQuery()) { result in
guard let data = try? result.get().data else { return }
print(data.hero.name) // Luke Skywalker
}

이렇게 ApolloClient 객체의 fetch 메소드에 있는 query 파라미터에 넣어주면 응답값이 반환된다 🤩

Apollo iOS 를 실제 프로젝트에 적용하는 일련의 과정 (?) 은 대충 이런 식이다. 이제 여기서 더 자세하게 다룰 것들은 다음 글들에 차근히 정리해 나갈 예정 ㅎㅎ 그럼 얼른 다음 글 쓰러 🏃🏻‍♀️💨 (연휴도 좀 알차게 보내구 ^^)

--

--