아이메세지 스티커 앱 개발기 - 나만의 스티커를 만들어보자

cheshire0105·2025년 6월 1일

iOS

목록 보기
39/45
post-thumbnail

서론

아이메세지 스티커는 대화를 더 재미있고 다채롭게 만들어주는 훌륭한 도구이다. 이번에 만든 앱은 유저가 키링 사진을 찍은 뒤, 사진의 배경을 제거 하고 그걸 아이메세지 스티커로 사용 하는 앱이다. 이 글에서는 아이메세지 스티커 앱을 만드는 기본적인 과정과 핵심 개념을 다룬다.

개발 준비 및 기본 구조

아이메세지 스티커 앱은 일반 iOS 앱과 달리 '메세지 앱 익스텐션' 형태로 개발된다. 핵심이 되는 두 개의 뷰 컨트롤러가 있다.

  1. MessagesViewController: 이 클래스는 MSMessagesAppViewController를 상속받는다. 스티커 앱의 시작점이라고 할 수 있으며, 스티커 브라우저를 설정하고 화면에 보여주는 역할을 한다.

    import UIKit
    import Messages
    
    class MessagesViewController: MSMessagesAppViewController {
    
        private var stickerBrowserVC: StickerBrowserViewController!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            setupStickerBrowser() // 스티커 브라우저 설정 함수 호출
        }
    
        private func setupStickerBrowser() {
            stickerBrowserVC = StickerBrowserViewController() // 스티커 브라우저 뷰 컨트롤러 인스턴스 생성
            addChild(stickerBrowserVC) // 자식 뷰 컨트롤러로 추가
            view.addSubview(stickerBrowserVC.view) // 뷰 계층에 추가
    
            // Auto Layout 설정
            stickerBrowserVC.view.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                stickerBrowserVC.view.topAnchor.constraint(equalTo: view.topAnchor),
                stickerBrowserVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
                stickerBrowserVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                stickerBrowserVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            ])
    
            stickerBrowserVC.didMove(toParent: self) // 자식 뷰 컨트롤러 설정 완료 알림
        }
    }

    viewDidLoad 시점에 setupStickerBrowser 함수를 호출하여 StickerBrowserViewController를 초기화하고, 화면 전체를 채우도록 Auto Layout 제약 조건을 설정한다.

  2. StickerBrowserViewController: 이 클래스는 MSStickerBrowserViewController를 상속받는다. 실제 스티커들을 화면에 나열하고 사용자와의 상호작용을 처리하는 역할을 한다.

    import UIKit
    import Messages
    
    class StickerBrowserViewController: MSStickerBrowserViewController {
    
        private var stickers: [MSSticker] = [] // 로드된 스티커들을 저장할 배열
        // ... (초기화 및 기타 코드) ...
    }

    이 컨트롤러는 MSStickerBrowserViewDataSource 프로토콜을 채택하여 스티커 브라우저에 표시할 스티커의 개수와 각 인덱스에 해당하는 스티커 객체를 제공해야 한다.

스티커 로딩의 비밀

스티커 앱의 핵심 기능은 바로 스티커 이미지를 불러와 사용자에게 보여주는 것이다. 이 과정은 다음과 같다.

  1. 스티커 이미지 준비: 스티커로 사용할 이미지 파일(주로 PNG 형식)을 준비한다. 이 파일들은 앱과 메세지 익스텐션이 공유할 수 있는 공간인 '앱 그룹(App Group)' 컨테이너에 저장하는 것이 일반적이다. 앱 그룹을 사용하면 본 앱에서 스티커 이미지를 관리하고, 메세지 익스텐션에서는 이 이미지를 읽어와 보여줄 수 있다.

  2. loadStickers 함수: StickerBrowserViewController 내의 이 함수가 스티커 로딩의 핵심 로직을 담당한다.

    @objc private func loadStickers() {
        stickers = [] // 기존 스티커 배열 초기화
    
        // 1. 앱 그룹 컨테이너 URL 가져오기
        guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.cheshire0105.SassyCatDog") else {
            print("❌ App Group container not found")
            return
        }
    
        do {
            // 2. 컨테이너 내 파일 목록 읽기 (PNG 파일 필터링)
            let contents = try FileManager.default.contentsOfDirectory(at: containerURL, includingPropertiesForKeys: [.fileSizeKey, .creationDateKey])
            let imageFiles = contents.filter { $0.pathExtension == "png" }
            
            print("📦 Found \(imageFiles.count) image files in container")
    
            for imageURL in imageFiles {
                do {
                    // ... (파일 정보 로깅) ...
                    
                    // 3. 이미지 파일로부터 UIImage 객체 생성
                    guard let image = UIImage(contentsOfFile: imageURL.path) else {
                        print("❌ Failed to load image: \(imageURL.lastPathComponent)")
                        continue
                    }
                    
                    // ... (이미지 정보 로깅) ...
                    
                    // 4. 이미지를 스티커용으로 변환 (리사이징 및 임시 저장)
                    if let stickerURL = makeStickerPNG(image) {
                        // 5. MSSticker 객체 생성
                        let sticker = try MSSticker(
                            contentsOfFileURL: stickerURL,
                            localizedDescription: "My Sticker" // 스티커 설명
                        )
                        stickers.append(sticker) // 배열에 추가
                    }
                    print("✅ Successfully created sticker from \(imageURL.lastPathComponent)")
                } catch {
                    // ... (오류 처리) ...
                }
            }
            
            print("📊 Total stickers loaded: \(stickers.count)")
            
            // 6. 스티커 브라우저 뷰 갱신 (메인 스레드에서)
            DispatchQueue.main.async { [weak self] in
                self?.stickerBrowserView.reloadData()
            }
        } catch {
            print("❌ Failed to read App Group contents: \(error)")
        }
    }

    단계별로 살펴보면, 먼저 앱 그룹 컨테이너의 URL을 가져온다. 그 다음 해당 경로에서 PNG 확장자를 가진 파일들을 찾는다. 각 이미지 파일을 UIImage 객체로 로드한 후, makeStickerPNG 함수를 통해 iMessage 스티커 규격에 맞게 처리한다.

    메시지는 세 가지 스티커 크기를 지원한다. 크기에 맞지 않는 ( 세로가 너무 길거나, 가로가 너무 길거나, 크기가 너무 크거나... ) 하면 아예 아이메세지 텍스트 필드에 입력 되지 않는다.

  • 작은 크기. 100 x 100 포인트 @3x (300 x 300 픽셀).

  • 중간. 136 x 136 포인트 @3x (408 x 408 픽셀).

  • 대형. 206 x 206 포인트 @3x (618 x 618 픽셀).

  1. makeStickerPNG 함수: 이 함수는 UIImage를 입력받아 스티커에 적합한 형태로 가공하고, 고유한 URL에 저장한 뒤 그 URL을 반환한다.

    private func makeStickerPNG(_ image: UIImage) -> URL? {
        let targetSize = CGSize(width: 300, height: 300) // 스티커 크기 (예: 300x300 포인트)
        UIGraphicsBeginImageContextWithOptions(targetSize, false, 1) // 이미지 컨텍스트 생성
        image.draw(in: CGRect(origin: .zero, size: targetSize)) // 이미지 리사이징하여 그리기
        guard let pngData = UIGraphicsGetImageFromCurrentImageContext()?.pngData() else { // PNG 데이터 추출
            UIGraphicsEndImageContext()
            return nil
        }
        UIGraphicsEndImageContext() // 이미지 컨텍스트 종료
    
        // UUID를 사용하여 고유한 파일명 생성
        let fileName = UUID().uuidString + ".png"
        let tmpURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName) // 임시 디렉토리에 저장할 URL
    
        do {
            try pngData.write(to: tmpURL, options: .atomic) // PNG 데이터 파일로 저장
            return tmpURL
        } catch {
            print("❌ Failed to write sticker PNG: \(error)")
            return nil
        }
    }

    여기서는 이미지를 300x300 포인트 크기로 리사이즈하고, PNG 데이터로 변환한다. 중요한 점은 파일 이름에 UUID를 사용하여 매번 다른 URL에 저장하도록 하는 것이다. 이렇게 하면 동일한 이미지를 여러 번 스티커로 만들어도 각 스티커가 고유한 MSSticker 객체로 인식될 수 있다. (실제로는 이미지 내용이 같다면 캐싱 등의 이유로 동일하게 처리될 수 있으나, 파일 URL 자체는 고유하게 만드는 것이 좋다.) 최종적으로 생성된 이미지 파일의 URL을 사용하여 MSSticker 객체를 만든다.

  2. MSStickerBrowserViewDataSource 구현: StickerBrowserViewControllerMSStickerBrowserViewDataSource 프로토콜의 메서드들을 구현해야 한다.

    // MARK: - MSStickerBrowserViewDataSource
    
    override func numberOfStickers(in stickerBrowserView: MSStickerBrowserView) -> Int {
        return stickers.count // 로드된 스티커의 총 개수 반환
    }
    
    override func stickerBrowserView(_ stickerBrowserView: MSStickerBrowserView, stickerAt index: Int) -> MSSticker {
        return stickers[index] // 해당 인덱스의 MSSticker 객체 반환
    }

    이 두 함수는 스티커 브라우저 뷰에게 몇 개의 스티커를 표시할지, 그리고 각 위치에 어떤 스티커를 표시할지를 알려준다. loadStickers 함수 마지막에 stickerBrowserView.reloadData()를 호출하면 이 데이터 소스 메서드들이 다시 호출되어 화면이 갱신된다.

스티커 자동 업데이트 기능

사용자가 앱 그룹 컨테이너에 새로운 스티커 이미지를 추가하거나 기존 스티커를 수정했을 때, 스티커 앱이 이를 감지하고 자동으로 업데이트하는 기능은 사용자 경험을 향상시킨다.

private func setupFileSystemObserver() {
    // UserDefaults 변경 감지 (주석: 실제로는 앱 그룹 내 파일 시스템 변경을 감지하는 것이 더 적합할 수 있다)
    fileSystemObserver = NotificationCenter.default.addObserver(
        forName: UserDefaults.didChangeNotification, // UserDefaults 변경 알림
        object: nil,
        queue: .main
    ) { [weak self] _ in
        print("📂 UserDefaults changed, reloading stickers...")
        self?.loadStickers() // 스티커 다시 로드
    }
    
    // 앱이 활성화될 때마다 스티커 새로고침
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(loadStickers), // loadStickers 함수 직접 호출
        name: UIApplication.didBecomeActiveNotification, // 앱 활성화 알림
        object: nil
    )
}

deinit { // 뷰 컨트롤러 해제 시 옵저버 제거
    if let observer = fileSystemObserver {
        NotificationCenter.default.removeObserver(observer)
    }
    // UIApplication.didBecomeActiveNotification에 대한 옵저버도 제거해야 한다.
    // NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
}

setupFileSystemObserver 함수는 두 가지 방식으로 업데이트를 감지한다.
1. UserDefaults.didChangeNotification: UserDefaults에 변경이 생기면 알림을 받아 스티커를 다시 로드한다. 코드에서는 이 방식을 사용했지만, 실제 앱 그룹 내 파일 변경을 직접 감지하는 방법(예: NSFilePresenter 프로토콜 활용 또는 주기적인 폴링)이 더 정확할 수 있다.
2. UIApplication.didBecomeActiveNotification: 메세지 앱이 백그라운드에 있다가 다시 활성화될 때 스티커를 새로고침한다. 이는 사용자가 다른 앱에서 스티커 파일을 변경하고 다시 메세지 앱으로 돌아왔을 때 유용하다.

이러한 알림을 받으면 loadStickers 함수를 호출하여 스티커 목록을 최신 상태로 유지한다. deinit에서는 등록된 옵저버를 반드시 제거하여 메모리 누수를 방지해야 한다.

결론

지금까지 아이메세지 스티커 앱을 개발하는 기본적인 과정을 살펴보았다. MessagesViewController로 기본 틀을 잡고, StickerBrowserViewController를 통해 실제 스티커를 로드하고 표시하며, 앱 그룹과 파일 시스템 관찰을 통해 스티커를 관리하고 업데이트하는 방법을 알아보았다.

코드는 간단해 보일 수 있지만, 이미지 처리, 앱 그룹 설정, 익스텐션의 생명주기 등 고려해야 할 부분이 많다. 이 글이 개성 넘치는 스티커 앱을 만드는 데 첫걸음이 되기를 바란다.

참고링크

Adding your sticker packs to Messages

0개의 댓글