관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 4. GraphQL - Apollo의 request pipeline, Interceptor, token, header 본문

iOS framework

[iOS - swift] 4. GraphQL - Apollo의 request pipeline, Interceptor, token, header

jake-kim 2022. 3. 3. 23:12

1. GraphQL - 개념

2. GraphQL - Apollo 사용 준비

3. GraphQL - Apollo 모델 생성 (generate model)

4. GraphQL - Apollo의 request pipeline, Interceptor, token, header

5. GraphQL - Apollo의 fetch 예제  (Pagination)

Request pipeline, Interceptor

https://www.apollographql.com/docs/ios/request-pipeline/

  • 작업이 실행되면 작업이라는 개체가 InterceptorProvider를 생성 - RequestChain이 되는 것
  • 이 chain이 First Interceptor, Second Interceptor들을 실행
  • Interceptor의 기능
    • HTTP Header 추가 가능
    • 서버에 request 기능
    • Interceptor가 작업 결과를 Apollo iOS 캐시에 기록도 가능
  • InterceptorProvider의 종류 (Apollo에서는 DefalutInterceptorProvider를 강력히 권장)
    • DefaultInterceptor (Source는 여기서 확인)

  • Custom Interceptor: InterceptorProvider를 상속받아서 구현
    • 예시 코드 (ApolloInterceptor를 상속받아서 구현)
// https://www.apollographql.com/docs/ios/request-pipeline/#example-interceptor-provider

import Foundation
import Apollo

struct NetworkInterceptorProvider: InterceptorProvider {

  // These properties will remain the same throughout the life of the `InterceptorProvider`, even though they
  // will be handed to different interceptors.
  private let store: ApolloStore
  private let client: URLSessionClient

  init(store: ApolloStore,
        client: URLSessionClient) {
    self.store = store
    self.client = client
  }

  func interceptors<Operation: GraphQLOperation>(for operation: Operation) -> [ApolloInterceptor] {
    return [
      MaxRetryInterceptor(),
      CacheReadInterceptor(store: self.store),
      UserManagementInterceptor(),
      RequestLoggingInterceptor(),
      NetworkFetchInterceptor(client: self.client),
      ResponseLoggingInterceptor(),
      ResponseCodeInterceptor(),
      JSONResponseParsingInterceptor(cacheKeyForObject: self.store.cacheKeyForObject),
      AutomaticPersistedQueryInterceptor(),
      CacheWriteInterceptor(store: self.store)
    ]
  }
}
  • 위에서 들어가는 Interceptor 인스턴스들은 모두 ApolloInterceptor를 상속받은 구현체
    • ex) RequestLoggingInterceptor(), ResponseLoggingInterceptor()
// https://www.apollographql.com/docs/ios/request-pipeline/#requestlogginginterceptor

import Apollo

class RequestLoggingInterceptor: ApolloInterceptor {

  func interceptAsync<Operation: GraphQLOperation>(
    chain: RequestChain,
    request: HTTPRequest<Operation>,
    response: HTTPResponse<Operation>?,
    completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {

    Logger.log(.debug, "Outgoing request: \(request)")
    chain.proceedAsync(request: request,
                        response: response,
                        completion: completion)
  }
}

class ResponseLoggingInterceptor: ApolloInterceptor {

  enum ResponseLoggingError: Error {
    case notYetReceived
  }

  func interceptAsync<Operation: GraphQLOperation>(
    chain: RequestChain,
    request: HTTPRequest<Operation>,
    response: HTTPResponse<Operation>?,
    completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {

    defer {
      // Even if we can't log, we still want to keep going.
      chain.proceedAsync(request: request,
                          response: response,
                          completion: completion)
    }

    guard let receivedResponse = response else {
      chain.handleErrorAsync(ResponseLoggingError.notYetReceived,
                              request: request,
                              response: response,
                              completion: completion)
      return
    }

    Logger.log(.debug, "HTTP Response: \(receivedResponse.httpResponse)")

    if let stringData = String(bytes: receivedResponse.rawData, encoding: .utf8) {
      Logger.log(.debug, "Data: \(stringData)")
    } else {
      Logger.log(.error, "Could not convert data to string!")
    }
  }
}

Header

  • Header를 넣어주려면 위에서 알아보았듯이, InterceptorProvider의 안에 ApolloInterceptor를 상속받은 인스턴스를 넣어주는 방식으로 사용
  • 흐름
    • ApolloClient(networkTranport:store:)를 싱글톤으로 사용 (networkTransport는 Intercetor, store는 캐싱)
    • networkTransport에 InterceptorProvider를 상속받은 커스텀 인스턴스를 주입
    • InterceptorProvider를 상속받은 커스텀 인스턴스안에 ApolloInterceptor를 상속받은 request 로깅, response 로깅, 헤더 정보 주입
  • ApolloInterceptor를 상속받아서 헤더정보를 삽입 & 토큰 정보도 주입

* 테스트에 사용될 토큰 관리 클래스 생성

class TokenManager {
  enum RenewError: Error {
    case unknown
  }
  
  static let shared = TokenManager()
  
  var token: Token? = Token()
  
  func renewToken(completion: @escaping (Result<Token, RenewError>) -> Void) {
    completion(.success(Token()))
  }
  
  private init() {}
}

struct Token {
  let value = "test-token"
  var isExpired: Bool { Bool.random() }
}
  • interceptAsync 메소드를 정의하여 request.addHeader를 이용해 토큰정보나 다른 정보를 헤더에 추가
// MyAPI+ApolloClient.swif  
  
  // https://www.apollographql.com/docs/ios/request-pipeline/#usermanagementinterceptor  
  private class HeaderInterceptor: ApolloInterceptor {
    enum UserError: Error {
      case noUserLoggedIn
    }
    
    func interceptAsync<Operation: GraphQLOperation>(
      chain: RequestChain,
      request: HTTPRequest<Operation>,
      response: HTTPResponse<Operation>?,
      completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
    ) {
      request.addHeader(name: "Language", value: Locale.current.languageCode ?? "*")
      
      guard let token = TokenManager.shared.token else {
        chain.handleErrorAsync(UserError.noUserLoggedIn,
                               request: request,
                               response: response,
                               completion: completion)
        return
      }
      
      if token.isExpired {
        TokenManager.shared.renewToken { [weak self] tokenRenewResult in
          guard let self = self else {
            return
          }
          
          switch tokenRenewResult {
          case .failure(let error):
            chain.handleErrorAsync(
              error,
              request: request,
              response: response,
              completion: completion
            )
          case .success(let token):
            self.addTokenAndProceed(
              token,
              to: request,
              chain: chain,
              response: response,
              completion: completion
            )
          }
        }
      } else {
        self.addTokenAndProceed(
          token,
          to: request,
          chain: chain,
          response: response,
          completion: completion
        )
      }
    }
    private func addTokenAndProceed<Operation: GraphQLOperation>(
      _ token: Token,
      to request: HTTPRequest<Operation>,
      chain: RequestChain,
      response: HTTPResponse<Operation>?,
      completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>
      ) -> Void) {
      request.addHeader(name: "Authorization", value: "Bearer \(token.value)")
      chain.proceedAsync(request: request,
                         response: response,
                         completion: completion)
    }
  }
  • 위에서 정의된 HeaderInterceptor를 InterceptorProvider를 상속받아서 커스텀한 NetworkInterceptorProvider의 interceptors 메소드 리턴값에 추가
// MyAPI+ApolloClient.swif

// https://www.apollographql.com/docs/ios/request-pipeline/#example-interceptor-provider
private struct NetworkInterceptorProvider: InterceptorProvider {
  private let store: ApolloStore
  private let client: URLSessionClient
  
  init(store: ApolloStore,
       client: URLSessionClient) {
    self.store = store
    self.client = client
  }
  
  func interceptors<Operation: GraphQLOperation>(for operation: Operation) -> [ApolloInterceptor] {
    return [
      MaxRetryInterceptor(),
      CacheReadInterceptor(store: self.store),
      HeaderInterceptor(), // <- 여기
      RequestLoggingInterceptor(),
      NetworkFetchInterceptor(client: self.client),
      ResponseLoggingInterceptor(),
      ResponseCodeInterceptor(),
      JSONResponseParsingInterceptor(cacheKeyForObject: self.store.cacheKeyForObject),
      AutomaticPersistedQueryInterceptor(),
      CacheWriteInterceptor(store: self.store)
    ]
  }
}
  • 위에서 IntercerptorProvider를 상속받아서 정의한 인스턴스를 ApolloClient 생성자에 주입
  // MyAPI+ApolloClient.swif
  private enum Constants {
    static let requestTimeoutSeconds = 30.0
    static let endpoint = URL(string: "https://apollo-fullstack-tutorial.herokuapp.com/graphql")!
  }
  
  private var client: ApolloClient {
    let sessionConfiguration = URLSessionConfiguration.default
    sessionConfiguration.timeoutIntervalForRequest = Constants.requestTimeoutSeconds
    let sessionClient = URLSessionClient(sessionConfiguration: sessionConfiguration, callbackQueue: .main)

    let store = ApolloStore(cache: InMemoryNormalizedCache())
    let networkInterceptorProvider = NetworkInterceptorProvider(store: store, client: sessionClient)
    let requestChainNetworkTransport = RequestChainNetworkTransport(
      interceptorProvider: networkInterceptorProvider,
      endpointURL: Constants.endpoint
    )
    return ApolloClient(networkTransport: requestChainNetworkTransport, store: store)
  }

* 전체 코드: https://github.com/JK0369/ExApollo

 

* 참고

https://www.apollographql.com/docs/ios/request-pipeline/#usermanagementinterceptor

https://www.apollographql.com/docs/ios/request-pipeline/

Comments