Skip to main content
← 블로그

가이드 모드 애니메이션 — 뿅 하고 나타나고, 뿅 하고 사라지고 싶었을 뿐인데

VauDium ·

GuideBar에 슬라이드 애니메이션을 넣으려다 SafeArea, 키보드, 마운트/언마운트와 싸운 이야기.

가이드 모드 애니메이션 — 뿅 하고 나타나고, 뿅 하고 사라지고 싶었을 뿐인데

Fecit에는 가이드 모드가 있습니다. 할 일을 만들 때 “현재 → 기대 → 난관 → 전략” 순서로 한 단계씩 안내해주는 기능입니다. 상단 탑바 자리에 파란 GuideBar가 나타나고, 단계를 밟고, 끝나면 사라집니다.

문제는 이 GuideBar가 그냥 나타나고 사라진다는 것이었습니다.

“위에서 슬라이딩으로 내려오게 하면 예쁘겠다.”

간단한 요구사항이었습니다. 결과적으로 간단하지 않았습니다.

1단계: 나타나는 건 쉬웠다

const slideAnim = useRef(new Animated.Value(-50)).current;
useEffect(() => {
    Animated.spring(slideAnim, {
        toValue: 0, useNativeDriver: true, tension: 80, friction: 12
    }).start();
}, []);

Animated.spring으로 위에서 톡 튀어나오는 애니메이션. 잘 됐습니다. 여기까지는 5분이었습니다.

2단계: 사라지는 건 지옥이었다

문제 1 — 컴포넌트가 먼저 죽는다

GuideBar는 guideMode ? <GuideBar /> : <RawTopBar /> 구조였습니다. X 버튼을 누르면 setGuideMode(false) → GuideBar 언마운트. 애니메이션? 실행할 시간이 없습니다.

“애니메이션 끝나고 나서 onClose 호출하면 되지 않나?”

const handleClose = () => {
    Animated.timing(slideAnim, {
        toValue: -50, duration: 200, useNativeDriver: true
    }).start(() => onClose());
};

이론적으로 맞습니다. 하지만…

문제 2 — 한 프레임의 깜빡임

애니메이션이 끝나고 onClose()setGuideMode(false) → GuideBar 언마운트 + SafeArea topColor 변경. 이게 같은 프레임에 일어나면서 화면이 한 순간 깜빡였습니다.

requestAnimationFrame으로 한 프레임 미뤄봤지만, 여전히 보였습니다.

문제 3 — SafeArea가 남는다

GuideBar에 SafeArea 영역을 포함시켜서 파란색이 맨 위까지 올라가게 했습니다. 그런데 슬라이드 아웃할 때 GuideBar는 올라가는데, SafeArea의 topColor는 별도 View라서 제자리에 남아있었습니다. 파란 바가 올라가도 위에 파란 잔상이.

문제 4 — -50이 부족하다

translateY: -50으로 올렸더니, SafeArea 높이(약 59px)보다 작아서 GuideBar가 완전히 사라지지 않고 SafeArea 영역에서 멈춰버렸습니다.

const slideOutDistance = -(insets.top + 50);

SafeArea 높이 + GuideBar 높이만큼 올려야 했습니다.

문제 5 — Spring이 바운스한다

Animated.spring으로 사라지게 했더니 올라가다가 살짝 내려왔다가 다시 올라갔습니다. overshootClamping: true를 줘봤지만 여전히 미세하게 멈추는 느낌이었습니다. 사라지는 데는 Animated.timing이 맞았습니다.

해결: 구조를 바꾸다

결국 근본적인 구조를 바꿨습니다.

Before: guideMode ? <GuideBar> : <RawTopBar>

GuideBar와 RawTopBar가 서로 교체되는 구조. GuideBar가 사라지면 RawTopBar가 나타나는데, 이 전환이 한 프레임에 일어나서 어색했습니다.

After: RawTopBar는 항상 렌더링, GuideBar는 absolute로 위에 겹침

{guideMode && (
    <View style={{position: "absolute", top: 0, left: 0, right: 0, zIndex: 10}}>
        <GuideBar ... />
    </View>
)}
<RawTopBar ... />

이렇게 하면:

  • GuideBar가 슬라이드 아웃할 때, 뒤에 RawTopBar가 이미 있음
  • SafeArea 색 전환 문제 없음 (GuideBar가 자체적으로 SafeArea padding을 포함)
  • 언마운트 타이밍 문제 없음 (애니메이션 끝난 후 onClosesetGuideMode(false))

보너스: 키보드 문제

가이드 모드에는 키보드 닫힘을 감지해서 다음 단계로 자동 이동하는 기능이 있습니다.

Keyboard.addListener("keyboardDidHide", () => {
    // 다음 단계로 이동
});

Retrospect 모드에서 step 0은 만족도 피커(키보드가 아닌 모달)입니다. 그런데 키보드가 열린 상태에서 가이드를 시작하면, 키보드가 닫히면서 keyboardDidHide가 발동 → 만족도를 건너뛰고 바로 회고로.

해결은 간단했습니다:

const currentField = steps[guideStepRef.current]?.field;
if (currentField === "satisfaction") return;

만족도 단계에서는 키보드 닫힘을 무시.

교훈

  1. 나타나는 애니메이션은 쉽고, 사라지는 애니메이션은 어렵다. 마운트는 컴포넌트가 있으니까 애니메이션할 수 있지만, 언마운트는 컴포넌트가 사라져야 하는데 애니메이션이 아직 안 끝났습니다.

  2. 삼항 교체(A ? X : Y)보다 겹침(absolute + 항상 렌더링)이 전환에 유리하다. 두 컴포넌트가 동시에 존재할 수 있어야 자연스러운 전환이 가능합니다.

  3. SafeArea는 생각보다 까다롭다. 컴포넌트가 SafeArea 영역까지 차지해야 한다면, 그 영역을 컴포넌트 자체에 포함시키는 게 제어하기 쉽습니다.

  4. spring은 나타날 때, timing은 사라질 때. 나타날 때는 톡 튀는 느낌이 좋지만, 사라질 때는 바운스 없이 깔끔하게.

결국 “뿅 하고 나타나고, 뿅 하고 사라지기”까지 약 한 시간이 걸렸습니다. 코드는 10줄 안팎이지만, 그 10줄을 찾기까지의 여정이 길었습니다.