← Back to list

App Store 5.1.1 가이드라인 리젝 대응기: SwiftUI Repository 패턴으로 게스트 기능 구현하기

소개

Nello in 펫프렌즈 기술블로그 · 2025-12-03 06:02 · 154 claps · 8.2 min read
#펫프렌즈 #app-development
Open on Medium ↗
Wiki topics: 📱 · Mobile Development

App Store 5.1.1 가이드라인 리젝 대응기: SwiftUI Repository 패턴으로 게스트 기능 구현하기

소개

안녕하세요! 오랜만에 포스팅하러 또 찾아온, iOS 앱 개발자 넬로입니다. 저희 회사에는 사내 카페라는 좋은 복지가 있습니다. 그리고 저는 이 카페를 이용할 수 있는 임직원용(?)앱을 관리하고 있습니다.

배경

어느날과 다름없이 이 앱의 개발을 마치고 심사를 넣었습니다. 그런데 다음 날 아침, App Store Connect에 빨간 불이 들어와 있었습니다.

iOS 개발자들의 심장을 자극하는 짤.Jpg 😮‍💨

iOS 개발자들의 심장을 자극하는 짤.Jpg 😮‍💨

요약하자면 로그인 안 해도 앱 좀 둘러보게 해줘라!는 것이었습니다. 사내 임직원 전용 앱이라 로그인이 필수라고 생각했지만, Apple의 가이드라인은 냉정했습니다.

빨리 돌아가서 고쳐와

빨리 돌아가서 고쳐와

그러나, 단순히 if isGuest { return } 처리만 하기에는 기존 코드가 너무 복잡하게 얽혀있었습니다. 특히 최근에 SwiftUI로 막연히 전환하면서 화면간 연결이 강하게 되어있는 상태였습니다. 결국 기존 로직을 망가뜨리지 않으면서, 가짜 데이터로 앱을 구동시켜야 한다는 미션이 주어졌습니다. 이 글은 Repository 패턴DI Container를 도입해 게스트모드를 구현하여 이 미션을 극복한 후기입니다.

문제

게스트 모드를 막상 넣으려니 세 가지 큰 벽에 부딪혔습니다.

  1. 스파게티 코드 위험: 기존 ViewModel들이 API 서비스를 직접 호출하고 있어, 여기에 if/else를 넣다간 지옥문이 열릴 것 같았습니다.
  2. 권한 에러: 게스트는 실제 API를 호출하면 401 Unauthorized를 반환합니다. 따라서, 자연스러운 Mock 데이터가 필요했습니다.
  3. 데이터 증발: 게스트 모드에서 장바구니에 담은 커피들은 로그인을 하면 날아가버리니 데이터 전환 전략이 필요했습니다.

해결

어떻게 해야할지 막막했지만, 급할수록 돌아가라는 말처럼 차근차근 아키텍처를 뜯어고치기로 했습니다.

1. Repository 패턴으로 ‘데이터 책임’ 떠넘기기

가장 먼저 ViewModelNetworkService를 직접 알지 못하게 막았습니다. Repository를 중간 관리자로 두기로 했습니다.

이게 Swift식 POP다! M̶y̶ ̶l̶i̶t̶t̶l̶e̶ ̶s̶o̶d̶a̶P̶O̶P̶!̶

이게 Swift식 POP다! M̶y̶ ̶l̶i̶t̶t̶l̶e̶ ̶s̶o̶d̶a̶P̶O̶P̶!̶

이제 ViewModel은 데이터가 서버에서 오는지, 제가 만든 가짜 파일(Mock)에서 오는지 알 필요가 없습니다. 그저메뉴 줘라고 요청하면 끝입니다. 이렇게 분리해두니 디버깅할 때도 훨씬 편했습니다.

protocol MenuRepository {
    func fetchMenuList() async throws -> [MenuModel]
}

final class DefaultMenuRepository: MenuRepository {
    private let dataSource: NetworkDataSource // 핵심! 프로토콜에 의존
    init(dataSource: NetworkDataSource) {
        self.dataSource = dataSource
    }
}

2. DI Container로 ‘태세 전환’ 자동화

게스트 모드 시작!버튼을 누르면 앱 전체가 가짜 데이터를 바라봐야 합니다. 이를 일일이 수정하는 건 비효율적이라, DI(Dependency Injection) Container를 직접 구현했습니다.

간단하게 구성한 DI Container 로직

간단하게 구성한 DI Container 로직

코드로 보면 이런 식입니다. isGuestMode 플래그 하나로 앱의 모든 데이터 소스를 교체합니다.

final class DIContainer {
    static let shared = DIContainer() // 혼자왔니? 어~ 싱글톤(?)이야..이 아니라 편의상 사용
    private var isGuestMode: Bool = false

    private var networkDataSource: NetworkDataSource {
        if let dataSource = _networkDataSource { return dataSource }

        let dataSource: NetworkDataSource = isGuestMode 
            ? MockNetworkDataSource()  // 게스트면 가짜
            : NetworkService()         // 회원이면 진짜

        _networkDataSource = dataSource
        return dataSource
    }

    func setGuestMode(_ isGuest: Bool) {
        guard isGuestMode != isGuest else { return }
        isGuestMode = isGuest
        resetDependencies() // 의존성 갈아끼우기
    }
}

3. 메소드 연기를 보여줄 Mock 데이터

게스트 모드에서 보여줄 데이터도 제가 직접 만들어야 했습니다. MockNetworkDataSource를 만들어 마치 서버 통신을 하는 것처럼 연기(Delay)를 시켰습니다.

final class MockNetworkDataSource: NetworkDataSource {
    func fetchMenuList() async throws -> BaseModel<[MenuModel]> {
        // 실제 네트워크처럼 0.3~0.8초 딜레이 (사용자 경험을 위해!)
        try await simulateNetworkDelay() 
        return BaseModel(message: "Success", data: MockData.menus)
    }
}

View 분리는 타협하지 말자

처음엔 MainView 파일 하나에 if isGuest를 넣어보려 했습니다. 하지만 스크롤 몇 번 해보고 바로 깨달았습니다.

이건 반드시 나중에 문제가 생길 수 있는 코드다..!!

이건 반드시 나중에 문제가 생길 수 있는 코드다..!!

따라서, 로그인용 MainView게스트용 GuestMainView를 분리했습니다. 코드가 중복되는 것 같아도 유지보수성을 위해 명확하게 분리하는 게 낫다고 판단했습니다. 가독성측면에서도 상당히 차이가 느껴졌습니다.

트러블슈팅

런치 스크린의 함정

앱 업데이트 팝업 로직도 같이 SwiftUI로 개선했었습니다.

하지만 비동기 처리로 인해, 런치 스크린뷰가 자동으로 넘어가버렸습니다. 이로인해 팝업을 확인 못하게된 이 이슈를 뒤늦게나마 동료 덕분에 발견했습니다. 👍

이 이슈는 팝업 선택을 해야만 런치 스크린이 정상적으로 종료되고, 다음 화면으로 떨어지게끔하여 해결했습니다.

환경은 항상 잘 인지할 수 있도록 하자!

“어? 왜 데이터가 안 나오지?”

알고 보니 환경 설정을 잘못해서 게스트 모드인데 실제 API를 호출하고 있었습니다.

또는 개발자가 환경을 잘못 인지하는 경우도 종종 있었습니다.

따라서, 현재 환경이나 게스트모드 여부를 확실히 알 수 있도록 DEV환경 및 GUEST MODE를 배너로 추가해서 아주 큼지막하게 띄워 잘 인지하도록 만들었습니다.

struct GuestMainView: View {
    // ...
    var body: some View {
        NavigationStack {
            VStack {
                headerSection 

                // 개발 환경에서만 보이는 배너
                #if DEV
                environmentBanner 
                #endif

                MyMenuListView(viewModel: myMenuViewModel)
            }
        }
    }
}

배너 디자인은 제가 셀프로 했습니다. SwiftUI 만세! 🙂

배너 디자인은 제가 셀프로 했습니다. SwiftUI 만세! 🙂

결론

  • App Store 심사 통과: (감격) 5.1.1 가이드라인 이슈 해결! 🎉 아묻따로 하이패스되었습니다.✌️
  • 확장성 확보: Repository 패턴 덕분에 나중에 서버 API가 바뀌어도 대응하기 쉬워졌습니다.
  • 자신감 상승: 아키텍처에 대한 지식과 경험이 이번 기회에 잘 쌓였습니다.

“바쁜와중에 리젝만 해결하면 되고 덜 중요한 앱이니, 대충 if문으로 때울까?”

하는 유혹이 컸던 게 사실입니다. 하지만 !!

“사이드 프로젝트니까 오히려 다양한 시도를 해보고 경험을 쌓을 수 있는 유일한 기회 아닐까?”

라고 생각을 다시 고쳐먹고 열심히 공부하고 설계하고 작업한 결과, 앱 지식의 기초 체력을 튼튼하게 다질 수 있었습니다.

혹시 저처럼 리젝을 받고 막막해하시는 분들이 계시다면, 이번 기회에 아키텍처를 한번 점검해 보시는 건 어떨까요? 성장을 이룰 수 있는 좋은기회가 될 수 있을겁니다.

Next

다음엔, 중니어 iOS 개발자가 Push 기능을 출시하면서 백엔드 영역까지 풀스택으로 작업해본 후기를 공유해보겠습니다!! 부족한 글 읽어주셔서 감사합니다.

참고


메타데이터
post_id
9cfed1086bfe
slug
app-store-5-1-1-가이드라인-리젝-대응기-swiftui-repository-패턴으로-게스트-기능-구현하기-9cfed1086bfe
url
https://techblog.pet-friends.co.kr/app-store-5-1-1-%EA%B0%80%EC%9D%B4%EB%93%9C%EB%9D%BC%EC%9D%B8-%EB%A6%AC%EC%A0%9D-%EB%8C%80%EC%9D%91%EA%B8%B0-swiftui-repository-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EA%B2%8C%EC%8A%A4%ED%8A%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-9cfed1086bfe
canonical_url
https://techblog.pet-friends.co.kr/app-store-5-1-1-%EA%B0%80%EC%9D%B4%EB%93%9C%EB%9D%BC%EC%9D%B8-%EB%A6%AC%EC%A0%9D-%EB%8C%80%EC%9D%91%EA%B8%B0-swiftui-repository-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EA%B2%8C%EC%8A%A4%ED%8A%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-9cfed1086bfe
author_url
https://medium.com/@hu.kwon
status
ok
fetched_at
2026-06-11 12:34:08