2. 온보딩 (Onboarding) - 관심사 선택과 튜토리얼
Interests Screen - 관심사 선택 화면
🎯 이번 단계에서 배울 것
- 온보딩 기능 모듈과 인증 모듈 연결하기
- Wrap 위젯으로 동적 레이아웃 만들기
- SingleChildScrollView로 스크롤 가능한 화면 구성
- BottomAppBar 사용법
- 모듈 간 의존성 설정 방법
📂 파일 구조
1 | features/ |
📝 1단계: 모듈 간 의존성 설정
authentication/pubspec.yaml:
1 | dependencies: |
🔍 모듈 간 의존성이란?
의존성(Dependency) 은 한 모듈이 다른 모듈의 코드를 사용하기 위해 필요한 연결입니다.
왜 필요한가?
- Authentication 모듈에서 Onboarding 화면으로 이동하려면
- Onboarding 모듈의
InterestsScreen
을 import 해야 합니다 - 그러려면 pubspec.yaml에 의존성을 추가해야 합니다
작동 방식:
1 | authentication 모듈 |
📝 2단계: Interests Screen 구현
전체 코드 (features/onboarding/lib/screens/interests_screen.dart):
1 | import 'package:design_system/design_system.dart'; |
🔍 코드 상세 설명
1. const 리스트란?
1 | const interests = [ |
- const: 컴파일 타임에 값이 결정되는 상수
- 앱 실행 중 절대 변하지 않는 데이터
- 메모리를 효율적으로 사용
왜 사용하는가?
- 관심사 목록은 앱 실행 중 변하지 않음
- 메모리에 한 번만 생성되어 재사용됨
2. SingleChildScrollView란?
1 | SingleChildScrollView( |
- 화면에 콘텐츠가 많을 때 스크롤 가능하게 만드는 위젯
- 자식 위젯이 화면을 벗어나면 자동으로 스크롤 활성화
작동 방식:
1 | 화면 높이: 800px |
3. Wrap 위젯이란?
1 | Wrap( |
- 자식 위젯들을 자동으로 줄바꿈하는 레이아웃 위젯
- 화면 너비를 넘으면 다음 줄로 이동
시각화:
1 | ┌─────────────────────────────────┐ |
spacing vs runSpacing:
1 | spacing: Sizes.size16 // 가로 간격 (같은 줄 내) |
비교 예시:
1 | // Row를 사용하면? |
4. for-in 반복문 (Collection for)
1 | children: [ |
- 리스트 안에서 직접 반복문 사용
- interests 리스트의 각 항목마다 카드 생성
기존 방식과 비교:
1 | // Before (map 사용) |
5. BottomAppBar란?
1 | bottomNavigationBar: BottomAppBar( |
- 화면 하단에 고정되는 바
- 스크롤해도 항상 하단에 표시됨
- elevation으로 그림자 효과 추가
6. BoxDecoration 상세
1 | decoration: BoxDecoration( |
각 속성 설명:
color
: 배경색borderRadius
: 모서리 둥글게boxShadow
: 그림자 효과color
: 그림자 색상 (투명도 5%)blurRadius
: 흐림 정도spreadRadius
: 퍼지는 정도
border
: 테두리
📝 3단계: 네비게이션 연결
birthday_screen.dart:
1 | import 'package:feature_onboarding/screens/interests_screen.dart'; |
login_form_screen.dart:
1 | import 'package:feature_onboarding/screens/interests_screen.dart'; |
🔍 코드 상세 설명
1. import 경로
1 | import 'package:feature_onboarding/screens/interests_screen.dart'; |
package:feature_onboarding
: pubspec.yaml에 추가한 의존성 이름/screens/interests_screen.dart
: 파일 경로
작동 방식:
1 | pubspec.yaml에 정의된 이름 |
📝 4단계: 개발용 임시 홈 화면 설정
lib/main.dart:
1 | import 'package:feature_onboarding/screens/interests_screen.dart'; |
🔍 왜 임시 홈 화면을 설정하나?
개발 효율성:
- 온보딩 화면을 개발할 때
- 매번 로그인/회원가입 과정을 거치지 않기 위해
- 바로 작업 중인 화면으로 이동
주의사항:
- 개발 완료 후 다시 원래 홈 화면으로 변경해야 함
- 주석으로 원래 코드를 남겨두면 좋음
🎨 화면 미리보기
1 | ┌─────────────────────────────────┐ |
📊 동작 흐름
1 | 1. 사용자가 생일 입력 완료 (BirthdayScreen) |
✅ 체크리스트
- authentication 모듈의 pubspec.yaml에 feature_onboarding 의존성 추가
- interests_screen.dart에서 관심사 리스트 정의
- Wrap 위젯으로 관심사 카드 레이아웃 구성
- SingleChildScrollView로 스크롤 가능하게 만들기
- BottomAppBar로 하단 Next 버튼 구현
- birthday_screen.dart에서 InterestsScreen으로 네비게이션 추가
- login_form_screen.dart에서 InterestsScreen으로 네비게이션 추가
- 앱 실행 시 InterestsScreen이 표시되는지 확인
💡 연습 과제
- 기본: interests 리스트에 새로운 관심사 3개 추가하기
- 기본: _buildInterestCard의 borderRadius 값을 변경해보기
- 중급: Wrap의 spacing과 runSpacing 값을 조정해서 간격 변경하기
- 중급: 관심사 카드의 색상을 다른 색으로 변경하기
- 고급: Column을 사용해서 Wrap과 같은 레이아웃 만들어보기 (힌트: 줄바꿈이 어려움)
Scroll Animations - 스크롤 애니메이션
🎯 이번 단계에서 배울 것
- ScrollController로 스크롤 위치 감지하기
- StatefulWidget으로 상태 관리하기
- AnimatedOpacity로 부드러운 애니메이션 구현
- 스크롤 이벤트 리스너 사용법
- 위젯 분리와 재사용 패턴
📂 파일 구조
1 | features/onboarding/ |
📝 1단계: StatelessWidget → StatefulWidget 변환
전체 코드 (interests_screen.dart 일부):
1 | class InterestsScreen extends StatefulWidget { |
🔍 코드 상세 설명
1. StatefulWidget vs StatelessWidget
1 | // Before |
왜 변경하는가?
- 스크롤 위치에 따라 AppBar 제목을 보이거나 숨기려면
- 변화하는 상태(_showTitle)가 필요
- StatefulWidget만 setState()로 상태 변경 가능
2. ScrollController란?
1 | final _scrollController = ScrollController(); |
- 스크롤 위치를 감지하고 제어하는 컨트롤러
- 현재 스크롤 위치(offset)를 알 수 있음
- 프로그램으로 스크롤 위치를 변경할 수도 있음
작동 방식:
1 | 사용자가 스크롤 |
3. initState()란?
1 |
|
- 위젯이 처음 생성될 때 한 번만 실행되는 메서드
- 리스너 등록, 컨트롤러 설정 등 초기화 작업 수행
생명주기:
1 | 위젯 생성 |
4. addListener()란?
1 | _scrollController.addListener(() { |
- 스크롤이 발생할 때마다 실행될 함수 등록
- 스크롤 위치 변화를 감지하는 핵심 메커니즘
5. offset 값 활용
1 | if (_scrollController.offset > 200) { |
offset
: 현재 스크롤 위치 (픽셀 단위)- 값이 클수록 더 아래로 스크롤한 것
시각화:
1 | 화면 최상단 (offset: 0) |
6. 중복 setState() 방지
1 | if (_scrollController.offset > 200) { |
왜 필요한가?
- 스크롤할 때마다 리스너가 계속 호출됨
- 같은 상태로 setState()를 반복 호출하면 비효율적
- 이미 같은 상태면 조기에 return으로 종료
비교:
1 | // Bad (비효율적) |
7. dispose()란?
1 |
|
- 위젯이 제거될 때 실행되는 메서드
- 메모리 누수 방지를 위해 리소스 정리
왜 필요한가?
- Controller는 메모리를 사용하는 객체
- 위젯이 사라져도 Controller가 남아있으면 메모리 낭비
- dispose()로 명시적으로 해제해야 함
메모리 관리:
1 | 위젯 생성 → Controller 생성 (메모리 사용) |
📝 2단계: AnimatedOpacity 적용
전체 코드 (build 메서드 일부):
1 |
|
🔍 코드 상세 설명
1. AnimatedOpacity란?
1 | AnimatedOpacity( |
- 투명도를 부드럽게 변화시키는 애니메이션 위젯
opacity
: 투명도 (0 = 완전 투명, 1 = 완전 불투명)duration
: 애니메이션 지속 시간
작동 방식:
1 | _showTitle = false |
비교 예시:
1 | // Without Animation (갑작스러움) |
2. Scrollbar 위젯
1 | Scrollbar( |
- 스크롤바를 표시하는 위젯
- 같은 controller를 공유해야 함
왜 필요한가?
- 사용자에게 스크롤 가능함을 시각적으로 알림
- 현재 스크롤 위치를 알 수 있음
3. controller 연결
1 | final _scrollController = ScrollController(); |
- 같은 ScrollController를 여러 위젯이 공유
- Scrollbar와 ScrollView가 동기화됨
📝 3단계: InterestButton 위젯 분리
전체 코드 (interest_button.dart):
1 | import 'package:design_system/design_system.dart'; |
🔍 코드 상세 설명
1. 위젯 분리 이유
1 | // Before (interests_screen.dart) |
왜 분리하는가?
- 각 버튼이 독립적인 선택 상태를 가져야 함
- 한 버튼만 다시 그리기 위해 (성능 향상)
- 코드 재사용성과 가독성 향상
2. widget.interest 접근
1 | class InterestButton extends StatefulWidget { |
- StatefulWidget의 속성은
widget.
으로 접근 - State 클래스에서 부모 위젯의 속성에 접근하는 방법
3. GestureDetector
1 | GestureDetector( |
- 제스처(탭, 드래그 등)를 감지하는 위젯
onTap
: 탭했을 때 실행할 함수
4. AnimatedContainer
1 | AnimatedContainer( |
- 속성 변화를 자동으로 애니메이션하는 Container
- color, padding, decoration 등의 변화가 부드럽게 전환됨
작동 방식:
1 | _isSelected = false (흰색) |
비교 예시:
1 | // Container (애니메이션 없음) |
5. 토글 로직
1 | void _onTap() { |
!
연산자: 논리 부정 (NOT)!true
=false
!false
=true
📝 4단계: interests_screen.dart 업데이트
변경된 부분:
1 | import '../widget/interest_button.dart'; |
변경 사항:
_buildInterestCard()
제거InterestButton
위젯으로 교체- import 추가
🎨 화면 미리보기
1 | ┌─────────────────────────────────┐ |
📊 동작 흐름
1 | 1. 화면 로드 |
✅ 체크리스트
- InterestsScreen을 StatefulWidget으로 변경
- ScrollController 생성 및 리스너 등록
- initState()와 dispose() 구현
- AnimatedOpacity로 AppBar 제목 애니메이션 추가
- Scrollbar 위젯 추가
- InterestButton 위젯 파일 생성
- GestureDetector와 AnimatedContainer 구현
- 버튼 탭 시 선택 상태 토글 기능 구현
- interests_screen.dart에서 InterestButton import 및 사용
💡 연습 과제
- 기본: 스크롤 임계값을 200에서 100으로 변경하기
- 기본: AnimatedOpacity의 duration을 500ms로 변경해보기
- 중급: 선택된 관심사가 3개 이상이면 다른 버튼을 선택 못하게 만들기
- 중급: 선택된 관심사 개수를 화면에 표시하기
- 고급: 스크롤 방향(위/아래)을 감지해서 다른 애니메이션 적용하기
Tutorial Screen - 튜토리얼 화면
🎯 이번 단계에서 배울 것
- DefaultTabController로 페이지 전환 구현
- TabBarView로 여러 페이지 관리
- TabPageSelector로 페이지 인디케이터 추가
- SafeArea로 안전 영역 설정
- 튜토리얼 UI 패턴 구현
📂 파일 구조
1 | features/onboarding/ |
📝 1단계: Interests Screen에 네비게이션 추가
interests_screen.dart 변경 부분:
1 | import 'tutorial_screen.dart'; |
🔍 코드 상세 설명
1. Navigator.of(context).push()
1 | Navigator.of(context).push( |
Navigator.push() vs Navigator.of(context).push():
Navigator.push(context, route)
: 축약 형태Navigator.of(context).push(route)
: 명시적 형태- 기능은 동일하지만,
of(context)
가 더 명확함
2. GestureDetector 적용
1 | GestureDetector( |
- Container는 기본적으로 탭 이벤트를 받지 못함
- GestureDetector로 감싸서 탭 가능하게 만듦
📝 2단계: Tutorial Screen 생성
전체 코드 (tutorial_screen.dart):
1 | import 'package:design_system/design_system.dart'; |
🔍 코드 상세 설명
1. DefaultTabController란?
1 | DefaultTabController( |
- 탭 기반 네비게이션을 제공하는 컨트롤러
- TabBar, TabBarView, TabPageSelector를 자동으로 연결
- 별도의 TabController 생성 없이 사용 가능
작동 방식:
1 | DefaultTabController |
2. length 속성
1 | length: 3 // 총 페이지 수 |
- TabBarView의 children 개수와 일치해야 함
- 인디케이터 점의 개수 결정
3. SafeArea란?
1 | SafeArea( |
- 기기의 노치, 상태바 등을 피해서 콘텐츠 배치
- 안전한 화면 영역에만 콘텐츠 표시
시각화:
1 | ┌─────────────────────────────────┐ |
비교 예시:
1 | // Without SafeArea |
4. TabBarView란?
1 | TabBarView( |
- 스와이프로 전환 가능한 페이지 뷰
- DefaultTabController와 자동으로 연결됨
- 좌우 스와이프로 페이지 이동
작동 방식:
1 | 사용자 스와이프 → |
5. TabPageSelector란?
1 | TabPageSelector( |
- 현재 페이지를 나타내는 점(dot) 인디케이터
- DefaultTabController와 자동으로 동기화
- PageView의 현재 위치를 시각적으로 표시
시각화:
1 | 페이지 1: ● ○ ○ |
6. 페이지 빌더 메서드
1 | Widget _buildFirstPage() { |
- 각 페이지의 UI를 별도 메서드로 분리
- 코드 가독성 향상
- 재사용 가능한 구조
7. crossAxisAlignment.start
1 | Column( |
- Column의 자식들을 왼쪽 정렬
start
: 왼쪽 (LTR 언어) / 오른쪽 (RTL 언어)
비교:
1 | // crossAxisAlignment.center (기본값) |
🎨 화면 미리보기
1 | 페이지 1: |
📊 동작 흐름
1 | 1. Interests Screen에서 Next 버튼 탭 |
✅ 체크리스트
- tutorial_screen.dart 파일 생성
- DefaultTabController 설정 (length: 3)
- SafeArea로 안전 영역 설정
- TabBarView로 3개 페이지 구현
- TabPageSelector로 인디케이터 추가
- 각 페이지 빌더 메서드 구현
- interests_screen.dart에 네비게이션 추가
- Next 버튼에 GestureDetector 적용
💡 연습 과제
- 기본: 페이지를 4개로 늘려보기 (length 및 children 수정)
- 기본: TabPageSelector의 색상을 다른 색으로 변경하기
- 중급: 각 페이지의 제목과 설명 텍스트를 다르게 만들기
- 중급: 페이지마다 배경색을 다르게 설정하기
- 고급: TabController를 직접 생성해서 프로그램으로 페이지 이동하기
AnimatedCrossFade - 제스처 기반 페이지 전환
🎯 이번 단계에서 배울 것
- TabBarView를 AnimatedCrossFade로 대체하기
- GestureDetector로 드래그 제스처 감지
- Enum으로 상태 관리하기
- CupertinoButton 사용법
- AnimatedOpacity로 조건부 위젯 표시
📂 파일 구조
1 | features/onboarding/ |
📝 1단계: Enum 정의
tutorial_screen.dart 상단에 추가:
1 | import 'package:design_system/design_system.dart'; |
🔍 코드 상세 설명
1. Enum이란?
1 | enum Direction { left, right } |
- 열거형(Enumeration): 미리 정의된 값들의 집합
- 제한된 선택지를 명확하게 표현
- 오타나 잘못된 값 사용 방지
왜 사용하는가?
1 | // Without Enum (문자열 사용) |
2. Direction Enum
1 | enum Direction { left, right } |
- 스와이프 방향을 나타냄
left
: 왼쪽으로 스와이프right
: 오른쪽으로 스와이프
3. Page Enum
1 | enum Page { first, second } |
- 현재 표시 중인 페이지
first
: 첫 번째 페이지second
: 두 번째 페이지
📝 2단계: State 클래스 수정
_TutorialScreenState 변경:
1 | class _TutorialScreenState extends State<TutorialScreen> { |
🔍 코드 상세 설명
1. 상태 변수
1 | Direction _direction = Direction.right; |
_direction
: 현재 드래그 방향_showingPage
: 현재 표시 중인 페이지
2. onPanUpdate란?
1 | void _onPanUpdate(DragUpdateDetails details) { |
- 사용자가 화면을 드래그하는 동안 계속 호출되는 콜백
details
: 드래그에 대한 상세 정보
작동 방식:
1 | 사용자가 화면 터치 |
3. DragUpdateDetails란?
1 | DragUpdateDetails details |
- 드래그 이벤트의 상세 정보를 담은 객체
- 주요 속성:
delta
: 이전 프레임에서 이동한 거리globalPosition
: 화면에서의 절대 위치localPosition
: 위젯 내에서의 상대 위치
4. details.delta.dx
1 | if (details.delta.dx > 0) { |
delta
: 이전 위치에서 변화량dx
: x축 변화량 (가로 방향)dx > 0
: 오른쪽으로 이동dx < 0
: 왼쪽으로 이동
시각화:
1 | ◀──────────────────────▶ |
5. onPanEnd란?
1 | void _onPanEnd(DragEndDetails details) { |
- 사용자가 손가락을 떼었을 때 호출되는 콜백
- 드래그 완료 후 최종 처리
작동 방식:
1 | onPanUpdate (계속 호출) |
6. 페이지 전환 로직
1 | if (_direction == Direction.left) { |
흐름:
1 | 드래그 방향 확인 |
📝 3단계: Build 메서드 구현
전체 코드:
1 |
|
🔍 코드 상세 설명
1. GestureDetector를 Scaffold 밖에 배치
1 | GestureDetector( |
왜 최상위에 배치하는가?
- 전체 화면에서 제스처 감지 가능
- Scaffold 내부에 있으면 일부 영역에서만 감지됨
2. AnimatedCrossFade란?
1 | AnimatedCrossFade( |
- 두 위젯 사이를 부드럽게 전환하는 애니메이션 위젯
- 한 위젯이 사라지면서(fade out) 다른 위젯이 나타남(fade in)
작동 방식:
1 | firstChild (opacity: 1) |
비교 예시:
1 | // Without Animation (즉시 전환) |
3. CrossFadeState
1 | crossFadeState: _showingPage == Page.first |
CrossFadeState.showFirst
: 첫 번째 자식 표시CrossFadeState.showSecond
: 두 번째 자식 표시
4. CupertinoButton이란?
1 | CupertinoButton( |
- iOS 스타일 버튼 위젯
- Material 버튼보다 둥근 모서리와 부드러운 느낌
비교:
1 | // Material Button |
5. AnimatedOpacity로 조건부 표시
1 | AnimatedOpacity( |
- 첫 번째 페이지에서는 버튼 숨김 (opacity: 0)
- 두 번째 페이지에서는 버튼 표시 (opacity: 1)
작동 방식:
1 | Page.first → opacity: 0 (안 보임, 클릭 안 됨) |
📝 4단계: 페이지 빌더 수정
변경된 부분:
1 | Widget _buildFirstPage() { |
🔍 변경 사항
1. Gaps.v52 → Gaps.v80
- 상단 여백을 더 크게 조정
- SafeArea 내에서 더 나은 시각적 균형
2. 페이지 개수 3개 → 2개
- TabBarView (3페이지) → AnimatedCrossFade (2페이지)
- 더 간단한 튜토리얼 플로우
3. 설명 텍스트 변경
- 두 번째 페이지 텍스트를 구체적으로 변경
- 각 페이지의 목적을 더 명확하게 표현
🎨 화면 미리보기
1 | 페이지 1 (오른쪽으로 스와이프): |
📊 동작 흐름
1 | 1. 첫 번째 페이지 표시 |
✅ 체크리스트
- Direction과 Page enum 정의
- State 클래스에 _direction, _showingPage 상태 변수 추가
- _onPanUpdate 메서드 구현 (드래그 방향 감지)
- _onPanEnd 메서드 구현 (페이지 전환)
- GestureDetector를 Scaffold 밖에 배치
- DefaultTabController 제거
- TabBarView를 AnimatedCrossFade로 교체
- TabPageSelector 제거
- CupertinoButton 추가
- AnimatedOpacity로 버튼 조건부 표시
- _buildThirdPage() 제거
- Gaps.v52를 v80으로 변경
- 두 번째 페이지 텍스트 변경
💡 연습 과제
- 기본: AnimatedCrossFade의 duration을 500ms로 변경해보기
- 기본: 버튼 색상을 다른 색으로 변경하기
- 중급: 3개 페이지를 지원하도록 확장하기 (Page enum에 third 추가)
- 중급: 드래그 속도(velocity)를 고려해서 페이지 전환하기
- 고급: AnimatedCrossFade 대신 PageView.builder로 무한 페이지 만들기
전체 플로우 정리
완성된 기능
1 | 1. Interests Screen (관심사 선택) |
최종 파일 구조
1 | features/ |
배운 핵심 개념
1. 레이아웃
- Wrap: 자동 줄바꿈 레이아웃
- SingleChildScrollView: 스크롤 가능한 화면
- SafeArea: 안전 영역 설정
2. 애니메이션
- AnimatedOpacity: 투명도 애니메이션
- AnimatedContainer: 속성 변화 애니메이션
- AnimatedCrossFade: 위젯 전환 애니메이션
3. 상태 관리
- StatefulWidget: 변화하는 상태 관리
- setState(): UI 업데이트
- Controller 패턴: ScrollController
4. 제스처
- GestureDetector: 제스처 감지
- onPanUpdate: 드래그 중
- onPanEnd: 드래그 완료
5. 위젯
- BottomAppBar: 하단 고정 바
- TabBarView: 페이지 뷰
- CupertinoButton: iOS 스타일 버튼