<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>크로스플랫폼 on monkshark.dev</title><link>https://monkshark.github.io/tags/%ED%81%AC%EB%A1%9C%EC%8A%A4%ED%94%8C%EB%9E%AB%ED%8F%BC/</link><description>Recent content in 크로스플랫폼 on monkshark.dev</description><generator>Hugo -- gohugo.io</generator><language>ko</language><lastBuildDate>Mon, 06 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://monkshark.github.io/tags/%ED%81%AC%EB%A1%9C%EC%8A%A4%ED%94%8C%EB%9E%AB%ED%8F%BC/index.xml" rel="self" type="application/rss+xml"/><item><title>#2 - Java에서 Flutter로</title><link>https://monkshark.github.io/p/java-to-flutter/</link><pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate><guid>https://monkshark.github.io/p/java-to-flutter/</guid><description>&lt;h2 id="학교-사업-프로그램"&gt;&lt;a href="#%ed%95%99%ea%b5%90-%ec%82%ac%ec%97%85-%ed%94%84%eb%a1%9c%ea%b7%b8%eb%9e%a8" class="header-anchor"&gt;&lt;/a&gt;학교 사업 프로그램
&lt;/h2&gt;&lt;p&gt;학교에서 학생들의 창업이나 프로젝트를 지원하는 프로그램이 있었다. 아이디어를 제출하고, 팀을 꾸려서 발표하고, 선정되면 활동비와 멘토링을 지원받는 구조였다.&lt;/p&gt;
&lt;p&gt;팀원 두 명과 함께 &amp;ldquo;학교 통합 앱&amp;quot;이라는 주제로 지원했다. 급식, 시간표, 공지사항을 한곳에서 볼 수 있는 앱. 매번 학교 홈페이지에 들어가야 하는 불편함을 해결하자는 단순한 아이디어였고, 다행히 선정되었다.&lt;/p&gt;
&lt;h2 id="java--xml-프로토타입"&gt;&lt;a href="#java--xml-%ed%94%84%eb%a1%9c%ed%86%a0%ed%83%80%ec%9e%85" class="header-anchor"&gt;&lt;/a&gt;Java + XML 프로토타입
&lt;/h2&gt;&lt;p&gt;당시 나는 Java를 조금 공부한 상태였다. 모바일 개발 경험은 전혀 없었지만, Java를 알고 있으니 Android 네이티브가 가장 접근하기 쉬워 보였다. 그래서 첫 프로토타입은 Java + XML Layout으로 만들었다.&lt;/p&gt;
&lt;h3 id="159커밋의-기록"&gt;&lt;a href="#159%ec%bb%a4%eb%b0%8b%ec%9d%98-%ea%b8%b0%eb%a1%9d" class="header-anchor"&gt;&lt;/a&gt;159커밋의 기록
&lt;/h3&gt;&lt;p&gt;이 프로토타입은 단순한 데모가 아니었다. &lt;a class="link" href="https://github.com/Monkshark/hansol_hs_java_app" target="_blank" rel="noopener"
 &gt;GitHub에 159개의 커밋&lt;/a&gt;이 남아 있을 정도로 꽤 오랜 기간 개발했다. v0.12.3 Beta까지 버전이 올라갔다.&lt;/p&gt;
&lt;p&gt;구현했던 기능들:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;급식 정보&lt;/strong&gt; — NEIS 공공데이터 API를 파싱해서 오늘/이번 주 급식을 보여줌. 영양정보까지 표시&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;시간표 조회&lt;/strong&gt; — NEIS API로 시간표 데이터를 가져와서 요일별로 표시&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;공지사항&lt;/strong&gt; — 학교 공지를 앱에서 확인할 수 있는 Fragment 구현&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;알림&lt;/strong&gt; — 매일 급식 알림을 보내주는 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;개발하면서 겪었던 문제들도 많았다. &lt;code&gt;FutureTask&lt;/code&gt;에서 &lt;code&gt;CompletableFuture&lt;/code&gt;로 비동기 처리 방식을 전환하기도 했고, UI 디자인을 대규모로 리팩토링한 적도 있었다 (v0.11.5 → v0.12.3 구간). 투박한 첫 UI에서 점점 나아지는 과정이 커밋 히스토리에 그대로 남아 있다.&lt;/p&gt;
&lt;p&gt;하지만 이 프로토타입의 가장 큰 가치는 &lt;strong&gt;실제로 NEIS API를 호출해서 데이터를 가져오고 화면에 보여줄 수 있다&lt;/strong&gt;는 걸 증명한 것이었다. 학교 앱이라는 아이디어가 기술적으로 가능하다는 확신을 얻었다.&lt;/p&gt;
&lt;h2 id="발표와-부스"&gt;&lt;a href="#%eb%b0%9c%ed%91%9c%ec%99%80-%eb%b6%80%ec%8a%a4" class="header-anchor"&gt;&lt;/a&gt;발표와 부스
&lt;/h2&gt;&lt;p&gt;프로토타입이 어느 정도 완성된 후, 참여 팀들과 선생님 앞에서 발표하는 자리가 있었다. 발표는 팀원이 맡았다. 앱의 컨셉, 해결하려는 문제, 현재 구현된 기능을 설명하고 실제 동작하는 프로토타입을 시연했다.&lt;/p&gt;
&lt;p&gt;그리고 학교 메인 홀에서 부스를 열었다.&lt;/p&gt;
&lt;p&gt;부스를 운영하면서 직접 학생들을 만났다. 관심을 가지는 학생도 있었고, &amp;ldquo;이런 게 왜 필요해?&amp;rdquo; 하는 반응도 있었다. 하지만 가장 중요한 건, 부스에서 &lt;strong&gt;두 가지 결정적인 사실&lt;/strong&gt;을 알게 되었다는 것이다.&lt;/p&gt;
&lt;h3 id="1-android와-ios-거의-반반"&gt;&lt;a href="#1-android%ec%99%80-ios-%ea%b1%b0%ec%9d%98-%eb%b0%98%eb%b0%98" class="header-anchor"&gt;&lt;/a&gt;1. Android와 iOS, 거의 반반
&lt;/h3&gt;&lt;p&gt;부스에 태블릿을 놓고 직접 만든 사전등록 앱을 실행해뒀다. 학번, 이름, 전화번호, 사용 중인 OS를 입력하는 간단한 폼이었는데, OS 필드를 집계해보니 Android와 iOS가 거의 반반이었다.&lt;/p&gt;
&lt;p&gt;이건 심각한 문제였다. 지금까지 Java + XML로 Android 앱만 만들고 있었는데, 그러면 &lt;strong&gt;학교 학생 절반이 이 앱을 쓸 수 없다&lt;/strong&gt;는 뜻이다.&lt;/p&gt;
&lt;p&gt;그렇다고 Swift로 iOS 버전을 따로 만들 수 있는 상황이 아니었다. 나는 Java를 조금 아는 1학년이었고, 새로운 언어와 완전히 다른 플랫폼을 동시에 학습하면서 두 벌의 앱을 유지보수한다? 비현실적이었다.&lt;/p&gt;
&lt;p&gt;이때 처음으로 &lt;strong&gt;크로스플랫폼 프레임워크&lt;/strong&gt;를 진지하게 고민하기 시작했다. React Native와 Flutter가 후보였는데, 당시 Flutter의 성장세가 가파르기도 했고, Dart 언어가 Java에서 넘어오기에 비교적 친숙해 보였다.&lt;/p&gt;
&lt;h3 id="2-2-3학년은-시간표가-다른데요"&gt;&lt;a href="#2-2-3%ed%95%99%eb%85%84%ec%9d%80-%ec%8b%9c%ea%b0%84%ed%91%9c%ea%b0%80-%eb%8b%a4%eb%a5%b8%eb%8d%b0%ec%9a%94" class="header-anchor"&gt;&lt;/a&gt;2. &amp;ldquo;2, 3학년은 시간표가 다른데요?&amp;rdquo;
&lt;/h3&gt;&lt;p&gt;부스에서 받은 질문 중 하나가 프로젝트의 방향을 바꿨다.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&amp;ldquo;1학년은 반만 입력하면 되는데, 2학년부터는 선택과목 때문에 시간표가 다 달라요.&amp;rdquo;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;1학년인 나는 이걸 몰랐다. 1학년은 학년과 반을 입력하면 시간표가 그대로 확정된다. 같은 반 학생은 모두 같은 시간표를 따른다.&lt;/p&gt;
&lt;p&gt;하지만 2·3학년은 완전히 다른 세계였다. 선택과목 제도 때문에 &lt;strong&gt;같은 반이라도 학생마다 시간표가 다르다.&lt;/strong&gt; A는 3교시에 물리학을, B는 같은 시간에 생명과학을 듣는다. 그리고 그 수업은 각각 다른 반 교실에서 진행된다.&lt;/p&gt;
&lt;p&gt;NEIS API에서 제공하는 시간표 데이터는 반(CLASS_NM)별로 내려온다. 1학년이라면 자기 반 데이터만 가져오면 끝이지만, 2·3학년의 선택과목 시간에는 여러 반의 수업이 뒤섞여 있었다. 단순히 API를 호출해서 보여주는 것으로는 해결이 안 되는, &lt;strong&gt;로직이 필요한 문제&lt;/strong&gt;였다.&lt;/p&gt;
&lt;p&gt;이걸 들었을 때 솔직히 막막했다. 하지만 동시에 &amp;ldquo;이걸 해결하면 진짜 쓸 수 있는 앱이 되겠다&amp;quot;는 생각이 들었다.&lt;/p&gt;
&lt;h2 id="기숙사에서-2주"&gt;&lt;a href="#%ea%b8%b0%ec%88%99%ec%82%ac%ec%97%90%ec%84%9c-2%ec%a3%bc" class="header-anchor"&gt;&lt;/a&gt;기숙사에서 2주
&lt;/h2&gt;&lt;p&gt;당시 기숙사에 살고 있었다. 평일 저녁 자습 시간과 주말을 온전히 개발에 쏟을 수 있는 환경이었다. 부스에서 받은 시간표 피드백을 해결하겠다는 목표를 잡고, 2주를 잡았다. 1주는 기획, 1주는 구현.&lt;/p&gt;
&lt;p&gt;개발을 시작한 지 얼마 안 된 상태에서 이 로직을 만들어야 했기 때문에, 쉽지 않을 거라는 건 알고 있었다.&lt;/p&gt;
&lt;h3 id="1주차-neis-api-분석과-로직-기획"&gt;&lt;a href="#1%ec%a3%bc%ec%b0%a8-neis-api-%eb%b6%84%ec%84%9d%ea%b3%bc-%eb%a1%9c%ec%a7%81-%ea%b8%b0%ed%9a%8d" class="header-anchor"&gt;&lt;/a&gt;1주차: NEIS API 분석과 로직 기획
&lt;/h3&gt;&lt;p&gt;먼저 NEIS API의 시간표 데이터 구조를 철저히 분석했다. API를 호출하면 이런 형태의 데이터가 온다:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;ALL_TI_YMD&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;20260414&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;GRADE&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;CLASS_NM&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;3&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;PERIO&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;4&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;ITRT_CNTNT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;물리학Ⅰ&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;날짜, 학년, 반, 교시, 과목명. 이걸 조합해서 개인 시간표를 만들어야 했다.&lt;/p&gt;
&lt;p&gt;핵심 문제를 정리하면:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;학년&lt;/th&gt;
 &lt;th&gt;시간표 결정 방식&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;1학년&lt;/td&gt;
 &lt;td&gt;학년 + 반 입력 → 시간표 확정 (단순 조회)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2·3학년&lt;/td&gt;
 &lt;td&gt;학년 전체 시간표에서 반별 과목 추출 → 사용자가 선택과목 선택 → 해당 과목의 반·교시 매칭 → 커스텀 시간표 생성&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;2·3학년의 흐름을 더 구체적으로 설계했다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a class="link" href="https://monkshark.github.io/hansol_hs_flutter_app/#api/timetable_data_api.md" target="_blank" rel="noopener"
 &gt;&lt;code&gt;getTimeTable()&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — NEIS API에서 해당 학년의 전체 시간표를 가져온다. 반 필터 없이 전체를 요청해서, 모든 반의 모든 과목 데이터를 확보한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;getAllSubjectCombinations()&lt;/code&gt;&lt;/strong&gt; — 가져온 전체 시간표에서 반별 과목 조합을 추출한다. &amp;ldquo;3반 4교시 = 물리학Ⅰ&amp;rdquo;, &amp;ldquo;5반 4교시 = 생명과학Ⅰ&amp;rdquo; 같은 매핑을 만든다. 이걸로 사용자에게 &amp;ldquo;어떤 과목을 듣는지&amp;rdquo; 선택지를 보여줄 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;사용자가 자기 선택과목을 고른다&lt;/strong&gt; — UI에서 과목 목록을 보여주고, 자기가 듣는 과목을 체크한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;getCustomTimeTable()&lt;/code&gt;&lt;/strong&gt; — 선택한 과목이 어느 반의 몇 교시에 있는지 역추적해서, 그 학생만의 개인 시간표를 생성한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

 &lt;blockquote&gt;
 &lt;p&gt;📎 시간표 API의 전체 구조는 &lt;a class="link" href="https://monkshark.github.io/hansol_hs_flutter_app/#api/timetable_data_api.md" target="_blank" rel="noopener"
 &gt;TimetableDataApi 문서&lt;/a&gt;에서 확인할 수 있다.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;일주일 동안 노트에 데이터 흐름을 그리고 또 그렸다. API 응답 구조부터 최종 시간표까지의 변환 과정을 머릿속에서 완전히 정리한 후에야 코드를 쓸 수 있겠다는 확신이 들었다.&lt;/p&gt;
&lt;h3 id="2주차-구현"&gt;&lt;a href="#2%ec%a3%bc%ec%b0%a8-%ea%b5%ac%ed%98%84" class="header-anchor"&gt;&lt;/a&gt;2주차: 구현
&lt;/h3&gt;&lt;p&gt;기획대로 코드를 작성했다. 처음이라 삽질도 많았지만, 기획을 확실히 해둔 덕분에 방향을 잃지는 않았다.&lt;/p&gt;
&lt;p&gt;까다로웠던 부분들:&lt;/p&gt;
&lt;h4 id="반별-과목-파싱"&gt;&lt;a href="#%eb%b0%98%eb%b3%84-%ea%b3%bc%eb%aa%a9-%ed%8c%8c%ec%8b%b1" class="header-anchor"&gt;&lt;/a&gt;반별 과목 파싱
&lt;/h4&gt;&lt;p&gt;NEIS API 응답에서 날짜별 → 반별 → 교시별 과목을 &lt;strong&gt;3중 중첩 Map&lt;/strong&gt;으로 정리해야 했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-dart" data-lang="dart"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 날짜 → 반번호 → [1교시, 2교시, 3교시, ...]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;API에서 넘어오는 데이터는 flat한 배열이라, 이걸 날짜와 반으로 그룹핑하고, 교시 순서대로 정렬해서 넣어야 했다. 교시(PERIO) 사이에 빈 시간이 있을 수도 있어서, 리스트를 교시 수만큼 미리 할당하고 해당 인덱스에 과목명을 넣는 방식으로 처리했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-dart" data-lang="dart"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;perio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;perio&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;선택과목 시간표를 처음 구상했을 때의 핵심 아이디어가 바로 이것이었다. &lt;strong&gt;빈 2차원 배열(요일 × 교시)을 먼저 만들어놓고, 사용자가 선택한 과목을 해당 위치에 채워 넣는 방식.&lt;/strong&gt; 빈 시간표라는 틀을 먼저 준비하고, 거기에 자기 과목을 하나씩 배치한다는 발상이 전체 로직의 출발점이었다.&lt;/p&gt;
&lt;h4 id="선택과목-매칭"&gt;&lt;a href="#%ec%84%a0%ed%83%9d%ea%b3%bc%eb%aa%a9-%eb%a7%a4%ec%b9%ad" class="header-anchor"&gt;&lt;/a&gt;선택과목 매칭
&lt;/h4&gt;&lt;p&gt;사용자가 고른 과목명이 어느 반의 몇 교시에 해당하는지 역추적하는 게 핵심이었다. 같은 과목이 여러 반에 걸쳐 있을 수 있다. 예를 들어 &amp;ldquo;물리학Ⅰ&amp;quot;이 3반에서도, 7반에서도 진행될 수 있다.&lt;/p&gt;
&lt;p&gt;이 문제를 해결하기 위해 &lt;a class="link" href="https://monkshark.github.io/hansol_hs_flutter_app/#data/subject.md" target="_blank" rel="noopener"
 &gt;&lt;code&gt;Subject&lt;/code&gt; 모델&lt;/a&gt;에 &lt;code&gt;subjectClass&lt;/code&gt;(반 번호)를 포함시켰다. 단순히 과목명만으로는 부족하고, &lt;strong&gt;어느 반의 물리학인지&lt;/strong&gt;까지 알아야 정확한 시간표를 만들 수 있었다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-dart" data-lang="dart"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nl"&gt;subjectName:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;물리학Ⅰ&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nl"&gt;subjectClass:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 3반에서 진행되는 물리학
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;커스텀 시간표를 만들 때는, 사용자가 선택한 과목 목록을 반별로 그룹핑한 뒤, 해당 반의 시간표에서 매칭되는 교시를 찾아서 개인 시간표에 채워 넣었다.&lt;/p&gt;
&lt;h4 id="폴백-전략"&gt;&lt;a href="#%ed%8f%b4%eb%b0%b1-%ec%a0%84%eb%9e%b5" class="header-anchor"&gt;&lt;/a&gt;폴백 전략
&lt;/h4&gt;&lt;p&gt;시간표가 항상 있는 건 아니다. 방학 중에는 시간표 데이터가 없고, 시험 기간에는 특별 시간표가 올라오기도 한다. 이번 주 데이터가 없으면 앱이 빈 화면을 보여주는 건 좋지 않았다.&lt;/p&gt;
&lt;p&gt;그래서 3단계 폴백을 구현했다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;이번 주&lt;/strong&gt; 시간표를 가져온다&lt;/li&gt;
&lt;li&gt;없으면 &lt;strong&gt;다음 주&lt;/strong&gt; 시간표를 가져온다&lt;/li&gt;
&lt;li&gt;그래도 없으면 &lt;strong&gt;지난 주&lt;/strong&gt; 시간표를 가져온다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이렇게 하면 방학 직전이나 직후에도 가장 가까운 시간표를 보여줄 수 있었다.&lt;/p&gt;
&lt;h4 id="캐싱"&gt;&lt;a href="#%ec%ba%90%ec%8b%b1" class="header-anchor"&gt;&lt;/a&gt;캐싱
&lt;/h4&gt;&lt;p&gt;NEIS API를 매번 호출하면 느리고, 불필요한 네트워크 요청이 늘어난다. SharedPreferences에 캐싱을 구현했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;12시간 이내&lt;/strong&gt;: 캐시 데이터를 바로 반환 (네트워크 요청 없음)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;12시간 ~ 3일&lt;/strong&gt;: Stale-While-Revalidate — 일단 캐시를 반환하되, 다음 요청 때 갱신&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3일 초과&lt;/strong&gt;: 캐시 삭제 후 새로 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SWR 패턴을 적용한 건 나중의 일이지만, 캐싱의 기본 개념은 이때 처음 구현했다. 네트워크 상태가 좋지 않은 학교 와이파이 환경에서 캐싱이 얼마나 중요한지 실감했다.&lt;/p&gt;
&lt;h2 id="159커밋을-버리는-결정"&gt;&lt;a href="#159%ec%bb%a4%eb%b0%8b%ec%9d%84-%eb%b2%84%eb%a6%ac%eb%8a%94-%ea%b2%b0%ec%a0%95" class="header-anchor"&gt;&lt;/a&gt;159커밋을 버리는 결정
&lt;/h2&gt;&lt;p&gt;시간표 로직을 완성한 뒤, 프로젝트 전체를 Flutter로 전환하기로 결정했다.&lt;/p&gt;
&lt;p&gt;159개의 커밋이 쌓인 Java 프로젝트를 버리는 건 쉬운 결정이 아니었다. 급식 파싱, 시간표 조회, UI 리팩토링까지 수개월의 작업이 담겨 있었다. 하지만 부스에서 확인한 현실이 명확했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;학교 학생의 절반이 iOS를 쓴다 → &lt;strong&gt;Android만으로는 안 된다&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Swift로 iOS를 따로 만들 여력이 없다 → &lt;strong&gt;크로스플랫폼이 필수&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Flutter는 Dart 하나로 양쪽을 커버한다 → &lt;strong&gt;유일한 현실적 선택지&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그리고 Java 프로토타입에서 이미 해결한 문제들 — NEIS API 파싱, 시간표 로직, 캐싱 전략 — 은 코드는 버려도 &lt;strong&gt;설계와 경험은 그대로 가져갈 수 있었다.&lt;/strong&gt; 실제로 Flutter로 다시 작성할 때 Java에서 삽질했던 부분들은 훨씬 빠르게 구현할 수 있었다.&lt;/p&gt;
&lt;p&gt;Java 프로토타입은 &lt;a class="link" href="https://github.com/Monkshark/hansol_hs_java_app" target="_blank" rel="noopener"
 &gt;GitHub&lt;/a&gt;에 그대로 남겨두었다. v0.12.3 Beta, 159커밋. 여기까지가 이 앱의 선사시대다.&lt;/p&gt;
&lt;h2 id="flutter로의-전환"&gt;&lt;a href="#flutter%eb%a1%9c%ec%9d%98-%ec%a0%84%ed%99%98" class="header-anchor"&gt;&lt;/a&gt;Flutter로의 전환
&lt;/h2&gt;&lt;p&gt;Dart는 Java와 문법이 비슷해서 언어 자체의 진입 장벽은 낮았다. 하지만 Flutter의 위젯 트리 개념은 XML Layout과 완전히 달랐다. XML에서는 화면을 선언적으로 정의하지만 로직과 분리되어 있었고, Flutter에서는 UI 자체가 코드다.&lt;/p&gt;
&lt;p&gt;처음에는 어색했지만, 익숙해지니 오히려 편했다. 특히 &lt;strong&gt;핫 리로드&lt;/strong&gt; — 코드를 수정하면 앱을 재시작하지 않아도 바로 화면에 반영된다 — 가 생산성을 극적으로 올려줬다. Java + XML 시절에는 빌드하고 에뮬레이터에 올리고 기다리는 시간이 길었는데, 그 시간이 거의 0으로 줄었다.&lt;/p&gt;
&lt;p&gt;시간표 로직도 Dart로 다시 작성했다. Java에서의 경험이 있으니 설계는 이미 머릿속에 있었고, Dart의 컬렉션 API가 Java보다 간결해서 코드량도 줄었다. &lt;code&gt;Map&lt;/code&gt;, &lt;code&gt;List&lt;/code&gt;, &lt;code&gt;Set&lt;/code&gt;의 함수형 메서드들이 데이터 파싱에 특히 유용했다.&lt;/p&gt;
&lt;h2 id="돌아보면"&gt;&lt;a href="#%eb%8f%8c%ec%95%84%eb%b3%b4%eb%a9%b4" class="header-anchor"&gt;&lt;/a&gt;돌아보면
&lt;/h2&gt;&lt;p&gt;부스에서 받은 피드백 두 개가 프로젝트의 방향을 결정했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;ldquo;iOS는 안 돼요?&amp;rdquo; → &lt;strong&gt;Flutter 전환&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;2학년 시간표는요?&amp;rdquo; → &lt;strong&gt;선택과목 커스텀 시간표 로직&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;사용자를 직접 만나는 게 얼마나 중요한지 체감한 순간이었다. 혼자 방에서 코딩만 했으면 절대 알 수 없었을 것들이다. 기획서를 아무리 잘 써도, 실제 사용자 앞에서 프로토타입을 보여주는 것만큼 확실한 검증은 없다.&lt;/p&gt;
&lt;p&gt;그리고 159커밋을 버린 건 아깝지만, 프로토타입의 목적은 원래 &amp;ldquo;버리기 위해 만드는 것&amp;quot;이다. 프로토타입에서 얻은 기술적 경험과 사용자 피드백이 Flutter 버전의 기초가 되었으니, 하나도 낭비된 게 아니었다.&lt;/p&gt;
&lt;h2 id="다음-글에서는"&gt;&lt;a href="#%eb%8b%a4%ec%9d%8c-%ea%b8%80%ec%97%90%ec%84%9c%eb%8a%94" class="header-anchor"&gt;&lt;/a&gt;다음 글에서는
&lt;/h2&gt;&lt;p&gt;Flutter 전환 직후 가장 먼저 만들고 싶었던 기능, 급식 알림. 단순해 보였던 이 기능을 제대로 만드는 데 1년이 걸린 이야기를 다룬다.&lt;/p&gt;</description></item></channel></rss>