감정일기 앱을 만들면서 가장 큰 문제 중 하나는 텍스트뷰(TextView)가 키보드에 가려지는 문제였습니다.
특히 텍스트뷰가 화면 아래쪽에 있는 경우, 키보드가 올라오면 입력 위치가 전혀 보이지 않아서 UX가 크게 떨어집니다.
그래서 이번 글에서는 텍스트뷰를 탭하면 자동으로 중앙으로 올려주고, 키보드에 가리지 않게 해주는 방법을 정리했습니다.
이 글을 보면 왜 이렇게 구현해야 하는지, 그리고 다른 화면에서도 스스로 이 기능을 구현할 수 있을 정도로 원리부터 실전 코드까지 이해할 수 있을 거예요.
🧩 1. 왜 이런 스크롤 처리가 필요할까?
문제는 단순합니다.
❌ 문제 1 — 텍스트뷰가 화면 아래에 있으면 키보드가 올라오는 순간 가려져 버림
→ 입력하는 내용이 안 보이는 상태가 됨
❌ 문제 2 — 텍스트뷰가 보이더라도 “너무 아래”에 있어 편집하기 불편
→ 대부분의 앱은 텍스트 입력 시 화면 중앙 근처로 올려줌 (카톡, 메모 앱 등)
그래서 스크롤 로직의 목표는 두 가지입니다.
🎯 목표
- 키보드에 가리지 않도록 자동 스크롤
- 텍스트뷰가 화면 중앙 근처에서 편하게 입력되도록 위치 이동
🧠 2. 이 기능을 구현하기 위한 핵심 원리 3가지
이 기능은 사실 아래 세 가지만 이해하면 아주 쉽게 구현할 수 있습니다.
원리 1️⃣ — 키보드가 올라오면 화면에서 사용할 수 있는 높이가 줄어든다
visibleHeight = 화면 전체 높이 - 키보드 높이
여기서 visibleHeight는 텍스트뷰가 보일 수 있는 최대 높이입니다.
원리 2️⃣ — 텍스트뷰의 Y 위치가 visibleHeight 이하로 떨어지면 키보드에 가리게 된다
특히 centerY가 화면의 60% 아래에 있다면 거의 항상 키보드가 가립니다.
if textViewCenterY > visibleHeight * 0.6 {
스크롤 필요
}
0.6(60%)라는 숫자는 "조금 아래쪽에 있으면 불편하다"는 기준점입니다.
원리 3️⃣ — UICollectionView나 UIScrollView는 특정 영역을 보이게 만드는 API가 있다
바로 아래 메서드입니다.
scrollRectToVisible(_: animated:)
이 메서드를 이용하면 원하는 텍스트뷰 위치를 화면 중앙에 배치할 수 있습니다.
📐 3. 텍스트뷰를 ‘적당한 중앙 위치’로 스크롤시키는 공식
텍스트뷰를 화면 너무 위나 아래로 보내지 않고,
딱 입력하기 좋은 위치로 가져오기 위해 다음과 같이 계산합니다.
targetY = textViewTop - visibleHeight * 0.3
visibleHeight * 0.3 → 화면 상단 30% 위치
텍스트뷰를 이 정도 위치에 오게 하면 시각적으로 가장 편안함
🚀 4. 최종 구현 코드 (그대로 사용 가능)
// MARK: - 키보드가 나타날 때 텍스트뷰 올리기
@objc private func handleKeyboardShow(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let frame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
// 1) 키보드 높이 추출
keyboardHeight = frame.height
// 2) collectionView 바닥 여백 확보
diaryCollectionView.contentInset.bottom = keyboardHeight + 20
diaryCollectionView.verticalScrollIndicatorInsets.bottom = keyboardHeight
// 3) 포커스된 텍스트뷰의 frame (전역 좌표)
guard let textViewFrame = activeTextViewFrame else { return }
// 4) 화면에서 "남아 있는 높이"
let visibleHeight = view.bounds.height - keyboardHeight
// 5) 텍스트뷰 중앙이 화면 하단 40%에 있으면 → 키보드에 가림
let textViewCenterY = textViewFrame.midY
if textViewCenterY > visibleHeight * 0.6 {
// 6) 텍스트뷰를 화면 중앙 위로 올리기 위한 offset
let targetY = textViewFrame.minY - (visibleHeight * 0.3)
// 7) 부드럽게 스크롤 이동
let rect = CGRect(
x: 0,
y: targetY,
width: view.bounds.width,
height: textViewFrame.height + keyboardHeight
)
diaryCollectionView.scrollRectToVisible(rect, animated: true)
}
}
이 기능은 결국 다음 3줄로 요약됩니다.
① visibleHeight = 화면높이 - 키보드높이
② textViewCenterY > visibleHeight * 0.6 → 스크롤 필요
③ targetY = textViewTop - visibleHeight * 0.3
🧩 activeTextViewFrame이란 무엇인가?
✔ 결론
지금 포커스된 텍스트뷰의 “전역 좌표”를 저장해두는 변수
즉, 화면 기준으로 텍스트뷰가 정확히 어디에 위치하는지 저장하는 역할.
✔ 왜 필요할까?
키보드가 올라오면 우리는 “현재 어떤 텍스트뷰가 선택되어 있는지” 알아야 함.
그 정보가 없으면
어떤 텍스트뷰를 화면 중앙으로 올려야 할지 계산이 불가능함.
🔍 왜 전역 좌표(Global frame)가 필요할까?
텍스트뷰는 이렇게 깊은 구조 안에 있어:
DiaryEditorViewController
→ UICollectionView
→ DiaryContentCell
→ UIStackView
→ DiaryContentView
→ UITextView
따라서 UITextView의 frame은
자기 부모(view), 그 부모(view), …
즉 계층마다 좌표계가 다름.
그러니까 위치 계산을 하려면
현재 화면(ViewController 기준)에서 텍스트뷰가 어디에 있는지
전역 좌표 변환이 필요함.
이걸 하는 코드가 바로 이것:
view.convert(view.bounds, to: self.diaryCollectionView)
- view: DiaryContentView (텍스트뷰가 포함된 뷰)
- view.bounds: DiaryContentView의 내부 좌표
- convert(_:to:): “이 뷰가 collectionView 좌표 기준으로 보면 어디인가?”
👉 DiaryContentView가 collectionView 안에서 정확히 어디에 있는지 계산해줌
👉 이것을 activeTextViewFrame에 저장
이제 키보드가 올라오면 스크롤을 계산할 때:
텍스트뷰 topY (textViewFrame.minY)
텍스트뷰 midY (textViewFrame.midY)
텍스트뷰 height (textViewFrame.height)
이런 데이터를 차례대로 사용할 수 있음.
🎯 실제로 어떻게 쓰이냐?
키보드가 올라왔을 때:
guard let textViewFrame = activeTextViewFrame else { return }
→ 현재 텍스트뷰 위치 읽기
let textViewCenterY = textViewFrame.midY
→ 키보드에 가리는지 판단
let targetY = textViewFrame.minY - (visibleHeight * 0.3)
→ 화면 중앙에 보이도록 올릴 위치 계산
diaryCollectionView.scrollRectToVisible(rect, animated: true)
→ 스크롤 이동
즉, activeTextViewFrame 없이는 이 전체 로직이 불가능함.
🎯 alwaysBounceVertical이 무엇인가?
👉 세로 스크롤 방향에서 “컨텐츠 높이가 화면보다 작아도 스크롤이 가능하게 만드는 옵션”
즉,
원래는 스크롤할 내용이 없으면 스크롤이 안 됨
하지만 alwaysBounceVertical = true를 주면
컨텐츠가 작은 화면에서도 위·아래로 살짝 움직이는 “바운스(튕김)” 효과가 생김
🎨 왜 바운스를 주는가? (UX 관점)
- 사용자 입장에서 “움직일 수 있는 화면”처럼 느껴짐
- 키보드가 올라오는 상황에서 스크롤 영역이 넓어짐
- ScrollRectToVisible()가 제대로 동작할 수 있는 여유 공간 확보
- collectionView가 딱딱하게 고정된 느낌이 아니라 부드러움
특히 텍스트 입력 UI에서는 필수 같은 역할을 함.

'감정일기(가칭)' 카테고리의 다른 글
| 📘 DiaryContentCell 리팩토링 기록 — 단일 String 통합 방식에서 → 구조체 기반 다중 필드 관리로 개선하기 (0) | 2025.11.17 |
|---|---|
| 📌 iOS 파일 선택(UIDocumentPicker) 시 이미지가 안 보이던 문제 해결하기 (0) | 2025.11.15 |
| 커스텀 캘린더 만들기 구조 설계 (0) | 2025.11.10 |
| 커스텀 캘린더 ViewModel, 이렇게 만들면 됩니다 (CalendarViewModel 완전 해부) (0) | 2025.11.10 |
| 📐 Compositional Layout에서 interGroupSpacing vs interItemSpacing 완벽 정리 (0) | 2025.10.29 |