Skip to content

<script setup>

<script setup>은 싱글 파일 컴포넌트(SFC) 내에서 Composition API를 사용할 때의 컴파일 타임 문법 설탕입니다. SFC와 Composition API를 모두 사용하는 경우 권장되는 문법입니다. 일반 <script> 문법에 비해 여러 가지 장점이 있습니다:

  • 보일러플레이트가 적고 더 간결한 코드
  • 순수 TypeScript로 props와 emit 이벤트 선언 가능
  • 더 나은 런타임 성능(템플릿이 중간 프록시 없이 동일한 스코프의 렌더 함수로 컴파일됨)
  • 더 나은 IDE 타입 추론 성능(코드에서 타입을 추출하는 언어 서버의 작업량 감소)

기본 문법

이 문법을 사용하려면 <script> 블록에 setup 속성을 추가하세요:

vue
<script setup> console.log('hello script setup') </script>

내부 코드는 컴포넌트의 setup() 함수의 내용으로 컴파일됩니다. 즉, 일반 <script>와 달리, <script setup> 내부의 코드는 컴포넌트 인스턴스가 생성될 때마다 실행됩니다(일반 <script>는 컴포넌트가 처음 import될 때 한 번만 실행됨).

최상위 바인딩은 템플릿에 노출됨

<script setup>을 사용할 때, <script setup> 내부에 선언된 모든 최상위 바인딩(변수, 함수 선언, import 등)은 템플릿에서 직접 사용할 수 있습니다:

vue
<script setup> // 변수 const msg = 'Hello!'  // 함수 function log() {  console.log(msg) } </script>  <template>  <button @click="log">{{ msg }}</button> </template>

import도 동일하게 노출됩니다. 즉, import한 헬퍼 함수를 methods 옵션을 통해 노출하지 않고도 템플릿 표현식에서 직접 사용할 수 있습니다:

vue
<script setup> import { capitalize } from './helpers' </script>  <template>  <div>{{ capitalize('hello') }}</div> </template>

반응성

반응형 상태는 반응성 API를 사용해 명시적으로 생성해야 합니다. setup() 함수에서 반환된 값과 마찬가지로, ref는 템플릿에서 참조할 때 자동으로 언래핑됩니다:

vue
<script setup> import { ref } from 'vue'  const count = ref(0) </script>  <template>  <button @click="count++">{{ count }}</button> </template>

컴포넌트 사용하기

<script setup>의 스코프 내 값은 커스텀 컴포넌트 태그 이름으로도 직접 사용할 수 있습니다:

vue
<script setup> import MyComponent from './MyComponent.vue' </script>  <template>  <MyComponent /> </template>

MyComponent를 변수로 참조한다고 생각하면 됩니다. JSX를 사용해본 적이 있다면 비슷한 개념입니다. 케밥 케이스의 <my-component>도 템플릿에서 동작하지만, 일관성을 위해 PascalCase 컴포넌트 태그 사용을 강력히 권장합니다. 또한 네이티브 커스텀 엘리먼트와 구분하는 데 도움이 됩니다.

동적 컴포넌트

컴포넌트가 문자열 키로 등록되는 것이 아니라 변수로 참조되기 때문에, <script setup> 내부에서 동적 컴포넌트를 사용할 때는 동적 :is 바인딩을 사용해야 합니다:

vue
<script setup> import Foo from './Foo.vue' import Bar from './Bar.vue' </script>  <template>  <component :is="Foo" />  <component :is="someCondition ? Foo : Bar" /> </template>

삼항 연산자에서 컴포넌트를 변수처럼 사용할 수 있다는 점에 주목하세요.

재귀 컴포넌트

SFC는 파일 이름을 통해 암묵적으로 자신을 참조할 수 있습니다. 예를 들어, FooBar.vue라는 파일은 템플릿에서 <FooBar/>로 자신을 참조할 수 있습니다.

이 기능은 import된 컴포넌트보다 우선순위가 낮습니다. 컴포넌트의 추론된 이름과 충돌하는 이름의 import가 있다면, import에 별칭을 지정할 수 있습니다:

js
import { FooBar as FooBarChild } from './components'

네임스페이스 컴포넌트

<Foo.Bar>처럼 점이 포함된 컴포넌트 태그를 사용해 객체 속성에 중첩된 컴포넌트를 참조할 수 있습니다. 이는 하나의 파일에서 여러 컴포넌트를 import할 때 유용합니다:

vue
<script setup> import * as Form from './form-components' </script>  <template>  <Form.Input>  <Form.Label>label</Form.Label>  </Form.Input> </template>

커스텀 디렉티브 사용하기

전역 등록된 커스텀 디렉티브는 평소처럼 동작합니다. 로컬 커스텀 디렉티브는 <script setup>에서 명시적으로 등록할 필요가 없지만, vNameOfDirective라는 네이밍 규칙을 따라야 합니다:

vue
<script setup> const vMyDirective = {  beforeMount: (el) => {  // 엘리먼트로 무언가를 수행  } } </script> <template>  <h1 v-my-directive>이것은 제목입니다</h1> </template>

다른 곳에서 디렉티브를 import하는 경우, 필요한 네이밍 규칙에 맞게 이름을 변경할 수 있습니다:

vue
<script setup> import { myDirective as vMyDirective } from './MyDirective.js' </script>

defineProps() & defineEmits()

propsemits 같은 옵션을 완전한 타입 추론 지원과 함께 선언하려면, <script setup> 내부에서 자동으로 사용할 수 있는 definePropsdefineEmits API를 사용할 수 있습니다:

vue
<script setup> const props = defineProps({  foo: String })  const emit = defineEmits(['change', 'delete']) // setup 코드 </script>
  • definePropsdefineEmits컴파일러 매크로로, <script setup> 내부에서만 사용할 수 있습니다. import할 필요가 없으며, <script setup>이 처리될 때 컴파일 과정에서 제거됩니다.

  • definePropsprops 옵션과 동일한 값을, defineEmitsemits 옵션과 동일한 값을 받습니다.

  • definePropsdefineEmits는 전달된 옵션을 기반으로 올바른 타입 추론을 제공합니다.

  • definePropsdefineEmits에 전달된 옵션은 setup 바깥의 모듈 스코프로 호이스팅됩니다. 따라서 옵션은 setup 스코프에서 선언된 로컬 변수를 참조할 수 없습니다. 그렇게 하면 컴파일 에러가 발생합니다. 하지만 import된 바인딩은 모듈 스코프에 있으므로 참조할 수 있습니다.

타입 전용 props/emit 선언

props와 emits는 defineProps 또는 defineEmits에 리터럴 타입 인자를 전달하여 순수 타입 문법으로도 선언할 수 있습니다:

ts
const props = defineProps<{  foo: string  bar?: number }>()  const emit = defineEmits<{  (e: 'change', id: number): void  (e: 'update', value: string): void }>()  // 3.3+: 더 간결한 대안 문법 const emit = defineEmits<{  change: [id: number] // 명명된 튜플 문법  update: [value: string] }>()
  • defineProps 또는 defineEmits는 런타임 선언 또는 타입 선언 중 하나만 사용할 수 있습니다. 둘을 동시에 사용하면 컴파일 에러가 발생합니다.

  • 타입 선언을 사용할 때, 동적 분석을 통해 동등한 런타임 선언이 자동으로 생성되어 중복 선언 없이도 올바른 런타임 동작을 보장합니다.

    • 개발 모드에서는 컴파일러가 타입에서 해당 런타임 유효성 검사를 추론하려고 시도합니다. 예를 들어 여기서 foo: string 타입은 foo: String으로 추론됩니다. 타입이 import된 타입을 참조하는 경우, 컴파일러가 외부 파일 정보를 알 수 없으므로 추론 결과는 foo: null(즉, any 타입과 동일)입니다.

    • 프로덕션 모드에서는 번들 크기를 줄이기 위해 배열 형식 선언이 생성됩니다(여기서 props는 ['foo', 'bar']로 컴파일됨).

  • 3.2 이하 버전에서는 defineProps()의 제네릭 타입 파라미터가 타입 리터럴 또는 로컬 인터페이스 참조로 제한되었습니다.

    이 제한은 3.3에서 해결되었습니다. 최신 Vue 버전은 타입 파라미터 위치에서 import된 타입과 제한된 복합 타입 참조를 지원합니다. 하지만 타입에서 런타임으로의 변환이 여전히 AST 기반이기 때문에, 조건부 타입 등 실제 타입 분석이 필요한 일부 복합 타입은 지원되지 않습니다. 단일 prop의 타입으로 조건부 타입을 사용할 수는 있지만, 전체 props 객체에는 사용할 수 없습니다.

반응형 props 구조 분해

Vue 3.5 이상에서는 defineProps의 반환값에서 구조 분해된 변수들이 반응형이 됩니다. Vue의 컴파일러는 동일한 <script setup> 블록 내에서 defineProps로 구조 분해된 변수를 접근할 때 자동으로 props.를 앞에 붙입니다:

ts
const { foo } = defineProps(['foo'])  watchEffect(() => {  // 3.5 이전에는 한 번만 실행됨  // 3.5+에서는 "foo" prop이 변경될 때마다 재실행됨  console.log(foo) })

위 코드는 다음과 같이 동등하게 컴파일됩니다:

js
const props = defineProps(['foo'])  watchEffect(() => {  // 컴파일러가 `foo`를 `props.foo`로 변환  console.log(props.foo) })

또한, JavaScript의 기본값 문법을 사용해 props의 기본값을 선언할 수 있습니다. 타입 기반 props 선언을 사용할 때 특히 유용합니다:

ts
interface Props {  msg?: string  labels?: string[] }  const { msg = 'hello', labels = ['one', 'two'] } = defineProps<Props>()

타입 선언 사용 시 props 기본값

3.5 이상에서는 반응형 props 구조 분해를 사용할 때 기본값을 자연스럽게 선언할 수 있습니다. 하지만 3.4 이하에서는 반응형 props 구조 분해가 기본적으로 활성화되어 있지 않습니다. 타입 기반 선언으로 props 기본값을 선언하려면 withDefaults 컴파일러 매크로가 필요합니다:

ts
interface Props {  msg?: string  labels?: string[] }  const props = withDefaults(defineProps<Props>(), {  msg: 'hello',  labels: () => ['one', 'two'] })

이 코드는 동등한 런타임 props default 옵션으로 컴파일됩니다. 또한, withDefaults 헬퍼는 기본값에 대한 타입 검사를 제공하고, 기본값이 선언된 속성에 대해 반환된 props 타입에서 선택적 플래그를 제거합니다.

INFO

withDefaults를 사용할 때 배열이나 객체와 같은 변경 가능한 참조 타입의 기본값은 함수로 감싸야 합니다. 이는 실수로 인한 수정 및 외부 부작용을 방지하기 위함입니다. 이렇게 하면 각 컴포넌트 인스턴스가 기본값의 고유한 복사본을 갖게 됩니다. 구조 분해를 사용할 때는 이 작업이 필요하지 않습니다.

defineModel()

  • 3.4+에서만 사용 가능

이 매크로는 부모 컴포넌트에서 v-model로 사용할 수 있는 양방향 바인딩 prop을 선언할 때 사용할 수 있습니다. 예시 사용법은 컴포넌트 v-model 가이드에서도 다룹니다.

내부적으로 이 매크로는 모델 prop과 해당 값 업데이트 이벤트를 선언합니다. 첫 번째 인자가 리터럴 문자열이면 prop 이름으로 사용되고, 그렇지 않으면 prop 이름은 기본값 "modelValue"가 됩니다. 두 경우 모두 prop 옵션과 모델 ref의 값 변환 옵션을 포함하는 추가 객체를 전달할 수 있습니다.

js
// "modelValue" prop을 선언, 부모에서 v-model로 사용 const model = defineModel() // 또는: 옵션이 있는 "modelValue" prop 선언 const model = defineModel({ type: String })  // 변경 시 "update:modelValue"를 emit model.value = 'hello'  // "count" prop을 선언, 부모에서 v-model:count로 사용 const count = defineModel('count') // 또는: 옵션이 있는 "count" prop 선언 const count = defineModel('count', { type: Number, default: 0 })  function inc() {  // 변경 시 "update:count"를 emit  count.value++ }

WARNING

defineModel prop에 default 값을 지정하고, 부모 컴포넌트에서 이 prop에 값을 제공하지 않으면 부모와 자식 컴포넌트 간 동기화가 깨질 수 있습니다. 아래 예시에서 부모의 myRef는 undefined이지만, 자식의 model은 1입니다:

Child.vue
vue
<script setup> const model = defineModel({ default: 1 }) </script>
Parent.vue
vue
<script setup> const myRef = ref() </script>  <template>  <Child v-model="myRef"></Child> </template>

수정자와 변환기

v-model 디렉티브와 함께 사용된 수정자에 접근하려면, defineModel()의 반환값을 구조 분해할 수 있습니다:

js
const [modelValue, modelModifiers] = defineModel()  // v-model.trim에 해당 if (modelModifiers.trim) {  // ... }

수정자가 있을 때는 값을 읽거나 부모에 동기화할 때 값을 변환해야 할 수 있습니다. getset 변환기 옵션을 사용해 이를 구현할 수 있습니다:

js
const [modelValue, modelModifiers] = defineModel({  // get()은 여기서 필요 없으므로 생략  set(value) {  // .trim 수정자가 사용된 경우, trim된 값을 반환  if (modelModifiers.trim) {  return value.trim()  }  // 그렇지 않으면 값을 그대로 반환  return value  } })

TypeScript와 함께 사용하기

definePropsdefineEmits처럼, defineModel도 모델 값과 수정자의 타입을 지정하는 타입 인자를 받을 수 있습니다:

ts
const modelValue = defineModel<string>() // ^? Ref<string | undefined>  // 옵션이 있는 기본 모델, required는 undefined 가능성을 제거 const modelValue = defineModel<string>({ required: true }) // ^? Ref<string>  const [modelValue, modifiers] = defineModel<string, 'trim' | 'uppercase'>() // ^? Record<'trim' | 'uppercase', true | undefined>

defineExpose()

<script setup>을 사용하는 컴포넌트는 기본적으로 닫혀 있습니다. 즉, 템플릿 ref나 $parent 체인을 통해 가져온 컴포넌트의 public 인스턴스는 <script setup> 내부에 선언된 바인딩을 노출하지 않습니다.

<script setup> 컴포넌트에서 속성을 명시적으로 노출하려면 defineExpose 컴파일러 매크로를 사용하세요:

vue
<script setup> import { ref } from 'vue'  const a = 1 const b = ref(2)  defineExpose({  a,  b }) </script>

부모가 템플릿 ref를 통해 이 컴포넌트의 인스턴스를 가져오면, 반환된 인스턴스는 { a: number, b: number } 형태가 됩니다(ref는 일반 인스턴스처럼 자동으로 언래핑됨).

defineOptions()

  • 3.3+에서만 지원

이 매크로를 사용하면 별도의 <script> 블록 없이 <script setup> 내부에서 컴포넌트 옵션을 직접 선언할 수 있습니다:

vue
<script setup> defineOptions({  inheritAttrs: false,  customOptions: {  /* ... */  } }) </script>
  • 이 매크로는 옵션을 모듈 스코프로 호이스팅하며, 리터럴 상수가 아닌 <script setup> 내 로컬 변수에는 접근할 수 없습니다.

defineSlots()

  • 3.3+에서만 지원

이 매크로는 슬롯 이름과 props 타입 체크를 위한 IDE 타입 힌트를 제공하는 데 사용할 수 있습니다.

defineSlots()는 타입 파라미터만 받고 런타임 인자는 받지 않습니다. 타입 파라미터는 속성 키가 슬롯 이름이고, 값 타입이 슬롯 함수인 타입 리터럴이어야 합니다. 함수의 첫 번째 인자는 슬롯이 받을 props이며, 이 타입이 템플릿에서 슬롯 props로 사용됩니다. 반환 타입은 현재 무시되며 any가 될 수 있지만, 향후 슬롯 내용 체크에 활용될 수 있습니다.

또한, setup 컨텍스트에 노출되거나 useSlots()로 반환되는 slots 객체와 동일한 slots 객체를 반환합니다.

vue
<script setup lang="ts"> const slots = defineSlots<{  default(props: { msg: string }): any }>() </script>

useSlots() & useAttrs()

<script setup> 내부에서 slotsattrs를 사용할 일은 드물지만, 템플릿에서는 $slots$attrs로 직접 접근할 수 있습니다. 드물게 필요할 경우 각각 useSlotsuseAttrs 헬퍼를 사용하세요:

vue
<script setup> import { useSlots, useAttrs } from 'vue'  const slots = useSlots() const attrs = useAttrs() </script>

useSlotsuseAttrs는 실제 런타임 함수로, 각각 setupContext.slotssetupContext.attrs와 동일한 값을 반환합니다. 일반 Composition API 함수에서도 사용할 수 있습니다.

일반 <script>와 함께 사용하기

<script setup>은 일반 <script>와 함께 사용할 수 있습니다. 일반 <script>가 필요한 경우는 다음과 같습니다:

  • <script setup>에서 표현할 수 없는 옵션 선언(예: inheritAttrs 또는 플러그인으로 활성화된 커스텀 옵션, 3.3+에서는 defineOptions로 대체 가능)
  • 명명된 export 선언
  • 한 번만 실행되어야 하는 부수 효과 실행 또는 객체 생성
vue
<script> // 일반 <script>, 모듈 스코프에서 한 번만 실행됨 runSideEffectOnce()  // 추가 옵션 선언 export default {  inheritAttrs: false,  customOptions: {} } </script>  <script setup> // setup() 스코프에서 실행(각 인스턴스마다) </script>

동일 컴포넌트에서 <script setup><script>를 조합하는 것은 위에서 설명한 시나리오에 한정됩니다. 구체적으로:

  • 이미 <script setup>에서 정의할 수 있는 옵션(예: props, emits)을 별도의 <script> 섹션에서 선언하지 마세요.
  • <script setup> 내부에서 생성된 변수는 컴포넌트 인스턴스의 속성으로 추가되지 않으므로 Options API에서 접근할 수 없습니다. 이런 방식의 API 혼용은 강력히 권장하지 않습니다.

지원되지 않는 시나리오에 해당한다면, <script setup> 대신 명시적인 setup() 함수를 사용하는 것을 고려하세요.

최상위 await

최상위 await<script setup> 내부에서 사용할 수 있습니다. 결과 코드는 async setup()으로 컴파일됩니다:

vue
<script setup> const post = await fetch(`/api/post/1`).then((r) => r.json()) </script>

또한, await된 표현식은 await 이후에도 현재 컴포넌트 인스턴스 컨텍스트가 유지되는 형식으로 자동 컴파일됩니다.

참고

async setup()Suspense와 함께 사용해야 하며, 현재는 실험적 기능입니다. 향후 릴리스에서 공식화 및 문서화할 예정이지만, 지금 궁금하다면 테스트를 참고해 동작 방식을 확인할 수 있습니다.

import 구문

Vue의 import 구문은 ECMAScript 모듈 명세를 따릅니다. 또한, 빌드 도구 설정에 정의된 별칭을 사용할 수 있습니다:

vue
<script setup> import { ref } from 'vue' import { componentA } from './Components' import { componentB } from '@/Components' import { componentC } from '~/Components' </script>

제네릭

제네릭 타입 파라미터는 <script> 태그의 generic 속성을 사용해 선언할 수 있습니다:

vue
<script setup lang="ts" generic="T"> defineProps<{  items: T[]  selected: T }>() </script>

generic의 값은 TypeScript에서 <...> 사이의 파라미터 리스트와 동일하게 동작합니다. 예를 들어, 여러 파라미터, extends 제약, 기본 타입, import된 타입 참조 등을 사용할 수 있습니다:

vue
<script  setup  lang="ts"  generic="T extends string | number, U extends Item" > import type { Item } from './types' defineProps<{  id: T  list: U[] }>() </script>

타입을 추론할 수 없는 경우, @vue-generic 디렉티브를 사용해 명시적으로 타입을 전달할 수 있습니다:

vue
<template>  <!-- @vue-generic {import('@/api').Actor} -->  <ApiSelect v-model="peopleIds" endpoint="/api/actors" id-prop="actorId" />   <!-- @vue-generic {import('@/api').Genre} -->  <ApiSelect v-model="genreIds" endpoint="/api/genres" id-prop="genreId" /> </template>

제네릭 컴포넌트 참조를 ref에서 사용하려면 vue-component-type-helpers 라이브러리를 사용해야 하며, InstanceType은 동작하지 않습니다.

vue
<script  setup  lang="ts" > import componentWithoutGenerics from '../component-without-generics.vue'; import genericComponent from '../generic-component.vue';  import type { ComponentExposed } from 'vue-component-type-helpers';  // 제네릭이 없는 컴포넌트에는 동작함 ref<InstanceType<typeof componentWithoutGenerics>>();  ref<ComponentExposed<typeof genericComponent>>();

제약 사항

  • 모듈 실행 방식의 차이로 인해, <script setup> 내부 코드는 SFC의 컨텍스트에 의존합니다. 외부 .js 또는 .ts 파일로 이동하면 개발자와 도구 모두 혼란을 초래할 수 있습니다. 따라서 **<script setup>**은 src 속성과 함께 사용할 수 없습니다.
  • <script setup>은 In-DOM 루트 컴포넌트 템플릿을 지원하지 않습니다.(관련 논의)
<script setup> has loaded