올리브영 테크블로그 포스팅 하이브리드 앱에 구축하는 iOS 개발자모드
Tech

하이브리드 앱에 구축하는 iOS 개발자모드

추상화와 모듈화를 이용하여 확장 가능한 개발자모드 설계하기

2025.11.06

안녕하세요! 올리브영의 iOS Junior Engineer Ted입니다.
저는 오늘, 올리브영 iOS 앱에 개발자모드를 구축했던 경험에 대해 이야기하려 합니다.
신입 개발자로 올리브영에 합류하여 첫 프로젝트로 진행했던 개발자모드 구축을 통해 하이브리드 앱 구조에 적응하고, FE와 소통하는 법을 배우며, SwiftUI로 개발한 경험을 공유하겠습니다 💪🏻💪🏻💪🏻


👶🏻 주니어 개발자가 경험한 올리브영 iOS 앱


올리브영 앱은? 하이브리드 앱!

신입 iOS 엔지니어로 올리브영에 합류했을 때 마주친 수많은 WebView에 적잖이 당황했었습니다 😯 입사 전에는 네이티브 위주의 개발을 주로 경험했고, WebView는 가볍게만 사용해보았습니다. 팀에 합류했을 때에는 많은 서비스가 웹 기반이었기 때문에 iOS 웹뷰에 대한 이해가 많이 필요했습니다. 처음으로 개발해보는 하이브리드 앱의 구조가 어렵기도 하면서 새롭다고 느꼈던 것 같습니다. 아래는 올리브영 앱의 일부 구조입니다. 한번 같이 보시죠!

올리브영 앱 구조

올리브영 앱 구조

위의 올리브영 앱 구조를 보시면 아시겠지만, 많은 부분이 웹 기반의 서비스입니다.
이런 웹 기반의 환경에서 고객에게 서비스를 제공하기위해, iOS 파트에서는 다음과 같은 일들을 FE와 소통하여 개발하고 있습니다.

1. URL 기반으로 올리브영 앱의 전반적인 Navigation을 관리합니다.

2. 각 서비스 영역에서 필요한 네이티브 기능들을 제공합니다.

느낌이 오지 않나요? 네 맞아요. 올리브영 앱은 원활한 고객경험을 위해 FE와 상당히 많은 부분을 고민하고 함께 개발합니다. 이런 상황에서 FE 개발자와 협업하는 방식도 저에게는 새로운 경험이었습니다. 새롭게 알아가야 할 게 많은 상황에서 저는 첫 프로젝트였던 개발자 모드 구축을 통해 WebView에 익숙해지며, FE 개발자분들과 협업하는 방식을 배웠습니다.


첫 프로젝트: 개발자모드


🧑🏻‍💻 매일 매일 들어오는 디버깅 요청: 개발자모드의 등장


올리브영 앱을 개발하다보면 FE개발자 분들과 소통할 일이 정말 많습니다. 웹과 iOS 앱이 함께 기능을 개발하며 고객에게 서비스를 전달하기 때문입니다. 필요한 기능을 웹에 API 형태로 전달하기도 하고 웹에서 전달받은 데이터를 이용해 앱에서 특정 기능을 동작하도록 합니다. 이 과정에서 iOS 웹뷰인 WKWebView의 WKScriptMessage를 통해 필요한 데이터를 주고받습니다. (올리브영에서는 이를 편의상, JS Interface라고 부르고 있습니다)

그러다보면, Slack에서 다음과 같은 대화가 잦아집니다.

슬랙의 수많은 디버깅 요청 내역

슬랙의 수많은 디버깅 요청

앱과 웹에서 데이터가 제대로 송수신이 되었는지 확인하기위해서는 직접 Xcode에서 빌드하여 디버깅을 해야합니다. 이에 더해서 Safari 웹 인스펙터를 이용하여 웹도 함께 디버깅을 해야하는 상황이 매번 연출되었습니다. 하이브리드 앱을 디버깅하기 위해서는 이렇게 양방향으로 디버깅을 해야하는 어려움이 있습니다. 이런 일이 계속 있다고 한다면? 상당히 비효율적이고 시간도 많이 소요됩니다. 이런 이유로 앱 개발자와 FE 개발자의 생산성 향상을 위해 올리브영에 개발자모드가 도입되었습니다.


개발자모드? 🤔

먼저, 개발자모드가 뭔지 알아야겠죠?
올리브영의 개발자모드는 앱 실행중에 다양한 디버깅과 테스트 기능들을 지원하는 툴 입니다. 앱 개발자뿐만 아니라, FE 개발자들도 실행중인 앱 내에서 로그를 파악하고, 테스트 목적으로 특정 기능을 실행할 수 있는 필수적인 툴 입니다. 물론 개발자들의 디버깅을 돕는 외부 라이브러리도 있습니다! 하지만, 하이브리드 앱 구조를 고려한 커스텀 기능들을 위해 직접 개발자모드를 구축했습니다.

올리브영 iOS의 개발자모드는 SwiftUI를 이용하여 개발했습니다. 개발자 모드는 외부 사용자에게 노출되지 않고, 개발환경에서만 사용되는 기능입니다.따라서 UI 완성도보다는 빠른 구현 속도와 유연한 변경이 더 중요합니다. SwiftUI를 선택한 가장 큰 이유는 선언형 UI의 생산성입니다.

화면의 상태를 데이터로 표현하고, 이를 기반으로 UI를 선언하듯 작성하면 기존 UIKit 대비 훨씬 빠르게 UI를 만들 수 있습니다. 간단한 View@State, @Binding만으로도 필요한 화면을 모두 구현할 수 있었고, 짧은 주기로 기능을 실험하고 수정하는 개발자모드 특성에 딱 맞았습니다. 결과적으로, SwiftUI를 통해 개발자 모드의 화면은 기획 → 개발 → 배포까지의 사이클이 크게 단축되었고, 개발자들에게 필요한 기능들을 빠르게 구현하고 배포할 수 있었습니다.
(SwiftUI 덕분에 빠르게 개발하고 퇴근이 가능해졌습니다!)

빠르게 개발하고 퇴근하자!
출처: MBC '무한도전' 캡처

What's in my 개발자모드? ⚙️


개발자모드 버튼

개발자 모드 플로팅 버튼 동작 애니메이션

개발자 모드 버튼

그럼 올리브영 iOS 앱의 개발자 모드에 대해 소개해드리겠습니다.
올리브영 개발자 모드는, 플로팅 버튼을 통해 진입할 수 있습니다. (물론 개발환경에서만 진입할 수 있습니다!) 앱과 별도의 UIWindow에 개발자 버튼을 배치하여 항상 모든 화면보다 개발자 모드가 위에 있을 수 있도록 처리했습니다. 올리브영 개발자들은 버튼을 클릭하여 개발자모드에 진입하여 다양한 기능들을 사용할 수 있게 되었습니다.

또한 버튼에 앱 버전, iOS 버전, 현재 서버,, 활성화된 기능, 을 표시해두어 빠르게 기기 버전과 앱 버전을 파악할 수 있게 했습니다. 덕분에 Slack이나 Jira를 통해 전달받는 이슈에서도 개발자 모드 버튼만 보면 앱 버전과 서버를 즉시 확인할 수 있어, 이슈 추적 속도가 크게 빨라졌습니다.



WebView Debugger

개발자 모드 내의 WebView 메뉴 스크린샷

개발자 모드 : WebView 메뉴

다양한 기능들이 있지만, 올리브영 개발자 모드는 웹뷰에 관련된 기능을 가장 많이 사용합니다. 이 기능들을 통해 FE 개발자와 앱 개발자들은 로그를 확인할 수 있게 되었고, 런타임에 다양한 기능들을 사용할 수 있게 되었습니다. 더 이상 Xcode에서 디버깅을 하지 않아도 되었고, FE-App 개발자 모두 앱에서 로그를 확인할 수 있어 불필요한 커뮤니케이션이 사라졌습니다! 그럼 어떤 기능이 있는지 간략히 보여드리겠습니다.

URL Editor

  • 최상단 웹뷰의 URL의 정보와 User Agent 정보를 확인할 수 있으며, URL 이동, Query Parameter 수정이 가능합니다

웹 로그 탐색기

  • 화면별 WKScriptMessage 로그, javascript 실행 로그를 확인하여 디버깅이 가능합니다.
  • URL 이동로그, 외부링크 앱 진입 로그를 통해 어떤 경로로 앱의 Flow를 디버깅할 수 있습니다.

웹 스토리지 뷰어

  • 최상단 웹뷰의 세션 스토리지, 로컬 스토리지, 쿠키 값을 확인할 수 있습니다.

Javascript Console

  • 최상단 웹뷰에 런타임에 스크립트를 앱에서 실행할 수 있습니다.

스택 스크린 로그

  • 앱의 뷰 계층 구조(View Hierarchy)를 시각화할 수 있으며, 각 화면별 로그를 그룹화 할 수 있습니다.



로컬 플래그: 기능 플래그 스위치

개발자 모드의 또 다른 주인공은 바로 로컬 플래그 입니다.

로컬 플래그는 다양한 이유로 도입된 기능입니다. 올리브영의 일부 서비스는 안정적인 운영을 위해 기능 플래그를 이용하고 있습니다. 새로운 기능을 배포할 때 기능 플래그를 이용해 혹시 모르는 상황에 대비해 언제든지 기존 동작으로 돌아갈 수 있도록 설계하고 있습니다. 앱은 롤백이 불가능하고, 배포하기 위한 프로세스가 복잡하다보니, 중요한 기능들에 대해서는 기능 플래그를 사용하고 있습니다.

기능 플래그를 이용해 개발을 하면 다음과 같은 프로세스가 추가적으로 발생합니다.

개발시에는 개발환경에만 필요한 플래그와 데이터를 서버에 등록해두어야 합니다. (운영 환경에 영향을 미치면 안됩니다!)

QA 시에는 해당 서버의 기능 플래그를 직접 On/Off하며 기존 동작과 새로운 기능이 모두 잘 동작하는지 테스트 해야합니다.


이런 프로세스가 추가적으로 생기면서, 따라오는 비효율과 리스크가 발생했습니다. 개발 혹은 QA 시에 매번 기능 플래그를 On/Off를 해야하니 비효율이 증가했습니다. 단순 true/false 플래그가 아닌, 리스트형태로 갖고 있는 데이터도 변경해야하다보니 꼼꼼하게 검수도 해야 했습니다. 또한, 개발시에 사용하는 플래그와 운영에서 사용하는 플래그가 분리되어있고, QA 테스트에서 On/Off를 수시로 해야한다면, 실수로 운영 환경에 플래그를 변경할 위험이 있습니다. 이와 같은 이유로 기능 플래그 값을 직접 수정하지 않고 테스트폰에서 직접 플래그 값을 조절할 수 있는 로컬 플래그 를 개발하게 되었습니다.


개발자모드 기능 플래그
개발자모드 기능 플래그
  • 로컬 플래그를 On하면 개발자가 설정하는 값으로 기능이 동작합니다
  • 로컬 플래그에서 값을 변경하면 사용자가 식별하기 쉽게 파란 동그라미와 변경됨 표시를 제공합니다

해당 기능을 통해, 원격 서버의 값 변경없이 개발 & QA 시에 테스트 기기에서 플래그값을 조절 할 수 있게 되었습니다. 이 덕분에, 운영상에 영향을 미칠 리스크가 사라졌습니다. 그뿐만이 아닙니다. QA 엔지니어와 앱 개발자 간에 플래그 조절을 위한 커뮤니케이션도 할 필요 없게 되었습니다. 앱 개발자는 이제 기능 개발에 집중할 수 있게 되었고, 이는 또 빠른 퇴근으로 이어질 수 있었습니다.

하지만, 이 기능을 구현하는데에 있어 많은 고민들이 필요했었는데, 그 고민의 결과를 잠깐 공유하고자 합니다.



고민의 결과

로컬 플래그 기능은 여러 버전을 거쳐서 현재의 안정적인 버전으로 자리를 잡았는데요. 이 과정에서 많은 리팩토링이 있었습니다. 리팩토링을 하며 모듈화, 추상화, 디자인패턴 등 많은 것을 경험할 수 있었습니다. 많은 고민들이 있었지만 구현상의 중요한 고민은 다음과 같았습니다.

어떻게 런타임에 리모트 플래그를 볼 것인지, 로컬 플래그를 볼 것인지 결정할 수 있을까? (리모트 - 서버에 저장된 값, 로컬 - 테스트 단말의 저장된 값)

운영 환경에는 절대 로컬 플래그로 동작하면 안된다! (그것은 사고니까..)

현재의 결과를 보며 어떻게 그 고민이 해소되었는지 보여드리겠습니다. 먼저 모듈 관계와 코드를 통해 OYFlag 모듈의 역할을 먼저 같이 보시죠!

플래그와 개발자모드 간의 모듈 관계도

플래그 & 개발자모드 모듈 관계

// MARK: OYFlag Module

/// 실제 플래그 값을 가지고 있는 Object
final class RemoteFlag {
  private var flags: [String: FlagValue] = [:]
  init() {
    fetchFlags()
  }
  ...
}
// MARK: OYFlag Module

/// Flag 값을 return 할 수 있는 프로토콜
protocol FlagProvidable {
  func value<T: Decodable>(for key: some FlagKey) -> T?
}

우선 모듈 관계입니다. Common Layer에 Flag Module이 있습니다. 이 플래그모듈의 역할은 단순합니다.

  • 플래그 값을 서버로부터 받아와서 가지고 있습니다.
  • 이 플래그 값을 Return 하는 프로토콜 타입이 있습니다.

이 모듈이 제공하는 프로토콜을 이용해서 이제, 운영에서는 운영용 Flag Provider를, 개발환경에서는 개발용 Flag Provider를 만들 수 있습니다.

다음으로는 이 프로토콜을 사용하는 OYDevMode의 모듈의 코드의 일부를 보시죠!


// MARK: OYDevMode

import OYFlag

final class DevFlagProvider: FlagProvidable {
  @Published private var isLocalFlagMode = false
  @Inject private var localFlag: LocalFlag! // 로컬 플래그는 컨테이너에 넣어두고, 개발자모드에서도 사용합니다
  private let remoteFlag = RemoteFlag()

  func value<T: Decodable>(for key: some FlagKey) -> T? {
    isLocalFlagMode ? localFlag.value(for: key.name) : remoteFlag.value(for: key.name)
  }
}

개발환경에서만 사용할 FlagProvider입니다. 운영에서 사용할 FlagProvider는 운영 모듈에서 따로 구현하여 Remote Flag 값만 Return 할 수 있게 했습니다. 따라서 이제는 앱이 메모리에 올라올 때에, 이 전역적으로 사용할 Provider의 Concrete 타입만 잘 정해서 넣어주면 됩니다. 이렇게 하면 운영에서는 운영 Provider를 사용할 수 있고 개발 환경에서는 개발용 Provider를 사용할 수 있습니다.


/// Container Register
#if DEBUG
// 개발 환경
OYContainer.register(FlagProvidable.self) { _ in DevFlagProvider() }
#else
// 운영 환경
OYContainer.register(FlagProvidable.self) { _ in FlagProvider() }
#endif

여기서 잠깐. OYContainer가 무엇이냐고요? OYContainer는 올리브영 iOS 프로젝트에서 자체적으로 개발하여 사용중인 경량 DI 컨테이너입니다. 일반적인 컨테이너와 같은 기능을 지원하고, @Inject, @LazyInject와 같은 커스텀 프로퍼티 래퍼를 이용하여 의존성 주입을 합니다. FlagProvidable를 해당 컨테이너에 넣어두고 여러 Feature 모듈에서 사용하고 있습니다.

아무쪼록 Protocol과 컨테이너를 이용하여 Flag를 사용하는 코드에서 운영환경이든 개발환경이든 관계 없이 코드를 작성할 수 있습니다. Flag를 필요로하는 비즈니스 로직 혹은 View 어느 곳이든 Container를 통해 FlagProvider를 이용할 수 있습니다. 바로 다음과 같이 사용하면 됩니다.

import OYFlag

struct ProductView: View {
  @Inject private var flagProvider: FlagProvidable!

  var body: some View {
    ...
    if flagProvider.value(for: ProductKey.featureA) == true {
      // 기능 플래그에 따른 특정 기능
    }
    ...
  }
}

그럼 개발자 모드에서는 어떻게 값을 변경하냐고요? 개발자모드에서는 간단하게 로컬 플래그를 변경할 수 있습니다! 컨테이너에 등록되어 있는 로컬 플래그 Object의 값을 변경하면 됩니다.

struct LocalFlagView: View {
  // 컨테이너에 등록되어 있는 LocalFlag
  @ObservedObject private var localFlag = OYContainer.resolve(LocalFlag.self)!

  var body: some View {
    Toggle(isOn: Binding(
      get: { localFlag.flagDict[key] },
      set: { newValue in localFlag.flagDict[key] = newValue } // Toggle 변경시 LocalFlag 값 변경
    )) {
      VStack {
        Text(key)
      }
    }
  }
}

이렇게 추상화와 모듈화를 이용한 설계 덕분에 해결하고 싶었던 문제를 해결할 수 있게 되었습니다.

✅ 개발자모드에서 로컬 플래그를 이용하여 런타임에 값을 변경
✅ 운영 환경과 개발 환경의 Flag Provider를 완전히 분리하여 운영에 미칠 리스크 원천 차단
✅ OCP(Open/Closed principle)을 지킴으로써 향후 A/B 테스트를 위한 ABFlagProvider가 추가되어도 수정없이 확장가능

처음에는 이렇게 깔끔하게 설계하지 못했었는데, 동료들과 고민하고 보완해나가니 배우는 것도 많았고 특히 추상화와 모듈화의 실용적인 적용을 경험할 수 있어 좋았습니다 😎 어때요 개발자모드 참 쉽죠?

어때요, 참 쉽죠
출처: EBS의 '그림을 그립시다'의 밥로스

🚀 0건의 디버깅 요청, 그리고 주니어의 성장

개발자모드 도입의 성과는 명확했습니다. 가장 큰 변화는 두 가지입니다. 첫째로, 웹뷰 관련 단순 디버깅 요청은 0건으로 수렴했습니다. 두번째로, 기능 플래그 때문에 서버의 값을 바꾸는 비효율적이고 위험한 일은 하지 않아도 됩니다. FE 개발자분들은 필요한 로그를 직접 앱에서 확인하고, QA 엔지니어분들은 로컬 플래그를 통해 서버 값 변경 없이도 자유롭게 테스트를 진행할 수 있습니다. "개발자 모드에 디버깅에 필요한건 다 있어서 개발이 많이 편해졌다"라는 FE 개발자분들의 긍정적인 피드백도 받을 수 있었습니다.

결과적으로 앱 개발자는 불필요한 커뮤니케이션 비용을 줄이고 본연의 기능 개발에 더 집중할 수 있게 되었습니다.

신입 개발자로서 첫 프로젝트를 진행하며, 올리브영 앱의 하이브리드 아키텍처와 도메인 지식을 빠르게 습득할 수 있었습니다. SwiftUI의 높은 생산성 덕분에 UI 개발 시간은 아끼고, FlagProvidable 프로토콜 설계처럼 더 중요하고 근본적인 고민에 시간을 쏟을 수 있었습니다.

개발자모드는 단순히 편의 기능을 만든 것이 아니라, 개발자들의 비효율을 해결하는 엔지니어링 이었습니다. 앞으로도 올리브영에서 동료들의 생산성을 높이고, 나아가 고객에게 더 나은 서비스를 제공하는데 기여하는 엔지니어로 성장하겠습니다! 💪🏻💪🏻 여러분의 경험과 질문을 댓글로 남겨주시면 함께 고민하고 성장할 수 있을 것 같습니다! 🙇🏻

iOSSwiftUIHybridApp
올리브영 테크 블로그 작성 하이브리드 앱에 구축하는 iOS 개발자모드
🥩
ted |
iOS App Engineer
요리하는 개발자