Skip to main content
← 블로그

가이드 모드를 손본 하루 — 영구 sticky, 한 프레임의 검정, 그리고 질문 하나

VauDium·

섹션 바는 계속 붙어 있고, 연출은 넣었다가 뺐고, 키보드 뒤는 하얗고, 질문은 너무 어려웠습니다. 전부 오늘 이야기입니다.

가이드 모드를 손본 하루 — 영구 sticky, 한 프레임의 검정, 그리고 질문 하나

페칫에는 가이드 모드가 있습니다. 할 일 하나를 붙잡고 “무엇을 이루려는지, 지금 어디에 있는지, 무엇이 발목을 잡을지”를 한 단계씩 묻는 대화형 흐름이요. 어제 여기에 집중 스크림 — 아직 안 온 단계를 어둡게 덮는 연출 — 을 넣었는데, 오늘은 그 여파를 정리하는 날이 됐습니다. 버그 제보로 시작해서, 연출을 한 바퀴 돌아 뺐고, 마지막엔 질문 하나를 다시 썼습니다.

1. 계속 붙어 있는 섹션 바 — onLayout의 좌표는 부모 기준이다

첫 제보는 이거였습니다. “할 일 상세에서 Project 섹션 바가 계속 sticky로 붙어 있는데?”

모바일 할 일 상세는 스크롤에 따라 섹션 바가 상단에 붙는 2단 sticky 구조입니다. 각 섹션 바의 Y 좌표를 onLayout으로 재서 “스크롤이 이 좌표를 지나면 붙인다”를 계산하죠. 어제 스크림을 깔면서 섹션 바들을 <View>{스크림}<섹션바/></View>로 감쌌는데, 좌표를 재는 onLayout은 안쪽 섹션 바에 그대로 뒀습니다.

React Native의 onLayout이 주는 layout.y직계 부모 기준입니다. 래퍼가 끼어드는 순간 다섯 개 섹션의 좌표가 전부 0으로 측정됐습니다. “y가 0이면 미측정으로 보고 버린다”는 방어 필터가 있었는데, 하필 Project 섹션만 “얘는 원래 최상단이라 0이 정상”이라는 옛 특례로 살아남았습니다. 결과: 교체할 다음 섹션 스톱은 전부 사라지고, Project 바 혼자 스크롤 내내 붙어 있는 화면.

수리는 onLayout을 래퍼로 옮기는 것, 그리고 이제 유효하지 않은 특례를 지우는 것이었습니다. 특례를 지운 이유가 재밌는데 — 같은 회귀가 또 나면 “영구 sticky”라는 요란한 고장 대신 “sticky가 조용히 안 붙는” 안전한 고장이 되게 하려는 겁니다. 측정 중인 요소를 래퍼로 감쌀 때는 측정도 함께 이사해야 합니다. 타입 에러도 안 나고, 그날은 멀쩡해 보이는 종류의 실수라서요.

2. 완료의 타이밍 — 스크림은 가이드 바를 기다리지 않는다

가이드를 끝까지 완주하면 완료 메시지가 뜨고, 1.5초 뒤에 가이드 바가 물러납니다. 스크림도 그 1.5초를 같이 기다렸다가 사라졌는데, 써 보면 어색합니다. 다 끝냈는데 화면은 아직 어두워요.

그래서 순서를 바꿨습니다. 완료 메시지가 뜨는 순간 스크림이 걷히고, 가이드 바만 남아 인사를 하고 퇴장합니다. “완료 → 화면이 밝아진다”가 한 박자로 붙으니 완주의 감각이 살아납니다. 데스크톱은 이미 이 순서였어서, 모바일을 맞춘 쪽입니다.

3. 연출을 넣었다가 뺀 이야기 — 한 프레임의 검정

내친김에 욕심을 냈습니다. 들어갈 때 300ms에 걸쳐 방이 어두워지고, 완주하면 400ms에 걸쳐 커튼이 걷히는 입장/퇴장 페이드. 구현은 됐고, 타입 체크도 통과했고, 그리고 실기기에서 가이드를 닫을 때 한 프레임짜리 검정이 번쩍였습니다.

진단 과정이 오늘의 가장 긴 여정이었습니다. 재현 경로는 “타이틀 단계에서 닫기”인데, 모바일 코드에는 타이틀을 덮는 스크림이 아예 없습니다. 없는 것이 번쩍일 수는 없으니 한참을 헤맸는데 — 범인은 타이틀 바로 아래 붙은 블록의 스크림이었고, 메커니즘은 useNativeDriver였습니다. 네이티브 드라이버로 굴린 opacity는 뷰가 언마운트될 때 애니메이션 노드가 분리되면서 스타일이 JS 쪽 값으로 복원되는 순간이 생길 수 있습니다. 그 한 프레임에 검정 스크림이 낡은 opacity로 그려진 겁니다. JS 드라이버로 바꿔도 다른 게 어색해졌습니다.

여기서 방향을 정했습니다. 연출보다 깨끗하게. 페이드를 전부 걷어내고 즉시 등장·즉시 해제로 돌아갔습니다. 데스크톱에 남아 있던 0.25초 페이드아웃도 오히려 지웠고요. 연출은 잔상 하나로 적자가 나는 장사입니다. 어두워지는 게 즉각이면 그것대로 단호한 맛이 있습니다.

4. 데스크톱 대칭 — 타이틀은 늘 “지나온 단계”다

모바일을 정리하고 데스크톱을 보니 어긋난 곳이 셋 나왔습니다.

타이틀에 스크림이 생긴다. 가이드의 규칙은 “지나온 단계는 계속 밝게”입니다 — 지나온 답이 다음 질문의 재료니까요. 타이틀은 가이드의 1단계라 두 번째 단계부터는 항상 지나온 단계인데, 데스크톱만 타이틀 전용 오버레이가 남아서 어둡게 덮고 있었습니다. 오버레이를 지웠습니다.

마지막 단계로 가는 스크롤이 끝까지 안 올라간다. 마지막 단계는 화면 맨 위의 작전(Tactics) 칸입니다. 부드러운 스크롤로 올라가는 도중, 0.3초 뒤에 실행되는 focus()가 브라우저의 “포커스된 요소를 보이게 하라” 자동 스크롤을 일으켜 진행 중인 애니메이션을 끊고 있었습니다. focus({preventScroll: true}) 한 줄로 스크롤이 완주합니다.

작전 칸 위에 얇은 검은 띠가 남는다. 본문 콘텐츠의 상단 패딩 12px는 어느 블록의 소유도 아니라서, 활성 블록을 스크림 위로 들어올려도 그 띠만 어둡게 남았습니다. 맨 위 블록의 리프트 범위를 본문 꼭대기까지 늘려서 덮었습니다.

전부 “모바일과 데스크톱이 같은 개념이면 같은 문법이어야 한다”는 원칙에서 나온 수리입니다.

5. 키보드 뒤의 흰 것 — 34px 계산 실수

모바일에서 키보드가 올라올 때 그 뒤가 하얗게 비었습니다. 정체는 KeyboardAvoidingView가 iOS에서 padding 방식으로 만드는 키보드 높이만큼의 빈 공간 — 콘텐츠가 없으니 스크림도 없고, 페이지 바탕색이 그대로 비칩니다.

하단 스크림 띠를 키보드 높이만큼 늘리면 되는데, 1차 시도는 실패였습니다. 키보드 위에 앉은 서식 액세서리 바까지 어둡게 덮어버렸거든요. 원인은 insets.bottom + keyboardHeight라는 계산 — iOS의 keyboardWillShow가 주는 키보드 높이에는 홈 인디케이터 인셋이 이미 포함돼 있습니다. 인셋을 또 더하니 띠가 키보드 꼭대기를 34px 넘어 액세서리를 침범한 겁니다. 키보드가 열려 있을 땐 키보드 높이만 쓰는 걸로 고치니 딱 맞습니다. 원리의 한계가 아니라 이중 계산이었어요.

6. 질문 하나를 바꾸다 — 무기준 질문이 제일 어려운 질문이다

마지막은 코드가 아니라 문장이었습니다. 가이드의 단계 중 “현재” — 지금 어디에 있는지를 적는 칸 — 가 제일 어렵다는 이야기가 나왔습니다.

기존 질문은 “현재 상황은 어떤가요?“였습니다. 들여다볼수록 이게 문제였습니다. 기준이 없는 질문은 범위가 무한하고, 무엇이든 답이 될 수 있는 질문이 사실은 제일 답하기 어려운 질문입니다. 가이드는 직전에 쓴 “희망”을 화면에 밝게 남겨 두는데, 질문이 그걸 써먹지 않고 있었어요.

지시형도 검토했고(“지금 상태를 사실 그대로 적어 보세요”), 구체성을 당부하는 꼬리 문장도 붙여 봤지만, 다른 단계가 전부 질문형인데 하나만 지시형이면 결이 갈라지고, 꼬리는 군더더기였습니다. 최종은 한 문장입니다.

“희망을 기준으로 지금 어디쯤인가요?”

방금 쓴 자기 답이 자(尺)가 되는 거리 질문입니다. 목표와 현재를 나란히 놓는 것 — 이 대조가 계획을 실행으로 바꾸는 힘이라는 건 동기심리학 쪽에 근거가 쌓여 있는 이야기인데, 그 구조가 질문 한 문장에 담겼는지가 이렇게 차이를 만듭니다.

오늘의 교훈

하나, 측정하는 코드와 측정당하는 요소는 한 몸입니다. 래퍼 하나가 좌표계를 바꿉니다. 둘, 연출은 무결점일 때만 연출입니다. 한 프레임의 잔상은 400ms의 우아함을 전부 무릅니다. 셋, 좋은 질문은 기준을 품고 있습니다. 화면에 이미 있는 답을 가리키는 것만으로 백지의 공포가 줄어듭니다.

가이드 모드는 오늘로 꽤 단단해졌습니다. 다음은 이 흐름을 쓰는 사람들이 실제로 어디서 멈추는지를 보는 일입니다.