지난글 링크
https://codingisland.tistory.com/116
[Flutter] AI로 노래가사 해석하는 앱개발
앱을 만들게 된 과정1. 앱을 왜 만들고 어떻게 시작했는가?앱을 만들게 된 이유동기: 일본어 공부가 하고 싶어짐 -> 평소 Jpop 많이 들음.영감: 유튜브 쇼츠 카지노 차무식이 팝송 500곡 외웠다길래
codingisland.tistory.com
https://codingisland.tistory.com/117
[Flutter] AI로 노래가사 해석하는 앱개발 - 2
지난 글 요약개발 동기: J-Pop을 즐겨 듣다 노래를 통째로 외워 일본어를 마스터하고 싶다는 생각에 개발을 시작했다제작 방식: Flutter의 중첩 구조가 낯설어 **AI와 협업하는 '바이브 코딩'**으로 Fi
codingisland.tistory.com
깃허브링크: https://github.com/rudIsland/AI_japaness_Lyrics_study
가명: 카시코이
Kasi-AI 일본어로 '카시코이(賢い)'는 '똑똑하다'라는 뜻입니다. AI가 똑똑하게 가사를 풀어준다
개발하며 터진 문제와 해결한 방안
1. 노래 가사 불러오기: "AI가 다 해줄 줄 알았지"
나는 평생 프로그래밍만 해서 기획에는 전혀 아는게 없었다. 처음엔 그냥 유튜브 영상 제목만 AI한테 주면, 얘가 알아서 원곡 제목이랑 가수명을 찾고 가사까지 싹 긁어올 줄 알았다. 근데 AI한테 커버곡 영상을 주니까 커버한 사람을 원곡 가수로 착각하고 가사도 엉뚱한 걸 가져온다. 전혀 도움이 안됐다.
- 해결: 결국 가수명과 제목을 사용자가 직접 입력하게 만들었다. 그리고 Netease와 LrcLib라는 두 종류의 API를 섞어 썼다. 하나만 쓰면 못 가져올 때가 있는데, 2개를 같이 돌리니까 성공률이 확 올라갔다. 가사를 못가져온 일은 그 뒤로 없었다. 덕분에 가사 싱크(타임스탬프)까지 가져와서 실시간 강조 기능도 넣을 수 있었다.

2. 소형 플레이어 (Mini Player) 개발 포기
유튜브 뮤직처럼 노래를 듣다가 화면을 나가도 하단에 작게 남아서 계속 재생되는 기능을 넣고 싶었다. 간지 나고 노래듣다가 흐름도 끊기지 않으니 말이다. 그런데 플러터의 위젯 트리 구조상 부모-자식 위젯 사이에 노래가 끊기지 않게 연결하는 게 생각보다 너무 빡셌다.
- 해결: 고민하다가 깨달았다. 내 앱의 목적은 '노래 감상'이 아니라 '가사 번역 공부'다. 주객전도가 되면 안 되겠다 싶어서 과감히 포기했다.
3. AI 해석 문제: "컴퓨터는 시키는 것만 한다"
AI는 똑똑해 보이지만 결국 시키는 것만 하는 기계다. 프롬프트(명령어)를 대충 주면 지 멋대로 번역을 하거나 형식을 망가뜨린다. 자연스러우면서도 공부하기 좋게 번역 시키려고 이 모델 저 모델 써보면서 토큰만 엄청 날렸다. 그렇게 2~3일은 명령만 내리다 날 샜다.
- 해결: AI는 AI로 맞대응했다. 다른 똑똑한 AI 모델한테 "야, 얘한테 어떻게 지시해야 번역 잘하냐?"라고 물어보고, 거기서 나온 프롬프트를 그대로 꽂아 넣었더니 훨씬 잘 한다.. 몇 번 시켜보니 완벽하게 잘하더라

4. FirebaseDB 추가 및 로그인 기능 추가에 대한 문제
원래는 로컬 DB인 Drift만 쓰려고 했다. 친구가 "로컬로 충분해"라고 하기도 했고. 근데 만들다 보니 욕심이 생겼다. 구글 로그인도 넣고 싶고, 클라우드 동기화도 하고 싶어서 Firebase를 추가하기로 했다. 여기서 지옥이 시작됐다.
1) DriftDB(로컬) 와 FirebaseDB(클라우드), 두 DB의 공존 전략 문제
로그인을 안하고 사용하는 사람과 로그인을 하고 사용하는 사람간에 권한차이를 둬야겠단 생각이 들었다.
그래서 비로그인은 로컬DB만 사용 / 로그인사용자는 클라우드DB로 관리되게끔 하려했다.
Drift는 SQL이고 Firbase는 NoSQL이라서 같은 기능이지만 코드구현은 달라야 하기에 두 로직을 어떻게 나누고, 어떻게 접근시키지란 고민이 많았다.
그 과정에서 기존 로직이 꼬여서 하루는 앱을 못키기도 했다.
나는 Flutter가 자연어에 가까운 언어라는 것을 생각했다. 바로 Implements(위임)을 활용하기로 했다.
abstract class DatabaseRepository {
/// ⚙️ 초기화
Future<void> saveInitialize();
/// 👤 [Group: Get] 조회
Future<AccountData?> getAccount(String uid);
Stream<List<VideoInfo>> getFrequentlyVideos();
Future<int> getTodayCompletedCount();
Future<bool> isVideoCompletedToday(String youtubeVideoId);
Future<List<VideoInfo>> getFavoriteVideos();
Stream<List<StudiedVideoUI>> getStudiedVideosUI();
Future<VideoInfo> getVideoFromYoutube(YoutubeVideo video);
Future<List<Lyric>> getLyricsForVideo(String youtubeVideoId);
Future<GlobalVideoInfo?> getGlobalVideo(String youtubeVideoId);
/// 📥 [Group: Save/Insert] 생성 및 저장
Future<void> saveAccount(User user, String? deviceId);
Future<void> saveFrequentlyVideo(VideoInfo video);
Future<void> saveGlobalVideo(GlobalVideoInfo video);
Future<void> saveStudyRecord(String youtubeVideoId);
Future<void> insertLyricsData(String youtubeVideoId, List<Lyric> lyrics);
Future<bool> getFavoriteStatus(String videoId);
/// 🛠️ [Group: Update] 수정
Future<void> updateFavoriteStatus(VideoInfo video, bool status);
Future<void> updateAnalyzedLyrics(String youtubeVideoId, List<Lyric> lyrics);
Future<void> updateLyricsData(String youtubeVideoId, List<Lyric> lyrics);
Future<void> updateIsAnalyzed(String youtubeVideoId, bool status);
Future<void> updateIsLyrics(String youtubeVideoId, bool status);
Future<void> updateSearchedInfo({
required String youtubeVideoId,
required String searchedArtist,
required String searchedTitle,
});
/// 🛠️ [Update] 특정 필드의 카운트를 1 올리고 날짜를 오늘로 기록
Future<void> updateDailyCount(String fieldName);
/// 🗑️ [Group: Delete] 삭제
Future<void> deleteStudiedRecord(int recordId);
}
이렇게 추상 클래스로 뼈대를 만들고, Drift와 Firebase 클래스가 각각 이걸 구현하게 했다. 그리고 ManagerDatabase라는 관리자 클래스를 만들어서 로그인 상태면 Firebase를, 아니면 Drift를 호출하게 위임시켰다. 덕분에 UI 코드에서는 로그인 여부를 일일이 따질 필요 없이 관리자 인스턴스만 부르면 되니까 훨씬 깔끔해졌다.

그 결과 ManagerDB클래스의 싱글톤 instance에 접근해서 필요한 함수를 호출하면 로그인 여부는 관리자클래스가 알아서 해주니 헷갈릴 일 없이 쉽게 구현할 수 있었다.
2) 로그인 여부에 따른 권한 문제
아무래도 로그인을 했느냐 안했느냐에 권한을 줘야했다. 로그인을 하면 다른 기기에서 같은 아이디로 로그인 했을때 기록이 동기화되어야 했고, 할 수 있는 권한도 차별화를 두고 싶었다.
| 구분 | 기능 (Feature) | 👤 비로그인 (Guest) | 🔐 로그인 (User) | 비고 |
| 핵심기능 | AI 가사 분석 | ❌ 불가 | ⭕ 사용 가능 | AI 토큰 비용 관리 목적 |
| 가사 검색(불러오기) | ❌ 불가 | ⭕ 사용 가능 | 정확한 가사 매칭 기능 | |
| 유튜브 영상 재생 | ⭕ 가능 | ⭕ 가능 | 기본 학습 기능은 모두 제공 | |
| 데이터 | 저장위치 | 내 휴대폰 | 클라우드 서버 | |
| 공부기록/즐겨찾기 | 기기에만 저장 | 계정에 저장(동기화) | ||
| 데이터 보존 | 앱 삭제시 초기화 | 앱 삭제해도 유지 | ||
| 기기연동 | 불가(기기 변경시 끝) | 가능(다른 폰 연동가능) | ||
| 기타 | 자주 듣는 곡 / 공부기록 | 로컬 기록 기반 | 서버 기록 기반 |
권한은 이렇게 부여했다.
핵심은 클라우드 집단지성이다. 로그인 유저가 AI로 가사를 한 번 분석해서 클라우드에 올려두면, 나중에 비로그인 유저가 그 노래를 들을 때 그 정보를 가져다 쓸 수 있다.
Ex)
1) 비로그인 유저가 노래A를 원함. -> 클라우드에는 노래A가 없어서 가사 정보도 없음. -> 노래는 들을 수 있지만 가사는 못봄
2) -> 로그인유저가 노래A를 원함 -> AI와 가사불러오기를 활용해 가사 정보를 불러와 클라우드에 저장됨.
3) -> 비로그인유저가 노래A를 다시 들으러 클릭함. -> 클라우드에 가사 정보가 있으니 가사를 볼 수 있음. -> 가사 정보를 로컬에 저장. -> 나중에 다시 들어와도 볼 수 있음.
이렇게 되면 로컬(Drift)유저도 Firebase에 접근이 필요하게 되긴한다. 하지만 이런 기능을 꼭 구현하고 싶었다.
5. 코드의 구조와 성능: 바이브 코딩의 한계
AI가 짜주는 대로 갖다 붙이다 보니 성능은 뒷전이었다. 근데 테스트를 해보니 동기화가 묘하게 느리더라. 범인은 Stream 남발과 StatefulWidget의 남용이었다.
- Future vs Stream: 유니티로 치면 Awake와 Update 차이다. 1회성 데이터는 Future면 충분한데 무지성으로 Stream을 꽂아놔서 데이터가 꼬였던 것이다. 호출 시점에만 딱 부르는 Future로 바꾸니 깔끔해졌다.
- Stateful 위젯: 플러터는 위젯 단위로 다시 그린다. 근데 화면 전체를 Stateful로 감싸버리니 텍스트 하나 바뀔 때 화면 전체를 다시 그리는 비효율이 발생했다. 변화가 필요한 부분만 쏙 빼서 Stateful로 만들고 나머지는 Stateless로 박아버리는 게 성능의 핵심이었다.

초록 테두리를 Stateless로 두어 변하지 않게 두었고, 그 화면 내에 변화가 필요한 위젯들인 빨간테두리에만 Stateful을 하여 구현해야했다. 하지만 나는 홈-기록-즐겨찾기 탭을 슬라이드로 넘기는 방식을 구현하고자 Stateful을 해야했다. 그래도 앱의 규모가 작아서 속도로 문제되진 않았다. 하지만 앞으로의 Flutter에서는 필요한 곳만 Stateful을 사용해야 할 것이다.
개발을 마친 후 앱 화면






마무리 소감
무언가를 만들어낸다는 감각은 언제나 재밌고 짜릿하다.
그게 내 순수 능력으로 만드는 것은 아닐지라도, 내 능력보다 더 엄청난 것을 만들어 줄 수 있게 해주는게 "AI"이다.
나는 그 AI를 활용하는 능력을 나쁘게 보진 않는다.
1. "AI가 다 해준 거 아냐?"라는 시선에 대해
하지만 당장 무언가를 완성하고 싶은 열망에 비해 내 능력치가 벅찰 때가 있고, 무엇보다 AI의 발전 속도는 내 성장 속도보다 훨씬 빠르다. 나는 이 흐름을 거부하기보다 적극적으로 올라타기로 했다.
이번 개발을 통해 느낀 건, AI가 앱을 완성시킬 수 있는 비중은 딱 60% 정도라는 것이다. 나머지 40%는 결국 사람이 채워야 한다. 내가 기초적인 Dart 문법을 알고, 유니티(C#)를 통해 객체지향 개념을 잡고 있었기에 이 앱도 완성될 수 있었다.
2. 왜 여전히 '개발자'가 필요한가
AI는 편리하지만 치명적인 단점이 있다. 앱이 커지면 AI는 이전에 짰던 로직들을 하나둘 잊어버리거나 배제하기 시작하고, 결국 거기서 심각한 버그가 터진다.
기획서의 의도에 맞게 전체 클래스를 설계하고, 유지보수가 가능한 구조를 짜는 건 여전히 사람의 몫이다. 이번 프로젝트에서 내가 직접 한 일들도 바로 그런 것들이다.
- 아키텍처 설계: 로그인 여부에 따라 DB를 갈아끼우는 Repository 패턴 도입
- 코드 가독성 리팩토링: AI가 뭉뚱그려 짠 코드를 위젯과 함수 단위로 분리
- 버그 체커: AI가 불필요하게 구현하거나 놓친 예외 상황들을 체크
위젯이 뭔지, 추상 클래스가 뭔지 모르는 상태에서 AI에게 떠맡기기만 했다면, 아마 이 앱은 중간에 스파게티 코드가 되어 터져버렸을 것이다.
3. 반성: 구조에 대한 아쉬움
나름 유지보수를 생각해서 Database, Screen, Widget, Service 같이 카테고리별로 폴더를 나누어 배치했다. 그런데 파일이 많아지다 보니 정작 필요한 파일을 찾기가 점점 힘들어졌다.
예전에 Spring 프로젝트를 할 때는 엔티티(Entity)별로 나눠서 관리하는 게 편했는데, Flutter는 기능(Feature)이나 화면(Screen)별로 로직과 위젯을 묶어서 관리하는 게 훨씬 효율적일 것 같다는 생각이 든다. 다음 프로젝트 때는 이 '폴더 구조'부터 제대로 잡고 시작해야겠다.
💡 이번 시리즈의 핵심 요약:
- 기획의 본질: AI는 도구일 뿐, 목적지는 사람이 정해야 한다.
- 기술의 융합: 유니티의 생명주기 개념이 플러터 이해에 큰 도움이 됐다.
- 구조의 중요성: 바이브 코딩을 하더라도 '유지보수'를 위한 아키텍처는 필수다.
개인적으로 매우 만족스러운 개발이였고, 취업하고싶다..
'프로젝트' 카테고리의 다른 글
| [Flutter] AI로 노래가사 해석하는 앱개발 - 2 (0) | 2026.01.25 |
|---|---|
| [Flutter] AI로 노래가사 해석하는 앱개발 (4) | 2026.01.18 |