티스토리 뷰

 

 

Hello from OpenAPI Generator | OpenAPI Generator

Description will go into a meta tag in <head />

openapi-generator.tech

 

OpenAPI Specification(OAS)


OAS란 통신을 위한 API 문서 규격입니다.

아래와 같이 API 경로, 요청 값, 응답 값, 에러 상태 등에 대해서 정의한 json, yaml 파일로 표현됩니다.

openapi.yaml
openapi: '3.1.0'
info:
  title: GreetingService
  version: 1.0.0
servers:
  - url: https://example.com/api
    description: Example service deployment.
paths:
  /greet:
    get:
      operationId: getGreeting
      parameters:
      - name: name
        required: false
        in: query
        description: The name used in the returned greeting.
        schema:
          type: string
      responses:
        '200':
          description: A success response with a greeting.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Greeting'
components:
  schemas:
    Greeting:
      type: object
      properties:
        message:
          type: string
      required:
        - message

 

 

OpenAPI Generator


OpenAPI Generator는 OAS를 이용해 통신에 필요한 객체들 (DTO, Client)를 생성해주는 기능입니다.

json, yaml 형태의 문서를 추출한 후 cli를 이용해 제너레이트하면 다양한 클라이언트 언어(Typescript, swift 등)에 맞게 요소들을 생성해줍니다.

 

WWDC에서도 소개된 적 있으니 참고하시면 좋을 것 같습니다.

https://developer.apple.com/videos/play/wwdc2023/10171/

 

Meet Swift OpenAPI Generator - WWDC23 - Videos - Apple Developer

Discover how Swift OpenAPI Generator can help you work with HTTP server APIs whether you're extending an iOS app or writing a server in...

developer.apple.com

 

왜 사용할까?


안정성

프론트엔드는 대부분 백엔드에 의존적이기 때문에 백엔드 API에 많은 영향을 받습니다. 

그리고 분리되어 있는 서비스인 백엔드의 변경사항을 코드로 예측하거나 대비할 수 없기 때문에 많은 문제들이 발생합니다.

 

이러한 문제를 해결하기 위헤 GraphQL과 같은 방식도 등장하였습니다.

하지만 GraphQL을 적용하기 위해선 백엔드, 프론트엔드 모두 초기 비용이 많이 필요하기 때문에 그 대안으로 Openapi generator를 적용해볼 수 있습니다.

 

Openapi generator를 이용하면 프론트엔드와 백엔드 API 스키마의 정합성을 컴파일 타임에 체크할 수 있습니다.

직접 프론트엔드 개발자가 API 문서를 보고 코드를 작성하는 것이 아니라 문서 기반으로 코드가 생성되기 때문에 백엔드 API가 변경된다면 그 지점을 코드에서 직접 확인하고 대응할 수 있습니다.

 

Specification Driven Design

Openapi generator는 OAS 문서를 기반으로 동작하기 때문에 문서 기반의 개발을 할 수 있습니다.

이를 통해 프론트엔드와 백엔드 간의 소통 비용을 줄일 수 있습니다.

 

보일러플레이트 코드 작성 줄이기

예를 들어 Swift에서는 Rest api 통신 결과를 파싱하기 위해 Codable를 채택하는 struct 객체를 생성해야합니다.

Openapi generator는 이러한 보일러플레이트 코드를 직접 생성해주기 때문에 개발 생산성이 높아집니다.

 

 

Swift-iOS에 적용해보기!


https://github.com/apple/swift-openapi-generator

 

GitHub - apple/swift-openapi-generator: Generate Swift client and server code from an OpenAPI document.

Generate Swift client and server code from an OpenAPI document. - apple/swift-openapi-generator

github.com

Generator 자체는 여러가지 종류가 있는데요, 저는 위 라이브러리를 사용했습니다.

 

핵심은 cli를 실행시키는 것인데, cli 실행 후엔 제너레이트된 파일을 사용하는 것은 프로젝트에 따라 다를 것 같습니다.

단순히 복사, 붙여넣기를 해도 되고 패키지 임베드를 하거나 저는 Tuist로 구성해 Tuist에 패키지를 연결해주었습니다.

 

우선 코드를 제너레이트하는 것부터 해보겠습니다.

 

Part1. 제너레이트하기


1. openapi.yaml 파일을 준비합니다.

swagger를 사용하신다면 여기에서 raw 파일을 확인할 수 있습니다.

 

https://editor.swagger.io

그리고 위 사이트를 통해 올바른 OAS문서 형태인지 검증할 수 있습니다!

openapi.yaml
openapi: '3.1.0'
info:
  title: GreetingService
  version: 1.0.0
servers:
  - url: https://example.com/api
    description: Example service deployment.
paths:
  /greet:
    get:
      operationId: getGreeting
      parameters:
      - name: name
        required: false
        in: query
        description: The name used in the returned greeting.
        schema:
          type: string
      responses:
        '200':
          description: A success response with a greeting.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Greeting'
components:
  schemas:
    Greeting:
      type: object
      properties:
        message:
          type: string
      required:
        - message

 

2. 필요에 따라 openapi-generator-config.yaml 파일도 작성해줍니다.

만약 client 없이 인터페이스와 DTO만 사용하고 싶다면 generate부분에 types만 남기면됩니다.

accessModifier는 말 그대로 접근제어자의 종류를 지정할 수 있습니다.

모듈화나 패키지화를 하면 public를 붙여주어야하기 때문에 이용해주시면 될 것 같습니다.

# Use the OpenAPI Generator CLI to generate Swift client and models
generate:
  - types
  - client
accessModifier: public

더 자세한 설정들은 https://swiftpackageindex.com/apple/swift-openapi-generator/1.3.0/documentation/swift-openapi-generator/configuring-the-generator 에서 확인하실 수 있습니다.

 

3. 제너레이트를 실행할 패키지를 만들어줍니다.

$ mkdir openapi-generator-cli
$ cd openapi-generator-cli
$ swift package init --type executable

 

4. 생성된 Pakage.swift파일을 아래와 같이 변경합니다.

// swift-tools-version:5.5
import PackageDescription

let package = Package(
    name: "openapi-generator-cli",
    platforms: [
        .macOS(.v10_15)
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-openapi-generator", from: "1.0.0"),
        .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.0.0"),
        .package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.0.0"),
    ],
     targets: [
        .executableTarget(
            name: "openapi-generator-cli",
            dependencies: [
                .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
            ],
            plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")]
        )
    ]
)

 

5. 폴더 구조를 아래와 같이 맞춰줍니다.

openapi-generator-cli/
├── Sources/
│   └── openapi-generator-cli/
│       ├── openapi.yaml // api 스펙
│       ├── openapi-generator-config.yaml // config 파일
│       └── main.swift

 

6. 패키지를 업데이트 및 빌드합니다.

$ swift package update
$ swift build

 

7.  제너레이트를 실행합니다.

$ swift run

아래와 같이 뜬다면 성공입니다! 

로그를 통해 제너레이트된 파일의 위치를 확인할 수 있습니다.

제너레이트된 폴더를 들어가보면 Types.swift에는 각 API명세에 대한 프로토콜과 Input(Request), Output(Response)에 형태, 반환하는 에러 종류 등 API인터페이스에 대한 내용이 정의 되어 있고,

public protocol APIProtocol: Sendable {
/// 투두 목록 조회
///
/// - Remark: HTTP `GET /api/v1/tasks`.
/// - Remark: Generated from `#/paths//api/v1/tasks/get(getTasks)`.
func getTasks(_ input: Operations.getTasks.Input) async throws -> Operations.getTasks.Output

// ...

public enum Operations {
    /// 투두 목록 조회
    ///
    /// - Remark: HTTP `GET /api/v1/tasks`.
    /// - Remark: Generated from `#/paths//api/v1/tasks/get(getTasks)`.
    public enum getTasks {
        public static let id: Swift.String = "getTasks"
        public struct Input: Sendable, Hashable {
            /// - Remark: Generated from `#/paths/api/v1/tasks/GET/header`.
            public struct Headers: Sendable, Hashable {
                public var accept: [OpenAPIRuntime.AcceptHeaderContentType<Operations.getTasks.AcceptableContentType>]
                /// Creates a new `Headers`.
                ///
                /// - Parameters:
                ///   - accept:
                public init(accept: [OpenAPIRuntime.AcceptHeaderContentType<Operations.getTasks.AcceptableContentType>] = .defaultValues()) {
                    self.accept = accept
                }
            }
            public var headers: Operations.getTasks.Input.Headers
            /// Creates a new `Input`.
            ///
            /// - Parameters:
            ///   - headers:
            public init(headers: Operations.getTasks.Input.Headers = .init()) {
                self.headers = headers
            }
        }
        @frozen public enum Output: Sendable, Hashable {
            public struct Code255: Sendable, Hashable {
                /// - Remark: Generated from `#/paths/api/v1/tasks/GET/responses/255/content`.
                @frozen public enum Body: Sendable, Hashable {
                    /// - Remark: Generated from `#/paths/api/v1/tasks/GET/responses/255/content/*\/*`.
                    case any(OpenAPIRuntime.HTTPBody)
                    /// The associated value of the enum case if `self` is `.any`.
                    ///
                    /// - Throws: An error if `self` is not `.any`.
                    /// - SeeAlso: `.any`.
                    public var any: OpenAPIRuntime.HTTPBody {
                        get throws {
                            switch self {
                            case let .any(body):
                                return body
                            }
                        }
                    }
                }
                /// Received HTTP response body
                public var body: Operations.getTasks.Output.Code255.Body
                /// Creates a new `Code255`.
                ///
                /// - Parameters:
                ///   - body: Received HTTP response body
                public init(body: Operations.getTasks.Output.Code255.Body) {
                    self.body = body
                }
            }
            /// 투두 목록 조회 성공
            ///
            /// - Remark: Generated from `#/paths//api/v1/tasks/get(getTasks)/responses/255`.
            ///
            /// HTTP response code: `255 code255`.
            case code255(Operations.getTasks.Output.Code255)
            /// The associated value of the enum case if `self` is `.code255`.
            ///
            /// - Throws: An error if `self` is not `.code255`.
            /// - SeeAlso: `.code255`.
            public var code255: Operations.getTasks.Output.Code255 {
                get throws {
                    switch self {
                    case let .code255(response):
                        return response
                    default:
                        try throwUnexpectedResponseStatus(
                            expectedStatus: "code255",
                            response: self
                        )
                    }
                }
            }
            public struct NotFound: Sendable, Hashable {
                /// - Remark: Generated from `#/paths/api/v1/tasks/GET/responses/404/content`.
                @frozen public enum Body: Sendable, Hashable {
                    /// - Remark: Generated from `#/paths/api/v1/tasks/GET/responses/404/content/*\/*`.
                    case any(OpenAPIRuntime.HTTPBody)
                    /// The associated value of the enum case if `self` is `.any`.
                    ///
                    /// - Throws: An error if `self` is not `.any`.
                    /// - SeeAlso: `.any`.
                    public var any: OpenAPIRuntime.HTTPBody {
                        get throws {
                            switch self {
                            case let .any(body):
                                return body
                            }
                        }
                    }
                }
                /// Received HTTP response body
                public var body: Operations.getTasks.Output.NotFound.Body
                /// Creates a new `NotFound`.
                ///
                /// - Parameters:
                ///   - body: Received HTTP response body
                public init(body: Operations.getTasks.Output.NotFound.Body) {
                    self.body = body
                }
            }
            /// 등록된 투두가 없습니다.
            ///
            /// - Remark: Generated from `#/paths//api/v1/tasks/get(getTasks)/responses/404`.
            ///
            /// HTTP response code: `404 notFound`.
            case notFound(Operations.getTasks.Output.NotFound)
            /// The associated value of the enum case if `self` is `.notFound`.
            ///
            /// - Throws: An error if `self` is not `.notFound`.
            /// - SeeAlso: `.notFound`.
            public var notFound: Operations.getTasks.Output.NotFound {
                get throws {
                    switch self {
                    case let .notFound(response):
                        return response
                    default:
                        try throwUnexpectedResponseStatus(
                            expectedStatus: "notFound",
                            response: self
                        )
                    }
                }
            }
            /// Undocumented response.
            ///
            /// A response with a code that is not documented in the OpenAPI document.
            case undocumented(statusCode: Swift.Int, OpenAPIRuntime.UndocumentedPayload)
        }
        @frozen public enum AcceptableContentType: AcceptableProtocol {
            case any
            case other(Swift.String)
            public init?(rawValue: Swift.String) {
                switch rawValue.lowercased() {
                case "*/*":
                    self = .any
                default:
                    self = .other(rawValue)
                }
            }
            public var rawValue: Swift.String {
                switch self {
                case let .other(string):
                    return string
                case .any:
                    return "*/*"
                }
            }
            public static var allCases: [Self] {
                [
                    .any
                ]
            }
        }
    }

Client.swift에는 실제로 동작하기 위해 APIProtocol에 대한 구현이 작성되어 있습니다.

public struct Client: APIProtocol {
    /// 투두 목록 조회
    ///
    /// - Remark: HTTP `GET /api/v1/tasks`.
    /// - Remark: Generated from `#/paths//api/v1/tasks/get(getTasks)`.
    public func getTasks(_ input: Operations.getTasks.Input) async throws -> Operations.getTasks.Output {
        try await client.send(
            input: input,
            forOperation: Operations.getTasks.id,
            serializer: { input in
                let path = try converter.renderedPath(
                    template: "/api/v1/tasks",
                    parameters: []
                )
                var request: HTTPTypes.HTTPRequest = .init(
                    soar_path: path,
                    method: .get
                )
                suppressMutabilityWarning(&request)
                converter.setAcceptHeader(
                    in: &request.headerFields,
                    contentTypes: input.headers.accept
                )
                return (request, nil)
            },
            deserializer: { response, responseBody in
                switch response.status.code {
                case 255:
                    let contentType = converter.extractContentTypeIfPresent(in: response.headerFields)
                    let body: Operations.getTasks.Output.Code255.Body
                    let chosenContentType = try converter.bestContentType(
                        received: contentType,
                        options: [
                            "*/*"
                        ]
                    )
                    switch chosenContentType {
                    case "*/*":
                        body = try converter.getResponseBodyAsBinary(
                            OpenAPIRuntime.HTTPBody.self,
                            from: responseBody,
                            transforming: { value in
                                .any(value)
                            }
                        )
                    default:
                        preconditionFailure("bestContentType chose an invalid content type.")
                    }
                    return .code255(.init(body: body))
                case 404:
                    let contentType = converter.extractContentTypeIfPresent(in: response.headerFields)
                    let body: Operations.getTasks.Output.NotFound.Body
                    let chosenContentType = try converter.bestContentType(
                        received: contentType,
                        options: [
                            "*/*"
                        ]
                    )
                    switch chosenContentType {
                    case "*/*":
                        body = try converter.getResponseBodyAsBinary(
                            OpenAPIRuntime.HTTPBody.self,
                            from: responseBody,
                            transforming: { value in
                                .any(value)
                            }
                        )
                    default:
                        preconditionFailure("bestContentType chose an invalid content type.")
                    }
                    return .notFound(.init(body: body))
                default:
                    return .undocumented(
                        statusCode: response.status.code,
                        .init()
                    )
                }
            }
        )
    }
    //...

 

Part2. Tuist에 통합


위에서 제너레이트된 파일을 직접 프로젝트에 옮기거나 제너레이트와 옮기는걸 동시에 수행해주는 쉘 스크립트를 작성해 사용해도 되지만, 저는 Tuist에 이 자체로 통합하고자 프로젝트 구조와 제너레이트 방식을 변경했습니다.

 

1. Pakage.swift 변경합니다.

제너레이트된 파일들을 접근할 타겟을 새로 생성해주었습니다.

OpenapiGenerated라는 타겟을 생성했고 외부에서 의존성을 추가할 수 있도록 products에 library를 추가해주었습니다.

여기서 OpenAPIURLSession 라는 의존성이 OpenapiGenerated에 추가되었는데 이는 Client를 통해 Http 요청을 보낼때 필요한 라이브러리로 Client를 사용하는 쪽에서 꼭 의존하고 있어야합니다.

 

*OpenAPIAsyncHTTPClient는 서버측에서 필요한 라이브러이니 헷갈리지 않게 주의하세요 (사실 제가 헷갈려서 헤맸습니다.. ㅡㅜ)

import PackageDescription

let package = Package(
    name: "OpenapiGenerator",
    platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)],
    products: [
        .library(
            name: "OpenapiGenerated",
            targets: ["OpenapiGenerated"]
        ),
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-openapi-generator", from: "1.0.0"),
        .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.0.0"),
        .package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.0.0"),
    ],
     targets: [
        .executableTarget(
            name: "openapi-generator-cli",
            dependencies: [
                .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime")
            ],
            plugins: []
        ),
        .target(
            name: "OpenapiGenerated",
            dependencies: [
                .product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"),
            ],
            plugins: []
        )
    ]
)

 

2. 폴더 구조도 변경해줍니다.

openapi-generator-cli
│   └── Sources
│       ├── OpenapiGenerated
│       └── openapi-generator-cli
│           ├── main.swift
│           ├── openapi-generator-config.yaml
│           └── openapi.yaml

 

3. MakeFile 구성

Tuist에 통합하기 위해 output 파일의 위치를 지정해주고싶었는데 swift run으로는 동작이 잘 안되어서
직접 바이너리에 접근해 생성하고자 했으나 swift build로 생성된 바이너리로는 동작이 안되어서 Make 파일을 구성해 git에서 바이너리를 가져와 생성하는 방식으로 변경했습니다.

# To see how to drive this makefile use:
#
#   % make help

# The following values can be changed here, or passed on the command line.
SWIFT_OPENAPI_GENERATOR_GIT_URL ?= https://github.com/apple/swift-openapi-generator
SWIFT_OPENAPI_GENERATOR_GIT_TAG ?= 1.0.0
SWIFT_OPENAPI_GENERATOR_CLONE_DIR ?= $(CURRENT_MAKEFILE_DIR)/.swift-openapi-generator
SWIFT_OPENAPI_GENERATOR_BUILD_CONFIGURATION ?= debug
OPENAPI_YAML_PATH ?= $(CURRENT_MAKEFILE_DIR)/Sources/openapi-generator-cli/openapi.yaml
OPENAPI_GENERATOR_CONFIG_PATH ?= $(CURRENT_MAKEFILE_DIR)/Sources/openapi-generator-cli/openapi-generator-config.yaml
OUTPUT_DIRECTORY ?= $(CURRENT_MAKEFILE_DIR)/Sources/OpenapiGenerated

# Derived values (don't change these).
CURRENT_MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST)))
CURRENT_MAKEFILE_DIR := $(patsubst %/,%,$(dir $(CURRENT_MAKEFILE_PATH)))
SWIFT_OPENAPI_GENERATOR_BIN := $(SWIFT_OPENAPI_GENERATOR_CLONE_DIR)/.build/$(SWIFT_OPENAPI_GENERATOR_BUILD_CONFIGURATION)/swift-openapi-generator

# If no target is specified, display help
.DEFAULT_GOAL := help

help:  # Display this help.
	@-+echo "Run make with one of the following targets:"
	@-+echo
	@-+grep -Eh "^[a-z-]+:.*#" $(CURRENT_MAKEFILE_PATH) | sed -E 's/^(.*:)(.*#+)(.*)/  \1 @@@ \3 /' | column -t -s "@@@"
	@-+echo
	@-+echo "The following options can be overriden on the command line:"
	@-+echo
	@-+echo "  SWIFT_OPENAPI_GENERATOR_GIT_URL (e.g. https://github.com/apple/swift-openapi-generator)"
	@-+echo "  SWIFT_OPENAPI_GENERATOR_GIT_TAG (e.g. 1.0.0)"
	@-+echo "  SWIFT_OPENAPI_GENERATOR_CLONE_DIR (e.g. .swift-openapi-generator)"
	@-+echo "  SWIFT_OPENAPI_GENERATOR_BUILD_CONFIGURATION (e.g. release)"
	@-+echo "  OPENAPI_YAML_PATH (e.g. openapi.yaml)"
	@-+echo "  OPENAPI_GENERATOR_CONFIG_PATH (e.g. openapi-generator-config.yaml)"
	@-+echo "  OUTPUT_DIRECTORY (e.g. Sources/ManualGeneratorInvocationClient/Generated)"

generate: $(SWIFT_OPENAPI_GENERATOR_BIN) $(OPENAPI_YAML_PATH) $(OPENAPI_GENERATOR_CONFIG_PATH) $(OUTPUT_DIRECTORY)  # Generate the sources using swift-openapi-generator.
	$< generate \
		--config "$(OPENAPI_GENERATOR_CONFIG_PATH)" \
		--output-directory "$(OUTPUT_DIRECTORY)" \
		"$(OPENAPI_YAML_PATH)"

build:  # Runs swift build.
	swift build

clean:  # Delete the output directory used for generated sources.
	@echo 'Delete entire directory: $(OUTPUT_DIRECTORY)? [y/N] ' && read ans && [ $${ans:-N} = y ] || (echo "Aborted"; exit 1)
	rm -rf "$(OUTPUT_DIRECTORY)"

clean-all: clean  # Clean everything, including the checkout of swift-openapi-generator.
	@echo 'Delete checkout of swift-openapi-generator $(SWIFT_OPENAPI_GENERATOR_CLONE_DIR)? [y/N] ' && read ans && [ $${ans:-N} = y ] || (echo "Aborted"; exit 1)
	rm -rf "$(SWIFT_OPENAPI_GENERATOR_CLONE_DIR)"


dump:  # Dump all derived values used by the Makefile.
	@echo "CURRENT_MAKEFILE_PATH = $(CURRENT_MAKEFILE_PATH)"
	@echo "CURRENT_MAKEFILE_DIR = $(CURRENT_MAKEFILE_DIR)"
	@echo "SWIFT_OPENAPI_GENERATOR_GIT_URL = $(SWIFT_OPENAPI_GENERATOR_GIT_URL)"
	@echo "SWIFT_OPENAPI_GENERATOR_GIT_TAG = $(SWIFT_OPENAPI_GENERATOR_GIT_TAG)"
	@echo "SWIFT_OPENAPI_GENERATOR_CLONE_DIR = $(SWIFT_OPENAPI_GENERATOR_CLONE_DIR)"
	@echo "SWIFT_OPENAPI_GENERATOR_BUILD_CONFIGURATION = $(SWIFT_OPENAPI_GENERATOR_BUILD_CONFIGURATION)"
	@echo "SWIFT_OPENAPI_GENERATOR_BIN = $(SWIFT_OPENAPI_GENERATOR_BIN)"
	@echo "OPENAPI_YAML_PATH = $(OPENAPI_YAML_PATH)"
	@echo "OPENAPI_GENERATOR_CONFIG_PATH = $(OPENAPI_GENERATOR_CONFIG_PATH)"
	@echo "OUTPUT_DIRECTORY = $(OUTPUT_DIRECTORY)"

$(SWIFT_OPENAPI_GENERATOR_CLONE_DIR):
	git \
		-c advice.detachedHead=false \
		clone \
		--branch "$(SWIFT_OPENAPI_GENERATOR_GIT_TAG)" \
		--depth 1 \
		"$(SWIFT_OPENAPI_GENERATOR_GIT_URL)" \
		$@

$(SWIFT_OPENAPI_GENERATOR_BIN): $(SWIFT_OPENAPI_GENERATOR_CLONE_DIR)
	swift \
		build \
		--package-path "$(SWIFT_OPENAPI_GENERATOR_CLONE_DIR)" \
		--configuration "$(SWIFT_OPENAPI_GENERATOR_BUILD_CONFIGURATION)" \
		--product swift-openapi-generator

$(OUTPUT_DIRECTORY):
	mkdir -p "$@"

.PHONY: help generate build clean clean-all dump

아래 명령어를 통해 OpenapiGenerated 폴더에 Client.swiftTypes.swift가 생성되게 됩니다.

$ make generate

 

4. Tuist에 로컬 패키지 추가

Tuist의 Pakage.swift 파일에 path 경로를 입력해 패키지를 추가해줍니다.

let package = Package(
    name: "App",
    dependencies: [
        .package(path: "../openapi-generator-cli")
    ],
    targets: []
)

 

필요한 모듈에 external 의존성을 추가한 후 generate 잊지 마세요!

 

Part3. Client 사용


이제 직접 코드로 서버와 통신해보겠습니다. (part2 생략하고 가능)

 

1. 필요한 모듈을 임포트해줍니다.

import OpenapiGenerated // part2 진행했을 경우 

import OpenAPIURLSession

 

OpenAPIURLSession의 경우 part2와 비슷하게 진행하셨다면 이미 의존성이 있을 텐데, 만약 없다면 아래 URL을 통해 의존성을 추가합니다. 
https://github.com/apple/swift-openapi-urlsession

 

2. 통신 코드 작성하기

이제 실제로 통신하는 코드를 작성할 수 있는데요, client를 생성한 후 간단하게 메서드 호출을 통해 서버와 통신할 수 있습니다.

let client = Client(
    serverURL: URL(string: "https://any.url.com")!,
    transport: URLSessionTransport()
)

do {
    let response = try await client.health()

    print("::: \(response)")
} catch {
	print("::: \(error)")
}

 

성공!!

 

3. 미들웨어 추가하기

헤더에 액세스 토큰 등의 공통 작업을 위해서 미들웨어를 추가할 수 있습니다.

+ 미들 웨어 부분은 업데이트 예정

 


이렇게 openapi generator를 적용해보았습니다.

사실 dto랑 repository 작성할 시간이 없어서 적용해보게 되었는데 생각보다 더욱 편리하고 장점이 많은 것 같습니다.

모바일 개발에서 아직 메이저한 방식은 아니지만 React 진영에선 많이 사용한다고 알고있습니다.

iOS, Swift 개발에도 많은 사람들이 적용하게 되었으면 좋겠네요!!

읽어주셔서 감사합니다.

 

+ 백엔드 배포에 맞춰 Yaml을 땡겨올 수 있는 파이프라인을 이용해본다면 어떨까요?! 😀

 

ref.

https://github.com/apple/swift-openapi-generator?tab=readme-ov-file

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함