[TipKit] iOS에서 숨겨진 기능을 알려주는 툴팁 (ToolTip) 만드는 방법

2025. 2. 18. 12:08Framework, Library

TipKit? 그게 왜 필요한 건데?

이번 글은 2023년, Apple이 WWDC에서 새롭게 발표했던 프레임워크 TipKit를 소개해보도록 하겠습니다.

TipKit은 Tip의 내용과 Tip이 나타나는 조건을 정의할 수 있는 애플의 프레임워크입니다.
Tip이라고 말하면 조금 생소할 수도 있는데요. UI UX 용어로는 툴팁 (Tool-Tip)이라고 많이 사용되는 컴포넌트가 애플에서는 Tip이라고 부릅니다.
말풍선 같은 모양으로 생겨 어떤 정보를 표출하는 화면을 생각하시면 될 겁니다!

출처 : https://developer.apple.com/documentation/tipkit

Tip은 말 그대로 팁, 즉 도움말입니다.
주로 사용자에게 새롭게 업데이트된 기능을 교육해 주거나, 숨겨진 기능을 발견할 수 있도록 도움을 주는 목적으로 사용되는 컴포넌트이죠.

<토스터 TOASTER> 앱에서도 후자의 목적으로 툴팁 기능을 추가했는데요.
앱 릴리즈 초기에는 아래 웹 화면 하단에 있는 툴바에서 가운데에 위치한 버튼이 <해당 링크의 열람 여부를 체크>할 수 있는 버튼이었지만,
처음 앱에 접속한 사용자는 버튼의 아이콘만으로 해당 기능의 존재를 모른다는 문제점이 있었습니다.
이런 경우, 앱 최초 접속자에게 "열람 버튼을 클릭해보세요!"라는 메시지의 툴팁을 표출하도록 하여, 숨겨진 기능을 발견할 수 있도록 도와주게 됩니다.
*하단에 있는 해당 레이아웃은 TipKit를 사용하지 않고, 직접 커스텀 Tool-Tip을 만들어 사용했습니다.

좌측의 링크 열람 완료 버튼의 존재를 사용자에게 알려주기 위해, 우측의 툴팁 기능을 추가하게 되었습니다.


위의 상황에서 툴팁을 만들기 위해 애플의 TipKit 프레임워크를 사용하지는 않았지만,
만약 Tip을 사용했다고 한다면, Tip의 내용은 "열람 버튼의 존재를 알려주는 것"이 될 것이고, Tip이 나타나는 조건은 "앱의 최초 접속자인 경우"가 되겠네요.

이와 같이 Tip은 사용자의 기능 교육 목적이나, 숨겨진 기능의 발견 목적으로 효과적인 기능이 됩니다.
단, 당연히 아무렇게나 만들어서 사용할 수는 없고 애플의 HIG (Human Interface Guidelines) 원칙에 따라 적합하게 사용해야겠죠?

해당 내용은 HIG의 Offering Help에 자세하게 나와있지만, 저는 중요한 원칙들만 몇 개 정리해 보도록 하겠습니다!

  • 팁을 디자인할 때, 사용자를 압도하지 않도록 주의하세요. (ensure you don’t overwhelm your users)
  • 불필요한 팁은 사용하지 마세요. 앱에 접속할 때마다 표출되거나, 산만하게 표출되는 팁은 지양. (Tips can become distracting when they appear unnecessarily.)
  • 교육적 내용이 아닌 홍보성 (promotional), 에러 메시지 (Error message)나 사용자가 취할 행동이 없는 (Not actionable) 팁은 적합하지 않습니다.

핵심은 단순하고, 명확하고, 사용자가 취할 수 있는 액션에 대해 가이드하는 역할이어야 한다는 것!

 

 

Tip 만들기 기초

자 이제 위에서 배운 개념을 바탕으로 Tip을 직접 만들어보겠습니다.

아래 화면에서 보이는 것과 같이 "questionmark.circle" 버튼이 있는 곳에서 Tip이 표출되도록 만들 것이고,
Tip에는 굵은 글씨의 타이틀과, Tip에 대한 설명글, 그리고 왼쪽의 이미지와 하단 Learn More로 쓰여진 버튼까지 포함시키고자 합니다.

기초로 만들어볼 Tip 화면


TipView는 Tip이라는 프로토콜을 채택해서 구현하게 됩니다.
필수로 구현해야 하는 title을 제외하고는, 선택적으로 정의해주면 되는데요. 어떤 요소들이 있는지 살펴보겠습니다.

  • title : Tip의 제목. -> Text 객체로 반환되며, 필수로 구현합니다.
  • message : title 아래에 표시되는 Text. -> title과 다르게, 옵셔널 객체로 반환되기 때문에 필요하지 않으면 생략할 수 있습니다.
  • image : Tip 왼쪽에 보이는 Image 객체. -> message와 마찬가지로 선택사항입니다.
  • actions : Tip에서 실행할 수 있는 버튼 동작의 Array. -> 선택적으로 버튼의 제목과 클릭 시 실행할 클로저를 정의할 수 있습니다.
import SwiftUI
import TipKit

struct ExampleTip: Tip {
    var title: Text {
        Text("Need Help?")
    }
    
    var message: Text? {
        Text("Tap here to learn more about how to use this feature.")
    }
    
    var image: Image? {
        Image(systemName: "questionmark.circle")
    }
    
    var actions: [Action] {
        [
            Tip.Action(title: "Learn More", perform: {
                print("Learn More button tapped")
            })
        ]
    }
}


이렇게 만들어진 Tip을 사용하기 위해서는 앱의 초기화 시점 혹은 특정 시점에 Tips.configure()를 호출해야 합니다.

Tips.configure는 TipKit의 데이터 컨테이너를 초기화하고,
팁의 상태 - 표시 빈도 (.displayFrequency), 데이터 저장소 위치 (.datastoreLocation), 테스트 옵션 등을 저장 및 관리하는 메서드입니다.
지금은 그냥 '아 팁을 보여지게 하기 위해서는 초반에 이렇게 설정해야 하는구나' 정도만 알고 넘어가 볼게요!

import SwiftUI
import TipKit

@main
struct ExampleApp: App {
    
    init() {
        try? Tips.configure()
    }
    ...

 

 

Tip을 표시하는 두 가지 방법 : inline, popover

Tip을 표시할 수 있는 두 가지 방법은 인라인 (inline)과 팝오버 (popover) 방식이 있습니다.

✔️ 중요한 정보를 가리지 않으려면 인라인 팁을 사용하세요. (Display an inline tip to avoid covering important information.)
✔️ 사용자 흐름을 방해하고 싶지 않을 때는 팝오버 팁을 사용하세요. (Display a popover tip when you don’t want to interrupt your content flow.)

왼쪽은 인라인 (inline) 방식, 오른쪽은 팝오버 (popover) 방식입니다.


위의 화면만 봐도 차이점이 명확하게 보이시죠!

인라인 (inline) 팁은 기존 View에서 한 자리를 당당하게(?) 한 자리를 하고 있는 방식이고, 
팝오버 (popover) 팁은 앱 UI 위에 화면을 가리면서 표시되는 방식인 것을 확인할 수 있습니다.

특히, 팝오버 방식을 사용하는 경우에는
HIG에서 이미 강조 표시되는 기능을 가리키기 때문에 별도의 이미지를 사용하지 않는 방식을 고려하라고 설명합니다.
(If you use a popover, consider excluding images because the tip already points to the highlighted feature.)


코드를 살펴보겠습니다.

인라인 팁은 기존 SwiftUI의 일반적인 View를 사용하는 방식과 동일하다고 생각하시면 됩니다.
크기나 배경색, 모서리 둥글기 같은 속성도 자유롭게 지정할 수 있는데요!
단 앞에서 말했듯 Tip의 사용 목적이 기능의 소개 또는 교육에 있기 때문에, 가이드를 주고자 하는 요소와 근접한 위치에 배치해야 한다는 점이 중요할 것 같습니다.

private let exampleTip = ExampleTip()

var body: some View {
    VStack(spacing: 20) {
        Text("This is an inline tip:")
        TipView(exampleTip, arrowEdge: .bottom) 
    }
    .padding()
}


반면, 팝오버 팁은 View처럼 사용하는 것이 아니라 .popoverTip() 이라는 수정자 (modifier)를 사용하게 됩니다.

한 번에 하나의 팁만 (자동으로) 표시될 수 있고,
기존 UI 요소를 가리면서 Tip이 표출되는 형태이기 때문에 훨씬 더 사용자에게 주목을 끌며 Tip을 소개할 수 있다는 특징이 있습니다.

private let exampleTip = ExampleTip()

ToolbarItem(placement: .topBarTrailing) {
    Button(action: {
        // 버튼 클릭 액션
    }) {
       Image(systemName: "questionmark.circle")
           .tint(Color.black)
    }
    .popoverTip(exampleTip)
}

 

 

특정 상황에서만 Tip이 표출되도록 제한하는 방법 : Rule

Tip이 특정 상태나, 사용자 행동에 따라 표시되는 조건을 구분하기 위해서 Rule을 사용할 수 있습니다.
Rule은 다시 파라미터 기반 룰 (Parameter-based Rules)과 이벤트 기반 룰 (Event-based Rules)로 나누어집니다.

  • Parameter Rules : 앱의 상태 (State and Boolean comparisons)에 따라 팁 표시 여부를 정하는 규칙
  • Event Rules : 사용자의 행동 (User actions)에 따라 팁 표시 여부를 정하는 규칙

Parameter Rule의 대표 사례는 "사용자의 로그인 여부"에 따라 팁을 표시하는 경우가 해당됩니다.

@Parameter 속성을 사용해 추적하고자 하는 앱의 상태 (여기서는 유저의 로그인 여부가 해당됩니다!)를 정의하고,
#Rule 매크로를 사용해 팁이 표출될 조건을 설정해주면 됩니다.
아래 코드를 보게 되면, @Parameter로 설정한 isLoggedIn 값이 ture일 때만 팁이 표출되도록 Tip의 Rule을 설정해준 것을 확인할 수 있습니다.

struct ExampleTip: Tip {
    @Parameter
    static var isLoggedIn: Bool = false		

    var rules: [Rule] {
        #Rule(Self.$isLoggedIn) {
            $0 == true
        }
    }
}


Event Rule의 대표 사례는 "사용자에 의해 발생한 특정 상호작용 횟수"에 따라 팁을 표시하는 경우가 해당됩니다.
*여기서 말하는 특정 상호작용이란, 사용자에 의해 버튼이 클릭되거나 화면에 방문한 이벤트 등을 의미한다고 보면 됩니다!

Event 객체를 정의해, 사용자에 의해 발생하는 상호작용을 표현합니다. Event는 고유한 id 값으로 식별합니다.
이후, #Rule 매크로를 사용해 팁이 표출될 조건을 위와 동일하게 설정해 주면 됩니다.
Event를 기록하기 위해서는 .donate() 메서드를 사용합니다.
해당 예시의 경우에는 특정 View의 방문한 횟수를 가지고 조건을 설정하기 위해, View의 onAppear 메서드 부분에 donate() 코드를 추가해 - View의 방문할 때마다 이벤트의 횟수를 증가시키는 것을 확인할 수 있습니다.

struct ExampleTip: Tip {
    static var enteredExampleView: Bool = Event(id: "enteredExampleView")		

    var rules: [Rule] {
        #Rule(Self.enteredExampleView) {
            $0.donation.count < 3
        }
    }
}

struct DetailView: View {
	...
}
.onAppear {
    ExampleTip.enteredExampleView.donate()
}

 

 

Tips.configure() 더 자세하게 알아보기

위에서 단순하게 '아 팁을 보여지게 하기 위해서는 초반에 이렇게 설정해야 하는구나' 라고만 설명하고 끝났던 configure()에 대해 더 자세하게 알아보려고 합니다!
위에서 Tips.configure는 TipKit의 데이터 컨테이너를 초기화하고, 팁의 상태를 저장 및 관리하는 메서드라고 설명했는데요.
보다 자세하게 configure에서 지정할 수 있는 Tip의 옵션을 살펴보도록 할게요!


displayFrequency는 Tip이 표시되는 빈도를 제어하는 속성입니다. 

default 값은 제한 없이 즉시 표시되는 .immediate 값이고,
이 외에도 .hourly (1시간 빈도), .daily (하루 빈도), .weekly (일주일 빈도), .monthly (한달 빈도) 등의 옵션 값을 설정할 수 있습니다.
물론, 이 속성 말고도 커스텀 TimeInterval 값을 사용해 빈도를 직접 지정하는 것도 가능하죠!

@main
struct ExampleApp: App {
    
    init() {
        try? Tips.configure([
            .displayFrequency(.daily)
        ])
    }
    ...


datastoreLocation은 Tip의 상태나 이벤트 데이터를 저장할 위치를 지정하는 속성입니다.

default 값은 애플리케이션의 전용 저장소를 의미하는 .applicationDefault이고,
앱 그룹 공유 컨테이너를 의미하는 .groupContainer(identifier:), 혹은 사용자 지정 URL에 저장하는 .url(:)도 자유롭게 사용할 수 있죠.
-> 동일한 사용자가 같은 iCloud 계정을 사용하는 경우, 아이폰과 아이패드에 동일한 앱을 설치해서 사용한다고 가정한다면 > 동일한 Tip을 같은 사용자에게 반복해서 보여줄 필요는 없겠죠? 이런 경우 Tip의 상태를 저장하는 공간이 어디일지를 고려해야할 것입니다!

@main
struct ExampleApp: App {
    
    init() {
        try? Tips.configure([
            .datastoreLocation(.applicationDefault)
        ])
    }
    ...


또한, Tip에서는 다양한 테스트 옵션을 제공하기도 합니다.
아래와 같은 메서드를 활용하여 모든 혹은 특정 팁에 대해서 Tip을 보이고 (show) 사라지게 (hide) 설정할 수 있죠!

// 모든 팁 표시하기
try? Tips.showAllTipsForTesting()

// 특정 팁만 표시하기
try? Tips.showTipsForTesting([ExampleTip1.self, ExampleTip2.self])

// 모든 팁 숨기기
try? Tips.hideAllTipsForTesting()

// 특정 팁만 숨기기
try? Tips.hideTipsForTesting([ExampleTip1.self, ExampleTip2.self])

// 테스트 시 기존 데이터를 초기화하기
try? Tips.resetDatastore()

 

 

여러 개의 Tip을 순서대로 표출하는 방법, TipGroup

지금까지 배운 내용으로 Tip을 자유롭게 만들고 표출하는데 전혀 문제가 없겠지만, 여기서 한 단계 더 나아가보겠습니다.
HIG에서는 여러 개의 Tip을 동시에 표출하는 액션을 "지양"하라고 설명합니다.
즉, 한 번에 하나의 Tip만 사용자에게 표출하도록 만들라는 의미죠!

그런데 만약, 한 화면에 보여줘야 할 팁이 두 개 이상이라면 어떻게 할까요?
이런 경우 사용할 수 있는 기능이 TipGroup입니다.

💡 TipGroup은 여러 개의 팁을 하나의 그룹으로 묶어 관리할 수 있도록 도와주는 기능입니다.
(A collection of tips that can be presented one at a time using a specific order or based on the first tip eligible for display.)


위의 영어 문장에서 보이는 것처럼 그룹으로 묶여있는 팁은 두 가지의 순서 옵션 중 하나로 표시될 수 있습니다.
해당 옵션은 TipGroup을 정의할 때 파라미터로 받게 되는 TipGroup.Priority 속성에 정의되어 있습니다.

  • .ordered : 그룹 내 팁이 정의된 순서대로 표시됩니다. (만약 앞에 있는 팁이 invalidated되거나 표출 조건이 만족되지 않는다면, 다음 팁은 표시되지 않습니다.)
  • .firstAvailable : 그룹 내에서 조건을 충족하는 첫 번째 팁만 표시됩니다. (여러 팁이 조건을 충족하더라도 첫 번째 팁만 표출됩니다.)


헷갈리면 안 될 것이!
ordered 같은 경우는 여러 개의 팁이 순서대로, 단계별로 사용자에게 기능을 표시할 수 있는 속성이라 - 그룹 내 지정된 팁이 조건만 맞는다면, 모두 순서대로 보이는 속성이고.
firstAvailable 같은 경우에는 첫 번째 조건만 맞는 팁만 보여지는 속성이라 - 그룹 내 지정된 여러 팁 중 하나만 보여지게 되는 속성입니다.

struct ExampleView: View {
    @State private var tips = TipGroup(.ordered) {
        ExampleTip1()
        ExampleTip2()
    }

    var body: some View {
        VStack {
            Text("Hello World")
                .popoverTip(tips.currentTip) 
            Button("Show Tip 1") {
                Tip1.show = true 
            }
        }
    }
}

 

 

Reference

 

TipKit - Apple Developer

새로운 TipKit 프레임워크를 사용하여 사용자에게 앱의 새로운 기능을 손쉽게 소개하고, 겉으로 드러나지 않은 기능을 안내하고, 작업을 더 빠르게 수행할 수 있는 방법을 보여 줄 수 있습니다.

developer.apple.com

 

Offering help | Apple Developer Documentation

Although the most effective experiences are approachable and intuitive, you can provide contextual help when necessary.

developer.apple.com

 

Make features discoverable with TipKit - WWDC23 - Videos - Apple Developer

Teach people how to use your app with TipKit! Learn how you can create effective educational moments through tips. We'll share how you...

developer.apple.com

 

Customize feature discovery with TipKit - WWDC24 - Videos - Apple Developer

Focused on feature discovery, the TipKit framework makes it easy to display tips in your app. Now you can group tips so features are...

developer.apple.com