#2 - Java에서 Flutter로

159커밋의 Java 프로토타입 → 부스에서 받은 피드백 → 크로스플랫폼의 필요성 → 기숙사에서 2주간 시간표 로직 개발

학교 사업 프로그램

학교에서 학생들의 창업이나 프로젝트를 지원하는 프로그램이 있었다. 아이디어를 제출하고, 팀을 꾸려서 발표하고, 선정되면 활동비와 멘토링을 지원받는 구조였다.

팀원 두 명과 함께 “학교 통합 앱"이라는 주제로 지원했다. 급식, 시간표, 공지사항을 한곳에서 볼 수 있는 앱. 매번 학교 홈페이지에 들어가야 하는 불편함을 해결하자는 단순한 아이디어였고, 다행히 선정되었다.

Java + XML 프로토타입

당시 나는 Java를 조금 공부한 상태였다. 모바일 개발 경험은 전혀 없었지만, Java를 알고 있으니 Android 네이티브가 가장 접근하기 쉬워 보였다. 그래서 첫 프로토타입은 Java + XML Layout으로 만들었다.

159커밋의 기록

이 프로토타입은 단순한 데모가 아니었다. GitHub에 159개의 커밋이 남아 있을 정도로 꽤 오랜 기간 개발했다. v0.12.3 Beta까지 버전이 올라갔다.

구현했던 기능들:

  • 급식 정보 — NEIS 공공데이터 API를 파싱해서 오늘/이번 주 급식을 보여줌. 영양정보까지 표시
  • 시간표 조회 — NEIS API로 시간표 데이터를 가져와서 요일별로 표시
  • 공지사항 — 학교 공지를 앱에서 확인할 수 있는 Fragment 구현
  • 알림 — 매일 급식 알림을 보내주는 기능

개발하면서 겪었던 문제들도 많았다. FutureTask에서 CompletableFuture로 비동기 처리 방식을 전환하기도 했고, UI 디자인을 대규모로 리팩토링한 적도 있었다 (v0.11.5 → v0.12.3 구간). 투박한 첫 UI에서 점점 나아지는 과정이 커밋 히스토리에 그대로 남아 있다.

하지만 이 프로토타입의 가장 큰 가치는 실제로 NEIS API를 호출해서 데이터를 가져오고 화면에 보여줄 수 있다는 걸 증명한 것이었다. 학교 앱이라는 아이디어가 기술적으로 가능하다는 확신을 얻었다.

발표와 부스

프로토타입이 어느 정도 완성된 후, 참여 팀들과 선생님 앞에서 발표하는 자리가 있었다. 발표는 팀원이 맡았다. 앱의 컨셉, 해결하려는 문제, 현재 구현된 기능을 설명하고 실제 동작하는 프로토타입을 시연했다.

그리고 학교 메인 홀에서 부스를 열었다.

부스를 운영하면서 직접 학생들을 만났다. 관심을 가지는 학생도 있었고, “이런 게 왜 필요해?” 하는 반응도 있었다. 하지만 가장 중요한 건, 부스에서 두 가지 결정적인 사실을 알게 되었다는 것이다.

1. Android와 iOS, 거의 반반

부스에 태블릿을 놓고 직접 만든 사전등록 앱을 실행해뒀다. 학번, 이름, 전화번호, 사용 중인 OS를 입력하는 간단한 폼이었는데, OS 필드를 집계해보니 Android와 iOS가 거의 반반이었다.

이건 심각한 문제였다. 지금까지 Java + XML로 Android 앱만 만들고 있었는데, 그러면 학교 학생 절반이 이 앱을 쓸 수 없다는 뜻이다.

그렇다고 Swift로 iOS 버전을 따로 만들 수 있는 상황이 아니었다. 나는 Java를 조금 아는 1학년이었고, 새로운 언어와 완전히 다른 플랫폼을 동시에 학습하면서 두 벌의 앱을 유지보수한다? 비현실적이었다.

이때 처음으로 크로스플랫폼 프레임워크를 진지하게 고민하기 시작했다. React Native와 Flutter가 후보였는데, 당시 Flutter의 성장세가 가파르기도 했고, Dart 언어가 Java에서 넘어오기에 비교적 친숙해 보였다.

2. “2, 3학년은 시간표가 다른데요?”

부스에서 받은 질문 중 하나가 프로젝트의 방향을 바꿨다.

“1학년은 반만 입력하면 되는데, 2학년부터는 선택과목 때문에 시간표가 다 달라요.”

1학년인 나는 이걸 몰랐다. 1학년은 학년과 반을 입력하면 시간표가 그대로 확정된다. 같은 반 학생은 모두 같은 시간표를 따른다.

하지만 2·3학년은 완전히 다른 세계였다. 선택과목 제도 때문에 같은 반이라도 학생마다 시간표가 다르다. A는 3교시에 물리학을, B는 같은 시간에 생명과학을 듣는다. 그리고 그 수업은 각각 다른 반 교실에서 진행된다.

NEIS API에서 제공하는 시간표 데이터는 반(CLASS_NM)별로 내려온다. 1학년이라면 자기 반 데이터만 가져오면 끝이지만, 2·3학년의 선택과목 시간에는 여러 반의 수업이 뒤섞여 있었다. 단순히 API를 호출해서 보여주는 것으로는 해결이 안 되는, 로직이 필요한 문제였다.

이걸 들었을 때 솔직히 막막했다. 하지만 동시에 “이걸 해결하면 진짜 쓸 수 있는 앱이 되겠다"는 생각이 들었다.

기숙사에서 2주

당시 기숙사에 살고 있었다. 평일 저녁 자습 시간과 주말을 온전히 개발에 쏟을 수 있는 환경이었다. 부스에서 받은 시간표 피드백을 해결하겠다는 목표를 잡고, 2주를 잡았다. 1주는 기획, 1주는 구현.

개발을 시작한 지 얼마 안 된 상태에서 이 로직을 만들어야 했기 때문에, 쉽지 않을 거라는 건 알고 있었다.

1주차: NEIS API 분석과 로직 기획

먼저 NEIS API의 시간표 데이터 구조를 철저히 분석했다. API를 호출하면 이런 형태의 데이터가 온다:

1
2
3
4
5
6
7
{
  "ALL_TI_YMD": "20260414",
  "GRADE": "2",
  "CLASS_NM": "3",
  "PERIO": "4",
  "ITRT_CNTNT": "물리학Ⅰ"
}

날짜, 학년, 반, 교시, 과목명. 이걸 조합해서 개인 시간표를 만들어야 했다.

핵심 문제를 정리하면:

학년시간표 결정 방식
1학년학년 + 반 입력 → 시간표 확정 (단순 조회)
2·3학년학년 전체 시간표에서 반별 과목 추출 → 사용자가 선택과목 선택 → 해당 과목의 반·교시 매칭 → 커스텀 시간표 생성

2·3학년의 흐름을 더 구체적으로 설계했다:

  1. getTimeTable() — NEIS API에서 해당 학년의 전체 시간표를 가져온다. 반 필터 없이 전체를 요청해서, 모든 반의 모든 과목 데이터를 확보한다.

  2. getAllSubjectCombinations() — 가져온 전체 시간표에서 반별 과목 조합을 추출한다. “3반 4교시 = 물리학Ⅰ”, “5반 4교시 = 생명과학Ⅰ” 같은 매핑을 만든다. 이걸로 사용자에게 “어떤 과목을 듣는지” 선택지를 보여줄 수 있다.

  3. 사용자가 자기 선택과목을 고른다 — UI에서 과목 목록을 보여주고, 자기가 듣는 과목을 체크한다.

  4. getCustomTimeTable() — 선택한 과목이 어느 반의 몇 교시에 있는지 역추적해서, 그 학생만의 개인 시간표를 생성한다.

📎 시간표 API의 전체 구조는 TimetableDataApi 문서에서 확인할 수 있다.

일주일 동안 노트에 데이터 흐름을 그리고 또 그렸다. API 응답 구조부터 최종 시간표까지의 변환 과정을 머릿속에서 완전히 정리한 후에야 코드를 쓸 수 있겠다는 확신이 들었다.

2주차: 구현

기획대로 코드를 작성했다. 처음이라 삽질도 많았지만, 기획을 확실히 해둔 덕분에 방향을 잃지는 않았다.

까다로웠던 부분들:

반별 과목 파싱

NEIS API 응답에서 날짜별 → 반별 → 교시별 과목을 3중 중첩 Map으로 정리해야 했다.

1
2
// 날짜 → 반번호 → [1교시, 2교시, 3교시, ...]
Map<String, Map<String, List<String>>>

API에서 넘어오는 데이터는 flat한 배열이라, 이걸 날짜와 반으로 그룹핑하고, 교시 순서대로 정렬해서 넣어야 했다. 교시(PERIO) 사이에 빈 시간이 있을 수도 있어서, 리스트를 교시 수만큼 미리 할당하고 해당 인덱스에 과목명을 넣는 방식으로 처리했다.

1
2
3
4
while (classList.length < perio) {
  classList.add('');
}
classList[perio - 1] = content;

선택과목 시간표를 처음 구상했을 때의 핵심 아이디어가 바로 이것이었다. 빈 2차원 배열(요일 × 교시)을 먼저 만들어놓고, 사용자가 선택한 과목을 해당 위치에 채워 넣는 방식. 빈 시간표라는 틀을 먼저 준비하고, 거기에 자기 과목을 하나씩 배치한다는 발상이 전체 로직의 출발점이었다.

선택과목 매칭

사용자가 고른 과목명이 어느 반의 몇 교시에 해당하는지 역추적하는 게 핵심이었다. 같은 과목이 여러 반에 걸쳐 있을 수 있다. 예를 들어 “물리학Ⅰ"이 3반에서도, 7반에서도 진행될 수 있다.

이 문제를 해결하기 위해 Subject 모델subjectClass(반 번호)를 포함시켰다. 단순히 과목명만으로는 부족하고, 어느 반의 물리학인지까지 알아야 정확한 시간표를 만들 수 있었다.

1
2
3
4
Subject(
  subjectName: "물리학Ⅰ",
  subjectClass: 3,  // 3반에서 진행되는 물리학
)

커스텀 시간표를 만들 때는, 사용자가 선택한 과목 목록을 반별로 그룹핑한 뒤, 해당 반의 시간표에서 매칭되는 교시를 찾아서 개인 시간표에 채워 넣었다.

폴백 전략

시간표가 항상 있는 건 아니다. 방학 중에는 시간표 데이터가 없고, 시험 기간에는 특별 시간표가 올라오기도 한다. 이번 주 데이터가 없으면 앱이 빈 화면을 보여주는 건 좋지 않았다.

그래서 3단계 폴백을 구현했다:

  1. 이번 주 시간표를 가져온다
  2. 없으면 다음 주 시간표를 가져온다
  3. 그래도 없으면 지난 주 시간표를 가져온다

이렇게 하면 방학 직전이나 직후에도 가장 가까운 시간표를 보여줄 수 있었다.

캐싱

NEIS API를 매번 호출하면 느리고, 불필요한 네트워크 요청이 늘어난다. SharedPreferences에 캐싱을 구현했다.

  • 12시간 이내: 캐시 데이터를 바로 반환 (네트워크 요청 없음)
  • 12시간 ~ 3일: Stale-While-Revalidate — 일단 캐시를 반환하되, 다음 요청 때 갱신
  • 3일 초과: 캐시 삭제 후 새로 요청

SWR 패턴을 적용한 건 나중의 일이지만, 캐싱의 기본 개념은 이때 처음 구현했다. 네트워크 상태가 좋지 않은 학교 와이파이 환경에서 캐싱이 얼마나 중요한지 실감했다.

159커밋을 버리는 결정

시간표 로직을 완성한 뒤, 프로젝트 전체를 Flutter로 전환하기로 결정했다.

159개의 커밋이 쌓인 Java 프로젝트를 버리는 건 쉬운 결정이 아니었다. 급식 파싱, 시간표 조회, UI 리팩토링까지 수개월의 작업이 담겨 있었다. 하지만 부스에서 확인한 현실이 명확했다.

  • 학교 학생의 절반이 iOS를 쓴다 → Android만으로는 안 된다
  • Swift로 iOS를 따로 만들 여력이 없다 → 크로스플랫폼이 필수
  • Flutter는 Dart 하나로 양쪽을 커버한다 → 유일한 현실적 선택지

그리고 Java 프로토타입에서 이미 해결한 문제들 — NEIS API 파싱, 시간표 로직, 캐싱 전략 — 은 코드는 버려도 설계와 경험은 그대로 가져갈 수 있었다. 실제로 Flutter로 다시 작성할 때 Java에서 삽질했던 부분들은 훨씬 빠르게 구현할 수 있었다.

Java 프로토타입은 GitHub에 그대로 남겨두었다. v0.12.3 Beta, 159커밋. 여기까지가 이 앱의 선사시대다.

Flutter로의 전환

Dart는 Java와 문법이 비슷해서 언어 자체의 진입 장벽은 낮았다. 하지만 Flutter의 위젯 트리 개념은 XML Layout과 완전히 달랐다. XML에서는 화면을 선언적으로 정의하지만 로직과 분리되어 있었고, Flutter에서는 UI 자체가 코드다.

처음에는 어색했지만, 익숙해지니 오히려 편했다. 특히 핫 리로드 — 코드를 수정하면 앱을 재시작하지 않아도 바로 화면에 반영된다 — 가 생산성을 극적으로 올려줬다. Java + XML 시절에는 빌드하고 에뮬레이터에 올리고 기다리는 시간이 길었는데, 그 시간이 거의 0으로 줄었다.

시간표 로직도 Dart로 다시 작성했다. Java에서의 경험이 있으니 설계는 이미 머릿속에 있었고, Dart의 컬렉션 API가 Java보다 간결해서 코드량도 줄었다. Map, List, Set의 함수형 메서드들이 데이터 파싱에 특히 유용했다.

돌아보면

부스에서 받은 피드백 두 개가 프로젝트의 방향을 결정했다.

  • “iOS는 안 돼요?” → Flutter 전환
  • “2학년 시간표는요?” → 선택과목 커스텀 시간표 로직

사용자를 직접 만나는 게 얼마나 중요한지 체감한 순간이었다. 혼자 방에서 코딩만 했으면 절대 알 수 없었을 것들이다. 기획서를 아무리 잘 써도, 실제 사용자 앞에서 프로토타입을 보여주는 것만큼 확실한 검증은 없다.

그리고 159커밋을 버린 건 아깝지만, 프로토타입의 목적은 원래 “버리기 위해 만드는 것"이다. 프로토타입에서 얻은 기술적 경험과 사용자 피드백이 Flutter 버전의 기초가 되었으니, 하나도 낭비된 게 아니었다.

다음 글에서는

Flutter 전환 직후 가장 먼저 만들고 싶었던 기능, 급식 알림. 단순해 보였던 이 기능을 제대로 만드는 데 1년이 걸린 이야기를 다룬다.