Projects
사이드 2025

Toonify

인스타그램에 연재 중인 인스타툰의 새 에피소드를 자동 감지하고 푸시 알림으로 알려주는 React Native 앱. GitHub Actions 배치로 1시간마다 업데이트를 체크합니다.

Role

기획 · 설계 · 개발 (1인)

Period

2025

Stack

React Native, Expo SDK 54, Supabase, GitHub Actions, AsyncStorage, hasdata API, ML Kit, Expo Push API

만든 이유

인스타그램에서 재미있는 인스타툰을 발견해도, 다음 편이 올라왔는지 확인하려면 직접 그 계정을 방문해야 합니다. 인스타그램의 일반 팔로우 알림은 팔로우한 모든 계정의 게시물이 섞여 오기 때문에 툰 업데이트만 골라서 받는 방법이 없었습니다. 여러 작가를 팔로우하면 새 편이 언제 올라왔는지 놓치기 쉽고, 결국 구독하던 시리즈를 잊어버리게 됩니다.

관심 작가를 등록해두면 새 에피소드가 올라올 때 알림을 받을 수 있는 앱을 만들었습니다.

주요 기능

  • 인스타툰 구독 관리 — 링크 붙여넣기만 하면 계정명·시리즈명·화수를 자동 파싱. 스와이프 삭제, 롱프레스 수정 지원
  • 자동 에피소드 감지 — 최신 게시물 12개를 완전 스캔해 캡션 키워드 매칭으로 새 화 감지. 캡션에 화수가 없을 경우 온디바이스 OCR로 이미지에서 직접 인식
  • 복수 화수 동시 알림 — 한 사이클에 여러 화가 올라온 경우 “3화, 4화가 추가되었어요” 형식으로 한 번에 알림
  • 직접 확인 배지 — 최신 12개 게시물 안에서 시리즈를 감지하지 못한 경우 직접 확인 배지를 표시
  • 푸시 알림 — 새 에피소드 발견 시 Expo Push API로 즉시 푸시 알림 전송
  • 다음 화 바로가기 — NEW 카드를 탭하면 안 본 화를 화수 오름차순으로 순서대로 이동
  • 읽음 처리 — 화수별 읽음 상태 관리, NEW 배지 표시
  • 앱 내 자동 체크 — GitHub Actions 외에도 앱 내에서 2시간마다 자동 체크 + 수동 새로고침 지원
  • 라이트 / 다크 모드 — 시스템 기본값 연동 및 수동 전환

서비스 흐름

앱 실행 → Expo push token을 Supabase에 저장 → 구독한 툰 목록도 Supabase에 동기화 → GitHub Actions가 1시간마다 hasdata API로 인스타그램 최신 게시물 12개 조회 → 캡션 매칭 + 필요 시 온디바이스 OCR로 새 에피소드 감지 → Expo Push API로 푸시 알림 → 폰에 알림 도착

기술적 기여

캡션 + 온디바이스 OCR 2단계 에피소드 감지

캡션에 화수 표기가 없는 경우를 대비해 2단계 폴백 구조를 설계했습니다. 먼저 캡션에서 n화 / n편 / ep.n / #n 패턴으로 화수를 추출하고, 캡션에 화수가 없으면 @react-native-ml-kit/text-recognition으로 게시물 이미지에서 텍스트를 인식해 화수를 뽑아냅니다. 캡션 매칭에는 시리즈명에서 3자 이상인 단어를 우선 사용해 짧은 단어로 인한 오탐을 방지했습니다.

OCR 호출 93% 절감 — last_post_id 기준점 설계

초기에는 GitHub Actions가 실행될 때마다 최신 포스트 12개 전체를 처음부터 재검사했습니다. 새 포스트가 없어도 매 사이클마다 OCR을 최대 12회씩 소모하는 구조였습니다.

Supabase toons 테이블에 last_post_id 컬럼을 추가해 마지막으로 처리한 포스트 ID를 기억하고, 그 이후 포스트만 처리하도록 변경했습니다.

상황기존개선
새 포스트 없을 때OCR 4~12회0회
새 포스트 1개 올라왔을 때OCR 12회최대 1~2회
하루 기준 (툰 5개, 8회 실행)160회12회
절감률약 93%

첫 체크 시 기준점을 가장 오래된 포스트(oldest) 로 설정하는 것도 중요했습니다. newest 기준으로 잡으면 등록 당시 이미 나와있던 이전 화들을 영원히 놓치는 버그가 생깁니다. 이 케이스를 포함해 회귀 테스트 4개로 동작을 고정했습니다.

GitHub Actions 배치 + Expo Push 알림 파이프라인

별도 서버 없이 GitHub Actions를 3시간 인터벌 배치 서버로 활용했습니다. 배치 스크립트가 Supabase에서 구독 목록과 push token을 읽어 인스타그램 최신 12개 게시물을 확인하고, 새 에피소드가 감지되면 Expo Push API로 해당 디바이스에 알림을 전송합니다. 이미 hasNewEpisode: true인 경우 중복 알림을 차단하는 로직도 포함했습니다.

AsyncStorage + Supabase 이중 저장 구조

오프라인 환경에서도 앱이 동작하도록 로컬은 AsyncStorage, 온라인 동기화는 Supabase로 이중 저장 구조를 설계했습니다. 앱이 포그라운드로 돌아올 때 배치 스크립트가 Supabase에 기록한 새 에피소드 정보를 로컬 상태에 반영해, 알림을 못 받았더라도 앱을 열면 업데이트가 표시됩니다.

다음 화 순차 네비게이션

새 에피소드가 여러 화 쌓인 경우 안 본 화를 화수 오름차순으로 정렬해 한 화씩 순서대로 이동하도록 설계했습니다. 카드를 탭할 때마다 readEpisode가 1씩 증가하고, 모두 소진하면 hasNewEpisodefalse로 전환됩니다.

트러블슈팅

OCR API → 온디바이스 전환

초기에 OCR.space 클라우드 API를 사용했으나 무료 한도가 빠르게 소진됐습니다.

GitHub Actions 서버는 네이티브 앱 환경이 아니라 OCR API를 호출할 수밖에 없었고, 이 API 비용이 한도를 빠르게 태웠습니다. 반면 앱 내 OCR은 디바이스에서 직접 실행할 수 있어 네트워크도, API 키도, 한도도 없는 온디바이스 OCR로 전환하기로 했습니다. 서버 측(GitHub Actions)은 캡션 키워드 매칭으로만 화수를 감지하고, 캡션에 화수가 없는 경우는 앱 안에서 온디바이스 OCR로 보완하는 구조입니다.

선택한 라이브러리 @react-native-ml-kit/text-recognition은 iOS에서 Apple Vision, Android에서 Google ML Kit를 사용하며 한국어를 지원합니다. 전환 과정에서 세 가지 문제를 겪었습니다.

iOS 최소 버전 에러 — ML Kit가 iOS 15.5 이상을 요구해 pod install이 실패했습니다. app.config.jsios.deploymentTarget을 직접 수정해도 Podfile이 Podfile.properties.json에서 값을 읽는 구조라 적용이 안 됐고, expo-build-properties 플러그인으로 해결했습니다.

expo-file-system API 변경 — 새 API의 Paths.cache가 문자열이 아닌 Directory 객체라서 new File(Paths.cache, filename) 내부에서 문자열 변환을 시도하다 cannot convert object to primitive value 에러가 났습니다. .uri 속성으로 문자열을 직접 꺼내서 해결했습니다.

const tempUri = Paths.cache.uri + `ocr_temp_${Date.now()}.jpg`;
const tempFile = new File(tempUri);

한국어 인식 안 됨 — 이미지 다운로드도, ML Kit 실행도 정상인데 result.text가 빈 문자열이었습니다. 원인은 기본 인식 스크립트가 Latin(영어)이었던 것. TextRecognitionScript.KOREAN을 명시하니 바로 해결됐습니다.


개발 빌드 Metro 서버 localhost 문제

ML Kit는 네이티브 모듈이라 Expo Go에서 동작하지 않아 개발 빌드가 필요했습니다. EAS로 dev 빌드를 올리고 Metro 서버에 연결하려는데 localhost:8081로 고정되어 폰에서 접근이 불가했습니다.

--host lan 옵션, 환경변수 설정, 핫스팟 등 여러 방법을 시도했으나 모두 실패했습니다. 원인은 프로젝트 루트에 ios/android 폴더가 존재하면 Expo CLI가 로컬 네이티브 빌드 환경으로 인식해 localhost를 사용하는 것이었습니다. 두 폴더를 삭제하니 올바른 LAN IP로 URL이 생성됐습니다.

rm -rf ios android
npx expo start --dev-client
# → exp+toonify://expo-development-client/?url=http://192.x.x.x:8081

EAS Build를 사용하는 경우 로컬에 ios/android 폴더가 없어도 무방합니다. 네이티브 모듈 변경 시에만 eas build --profile development로 새 바이너리를 빌드하고, 이후 JS 수정은 Metro Fast Refresh로 즉시 반영됩니다.


분류문제원인해결
OCR 전환API 한도 소진클라우드 API 의존ML Kit 온디바이스 전환
iOS pod install 실패ML Kit iOS 15.5 이상 요구expo-build-properties 플러그인
expo-file-system 에러Paths.cache가 객체, 문자열 아님.uri로 문자열 직접 추출
OCR 결과 빈 문자열기본 스크립트가 LatinTextRecognitionScript.KOREAN 명시
개발 환경Metro localhost 고정ios / android 폴더 존재두 폴더 삭제 후 재시작
최적화OCR 과다 호출매 사이클 전체 재스캔last_post_id 기준점 → 93% 절감