Today I Learned 1회차 - Dart 기본 문법 강의 정리

3 분 소요

오늘부터 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)가 필요하다.

해결 과정:

  1. Header에서 관리하던 scorecount 변수를 Home으로 이동
  2. Home에서 이 변수들을 Header 위젯으로 props 전달
  3. CardBoard에서 부모인 Home 위젯에게 이벤트 콜백 호출
  4. 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에서 scoreHeader에 전달할 수 있도록 Header의 생성자를 수정해야 한다.

const Header({super.key, this.tryCount = 0, this.score = 0);

다음으로 Home에서 생성하는 CardBoardHome에게 이벤트를 전달할 수 있도록 콜백 함수를 전달해야 한다.

Expanded(
  child: CardBoards(
    updateTryCount: updateTryCount,
    updateScore: updateScore,
  ),
),

updateScore 함수는 다음과 같이 구현한다:

void updateScore() {
  setState(() {
    score += 100;
  });
}

마지막으로 CardBoardsonTapCard 함수에서 조건 만족 시 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 이벤트가 실행되어 정상적으로 초기화된다.

태그:

카테고리:

업데이트:

댓글남기기