상세 컨텐츠

본문 제목

[Swift] Modal 화면에서 Push 전환이 필요할 때

카테고리 없음

by kimrindev 2025. 7. 9. 18:51

본문

 

 

 

Modal을 사용한 독립적인 플로우 구현 시 자주 마주치는 문제와 해결 방법을 실제 코드와 함께 살펴보겠습니다!

언제 Modal 기반 플로우가 필요한가?

독립적인 플로우의 특징

회원가입, 약관동의, 온보딩 같은 플로우는 메인 앱과는 다른 성격을 가진다:

  • 일회성 작업: 한 번 완료하면 다시 볼 일이 거의 없음
  • 완료 후 진입 개념: 메인 앱으로 "돌아가는" 것이 아니라 "진입"하는 개념
  • 순차적 진행: 단계별로 반드시 거쳐야 하는 절차

이러한 특성 때문에 독립적인 플로우는 Modal 기반으로 관리하는 것이 적합하다.

 

Modal 사용이 필수인 이유: 접근 제어

약관동의나 회원가입 플로우를 Modal로 분리하는 가장 중요한 이유는 접근 제어 포인트인데

Navigation 방식의 치명적인 문제점

만약 약관동의를 메인 Navigation Stack에 추가한다면:

// 문제가 되는 접근 방식
class HomeViewController: UIViewController {
    @objc private func startTermsFlow() {
        let termsVC = TermsViewController()
        navigationController?.pushViewController(termsVC, animated: true)
        // 사용자가 약관동의 중간에 다른 화면으로 이동 가능
    }
}
 

코드의 구성에 따라 다르겠지만 이러한 문제점이 생길수있다는점이다.

  • 사용자가 뒤로가기 버튼으로 메인 화면 복귀
  • 탭바의 다른 메뉴 선택 가능
  • 딥링크를 통한 다른 화면 이동 가능
  • 결과: 약관동의 미완료 상태에서 서비스 모든 기능 접근 가능

결국 개발자가 의도한 동작대로 가지 않는 시나리오 인거다. 

 

Modal 방식의 확실한 보안

// 올바른 접근 방식
class HomeViewController: UIViewController {
    @objc private func startTermsFlow() {
        let termsVC = TermsViewController()
        termsVC.modalPresentationStyle = .fullScreen
        present(termsVC, animated: true)
        // 사용자는 오직 두 가지 선택만 가능: 완료 또는 종료
    }
}

Modal로 분리하면 사용자에게 두 가지 선택지만 제공된다

  1. 약관동의 완료
  2. 앱 종료

중간에 다른 곳으로 빠져나갈 여지가 원천적으로 차단하는것이다.

법적 안전성 확보

특히 금융, 의료, 개인정보 처리 앱에서는 이런 접근 제어가 필수인데 

  • 개인정보 처리방침 미동의 상태에서 개인정보 입력 화면 접근 → 개인정보보호법 위반
  • 미성년자 인증 미완료 상태에서 결제 기능 접근 → 법적 문제

결론적으로 Modal을 사용한 컨텍스트 분리는 단순한 UI 패턴이 아니라, 앱의 보안과 법적 안전성을 보장하는 핵심적인 아키텍처 설계 결정인셈이다. 

 

허나 여기서문제가 발생하는데

Modal 사용 시 발생하는 문제

 

단일 ViewController Modal의 한계

일반적인 Modal Present 방식은 이러하다 

let termsVC = TermsViewController()
present(termsVC, animated: true)

 

 

 

 

허나 약관동의같은 부분은 위 이미지처럼 다음을 포함한 형식으로 여러단계에 걸쳐서 구성되는 경우가 많다. 

이용약관 동의 → 개인정보처리방침 동의 → 마케팅 수신동의 → 회원가입 등등 

 

 

사용자가 "다음" 버튼을 눌렀을 때 다음 화면으로 이동해야 하는데:

 

class TermsViewController: UIViewController {
    @objc private func nextButtonTapped() {
        let privacyVC = PrivacyViewController()
        
        // 이 코드는 아무 일도 하지 않습니다!
        navigationController?.pushViewController(privacyVC, animated: true)
        
        print(navigationController) // nil 출력
        // Modal로 띄워진 단일 VC에는 navigationController가 없음
    }
}

 

 

navigationController?.pushViewController(privacyVC, animated: true)

 

Modal로 present된 단일 ViewController에는 navigationController가 nil이기 때문에 pushViewController는 동작하지 않는다. 

 

간단하게 모달을 하나 더띄우거나 dismiss하고 새로 띄우면 되는거 아닌가? 할수도 있지만 또 다른 문제점을 야기하는데

 

잘못된 해결책들과 그 문제점

방법 1: Modal 위에 또 다른 Modal 띄우기

@objc private func nextButtonTapped() {
    let privacyVC = PrivacyViewController()
    present(privacyVC, animated: true) // Modal 위에 Modal
}

 

 

  • Modal이 계속 쌓이면서 사용자 혼란 증가
  • 뒤로가기 동작이 부자연스러움
  • "처음으로 돌아가기" 구현이 복잡해짐
  • 메모리 누적으로 인한 성능 저하

 

방법 2: Modal을 dismiss하고 새로운 Modal 띄우기

 
@objc private func nextButtonTapped() {
    dismiss(animated: true) {
        // 메인 화면에서 다음 Modal 띄우기
        let privacyVC = PrivacyViewController()
        self.presentingViewController?.present(privacyVC, animated: true)
    }
}

 

 

  • 화면이 깜빡이면서 사용자 경험 저하
  • 플로우의 연속성이 끊어짐
  • 이전 화면으로 돌아가기 불가능
  • 진행 상황 추적이 어려움

 

 

그렇기때문에 Modal은 써야 하지만, 내부에서 자연스러운 화면 전환도 필요한것이고.

이 두 가지 요구사항을 모두 만족시키는 방법이 바로 UINavigationController로 Modal을 감싸는 것이다.

 

// 올바른 해결책
let termsVC = TermsViewController()
let navController = UINavigationController(rootViewController: termsVC)
navController.modalPresentationStyle = .fullScreen
present(navController, animated: true)

 

 

이 방법의 장점

1. Modal의 보안성 + Navigation의 편의성

  • 컨텍스트 분리로 인한 접근 제어 유지
  • 내부에서 자연스러운 Push 전환 가능

2. 자연스러운 뒤로가기

  • Navigation Bar의 뒤로가기 버튼이 자동으로 생성
  • 사용자는 익숙한 방식으로 이전 단계로 돌아갈 수 있음

3. 깔끔한 전체 플로우 종료

class FinalStepViewController: UIViewController {
    @objc private func completeFlow() {
        // 전체 Modal Navigation을 한 번에 종료
        dismiss(animated: true)
        // 메인 앱으로 깔끔하게 복귀
    }
}

 

 

그래서 실제로 어떻게 진행되는지 궁금해서 테스트를 진행해보았다. 

동일한 ViewController를 사용하되, 하나는 단일 Modal로, 하나는 NavigationController로 감싸서 비교했다.

 

단일 Modal Present 결과

시각적 결과

- navigationController가 없어서 Navigation Bar가 표시되지 않음

- title 속성은 설정되지만 화면에 보이지 않음

- 상단에 제목 영역 없음


Push 전환 시도 결과

 

- navigationController가 nil이므로 Push 불가능

- beforeCount와 afterCount 모두 0 (변화 없음)

- 대안으로 새로운 Modal을 띄우게 됨


전체 플로우 종료 시도 결과

- Modal이 여러 개 쌓여있으면 하나씩만 닫힘
- 사용자는 여전히 이전 Modal들을 봐야 함
- 전체 플로우를 한 번에 종료할 수 없음

 


 

 

@objc
private func touchUpInsideDirectModalButton(_ sender: UIButton) {
    let modalVC = ModalViewController1()
    modalVC.modalPresentationStyle = .fullScreen
    modalVC.title = "모달만 띄운경우"
    self.present(modalVC, animated: true)
}

 

 

@objc
private func tryPush() {
    let nextVC = ModalViewController2()
    
    // Push 시도 전후의 ViewController 개수 확인
    let beforeCount = navigationController?.viewControllers.count ?? 0
    navigationController?.pushViewController(nextVC, animated: true)
    
    // 잠깐 대기 후 결과 확인
    try? await Task.sleep(for: .seconds(0.5))
    let afterCount = navigationController?.viewControllers.count ?? 0
    
    if afterCount > beforeCount {
        updateStatus("Push 성공!")
    } else {
        // 단일 Modal에서는 여기로 진입
        updateStatus("Push 실패! navigationController가 nil입니다.")
        try? await Task.sleep(for: .seconds(2))
        await showAlternativeAction() // 새로운 Modal 띄우기
    }
}

 

@objc private func backToRoot() {
    // 단일 Modal에서는 navigationController가 nil
    dismiss(animated: true) // 현재 Modal만 닫힘
}

 

NavigationController Modal Present 결과

시각적 결과

- Navigation Bar가 표시되어 title이 보임
- 자연스러운 Navigation 인터페이스 제공
- 상단에 제목과 표준 Navigation Bar 스타일 적용


Push 전환 시도 결과

동일한 tryPush() 코드 실행 시:
- navigationController가 존재하므로 Push 성공
- beforeCount = 1, afterCount = 2 (ViewController 추가됨)
- 자연스러운 화면 전환 가능
- "Push 성공!" 메시지 표시
- 새로운 화면으로 부드럽게 전환


결과:


한 번의 dismiss로 전체 플로우 종료
깔끔하게 메인 앱으로 복귀
중간에 쌓인 모든 화면들이 한 번에 정리됨
사용자는 바로 메인 화면으로 돌아감


 

 

 

@objc
private func touchUpInsideStackModalButton(_ sender: UIButton) {
    let modalVC = ModalViewController1()
    modalVC.title = "네비모달을 띄운경우"
    let navController = UINavigationController(rootViewController: modalVC)
    navController.modalPresentationStyle = .fullScreen
    self.present(navController, animated: true)
}

 

@objc
private func tryPush() {
    let nextVC = ModalViewController2()
    
    // Push 시도 전후의 ViewController 개수 확인
    let beforeCount = navigationController?.viewControllers.count ?? 0
    navigationController?.pushViewController(nextVC, animated: true)
    
    // 잠깐 대기 후 결과 확인
    try? await Task.sleep(for: .seconds(0.5))
    let afterCount = navigationController?.viewControllers.count ?? 0
    
    if afterCount > beforeCount {
        updateStatus("Push 성공!")
    } else {
        // 단일 Modal에서는 여기로 진입
        updateStatus("Push 실패! navigationController가 nil입니다.")
        try? await Task.sleep(for: .seconds(2))
        await showAlternativeAction() // 새로운 Modal 띄우기
    }
}
@objc
private func backToRoot() {
    if let navController = navigationController,
       let presentingVC = navController.presentingViewController {
        // 전체 Modal Navigation을 한 번에 dismiss
        presentingVC.dismiss(animated: true)
    }
}

 

깃허브 원본 코드 링크

 

 

 

결론

Modal 사용은 선택이 아닌 필수인셈이다. 보안과 법적 요구사항 때문입니다. 

하지만 단순한 Modal만으로는 복잡한 플로우를 처리할 수 없다. Push 전환이 안 되기 때문입니다.

( 정확히는 navigationController가 nil이기때문에 Push를 할수 없는것)

 

 

 

따라서 UINavigationController로 Modal을 감싸는 것이 유일한 올바른 해결책!

  • Modal의 보안성은 유지하면서
  • Navigation의 편의성도 함께 얻을 수 있고
  • 사용자에게는 자연스러운 플로우를 제공할 수 있다.

 

"Modal을 쓸 수밖에 없고, Modal을 제대로 쓰려면 NavigationController로 감쌀 수밖에 없다" - 이것이 핵심입니다.