---
title: "정렬 가능한 리스트"
description: "드래그하는 동안 순서가 바뀌는 리스트, 탭 바, 그리드. makeSortableList는 초안을 렌더링하고, makeSortableItem은 행을 만들며, 드롭하면 새 순서가 ref에 기록됩니다. 핸들, 드롭 선, 자리 맞바꾸기."
canonical_url: "https://zizigy.github.io/vue-dnd-kit/ko/docs/guides/sortable-lists"
---

> Index of every page on this site: [llms.txt](https://zizigy.github.io/vue-dnd-kit/llms.txt)

<example height="25rem" hint="레이어를 그립으로 드래그하세요. 패널은 바로 움직이고, 아트보드는 드롭할 때 바뀝니다" name="layers-panel">



</example>

## 리스트와 행

정렬 가능한 리스트에는 컴포넌트 두 개가 필요합니다. 리스트와 그 행입니다.

```vue [LayersPanel.vue]
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeSortableList } from '@vue-dnd-kit/core';
import LayerRow from './LayerRow.vue';

const layers = defineModel<Layer[]>({ required: true });
const list = useTemplateRef<HTMLElement>('list');
const { items } = makeSortableList(list, { items: layers });
</script>

<template>
  <ul ref="list">
    <LayerRow v-for="layer in items" :key="layer.id" :layer="layer" />
  </ul>
</template>
```

```vue [LayerRow.vue]
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeSortableItem } from '@vue-dnd-kit/core';

const props = defineProps<{ layer: Layer }>();
const el = useTemplateRef<HTMLElement>('el');
makeSortableItem(el, { item: () => props.layer });
</script>

<template>
  <li ref="el">{{ layer.name }}</li>
</template>
```

행은 자신이 렌더링된 리스트에 등록됩니다. 한 컴포넌트에서 리스트를 여러 개 만들 수도 있습니다(두
컬럼, 리스트 안의 리스트). 각 행은 자신을 감싼 요소의 리스트에 들어가며, 리스트 안의 리스트에서는
가장 안쪽 리스트에 들어갑니다.

## 초안

`makeSortableList`는 `items`, 즉 배열의 **초안**을 반환합니다. 드래그하는 동안 초안은 실시간으로
순서가 바뀝니다. 드래그 중인 행이 새 자리를 차지하고, 나머지 행은 비켜납니다. 원본 배열은 드롭할
때까지 바뀌지 않습니다. 드롭하면 라이브러리가 새 순서를 배열에 기록합니다.

위의 레이어 패널은 둘 다 보여 줍니다. 패널은 `items`를 렌더링해 드래그하는 동안 움직이고,
아트보드는 배열 자체인 `layers`를 읽어 손을 놓을 때만 다시 쌓입니다.

- 원본 배열이 아니라 `items`를 렌더링하세요. 원본 배열을 렌더링하면 드롭할 때까지 아무것도
움직이지 않습니다.
- 기록은 리스트의 `onDrop` 다음에 일어납니다. `onDrop`에서 원본 배열은 아직 이전 순서이고,
`e.result`에 새 순서가 들어 있습니다.
- `layers`는 일반 ref이므로(여기서는 `defineModel`이고, 기록하면 `update:modelValue`가
발생합니다), 앱의 다른 부분도 다른 변경과 똑같이 이 변경을 봅니다.

```ts
makeSortableList(list, {
  items: layers,
  onDrop: (e) => {
    console.log(e.from.index, '→', e.to.index, e.result.get(e.to.list));
  },
});
```

<tip>

스토어, 서버 상태, 불변 데이터를 쓰나요? ref 대신 getter나 `computed`를 넘기세요. 라이브러리는
읽기 전용 소스에 기록하지 않습니다. `e.preventDefault()`를 호출하고 `e.result`를 직접 적용하세요.
[제어 리스트](https://zizigy.github.io/docs/guides/shared-lists#controlled-lists)를 참고하세요.

</tip>

## Key

행은 인덱스나 자동 id가 아니라 항상 데이터로 식별합니다. 기본 key는 `item.id`이고, 문자열과
숫자는 항목 자체입니다.

```ts
// LayersPanel.vue: your items have another key
makeSortableList(list, { items: layers, getKey: (layer) => layer.uuid });

// LayerRow.vue: `item` is keyed with the list's getKey…
makeSortableItem(el, { item: () => props.layer });
// …or pass the key yourself
makeSortableItem(el, { id: () => props.layer.uuid });
```

- `v-for`에도 같은 key를 쓰세요: `:key="layer.uuid"`. `:key="index"`를 쓰면 엉뚱한 행이
애니메이션되고 상태가 행 사이를 오갑니다.
- key는 항목을 주고받는 모든 리스트에서 고유해야 합니다.
- `item`과 `id`는 getter로 넘기세요. 그래야 props가 바뀌어도 행이 동기화된 상태를 유지합니다.

## 핸들

기본적으로 행 전체에서 드래그를 시작할 수 있습니다. 단, 그 안의 버튼, 입력 필드 등 컨트롤은
예외입니다. 레이어의 눈 아이콘과 자물쇠는 일반 버튼으로 남습니다. 한 부분으로만 드래그하려면
`handle`을 넘기세요.

```vue [LayerRow.vue]
<script setup lang="ts">
makeSortableItem(el, { item: () => props.layer, handle: '.grip' });
</script>

<template>
  <li ref="el">
    <button type="button" class="grip" :aria-label="`Move ${layer.name}`">⠿</button>
    {{ layer.name }}
  </li>
</template>
```

- `handle`은 항목 안의 선택자이거나 요소 ref입니다.
- 마우스 드래그는 핸들에서 평소의 4 px 없이 바로 시작합니다. 움직이지 않은 누르기는 여전히
클릭이므로, 핸들이 `click`으로 메뉴를 열 수도 있습니다.
- 핸들은 드래그 속성(`role`, `tabindex`, `aria-*`)을 받고, 키보드 드래그는 핸들에서 시작합니다.

핸들은 `aria-label`이 있는 `<button type="button">`으로 만드세요. 그대로 포커스할 수 있고, 스크린
리더가 레이블을 읽어 줍니다. 아이콘만 든 `<span>`도 동작하지만(라이브러리가 `tabindex="0"`과
`role="button"`을 줍니다), 의미 있는 이름이 없습니다.

<note>

터치 드래그는 200 ms 동안 길게 누른 뒤에 시작하므로 페이지는 계속 스크롤할 수 있습니다.
핸들에서 바로 드래그하려면 핸들에 `touch-action: none`을, 항목에
`activation: { touch: { distance: 4 } }`를 주세요.

</note>

## 전략

`strategy`는 리스트가 어떻게 배치되는지 알려 주어, 행이 올바른 방향으로 자리를 비키게 합니다.

<table>
<thead>
  <tr>
    <th>
      <code>
        strategy
      </code>
    </th>
    
    <th>
      용도
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        'vertical'
      </code>
      
      (기본값)
    </td>
    
    <td>
      행이 세로로 쌓인 리스트.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'horizontal'
      </code>
    </td>
    
    <td>
      한 줄: 탭, 칩, 타임라인.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'grid'
      </code>
    </td>
    
    <td>
      CSS 그리드와 줄바꿈되는 flex 행. 크기가 다른 항목도 됩니다.
    </td>
  </tr>
</tbody>
</table>

탭 한 줄은 가로 리스트입니다.

<example height="23rem" hint="탭을 드래그하세요. 탭을 닫거나 새로 열 수도 있습니다" name="browser-tabs">



</example>

```ts [Browser.vue]
const { items } = makeSortableList(strip, {
  items: tabs,
  strategy: 'horizontal',
  axis: 'x',        // the tabs stay on their row
  animateChanges: true, // the buttons open and close tabs: slide the others then too
});
```

탭의 닫기 버튼은 드래그할 수 있는 탭 안에서도 동작하고, 드래그를 끝내는 클릭은 탭을 선택하지
않습니다. 행은 드래그하는 동안 저절로 미끄러지듯 움직입니다. `animateChanges`는 드래그가 아닌 변경, 여기서는
버튼에 의한 변경에도 같은 효과를 줍니다([애니메이션](https://zizigy.github.io/docs/guides/animations) 참고).

그리드는 크기가 제각각인 타일로 2차원 정렬을 합니다.

<example height="31rem" hint="사진을 드래그하세요. 큰 사진도 됩니다" name="photo-grid">



</example>

```ts [PhotoGrid.vue]
const { items } = makeSortableList(grid, { items: photos, strategy: 'grid' });
```

```css
.grid { display: grid; grid-template-columns: repeat(4, 76px); grid-auto-rows: 76px; grid-auto-flow: dense; }
.wide { grid-column: span 2; }
.tall { grid-row: span 2; }
```

`grid-auto-flow: dense`는 큰 타일이 움직여도 그리드를 빈틈없이 유지합니다. 이것이 없으면 그리드는
타일이 더 이상 들어가지 않는 곳에 구멍을 남기고 행을 하나 늘립니다.

## 축 고정

```ts
makeSortableList(list, { items: layers, axis: 'y' });
```

`axis: 'y'`는 드래그를 세로축에, `'x'`는 가로축에 묶어 둡니다. 위의 레이어와 탭이 이를 씁니다.
항목 자체의 `axis`가 리스트의 것보다 우선합니다. 드래그를 리스트 안에도 가두려면
`restrictToParent` 모디파이어를 추가하세요. 자세한 내용은 [제약](https://zizigy.github.io/docs/guides/constraints)에
있습니다.

## 자리 스타일링

드래그 중인 행은 안착할 자리로서 리스트에 남고, `data-dnd-placeholder`를 받습니다. 기본 스타일은
명시도 0으로 불투명도를 40%로 낮추므로, 직접 작성한 규칙이 항상 우선합니다.

```css [LayerRow.vue]
.layer[data-dnd-placeholder] {
  opacity: 1;
  box-shadow: inset 0 0 0 1.5px #42b883;
}

.layer[data-dnd-placeholder] > * {
  visibility: hidden;   /* an empty slot of the row's exact size */
}
```

`makeSortableItem`은 같은 상태를 ref로 반환하고, 몇 가지를 더 반환합니다.

<table>
<thead>
  <tr>
    <th>
      반환값
    </th>
    
    <th>
      의미
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        isPlaceholder
      </code>
    </td>
    
    <td>
      이 행이 드래그 중인 항목이 안착할 자리입니다.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        isDragging
      </code>
    </td>
    
    <td>
      이 행을 드래그하는 중입니다(멀티 드래그에서는 드래그 중인 모든 행).
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        index
      </code>
    </td>
    
    <td>
      초안에서의 인덱스. 드래그하는 동안 바뀝니다.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        selected
      </code>
    </td>
    
    <td>
      쓰기 가능하며, 프로바이더의 선택에 연결됩니다.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        indicator
      </code>
    </td>
    
    <td>
      <a href="#an-insertion-line">
        인디케이터 모드
      </a>
      
      에서 선이 들어갈 위치.
    </td>
  </tr>
</tbody>
</table>

리스트는 자체 강조 표시를 위한 `isOver`와 `isAccepting`, 그리고 드래그 중인 항목과 이 리스트
초안에서의 그 위치인 `activeKey`와 `activeIndex`를 반환합니다. 모든 상태와 라이브 데모는
[스타일링](https://zizigy.github.io/docs/guides/styling)에 있습니다.

## 기능 끄기

<table>
<thead>
  <tr>
    <th>
      옵션
    </th>
    
    <th>
      막는 것
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      항목의 <code>
        disabled
      </code>
    </td>
    
    <td>
      이 항목은 드래그할 수 없습니다. 다른 항목은 여전히 이 항목을 지나 움직입니다. 위쪽 레이어를 잠근 뒤, 다른 레이어를 그 위로 드래그해 보세요.
    </td>
  </tr>
  
  <tr>
    <td>
      리스트의 <code>
        disabled
      </code>
    </td>
    
    <td>
      리스트에서 나가는 드래그도, 리스트로 들어오는 드롭도 없습니다.
    </td>
  </tr>
  
  <tr>
    <td>
      리스트의 <code>
        sort: false
      </code>
    </td>
    
    <td>
      리스트 안에서는 순서를 바꿀 수 없지만, 밖으로는 나갈 수 있습니다. 팔레트가 그렇습니다. <a href="/docs/guides/shared-lists#what-goes-in-and-out-pull-and-put">
        항목을 공유하는 리스트
      </a>
      
      를 참고하세요.
    </td>
  </tr>
</tbody>
</table>

모두 getter나 ref를 받으며, 다음 드래그부터 적용됩니다.

```ts [LayerRow.vue]
makeSortableItem(el, { item: () => props.layer, handle: '.grip', disabled: () => props.layer.locked });
```

다른 행이 무엇을 하든 항상 처음이나 마지막에 있어야 하는 행은 아예 항목이 아닙니다. 리스트 밖에
렌더링하세요. 패널의 Background 레이어는 `<ul>` 아래에 있습니다.

## 항목 일부만 보여 주기

검색창이나 태그처럼, `filter`는 `true`를 반환한 항목만 보여 주고 드래그는 그 항목들 사이에서
정렬합니다. 나머지 항목은 원본 배열 안의 제자리에 그대로 남습니다.

<example height="27rem" hint="태그를 고르거나 검색한 뒤 작업을 드래그하세요. 배열은 숨겨진 작업을 제자리에 유지합니다" name="filtered-list">



</example>

```ts [Backlog.vue]
const query = ref('');
const tag = ref<Tag | null>(null);

const { items } = makeSortableList(list, {
  items: tasks,
  filter: (task) => (!tag.value || task.tag === tag.value) && task.title.includes(query.value),
});
```

- `items`에는 조건에 맞는 항목이 들어 있습니다. 평소처럼 렌더링하세요. 필터는 반응형이므로 검색창에
입력하면 리스트가 바로 바뀝니다.
- 드롭은 배열 전체를 기록합니다. 숨겨진 각 항목은 원래 뒤따르던 보이는 항목 바로 뒤에 남습니다. 그
항목이 옮겨진 항목이라면, 그 앞에서 옮겨지지 않은 가장 가까운 항목 뒤에 남습니다.
- `e.to.index`와 리스트의 `activeIndex`는 보이는 항목을 기준으로 셉니다. `e.result`에는 숨겨진
항목을 포함한 배열 전체가 들어 있습니다.

<note>

원본 배열을 걸러 내는 `computed`를 `items`로 넘겨도 되지만, 그것은 읽기 전용입니다. `e.result`를
직접 적용하고 숨겨진 항목이 어디로 갈지도 직접 계산해야 합니다. `filter`는 둘 다 해 줍니다.

</note>

## 삽입선

`placeholder: 'indicator'`를 쓰면 드래그하는 동안 리스트는 그대로 있고, 선이 드롭될 위치를
보여 줍니다. 선은 직접 그립니다.

<example height="27rem" hint="이슈를 드래그하세요. 선이 들어갈 위치를 보여 줍니다" name="issue-table">



</example>

```ts [IssueTable.vue]
const { items, indicator } = makeSortableList(body, { items: issues, placeholder: 'indicator' });
```

```vue [IssueRow.vue]
<script setup lang="ts">
const { indicator } = makeSortableItem(el, { item: () => props.issue });
</script>

<template>
  <div ref="el" class="row" :class="indicator && `drop-${indicator.side}`">…</div>
</template>

<style scoped>
.row.drop-before::before { content: ''; position: absolute; top: -1.5px; left: 6px; right: 6px; height: 2px; background: #42b883; }
.row.drop-after::before { content: ''; position: absolute; bottom: -1.5px; left: 6px; right: 6px; height: 2px; background: #42b883; }
</style>
```

<table>
<thead>
  <tr>
    <th>
      <code>
        indicator
      </code>
      
       필드
    </th>
    
    <th>
      담고 있는 것
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        list
      </code>
    </td>
    
    <td>
      드롭이 들어갈 리스트.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        index
      </code>
    </td>
    
    <td>
      렌더링된 리스트에서의 삽입 인덱스.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        key
      </code>
    </td>
    
    <td>
      선이 붙은 항목. 빈 리스트에서는 <code>
        null
      </code>
      
      입니다(이때는 리스트에 그리세요).
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        side
      </code>
    </td>
    
    <td>
      그 항목의 <code>
        'before'
      </code>
      
       또는 <code>
        'after'
      </code>
      
      . <a href="#swap-instead-of-shift">
        자리를 맞바꾸는
      </a>
      
       리스트에서는 <code>
        'swap'
      </code>
      
      . 트리에는 <code>
        'inside'
      </code>
      
      가 추가됩니다.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        depth
      </code>
      
      , <code>
        parentKey
      </code>
    </td>
    
    <td>
      트리 전용: 드롭이 안착할 위치. <a href="/docs/guides/trees#an-insertion-line">
        트리
      </a>
      
      를 참고하세요.
    </td>
  </tr>
</tbody>
</table>

- 리스트의 `indicator`는 선이 그 리스트에 있는 동안 설정되고(표가 이를 출력합니다), 항목의
`indicator`는 선이 그 항목에 붙어 있는 동안 설정됩니다.
- 드래그 중인 행은 제자리에 남아 플레이스홀더로 표시됩니다. 흐리게 처리하세요.
- 키보드로도 선을 움직일 수 있습니다.
- 이 모드에서는 모든 대상 밖에 드롭하면 취소됩니다(`dropOutside: 'cancel'`). 리스트가 다른 순서를
보여 준 적이 없기 때문입니다.
- 오버레이가 그 아래 행의 일부를 가립니다. 오버레이를 약간 투명하게 하면 선이 비쳐 보입니다:
`[data-dnd-overlay] .row { opacity: 0.85 }`.

## 밀어내는 대신 자리 맞바꾸기

`behavior: 'swap'`을 쓰면 드래그 중인 항목이 그 아래 항목과 자리를 맞바꾸고, 나머지는 그대로
있습니다.

<example height="29rem" hint="두 사람의 자리를 맞바꾸세요. 컬럼 사이에서도 됩니다. On drop도 시험해 보세요" name="on-call">



</example>

```ts [Rotation.vue]
const { items } = makeSortableList(list, {
  id: props.column.id,
  items: toRef(props.column, 'people'),
  group: 'rotation',
  behavior: 'swap',
  placeholder: () => props.placeholder,   // 'item' (live) or 'indicator' (on drop)
  onDrop: (e) => {
    if (e.partner) emit('swap', `${e.items[0]?.name} ⇄ ${e.partner.item.name}`);
  },
});
```

- **실시간**(`placeholder: 'item'`, 기본값): 드래그하는 동안 맞바뀐 모습이 보입니다. 되돌아오거나
리스트 밖으로 나가면 원래대로 돌아갑니다.
- **드롭할 때**(`placeholder: 'indicator'`): 드롭할 때까지 아무것도 움직이지 않습니다.
인디케이터는 드롭이 자리를 맞바꿀 항목을 `side: 'swap'`으로 표시합니다. 선을 그리는 대신 그
항목에 외곽선을 그리세요. 화살표 키는 표시를 항목에서 항목으로 옮기고, 드래그 중인 항목으로
돌아오면 표시가 없습니다.

```vue [PersonCell.vue]
<li ref="el" :class="{ target: indicator?.side === 'swap' }">…</li>
```

드롭 이벤트에는 `mode: 'swap'`과, 자리를 맞바꾼 항목인 `partner`가 있습니다. `partner`에는 그
항목의 `key`, `item`, 그리고 그 항목 자신의 `from`과 `to`가 들어 있습니다. 같은 그룹의 두
리스트가 모두 `behavior: 'swap'`이면, 두 컬럼처럼 리스트 사이에서도 자리를 맞바꿉니다. 이때
포인터 아래의 항목이 다른 리스트로 옮겨가므로, 나가는 것(`group.pull`)과 들어오는 것(`group.put`)이
모두 허용되어야 합니다. [항목을 공유하는 리스트](https://zizigy.github.io/docs/guides/shared-lists)를 참고하세요. 멀티
드래그는 자리를 맞바꾸지 않고 정렬합니다.

## 키보드

행이나 그 핸들에 포커스를 두고 <kbd value="Space">



</kbd>

나 <kbd value="Enter">



</kbd>

를 누르면 집습니다.
리스트 방향의 화살표 키는 한 자리씩 옮기고, 그리드에서 위아래 키는 다음 줄로 가며, 리스트와
교차하는 방향의 화살표 키는 같은 그룹의 다음 리스트로 옮깁니다. 리스트 끝을 지나면 화살표 키는 그
방향의 다음 리스트(위아래로 쌓인 리스트)로 이어서 가고, 없으면 그쪽의 드롭 대상(휴지통 영역)으로
갑니다. 다시 <kbd value="Space">



</kbd>

나
<kbd value="Enter">



</kbd>

를 누르면 드롭하고, <kbd value="Escape">



</kbd>

를 누르면 되돌립니다. 이 페이지의 모든
데모가 그렇게 동작합니다. 스크린 리더 문구는 [접근성](https://zizigy.github.io/docs/guides/accessibility)에 있습니다.

## 애니메이션

드래그하는 동안 행은 새 자리로 미끄러지듯 이동하고, 드롭한 행은 제자리에 안착합니다. 행이 들어오고
나갈 때 리스트 자체도 새 크기로 커지거나 줄어듭니다(`animateSize`). 기본으로 켜져 있고 코드가 필요
없습니다. 행을 `<TransitionGroup>`으로 감싸지 마세요. 이동이 두 번
애니메이션됩니다. 행의 `transform`에 CSS `transition`을 걸지도 마세요. 드래그가 아닌
변경(추가, 제거, 섞기)에는 탭처럼 리스트의 `animateChanges` 옵션을 켜세요.
[애니메이션](https://zizigy.github.io/docs/guides/animations)을 참고하세요.

전체 옵션 목록은 [makeSortableList](https://zizigy.github.io/docs/api/make-sortable-list)와
[makeSortableItem](https://zizigy.github.io/docs/api/make-sortable-item) API에 있습니다.

## Sitemap

See the full [sitemap](https://zizigy.github.io/vue-dnd-kit/sitemap.md) for all pages.
