---
title: "List 는 칸이 모자라면 알아서 늘린다"
description: "속은 그냥 배열입니다. 꽉 차면 더 큰 배열을 만들어 옮기는 일을 대신 해 줄 뿐입니다."
author: "유니얼"
pubDate: 2026-09-08
category: "programming/csharp/collections"
tags: ["C#", "자료구조"]
canonical: https://blog.unialgames.com/posts/csharp-list/
---
배열은 만들 때 칸 개수가 정해지고 그 뒤로 안 바뀝니다.
개수를 미리 아는 것은 괜찮습니다. 문제는 모를 때입니다.

사용자가 입력하는 대로 담아야 한다면 몇 칸을 잡아야 할까요.
앞에서 본 방식대로 넉넉하게 잡을 수는 있습니다.

```csharp
int[] nums = new int[1000];
int count = 0;                  // 지금 몇 개 들어 있는지 따로 세야 합니다
```

칸 1000개를 잡아 두고 3개만 쓰면 997칸이 놀고, 1001개가 들어오면 터집니다.
그리고 **몇 개가 들어 있는지를 변수로 따로 관리**해야 합니다.
`nums.Length` 는 1000이지 3이 아니니까요.

> **칸이 모자라면 알아서 늘리고, 개수도 스스로 세는 것이 `List` 입니다.**

## List 는 이렇게 생겼다

```csharp
using System.Collections.Generic;

List<int> nums = new List<int>();

nums.Add(90);
nums.Add(85);
nums.Add(78);

Console.WriteLine(nums.Count);     // 3
Console.WriteLine(nums[1]);        // 85
```

`List<int>` 의 꺾쇠 안이 **담을 타입**입니다.
`List<string>`, `List<double>` 처럼 바꿔 쓰면 됩니다.
이 꺾쇠 문법의 정체는 나중에 제네릭에서 다룹니다.
지금은 "담을 타입을 적는 자리" 정도로 보면 충분합니다.

배열과 다른 점이 두 가지 보입니다.

- 만들 때 칸 개수를 안 적습니다.
- 개수가 `Length` 가 아니라 **`Count`** 입니다. 그리고 실제로 들어 있는 개수입니다.

꺼내는 것은 배열과 똑같이 대괄호에 번호입니다. 인덱스도 0부터입니다.

| 하는 일 | 배열 | List |
| --- | --- | --- |
| 만들기 | `new int[3]` | `new List<int>()` |
| 개수 | `Length` (칸 수) | `Count` (들어 있는 수) |
| 꺼내기 | `a[0]` | `list[0]` |
| 넣기 | `a[0] = 5` | `list.Add(5)` |
| 빼기 | 없음 | `list.RemoveAt(0)` |

## 속은 그냥 배열이다

`List` 가 마법을 부리는 것은 아닙니다. **안에 배열을 하나 들고 있습니다.**

칸이 꽉 찼는데 하나를 더 넣으라고 하면, 앞 글 마지막에서 우리가 손으로 했던 일을
그대로 합니다. 더 큰 배열을 새로 만들고, 전부 옮겨 담고, 새 배열을 쓰기 시작합니다.

<x-cells>
<script type="application/json">
{
  "label": "칸이 꽉 차면 무슨 일이 일어나는가",
  "cap": 4,
  "items": ["1", "2", "3"],
  "grow": 2,
  "ops": [
    { "op": "add", "value": "4" },
    { "op": "add", "value": "5" },
    { "op": "add", "value": "6" }
  ],
  "note": "네 번째 Add 는 빈 칸에 놓기만 하니 옮긴 칸이 0입니다. 다섯 번째에서 칸이 모자라 8칸을 새로 만들고 들어 있던 4개를 전부 옮깁니다. 그다음 Add 는 다시 0칸입니다. 늘리는 일이 매번 일어나지 않는다는 게 이 그림의 요지입니다."
}
</script>
</x-cells>

늘릴 때 **두 배로** 잡는 것이 요령입니다. 한 칸씩만 늘리면 `Add` 할 때마다
전부 옮겨야 해서 앞에서 본 문자열 `+` 와 같은 꼴이 납니다.
두 배로 잡으면 옮기는 일이 점점 드물게 일어나서,
평균으로 보면 `Add` 하나가 거의 공짜에 가까워집니다.

몇 개쯤 들어올지 짐작이 되면 미리 알려줄 수도 있습니다.

```csharp
List<int> nums = new List<int>(1000);   // 처음부터 1000칸 잡고 시작
```

이건 칸 개수를 정하는 게 아니라 **처음에 잡을 크기**를 알려주는 것입니다.
1001개를 넣어도 터지지 않고 알아서 늘어납니다. 옮기는 일을 줄여 줄 뿐입니다.

## 중간에 넣고 빼면 밀린다

`List` 는 중간에 끼워 넣거나 빼는 것도 됩니다.

```csharp
nums.Insert(0, 100);     // 맨 앞에 끼워 넣기
nums.RemoveAt(0);        // 첫 칸 빼기
```

편해 보이는데 공짜가 아닙니다. 배열은 칸이 빈틈없이 붙어 있어야 하니까,
맨 앞에 하나 끼워 넣으면 **뒤의 모든 값이 한 칸씩 밀려야** 합니다.
빼면 반대로 한 칸씩 당겨집니다.

<x-cells>
<script type="application/json">
{
  "label": "같은 세 번의 연산, 옮긴 칸은 전혀 다르다",
  "cap": 6,
  "items": ["10", "20", "30", "40"],
  "grow": 2,
  "ops": [
    { "op": "add", "value": "50" },
    { "op": "insert", "at": 0, "value": "5" },
    { "op": "remove", "at": 0 }
  ],
  "note": "맨 뒤에 Add 는 0칸입니다. 맨 앞에 Insert 는 뒤에 있던 것이 전부 한 칸씩 밀려서 5칸입니다. 맨 앞을 RemoveAt 하면 이번에는 전부 한 칸씩 당겨져서 또 5칸입니다. 들어 있는 것이 만 개면 이 숫자도 만이 됩니다."
}
</script>
</x-cells>

**들어 있는 개수만큼 움직입니다.** 위 그림에서 옮긴 칸 수를 보세요.
맨 뒤는 0이고 맨 앞은 들어 있는 만큼입니다.

| 하는 일 | 비용 |
| --- | --- |
| `list[i]` 로 꺼내기 | 어디를 꺼내든 같다 |
| 맨 뒤에 `Add` | 대부분 그냥 한 칸 채운다 |
| 중간이나 앞에 `Insert` / `RemoveAt` | 뒤에 있는 것들이 전부 밀린다 |

만 개짜리 리스트의 맨 앞을 반복문 안에서 계속 지우고 있다면,
그건 겉보기와 달리 무거운 코드입니다.

## 배열이냐 List 냐

**개수가 변하면 `List`, 고정이면 배열**입니다.

요일 7개, 바둑판 19 × 19 처럼 개수가 처음부터 정해진 것은 배열이 맞습니다.
개수가 코드에 적혀 있으니 읽는 사람도 "여기는 안 늘어난다" 를 압니다.

인벤토리, 접속한 사용자 목록처럼 실행 중에 개수가 변하는 것은 `List` 입니다.
배열로 하면 결국 `List` 가 해 주는 일을 손으로 다시 만들게 됩니다.

애매하면 `List` 를 쓰면 됩니다. 실무에서 훨씬 자주 씁니다.

## 정리

- `List` 는 칸이 모자라면 알아서 늘어나고, 들어 있는 개수를 `Count` 로 알려줍니다.
- 꺾쇠 안이 담을 타입입니다. 꺼내는 것은 배열과 똑같이 대괄호에 번호입니다.
- 속은 배열입니다. 꽉 차면 두 배 크기로 새로 만들어 전부 옮깁니다.
- 두 배로 늘리기 때문에 옮기는 일이 드물게 일어납니다. `Add` 는 대체로 싸게 끝납니다.
- 중간이나 앞에 넣고 빼면 뒤가 전부 밀립니다. 이건 싼 연산이 아닙니다.
- 개수가 변하면 `List`, 고정이면 배열입니다.