> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 컬렉션

> Laravel의 Collection 클래스를 사용해 데이터를 효율적으로 조작하는 방법을 설명합니다.

## 컬렉션이란

`Illuminate\Support\Collection` 클래스는 배열 데이터를 조작하기 위한 유창한 래퍼입니다.
PHP의 표준 배열 함수를 개별로 호출하는 대신 메서드 체인으로 직관적으로 데이터를 가공할 수 있습니다.

```php theme={null}
// 일반 배열 조작
$names = array_filter(
    array_map(fn ($user) => $user['name'], $users),
    fn ($name) => $name !== null
);

// 컬렉션을 사용하면
$names = collect($users)
    ->pluck('name')
    ->filter()
    ->values();
```

<Info>
  컬렉션은 **불변(immutable)** 입니다. 각 메서드는 원본 컬렉션을 변경하지 않고 새로운 컬렉션 인스턴스를 반환합니다. 원본 데이터를 유지하면서 안전하게 조작할 수 있습니다.
</Info>

## 컬렉션 작성

### `collect()` 헬퍼

가장 자주 사용되는 방법입니다. 배열을 전달해 컬렉션을 생성합니다.

```php theme={null}
use Illuminate\Support\Collection;

// 배열에서 컬렉션을 작성
$users = collect([
    ['name' => '山田太郎', 'age' => 28, 'role' => 'admin'],
    ['name' => '鈴木花子', 'age' => 34, 'role' => 'editor'],
    ['name' => '佐藤次郎', 'age' => 22, 'role' => 'viewer'],
]);

// 중첩된 배열도 OK
$products = collect([
    ['name' => '노트북 PC', 'price' => 120000, 'stock' => 5],
    ['name' => '마우스', 'price' => 3500, 'stock' => 20],
    ['name' => '키보드', 'price' => 8000, 'stock' => 12],
]);
```

### `Collection::make()`

`collect()`와 동등합니다. 파사드 스타일로 쓰고 싶은 경우에 사용합니다.

```php theme={null}
$collection = Collection::make([1, 2, 3]);
```

### `Collection::fromJson()`

JSON 문자열에서 컬렉션을 작성합니다. 외부 API의 응답을 처리하는 경우에 편리합니다.

```php theme={null}
$collection = Collection::fromJson('[{"name":"山田太郎"},{"name":"鈴木花子"}]');
```

## 자주 사용하는 메서드

### `map` — 각 요소를 변환

컬렉션의 각 요소에 처리를 적용하고 변환된 새로운 컬렉션을 반환합니다.

```php theme={null}
$users = collect([
    ['name' => '山田太郎', 'email' => 'yamada@example.com'],
    ['name' => '鈴木花子', 'email' => 'suzuki@example.com'],
]);

// 이메일 주소를 마스크하여 표시용으로 변환
$masked = $users->map(function (array $user) {
    $parts = explode('@', $user['email']);
    return [
        'name' => $user['name'],
        'email' => substr($parts[0], 0, 2) . '***@' . $parts[1],
    ];
});

// [['name' => '山田太郎', 'email' => 'ya***@example.com'], ...]
```

### `filter` / `reject` — 조건으로 좁히기

`filter()`는 조건을 만족하는 요소만 남기고, `reject()`는 조건을 만족하는 요소를 제외합니다.

```php theme={null}
$products = collect([
    ['name' => '노트북 PC', 'price' => 120000, 'in_stock' => true],
    ['name' => '마우스', 'price' => 3500, 'in_stock' => false],
    ['name' => '키보드', 'price' => 8000, 'in_stock' => true],
]);

// 재고 있는 상품만 취득
$inStock = $products->filter(fn ($product) => $product['in_stock']);

// 재고 없는 상품을 제외(reject는 filter의 반전)
$available = $products->reject(fn ($product) => ! $product['in_stock']);

// 인수 없는 filter()는 falsy한 값을 제거
$names = collect(['山田', '', null, '鈴木', false])->filter()->values();
// ['山田', '鈴木']
```

<Tip>
  `filter()` 후에는 인덱스가 빠지게 됩니다. `values()`를 호출하면 0부터 시작하는 연번으로 다시 매겨집니다.
</Tip>

### `first` / `last` — 요소를 1건 취득

조건을 만족하는 최초 또는 마지막 요소를 반환합니다.

```php theme={null}
$orders = collect([
    ['id' => 1, 'status' => 'shipped', 'amount' => 5000],
    ['id' => 2, 'status' => 'pending', 'amount' => 12000],
    ['id' => 3, 'status' => 'pending', 'amount' => 3500],
]);

// 최초의 미처리 주문을 취득
$nextOrder = $orders->first(fn ($order) => $order['status'] === 'pending');
// ['id' => 2, 'status' => 'pending', 'amount' => 12000]

// 조건에 일치하지 않는 경우의 기본값
$order = $orders->first(fn ($order) => $order['status'] === 'cancelled', null);

// 마지막 요소
$latest = $orders->last();
```

### `pluck` — 특정 키의 값만 꺼내기

중첩된 배열이나 모델에서 특정 필드만 뽑아냅니다.

```php theme={null}
$users = collect([
    ['id' => 1, 'name' => '山田太郎', 'department' => '개발부'],
    ['id' => 2, 'name' => '鈴木花子', 'department' => '영업부'],
    ['id' => 3, 'name' => '佐藤次郎', 'department' => '개발부'],
]);

// 이름만 꺼내기
$names = $users->pluck('name');
// ['山田太郎', '鈴木花子', '佐藤次郎']

// id 를 키로 매핑
$nameById = $users->pluck('name', 'id');
// [1 => '山田太郎', 2 => '鈴木花子', 3 => '佐藤次郎']
```

### `groupBy` — 특정 키로 그룹화

```php theme={null}
$users = collect([
    ['name' => '山田太郎', 'department' => '개발부'],
    ['name' => '鈴木花子', 'department' => '영업부'],
    ['name' => '佐藤次郎', 'department' => '개발부'],
    ['name' => '田中美咲', 'department' => '영업부'],
]);

$byDepartment = $users->groupBy('department');
// [
//   '개발부' => [['name' => '山田太郎', ...], ['name' => '佐藤次郎', ...]],
//   '영업부' => [['name' => '鈴木花子', ...], ['name' => '田中美咲', ...]],
// ]

// 클로저로 그룹 키를 동적으로 지정
$byFirstChar = $users->groupBy(fn ($user) => mb_substr($user['name'], 0, 1));
```

### `sortBy` / `sortByDesc` — 정렬

```php theme={null}
$products = collect([
    ['name' => '노트북 PC', 'price' => 120000],
    ['name' => '마우스', 'price' => 3500],
    ['name' => '키보드', 'price' => 8000],
]);

// 가격의 오름차순
$cheapFirst = $products->sortBy('price');

// 가격의 내림차순
$expensiveFirst = $products->sortByDesc('price');

// 여러 키로 정렬
$sorted = $products->sortBy([
    ['price', 'asc'],
    ['name', 'asc'],
]);
```

### `each` — 각 요소에 처리를 실행

부작용이 있는 처리(로그 출력, 이메일 전송 등)에 사용합니다. 컬렉션 자체는 변경하지 않습니다.

```php theme={null}
$orders = collect([
    ['id' => 1, 'user_id' => 10, 'amount' => 5000],
    ['id' => 2, 'user_id' => 11, 'amount' => 12000],
]);

$orders->each(function (array $order) {
    \Log::info("주문 #{$order['id']} 를 처리 중", ['amount' => $order['amount']]);
    // 알림 전송이나 큐에의 디스패치 등
});

// false 를 반환하면 도중에 처리를 빠져나올 수 있음
$orders->each(function (array $order) {
    if ($order['amount'] > 10000) {
        return false; // 루프를 빠져나옴
    }
    // ...
});
```

### `flatMap` — 맵 후에 1단계 평탄화

각 요소를 배열로 변환하고 전체를 1차원으로 평탄화합니다.

```php theme={null}
$users = collect([
    ['name' => '山田太郎', 'tags' => ['php', 'laravel']],
    ['name' => '鈴木花子', 'tags' => ['javascript', 'vue']],
]);

// 모든 사용자의 태그를 플랫한 목록으로
$allTags = $users->flatMap(fn ($user) => $user['tags']);
// ['php', 'laravel', 'javascript', 'vue']
```

### `reduce` — 집계

컬렉션 전체를 하나의 값으로 접어 넣습니다.

```php theme={null}
$orders = collect([
    ['product' => '노트북 PC', 'quantity' => 1, 'price' => 120000],
    ['product' => '마우스', 'quantity' => 2, 'price' => 3500],
    ['product' => '키보드', 'quantity' => 1, 'price' => 8000],
]);

// 합계 금액을 계산
$total = $orders->reduce(
    fn ($carry, $order) => $carry + ($order['price'] * $order['quantity']),
    0
);
// 135000
```

<Tip>
  단순한 합계라면 `sum()`을 사용할 수 있습니다: `$orders->sum(fn ($o) => $o['price'] * $o['quantity'])`
</Tip>

### `reduceInto` — 객체로 집계

`reduce`와 동일하게 접어 넣기 처리를 수행하지만, 콜백이 반환값을 반환할 필요가 없다는 점이 다릅니다. 기존의 객체를 직접 뮤테이트할 때 편리합니다.

```php theme={null}
class OrderStats
{
    public int $total = 0;
    public int $count = 0;
}

$orders = collect([
    ['amount' => 100],
    ['amount' => 250],
    ['amount' => 50],
]);

$stats = $orders->reduceInto(new OrderStats, function (OrderStats $stats, array $order) {
    $stats->total += $order['amount'];
    $stats->count++;
});

$stats->total; // 400
$stats->count; // 3
```

<Tip>
  `reduce`는 콜백의 반환값이 다음의 `$carry`가 되므로, 원시 값의 집계에 적합합니다. `reduceInto`는 객체를 그대로 계속 변경하기 때문에 단순한 코드가 됩니다.
</Tip>

스칼라나 배열로 집계하는 경우, 콜백에서 참조 전달(`&`)을 사용합니다.

```php theme={null}
$collection = collect([1, 2, 3, 4, 5]);

$even = $collection->reduceInto([], function (array &$result, int $value) {
    if ($value % 2 === 0) {
        $result[] = $value;
    }
});

// [2, 4]
```

### `chunk` — 분할

큰 컬렉션을 지정 사이즈로 분할합니다. 배치 처리나 화면 표시에서 편리합니다.

```php theme={null}
$users = collect(range(1, 100))->map(fn ($i) => ['id' => $i, 'name' => "사용자{$i}"]);

// 10건씩으로 분할
$chunks = $users->chunk(10);
// $chunks->count() === 10

// 배치별로 처리
$chunks->each(function (\Illuminate\Support\Collection $batch) {
    // 배치별의 DB 조작이나 이메일 전송 등
});
```

## 메서드 체인

컬렉션의 진가는 메서드 체인에 있습니다. 여러 조작을 이어서 복잡한 데이터 변환을 하나의 식으로 표현할 수 있습니다.

```php theme={null}
$orders = collect([
    ['customer' => '山田太郎', 'status' => 'completed', 'amount' => 5000, 'category' => '전자기기'],
    ['customer' => '鈴木花子', 'status' => 'pending',   'amount' => 12000, 'category' => '전자기기'],
    ['customer' => '佐藤次郎', 'status' => 'completed', 'amount' => 3500, 'category' => '문구'],
    ['customer' => '田中美咲', 'status' => 'completed', 'amount' => 8000, 'category' => '전자기기'],
    ['customer' => '伊藤健一', 'status' => 'cancelled', 'amount' => 2000, 'category' => '문구'],
]);

// 완료된 전자기기 주문을 금액 내림차순으로 취득하고, 고객명과 금액만 추출
$result = $orders
    ->filter(fn ($order) => $order['status'] === 'completed')
    ->filter(fn ($order) => $order['category'] === '전자기기')
    ->sortByDesc('amount')
    ->map(fn ($order) => [
        'customer' => $order['customer'],
        'amount'   => number_format($order['amount']) . '원',
    ])
    ->values();

// [
//   ['customer' => '田中美咲', 'amount' => '8,000원'],
//   ['customer' => '山田太郎', 'amount' => '5,000원'],
// ]
```

## Eloquent 와의 연계

Eloquent 쿼리의 결과는 항상 `Illuminate\Database\Eloquent\Collection` 인스턴스로 반환됩니다.
이것은 기본 `Collection`을 상속하고 있어 위의 메서드가 모두 사용 가능합니다.

```php theme={null}
use App\Models\User;
use App\Models\Order;

// get() 의 결과는 컬렉션
$users = User::where('is_active', true)->get(); // Collection

// 컬렉션의 메서드를 그대로 사용 가능
$adminEmails = User::all()
    ->filter(fn ($user) => $user->role === 'admin')
    ->pluck('email');

// 릴레이션의 로드된 데이터도 컬렉션 조작 가능
$orders = Order::with('items')->where('status', 'completed')->get();

$summary = $orders->map(fn ($order) => [
    'id'         => $order->id,
    'customer'   => $order->user->name,
    'item_count' => $order->items->count(),
    'total'      => $order->items->sum('price'),
]);
```

<Warning>
  데이터베이스에서 처리할 수 있는 필터링이나 정렬은 컬렉션 조작이 아닌 쿼리 빌더(`where`, `orderBy` 등)에서 수행합시다. 컬렉션으로 변환한 후에 필터링하면 불필요한 데이터를 모두 메모리에 읽어 들이고 맙니다.
</Warning>

### Eloquent 고유의 컬렉션 메서드

`Eloquent\Collection`에는 추가의 메서드가 있습니다.

```php theme={null}
$users = User::all();

// 모델을 ID로 검색
$user = $users->find(1);

// 주 키의 컬렉션을 취득
$ids = $users->modelKeys(); // [1, 2, 3, ...]

// 릴레이션을 함께 로드
$users->load('orders', 'profile');

// 차집합·교집합
$diff = $users->diff($otherUsers);
$intersect = $users->intersect($otherUsers);
```

## Lazy Collections

일반적인 컬렉션은 데이터 전체를 메모리에 읽어 들이지만, `LazyCollection`은 PHP의 제너레이터를 활용하여 데이터를 한 건씩 처리합니다.
수만 건 이상의 대량 데이터를 다루는 경우에 메모리를 절약할 수 있습니다.

```php theme={null}
use Illuminate\Support\LazyCollection;

// 일반 컬렉션: 전체 데이터를 메모리에 읽어 들임
$users = User::all(); // 10만 건이 있으면 10만 건이 메모리에 올라감

// LazyCollection: 한 건씩 처리
User::cursor()->each(function (User $user) {
    // 사용자를 한 건씩 처리
    // 메모리에 유지하는 것은 항상 한 건분만
});
```

### LazyCollection 작성

```php theme={null}
use Illuminate\Support\LazyCollection;

// 클로저(제너레이터)에서 작성
$lazy = LazyCollection::make(function () {
    $handle = fopen('large-file.csv', 'r');

    while (($line = fgetcsv($handle)) !== false) {
        yield $line;
    }

    fclose($handle);
});

// Eloquent의 cursor() 메서드가 LazyCollection 을 반환
$lazy = User::where('is_active', true)->cursor();
```

### 대량 데이터의 배치 처리

```php theme={null}
use App\Models\Order;

// 전체 주문을 한 건씩 처리(메모리 효율이 좋음)
Order::cursor()
    ->filter(fn ($order) => $order->amount > 10000)
    ->each(fn ($order) => $order->sendConfirmationEmail());

// cursor() 로 전체를 한 건씩 페치하면서 filter/each 로 파이프라인 처리
Order::where('status', 'completed')
    ->cursor()
    ->filter(fn ($order) => $order->amount > 10000)
    ->each(fn ($order) => $order->sendConfirmationEmail());
```

<Info>
  `cursor()`는 데이터베이스에서 한 건씩 페치하기 때문에 대량의 레코드를 처리할 때 메모리를 대폭 절약할 수 있습니다. 다만 데이터베이스 접속은 처리의 완료까지 유지됩니다.
</Info>

### `take` 와 `skip` 으로 페이징

```php theme={null}
$lazy = LazyCollection::make(function () {
    foreach (range(1, 1000000) as $i) {
        yield $i;
    }
});

// 최초의 1000건을 스킵하고 다음의 100건을 취득
$page = $lazy->skip(1000)->take(100)->values();
```

## 정리

<AccordionGroup>
  <Accordion title="자주 사용하는 메서드 정리">
    | 메서드                           | 설명                  |
    | ----------------------------- | ------------------- |
    | `collect($array)`             | 컬렉션을 작성             |
    | `map($callback)`              | 각 요소를 변환            |
    | `filter($callback)`           | 조건으로 좁히기            |
    | `reject($callback)`           | 조건에 맞는 요소를 제외       |
    | `first($callback)`            | 조건을 만족하는 최초의 요소를 취득 |
    | `pluck($key)`                 | 특정 키의 값을 뽑아냄        |
    | `groupBy($key)`               | 특정 키로 그룹화           |
    | `sortBy($key)`                | 오름차순으로 정렬           |
    | `sortByDesc($key)`            | 내림차순으로 정렬           |
    | `each($callback)`             | 각 요소에 처리 실행(부작용)    |
    | `flatMap($callback)`          | 맵 후에 평탄화            |
    | `reduce($callback, $initial)` | 집계해서 하나의 값으로        |
    | `chunk($size)`                | 지정 사이즈로 분할          |
    | `values()`                    | 인덱스를 다시 매김          |
    | `sum($key)`                   | 합계값을 계산             |
    | `count()`                     | 건수를 취득              |
  </Accordion>

  <Accordion title="컬렉션 vs 배열 함수">
    컬렉션은 배열 함수보다 훨씬 가독성이 높아집니다.

    ```php theme={null}
    // 배열 함수를 사용한 경우
    $result = array_values(array_filter(
        array_map(fn ($u) => $u['name'], $users),
        fn ($name) => strlen($name) > 2
    ));

    // 컬렉션을 사용한 경우
    $result = collect($users)
        ->pluck('name')
        ->filter(fn ($name) => strlen($name) > 2)
        ->values()
        ->all();
    ```
  </Accordion>

  <Accordion title="일반 컬렉션 vs LazyCollection">
    * **일반 컬렉션**: 수백\~수천 건 정도의 데이터. 단순하고 직관적.
    * **LazyCollection**: 수만 건 이상의 대량 데이터. 메모리를 절약하고 싶은 경우. `cursor()`와 조합하면 효과적.
    * Eloquent의 `get()`은 일반 컬렉션, `cursor()`는 LazyCollection을 반환합니다.
  </Accordion>
</AccordionGroup>


## Related topics

- [Eloquent 컬렉션](/ko/eloquent-collections.md)
- [컬렉션의 고차 메시지](/ko/advanced/higher-order-messages.md)
- [WebSocket (Jetstream / Firehose)](/ko/packages/laravel-bluesky/websocket.md)
- [Eloquent API 리소스](/ko/eloquent-resources.md)
- [헬퍼 함수](/ko/helpers.md)
