[Flutter] Dart 3.0 Patterns 알아보기

S_Soo100·2023년 5월 27일
2

flutter

목록 보기
11/19
post-thumbnail

개요: 패턴이란?

패턴은 문장 및 표현식과 같은 Dart 언어의 구문 범주입니다. 패턴은 실제 값과 일치할 수 있는 값 집합의 모양을 나타냅니다.

1. 패턴이 하는 일

일반적으로 패턴은 문맥과 패턴의 형태에 따라서 값을 일치(매칭)시키거나 구조분해 한다.

  1. 매칭(패턴을 통한 일치 여부 확인)

    • 특정 형태인가? (Has a certain shape.)
    • 특정 ‘상수’인가? (Is a certain constant.)
    • 특정 값과 같은가? (Is equal to something else.)
    • 특정 타입인가? (Has a certain type.)
  2. 구조 분해

    • 패턴 구조 분해(pattern destructuring)는 편리한 ‘선언적’ 구문을 제공한다. 이 구문은 특정 값을 그 구성요소들로 분해할 수 있다.
    • 동일한 패턴을 사용하여 프로세스 안에서 해당 부분의 일부 또는 전체에 변수를 바인딩 할 수 있다.

1.1. 매칭(Matching) - 일치 여부 확인 및 하위 작업

패턴은 값이 내가 원하는 형식과 일치하는지 항시 확인한다. 즉, 값이 패턴과 일치하는지 확인하는 것이다.

매칭(일치 여부 확인)의 구성요소는 내가 사용하는 패턴의 종류에 따라 나뉜다.

예를 들어서 아래 코드에서 보이듯 상수 패턴은 주어진 값이 패턴의 상수와 같으면 매칭 한다.

switch (number) {
  // 1 == 숫자인 경우 상수 패턴 일치.
  case 1:
    print('one');
}

많은 패턴은 ‘서브 패턴’을 사용한다. 각각 외부 패턴 혹은 내부 패턴이라고 부르기도 하며 패턴은 서브 패턴에서 재귀적으로 일치한다.

예를 들어 컬렉션 유형 패턴의 개별 필드는 가변 패턴 또는 상수 패턴일 수 있습니다:

const a = 'a';
const b = 'b';
switch (obj) {
	// List 패턴 [a, b]는 객체가 두 개의 필드가 있는 목록인 경우 객체를 먼저 일치시킵니다,
	// 그 다음 필드가 상수(constant) 서브 패턴 'a'와 'b'와 일치하면 됩니다.
  case [a, b]:
    print('$a, $b');
}

일치하는 값의 일부를 무시하려면 와일드카드 패턴을 사용한다.

와일드 카드 패턴은 무시하고 싶은 값에 언더스코어 ( _ ) 를 사용한다.

그러면 List 패턴의 와일드카드를 제외한 나머지 요소를 사용할 수 있습니다.

와일드카드(Wildcard)

패턴에 배치하는 언더스코어( _ )를 ‘와일드카드’라고 부른다.

이는 변수 혹은 식별자 패턴으로 특정 변수에 바인딩 하거나 할당하지 않는다.

그리고 구조 분해를 위해 하위 패턴이 필요한 위치에 배치할 수 있다.

var list = [1, 2, 3];
var [_, two, _] = list;

와일드카드에도 타입 어노테이션을 할 수 있는데, 그러면 값의 타입만 비교한다.

switch (record) {
  case (int _, String _):
    print('First field is int and second is String.');
}

1.2. 구조 분해(Destructuring)

(자바스크립트를 했다면 너무 익숙하다)

객체의 모양과 패턴이 일치하면 패턴은 객체 안에 있는 데이터에 접근, 값을 추출해낼수 있다.

var numList = [1, 2, 3];
// List 패턴 [a, b, c]는 numList...에서 세 요소를 구조 분해 한다.
var [a, b, c] = numList;
// ...그리고 새로운 변수에 할당했고 이를 사용할 수 있다.
print(a + b + c); // 6

구조 분해 패턴 안에 모든 종류의 패턴을 중첩할 수 있습니다.

예를 들어, 아래 패턴은 2개의 요소를 가진 list이며 첫 번째 요소가 a나 b인 경우 구조분해 해서

두 번째 요소를 print하고 있다.

switch (list) {
  case ['a' || 'b', var c]:
    print(c);
}

2. 패턴을 어디에서 사용하는가?

대표적으로 아래의 경우에서 패턴을 사용할 수 있다.

  • 로컬 변수의 선언과 할당
  • 반복문(for 및 for-in)
  • if문과 switch문
  • 콜렉션 리터럴의 제어 흐름(Control flow)

2.1. 변수 선언

로컬 변수의 선언에 패턴 변수를 사용할 수 있다.

왼 쪽의 패턴과 오른 쪽의 값이 일치하면 구조분해 하여 로컬 변수에 바인딩 한다.

// a, b, c라는 새로운 변수가 선언되었다.
var (a, [b, c]) = ('str', [1, 2]);

패턴 변수 선언시 var 또는 final로 시작하고 그 뒤에 패턴을 적는다.

2.2. 변수 할당

변수 할당은 임시 변수를 선언하지 않고도 구조 분해된 두 변수의 값을 바꾸게 해준다.

var (a, b) = ('left', 'right');
(b, a) = (a, b); // 임시변수 없이 두 변수의 값을 서로에게 할당
print('$a $b'); // "right left".

2.3. Switch 구문과 표현식

if문과 switch문에도 모두 패턴을 적용할 수 있다.

case 패턴이 일치한 경우만 구조 분해하여 할당하며, 그 이외의 경우에는 넘어간다.

패턴이 case안에서 구조 분해한 값은 로컬 변수가 되며 그 스코프는 해당 case문 안으로 한정한다.

switch (obj) {
  // obj == 1 이면 일치한다.
  case 1:
    print('one');

	// 객체의 값이 'first'와 'last'의 상수 값 사이에 있는 경우 일치한다.
  case >= first && <= last:
    print('in range');

  // obj가 2개의 필드를 가진 record타입이면 일치, 그리고 2개의 필드를 a와 b에 할당한다.
  case (var a, var b):
    print('a = $a, b = $b');

  default:
}

( || )를 쓰는 Logical-or 패턴도 여러개의 case들이 본문 공유할때 유용하다.

var isPrimary = switch (color) {
  Color.red || Color.yellow || Color.blue => true,
  _ => false
};
// 이렇게 3개의 case를 한 줄로 쓸 수 있다.
// 패턴을 채용하지 않으면 아래와 같다.

var isPrimary = switch (color) {
  case Color.red :
		return true;

  case Color.yellow :
		return true;

  case Color.blue :
		return true;

	default:
	  return false;
};

switch문은 ||를 쓰지 않고도 본문을 공유하는 많은 case를 가질 수 있지만

다수의 case들이 guard기능을 공유할때는 여전히 매우 유용하다.

switch (shape) {
  case Square(size: var s) || Circle(size: var s) when s > 0:
    print('Non-empty symmetric shape');
}

(영어로 guard가 무엇인가 모르겠지만, 예시 코드를 보면 when s > 0: 을 통해 size가 0혹은 0보다 작아지는 것을 막아주고 있다)

2.4. 반복문

패턴을 통해 collection의 값을 반복하거나 구조 분해 할 수 있다.

아래 예시 코드에서는 <Map>.entries 를 통해 Map의 ****key와 value를 구조분해 해서 반복문을 돌린다.

(MapEntry와 <Map>.entries 는 [키, 값]을 한 쌍으로 하는 배열을 준다.)

Map<String, int> hist = {
  'a': 23,
  'b': 100,
};

for (var MapEntry(key: key, value: count) in hist.entries) {
  print('$key occurred $count times'); 
	// a occurred 23 times
	// b occurred 100 times
}

위 코드에서 객체 패턴은 hist.entries에 MapEntry가 있는지 확인한 다음
명명된 필드 하위 패턴인 key와 value로 재귀한다.

각 반복에서 MapEntry의 key 및 value를 호출하고 결과를 각각 로컬 변수 key 및 count에 바인딩한다.

3. 패턴 사용 사례

아래 경우에 대한 설명

  • 패턴을 사용해야 하는 시기와 이유.
  • 어떤 종류의 문제를 해결할 수 있는지.
  • 어떤 관용구에 가장 적합한지.

3.1. 구조 분해를 활용한 다수의 값 반환

Records 타입을 통해 한 번의 함수 호출로 여러개의 값을 집계하고 반환할 수 있다.

패턴은 함수 호출에 record안에 있는 필드를 로컬 변수로 구조 분해, 할당하는 기능을 추가해준다.

별도의 지역함수를 선언할 필요 없이 name과 age를 손쉽게 꺼내 쓸 수 있다.

var info = userInfo(json); // json을 해석, Records 타입의 ( var, var )등을 return
var name = info.$1;
var age = info.$2;

(여기서 한 걸음 더 나아가서 리액트 훅을 사용하는 것 처럼) 변수 선언 또는 할당 패턴과 그 하위 패턴으로 Records 패턴을 사용하여 함수가 반환하는 record의필드를 로컬 변수로 분해할 수 있습니다.

var (name, age) = userInfo(json);

3.2. class 인스턴스를 구조분해

객체 패턴(SomeClass(x: subpattern1, y: subpattern2))은 명명된 객체 유형과 매치(일치)하므로

객체의 클래스가 이미 노출한 getter를 사용하여 해당 데이터를 구조 분해 할 수 있다.

클래스를 구조 분해 하기 위해서는 각 프로퍼티의 타입을 맞춰 사용한 다음 괄호 안에 구조 분해할 프로퍼티를 입력한다.

final Foo myFoo = Foo(one: 'one', two: 2);
var Foo(:one, :two) = myFoo; // 변수 one과 two에 Foo클래스인 myFoo의 프로퍼티를 할당
print('one $one, two $two'); // one 'one', two 2

(myFoo.one, myFoo.two를 다음과 같이 줄이는 것)

3.3. 대수적(Algebraic) 데이터 타입

(Algebraic data type : 곱타입, 합타입 등 다른 타입 값을 가지는 타입)

객체의 구조 분해와 switch-case구문은 대수적 데이터 타입 스타일로 코드를 작성하는데 도움이 된다.

아래와 같은 경우 사용한다.

  • 관련된 타입의 패밀리가 있는 경우
    (You have a family of related types)
  • 각각의 타입에 대한 특정 동작이 필요한 경우
    (You have an operation that needs specific behavior for each type)
  • 다른 타입에서 일아나는 동작들을 한 곳에 그룹화 하고 싶은 경우
    (You want to group that behavior in one place instead of spreading it across all the different type definitions)

모든 타입에 대해 하나씩 연산을 구현하는 대신, 단일 함수에서 하위 타입별로 알아서 연산하게 한다.

sealed class Shape {}

class Square implements Shape {
  final double length;
  Square(this.length);
}

class Circle implements Shape {
  final double radius;
  Circle(this.radius);
}

double calculateArea(Shape shape) => switch (shape) {
      Square(length: var l) => l * l,
      Circle(radius: var r) => math.pi * r * r
    };

예시 코드의 Square도 Circle도 모두 Shape의 하위 타입이기 때문에 calculateArea는 이 두 타입 모두를 파라미터로 받을 수 있으며, switch문을 통해 타입을 알아서 구분하고 그에 맞게 연산한다.

코드를 보면 길이와 반지름을 알아서 구조 분해로 꺼내와서 각각에게 맞게 곱하고 있다.

3.5. JSON을 validation하기 위한 패턴의 사용

Map과 List타입도 JSON의 데이터가 가진 key-value구조를 아주 잘 해석한다.

var json = {
  'user': ['Lily', 13]
};
var {'user': [name, age]} = json;

위의 예시는 JSON 데이터의 구조를 우리가 이미 알고있다면 효과적이다.

하지만 데이터는 일반적으로 네트워크와 같은 외부 소스에서 가져온다.

따라서 먼저 데이터의 유효성을 검사하여 구조를 확인해야 한다.

만일 패턴이 없으면 유효성 검사는 아래 코드처럼 매우 길어진다.

if (json is Map<String, Object?> &&
    json.length == 1 &&
    json.containsKey('user')) {
  var user = json['user'];
  if (user is List<Object> &&
      user.length == 2 &&
      user[0] is String &&
      user[1] is int) {
    var name = user[0] as String;
    var age = user[1] as int;
    print('User $name is $age years old.');
  }
}

단일 case패턴(if문을 통한)만 가지고도 위와 동일한 유효성 검사를 실행 할 수 있다.

패턴은 더 간략하고 선언적인(declarative) JSON 유효성 검사법을 제공한다.

if (json case {'user': [String name, int age]}) {
  print('User $name is $age years old.');
}

이 case패턴은 아래 5가지 경우를 동시에 검증한다.

  • JSON이 맵 구조형이며 null이 아님을 검사
    (json is a map, because it must first match the outer map pattern to proceed
    - And, since it’s a map, it also confirms json is not null)
  • JSON이 user라는 key를 가지고 있는지 검사
    (json contains a key user)
  • user가 2개의 값을 가지고 있는 List인지 검사
    (The key user pairs with a list of two values)
  • List안에 있는 값들의 타입이 각각 String, int인지 검사
    (The types of the list values are String and int)
  • 새 로컬 변수가 String과 int 타입인지 검사
    (The new local variables to hold the values are String and int)
profile
플러터, 리액트

0개의 댓글