Dart의 패턴과 레코드 자세히 알아보기

1. 소개

Dart 3은 언어, 즉 문법의 새로운 주요 카테고리에 패턴을 도입합니다. Dart 코드를 작성하는 이 새로운 방법 외에도 일부 다른 언어의 향상된 기능(예: 다양한 유형의 데이터를 번들로 묶는 레코드, 액세스 제어를 위한 클래스 수정자 및 새로운 switch 표현식if-case 문)이 있습니다.

이러한 특징은 Dart 코드를 작성할 때 선택의 폭을 넓혀줍니다. 이 Codelab에서는 이러한 기능을 사용하여 코드를 더 작고 간소하며 유연하게 만드는 방법을 알아봅니다.

이 Codelab은 개발자가 Flutter 및 Dart와 친숙하다고 가정하지만, 필수는 아닙니다. 시작하기 전에 다음 리소스로 기본사항에 대해 알아보는 것이 좋습니다.

빌드할 항목

이 Codelab은 Flutter로 JSON 문서를 표시하는 애플리케이션을 만듭니다. 애플리케이션은 외부 소스에서 가져온 JSON을 시뮬레이션합니다. JSON에는 수정 날짜, 제목, 헤더, 문단 등 문서 데이터가 포함됩니다. Flutter 위젯이 데이터를 필요로 하는 곳마다 데이터가 전송되고 압축해제될 수 있도록 데이터를 레코드로 적절하게 압축하는 코드를 작성합니다.

그런 다음, 값이 패턴과 일치할 때 해당 패턴을 사용하여 적절한 위젯을 빌드합니다. 또한 패턴을 사용하여 데이터를 로컬 변수로 디스트럭처링하는 방법도 알아봅니다.

이 Codelab에서 빌드하는 최종 애플리케이션, 제목, 마지막 수정 날짜, 헤더와 문단이 있는 문서

학습할 내용

  • 다양한 유형의 여러 값을 저장하는 레코드를 만드는 방법
  • 레코드를 사용하여 함수에서 여러 값을 반환하는 방법
  • 패턴을 사용하여 레코드와 다른 객체의 데이터를 일치시키고 확인하고 디스트럭처링하는 방법
  • 패턴에 일치하는 값을 새 변수 또는 기존 변수에 결합하는 방법
  • 새로운 switch 문 기능, switch 표현식, if-case 문을 사용하는 방법
  • 포괄성 검사를 활용하여 모든 사례가 switch 문 또는 switch 표현식에서 처리되도록 하는 방법

2. 환경 설정

  1. Flutter SDK 설치
  2. Visual Studio Code(VS Code)와 같은 편집기 설정
  3. 최소 하나의 타겟 플랫폼(iOS, Android, 데스크톱 또는 웹브라우저)에 맞는 플랫폼 설정 단계 진행

3. 프로젝트 만들기

패턴, 레코드, 기타 새로운 기능을 알아보기 전에 잠시 시간을 내어 코드를 작성할 환경과 간단한 Flutter 프로젝트를 설정합니다.

Dart 가져오기

  • Dart 3을 사용하고 있는지 확인하려면 다음 명령어를 실행합니다.
flutter channel stable
flutter upgrade
dart --version # This should print "Dart SDK version: 3.0.0" or higher

Flutter 프로젝트 만들기

  1. flutter create 명령어를 사용하여 patterns_codelab이라는 새 프로젝트를 만듭니다. --empty 플래그를 사용하면 lib/main.dart 파일에 있는 표준 Counter 앱(내용을 삭제해야 함)을 만들지 않습니다.
flutter create --empty patterns_codelab
  1. 그런 다음, VS Code를 사용하여 patterns_codelab 디렉터리를 엽니다.
code patterns_codelab

'flutter create' 명령어로 만든 프로젝트를 표시하는 VS Code 스크린샷

최소 SDK 버전 설정

  • Dart 3 이상을 사용하도록 프로젝트에 SDK 버전 제약사항 설정

pubspec.yaml

environment:
  sdk: ^3.0.0

4. 프로젝트 설정

이번 단계에서는 두 개의 Dart 파일을 만듭니다.

  • main.dart 파일: 앱의 위젯 포함
  • data.dart 파일: 앱 데이터 제공

앱 데이터 정의

  • lib/data.dart라는 새 파일을 만들어 다음 코드를 추가합니다.

lib/data.dart

import 'dart:convert';

class Document {
  final Map<String, Object?> _json;
  Document() : _json = jsonDecode(documentJson);
}

const documentJson = '''
{
  "metadata": {
    "title": "My Document",
    "modified": "2023-05-10"
  },
  "blocks": [
    {
      "type": "h1",
      "text": "Chapter 1"
    },
    {
      "type": "p",
      "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
    },
    {
      "type": "checkbox",
      "checked": false,
      "text": "Learn Dart 3"
    }
  ]
}
''';

I/O 스트림 또는 HTTP 요청과 같은 외부 소스에서 데이터를 수신하는 프로그램을 상상해 보세요. 이 Codelab에서는 documentJson 변수에 여러 줄의 문자열이 포함된 JSON 수신 데이터를 모의 처리하여 더 현실적인 사용 사례를 간소화합니다.

이 JSON 데이터는 Document 클래스에 정의되어 있고 이 Codelab 후반부에서는 파싱된 JSON에서 데이터를 반환하는 함수를 추가합니다. 이 클래스는 생성자에서 _json 필드를 정의하고 초기화합니다.

앱 실행

flutter create 명령어는 기본 Flutter 파일 구조의 일부로 lib/main.dart 파일을 만듭니다.

  1. 애플리케이션의 시작점을 만들려면 main.dart의 콘텐츠를 아래의 코드로 교체하세요.

lib/main.dart

import 'package:flutter/material.dart';

import 'data.dart';

void main() {
 runApp(const DocumentApp());
}

class DocumentApp extends StatelessWidget {
 const DocumentApp({super.key});

 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     theme: ThemeData(useMaterial3: true),
     home: DocumentScreen(
       document: Document(),
     ),
   );
 }
}

class DocumentScreen extends StatelessWidget {
 final Document document;

 const DocumentScreen({
   required this.document,
   Key? key,
 }) : super(key: key);

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: const Text('Title goes here'),
     ),
     body: Column(
       children: [
         Center(
           child: Text('Body goes here'),
         ),
       ],
     ),
   );
 }
}

다음 두 개의 위젯을 앱에 추가했습니다.

  • DocumentApp: UI의 테마를 지정하기 위해 최신 버전의 Material Design을 설정합니다.
  • DocumentScreen: Scaffold 위젯을 사용하여 페이지의 시각적 레이아웃을 제공합니다.
  1. 모든 것이 원활하게 실행되는지 확인하려면 Run and Debug(실행 및 디버그)를 클릭하여 호스트 머신에서 앱을 실행합니다.

왼쪽에 있는 작업표시줄의 'Run and debug'(실행 및 디버그) 섹션에서 제공하는 'Run and debug'(실행 및 디버그) 버튼

  1. 기본적으로 Flutter는 어느 타겟 플랫폼이든지 사용할 수 있는 플랫폼을 선택합니다. 타겟 플랫폼을 변경하려면 상태 표시줄에서 현재 플랫폼을 선택하세요.

VS Code의 타겟 플랫폼 선택기 스크린샷

DocumentScreen 위젯에 정의된 titlebody 요소가 포함된 빈 프레임이 표시됩니다.

이 단계에서 빌드된 애플리케이션의 스크린샷

5. 레코드 생성 및 반환

이 단계에서는 레코드를 사용하여 함수 호출에서 여러 값을 반환합니다. 그런 다음, 값에 액세스하고 UI에 레코드를 반영하도록 DocumentScreen 위젯에서 해당 함수를 호출합니다.

레코드 생성 및 반환

  • data.dart에서 Document 클래스에 하나의 레코드를 반환하는 getMetadata라는 새 함수를 추가합니다.

lib/data.dart

(String, {DateTime modified}) getMetadata() {
  var title = "My Document";
  var now = DateTime.now();

  return (title, modified: now);
}

이 함수의 반환 유형은 두 개의 필드를 가진 레코드로, 하나의 유형은 String이고 다른 하나의 유형은 DateTime입니다.

return 문은 괄호 안의 두 개의 값을 묶어(예: (title, modified: now)) 새 레코드를 구성합니다.

첫 번째 필드는 위치로 나타내고 이름이 지정되어 있지 않으며 두 번째 필드는 modified라고 이름이 지정되어 있습니다.

레코드 필드 액세스

  1. DocumentScreen 위젯에서는 레코드를 가져와서 레코드 값에 액세스할 수 있도록 build 메서드에서 getMetadata()을 호출합니다.

lib/main.dart

  @override
  Widget build(BuildContext context) {
    var metadataRecord = document.getMetadata();

    return Scaffold(
      appBar: AppBar(
        title: Text(metadataRecord.$1),
      ),
      body: Column(
        children: [
          Center(
            child: Text(
              'Last modified ${metadataRecord.modified}',
            ),
          ),
        ],
      ),
    );
  }

getMetadata() 함수는 레코드를 반환하며 이 레코드는 로컬 변수 metadataRecord에 할당되어 있습니다. 레코드는 하나의 함수 호출로 여러 값을 반환하여 하나의 변수에 할당할 수 있는 가볍고 쉬운 방법입니다.

레코드에 구성된 개별 필드에 액세스하려면 레코드의 기본 getter 문법을 사용하면 됩니다.

  • 위치로 지정된 필드(title처럼 이름이 없는 필드)를 가져오려면 레코드에 getter $<num>을 사용하세요. 그러면 이름이 지정되지 않은 필드만 반환됩니다.
  • modified처럼 이름이 지정된 필드는 위치 getter가 없으므로 metadataRecord.modified와 같이 이름을 직접 사용하면 됩니다.

위치 필드에 대해 getter의 이름을 확인하려면 $1에서 시작하고 이름이 지정된 필드를 건너뛰면 됩니다. 예:

var record = (named: v', ‘y', named2: x', ‘z');
print(record.$1); // prints y
print(record.$2) // prints z
  1. 앱에 표시된 JSON 값을 확인하려면 핫 리로드하세요. VS Code Dart 플러그인은 파일을 저장할 때마다 핫 리로드됩니다.

제목과 수정된 날짜를 표시하는 앱의 스크린샷

각 필드에서 실행한 작업을 확인할 수 있습니다(즉, 필드 유형을 유지).

  • Text() 메서드는 문자열을 첫 번째 인수로 사용합니다.
  • modified 필드는 DateTime이고 문자열 보간 유형을 사용하여 String으로 변환됩니다.

다양한 유형의 데이터를 유형에 안전하게 반환하는 다른 방법은 더 상세한 정보를 가진 클래스를 정의하는 것입니다.

6. 패턴 일치 및 디스트럭처링

레코드는 효율적으로 다양한 유형의 데이터를 수집하고 쉽게 주변에 전달합니다. 이제 패턴을 사용하여 코드를 개선하세요.

패턴은 하나 이상의 값이 사용할 수 있는 구조(예: 청사진)를 나타냅니다. 패턴은 실제 값과 비교하여 서로 일치하는지 확인합니다.

패턴과 값이 일치하는 일부 패턴은 패턴에서 데이터를 추출하여 일치하는 값을 디스트럭처링합니다. 디스트럭처링 방식을 사용하면 객체에서 값을 압축해제하여 로컬 변수에 할당하거나 값에 매칭을 계속 실행합니다.

로컬 변수에 레코드 디스트럭처링

  1. DocumentScreenbuild 메서드를 리팩터링하여 getMetadata()를 호출하고 리팩터링한 메서드를 사용하여 패턴 변수 선언을 초기화합니다.

lib/main.dart

  @override
  Widget build(BuildContext context) {
    var (title, :modified) = document.getMetadata(); // New

    return Scaffold(
      appBar: AppBar(
        title: Text(title), // New
      ),
      body: Column(
        children: [
          Center(
            child: Text(
              'Last modified $modified', // New
            ),
          ),
        ],
      ),
    );
  }

레코드 패턴 (title, :modified)에는 getMetadata()에서 반환하는 레코드 필드와 일치하는 두 개의 변수 패턴이 포함됩니다.

  • 결과는 두 개의 필드(이 중 하나는 modified로 이름이 지정된 필드)가 포함된 레코드이므로 표현식은 하위 패턴과 일치합니다.
  • 표현식과 하위 패턴이 일치하므로 변수 선언 패턴은 표현식을 디스트럭처링하며 값에 액세스하여 같은 유형과 이름(String title, DateTime modified)의 새로운 로컬 변수로 결합합니다.

변수 패턴 :modified의 문법은 modified: modified의 약식 표기입니다. 다른 이름의 새 로컬 변수를 만들려면 modified: localModified라고 쓰면 됩니다.

  1. 이전 단계와 같은 결과를 보려면 핫 리로드하세요. 동작은 정확히 동일합니다. 코드를 더 간결하게 만들었을 뿐입니다.

7. 패턴을 사용하여 데이터 추출

특정 문맥에서 패턴은 일치하는지 확인하고 디스트럭처링하는 것뿐만 아니라 패턴이 일치하는지 여부에 따라 코드가 해야 하는 작업에 관해 결정도 합니다. 이를 반박 가능 패턴(refutable pattern)이라고 합니다.

앞선 단계에서 사용한 변수 선언 패턴은 반박 불가 패턴(irrefutable pattern)입니다. 값은 패턴과 일치해야 하며 그렇지 않으면 오류입니다. 이 경우 디스트럭처링하지 않습니다. 모든 변수 선언이나 할당을 생각해 보세요. 같은 유형이 아니면 값을 변수에 할당할 수 없습니다.

반면, 반박 가능 패턴은 다음과 같이 제어 흐름 문맥에서 사용됩니다.

  • 이 패턴은 비교하려는 값이 일치하지 않을 것이라고 예상합니다.
  • 즉, 값이 일치하는지에 따라 제어 흐름에 영향을 줍니다.
  • 이 패턴은 값이 일치하지 않아도 오류로 인해 실행이 중단되지 않습니다. 곧바로 다음 문으로 이동합니다.
  • 패턴이 일치할 때만 사용할 수 있는 변수를 디스트럭처링하고 결합하게 됩니다.

패턴을 사용하지 않고 JSON 값 읽기

이 섹션에서는 JSON 데이터와 연동하는 데 패턴을 어떻게 사용할 수 있을지 알아보기 위해 패턴 매칭을 사용하지 않고 데이터를 읽습니다.

  • 이전 버전의 getMetadata()_json 맵에서 값을 읽어오는 버전으로 교체합니다. 이 버전의 getMetadata()Document 클래스에 복사하여 붙여넣습니다.

lib/data.dart

(String, {DateTime modified}) getMetadata() {
  if (_json.containsKey('metadata')) {
    var metadataJson = _json['metadata'];
    if (metadataJson is Map) {
      var title = metadataJson['title'] as String;
      var localModified = DateTime.parse(metadataJson['modified'] as String);
      return (title, modified: localModified);
    }
  }
  throw const FormatException('Unexpected JSON');
}

이 코드는 데이터가 패턴을 사용하지 않고 올바르게 구조화되어 있는지 확인합니다. 이후 단계에서는 더 적은 코드를 사용하여 동일한 확인 작업을 실행하기 위해 패턴 매칭을 사용합니다. 다른 작업을 하기 전에 세 가지 검사를 실행합니다.

  • JSON에는 예상 데이터 구조(예: if (_json.containsKey('metadata')))가 포함됩니다.
  • 데이터는 예상 유형(예: if (metadataJson is Map))이 있습니다.
  • 이전 검사에서 암시적으로 확인된 데이터는 null이 아닙니다.

맵 패턴을 사용하여 JSON 값 읽기

반박 가능 패턴을 통해 JSON이 맵 패턴을 사용한 예상 구조를 포함하고 있는지 확인할 수 있습니다.

  • 이전 버전의 getMetadata()를 다음 코드로 교체합니다.

lib/data.dart

  (String, {DateTime modified}) getMetadata() {
    if (_json
        case {
          'metadata': {
            'title': String title,
            'modified': String localModified,
          }
        }) {
      return (title, modified: DateTime.parse(localModified));
    } else {
      throw const FormatException('Unexpected JSON');
    }
  }

여기에서 Dart 3에 도입된 새로운 종류의 if 문, if-case를 볼 수 있습니다. case 본문은 사례 패턴이 _json의 데이터와 일치할 때만 실행됩니다. 이러한 일치는 수신된 JSON을 확인하기 위해 첫 번째 버전의 getMetadata()에서 작성한 검사와 동일한 검사를 사용합니다. 이 코드는 다음을 확인합니다.

  • _json이 Map 유형임
  • _jsonmetadata 키가 포함됨
  • _json은 null이 아님
  • _json['metadata']도 Map 유형임
  • _json['metadata']titlemodified 키가 포함됨
  • titlelocalModified는 문자열이며 null이 아님

값이 일치하지 않으면 패턴은 반박하며(실행을 계속하지 않음) else 절을 진행합니다. 성공적으로 일치하는 경우 패턴은 titlemodified 값을 맵에서 디스트럭처링하고 새로운 로컬 변수로 결합합니다.

패턴의 전체 목록은 기능 사양의 패턴 섹션에 있는 표를 참고하세요.

8. 더 많은 패턴을 위한 앱 준비

지금까지 JSON 데이터의 metadata 부분을 해결했습니다. 이 단계에서는 blocks 목록의 데이터를 처리하고 앱에 이 데이터를 렌더링하기 위해 비즈니스 로직을 약간 미세하게 조정합니다.

{
  "metadata": {
    // ...
  },
  "blocks": [
    {
      "type": "h1",
      "text": "Chapter 1"
    },
    // ...
  ]
}

데이터를 저장하는 클래스 만들기

  • JSON 데이터의 블록 중 하나에 맞게 데이터를 읽고 저장하는 데 사용할 새 클래스(Block)를 data.dart에 추가합니다.

lib/data.dart

class Block {
  final String type;
  final String text;
  Block(this.type, this.text);

  factory Block.fromJson(Map<String, dynamic> json) {
    if (json case {'type': var type, 'text': var text}) {
      return Block(type, text);
    } else {
      throw const FormatException('Unexpected JSON format');
    }
  }
}

팩토리 생성자 fromJson()은 앞서 본 맵 패턴과 동일한 if-case를 사용합니다.

키 중 하나인 checked가 패턴에서 처리되지 않더라도 json은 맵 패턴과 일치합니다. 맵 패턴은 패턴에서 명시적으로 처리하지 않는 맵 객체의 모든 항목을 무시합니다.

블록 객체 목록 반환

  • 다음으로 새 함수 getBlocks()Document 클래스에 추가합니다. getBlocks()는 JSON을 Block 클래스의 인스턴스로 파싱하고 블록 목록을 반환하여 UI에 렌더링합니다.

lib/data.dart

  List<Block> getBlocks() {
    if (_json case {'blocks': List blocksJson}) {
      return <Block>[
        for (var blockJson in blocksJson) Block.fromJson(blockJson)
      ];
    } else {
      throw const FormatException('Unexpected JSON format');
    }
  }

getBlocks() 함수는 UI를 빌드하기 위해 나중에 사용할 Block 객체의 목록을 반환합니다. 익숙한 if-case 문은 검사를 실행하고 blocks 메타데이터 값을 blocksJson이라는 새 List에 전송합니다(패턴을 사용하지 않음, 전송할 toList() 메서드 필요).

목록 리터럴에는 Block 객체로 새 목록을 채우기 위해 컬렉션 for가 포함됩니다.

이 섹션에서는 이 Codelab에서 이미 시도했던 패턴 관련 기능을 사용하지 않습니다. 다음 단계에서는 UI에 목록 항목을 렌더링할 준비를 합니다.

9. 패턴을 사용하여 문서 표시

이제 if-case 문과 반박 가능 패턴을 사용하여 JSON 데이터를 성공적으로 디스트럭처링하고 재구성합니다. 하지만 if-case는 패턴을 사용하는 제어 흐름 구조를 개선하는 방법 중 하나일 뿐입니다. 이제 반박 가능 패턴에 관한 지식을 switch 문에 적용해 보세요.

switch 문으로 패턴을 사용하여 렌더링 항목 제어

  • main.dart에 새 위젯 BlockWidget을 만들어 위젯의 type 필드에 따라 각 블록의 스타일을 결정합니다.

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

  const BlockWidget({
    required this.block,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    TextStyle? textStyle;
    switch (block.type) {
      case 'h1':
        textStyle = Theme.of(context).textTheme.displayMedium;
      case 'p' || 'checkbox':
        textStyle = Theme.of(context).textTheme.bodyMedium;
      case _:
        textStyle = Theme.of(context).textTheme.bodySmall;
    }

    return Container(
      margin: const EdgeInsets.all(8),
      child: Text(
        block.text,
        style: textStyle,
      ),
    );
  }
}

build 메서드의 switch 문은 block 객체의 type 필드에 따라 전환됩니다.

  1. 첫 번째 case 문은 상수 문자열 패턴을 사용합니다. block.type이 상숫값 h1과 같으면 패턴은 일치합니다.
  2. 두 번째 case 문은 하위 패턴과 마찬가지로 두 개의 상수 문자열 패턴이 있는 논리 연산 OR 패턴을 사용합니다. block.type이 하위 패턴 p 또는 checkbox 중 하나와 일치하면 패턴은 일치합니다.
  1. 마지막 case는 와일드 카드 패턴 _입니다. switch 문의 case에서 와일드 카드는 모든 것과 일치합니다. 와일드 카드는 switch 문에서 계속 허용되고 있는 default 절(약간 더 상세해짐)과 동일하게 작동합니다.

와일드 카드 패턴은 패턴이 허용되는 곳은 어디서나 사용할 수 있습니다(예: var (title, _) = document.getMetadata();와 같은 변수 선언 패턴).

이 문맥에서 와일드 카드는 변수와 결합하지 않습니다. 두 번째 필드를 삭제합니다.

다음 섹션에서는 Block 객체를 표시한 후 더 많은 switch 기능을 알아봅니다.

문서 콘텐츠 표시

DocumentScreen 위젯의 build 메서드에서 getBlocks()를 호출하여 Block 객체 목록을 포함하는 로컬 변수를 만듭니다.

  1. DocumentationScreen에 있는 기존의 build 메서드는 아래 버전으로 교체합니다.

lib/main.dart

  @override
  Widget build(BuildContext context) {
    var (title, :modified) = document.getMetadata();
    var blocks = document.getBlocks(); // New

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Column(
        children: [
          // New
          Text('Last modified: $modified'),
          Expanded(
            child: ListView.builder(
              itemCount: blocks.length,
              itemBuilder: (context, index) {
                return BlockWidget(block: blocks[index]);
              },
            ),
          ),
        ],
      ),
    );
  }

BlockWidget(block: blocks[index]) 줄은 getBlocks() 메서드에서 반환하는 블록 목록의 항목마다 BlockWidget 위젯을 생성합니다.

  1. 애플리케이션을 실행한 후 화면에 블록이 표시되는 것을 볼 수 있습니다.

JSON 데이터의 'blocks' 섹션에 있는 콘텐츠를 표시하는 앱의 스크린샷

10. switch 표현식 사용

패턴은 switchcase에 많은 기능을 추가합니다. 더 많은 위치에서 이 기능을 유용하게 만들기 위해 Dart는 switch 표현식을 제공합니다. 일련의 case 문은 변수 할당 또는 return 문에 직접 값을 제공할 수 있습니다.

switch 문을 switch 표현식으로 변환

Dart 분석기는 코드에 변경사항을 적용할 수 있도록 지원합니다.

  1. 이전 섹션의 switch 문으로 커서를 이동합니다.
  2. 제공되는 지원을 확인하려면 전구 모양 아이콘을 클릭하세요.
  3. Convert to switch expression(switch 표현식으로 변환) 지원을 선택합니다.

VS Code에서 제공하는 'convert to switch expression'(switch 표현식으로 변환) 지원 스크린샷

이 코드의 새 버전은 다음과 같습니다.

TextStyle? textStyle;
textStyle = switch (block.type) {
  'h1' => Theme.of(context).textTheme.displayMedium,
  'p' || 'checkbox' => Theme.of(context).textTheme.bodyMedium,
  _ => Theme.of(context).textTheme.bodySmall
};

switch 표현식은 switch 문과 비슷해 보입니다. 하지만 case 키워드를 제거했고 case 본문에서 패턴을 분리하기 위해 =>를 사용합니다. switch 문과 달리 switch 표현식은 값을 반환하고 표현식을 사용할 수 있는 곳 어디에서나 사용할 수 있습니다.

11. 객체 패턴 사용

Dart는 객체 지향 언어이므로 모든 객체에 패턴이 적용됩니다. 이 단계에서는 UI의 날짜 렌더링 로직을 개선하기 위해 객체 패턴을 변환하고 객체 속성을 디스트럭처링합니다.

객체 패턴에서 속성 추출

이 섹션에서는 패턴을 사용하여 마지막으로 수정된 날짜를 표시하는 방식을 개선합니다.

  • formatDate 메서드를 main.dart에 추가합니다.

lib/main.dart

String formatDate(DateTime dateTime) {
  var today = DateTime.now();
  var difference = dateTime.difference(today);

  return switch (difference) {
    Duration(inDays: 0) => 'today',
    Duration(inDays: 1) => 'tomorrow',
    Duration(inDays: -1) => 'yesterday',
    Duration(inDays: var days, isNegative: true) => '${days.abs()} days ago',
    Duration(inDays: var days) => '$days days from now',
  };
}

이 메서드는 difference 값에 따라 변환되는 switch 표현식, Duration 객체를 반환합니다. 이 객체는 JSON 데이터에서 todaymodified 값 사이의 기간을 나타냅니다.

switch 표현식의 각 사례는 객체의 inDaysisNegative 속성에 대해 getter를 호출하여 일치하는 객체 패턴을 사용하고 있습니다. 이 구문은 Duration 객체를 생성할 수 있는 것처럼 보이지만 실제로는 difference 객체의 필드에 액세스하는 것입니다.

첫 번째 세 개의 사례는 객체 속성 inDays와 일치하는 상수 하위 패턴 0, 1, -1을 사용하고 이에 대응하는 문자열을 반환합니다.

마지막 두 개의 사례는 오늘, 어제, 내일 이상의 기간을 처리합니다.

  • isNegative 속성이 불리언 상수 패턴 true와 일치하면, 즉 수정된 날짜가 과거이면 며칠 전인지 표시됩니다.
  • 이 경우 차이를 포착하지 않으면 기간은 양수로 된 일수가 되어야 하므로(isNegative: false를 사용하여 명시적으로 확인할 필요는 없음) 수정된 날짜는 미래이고 지금부터의 일수를 표시합니다.

주(week)에 형식을 지정하는 로직 추가

  • UI가 7일이 넘는 기간을 로 표시하도록 이 기간을 식별하기 위해 새로운 두 가지 사례를 형식 지정 함수에 추가합니다.

lib/main.dart

String formatDate(DateTime dateTime) {
  var today = DateTime.now();
  var difference = dateTime.difference(today);

  return switch (difference) {
    Duration(inDays: 0) => 'today',
    Duration(inDays: 1) => 'tomorrow',
    Duration(inDays: -1) => 'yesterday',
    Duration(inDays: var days) when days > 7 => '${days ~/ 7} weeks from now', // New
    Duration(inDays: var days) when days < -7 => '${days.abs() ~/ 7} weeks ago', // New
      Duration(inDays: var days, isNegative: true) => '${days.abs()} days ago',
      Duration(inDays: var days) => '$days days from now',
  };
}

이 코드는 검사 절을 사용합니다.

  • 검사 절은 케이스 패턴 뒤에 when 키워드를 사용합니다.
  • 검사 절은 if-case 문, switch 문, switch 표현식에서 사용할 수 있습니다.
  • 검사 절은 패턴이 일치한 후에만 조건을 패턴에 추가합니다.
  • 검사 절이 false이면 전체 패턴을 반박하고 실행은 다음 사례로 넘어갑니다.

UI에 새로 형식이 지정된 날짜 추가

  1. 마지막으로, DocumentScreenbuild 메서드를 업데이트하여 formatDate 함수를 사용합니다.

lib/main.dart

  @override
  Widget build(BuildContext context) {
    var (title, :modified) = document.getMetadata();
    var formattedModifiedDate = formatDate(modified); // New
    var blocks = document.getBlocks();

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Column(
        children: [
          Text('Last modified: $formattedModifiedDate'), // New
          Expanded(
            child: ListView.builder(
              itemCount: blocks.length,
              itemBuilder: (context, index) =>
                BlockWidget(block: blocks[index]),
            ),
          ),
        ],
      ),
    );
  }
  1. 앱에서 변경사항을 확인하려면 다음과 같이 핫 리로드합니다.

formatDate() 함수를 사용하여 'Last modified: 2 weeks ago' 문자열을 표시하는 앱의 스크린샷

12. 포괄적 switch를 위한 클래스 봉인

마지막 switch의 끝부분에는 와일드 카드나 기본 사례를 사용하지 않았습니다. 실행되지 못할 수도 있는 값에 대한 사례를 항상 포함하는 것은 좋지만, 정의한 사례가 사용할 가능성이 있는 모든 가능한 값 inDays를 처리하는 것을 알고 있으므로 이와 같은 간단한 예에서는 포함하지 않아도 괜찮습니다.

switch 문에서 모든 사례가 처리되는 경우 이를 포괄적 switch라고 합니다. 예를 들어, switch 문에 truefalse에 대한 사례가 있다면 bool 유형에 따른 전환은 포괄적입니다. 마찬가지로 enum 값 각각에 대해 사례가 있는 경우 enum 유형에 따른 전환은 포괄적입니다. enum은 상숫값고정된 숫자를 나타내기 때문입니다.

Dart 3은 새 클래스 수정자인 sealed를 사용하여 객체와 클래스 계층 구조에 대한 포괄성 검사를 확장했습니다. Block 클래스를 봉인된 슈퍼클래스로 리팩터링하세요.

서브클래스 만들기

  • data.dartBlock을 확장한 세 개의 새로운 클래스, HeaderBlock, ParagraphBlock, CheckboxBlock을 만듭니다.

lib/data.dart

class HeaderBlock extends Block {
  final String text;
  HeaderBlock(this.text);
}

class ParagraphBlock extends Block {
  final String text;
  ParagraphBlock(this.text);
}

class CheckboxBlock extends Block {
  final String text;
  final bool isChecked;
  CheckboxBlock(this.text, this.isChecked);
}

이러한 클래스 각각은 원본 JSON의 다양한 type 값('h1', 'p', 'checkbox')에 대응합니다.

슈퍼클래스 봉인

  • Block 클래스를 sealed로 표시합니다. 그런 다음, if-case를 switch 표현식으로 리팩터링하여 JSON에 지정된 type에 대응하는 서브클래스를 반환하도록 합니다.

lib/data.dart

sealed class Block {
  Block();

  factory Block.fromJson(Map<String, Object?> json) {
    return switch (json) {
      {'type': 'h1', 'text': String text} => HeaderBlock(text),
      {'type': 'p', 'text': String text} => ParagraphBlock(text),
      {'type': 'checkbox', 'text': String text, 'checked': bool checked} =>
        CheckboxBlock(text, checked),
      _ => throw const FormatException('Unexpected JSON format'),
    };
  }
}

sealed 키워드는 클래스 수정자로, 같은 라이브러리에서만 이 클래스를 확장하거나 구현할 수 있음을 의미합니다. 분석기에서 이 클래스의 하위유형을 알고 있으므로 switch 문에서 하위유형 중 하나를 처리하는 데 실패하거나 switch 문이 포괄적이지 않으면 오류가 보고됩니다.

위젯을 표시하기 위해 switch 표현식 사용

  1. 각 사례에 대해 객체 패턴을 사용하는 switch 표현식으로 main.dart의 BlockWidget 클래스를 업데이트합니다.

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

  const BlockWidget({
    required this.block,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.all(8),
      child: switch (block) {
        HeaderBlock(:var text) => Text(
          text,
          style: Theme.of(context).textTheme.displayMedium,
        ),
        ParagraphBlock(:var text) => Text(text),
        CheckboxBlock(:var text, :var isChecked) => Row(
          children: [
            Checkbox(value: isChecked, onChanged: (_) {}),
            Text(text),
          ],
        ),
      },
    );
  }
}

BlockWidget의 첫 번째 버전에서는 TextStyle을 반환하기 위해 Block 객체의 필드에 따라 switch 문을 적용했습니다. 이제 Block 객체 자체의 인스턴스에 따라 swtich 표현식을 적용하고 서브클래스를 나타내는 객체 패턴에 일치하는지 확인하여 이 과정에서 객체의 속성을 추출합니다.

Dart 분석기는 Block이 봉인 클래스가 됐기 때문에 switch 표현식에서 각 서브클래스를 처리하는지 검사할 수 있습니다.

또한, 여기에서 switch 표현식을 사용하면 전에는 별도의 반환 문이 필요했던 것과 달리 결과를 child 요소에 바로 전달할 수 있습니다.

  1. 처음에 렌더링된 체크박스 JSON 데이터를 보려면 핫 리로드하세요.

'Learn Dart 3' 체크박스를 표시하는 앱 스크린샷

13. 축하합니다

패턴, 레코드, 개선된 switch와 case 및 봉인 클래스를 사용한 실험을 성공적으로 마쳤습니다. 많은 내용을 다루었지만 이러한 기능을 간략하게 살펴본 것에 불과합니다. 패턴에 관한 자세한 내용은 기능 사양을 참고하세요.

다양한 패턴 유형과 패턴이 표시될 수 있는 다양한 문맥 및 하위 패턴의 잠재적 중첩은 수많은 동작의 가능성을 만들어 냅니다. 또한, 확인하기도 쉽습니다.

패턴을 사용하여 Flutter로 콘텐츠를 표현하는 모든 종류의 방법을 상상해 볼 수 있습니다. 패턴을 사용하면 몇 줄의 코드로 UI를 빌드할 수 있도록 데이터를 안전하게 추출할 수 있습니다.

다음 단계

  • Dart 문서의 언어 섹션에서 패턴, 레코드, 개선된 switch와 case, 클래스 수정자에 관한 문서를 확인해 보세요.

참조 문서

저장소에서 전체 샘플을 확인해 보세요.

각각의 새로운 기능에 대한 자세한 사양은 아래의 원본 디자인 문서를 확인해 보세요.