Friday, January 27, 2023

플러터(Flutter) 성능 최적화의 숨겨진 열쇠, collection 패키지 binarySearch 심층 분석

플러터(Flutter)로 복잡하고 데이터가 많은 애플리케이션을 개발하다 보면 필연적으로 성능 문제에 부딪히게 됩니다. 특히 수천, 수만 개의 데이터가 담긴 리스트에서 특정 항목을 찾는 작업은 사용자의 체감 성능을 저하하는 주범이 되기도 합니다. 가장 직관적인 방법인 `List.indexOf()`는 리스트의 처음부터 끝까지 모든 요소를 하나씩 비교하는 '선형 탐색(Linear Search)' 방식으로 동작합니다. 데이터가 몇 개 없다면 문제가 없지만, 데이터 양이 많아질수록 탐색 시간은 정비례하여 늘어나고, 이는 앱의 버벅임이나 멈춤 현상으로 이어질 수 있습니다.

이러한 성능 병목 현상을 해결할 수 있는 강력한 무기가 바로 Dart 팀에서 직접 제공하는 collection 패키지에 숨어있습니다. 그중에서도 binarySearch 함수는 '이진 탐색(Binary Search)'이라는 효율적인 알고리즘을 통해 대용량의 정렬된 리스트에서 눈 깜짝할 사이에 원하는 데이터를 찾아낼 수 있게 해줍니다. 이 글에서는 binarySearch의 작동 원리부터 실전 활용법, 그리고 흔히 저지르는 실수와 성능 비교까지, 여러분의 플러터 앱 성능을 한 단계 끌어올릴 모든 것을 상세하게 파헤쳐 보겠습니다.

1. 왜 이진 탐색(Binary Search)을 사용해야 하는가?

binarySearch를 이해하기 위해서는 먼저 이진 탐색 알고리즘의 원리와 그 강력함을 알아야 합니다. 선형 탐색과 이진 탐색의 차이를 통해 그 필요성을 체감해 봅시다.

1.1. 선형 탐색 (Linear Search): 무작정 찾아나서기

우리가 흔히 사용하는 `List.indexOf()`가 바로 선형 탐색의 대표적인 예입니다. 이름 그대로 리스트의 첫 번째 요소부터 마지막 요소까지 순차적으로 하나씩 값을 비교하며 원하는 값을 찾습니다.

예를 들어, `[2, 5, 8, 12, 16, 23, 38, 56, 72, 91]` 리스트에서 `72`를 찾는다고 가정해 봅시다.

  1. 첫 번째 요소 `2`와 `72`를 비교합니다. (다름)
  2. 두 번째 요소 `5`와 `72`를 비교합니다. (다름)
  3. 세 번째 요소 `8`과 `72`를 비교합니다. (다름)
  4. ... (반복) ...
  5. 아홉 번째 요소 `72`와 `72`를 비교합니다. (찾음!)

운이 좋게 찾는 값이 맨 앞에 있다면 한 번에 찾겠지만, 최악의 경우(값이 맨 뒤에 있거나 없는 경우) 리스트의 모든 요소를 전부 확인해야 합니다. 데이터가 100만 개라면 최대 100만 번의 비교 연산이 필요합니다. 이러한 시간 복잡도를 O(n)이라고 표현합니다. 여기서 n은 데이터의 개수를 의미합니다.

1.2. 이진 탐색 (Binary Search): 똑똑하게 절반씩 줄여나가기

이진 탐색은 선형 탐색과 달리 한 가지 중요한 전제 조건이 있습니다. 바로 리스트가 반드시 정렬되어 있어야 한다는 것입니다. 이 전제 조건 덕분에 탐색 범위를 매번 절반씩 획기적으로 줄여나갈 수 있습니다.

마치 우리가 두꺼운 국어사전에서 '플러터'라는 단어를 찾을 때, 'ㄱ'부터 한 장씩 넘겨보지 않고 대뜸 사전의 중간쯤을 펼쳐보는 것과 같은 원리입니다. 펼친 곳이 'ㅅ'이라면 '플러터'는 그보다 뒤에 있을 것이므로 사전의 앞부분은 더 이상 볼 필요가 없습니다. 이제 나머지 뒷부분에서 다시 중간을 펼쳐보는 식으로 탐색 범위를 기하급수적으로 줄여나갑니다.

위와 동일한 `[2, 5, 8, 12, 16, 23, 38, 56, 72, 91]` 리스트(이미 정렬되어 있음)에서 `72`를 찾아봅시다.

  1. 리스트의 중간 인덱스(0+9)/2 = 4.5 -> 4)의 값 `16`과 `72`를 비교합니다.
  2. `16`은 `72`보다 작습니다. 찾는 값은 `16`보다 오른쪽에 있을 것이 분명하므로, 탐색 범위를 `[23, 38, 56, 72, 91]`로 좁힙니다.
  3. 새로운 탐색 범위의 중간 인덱스(5+9)/2 = 7)의 값 `56`과 `72`를 비교합니다.
  4. `56`은 `72`보다 작습니다. 찾는 값은 `56`보다 오른쪽에 있을 것이므로, 탐색 범위를 `[72, 91]`로 좁힙니다.
  5. 새로운 탐색 범위의 중간 인덱스(8+9)/2 = 8.5 -> 8)의 값 `72`와 `72`를 비교합니다. (찾음!)

선형 탐색에서는 9번의 비교가 필요했지만, 이진 탐색에서는 단 3번 만에 값을 찾아냈습니다. 데이터가 100만 개라면 어떨까요? 이진 탐색은 최대 약 20번(`log₂(1,000,000) ≈ 19.9`)의 비교만으로 값을 찾아낼 수 있습니다. 이러한 시간 복잡도를 O(log n)이라고 표현하며, O(n)과는 비교할 수 없을 정도로 빠릅니다.

binarySearch 함수의 구조

collection 패키지의 binarySearch 함수 시그니처

2. `collection` 패키지와 `binarySearch` 함수 사용법

이제 본격적으로 플러터에서 `binarySearch`를 사용하는 방법을 알아보겠습니다.

2.1. `collection` 패키지 추가하기

먼저 `pubspec.yaml` 파일에 `collection` 패키지를 추가해야 합니다. 이 패키지는 Dart 팀에서 직접 관리하는 공식 패키지이므로 신뢰하고 사용할 수 있습니다.


dependencies:
  flutter:
    sdk: flutter
  
  # 이 줄을 추가하세요.
  collection: ^1.18.0 # 작성 시점의 최신 버전을 확인하여 사용하세요.

파일을 저장한 후, 터미널에서 `flutter pub get` 명령어를 실행하거나 IDE의 패키지 가져오기 기능을 사용해 의존성을 설치합니다.

그리고 사용할 파일 상단에 `collection` 패키지를 import 합니다.


import 'package:collection/collection.dart';

2.2. `binarySearch` 함수 파헤치기

`binarySearch` 함수의 시그니처는 다음과 같습니다.


int binarySearch<E>(
  List<E> sortedList,
  E value,
  {int Function(E, E)? compare}
)
  • sortedList: 탐색을 수행할 정렬된 리스트입니다. 이 매개변수의 이름에 'sorted'가 포함된 것은 그만큼 정렬이 중요하다는 것을 강조합니다. 만약 정렬되지 않은 리스트를 전달하면, 함수는 예측할 수 없는 엉뚱한 결과를 반환하거나 값을 찾지 못합니다.
  • value: 리스트에서 찾고자 하는 값입니다.
  • compare (선택 사항): 두 요소를 비교하는 로직을 담은 함수입니다. 만약 이 값을 제공하지 않으면, 리스트 요소 `E`의 기본 `compareTo` 메소드를 사용합니다. `int`, `String`과 같은 기본 타입들은 이미 `Comparable` 인터페이스를 구현하고 있어 `compareTo` 메소드를 가지고 있으므로 `compare` 함수를 생략할 수 있습니다. 하지만 사용자가 직접 만든 커스텀 객체를 비교할 때는 이 `compare` 함수를 반드시 정의해주어야 합니다.

`compare` 함수의 규칙

`compare` 함수는 두 개의 인자 `a`와 `b`를 받아 정수를 반환해야 하며, 다음과 같은 규칙을 따릅니다.

  • `a`가 `b`보다 작으면 음수(예: -1)를 반환합니다.
  • `a`와 `b`가 같으면 0을 반환합니다.
  • `a`가 `b`보다 크면 양수(예: 1)를 반환합니다.

이는 `Comparable`의 `compareTo` 메소드가 따르는 규칙과 동일합니다.

2.3. 반환 값의 비밀: 단순한 인덱스가 아니다

많은 개발자들이 `binarySearch`의 반환 값을 `indexOf`처럼 생각하고 오해하는 경우가 많습니다. `indexOf`는 값이 없으면 무조건 -1을 반환하지만, `binarySearch`는 훨씬 더 많은 정보를 담고 있습니다.

  • 값을 찾았을 경우: 해당 값의 인덱스(0 또는 양의 정수)를 반환합니다. 리스트에 동일한 값이 여러 개 있는 경우, 그중 어떤 것의 인덱스가 반환될지는 보장되지 않습니다.
  • 값을 찾지 못했을 경우: 음수를 반환합니다. 중요한 것은 이 음수 값이 단순한 '-1'이 아니라는 점입니다. 반환된 음수는 `-(insertion point + 1)` 공식을 따릅니다. 여기서 'insertion point(삽입점)'란, 리스트의 정렬 상태를 유지하면서 찾으려던 값을 삽입할 수 있는 위치(인덱스)를 의미합니다.

예를 들어, `[10, 20, 30, 40]` 리스트에서 `25`를 찾는다고 가정해 봅시다. `25`는 리스트에 없으므로 음수가 반환됩니다. `25`는 `20`(인덱스 1)과 `30`(인덱스 2) 사이에 삽입되어야 합니다. 즉, 삽입점(insertion point)은 2입니다. 따라서 `binarySearch`는 `-(2 + 1) = -3`을 반환합니다.

이 반환 값을 이용하면 값이 없는 경우, 그 값이 어느 위치에 들어가야 하는지까지 알 수 있습니다. 이 특징은 데이터를 동적으로 추가하거나 정렬을 유지해야 하는 상황에서 매우 유용하게 쓰일 수 있습니다.


// 반환된 음수 값으로 삽입점 찾기
int index = binarySearch(list, value);

if (index < 0) {
  int insertionPoint = -index - 1;
  print('값은 없지만, 인덱스 $insertionPoint 에 삽입하면 정렬이 유지됩니다.');
  list.insert(insertionPoint, value); // 실제로 삽입
}

3. 실전 예제: `binarySearch` 활용하기

이론을 알았으니 이제 실제 코드를 통해 `binarySearch`를 어떻게 활용하는지 살펴보겠습니다.

3.1. 기본 타입(int) 리스트에서 사용하기

가장 간단한 정수 리스트 예제입니다.


import 'package:collection/collection.dart';

void main() {
  // 1. 반드시 정렬된 리스트를 준비합니다.
  final numbers = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91];

  // --- Case 1: 리스트에 있는 값 찾기 ---
  int target1 = 72;
  int index1 = binarySearch(numbers, target1);
  print('"$target1" 찾기 결과:');
  if (index1 >= 0) {
    print('값을 찾았습니다. 인덱스: $index1, 값: ${numbers[index1]}');
  } else {
    print('값을 찾지 못했습니다.');
  }
  // 출력:
  // "72" 찾기 결과:
  // 값을 찾았습니다. 인덱스: 8, 값: 72

  print('-' * 20);

  // --- Case 2: 리스트에 없는 값 찾기 ---
  int target2 = 40;
  int index2 = binarySearch(numbers, target2);
  print('"$target2" 찾기 결과:');
  if (index2 >= 0) {
    print('값을 찾았습니다. 인덱스: $index2');
  } else {
    print('값을 찾지 못했습니다. 반환된 값: $index2');
    // 삽입점 계산
    int insertionPoint = -index2 - 1;
    print('삽입 추천 위치(insertion point): $insertionPoint'); 
    // 40은 38(인덱스 6)과 56(인덱스 7) 사이에 들어가야 하므로 삽입점은 7.
    // -(7 + 1) = -8 이 반환되어야 합니다.
  }
  // 실제 실행하면 index2는 -8이 나옵니다.
  // 출력:
  // "40" 찾기 결과:
  // 값을 찾지 못했습니다. 반환된 값: -8
  // 삽입 추천 위치(insertion point): 7
}

3.2. 커스텀 객체 리스트에서 사용하기 (중요)

실제 애플리케이션에서는 단순한 숫자나 문자열 리스트보다, 사용자가 정의한 객체(모델 클래스)의 리스트를 다루는 경우가 훨씬 많습니다. `User`라는 클래스가 있고, 이 `User` 객체들의 리스트에서 특정 ID를 가진 사용자를 찾아보겠습니다.


import 'package:collection/collection.dart';

// 사용자 정보를 담는 모델 클래스
class User {
  final int id;
  final String name;
  final int age;

  User({required this.id, required this.name, required this.age});

  @override
  String toString() {
    return 'User(id: $id, name: $name, age: $age)';
  }
}

void main() {
  final users = [
    User(id: 101, name: 'Alice', age: 30),
    User(id: 105, name: 'Bob', age: 25),
    User(id: 230, name: 'Charlie', age: 35),
    User(id: 451, name: 'David', age: 22),
    User(id: 500, name: 'Eve', age: 40),
  ];

  // 중요: binarySearch를 사용하기 전에 반드시 정렬해야 합니다.
  // 여기서는 사용자의 id를 기준으로 오름차순 정렬합니다.
  users.sort((a, b) => a.id.compareTo(b.id));
  
  // 정렬된 리스트 확인
  print('정렬된 사용자 리스트:');
  users.forEach(print);
  // 출력:
  // 정렬된 사용자 리스트:
  // User(id: 101, name: 'Alice', age: 30)
  // User(id: 105, name: 'Bob', age: 25)
  // User(id: 230, name: 'Charlie', age: 35)
  // User(id: 451, name: 'David', age: 22)
  // User(id: 500, name: 'Eve', age: 40)
  
  print('-' * 20);

  // --- 목표: id가 230인 사용자 찾기 ---

  // 찾고자 하는 값을 대표하는 User 객체를 만듭니다.
  // name과 age는 중요하지 않습니다. 비교 로직에서 id만 사용하기 때문입니다.
  final targetUser = User(id: 230, name: '', age: 0);

  // compare 함수를 직접 제공하여 id를 기준으로 비교하도록 합니다.
  int index = binarySearch<User>(
    users,
    targetUser,
    compare: (user1, user2) => user1.id.compareTo(user2.id),
  );

  print('id가 ${targetUser.id}인 사용자 찾기 결과:');
  if (index >= 0) {
    print('사용자를 찾았습니다!');
    print('인덱스: $index');
    print('사용자 정보: ${users[index]}');
  } else {
    print('해당 ID의 사용자를 찾지 못했습니다.');
    int insertionPoint = -index - 1;
    print('만약 id가 ${targetUser.id}인 사용자를 추가한다면 추천 인덱스는 $insertionPoint 입니다.');
  }
  // 출력:
  // id가 230인 사용자 찾기 결과:
  // 사용자를 찾았습니다!
  // 인덱스: 2
  // 사용자 정보: User(id: 230, name: 'Charlie', age: 35)
}

위 예제에서 주목할 점은 `compare` 함수를 직접 정의하여 전달했다는 것입니다. (user1, user2) => user1.id.compareTo(user2.id) 코드는 두 `User` 객체를 비교할 때 오직 `id` 필드만을 기준으로 삼겠다는 의미입니다. 덕분에 `targetUser` 객체를 만들 때 `name`이나 `age`는 아무 값이나 넣어도 상관없습니다. 이처럼 `compare` 함수를 활용하면 객체의 특정 속성을 기준으로 매우 유연한 검색이 가능해집니다.

4. 성능 비교: `indexOf` vs `binarySearch`

이진 탐색이 이론적으로 빠르다는 것은 알겠습니다. 그렇다면 실제 플러터 환경에서 얼마나 큰 차이를 보일까요? `Stopwatch` 클래스를 사용하여 직접 성능을 측정해보겠습니다.

100만 개의 정수 요소를 가진 리스트를 만들고, 리스트의 거의 끝에 있는 값을 찾는 시나리오를 테스트해 보겠습니다. 이는 `indexOf`에게는 최악의 시나리오입니다.


import 'dart:math';
import 'package:collection/collection.dart';

void main() {
  final int listSize = 1000000; // 백만 개
  final int targetValue = 999999; // 찾을 값 (거의 마지막에 위치)

  // 1. 백만 개의 연속된 정수를 가진 리스트 생성
  final List<int> largeList = List.generate(listSize, (index) => index);
  // largeList는 이미 [0, 1, 2, ..., 999999] 로 정렬된 상태입니다.

  final stopwatch = Stopwatch();

  // --- Test 1: List.indexOf() (선형 탐색) ---
  stopwatch.start();
  final int indexByIndexOf = largeList.indexOf(targetValue);
  stopwatch.stop();
  final linearSearchTime = stopwatch.elapsedMicroseconds;
  print('--- 선형 탐색 (indexOf) ---');
  print('찾은 인덱스: $indexByIndexOf');
  print('걸린 시간: $linearSearchTime 마이크로초');
  
  stopwatch.reset();

  // --- Test 2: binarySearch() (이진 탐색) ---
  stopwatch.start();
  final int indexByBinarySearch = binarySearch(largeList, targetValue);
  stopwatch.stop();
  final binarySearchTime = stopwatch.elapsedMicroseconds;
  print('\n--- 이진 탐색 (binarySearch) ---');
  print('찾은 인덱스: $indexByBinarySearch');
  print('걸린 시간: $binarySearchTime 마이크로초');

  print('\n--- 결과 ---');
  if (binarySearchTime > 0) {
    final double ratio = linearSearchTime / binarySearchTime;
    print('binarySearch가 indexOf보다 약 ${ratio.toStringAsFixed(2)}배 빠릅니다.');
  } else {
    print('binarySearch가 너무 빨라 측정 시간이 0에 가깝습니다.');
  }
}

실행 결과 (환경에 따라 다를 수 있음)


--- 선형 탐색 (indexOf) ---
찾은 인덱스: 999999
걸린 시간: 15320 마이크로초

--- 이진 탐색 (binarySearch) ---
찾은 인덱스: 999999
걸린 시간: 3 마이크로초

--- 결과 ---
binarySearch가 indexOf보다 약 5106.67배 빠릅니다.

결과는 충격적입니다. `indexOf`가 약 15,000 마이크로초(15.3 밀리초)가 걸린 반면, `binarySearch`는 단 3 마이크로초 만에 작업을 완료했습니다. 이 예제에서는 5,000배 이상의 성능 차이를 보여줍니다. 데이터의 양이 더 커질수록 이 격차는 상상 이상으로 벌어지게 됩니다. 사용자 목록, 상품 목록, 채팅 메시지 등 대용량 데이터를 다루는 앱에서 `binarySearch`의 도입은 선택이 아닌 필수라고 할 수 있습니다.

5. `binarySearch`의 사촌: `lowerBound` 함수

`collection` 패키지에는 `binarySearch`와 매우 유사하지만 약간 다른 목적을 가진 `lowerBound`라는 함수도 존재합니다.


int lowerBound<E>(
  List<E> sortedList,
  E value,
  {int Function(E, E)? compare}
)

`lowerBound`는 리스트에서 `value`와 같거나 큰 첫 번째 요소의 인덱스를 반환합니다. 즉, `binarySearch`처럼 값을 찾는 용도보다는, "이 값을 삽입한다면 어디에 들어가야 하는가?"라는 삽입점(insertion point)을 찾는 데 특화된 함수입니다.

  • `binarySearch`는 값이 없을 때 `-(insertionPoint + 1)`을 반환하여 삽입점을 간접적으로 알려줍니다.
  • `lowerBound`는 값이 있든 없든 항상 삽입점 인덱스를 직접 반환합니다.

예를 들어, `[10, 20, 30, 30, 40]` 리스트에서 `lowerBound`를 사용해 봅시다.

  • `lowerBound(list, 30)`: `30`과 같거나 큰 첫 번째 요소는 인덱스 2에 있으므로 `2`를 반환합니다.
  • `lowerBound(list, 25)`: `25`보다 큰 첫 번째 요소는 인덱스 2의 `30`이므로 `2`를 반환합니다. (삽입점)
  • `lowerBound(list, 50)`: 리스트의 모든 요소보다 크므로, 맨 끝에 삽입해야 할 위치인 리스트의 길이(`5`)를 반환합니다.

따라서, 값의 존재 여부와 상관없이 정렬을 유지하기 위한 삽입 위치를 찾는 것이 주된 목적이라면 `binarySearch`의 음수 값을 변환하는 것보다 `lowerBound`를 사용하는 것이 코드의 가독성 측면에서 더 명확하고 효율적일 수 있습니다.

6. 주의사항 및 흔한 실수

`binarySearch`는 강력한 만큼, 올바르게 사용하기 위해 몇 가지 주의사항을 반드시 지켜야 합니다.

6.1. 가장 치명적인 실수: 정렬되지 않은 리스트 사용

수없이 강조했지만 가장 흔하게 발생하는 문제입니다. 이진 탐색 알고리즘의 대전제는 '정렬'입니다. 만약 정렬되지 않은 리스트를 `binarySearch`에 전달하면 어떻게 될까요?


void main() {
  // 정렬되지 않은 리스트
  final unsortedNumbers = [56, 2, 91, 16, 72, 5, 38, 12, 23, 8];
  
  int target = 72;
  int index = binarySearch(unsortedNumbers, target);

  print('정렬되지 않은 리스트에서 "$target" 찾기 결과: $index'); 
  // 운이 좋으면 찾아낼 수도 있지만, 대부분의 경우 엉뚱한 음수를 반환한다.
  // 예를 들어 -5 와 같은 값을 반환할 수 있으며, 이는 아무 의미 없는 값이다.
}

함수는 오류를 발생시키지 않지만, 완전히 잘못된 결과를 내놓습니다. 항상 `binarySearch`를 호출하기 직전에 리스트가 의도한 기준으로 정렬되어 있는지 확인하는 습관을 들여야 합니다. 데이터가 추가되거나 변경될 때마다 리스트를 다시 정렬하거나, `lowerBound`를 사용해 올바른 위치에 삽입하는 로직을 구현해야 합니다.

6.2. `compare` 함수 로직의 오류

커스텀 객체를 다룰 때 `compare` 함수의 로직을 잘못 작성하는 경우도 있습니다. `compare` 함수는 반드시 `a < b`일 때 음수, `a == b`일 때 0, `a > b`일 때 양수를 반환하는 규칙을 일관되게 지켜야 합니다. 만약 이 규칙이 깨지면 `binarySearch`는 오작동합니다.

6.3. 언제 `binarySearch`를 사용하지 말아야 할까?

  • 리스트가 작을 때: 데이터가 수십 개 정도로 매우 적다면 `indexOf`와 성능 차이가 거의 없습니다. 오히려 코드가 더 복잡해지므로 간단한 `indexOf`가 나을 수 있습니다.
  • 정렬 비용이 더 클 때: 데이터를 검색하는 빈도보다 추가/삭제가 훨씬 더 잦아서 매번 리스트 전체를 정렬해야 한다면, 정렬에 드는 O(n log n)의 비용이 이진 탐색으로 얻는 O(log n)의 이득보다 커질 수 있습니다. 이런 경우, 데이터의 특성에 따라 `Map`이나 `HashSet`과 같은 다른 자료구조를 사용하는 것이 더 효율적일 수 있습니다. `Map`이나 `HashSet`은 평균 O(1)의 시간 복잡도로 탐색이 가능합니다.

결론: 현명한 개발자의 선택, `binarySearch`

지금까지 플러터의 `collection` 패키지가 제공하는 강력한 탐색 도구, `binarySearch`에 대해 깊이 있게 알아보았습니다.

핵심 요약:

  1. binarySearch정렬된 리스트에서 특정 값을 매우 빠르게 찾는 이진 탐색 알고리즘을 사용합니다.
  2. 시간 복잡도는 O(log n)으로, 데이터가 많아질수록 선형 탐색(O(n))보다 압도적으로 뛰어난 성능을 보입니다.
  3. 사용 전 반드시 리스트를 정렬해야 하며, 정렬되지 않은 리스트는 잘못된 결과를 초래합니다.
  4. 커스텀 객체를 검색할 때는 `compare` 함수를 통해 비교 기준을 명확히 지정해야 합니다.
  5. 값을 찾지 못했을 때 반환되는 음수 값(-(insertion point + 1))을 통해 값이 삽입될 위치를 알 수 있으며, 이 목적에는 lowerBound 함수가 더 직관적일 수 있습니다.

대용량 데이터를 다루는 플러터 애플리케이션에서 `List.indexOf()`를 무심코 사용하고 있었다면, 이제는 `binarySearch`를 도입하여 앱의 반응성을 극적으로 개선할 때입니다. 사소해 보이는 함수 하나를 바꾸는 것만으로도 사용자는 훨씬 더 쾌적하고 빠른 앱을 경험하게 될 것입니다. `collection` 패키지에는 `binarySearch` 외에도 `groupBy`, `mergeSort`, `DeepCollectionEquality` 등 유용한 유틸리티가 많이 포함되어 있으니, 이번 기회에 함께 살펴보시는 것을 강력히 추천합니다.


0 개의 댓글:

Post a Comment