develop/Flutter

Dart의 고오급 함수를 적절히 사용하여 코드 퀄리티 높이기

방뎁 2024. 11. 17. 22:22

1. 고차 함수 (Higher-order Functions)

고차 함수는 다른 함수를 인자로 받거나, 함수를 반환하는 함수임
Dart에서는 고차 함수를 사용하여 코드를 더욱 유연하고 재사용 가능하게 만들 수 있음

1.1 함수 전달

void executeFunction(String message, Function(String) callback) {
  callback(message);
}

void printMessage(String message) {
  print(message);
}

void main() {
  executeFunction("Hello, Dart!", printMessage); // Hello, Dart!
}

1.2 함수 반환

Function makeMultiplier(int multiplier) {
  return (int value) => value * multiplier;
}

void main() {
  var triple = makeMultiplier(3);
  print(triple(4)); // 12
}

함수를 반환하여 재사용 가능한

예를 들어 API에서 특정한 값이 들어올 때, 변환하여 사용하거나
계산하여 사용해야 할때 유용하며
재사용이 가능하기 때문에 중복 코드를 줄일 수 있음

2. 익명 함수 (Anonymous Functions)

간단한 작업을 위해 함수 본문을 인라인으로 정의함
불필요한 함수 정의를 줄임

2.1 기본 사용법

void main() {
  var numbers = [1, 2, 3, 4];
  numbers.forEach((number) {
    print(number * 2); // 2, 4, 6, 8
  });
}

2.2 축약 표현 (Fat Arrow)

한 줄로 표현할 수 있는 익명 함수는 화살표(=>)로 축약하여 사용

void main() {
  var numbers = [1, 2, 3, 4];
  numbers.forEach((number) => print(number * 2));
}

3. 클로저 (Closures)

클로저는 자신이 정의된 스코프 외부의 변수를 참조할 수 있는 함수입니다.
Dart의 클로저는 변수를 캡처하여, 외부 함수의 실행이 끝난 후에도 해당 변수에 접근 가능

Function makeCounter() {
  int count = 0;

  return () {
    count++;
    return count;
  };
}

void main() {
  var counter = makeCounter();
  print(counter()); // 1
  print(counter()); // 2
  print(counter()); // 3
}

4. 컬렉션 조작 함수

List, Map, Set 의 사용

4.1 map

map은 컬렉션의 각 요소를 변환하여 새로운 컬렉션을 반환

void main() {
  var numbers = [1, 2, 3];
  var doubled = numbers.map((n) => n * 2).toList();
  print(doubled); // [2, 4, 6]
}

4.2 where

where는 조건에 맞는 요소만 필터링 됨

void main() {
  var numbers = [1, 2, 3, 4, 5];
  var evenNumbers = numbers.where((n) => n.isEven).toList();
  print(evenNumbers); // [2, 4]
}

4.3 reduce

reduce는 컬렉션을 단일 값으로 축약 되어 값을 반환

void main() {
  var numbers = [1, 2, 3, 4];
  var sum = numbers.reduce((a, b) => a + b);
  print(sum); // 10
}

값을 모두 더하거나 뺄 때 특히 유용함

4.4 fold

foldreduce와 유사하지만, 초기 값을 제공할 수 있음

void main() {
  var numbers = [1, 2, 3, 4];
  var result = numbers.fold(10, (a, b) => a +b);
  print(result); // 20
}

즉 초기값 + reduce 함수 실행 되는 형태

5. Generator Functions

Dart에서는 sync*async* 키워드를 사용해 제너레이터 함수를 정의 가능
데이터를 한 번에 반환하지 않고, 필요할 때마다 생성되어 메모리에 유리한 편

5.1 sync* (동기 제너레이터)

Iterable<int> generateNumbers(int count) sync* {
  for (int i = 1; i <= count; i++) {
    yield i;
  }
}

void main() {
  var numbers = generateNumbers(5);
  print(numbers.toList()); // [1, 2, 3, 4, 5]
}

5.2 async* (비동기 제너레이터)

Stream<int> generateNumbers(int n) async* {
  for (int i = 1; i <= n; i++) {
    await Future.delayed(Duration(milliseconds: 500));
    yield i;
  }
}

void main() async {
  await for (var number in generateNumbers(3)) {
    print(number); // 1, 2, 3
  }
}

0.5초 마다 1, 2, 3 이 출력됨
스트림 형식이니, APi에서 받아오는 경우 해당 함수를 섞어 쓰면
UI 적 측면에서도 도움이됨

6. 커링 (Currying)

커링은 함수의 일부 인자를 고정하여 새로운 함수를 생성하는 패턴

Function add(int a) {
  return (int b) => a + b;
}

void main() {
  var addFive = add(5);
  print(addFive(10)); // 15
}

추가 포스팅 참조

7. 컴포지션 (Composition)

여러 함수를 결합하여 새로운 함수 생성
Dart에서는 이를 클로저와 함께 구현 가능

Function compose(Function f, Function g) {
  return (x) => f(g(x));
}

void main() {
  var double = (int x) => x * 2;
  var increment = (int x) => x + 1;

  var doubleThenIncrement = compose(increment, double);
  print(doubleThenIncrement(3)); // 7
}

추가 포스팅 참조

고차 함수, 클로저, 컬렉션 조작 메서드, 그리고 제너레이터 등을 적절히 활용하면
더욱 읽기 쉽고 유지보수하기 쉬운 코드를 작성 할 수 있음
다만, 과하게 사용하면 오히려 읽기 어려운 코드가 될 수 있기 때문에
적절히 사용하는 것을 추천