navigationItem.title과 관련하여 신기한 점을 발견하였습니다.
// 1번
navigationItem.title = "안녕하세요"
// 2번
navigationItem.title = "\(getNicknameFromUserDefaults() ?? "대장")님의 다마고치"
// 3번
let title = "\(getNicknameFromUserDefaults() ?? "대장")님의 다마고치"
navigationItem.title = title
1번과 같이 문자열 보간법이 없이 정의하면 해당 title이 backBarButtonItem이 나타나는데, 2,3번과 같이 문자열 보간법으로 title을 지정하면 ‘Back’버튼이 나오는 현상을 발견하였습니다.
navigationItem.backBarButtonItem = UIBarButtonItem(title: "설정", style: .plain, target: nil, action: nil)와 같이 설정을 지정하면 해당 문구로 변경되지만, 왜 이런 현상이 나타나는지 궁금하여 디버깅해보았습니다.
문자열 보간법과 문자열 타입 확인
문자열 보간법으로 정의된 문자열과 정의되지 않은 문자열을 확인했을 때, 둘 다 옵셔널 타입으로 나타났습니다. viewWillAppear()에 각 함수를 출력한 결과는 다음과 같이 동일하였습니다.
print(#function, navigationItem.title, navigationItem.backBarButtonItem?.title)
print(type(of: navigationItem.backBarButtonItem?.title), type(of: navigationItem.title))
// 1번
viewWillAppear(_:) Optional("안녕하세요") nil
Optional<String> Optional<String>
// 2, 3번
viewWillAppear(_:) Optional("대장님의 다마고치") nil
Optional<String> Optional<String>
우선 여기서 신기한 점은 모든 형식의 타입이 Optional<String>이라는 것입니다. 문자열 보간법은 그렇다 쳐도 명시적으로 정의한 것 마저 옵셔널 형태로 출력되었습니다. 그리고 navigationItem.backBarButtonItem?.title 또한 둘 다 nil을 반환하고 있습니다.
Print Description of ~ 확인
그 다음으론 [View Hierarchy] → 해당 컴포넌트의 ‘Print Description of ~’를 통해 확인하였습니다.
// 1번
Printing description of $19:
<UIButtonLabel: 0x105319cd0; frame = (0 1; 73.6667 20.3333); text = '안녕하세요'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600002903b80>>
// 2, 3번
Printing description of $19:
<_UIModernBarButton: 0x103d18900; frame = (30.3333 10.6667; 37.6667 21.3333); opaque = NO; userInteractionEnabled = NO; gestureRecognizers = <NSArray: 0x6000002380c0>; layer = <CALayer: 0x600000247f00>>
여기서 확인을 해보면 1번은 UIButtonLabel로 정의가 되어있고, 2,3번은 UIModernBarButton으로 정의가 되어있습니다. UIButtonLabel의 layer는 _UILabelLayer 이고, UIModernBarButton의 layer는 CALayer 인 것으로 보아, 각 버튼이 그려지는 계층이 다름을 알 수 있습니다. (1번은 UIKit 계층에서 관리하고, 2번은 Core Animation 계층에서 관리할 것입니다.)
디버깅
그렇다면 일반 문자열과 문자열 보간법에서 차이점이 생겨서 객체를 그리는 계층(layer)이 다를 수 있다는 것을 유추할 수 있기 때문에 문자열과 문자열 보간법에 대해 디버깅을 해보았습니다.
각 객체들에 Break point를 걸어두고 해당 객체들을 확인해보았습니다. 여기서 알 수 있었던 사실은 문자열 보간법은 SwiftNativeNSString객체로 관리되고 있음을 알 수 있었습니다.
또한 다음 명령어를 통해 해당 각 문자열의 메모리 주소를 확인하고, 이를 po 명령어를 통해 확인해보았습니다.
print(Unmanaged.passUnretained((변수명) as AnyObject).toOpaque())
여기서 신기했던 점은, po 명령어를 사용했을 때, 문자열 보간법으로 저장한 값은 확인할 수 있었지만, 문자열은 확인할 수 없었습니다.
문자열 데이터는 정적 메모리에 저장되며, 런타임 최적화를 위해 Tagged Pointer 방식으로 관리됩니다. Stack 영역은 지역 변수, 매개 변수, 리턴값, 구조체 등의 값타입이 저장되는 영역이기 때문에 문자열 데이터는 Stack 영역에 크기로 저장되지 않았을까 생각하였습니다.
NSTaggedPointerString
NSTaggedPointerString은 메모리 주소에 문자열 데이터를 직접 인코딩하는 최적화 방식으로, 주소 자체가 데이터를 포함하거나 메타데이터를 포함합니다. 또한 정적 문자열이기 때문에 별도의 힙 메모리를 할당하지 않습니다. 참조 방식이 다르기 때문에 po 명령어를 통해 메모리를 참조하여 데이터를 출력하려고 하면 EXC_BAD_ACCESS오류가 발생합니다.
그 다음은 문자열 보간법이 저장된 메모리 주소인 0x600001757c40에 대한 범위를 찾아보았습니다. 터미널에서 다음 명령어를 통해 확인하였습니다. MALLOC은 힙 영역입니다.
vmmap (PID) | grep MALLOC
그 결과 MALLOC_NANO 범위에 포함됨을 알 수 있었고, 이를 통해 문자열 보간법은 Heap 영역에 저장됨을 유추할 수 있었습니다.
결론
여기서 ‘그럼 왜 Back 버튼으로 나타나는가’에 대해 답변을 하기 위해선 사실 UIKit의 내부 코드를 확인해보아야 알 수 있습니다. (NavigationItem 등이 어떻게 개발되어있는지를 확인할 수 있어야 하기 때문입니다.)
하지만 이를 통해 알 수 있었던 점은 다음과 같습니다.
- 일반 문자열과 문자열 보간법은 출력 결과는 같지만, 다른 방식으로 동작하고 있다.
- 일반 문자열은 UIButtonLabel로, 문자열 보간법은 UIModernBarButton로 버튼을 정의합니다.
- 문자열 보간법은 힙 영역에 저장되는 것으로 유추해볼 수 있습니다.
'iOS > UIKit' 카테고리의 다른 글
[UIKit] PinLayout이란 (2) | 2025.07.30 |
---|---|
[UIKit] 메모리 관점에서의 CollectionView, PageView, ScrollView의 동작 방식 (2) | 2025.01.29 |
[iOS-UIKit] Final 키워드를 사용하는 이유 (최적화의 관점) (0) | 2025.01.19 |
[UIKit-Storyboard] 스토리보드 컴파일 과정 (0) | 2024.12.31 |
[UIKit-Code] UIKit 코드 기반 프로젝트 세팅하기 (스토리보드 없애기) (0) | 2023.12.27 |