3기 김태영
56분 전•

10월 27일 공부일지

10월 27일

오늘 공부에 투자한 순수한 시간: 4시간

학습한 개념 : 구독 추가 FAB 화면의 서비스 로고 선택과 이름 입력 상태 연결기능 구현

----------------------------------------------------------------------------------------------------------

<핵심 코드 1: 구독 추가 폼 화면 — 선택 결과를 받고 폼 상태에 반영하는 코드>

Future<void> _openLogoSheet() async {

final selected = await showModalBottomSheet<CatalogService>(

context: context,

isScrollControlled: true,

useSafeArea: true,

showDragHandle: true,

builder: (_) => DraggableScrollableSheet(

initialChildSize: 0.92,

minChildSize: 0.70,

maxChildSize: 0.95,

expand: false,

builder: (context, scrollController) => LogoSelectionSheet(

services: widget.services,

scrollController: scrollController,

),

),

);

if (!mounted || selected == null) return;

_selectService(selected);

}

void _selectService(CatalogService service) {

_nameController.value = TextEditingValue(

text: service.name,

selection: TextSelection.collapsed(offset: service.name.length),

);

setState(() {

_serviceId = service.id;

_category = service.category;

});

}

-> 이 코드는 로고 검색 시트를 열고, 선택한 서비스 정보를 현재 폼에 적용하는 부분이다. openLogoSheet는 시트를 열어 결과를 받는 일을 맡고, selectService는 받은 서비스의 이름, ID, 카테고리를 폼에 반영한다.

subscription_form_screen.dart에 있는 _openLogoSheet가 실행되면 showModalBottomSheet가 폼 위에 시트를 띄운다. <CatalogService>는 시트가 닫힐 때 돌려받을 서비스 정보의 타입을 정한다.

final selected = await는 시트의 결과가 돌아올 때까지 다음 처리를 기다렸다가, 반환값을 selected에 담는다는 의미이다.

isScrollControlled는 스크롤과 높이 조절을 위한 설정이고, useSafeArea는 상단과 좌우 시스템 영역을 피하게 한다. showDragHandle은 드래그 표시를 보여준다.

DraggableScrollableSheet는 부모 위젯 높이의 92%로 열려 70~95% 사이에서 사용자가 조절할 수 있게 한다. 안쪽에는 LogoSelectionSheet를 넣고, 폼이 가진 서비스 목록과 시트에서 받은 scrollController를 전달한다.

Navigator.pop이 반환한 서비스는 selected에 들어온다. !mounted는 폼의 State가 더 이상 위젯 트리에 연결되어 있지 않은 경우이고, selected == null은 선택 결과 없이 시트를 닫은 경우이다. 해당하면 return으로 끝내고, 유효한 결과가 있을 때 _selectService를 호출한다.

selectService에서는 nameController.value에 이름과 커서 위치를 함께 넣는다. TextSelection.collapsed의 offset을 이름 길이로 지정했으므로 커서는 이름 끝에 놓인다. 이어서 setState 안에서 serviceId와 category를 바꾸면, 새 값이 로고 타일에 전달되고 ServiceIcon이 해당 ID의 로고를 표시한다. 이름은 컨트롤러에 연결된 입력칸에 반영된다.

즉, 시트에서 돌아온 서비스 정보를 폼이 관리하는 상태로 옮기는 중심이 이 코드이다. 금액과 결제일은 유지하고 서비스 관련 값만 갱신한다. 이름 자동완성에서도 _selectService를 사용하면 같은 규칙으로 처리할 수 있다.

----------------------------------------------------------------------------------------------------------

<핵심 코드 2: 폼의 로고 타일 — 탭을 폼 화면의 함수에 연결하는 코드>

// 폼 화면에서 SubscriptionFormBody를 만들 때 전달

onLogoTap: _openLogoSheet,

// SubscriptionFormBody가 전달받는 함수

final VoidCallback onLogoTap;

// SubscriptionFormBody에서 로고 타일에 전달

_LogoTile(

serviceId: serviceId,

category: category,

onTap: onLogoTap,

),

// _LogoTile이 전달받은 함수를 탭 동작에 연결

InkWell(

onTap: onTap,

borderRadius: BorderRadius.circular(20),

child: 기존로고위젯,

)

-> 이 코드는 사용자가 로고 타일을 눌렀을 때 폼 화면의 openLogoSheet가 실행되도록 함수를 연결하는 부분이다. 폼 화면에서 만든 동작을 SubscriptionFormBody에 전달하고, 다시 LogoTile을 거쳐 InkWell의 onTap에 연결한다. 함수 전달과 탭 이후의 실행을 나눠서 보면 된다.

먼저 subscription_form_screen.dart에서 SubscriptionFormBody를 만들 때 onLogoTap: _openLogoSheet를 전달한다. 괄호 없이 함수 자체를 전달했으므로, 화면을 구성할 때 동작을 맡겨두고 실제로 로고를 눌렀을 때 호출하도록 준비하는 것이다.

subscription_form_body.dart의 final VoidCallback onLogoTap은 전달받은 함수를 보관하는 필드이다. VoidCallback은 인자를 받지 않고 반환값을 사용하지 않는 콜백의 형태를 나타낸다. 시트의 결과를 기다리는 처리는 _openLogoSheet 안에서 이루어지므로 타일은 탭할 때 실행할 함수만 가지면 된다.

SubscriptionFormBody는 _LogoTile을 구성하면서 onTap: onLogoTap으로 같은 함수를 넘긴다. 이때 serviceId와 category는 현재 로고를 표시하는 데 필요한 값이고, onTap은 사용자가 눌렀을 때 할 일이다.

LogoTile에서는 기존 로고 위젯을 InkWell로 감싸고 onTap에 전달받은 함수를 연결한다. InkWell이 탭을 감지하면 이 콜백을 호출하고, 폼 화면의 openLogoSheet가 실행되어 시트가 열린다.

따라서 준비할 때는 폼 화면에서 Body, 타일 순서로 함수가 내려가고, 사용할 때는 타일의 탭이 폼 화면에 정의한 동작을 실행하는 계기가 된다. 시트가 열린 뒤 검색과 선택은 시트가 맡고, 결과를 받은 폼이 상태를 바꾸면 타일은 새 serviceId와 category를 받아 갱신된 로고를 보여준다.

이 코드가 중요한 이유는 표시를 담당하는 타일과 상태를 관리하는 폼을 동작으로 연결하기 때문이다. 타일은 로고 표시와 탭 감지를 담당하고, 폼은 시트를 열고 결과를 적용하는 일을 맡는다. 타일의 모양을 바꿔도 서비스 선택 흐름은 폼에서 계속 관리할 수 있다.

----------------------------------------------------------------------------------------------------------

<핵심 코드 3: 새 로고 검색 시트 — 검색하고 선택한 서비스를 반환하는 코드>

String _query = '';

@override

Widget build(BuildContext context) {

final results = searchCatalogServices(widget.services, _query);

return Column(

children: [

TextField(

onChanged: (value) {

setState(() => _query = value);

},

),

Expanded(

child: ListView(

controller: widget.scrollController,

children: [

for (final service in results)

ListTile(

leading: ServiceIcon(

serviceId: service.id,

category: service.category,

size: 44,

),

title: Text(service.name),

onTap: () {

Navigator.pop<CatalogService>(context, service);

},

),

],

),

),

],

);

}

-> 이 코드는 검색어에 맞는 서비스를 보여주고, 선택한 항목의 정보를 폼 화면에 반환하는 부분이다. logo_selection_sheet.dart의 LogoSelectionSheet가 검색어를 관리하고, 폼에서 받은 카탈로그 목록과 검색어로 결과를 계산한다.

_query는 검색창에 입력한 글자를 보관하는 변수이다. 처음에는 빈 문자열이므로 기존 검색 규칙에 따라 서비스 15개를 모두 보여준다.

TextField에 글자를 입력하면 onChanged가 바뀐 값을 받고, setState 안에서 _query를 변경한다. 이후 build가 다시 실행되어 새 검색어로 results를 구하고 목록을 구성한다.

searchCatalogServices(widget.services, _query)는 폼에서 전달받은 목록을 기존 검색 규칙으로 좁히는 부분이다. 서비스 이름과 검색용 별칭을 활용하므로 이름 자동완성과 같은 기준으로 서비스를 찾는다.

화면은 Column으로 검색창과 목록을 위아래로 배치하고, Expanded가 검색창 아래의 남은 공간을 목록에 배정한다. ListView는 목록이 길어졌을 때 스크롤할 수 있게 한다. widget.scrollController에는 DraggableScrollableSheet의 컨트롤러를 연결해 시트의 높이 조절과 목록 스크롤이 함께 동작하게 한다.

for문은 results에 들어 있는 각 service마다 ListTile을 만든다. ServiceIcon은 service.id와 category를 받아 로고를 표시한다.

항목을 누르면 Navigator.pop<CatalogService>(context, service)가 시트를 닫으면서 해당 서비스 객체를 반환한다. 예를 들어 넷플릭스를 골랐다면 그 ID와 이름, 카테고리가 담긴 객체가 돌아가고, 폼의 await showModalBottomSheet<CatalogService>가 이를 selected로 받는다. 보내는 쪽의 pop과 받는 쪽의 await가 한 쌍으로 연결되는 것이다.

핵심은 검색 결과를 선택 가능한 목록으로 만들고 그중 하나를 반환한다는 점이다. 시트에서 서비스를 고른 뒤에는 폼이 이 정보를 자신의 상태에 적용하고, 갱신된 값으로 로고와 이름을 보여준다. 검색 상태와 폼 상태를 서비스 객체로 연결하는 구조이다.

----------------------------------------------------------------------------------------------------------

<핵심 코드 4: 서비스 이름 입력칸 — 같은 컨트롤러를 실제 입력칸까지 연결하는 코드>

final _nameController = TextEditingController();

final _nameFocusNode = FocusNode();

nameController: _nameController,

nameFocusNode: _nameFocusNode,

controller: nameController,

focusNode: nameFocusNode,

Autocomplete<CatalogService>(

textEditingController: controller,

focusNode: focusNode,

optionsBuilder: (value) =>

searchCatalogServices(services, value.text),

displayStringForOption: (service) => service.name,

onSelected: onSelected,

fieldViewBuilder: (

context,

fieldController,

fieldFocusNode,

onSubmitted,

) => TextField(

controller: fieldController,

focusNode: fieldFocusNode,

onChanged: onChanged,

),

)

-> 이 코드는 폼 화면이 만든 이름 컨트롤러와 포커스 객체를 실제 TextField까지 연결하는 부분이다. 폼이 바꾼 이름은 입력칸에 보이고, 직접 입력한 이름도 같은 컨트롤러에서 읽을 수 있다. 폼의 이름과 입력칸의 글자를 같은 기준으로 관리하는 것이다.

1. 폼 화면이 이름 입력을 관리하기

TextEditingController는 입력칸의 글자와 커서 같은 편집 상태를 관리하고, FocusNode는 입력칸의 포커스를 관리한다. 폼 화면이 두 객체를 생성하고 보관해 이름 입력을 제어한다. initState에서 초기 이름을 넣고, dispose에서 두 객체를 해제한다. 저장할 이름은 _nameController.text.trim()에서 읽고, 빈 값인지를 저장 조건에 사용한다.

2. 서비스 선택을 한곳에서 처리하기

로고 시트에서 선택한 결과와 이름 자동완성에서 선택한 결과를 _selectService로 모은다. 이 함수가 이름 컨트롤러에 서비스 이름을 넣고, 서비스 ID와 카테고리도 함께 갱신한다. TextEditingValue로 이름과 커서를 함께 바꾸므로 새 이름의 끝부터 이어서 수정할 수 있다.

3. 입력칸까지 컨트롤러 전달하기

폼이 가진 nameController와 nameFocusNode를 SubscriptionFormBody의 nameController, nameFocusNode 인자로 넘긴다. Body는 이를 NameAutocomplete의 controller와 focusNode로 전달한다. 같은 객체의 참조를 이어주므로 폼의 입력 상태가 아래의 입력칸까지 연결된다.

4. 자동완성이 전달받은 입력칸을 사용하기

NameAutocomplete는 받은 객체를 Autocomplete의 textEditingController와 focusNode에 연결한다. optionsBuilder는 현재 글자로 카탈로그를 검색하고, displayStringForOption은 선택한 객체를 입력칸에 표시할 때 service.name을 사용하도록 정한다. onSelected는 선택 결과를 폼에 전달한다.

fieldViewBuilder에서 받은 fieldController와 fieldFocusNode를 실제 TextField에 연결하면 자동완성이 글자와 포커스 변화를 따라갈 수 있게 해준다. 사용자가 직접 입력할 때는 onChanged를 통해 폼의 저장 버튼 상태도 다시 계산하도록 연결한다.

전체 흐름을 보면, 먼저 폼에서 컨트롤러를 만들고 Body와 NameAutocomplete를 거쳐 입력칸에 연결한다. 그다음 시트에서 서비스를 고르면 _selectService가 이미 연결된 컨트롤러의 값을 바꾸고, 입력칸이 새 이름을 표시한다. 동시에 setState로 로고와 카테고리도 갱신된다. 즉, 준비는 1→3→4의 흐름이고 선택할 때 2가 그 연결을 사용하는 것이다.

이 코드가 중요한 이유는 보이는 이름과 저장할 이름의 기준이 같아지기 때문이다. 선택한 이름을 사용자가 다시 수정해도 그 글자가 같은 컨트롤러에 들어오므로, 저장할 때 현재 입력칸의 이름을 읽을 수 있다.

----------------------------------------------------------------------------------------------------------

<전체 단계 연결>

-> 전체 흐름은 카탈로그 서비스를 선택하고 폼에 반영한 뒤 구독 정보로 저장하는 과정이다. 로고 선택은 해당 카탈로그 서비스와 구독을 연결하는 의미이다. 폼과 홈 목록은 서비스 ID를 기준으로 같은 로고를 표시한다.

먼저 홈의 구독 추가 FAB를 누르면 AppRouter가 카탈로그의 서비스 15개를 읽어 폼에 전달한다. 폼은 이 목록과 현재 작성 중인 값을 관리하고, SubscriptionFormBody를 만들면서 로고를 눌렀을 때 실행할 _openLogoSheet도 함께 전달한다.

사용자가 로고 타일을 누르면 InkWell에 연결된 콜백이 실행되고, 폼 화면이 검색 시트를 연다. 시트에는 폼이 가진 카탈로그 목록이 전달된다. 사용자는 검색어로 목록을 좁히고 로고와 이름을 확인하며 항목을 고른다. Scan 시트와 비슷한 화면 구성을 사용하되, 이 목록은 설치 여부와 관계없이 카탈로그 전체를 기준으로 한

다.

넷플릭스를 선택하면 Navigator.pop이 시트를 닫으면서 그 서비스 객체를 반환한다. 기다리던 openLogoSheet는 selected로 결과를 받고, 폼이 유지되고 선택 결과가 있을 때 selectService를 실행한다. 선택 없이 닫으면 작성 중인 값은 그대로 유지된다.

_selectService는 이름 컨트롤러의 글자를 바꾸고 서비스 ID와 카테고리를 갱신한다. 같은 컨트롤러에 연결된 이름 입력칸에는 넷플릭스가 표시되고, 로고 타일은 새 ID에 해당하는 이미지를 보여준다. 금액과 결제일 등 이미 작성한 다른 항목은 유지되므로 사용자는 이어서 구독 정보를 완성할 수 있다.

이후 저장 버튼을 누르면 폼이 컨트롤러의 현재 이름과 서비스 ID를 포함한 구독 정보를 기존 저장 흐름에 전달한다. 홈 목록은 저장된 ID로 로고를 표시한다. 즉, 타일은 탭을 알리고, 시트는 선택한 서비스를 반환하고, 폼은 상태를 갱신하며, 저장 기능은 완성된 값을 기록한다. 함수가 전달되는 방향과 데이터가 돌아오는 방향을 보면 여러 파일이 하나의 기능으로 연결되는 이유를 이해할 수 있다.

----------------------------------------------------------------------------------------------------------

키워드

ㄴ꼬리질문1 하나씩 들어가보자 구독 추가 폼 화면에서 로고 타일을 클릭하면 시트가 열릴 수 있도록 해야 하는데 이 부분을 구현하려면 어떤 파일에 어떤 코드를 작성해야 하는지 설명해주고, 작성한 코드를 설명해줘

ㄴ꼬리질문2 onTap을 썼다는 건 눌렀다는 사실만 전달해서 시트를 열 수 있도록 신호를 보내주는 거고, 시트가 열리고 나서부터는 관여를 하지 않는거지?

ㄴ꼬리질문3 onTap을 통해 subscription_form_screen.dart 파일에 있는 SubscriptionFormBody라는 클래스를 호출해서 시트를 여는거야?

ㄴ꼬리질문4 다음으로 "새 로고 검색 시트 - 15개 로고 목록, 검색창, 선택 동작 제공" 부분에 대해 이 부분을 구현하려면 어떤 파일에 어떤 코드를 작서해야 하는지 설명해주고, 작성 해야하는 코드들을 설명해줘

ㄴ꼬리질문5 Catalog Service를 반환한다는 거는 내가 선택한 로고를 로고 타일에 그려준다는거지?

ㄴ꼬리질문6 _openLogoSheet의 빈 목록 부분을 새 시트로 바꾸고, 선택 결과를 받는다는게 무슨 말인지 설명해줘

ㄴ꼬리질문7 “선택 결과를 받는다”는 것은 사용자가 목록에서 예를 들어 넷플릭스를 누르면, 시트가 닫히면서 “넷플릭스를 선택했다”는 서비스 정보를 _openLogoSheet로 돌려준다는 뜻 "

이 부분에 해당하는 코드를 보여주고 설명해줘

ㄴ꼬리질문8 시트에서 서비스를 골라 폼의 _name만 바꿔서는 보이는 글자가 바뀌지 않을 수 있습니다 이 말의 의미는 서비스 로고를 골랐지만 입력창에는 해당 서비스 로고의 이름이 반영이 안되는 말인거야?

ㄴ꼬리질문9 현재 대화하고 있는 부분의 세부 단계를 연결해서 하나의 전체적인 매커니즘과 흐름을 설명해줘

...

AI 대화 내역 링크:

챗GPT: https://chatgpt.com/s/cx_6ae0b43ca7708191943acb2230f0891c

오늘 잘한 점: 핵심 코드를 고를 때 눈에 보이는 화면 구성뿐 아니라 데이터가 어디에서 들어오고, 누가 상태를 바꾸는지를 중요하게 생각하면서 공부했다. 폼이 함수를 전달하는 과정과 시트가 선택 결과를 반환하는 과정을 구분해서 보니 각 파일이 맡은 역할이 더 분명해졌던 것 같다.

오늘 부족했던 점: 전체 코드를 하나씩 다 공부하면 좋았겠지만 그렇게 하면 시간이 너무 오래 걸릴 것 같아서 데이터 이동과 상태 관리에 해당하는 핵심 코드 위주로 공부했다. 따라서 화면의 세부 배치나 검색 결과가 없을 때의 표시처럼 주변 코드까지 충분히 살펴보지는 못한 것 같다.

오늘 총평: 로고를 누르고 서비스를 고르는 기능도 안쪽에서는 함수 전달, 선택 결과 반환, 상태 변경, 입력칸 연결이라는 여러 과정으로 이루어져 있었다. 이는 내가 간단한 앱을 만들때와 비슷한 로직인 것 같다는 생각이 들었다. 처음부터 파일별로 외우기보다 사용자의 행동을 시작점으로 삼고, 그다음 실행되는 함수와 이동하는 값을 따라가니 전체 구조가 더 잘 보였고, 이로 인해 이해가 더 잘 된 것 같다.

0
0

여기에 파일을 드롭하세요