Today I Learned 1회차 - Dart 기본 문법 강의 정리
오늘부터 TIL(Today I Learned) 작성을 시작한다. 주로 Flutter와 관련된 내용을 다루겠지만, 간간히 사업 관련 인사이트도 함께 정리해 볼 예정이다.
Dart 기본 강의 수강
1. Why Flutter?
기존에는 React Native를 선택하는 가장 큰 이유가 Code Push 활용 가능성이었다. Code Push를 사용하면 앱 스토어 심사 과정을 거치지 않고도 UI 업데이트와 같은 간단한 로직을 배포된 서비스에 바로 적용할 수 있기 때문이다.
하지만 최근 Flutter에서도 Code Push와 동일한 기능을 제공하는 Shorebird
서비스가 등장했다. 이로 인해 Flutter 선택의 장벽이 한층 낮아졌다.
2. Dart 함수와 Flutter 위젯
Named Parameter
- 함수에서 named parameter를 사용하면 호출 시 인자의 순서와 상관없이 사용할 수 있다
{}
를 사용하면 named parameter를 의미하며,required
또는 optional 타입을 명시해야 한다- named parameter의 반대 개념은 positional parameter이다
Stateful Widget
- 위젯의 상태가 변경 가능한 위젯
- 동적인 UI를 구현할 때 사용
setState 함수
build
함수를 다시 호출하여 화면을 재렌더링하는 함수- 상태 변경 시 UI에 반영하기 위해 반드시 호출해야 함
3. 동기 vs 비동기
동기 (Synchronous)
- 한 작업이 완료될 때까지 다음 작업이 블록킹됨
- 순차적으로 실행
비동기 (Asynchronous)
void main() {
print('작업 1 시작');
performTask();
print('작업 1 완료');
}
Future<void> performTask() async {
await Future.delayed(Duration(seconds: 2));
print('작업 2 실행');
}
C#의 Task와 유사한 개념으로, 비동기 작업의 결과를 나타내는 타입이다.
async 키워드
- 함수에 async를 붙이면 비동기 함수가 됩니다.
- 비동기 함수는 항상 Future 객체를 반환되도록 설계 되어있습니다.
await 키워드
- await은 비동기 함수 내에서만 사용이 가능합니다.
- await은 Future가 완료될때가지 기다리며 완료되면 결과 값을 반환합니다.
- await은 비동기 코드를 동기 코드처럼 작성할 수 있게 해줍니다.
4. 위젯 트리 (Widget Tree)
샘플 앱의 위젯은 위와 같은 트리 구조로 표현된다.
핵심 포인트는 위젯이 변경되었을 때 다시 그릴 UI 영역을 지정할 수 있다는 점이다.
가장 간단한 방식은 최상단 위젯인 MyApp
을 업데이트(setState
)하는 것이지만, 이는 변경이 불필요한 UI까지 다시 그리게 되어 비효율적이다.
따라서 변경이 필요한 위젯 영역만 Stateful Widget으로 설계하는 것이 올바른 접근 방식이다.
상태 공유 문제 해결
CardWidget
을 업데이트할 때 Text Widget
을 어떻게 업데이트할 수 있을까?
상태 관리 라이브러리를 사용하면 쉽게 해결할 수 있지만, 기본 Flutter만으로는 상위 위젯을 통한 상태 끌어올리기(State Lifting)가 필요하다.
해결 과정:
Header
에서 관리하던score
와count
변수를Home
으로 이동Home
에서 이 변수들을Header
위젯으로 props 전달CardBoard
에서 부모인Home
위젯에게 이벤트 콜백 호출Home
위젯이CardBoard
의 이벤트를 받아Header
위젯에 정보 전달
void onTapCard(int cardIndex) {
print('$cardIndex 번째 카드를 선택하셨습니다.');
if (instantFirstCard == -1) {
instantFirstCard = cardIndex;
} else {
// 두번째 카드가 선택되었을때 로직 추가
widget.updateTryCount(); // 왜 앞에 widget 가 붙을까?
var firstCard = cards[instantFirstCard];
var secondCard = cards[cardIndex];
if (firstCard == secondCard) {
print('짝이 맞았습니다.');
instantFirstCard = -1;
} else {
resetInstantCards(instantFirstCard, cardIndex);
}
}
setState(() {
cardsFlippedState[cardIndex] = true;
});
}
widget 키워드 사용 이유
CardBoards
위젯 생성 시 전달받은 updateTryCount
를 호출할 때 widget
을 사용하는 이유:
StatefulWidget
을 상속받은 CardBoards
클래스의 State인 _CardBoardsState
에서 원본 위젯에 접근하기 위해 widget
키워드를 사용한다.
5. Dart 클래스 고급 기능
late 키워드
- 사용 시점에는 반드시 초기화되어 있다고 가정하므로 nullable 체크가 불필요
- 실제로는
initState
함수에서 해당 변수를 초기화 - 변수 초기화 시점을 지연시켜 화면 렌더링 성능을 최적화
- 위젯에서 무거운 변수 사용 시
late
키워드 활용 권장 - 주의: 반드시 사용 전에 초기화해야 함
List.generate 메소드
List.generate(cardsValue.length, (index) {
return CardModel(index: index, cardValue: cardsValue[index]);
});
실습 과제
1. 카드 짝 맞추기 성공 시 점수 100점 추가
점수를 100씩 올리기 위해서는 먼저 Home
에서 score
를 Header
에 전달할 수 있도록 Header
의 생성자를 수정해야 한다.
const Header({super.key, this.tryCount = 0, this.score = 0);
다음으로 Home
에서 생성하는 CardBoard
가 Home
에게 이벤트를 전달할 수 있도록 콜백 함수를 전달해야 한다.
Expanded(
child: CardBoards(
updateTryCount: updateTryCount,
updateScore: updateScore,
),
),
updateScore
함수는 다음과 같이 구현한다:
void updateScore() {
setState(() {
score += 100;
});
}
마지막으로 CardBoards
의 onTapCard
함수에서 조건 만족 시 updateScore
를 호출한다:
void onTapCard(int cardIndex) {
print('$cardIndex 번째 카드를 선택하셨습니다.');
if (instantFirstCard == -1) {
instantFirstCard = cardIndex;
} else {
widget.updateTryCount();
var firstCard = cards[instantFirstCard];
var secondCard = cards[cardIndex];
if (firstCard == secondCard) {
print('짝이 맞았습니다.');
instantFirstCard = -1;
widget.updateScore();
} else {
resetInstantCards(instantFirstCard, cardIndex);
}
}
}
2. 새 게임 버튼으로 게임 리셋 기능 구현
Header
에 리셋 버튼을 추가한다:
Expanded(
child: Container(
margin: EdgeInsets.only(top: 10, bottom: 10, left: 5, right: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
color: Color(0xffFF9999),
),
child: TextButton(
onPressed: onTabReset, child: Center(child: Text('리셋'))),
),
),
위젯 상태를 Home
에서 관리하는 현재 구조를 유지하기 위해 Header
생성자에서 콜백 함수를 받도록 수정한다:
const Header({super.key, this.tryCount = 0, this.score = 0, this.onNewGame});
다음으로 Home
에서 Header
에 게임 초기화 함수를 전달한다:
class Home extends StatefulWidget {
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
int tryCount = 0;
int score = 0;
void resetGame() {
setState(() {
tryCount = 0;
score = 0;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xffECE7E4),
appBar: AppBar(
title: const Text('짝맞추기 게임'),
backgroundColor: const Color(0xff92CBFF),
),
body: Padding(
padding: EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Header(
tryCount: tryCount,
score: score,
onNewGame: resetGame,
),
SizedBox(height: 20),
Expanded(
child: CardBoards(
updateTryCount: updateTryCount,
updateScore: updateScore,
),
),
],
),
),
);
}
}
이렇게 구현하면 Header
에서 버튼 클릭 시 Home
에서 전달받은 resetGame
이벤트가 실행되어 정상적으로 초기화된다.
댓글남기기