안녕하세요
최근 저는 RIBs아키텍처를 사용한 앱을 개발하고 있습니다.
RIBs아키텍처의 경우 Builder, Router, Interfactor (+ Presenter)로 구성된 구조를 가집니다.
RIBs의 장점중 하나는 모듈이 해제(detach)될 때 구성요소들의 메모리해제 여부를 확인하는 감시자 내부적으로 동작하고 있습니다.
따라서, 특정 RIB을 제거시 메모리 누수가 발생하지 않음을 런타임에 확인할 수 있습니다.
먼저 제가 겪은 문제를 말씀드리겠습니다.
RIBs 메모리 해제 트러블 슈팅
제가 개발하는 앱의 경우 많은 RIB이 존재하고 복잡한 네비게이션 구조를 가집니다.
따라서 RiB이 detach됨가 동시에 Presenter(=UIViewController)를 네비게이션 계층에서 없에 메모리 해제 시켜야합니다.
제가 해당 RIB의 뷰컨트롤러를 프레젠트할 때 아래와 같은 동작을 실행합니다.
먼저 Builder로 생성한 A RIB의 라우터로 부터 A 뷰컨트롤러를 획득합니다.
그 후 해당 뷰컨트롤러를 네비게이션 컨트롤러로 감싸 현재 뷰컨트롤러인 B뷰컨트롤러에서 present합니다.
A RIB의 경우 사라지기 전에 A 뷰컨트롤러에서 Alert를 표시합니다.
Alert에서 확인을 누를 경우 A RIB은 detach되고 사진의 네비게이션 컨트롤러는 dismiss함수를 호출하여 스택에서 제거됩니다.
문제는 여기서 발생합니다, A 뷰컨트롤러가 매모리 해제되지 않았습니다.
뭐가 문제였을까?
시행착오를 조금 겪은후 원론적인 접근을 하자고 생각하게 되었고,
제가 뷰컨트롤러 계층에 대한 정확한 이해를 하고 있는 것인지에 대한 생각을 하게되었습니다.
지금까지의 경우 present를 통해 모달로 화면을 띄우고 dismiss를 띄워진 화면 객체에 호출하면 닫히는 구나!
이 정도로 알고 있었고 그걸로 충분하다고 생각하였지만 아니였습니다.
dismiss함수의 공식 문서를 읽어보면 다음과 같습니다.
뷰컨트롤러에 의해서 모달로 표시된 뷰컨트롤러를 Dismiss한다.
present된 뷰컨트롤러에서 호출 시 Dismiss된다고 생각했던 것과는 설명이 상이합니다.
좀 더 자세한 설명을 읽어보면,
특정 뷰컨트롤러를 present한 뷰컨트롤러는 presemt한 뷰컨트롤러를 Dismiss할 책임을 가진다.

지금까지 사용한 방식에 완전히 반하는 설명이 적혀있는 것을 확인할 수 있습니다.
즉, 특정 뷰컨트롤러를 Present한 뷰컨트롤러에서 해당 뷰컨트롤러를 Dismiss해야 한다는 것입니다.
정리하자면, Dismiss함수는 해당 함수를 호출하는 뷰컨트롤러를 닫는 함수가 아닙니다.
해당 뷰컨트롤러가 Present한 뷰컨트롤러를 Dismiss하는 함수입니다.
Dismiss호출하면 해당 뷰컨트롤러가 닫히던데?
하지만, 뷰컨트롤러에서 Dismiss를 호출하면 해당 화면이 닫힌다는 것 역시 맞습니다.
결과적으로 이렇게 동작한다.
먼저 현재 화면에 보여지는 뷰컨트롤러가 A일 때 Presented, Presenting관계는 다음과 같습니다.
실험 결과 정확한 함수호출을 확인할 수는 없지만 결과적으로 dismiss함수는 다음과 같이 동작합니다.
Presenting뷰컨트롤러가 존재하는 경우 자기 자신이 Dismiss되는 것이 아닌 해당 뷰컨트롤러를 Dismiss합니다.
※ 그림상에 오타가 있습니다 presenting -> presented
계층의 하단 뷰컨트롤러를 Dismiss하면 상단의 모든 뷰컨트롤러도 함께 사라집니다.
공식문서에 따르면 이경우 중간 뷰컨트롤러들은 즉시 삭제되고 최상단 뷰컨트롤러에만 트렌지션 애니메이션이 적용된다고 합니다.
※ 그림상에 오타가 있습니다 presenting -> presented
아래사진의 경우 가장 일반적인 경우로 호출한 뷰컨트롤러가 Dismiss되게 됩니다.
※ 그림상에 오타가 있습니다 presenting -> presented
돌아와서, 내 문제는 무엇이었을까?
이제 저에게 발생한 문제를 해결해 보겠습니다.
앞서 말씀드린대로 A RIB을 해재하려고 하는 경우 A뷰컨트롤러에서는 Alert뷰컨트롤러를 화면상에 표시하게 됩니다.
※ NavigationController스택에 속한 뷰컨트롤러에서 Present한 뷰컨트롤러의 PresentingViewController는 네비게이션 컨트롤러로 지정되게 됩니다.
(반대로 네비게이션 컨트롤러의 PresentedViewController가 해당 뷰컨트롤러로 지정된다.)
여기서 저는 A 뷰컨트롤러를 제거하기위해 네비게이션 컨트롤러의 dismiss함수를 호출할 것입니다.
해당 함수호출은 Alert컨트롤러에서 버튼을 누른 직후 임으로 Alert컨트롤러는 아직 뷰컨트롤러 계층에 머무르는 상태일 것입니다.
(커스텀 Alert컨트롤러로 버튼을 누른다고 즉시 계층에서 배제되지 않음)
따라서 네비게이션 컨트롤러의 dismiss함수는 Alert컨트롤러를 Dismiss하게되고 네비게이션 컨트롤러를 Dismiss하지 않습니다.
왜냐하면 현재 네비게이션 컨트롤러가 present한 뷰컨트롤러가 존재함으로 자기자신이 아닌 Alert컨트롤러가 Dismiss되기 때문입니다.
그렇기에 A뷰컨트롤러 역시 해제되지 못하게되고 메모리 누수가 감지되게되는 것이였습니다.
따라서 저는 해당 문제를 Alert뷰컨트롤러가 스택에서 제거된 이후
네비게이션 컨트롤러의 Dismiss함수를 호출하는 방법으로 해결했습니다.
아래코드는 Alert컨트롤러가 뷰컨트롤러 계층에서 사라진 이후 호출되는 콜백함수입니다.
router?.request(.dismissAlert(completion: { [weak self] in
guard let self else { return }
listener?.request(request: .exitPage(isMissionCompleted: false))
}))
특정 뷰컨트롤러에서 dismiss함수 호출을 통해 해당 함수를 호출한 뷰컨트롤러를 제거하려면
해당 뷰컨트롤러가 Present중인 뷰컨트롤러를 먼저 Dismiss해야합니다.
(presenting viewController가 비어있어야한다.)
결론
뷰컨트롤러 Dismiss함수는 연쇄적으로 형성되어 있는 컨트롤러 스택을 한번에 제거할 수 있을 정도로 강력한 함수입니다.
하지만, 동작 방식을 제대로 이해하지 못할 경우 메모리 누수 문제를 해결하지 못하는 미궁에 빠져버립니다.
특정 뷰컨트롤러의 Dismiss함수를 호출할 때
현재 없애려고 하는 것이 Presented 뷰컨트롤러인지 자기 자신인지 주의가 필요합니다!!
아래 프로젝트는 실험을 진행한 프로젝트입니다, 메모리 주소를 출력하도록 설정되어 있으니 흐름을 파악하는데 도움이됩니다.
RIBs를 사용하여 진행중인 프로젝트의 출시물입니다.
많이 부족하지만 점점 개선되는 모습을 보여드릴게요! 감사합니다.
Orbit(오르비 알람): 기상알람, 운세
◆ 기상 후 하루 운세 제공 Orbit은 단순한 알람을 넘어, 아침을 즐겁게 시작할 수 있도록 운세 콘텐츠를 제공합니다. 운세는 재미를 넘어, 하루를 더 의미 있게 계획할 수 있도록 도울 수 있어
apps.apple.com
실험을 하며 알아낸 점
- present시 presentingViewController프로퍼티 값은 viewWillAppear이후에 확인할 수 있다.
- presentedViewController의 경우 present함수 호출 즉시 확인 가능
- 네비게이션 컨트롤러를 present하는 경우 네비게이션 컨트롤러의 루트 뷰컨트롤러의 presentingViewController는 네비게이션 컨트롤러가 아닌 네비게이션 컨트롤러를 프레젠트한 상위 컨트롤러이다. 하지만, 상위 뷰컨트롤러의 presentedViewController는 네비게이션 컨트롤러를 가리킨다.
- 앞서언급했듯 네비게이션 컨트롤러 스택에 속한 뷰컨트롤러가 present호출 하면 해당 뷰컨트롤러의 presentingViewController는 네비게이션 컨트롤러가 된다. 네비게이션 컨트롤러는 네비게이션 컨트롤러 스택에 추가될 수 없음으로 네비게이션 컨트롤러가 네비게이션 스택에 존재하는 경우는 고려하지 않아도된다.
'트러블 슈팅(iOS)' 카테고리의 다른 글
[트러블 슈팅] AsyncSequence 순환참조 트랩 (0) | 2025.04.18 |
---|---|
[트러블슈팅] Tuist에서 Firebase사용시 발생할 수 있는 문제(with ABTest) (0) | 2025.03.15 |
[트러블슈팅] Actor를 활용한 동시성 관리 & GCD와 함께 사용시 주의할 점 (0) | 2025.02.26 |
[트러플 슈팅] viewDidLoad의 호출시점 (0) | 2024.07.29 |