문서/가이드

오버레이

드래그하는 동안 포인터를 따라가는 것. 기본 클론과 그 스타일링, 드래그에 따라 바뀌는 오버레이 컴포넌트, 여러 항목 한꺼번에, 크기, 그리고 렌더링 위치.

이 페이지 편집

이 페이지는 AI가 번역했으며 부정확한 내용이 있을 수 있습니다. 영어 원문 읽기

드래그하는 동안 박스 하나가 포인터를 따라갑니다. 이것이 오버레이입니다. 드래그 중인 요소 자체는 안착할 자리로서 제자리에 남습니다. 드롭하면 오버레이가 그 자리로 날아가 사라집니다.

오버레이는 컴포넌트 밖, body로 텔레포트됩니다. 기본적으로 드래그 중인 요소의 클론을 보여 줍니다.

Live

데모 불러오는 중…

<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Expenses from './Expenses.vue';
</script>

<template>
  <DnDProvider>
    <Expenses />
  </DnDProvider>
</template>

기본 클론

클론은 무엇이든 움직이기 전, 집을 때 만들어집니다. 요소의 깊은 복사본입니다.

  • 폼 값과 <canvas> 픽셀이 복사됩니다.
  • 요소가 리스트 밖에서 잃게 될 계산된 스타일은 인라인으로 들어갑니다. 상속된 글꼴과 색상, 조상을 통해 적용되는 스타일(.list li { … }), 표 셀의 너비 같은 것입니다. 표의 행은 별도의 표로 감싸서 옵니다.
  • id와 data-dnd-* 속성은 제거되고, 클론은 aria-hidden입니다.
  • UI 키트(Vuetify, Quasar, PrimeVue, Naive UI)의 물결 효과 등 누르기 효과는 빠집니다.

위 표에서 값을 입력한 행은 텍스트, 금액, 체크, 차트와 함께 옮겨지며, 실제 셀의 너비를 유지하는 셀로 된 별도의 표에 담깁니다.

함께 옮겨지면 안 되는 다른 것에는 **data-dnd-no-clone**을 표시하세요. 메뉴 버튼, 툴팁, 선택 표시 같은 것입니다. 각 행의 제거 버튼은 남습니다.

ExpenseRow.vue
<tr ref="el" class="row">
  …
  <td><button type="button" aria-label="Remove" data-dnd-no-clone @click="emit('remove')">×</button></td>
</tr>

배경은 추측하지 않습니다

클론은 요소를 있는 그대로 보여 줍니다. 리스트가 배경을 칠해서 자체 배경이 없는 행(흰 <ul> 안의 <li>, v-list-item, 표의 행)은 비쳐 보이는 클론이 됩니다. CSS로 리스트의 배경을 주세요. 여기서는 셀에 줍니다.

ExpenseRow.vue
<style scoped>
[data-dnd-overlay] .row td {
  background: color-mix(in srgb, currentColor 4%, Canvas);
}
</style>

클론은 Vue의 스코프 속성을 포함해 요소의 클래스와 속성을 그대로 유지하므로, 이 규칙은 전역 스타일시트뿐 아니라 행의 scoped 스타일에서도 동작합니다. 다른 방법은 행 자체에 배경을 주는 것입니다. 떠 있도록 만든 아이콘과 타일은 의도적으로 투명하게 남습니다.

박스 스타일링

박스는 data-dnd-overlay가 있는 요소입니다. position: fixed이고, 맨 위에 있으며 (z-index: 9999), pointer-events: none과 inert이고, 속성 값이 지금 무엇을 하는지 알려 줍니다.

data-dnd-overlay시점
"dragging"포인터를 따라갑니다.
"dropping"자리에 안착하거나, 원래 자리로 돌아가거나, 비동기 onBeforeDrop을 기다립니다.

드래그하는 동안 들어 올린 모습을 주고, 안착하면 사라지게 합니다.

[data-dnd-overlay='dragging'] .row td {
  box-shadow: 0 14px 20px -16px rgb(0 0 0 / 0.7);
}

박스는 transform으로 움직이고 크기를 width와 height로 받습니다. 박스가 아니라 그 안의 내용을 스타일링하세요. 클론은 박스를 채우며, 루트에 width: 100%, height: 100%, transform: none이 설정됩니다. 회전과 확대는 rotate와 scale 속성으로 하세요. 트랜지션과 키프레임 모두 이 속성을 애니메이션합니다. 레시피는 애니메이션에 있습니다.

오버레이 컴포넌트

클론이 아닌 다른 것을 보여 주려면 컴포넌트를 넘기세요. 컴포넌트는 박스 안에 렌더링되고 드래그를 props로 받으며, 이 props는 드래그하는 동안 바뀝니다. 드래그가 어디에 있는지, 그곳이 드래그를 받는지, 드롭이 복사인지, 그리고 드롭 자체입니다.

Live

데모 불러오는 중…

<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Column from './Column.vue';

const columns = ref([
  {
    id: 'Backlog',
    tickets: [
      { id: 't1', title: 'Dark mode toggle', tag: 'UI', approved: false },
      { id: 't2', title: 'Retry failed uploads', tag: 'API', approved: true },
      { id: 't3', title: 'Keyboard shortcuts', tag: 'UI', approved: false },
    ],
  },
  { id: 'Doing', tickets: [{ id: 't4', title: 'Faster search', tag: 'API', approved: true }] },
  { id: 'Done', tickets: [{ id: 't5', title: 'Onboarding tour', tag: 'UI', approved: true }] },
]);
</script>

<template>
  <DnDProvider>
    <div class="board">
      <Column v-for="column in columns" :key="column.id" :column="column" />
    </div>
  </DnDProvider>
</template>

<style scoped>
.board {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 12px;
  width: 100%;
  max-width: 600px;
  margin: 0 auto;
}
</style>
TicketOverlay.vue
<script setup lang="ts">
import { computed } from 'vue';
import type { DnDStatus, DragItem, DragMode, Over } from '@vue-dnd-kit/core';

const props = defineProps<{ items: DragItem[]; over: Over | null; accepted: boolean; status: DnDStatus; mode: DragMode }>();
const ticket = computed(() => props.items[0]?.item as Ticket);

const state = computed(() => {
  if (props.status === 'dropping') return props.over ? 'landed' : 'back'; // a cancel flies back
  if (!props.over) return 'free';
  return props.accepted ? 'ok' : 'refused';
});
</script>

<template>
  <!-- Fills the overlay box, which takes the size of the slot. -->
  <div class="ticket-overlay" :data-state="state">
    {{ ticket.title }}
    <span v-if="mode === 'copy'" class="copy">+ copy</span>
  </div>
</template>

<style scoped>
.ticket-overlay { width: 100%; height: 100%; }
.ticket-overlay[data-state='refused'] { background: color-mix(in srgb, #ef4444 8%, Canvas); }
</style>
Prop담고 있는 것
active드래그(id, ids, data, type, …).
items드래그 중인 모든 항목, 집은 항목이 처음: { id, item, data, type }. 정렬 가능한 드래그에서 item은 리스트의 항목입니다.
over드래그 아래의 대상. 드롭이 안착하는 동안에는 안착하는 대상. 아무것도 없는 곳 위에서, 그리고 거부되거나 취소된 드래그가 돌아가는 동안에는 null. 정렬 가능한 리스트의 over.id는 리스트의 id입니다. 위의 컬럼은 자기 이름을 씁니다.
acceptedover가 드래그를 받는지. 받지 않으면 오버레이를 빨갛게 물들이세요.
status'dragging', 안착하는 동안에는 'dropping'.
mode'move', 'copy'(clone이 있는 리스트에서 Alt를 누르고 드래그하거나 팔레트에서 드래그), 'swap', 'insert'(일반 draggable이 리스트로 들어감).
to지금 드롭하면 항목이 놓일 곳: { list, index }, 트리에서는 parentKey와 depth도. 어떤 리스트에도 놓지 않을 때는 null. 예를 들어 일반 대상 위. 드롭이 갈 곳을 참고하세요.

props의 타입은 OverlayProps이며, 읽는 것만 선언하면 됩니다. 드래그되는 대상에 컴포넌트를 넘기세요.

import TicketOverlay from './TicketOverlay.vue';

makeSortableList(listRef, { items: tickets, overlay: TicketOverlay });   // every item of the list
makeSortableItem(el, { item: () => props.ticket, overlay: TicketOverlay }); // this item
makeDraggable(el, { id: 'chip', overlay: TicketOverlay });                // a plain draggable

컴포넌트는 그대로 넘기거나, 바꾸려면 shallowRef에 담아 넘기거나(드래그 중에도 됩니다. 오버레이가 바로 바뀝니다), 함수로 넘길 수 있습니다. Vue는 함수를 함수형 컴포넌트로 받아들입니다. TSX에서는 overlay: (props) => <TicketOverlay {...props} compact />처럼 씁니다. 깊은 ref나 reactive에는 절대 넣지 마세요. Vue가 컴포넌트를 반응형으로 만들고 경고합니다.

오버레이 컴포넌트의 루트는 박스를 채워야 합니다(width: 100%; height: 100%). 박스는 자리의 크기를 받고 드롭할 때 그 크기를 애니메이션합니다. 루트에 고정 크기를 주면 박스 안에서 그 크기가 유지됩니다.

프로바이더의 #overlay 슬롯

자체 컴포넌트가 없는 모든 드래그에는 프로바이더의 슬롯이 오버레이를 렌더링합니다. 앱 루트 옆에 한 번 정의하면 앱의 모든 리스트와 draggable이 이를 씁니다. 여기서 메모는 자체 고스트를 받고, 사진은 클론으로 돌아갑니다.

Live

데모 불러오는 중…

<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Moodboard from './Moodboard.vue';
import NoteGhost from './NoteGhost.vue';
</script>

<template>
  <DnDProvider>
    <Moodboard />
    <!--
      The overlay of every drag in the app that has none of its own. Notes get a ghost
      of their own; for photos the slot renders nothing, so they fall back to the clone.
    -->
    <template #overlay="{ items, status }">
      <NoteGhost v-if="items[0]?.type.includes('note')" :items="items" :status="status" />
    </template>
  </DnDProvider>
</template>
App.vue
<DnDProvider>
  <RouterView />
  <template #overlay="{ items, status }">
    <NoteGhost v-if="items[0]?.type.includes('note')" :items="items" :status="status" />
  </template>
</DnDProvider>

슬롯은 오버레이 컴포넌트와 같은 props를 받습니다. 아무것도 렌더링하지 않으면(위의 v-if) 드래그는 클론을 씁니다. 드래그는 type으로 구별하세요. 정렬 가능한 항목의 타입은 직접 지정하지 않는 한(makeSortableItem(el, { type })) 그룹 이름입니다.

오버레이를 바꾸는 대상

드롭 대상은 자신이 받는 드래그가 위에 있는 동안 오버레이가 무엇을 보여 줄지 정할 수 있습니다. makeDroppable, 리스트, 트리는 overOverlay를 받습니다(리스트나 트리의 overlay는 자기 항목을 위한 것입니다). 폴더 위에서는 드래그 중인 메시지의 칩이 드롭이 할 일을 보여 주는 모습으로 바뀝니다.

Live

데모 불러오는 중…

<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Mailbox from './Mailbox.vue';
</script>

<template>
  <!-- The mailbox reads the selection with `useDnD()`: it renders inside the provider. -->
  <DnDProvider>
    <Mailbox />
  </DnDProvider>
</template>
Folder.vue
makeDroppable(el, {
  id: () => props.folder.id,
  data: () => props.folder,
  overOverlay: MovePill,
  onDrop: (event) => emit('move', event.active.ids),
});

대상의 컴포넌트는 다른 오버레이 컴포넌트와 같은 props를 받고(over도 그중 하나입니다), 드래그 자체의 오버레이(그 컴포넌트, 프로바이더의 슬롯, 또는 클론)를 기본 슬롯으로 받습니다. MovePill은 슬롯을 빼므로 칩을 대체합니다.

MovePill.vue
<script setup lang="ts">
import { computed } from 'vue';
import type { DragItem, Over } from '@vue-dnd-kit/core';

const props = defineProps<{ items: DragItem[]; over: Over | null }>();
// The folder under the drag: its `data`.
const folder = computed(() => props.over?.data as Folder | undefined);
</script>

<template>
  <!-- Fills the box like any overlay component. -->
  <div class="pill-box">
    <span v-if="folder" class="pill">Move {{ items.length }} to {{ folder.name }}</span>
  </div>
</template>

<slot />을 렌더링하면 드래그 자체의 오버레이를 유지한 채 감쌀 수 있습니다. 위 오버레이 컴포넌트의 보드에서 Done이 하는 일이 바로 그것입니다. Done 위에서 승인된 티켓은 자기 오버레이를 유지하고, Done이 그 위에 도장을 찍으며, 다른 컬럼에서 온 티켓은 드롭될 때 색종이가 터집니다.

Column.vue
makeSortableList(listRef, {
  items: tickets,
  group: { name: 'release', put },
  overlay: TicketOverlay, // the tickets' own, in every column
  overOverlay: strict ? DoneOverlay : undefined, // Done's, around it
});
DoneOverlay.vue
<template>
  <div class="done">
    <slot /> <!-- TicketOverlay -->
    <span class="stamp">Shipped</span>
    <span v-if="status === 'dropping' && arrives" class="confetti">…</span>
  </div>
</template>
  • 대상이 드래그를 받는 동안에만 보이고, 드롭이 그 대상에 안착하는 동안에도 남습니다. 대상을 벗어나면 드래그 자체의 오버레이가 돌아옵니다.
  • 바뀔 때는 크로스페이드됩니다. 방식은 프로바이더의 overlayTransition이 정합니다. false는 바로 바꾸고, 객체는 내용을 감싸는 <Transition>의 props입니다(name, mode, GSAP이나 motion용 JS 훅). mode: 'out-in'이 없으면 잠깐 둘 다 보입니다. .<name>-leave-active에서 떠나는 쪽에 position: absolute를 주세요.
  • 드롭은 색종이를 기다립니다. 안착하는 동안 오버레이 안에서 실행되는 CSS 애니메이션과 트랜지션은 끝까지 재생되며, 최대 2초입니다.
  • overlay: false인 드래그는 어떤 대상 위에서도 오버레이가 없습니다.

폴더는 또한 필을 자기만의 방식으로, 폴더 아이콘 안으로 안착시킵니다. 대상도 overDropAnimation을 가질 수 있습니다. 애니메이션을 참고하세요.

드롭이 갈 곳: to

to는 지금 드롭하면 항목이 어디에 놓일지를 오버레이에 알려 줍니다. 드롭 이벤트의 to가 알려 줄 것과 같습니다. 리스트와 그 안의 인덱스, 트리에서는 부모와 깊이입니다. 자리가 움직이면 바뀝니다. 이 사이트 메뉴의 페이지는 어느 섹션의 몇 번째 행으로 들어갈지 알려 줍니다.

Live

데모 불러오는 중…

<script setup lang="ts">
import { provide, ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import MenuTree from './MenuTree.vue';
import { MENU, site } from './menu';

// Provided above the provider: its overlay renders under it, so the overlay sees it too.
provide(MENU, ref(site()));
</script>

<template>
  <DnDProvider>
    <MenuTree />
  </DnDProvider>
</template>
MoveOverlay.vue
<script setup lang="ts">
import { computed } from 'vue';
import type { DragItem, DropDestination } from '@vue-dnd-kit/core';

const props = defineProps<{ items: DragItem[]; to: DropDestination | null }>();
const page = computed(() => props.items[0]!.item as MenuNode);
// `parentKey` is the section the page would go into, `null` at the top level.
const where = computed(() => (props.to ? pathOf(menu.value, props.to.parentKey) : null));
</script>

<template>
  <div class="move">
    <div class="row">{{ page.title }}</div>
    <p v-if="to && where" class="where">Move to <b>{{ where }}</b>, row {{ to.index + 1 }}</p>
  </div>
</template>

모든 대상 밖에서도 to는 여전히 자리의 위치입니다. 기본적으로 그곳에서 드롭하면 항목은 자리가 있는 곳에 남습니다(dropOutside: 'placeholder'). 드롭이 항목을 어떤 리스트에도 놓지 않을 때는 null입니다. 일반 대상 위, 항목을 거부하는 리스트 위, dropOutside: 'cancel'이나 'remove'일 때의 대상 밖이 그렇습니다.

우선순위

각 드래그에는 해당하는 첫 번째 것이 쓰입니다.

  1. 드롭 대상의 overOverlay(리스트나 트리의 것도 마찬가지). 드래그가 그 위에 있고 대상이 드래그를 받는 동안입니다. 그렇지 않았다면 보였을 것이 그 기본 슬롯이 됩니다.
  2. 항목 자체의 overlay(makeSortableItem, makeDraggable).
  3. 그 리스트나 트리의 overlay.
  4. 프로바이더의 #overlay 슬롯. 단, 아무것도 렌더링하지 않으면 제외.
  5. 기본 클론. 멀티 드래그에서는 클론의 열이나 더미입니다. 멀티 드래그를 참고하세요.

드래그에 오버레이가 아예 있는지(overlay: false)는 드래그가 시작될 때 정해집니다. 어떤 컴포넌트를 보여 줄지는 실시간으로 읽습니다. 드래그 중에 바꾼 ref가 컴포넌트를 바꾸고, 자기 오버레이가 있는 대상에 들어가거나 나올 때도 바뀝니다.

DragPreview: 레이아웃 안의 클론

DragPreview는 드래그 중인 항목 하나에 대해 라이브러리가 만든 클론을 렌더링합니다. 오버레이 컴포넌트는 이를 여러 개 배치하거나, 하나에 틀을 씌울 수 있습니다. 책을 클릭해 고른 뒤 하나를 드래그해 보세요. 오버레이가 실제 클론을 부채꼴로 펼칩니다.

Live

데모 불러오는 중…

<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Shelf from './Shelf.vue';
</script>

<template>
  <DnDProvider>
    <Shelf />
  </DnDProvider>
</template>
FanOverlay.vue
<script setup lang="ts">
import { computed } from 'vue';
import { DragPreview, type DragItem } from '@vue-dnd-kit/core';

const props = defineProps<{ items: DragItem[] }>();
const fan = computed(() =>
  props.items.slice(0, 4).map((entry, index) => ({
    entry,
    angle: index === 0 ? 0 : (index % 2 ? -1 : 1) * Math.ceil(index / 2) * 10,
  })),
);
</script>

<template>
  <div class="fan">
    <div v-for="{ entry, angle } in fan" :key="entry.id" class="card" :style="{ transform: `rotate(${angle}deg)` }">
      <DragPreview :id="entry.id" class="face" :style="{ width: '100%', height: '100%' }">
        <div class="blank" /> <!-- an item without a clone -->
      </DragPreview>
    </div>
    <span v-if="items.length > 1" class="count">{{ items.length }}</span>
  </div>
</template>
  • 클론은 집은 항목과, 드롭 순서상 그 옆의 다른 항목들에 대해 프로바이더의 overlayPreviews만큼 존재합니다(기본값 5, 'all'이면 전부). 집을 때 렌더링되어 있는 경우에 한합니다. 그 밖의 항목에는 DragPreview가 기본 슬롯을 렌더링합니다.
  • 미리보기는 집을 때의 항목 크기를 가지며, 이 크기는 인라인으로 지정됩니다. 위처럼 그대로 전달되는 style로 크기를 정하세요(클래스로는 덮어쓸 수 없습니다).
  • 정렬 가능한 멀티 드래그(multiple: 'group' 또는 'gaps')에서는 드롭할 때 각 미리보기가 자기 항목의 자리에 안착합니다. 래퍼는 rotate 속성이 아니라 transform으로 회전하세요. 안착하는 동안 원래대로 돌아갑니다. 항목의 모양(모서리, 그림자)은 class="face"처럼 미리보기 자체에 주세요. 래퍼는 박스 안에 남습니다.

멀티 드래그 가이드에는 클론 대신 개수와 제목을 보여 주는 오버레이가 있습니다.

크기: overlayFit

크기가 다른 리스트 위에서는 박스가 안착할 자리의 크기를 받습니다. 이는 프로바이더의 overlayFit으로 바꿉니다. DnDProvider 페이지에 좁은 리스트와 넓은 리스트로 된 데모가 있습니다.

overlayFit(프로바이더)박스는
'slot'(기본값)안착할 자리의 크기로 부드럽게 바뀝니다. 넓은 리스트 위에서는 넓어집니다.
'source'드래그 중인 요소의 크기를 유지합니다.

'slot'에서는 포인터를 중심으로 박스 크기가 바뀌며, 포인터는 박스 안의 같은 비율 위치에 머뭅니다. 내용은 절대 확대·축소되지 않고 새 크기에 맞춰 다시 배치되므로, 그 데모의 작업처럼 flex 행은 어떤 너비에서도 제대로 보입니다. 가는 선처럼 항목보다 훨씬 작게 스타일링한 자리는 그 축 방향으로 박스를 줄이지 않습니다.

드롭은 두 설정 모두에서 같습니다. 박스는 안착할 요소로 날아가 그 요소의 크기로 애니메이션되고, 내용은 그동안 다시 배치됩니다. 항목이 두 리스트에서 다르게 보이면(팔레트의 작은 칩, 보드의 완전한 카드), 원본의 클론이 대상의 크기로 다시 배치되어 그곳에서 찌그러지거나 늘어나 보일 수 있습니다. 이를 바로잡는 방법은 두 가지입니다.

  • 위의 작업 행처럼 항목에 두 크기 모두에서 동작하는 레이아웃을 주거나, 안착할 수 있는 모든 리스트에서 같은 크기를 주세요.
  • 오버레이 컴포넌트에서 대상의 모양을 렌더링하세요. 컴포넌트는 over를 받으므로 위에 있는 리스트의 모양으로 바꿀 수 있고, 어떤 크기에서도 박스를 채웁니다.

렌더링 위치: overlayTo

프로바이더는 오버레이를 body로 텔레포트합니다. showModal()로 연 모달 <dialog>는 body의 모든 것보다 위인 top layer에 있으므로, 그곳의 오버레이는 다이얼로그 아래에 가려집니다. overlayTo가 다이얼로그 안의 선택자나 요소를 가리키게 하세요. DnDProvider 페이지에 이 설정이 있을 때와 없을 때를 비교해 볼 수 있는 다이얼로그가 있습니다.

index.vue
<script setup lang="ts">
const dialog = useTemplateRef<HTMLDialogElement>('dialog');
const open = ref(false);
// While the dialog is open, the overlay renders inside it.
const overlayTo = computed(() => (open.value ? '#columns-dialog' : 'body'));

const show = () => {
  dialog.value?.showModal();
  open.value = true;
};
</script>

<template>
  <DnDProvider :overlay-to="overlayTo">
    <button type="button" @click="show">Edit columns</button>
    <dialog id="columns-dialog" ref="dialog" @close="open = false">
      <ColumnList v-model:columns="columns" />
    </dialog>
  </DnDProvider>
</template>
  • 프로바이더가 마운트된 뒤 오버레이를 렌더링할 때 대상이 페이지에 있어야 합니다. 닫힌 <dialog>도 숨겨져 있을 뿐 페이지에 있습니다. 닫혀 있는 동안에는 body로 되돌려 다른 드래그의 오버레이가 보이게 하세요.
  • 박스는 position: fixed입니다. 대상이나 그 조상에 transform, filter, contain이 있으면 뷰포트 대신 그 요소가 박스의 기준이 되므로, 다이얼로그에는 쓰지 마세요.
  • 클론의 스타일은 표시되는 위치에 맞춰 계산됩니다. 섀도 루트에서는 overlayTo가 그 안을 가리키게 하면 클론이 섀도 루트의 스타일을 유지합니다.

오버레이 없이

makeDraggable(el, { overlay: false })에는 박스가 전혀 없습니다. 반환된 transform으로 요소 자체가 움직입니다.

const { transform } = makeDraggable(el, { overlay: false });
const style = computed(() =>
  transform.value ? { translate: `${transform.value.x}px ${transform.value.y}px` } : undefined,
);

드래그 앤 드롭을 참고하세요. 박스가 있는 드래그가 안착하는 방식은 그 dropAnimation이 정합니다. 드롭 대상의 것(overDropAnimation), 항목의 것(또는 그 리스트의 것), 또는 프로바이더의 것입니다. 애니메이션을 참고하세요.