ShadowEng
완료유튜브 영상으로 발음을 따라 하고 AI가 오류를 시각적으로 짚어주는 영어 섀도잉 학습 앱
사용자 주도형 콘텐츠
유튜브 URL을 직접 입력해 원하는 구간을 학습. 영상은 서버에 저장하지 않고 그때그때 재생.
YouTube Player + ExoPlayer 병행발음 오류 시각화
단순히 틀렸다고만 표시하지 않고, 강세·발음 포인트를 Canvas로 문장 위에 직접 그려서 보여줌.
Canvas API 좌표 정합게이미피케이션 학습 동기
매일 난이도별 문장을 풀어 점수를 쌓고 리그에서 경쟁. 상위 승격·하위 강등, 장기 미사용 시 Freeze로 순위 보호.
일일 챌린지 · 리더보드Overview
영어 섀도잉(shadowing) 학습은 원어민 음성을 듣고 그대로 따라 말하는 방식인데, 기존 앱들은 자체 제공 콘텐츠 안에서만 연습할 수 있어 사용자가 정말 듣고 싶은 영상으로는 연습할 수 없다는 한계가 있었다. ShadowEng은 사용자가 유튜브 URL을 직접 입력해 원하는 구간을 골라 섀도잉하고, AI가 발음 오류를 화면 위에 시각적으로 짚어주는 학습 앱이다. SSAFY 특화 프로젝트로 5명이 진행했고, 1위를 수상했다.
핵심 기능
- 유튜브 콘텐츠 등록: URL을 입력하면 메타데이터를 확보하고, 원하는 구간(start~end)을 직접 선택해 학습 스크립트로 등록한다.
- 무자막/자막 모드 셰도잉: 원어민 음성을 듣고 따라 말한 뒤, 녹음을 서버로 전송해 발음 평가를 받는다.
- 발음 오류 시각화: 평가 결과의 단어별 상태(끌림/급함/누락)를 문장 위에 화살표·곡선·하이라이트로 덧그려 보여준다.
- 맞춤 스크립트 자동 생성: 반복되는 오류 패턴을 모아 다음 학습 스크립트에 우선 반영한다.
- 일일 챌린지 · 리그: 매일 난이도별 문장으로 점수를 쌓고, 리그 단위로 사용자끼리 경쟁한다.
내 역할
PL(개발 리더) + 프론트엔드/인프라 전담. 원래 6명 팀이었는데 1명이 중도 이탈해 5명으로 줄었고, 본인을 제외한 팀원 전원이 비전공자(그중 2명은 개발 경험이 거의 없는 상태)였다. 유일한 개발 경험자로서 PL을 맡아, 기능을 난이도와 의존 관계 기준으로 쪼개 팀원별로 배분하는 역할까지 함께 했다. Kotlin·Jetpack Compose 기반 Android 앱 전체 개발, Canvas API 기반 발음 오류 시각화, 유튜브 영상·게임 음성 재생, Jenkins·Docker·AWS 기반 CI/CD 및 배포 환경 구축을 담당했다.
Architecture (개요)
Android (Kotlin/Compose)
│ YouTube Player(영상) · ExoPlayer(게임 음성) · Canvas(오류 시각화)
▼
Backend API ──▶ 발음 평가(AI)
- 화면별로 다른 상태 관리 패턴: 대부분 화면은 ViewModel이 UiState(StateFlow)를 들고 Compose가 구독하는 MVVM 계열로 구성했고, 상태 전이가 특히 복잡한 게임 플레이 화면 한 곳만 State/Intent/Effect를 분리한 MVI(Contract 패턴)로 구현했다.
- Feature 단위 모듈화:
study(학습 세션·하이라이트·리포트),game(일일 챌린지·리그),content(유튜브 URL 등록·구간 선택) 등 기능 단위로 패키지를 나누고, 그중 상태가 복잡한 도메인(study/game)은 api/domain/mapper/presentation/repository 레이어로 세분화했다. - SavedStateHandle: 게임 플레이처럼 여러 단계(카운트다운·녹음·평가·결과)를 거치는 화면에서, 화면 회전이나 프로세스 재생성이 일어나도 진행 상태가 유지되도록 SavedStateHandle에 실었다.
기술 선택 이유
- 화면 복잡도에 따른 아키텍처 혼합 (MVVM + 부분 MVI): 대부분 화면은 익숙한 MVVM으로 충분했다. 게임 플레이 화면은 기획 초기 더 복잡한 형태를 기준으로 State/Intent/Effect를 분리한 MVI(Contract 패턴)를 도입했는데, 이후 일정상 게임 모드 자체가 지금 형태로 단순화되면서 최종 결과물만 보면 다소 과한 구조가 됐다. 이미 동작하는 구조를 되돌리는 비용보다 남은 일정을 완성도에 쓰는 쪽을 택해 그대로 유지했다.
- Canvas API (SpanStyle 대신): 발음 오류를 단순 배경색(SpanStyle)으로만 표시하면 “끌림”이나 “급함” 같은 시간축 정보를 담을 수 없었다.
TextLayoutResult.getBoundingBox(charIndex)로 문자 단위 좌표를 얻어 그 위에 Canvas로 화살표·곡선을 직접 그리는 방식을 택해, 단순 색칠보다 풍부한 피드백을 표현했다. - YouTube Player + ExoPlayer 병행: 유튜브 영상 재생은 유튜브 자체 IFrame 플레이어를 그대로 썼고, 게임 모드에서 재생하는 참조 음성 파일은 ExoPlayer로 별도 처리했다. 용도가 다른 두 미디어(외부 유튜브 영상 vs 자체 음성 파일)를 하나의 플레이어로 억지로 통일하지 않고, 각각에 맞는 재생 방식을 그대로 썼다.
배운 점
- 아키텍처는 앱 전체에 하나로 통일해야 한다는 강박을 버렸다. 상태 흐름이 단순한 화면은 팀이 이미 익숙한 MVVM으로, 유독 복잡한 화면 하나만 MVI로 — 화면별 복잡도에 맞게 패턴을 섞어 써도 된다는 걸 PL 역할을 하며 체감했다.
- 개발 경험이 없는 팀원에게 일을 배분할 때는 “무엇을 만들 것인가”보다 “어떤 순서로, 어디까지 혼자 해낼 수 있는가”를 먼저 판단해야 한다는 걸 배웠다.
- 문자 단위 좌표(
getBoundingBox)를 다루면서, 텍스트 위에 그림을 겹치는 UI는 텍스트 레이아웃을 얼마나 정확히 이해하느냐에 품질이 좌우된다는 걸 알게 됐다.