[개발일지] 03. 신기하고 재밌는 탭탭 온보딩! 어떻게 구현했을까요?

2026. 1. 14. 01:05·탭탭 - TapTap/개발일지
728x90

탭탭 온보딩

탭탭은 사파리 익스텐션을 기반으로 동작하는 서비스입니다. 그래서 사용자가 처음 앱을 접했을 때, 사파리 확장 프로그램 설정 방법이나 문장을 하이라이트하는 방식처럼 다소 낯설 수 있는 기능들을 온보딩에서 가이딩을 통해 안내하고 있습니다.

 

특히 하이라이트 온보딩은 단순한 설정 화면이 아니라 실제 사용 흐름을 따라가며 보여주는 인터랙션 중심으로 구성되어 있습니다. 두 번 탭 → 드래그 → 색상 선택 → 메모 추가까지, 사용자가 자연스럽게 기능을 이해할 수 있도록 꽤 많은 애니메이션과 뷰 이동이 포함돼 있습니다.

 

이번 글에서는 이처럼 상태가 복잡하게 얽혀 있고, 특정 영역만 움직여야하는 탭탭의 온보딩 화면을 SwiftUI로 어떻게 구현했는지, 그리고 그 과정에서 겪었던 이슈들과 해결 방법들을 정리해보려고 합니다.

Lottie? 직접 구현?

온보딩 기획을 마친 뒤 디자인 팀과 이야기를 나눴을 때, 처음에는 SwiftUI로 모든 화면과 애니메이션을 일일이 구현하기보다는 Lottie를 활용하는 편이 더 효율적이지 않을까라는 의견도 있었습니다.

 

하지만 실제로 구현 범위를 놓고 검토해보니, 몇 가지 명확한 문제점들이 있었습니다.

 

첫 번째로, 온보딩 전체 플로우를 Lottie 하나로 처리하기에는 구조가 너무 복잡했습니다. 탭탭 온보딩은 단순히 재생되는 애니메이션이 아니라, 사용자의 탭, 더블 탭과 같은 제스처에 따라 특정 단계에서 멈추거나 다음 단계로 넘어가는 인터랙션 중심의 흐름이었기 때문입니다. 이번 분기 구조를 Lottie로 관리하는 데에는 한계가 있을 것이라 판단되었습니다.

 

두 번째는 기기 화면 사이즈에 대한 대응 문제였습니다. 하이라이트 영역, 툴팁 위치, 손가락 가이드 이미지 등은 텍스트 레이아웃과 연결되어 있는데, 이를 고정된 비율의 Lottie 애니메이션으로 처리할 경우 기기마다 레이아웃이 깨질 수 있다고 판단되었습니다.

 

마지막으로, 만약 온보딩 전체 플로우를 Lottie로 구현한다면 차라리 동영상 형태로 재생하는 것과 크게 다르지 않다는 결론에 도달했습니다. 애초에 Lottie는 탭탭 온보딩처럼 흐름이 길고 단계가 많은 애니메이션보다는, 탭에 대한 피드백이나 짧은 강조 효과처럼 단일 인터랙션을 보조하는 경량 애니메이션에 더 적합한 도구라도 판단했습니다.

 

이런 이유로, 탭탭 온보딩은 SwiftUI의 상태 관리와 애니메이션을 기반으로 직접 구현하는 방향을 선택하게 되었고, 간단한 탭 피드백이나 포인트 인터랙션과 같은 부분은 Lottie로 처리하기로 하였습니다.

플로우 설계

탭탭 온보딩 애니메이션은 After Delay와 Gesture의 조합으로 구성되어있습니다. 즉, 시간 기반 흐름과 사용자 인터랙션이 함께 구성되어있습니다. 단순히 하나의 애니메이션이 끝나면 다음이 실행되는 구조가 아니라, 지연된 애니메이션과 제스처 이벤트가 서로 얽혀 있는 형태였습니다. 

 

그래서 바로 구현에 들어가기보다는, 먼저 전체 애니메이션 플로우를 정리하고 도식화하는 작업부터 진행했습니다. 어떤 시점에 어떤 상태가 바뀌고, 그 상태 변화가 어떤 뷰의 등장과 이동, 사라짐으로 이어지는지를  한 눈에 볼 수 있도록 정리하는 게 필요했습니다.

 

저희 디자이너 분의 작업물이라 모자이크 처리하였습니다!

디자이너에게 프로토타입 형태로 전달받았고, 각 단계마다 어떤 인터랙션이 발생하는지에 대한 정보가 함께 포함되어 있었습니다. 프로토타입에 담긴 인터랙션과 전환 타이밍을 하나씩 정리해 애니메이션 플로우를 도식화하였습니다.

 

머메이드 너무 길어여..

플로우차트를 그릴 때 온보딩 전체 흐름을 한눈에 파악할 수 있는지를 가장 중요하게 생각했습니다. 특히 각 애니메이션이 몇 초 동안 실행되는지, 그리고 그 애니메이션이 끝난 뒤 몇 초 후 어떤 이벤트가 발생하는지처럼 시간 흐름에 따른 변화를 직관적으로 이해할 수 있도록 구성하려고 했습니다. 사용자의 탭이나 더블 탭 같은 인터랙션에 따른 분기 처리와, After Delay로 이어지는 시간 흐름에 따른 애니메이션 전개를 함께 표현하는데 집중했습니다.

 

View 구조 설계

애니메이션 흐름을 도식화하는 작업을 마무리한 뒤, View 설계 단계에 들어섰습니다. 탭탭 온보딩의 특징은 화면 전환이 반복되는 구조가 아니라, 하나의 화면을 베이스로 그 위에서 애니메이션 이벤트가 발생하고, 뷰가 이동하거나 새로운 요소가 추가 및 제거되는 방식으로 구성되어 있다는 점이었습니다.

 

즉, 온보딩 전체가 여러 화면의 집합이 아니라 하나의 베이스 뷰 위에서 상태에 따라 변화하는 구조였기 때문에, 가장 먼저 이 베이스가 되는 뷰의 레이아웃과 역할을 어떻게 가져갈지부터 설계했습니다.

 

베이스 뷰의 구조를 살펴보면, 실제로 이벤트가 발생하는 영역은 가운데에 위치한 텍스트 요소에 한정되어 있었고, 그 외의 영역은 사용자의 시선을 분산시키지 않도록 Dim 처리되어야 하는 구조였습니다. 그래서 View 설계 단계에서부터 이 두 역할을 명확히 분리했습니다. 하나는 실제 인터랙션과 애니메이션 이벤트를 처리하는 메인 영역, 다른 하나는 Dim 영역입니다. 

 

이렇게 View 구조를 나눈 이유는 비교적 단순합니다. 첫 번째 문단과 두 번째 문단을 감싸는 영역에 .overlay 모디파이어를 사용해 Dim 처리를 한 번에 적용하기 위함이었습니다. 좌표를 직접 계산해서 offset이나 GeometryReader로 Dim 영역을 제어하는 방식도 고려해보았지만, 이 경우 기기 화면 크기나 텍스트 레이아웃이 달라질 때 레이아웃이 깨질 가능성이 높아집니다. 

 

FirstParnassusView()
  .overlay {
    Rectangle()
      .frame(maxWidth: .infinity, maxHeight: .infinity)
      .background(.dim)
      .opacity(showOverlay ? 0.4 : 0)
      .animation(.easeOut(duration: 0.3), value: showOverlay)
    }

반면, 실제 뷰 요소를 기준으로 Dim 레이어를 올리는 방식은 레이아웃의 변화에 자연스럽게 대응할 수 있고, 기기 크기가 달라져도 별도의 좌표 보정이 필요 없다는 장점이 있습니다. 

이벤트 구현 방식

이벤트 구현 방식에 대해서는 고민이 많았습니다. 탭탭 온보딩에서 대부분 이벤트는 시간 차이를 두고, ease in/out 애니메이션으로 나타나고 사라지는 구조였기 때문에, 시간 흐름을 어떻게 제어할지가 핵심이었습니다. 개발 초기에는 타이머와 withAnimation + Delay 방식을 고려해보았지만 아래와 같은 이유로 asyncAfter를 사용해서 구현하였습니다.

 

1. 타이머

private var timer: Timer?
private var step = 0

private func highlightAnimation() {
  step = 0
  timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] timer in
    guard let self = self else { return }
    
    switch step {
    case 0:
      showSecondHighlightTip = false
      showOneFinger = true
      step = 1
      timer.invalidate()
      timer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { _ in
        // ...
      }
    case 1:
      // ...
    }
  }
}

타이머 관리가 복잡하고, 코드가 복잡하다는 점. 가장 큰 문제는 메모리 누수 위험이 있다고 판단되었습니다.

 

2. withAnimation + delay

withAnimation(.easeInOut.delay(2.0)) {
  showSecondHighlightTip = false
}

withAnimation(.easeInOut.delay(3.5)) {
  showOneFinger = true
}

복잡한 로직이 불가능하고, 순차 제어가 어렵다고 판단되었습니다.

 

3. asyncAfter

private func highlightAnimation() {
    showSecondHighlightTip = true
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
      showSecondHighlightTip = false
      showOneFinger = true
      
      DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
        withAnimation(.easeInOut(duration: 0.8)) {
          highlightProgress = 0.0
        }
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
          fingerImageName = "OneFinger"
          
          DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
            showOneFinger = false
            showThirdHighlightTip = true
            
            DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
              showThirdHighlightTip = false
            }
          }
        }
      }
    }
  }

개발 초기에는 위와 같은 이유로 asyncAfter를 중첩해서 이벤트를 구현했습니다. 하지만 이 방식에도 몇 가지 문제가 있었습니다. 

 

첫 번째 문제는 중첩이 너무 깊어져 콜백 지옥 형태가 되었고, 나중에 이벤트를 수정하거나 추가/삭제할 때 흐름을 추적하기 어렵다는 점이었습니다. 

 

두 번째 문제는 메모리와 뷰 lifecylcle과 관련이 있습니다. asyncAfter는 단순히 클로저를 큐에 등록할 뿐이며, 클로저에서 [self]를 강하게 잡지 않으면 Swift ARC는 정상적으로 동작합니다. 또한 SwiftUI에서 @State나 @Published 같이 값을 변경하는 클로저는 강한 참조 사이클을 만들지 않기 때문에, 지금 코드 자체가 바로 메모리 누수를 일으키지는 않습니다.

 

하지만 뷰가 사라진 이후에도 예약된 클로저가 실행되는 예외적인 경우가 발생할 수 있어, 불필요한 상태 변경이나 성능 저하가 생길 가능성이 있다고 판단되었고, 현재는 아래와 같이 Task 기반으로 구현하였습니다.

@State private var animationTask: Task<Void, Never>?

private func highlightAnimation() {
    animationTask?.cancel()
    
    animationTask = Task {
        showSecondHighlightTip = true
        try? await Task.sleep(nanoseconds: 2_000_000_000)
        
        showSecondHighlightTip = false
        showOneFinger = true
        try? await Task.sleep(nanoseconds: 1_500_000_000) 
        
        withAnimation(.easeInOut(duration: 0.8)) {
            highlightProgress = 0.0
        }
        try? await Task.sleep(nanoseconds: 500_000_000) 
        
        fingerImageName = "OneFinger"
        try? await Task.sleep(nanoseconds: 2_000_000_000) 
        
        showOneFinger = false
        showThirdHighlightTip = true
        try? await Task.sleep(nanoseconds: 2_000_000_000) 
        
        showThirdHighlightTip = false
    }
}

 

뷰가 이동하는 애니메이션

탭탭 온보딩 이벤트 중에는 드래그 애니메이션을 표현하기 위해 하이라이팅과 손가락 이미지가 이동하거나, 메모 박스를 보여주기 위해 기존 뷰가 이동하는 등 UI 요소가 움직이는 애니메이션이 존재합니다. 구현 방식 자체는 간단합니다. offset 모디파이어를 활용하면 위치 이동을 쉽게 표현할 수 있고, 애니메이션을 추가하면 자연스럽게 요소가 이동하는 효과를 줄 수 있습니다.

if showDrag {
  Color.highlightDrag.opacity(0.25)
  .frame(width: geometry.size.width * highlightProgress)
  .frame(height: geometry.size.height + 6)
  .offset(x: geometry.size.width * (1 - highlightProgress))
}

위는 실제 탭탭 온보딩 코드입니다. 이 애니메이션의 핵심은 highlightProgress라는 값입니다. 이 값은 1.0에서 시작해서 0.0으로 변하는데, 이 변화에 따라 하이라이트 영역의 너비와 위치가 동시에 조정됩니다.

마치며

 

이번 글에서는 탭탭 온보딩 화면의 복잡한 애니메이션과 이벤트 흐름을 어떻게 설계하고 구현했는지 살펴보았습니다. 온보딩에서의 이벤트는 간단한 애니메이션이지만 생각보다 고려해야할 점이 많았습니다. 뷰 상태 관리, 이벤트 흐름, 사용자 인터랙션 등 작은 요소가 UX에 영향을 주기 때문에, 설계 단계에서부터 꼼꼼히 계획하는 것이 중요하다는 것을 느꼈던 경험이었습니다. 

'탭탭 - TapTap > 개발일지' 카테고리의 다른 글

[개발일지] 02. 쉐어 익스텐션에서 탭탭 익스텐션 데이터를 추출하는 방법  (0) 2026.01.06
[개발일지] 01. TapTap Extension이 NativeApp과 통신하는 방법  (0) 2026.01.06
'탭탭 - TapTap/개발일지' 카테고리의 다른 글
  • [개발일지] 02. 쉐어 익스텐션에서 탭탭 익스텐션 데이터를 추출하는 방법
  • [개발일지] 01. TapTap Extension이 NativeApp과 통신하는 방법
여성일
여성일
  • 여성일
    성일노트
    여성일
  • 전체
    오늘
    어제
    • 분류 전체보기 N
      • 탭탭 - TapTap
        • 리팩토링
        • 트러블슈팅
        • 개발일지
      • 애플 디벨로퍼 아카데미
        • 챌린지 회고
        • 하루의 날씨
      • Swift Student Challenge 202..
      • AI를 잘쓰는 개발자가 될래요
      • 우리 같이 협업하자
      • ToyProject - 사카마카 (살까말까 고민 ..
      • ToyProject - Book2OnNon (모바..
      • ToyProject - 바꿔조 (환율 계산기)
      • iOS N
        • iOS
        • Vapor
        • Design Pattern
        • CoreData
        • Tuist
        • RxSwift
        • ReactorKit
        • TCA N
      • Swift
        • Swift 기본기
        • UIkit
        • SwiftUI
      • 원티드 프리온보딩 챌린지 iOS 과정
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.6
    여성일
    [개발일지] 03. 신기하고 재밌는 탭탭 온보딩! 어떻게 구현했을까요?
    상단으로

    티스토리툴바