개발 › Flutter
개발 › Flutter 카테고리의 글
- CRUD의 빈칸 채우기 — 수정도 값 객체로 재검증한다
지금까지 앱은 추가·토글·삭제는 했지만 제목 수정이 없었습니다. CRUD의 U(Update)가 비어 있었죠. 이번 편에 그 빈칸을 채웁니다. 핵심은 수정도 추가와 똑같이 도메인 값 객체(TodoTitle)로 재검증한다는 것 — 빈 제목·과길이는 편집에서도 막혀야 하니까요. 그리고 편집 다이얼로그가 왜 bloc에 직접 접근하지 못하는지(showDialog는 새 라우트라 InheritedWidget 밖), 그래서 다이얼로그는 텍스트만 모으고 타일이 이벤트를 던지는 단방향 패턴까지, 돌아가는 코드와 테스트로 봅니다. Flutter 실전 시리즈 12편입니다.
- LIKE를 버리고 FTS5로 — 진짜 전문 검색
7편부터 계속 미뤄온 숙제입니다. 제목 검색을 LIKE '%…%'로 짜면서 매 편 "정직하게" 절에 "이건 인덱스를 못 타니 진짜 검색은 FTS의 몫"이라고 적어 왔죠. 이번 편에 그걸 갚습니다. SQLite의 FTS5 가상 테이블로 제목을 역색인하고, 트리거로 원본과 동기화하고, LIKE를 MATCH로 바꿉니다. 이미 깔린 DB엔 마이그레이션으로 FTS를 얹고 기존 행을 백필하죠. 그리고 한글 토크나이저의 함정까지 — 돌아가는 테스트와 함께 봅니다. Flutter 실전 시리즈 11편입니다.
- 이미 깔린 DB에 인덱스 더하기 — 스키마 마이그레이션
9편에서 keyset 페이징을 짜며 "인덱스가 있으면 O(log n), 근데 인덱스를 더하려면 스키마 버전을 올려야 하고 그건 다음 숙제"라고 미뤘습니다. 이번 편에 그걸 갚습니다. keyset 정렬 키에 복합 인덱스를 거는 것 자체는 CREATE INDEX 한 줄이지만, 이미 사용자 폰에 깔려 있는 DB엔 그냥 못 넣습니다. onCreate는 새 설치만 타니까요. 그래서 버전을 v1→v2로 올리고 onUpgrade로 데이터를 지키며 인덱스를 더하는 마이그레이션을, 실제로 v1을 v2로 올려 보는 테스트까지 함께 봅니다. Flutter 실전 시리즈 10편입니다.
- offset은 왜 밀리나 — keyset 페이징으로 갈아타기
8편에서 페이징을 offset(LIMIT/OFFSET)으로 짰고, "정직하게" 절에 이렇게 적었습니다 — offset은 페이지 사이에 목록이 바뀌면 항목을 건너뛰거나 중복하고, 그럴 땐 keyset이 답이라고요. 이번 편에서 그 keyset(cursor) 페이징을 실제로 구현합니다. 위치(offset)가 아니라 "마지막으로 본 항목의 정렬 키 이후"를 조회하는 발상, SQLite 행 값 비교로 커서를 한 줄에 담는 법, 그리고 페이지 사이에 앞 항목이 삭제돼도 건너뛰지 않는 걸 돌아가는 테스트로 증명합니다. Flutter 실전 시리즈 9편입니다.
- 전부 올리지 않는다 — 페이징과 무한 스크롤
7편의 마지막 숙제였던 페이징을 구현합니다. 목록이 수천 건이면 한 번에 다 올릴 수 없으니, LIMIT/OFFSET으로 한 페이지씩 끊어 오고 바닥에 닿으면 이어 붙이죠. "다음 페이지가 있는지"를 별도 카운트 없이 N+1로 아는 기법, ScrollController로 바닥을 감지하는 법, 그리고 변경 뒤 왜 첫 페이지로 돌아가는지 — 오프셋 페이징의 정합성 한계와 keyset 페이징이라는 대안까지, 돌아가는 코드와 테스트로 봅니다. Flutter 실전 시리즈 8편입니다.
- 미뤄 둔 것을 구현할 때 — SQL 검색과 디바운스
6편에서 "목록이 커지면 검색을 SQL로 내리고 디바운스가 필요하다"고 미뤄 뒀습니다. 이번 편에서 그걸 실제로 구현합니다. 조회 조건을 도메인 값(TodoQuery)으로 만들어 계약을 따라 내려보내고, sqflite가 WHERE/LIKE로 걸러 오게 하고, 2편에서 일부러 안 짰던 이벤트 트랜스포머를 디바운스로 직접 짭니다. 미뤄 둔 결정을 실제로 구현할 때 계층이 그 변경을 감당하는지, 그리고 injectable이 Duration을 주입하려 해서 실제로 데인 함정까지, 돌아가는 코드와 테스트로 봅니다. Flutter 실전 시리즈 7편입니다.
- 검색·필터를 SQL로 안 내리고 화면에 둔 이유
시리즈를 닫았다고 했는데, 앱을 쓰다 보니 검색과 필터가 필요해졌습니다. 지금까지의 반사신경은 "규칙은 도메인에, 저장은 SQL에"였죠(1·4편). 그런데 이번엔 반대로, 검색·필터를 아래 계층으로 내리지 않고 화면(bloc)에 뒀습니다. 목록이 이미 손안에 있는데 키 입력마다 DB를 때릴 이유가 없거든요. 모든 관심사를 밑으로 미는 게 능사가 아니라는 것, 그리고 검색창이 타이핑 중에도 포커스를 안 잃는 게 지하 탐사의 그 재조정 덕이라는 것을, 실제 코드와 테스트로 봅니다. Flutter 실전 시리즈 6편입니다.
- 조립된 앱이 진짜로 도는지 — 통합 테스트로 시리즈를 닫다
계층마다 테스트는 다 초록이었습니다. 그런데 실제 컨테이너로 조립하고 실제 SQLite에 붙였을 때 전부 맞물려 도는지는, 따로 증명해야 합니다. 실제 조립 루트로 앱을 부팅해 bloc→유스케이스→repository→sqflite까지 관통하고, 파일 DB로 '재시작'해 데이터가 살아남는지까지 확인합니다. 그 과정에서 pumpAndSettle이 실제 IO를 못 몰아 무한히 도는 함정도 정직하게 짚고, 다섯 편으로 쌓은 앱을 회고하며 실전 시리즈를 닫습니다. Flutter 실전 시리즈 5편(마지막)입니다.
- 저장소를 sqflite로 갈아 끼우는데 도메인은 한 줄도 안 고쳤다
앱을 끄면 할 일이 사라졌습니다. 인메모리였으니까요. 이제 진짜 SQLite(sqflite)를 붙입니다. 그런데 저장소를 통째로 바꾸는데 도메인도, 유스케이스도, bloc도, 화면도 한 줄을 안 고쳤습니다 — 1편에서 prod/fake로 갈라 둔 그 자리에 데이터소스만 꽂았거든요. 계약을 행 단위(INSERT/UPDATE/DELETE)로 바꾸고, DB 여는 비동기를 @preResolve로 주입하고, 진짜 SQL을 ffi로 테스트하는 과정을, 클린 아키텍처가 값을 하는 순간으로 봅니다. Flutter 실전 시리즈 4편입니다.
- 화면을 붙이다 — BlocProvider를 직접 짜서 트리에 얹기
1편에서 뼈대를, 2편에서 손수 짠 bloc을 만들었지만 둘 다 화면이 없었습니다. 이제 붙입니다. 그런데 bloc을 화면 깊은 곳의 위젯이 어떻게 집고, StreamController는 누가 닫을까요? 답은 지하 탐사에서 이미 팠습니다 — InheritedWidget으로 트리에 얹고(BlocProvider의 정체), StatefulWidget의 수명으로 dispose하죠. StreamBuilder로 sealed 상태를 그리고, 텍스트 필드·체크박스는 이벤트만 되던지는 단방향 흐름을, 실제 코드와 화면째 돌아가는 위젯 테스트로 완성합니다. Flutter 실전 시리즈 3편입니다.
- flutter_bloc을 지우고 BLoC을 60줄로 짠 이유
상태 관리 하면 으레 flutter_bloc을 import하지만, 이번엔 지웁니다. BLoC 패턴의 본질은 패키지가 아니라 "이벤트가 sink로 들어가고 → UI 없는 로직이 돌고 → 상태가 stream으로 나온다" 이 세 줄이거든요. StreamController 두 개로 직접 짜면, 패키지가 감춰 두던 순차 처리·상태 방출이 전부 눈에 보입니다. 3편에서 Provider를 40줄로 짰듯, BLoC을 60줄로 짜서 원리를 드러내고, 날것 입력이 값 객체를 만나는 지점까지 실제 코드와 돌아가는 테스트로 봅니다. Flutter 실전 시리즈 2편입니다.
- UI를 한 줄도 안 짜고 앱을 시작하는 이유 — 뼈대부터 세우기
할 일 앱을 만드는데 첫 편에서 화면을 한 장도 안 그립니다. 대신 도메인·데이터·의존성 주입이라는 뼈대를 먼저 세우죠. 왜냐고요? UI부터 짠 앱은 커질수록 무너지거든요. 실패를 예외가 아니라 값(Either)으로 흘리고, 잘못된 값은 값 객체에서 막고, 시간과 id는 주입해서 테스트를 결정적으로 만드는 — 작지만 완성된 앱의 골격을 실제 코드와 돌아가는 테스트로 세웁니다. Flutter 실전 시리즈 1편입니다.
- 부모 밖 버튼이 안 눌리는 이유 — 히트 테스트
부모 밖으로 삐져나온 버튼은 왜 안 눌리고, Stack에서 위 위젯이 왜 탭을 먼저 먹고, IgnorePointer와 AbsorbPointer는 뭐가 다를까요? 전부 히트 테스트에서 나옵니다 — 포인터가 내려오면 RenderObject 트리를 훑어 "무엇을 맞혔나"를 정하죠. size 안인가 → 자식(앞부터) → 나 자신, 이 세 줄의 규칙과 그 결과가 8편의 아레나로 가는 흐름을 실제 소스와 돌아가는 테스트로 봅니다. Flutter 지하 탐사 시리즈 9편입니다.
- 끌면 탭이 취소되는 이유 — 제스처 아레나
스크롤 리스트 안에서 항목은 탭이 되는데, 끌면 탭이 취소되고 스크롤이 되죠. GestureDetector 두 개가 겹치면 누가 이기고, Listener는 GestureDetector와 뭐가 다를까요? 전부 한 메커니즘에서 나옵니다 — 제스처 아레나. 포인터 하나를 두고 여러 인식기(tap·drag·scroll)가 경쟁하고, 움직임이 승부를 가릅니다. 원시 포인터부터 아레나 resolve/sweep, HitTestBehavior까지 실제 소스와 돌아가는 테스트로 봅니다. Flutter 지하 탐사 시리즈 8편입니다.
- 애니메이션이 도는 법 — Ticker와 AnimationController
AnimationController에 왜 vsync를 넘기고, 왜 dispose를 해야 하고, Timer로 애니메이션을 하면 왜 끊길까요? 전부 한 사실에서 나옵니다 — 애니메이션은 별도 스레드가 아니라, 매 프레임(vsync)마다 값을 조금씩 바꾸는 것입니다. Ticker가 프레임마다 깨우고, AnimationController가 흐른 시간을 0..1 값으로, Tween과 Curve가 그 값을 화면으로 옮기죠. 6편의 프레임 파이프라인 위에서 애니메이션이 어떻게 도는지, 실제 소스와 돌아가는 테스트로 봅니다. Flutter 지하 탐사 시리즈 7편입니다.
- setState부터 픽셀까지 — 한 프레임의 여정
setState를 했는데 화면이 즉시 안 바뀌고, build에서는 위젯 크기를 못 읽고, addPostFrameCallback은 뭔가 나중에 돌죠. 전부 한 사실에서 나옵니다 — 화면은 정해진 순서의 파이프라인으로, 한 프레임에 한 번 만들어집니다. dirty 표시 + 프레임 예약부터 build→layout→paint→합성까지, WidgetsBinding.drawFrame의 실제 순서를 소스와 테스트로 따라가고, 네 번째 트리(Layer)와 16ms 예산까지 봅니다. 앞선 다섯 편이 왜 전부 이 예산을 지키기 위한 것이었는지, 시리즈를 묶는 6편입니다.
- UI가 얼어붙는 이유 — 이벤트 루프와 아이솔레이트
무거운 계산을 하면 UI가 얼어붙고(jank), async를 붙여도 안 풀리고, print 순서는 이상하죠. 전부 한 사실에서 나옵니다 — Dart는 한 스레드(이벤트 루프 하나)에서 돕니다. async는 스레드가 아니라 그 루프에 "이어서 실행"을 예약하는 것일 뿐이에요. 실행 순서(동기→마이크로태스크→이벤트), async 함수의 동기 첫 줄, 프레임이 왜 밀리는지, 그리고 진짜 병렬을 위한 아이솔레이트까지 — 결정적인 순서를 돌아가는 테스트로 증명합니다. Flutter 지하 탐사 시리즈 5편입니다.
- 레이아웃이 터지는 이유 — RenderObject와 제약
A RenderFlex overflowed", "Vertical viewport was given unbounded height", "Column 안에 ListView를 넣으면 터진다"… Flutter 레이아웃 에러는 하나같이 무섭게 생겼지만, 전부 한 규약에서 나옵니다 — 제약(constraints)은 내려가고, 크기(size)는 올라오고, 위치는 부모가 정한다. build 아래의 셋째 트리인 RenderObject 층으로 내려가, 레이아웃 프로토콜·tight와 loose 제약·relayout boundary·RepaintBoundary를 실제 rendering 소스(3.44.8)와 커스텀 RenderBox 테스트로 파고듭니다. Flutter 지하 탐사 시리즈 4편입니다.
- Provider를 40줄로 만들기 — watch와 read의 정체
Provider·Riverpod은 대체 무엇을 해주는 걸까요? 사실 재료는 1편(InheritedWidget 구독)과 2편(동일 인스턴스 스킵)에서 이미 다 봤습니다. 그래서 이번엔 파고들지 않고 직접 만듭니다. 상태관리의 알맹이를 40줄로 구현해, context.watch와 context.read가 사실 프레임워크 메서드 두 개(dependOn / getInherited)일 뿐임을 확인합니다. 실제 동작하는 미니 Provider와 "watch한 것만 리빌드된다"는 증명 테스트까지. Flutter 지하 탐사 시리즈 3편입니다.
- State가 엉뚱한 행에 남는 이유 — 재조정과 Key
리스트에서 항목을 지우거나 순서를 바꿨더니 엉뚱한 행의 입력값·상태가 남는 그 버그. "리스트엔 Key를 주세요", "const가 빠르다" 같은 조언들. 전부 한 메커니즘에서 나옵니다 — 재조정(reconciliation). 부모가 리빌드할 때 새 Widget을 옛 Element에 어떻게 이어 붙이는지, updateChild의 세 갈래와 canUpdate, 리스트의 key 매칭, GlobalKey의 재부모화를 실제 framework.dart(3.44.8) 소스와 돌아가는 테스트로 파고듭니다. Flutter 지하 탐사 시리즈 2편입니다.
- BuildContext 지하 탐사 — Element, 트리, 그리고 단일 스레드
BuildContext는 Flutter에서 가장 자주 쓰면서 가장 덜 이해되는 것입니다. "await 뒤엔 context 쓰지 마라", "of()는 build에서만" 같은 규칙들을 외워서 지키지만 왜 그런지는 모르죠. 이 규칙들은 전부 한 사실에서 나옵니다 — BuildContext는 사실 Element다. 그 Element를 따라 지하까지 내려가, 규칙들이 왜 그렇게 생겼는지 실제 framework.dart 소스와 돌아가는 테스트로 밝혀 봅니다. Flutter 3.44.8 기준. Flutter 지하 탐사 시리즈 1편입니다.