Vue.js 를 배워보자

Vue 3와 TypeScript로 구현한 실무적인 발주서 작성 화면

_Blue_Sky_ 2025. 5. 31. 19:29
728x90
 
Vue 3의 <script setup> 문법과 TypeScript를 활용하여 실무에서 자주 사용되는 발주서 작성 화면을 구현하는 방법을 소개합니다. 이 예제는 발주자 정보, 품목 목록, 발주 날짜를 관리하며, 실시간 유효성 검사와 동적 폼 관리 기능을 포함합니다. 실무 환경에서 요구되는 타입 안정성, 사용자 경험, 확장성을 고려한 설계로, 실제 프로젝트에 바로 적용할 수 있는 코드를 제공합니다.
1. 프로젝트 개요
발주서 작성 화면은 비즈니스 애플리케이션에서 자주 등장하는 기능입니다. 사용자가 발주자 이름, 발주 날짜, 품목 목록(품목명, 수량, 단가)을 입력하고, 입력값의 유효성을 검사한 뒤 제출할 수 있는 폼을 구현했습니다. 주요 기능은 다음과 같습니다:
  • 타입 안정성: TypeScript로 데이터 구조를 명확히 정의.
  • 반응형 데이터: Vue 3의 refcomputed를 사용해 실시간 반영.
  • 실시간 유효성 검사: watch를 활용해 입력값 변경 시 즉시 피드백 제공.
  • 동적 품목 관리: 품목 추가/삭제 기능.
  • 제출 로직: 폼 유효성 확인 후 제출 가능.
  • UI/UX: 직관적인 테이블과 에러 메시지로 사용자 경험 개선.
2. 코드 구현
728x90
2.1. 환경 설정
Vue 3 프로젝트에 TypeScript와 date-fns 라이브러리를 설치합니다:
npm install date-fns
date-fns는 날짜 포맷팅과 비교에 사용됩니다. 예제는 <script setup> 모드와 TypeScript를 사용하며, Vite 기반 프로젝트를 가정합니다.
2.2. 전체 코드
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { format } from 'date-fns'

// 데이터 타입 정의
interface OrderItem {
  id: number
  productName: string
  quantity: number
  unitPrice: number
}

interface PurchaseOrder {
  orderer: string
  orderDate: string
  items: OrderItem[]
}

// 반응형 데이터
const purchaseOrder = ref<PurchaseOrder>({
  orderer: '',
  orderDate: format(new Date(), 'yyyy-MM-dd'),
  items: []
})

// 유효성 검사 에러 메시지
const errors = ref({
  orderer: '',
  orderDate: '',
  items: [] as string[]
})

// 새 품목 입력
const newItem = ref<Partial<OrderItem>>({
  productName: '',
  quantity: 0,
  unitPrice: 0
})

// 유효성 검사 규칙
const validateOrderer = (value: string): string => {
  if (!value.trim()) return '발주자 이름은 필수입니다.'
  if (value.length < 2) return '발주자 이름은 2자 이상이어야 합니다.'
  return ''
}

const validateOrderDate = (value: string): string => {
  const today = format(new Date(), 'yyyy-MM-dd')
  if (!value) return '발주 날짜는 필수입니다.'
  if (value > today) return '발주 날짜는 미래일 수 없습니다.'
  return ''
}

const validateItem = (item: Partial<OrderItem>, index: number): string => {
  if (!item.productName?.trim()) return `품목 ${index + 1}: 품목명은 필수입니다.`
  if (!item.quantity || item.quantity <= 0) return `품목 ${index + 1}: 수량은 1 이상이어야 합니다.`
  if (!item.unitPrice || item.unitPrice <= 0) return `품목 ${index + 1}: 단가는 1 이상이어야 합니다.`
  return ''
}

// 폼 유효성 검사
const isFormValid = computed(() => {
  const ordererError = validateOrderer(purchaseOrder.value.orderer)
  const dateError = validateOrderDate(purchaseOrder.value.orderDate)
  const itemErrors = purchaseOrder.value.items.map((item, index) => validateItem(item, index))
  return !ordererError && !dateError && itemErrors.every(error => !error)
})

// 실시간 유효성 검사
watch(
  () => purchaseOrder.value.orderer,
  (newValue) => {
    errors.value.orderer = validateOrderer(newValue)
  }
)

watch(
  () => purchaseOrder.value.orderDate,
  (newValue) => {
    errors.value.orderDate = validateOrderDate(newValue)
  }
)

watch(
  () => purchaseOrder.value.items,
  (newItems) => {
    errors.value.items = newItems.map((item, index) => validateItem(item, index))
  },
  { deep: true }
)

// 품목 추가
const addItem = () => {
  const itemError = validateItem(newItem.value, purchaseOrder.value.items.length)
  if (itemError) {
    alert(itemError)
    return
  }
  purchaseOrder.value.items.push({
    id: purchaseOrder.value.items.length + 1,
    productName: newItem.value.productName!,
    quantity: newItem.value.quantity!,
    unitPrice: newItem.value.unitPrice!
  })
  newItem.value = { productName: '', quantity: 0, unitPrice: 0 }
}

// 품목 삭제
const removeItem = (id: number) => {
  purchaseOrder.value.items = purchaseOrder.value.items.filter(item => item.id !== id)
}

// 발주서 제출
const submitOrder = () => {
  if (!isFormValid.value) {
    alert('모든 필드를 올바르게 입력해주세요.')
    return
  }
  console.log('발주서 제출:', purchaseOrder.value)
  alert('발주서가 성공적으로 제출되었습니다!')
}

// 총액 계산
const totalAmount = computed(() => {
  return purchaseOrder.value.items.reduce(
    (sum, item) => sum + item.quantity * item.unitPrice,
    0
  )
})
</script>

<template>
  <div class="purchase-order-form">
    <h2>발주서 작성</h2>
    <div class="form-group">
      <label>발주자</label>
      <input v-model="purchaseOrder.orderer" placeholder="발주자 이름" />
      <span class="error" v-if="errors.orderer">{{ errors.orderer }}</span>
    </div>
    <div class="form-group">
      <label>발주 날짜</label>
      <input type="date" v-model="purchaseOrder.orderDate" />
      <span class="error" v-if="errors.orderDate">{{ errors.orderDate }}</span>
    </div>
    <div class="form-group">
      <h3>품목 추가</h3>
      <div class="item-input">
        <input v-model="newItem.productName" placeholder="품목명" />
        <input type="number" v-model.number="newItem.quantity" placeholder="수량" />
        <input type="number" v-model.number="newItem.unitPrice" placeholder="단가" />
        <button @click="addItem">품목 추가</button>
      </div>
    </div>
    <div class="items-list" v-if="purchaseOrder.items.length">
      <h3>품목 목록</h3>
      <table>
        <thead>
          <tr>
            <th>품목명</th>
            <th>수량</th>
            <th>단가</th>
            <th>합계</th>
            <th>삭제</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="(item, index) in purchaseOrder.items" :key="item.id">
            <td>{{ item.productName }}</td>
            <td>{{ item.quantity }}</td>
            <td>{{ item.unitPrice }}</td>
            <td>{{ item.quantity * item.unitPrice }}</td>
            <td><button @click="removeItem(item.id)">삭제</button></td>
          </tr>
        </tbody>
      </table>
      <p v-for="(error, index) in errors.items" :key="index" class="error" v-if="error">
        {{ error }}
      </p>
    </div>
    <div class="total">
      <strong>총액: {{ totalAmount }} 원</strong>
    </div>
    <button :disabled="!isFormValid" @click="submitOrder">발주서 제출</button>
  </div>
</template>

<style scoped>
.purchase-order-form {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}
.form-group {
  margin-bottom: 20px;
}
.form-group label {
  display: block;
  margin-bottom: 5px;
}
input {
  padding: 8px;
  width: 100%;
  margin-bottom: 5px;
}
.item-input {
  display: flex;
  gap: 10px;
}
.item-input input {
  flex: 1;
}
.error {
  color: red;
  font-size: 12px;
}
table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 20px;
}
th,
td {
  border: 1px solid #ddd;
  padding: 8px;
  text-align: left;
}
button {
  padding: 8px 16px;
  background-color: #007bff;
  color: white;
  border: none;
  cursor: pointer;
}
button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}
.total {
  margin-top: 20px;
  font-size: 18px;
}
</style>
 
 
3. 코드 분석
3.1. TypeScript와 데이터 구조
TypeScript의 인터페이스(PurchaseOrder, OrderItem)를 사용해 데이터 구조를 명확히 정의했습니다. 이는 코드 가독성과 유지보수성을 높이며, IDE의 자동완성 및 타입 체크 기능을 활용할 수 있습니다.
  • PurchaseOrder: 발주자, 발주 날짜, 품목 목록을 포함.
  • OrderItem: 품목명, 수량, 단가를 포함하며, 고유 ID로 관리.
3.2. 반응형 데이터 관리
Vue 3의 ref를 사용해 purchaseOrdernewItem을 반응형으로 관리합니다. computed로 계산된 totalAmount는 품목 목록의 총액을 실시간으로 반영하며, isFormValid는 폼의 전체 유효성을 확인합니다.
3.3. 실시간 유효성 검사
watch를 활용해 입력값의 변화를 감지하고, 각 필드에 대해 실시간 유효성 검사를 수행합니다:
  • 발주자: 비어 있지 않고, 2자 이상.
  • 발주 날짜: 비어 있지 않고, 미래 날짜 불가.
  • 품목: 품목명 필수, 수량과 단가는 1 이상.
errors 객체는 각 필드의 에러 메시지를 저장하며, UI에 즉시 반영됩니다.
3.4. 동적 품목 관리
  • 추가: addItem 함수는 새 품목의 유효성을 확인한 후 목록에 추가하고 입력 필드를 초기화합니다.
  • 삭제: removeItem 함수는 선택한 품목을 ID로 필터링해 제거합니다.
3.5. UI/UX
  • 입력 오류는 각 필드 아래에 빨간색으로 표시되어 사용자에게 즉각적인 피드백을 제공합니다.
  • 품목 목록은 테이블로 시각화되며, 각 행에 삭제 버튼을 포함합니다.
  • 제출 버튼은 isFormValidtrue일 때만 활성화되어 잘못된 입력을 방지합니다.
4. 실무적 활용
4.1. 확장 가능성
  • API 연동: submitOrder 함수에 axios.post를 추가해 서버로 데이터를 전송할 수 있습니다.
    ts
     
    import axios from 'axios'
    const submitOrder = async () => {
      if (!isFormValid.value) {
        alert('모든 필드를 올바르게 입력해주세요.')
        return
      }
      try {
        await axios.post('/api/purchase-order', purchaseOrder.value)
        alert('발주서가 성공적으로 제출되었습니다!')
      } catch (error) {
        alert('제출 중 오류가 발생했습니다.')
      }
    }
  • 품목 검색: 품목명을 입력할 때 서버에서 품목 목록을 조회해 자동완성 기능을 추가.
  • 상태 관리: 복잡한 경우 Pinia로 상태를 중앙화.
  • 다국어 지원: vue-i18n으로 에러 메시지와 라벨을 다국어로 처리.
4.2. 실무적 고려사항
  • 성능 최적화: 품목 목록이 많아질 경우, v-for:key를 사용해 렌더링 효율성을 유지합니다.
  • 사용자 경험: 입력 필드에 포커스 이동, 엔터키로 품목 추가 등 UX 개선 가능.
  • 테스트: 유효성 검사와 제출 로직을 위한 단위 테스트(Jest, Vitest) 추가.
5. 결론
이 예제는 Vue 3의 <script setup>과 TypeScript를 활용해 실무적인 발주서 작성 화면을 구현한 사례입니다. 타입 안정성, 반응형 데이터, 실시간 유효성 검사, 동적 폼 관리 등 현대 웹 개발의 핵심 요소를 포함하며, 실제 프로젝트에 적용 가능한 구조를 제공합니다. 추가 기능(예: API 연동, 다국어 지원)이나 UI 라이브러리(Tailwind CSS, Vuetify)를 도입하면 더욱 강력한 애플리케이션으로 확장할 수 있습니다.

 
 
 
 

실시간 유효성 검사 코드를 간소화하기 위해, 개별 watch 호출을 하나로 통합하고, watch 대상과 유효성 검사 로직을 간결하게 정리할 수 있습니다. 아래는 간소화된 코드입니다. 여기서는 purchaseOrder 객체 전체를 감시하여 모든 유효성 검사를 한 번에 처리합니다.
ts
 
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { format } from 'date-fns'

// 데이터 타입 정의 (기존과 동일)
interface OrderItem {
  id: number
  productName: string
  quantity: number
  unitPrice: number
}

interface PurchaseOrder {
  orderer: string
  orderDate: string
  items: OrderItem[]
}

// 반응형 데이터 (기존과 동일)
const purchaseOrder = ref<PurchaseOrder>({
  orderer: '',
  orderDate: format(new Date(), 'yyyy-MM-dd'),
  items: []
})

const errors = ref({
  orderer: '',
  orderDate: '',
  items: [] as string[]
})

// 유효성 검사 함수 (기존과 동일)
const validateOrderer = (value: string): string => {
  if (!value.trim()) return '발주자 이름은 필수입니다.'
  if (value.length < 2) return '발주자 이름은 2자 이상이어야 합니다.'
  return ''
}

const validateOrderDate = (value: string): string => {
  const today = format(new Date(), 'yyyy-MM-dd')
  if (!value) return '발주 날짜는 필수입니다.'
  if (value > today) return '발주 날짜는 미래일 수 없습니다.'
  return ''
}

const validateItem = (item: Partial<OrderItem>, index: number): string => {
  if (!item.productName?.trim()) return `품목 ${index + 1}: 품목명은 필수입니다.`
  if (!item.quantity || item.quantity <= 0) return `품목 ${index + 1}: 수량은 1 이상이어야 합니다.`
  if (!item.unitPrice || item.unitPrice <= 0) return `품목 ${index + 1}: 단가는 1 이상이어야 합니다.`
  return ''
}

// 간소화된 실시간 유효성 검사
watch(
  purchaseOrder,
  (newValue) => {
    errors.value.orderer = validateOrderer(newValue.orderer)
    errors.value.orderDate = validateOrderDate(newValue.orderDate)
    errors.value.items = newValue.items.map((item, index) => validateItem(item, index))
  },
  { deep: true }
)

// 나머지 코드는 동일 (addItem, removeItem, submitOrder, totalAmount 등)
</script>
간소화된 부분 설명
  1. 단일 watch 사용:
    • 기존에는 orderer, orderDate, items를 각각 별도의 watch로 감시했습니다. 이를 purchaseOrder 객체 전체를 감시하도록 통합하여 코드 중복을 줄였습니다.
    • watch(purchaseOrder, ...)purchaseOrder의 모든 속성 변경을 감지하며, { deep: true } 옵션으로 items 배열의 내부 변경도 추적합니다.
  2. 유효성 검사 통합:
    • 하나의 watch 콜백 내에서 orderer, orderDate, items에 대한 유효성 검사를 모두 수행합니다.
    • errors.value 객체를 한 번에 업데이트하여 코드가 더 간결해졌습니다.
  3. 장점:
    • 코드 라인이 줄어들어 가독성이 향상되었습니다.
    • 유지보수가 쉬워졌으며, 새로운 필드를 추가할 때 watch를 추가로 작성할 필요 없이 콜백 내부에 유효성 검사만 추가하면 됩니다.
  4. 주의점:
    • deep: true는 깊은 객체 변경을 감지하지만, 성능에 약간의 영향을 줄 수 있습니다. items 배열이 매우 크거나 빈번히 변경되는 경우, 성능 최적화를 위해 특정 속성만 감시하도록 세분화할 수 있습니다.
    • 예를 들어, 성능이 중요한 경우 items 배열의 특정 변경(추가/삭제)만 감지하도록 로직을 조정할 수 있습니다.
추가 개선 가능성
  • Debouncing: 입력이 빈번할 경우, watchdebounce를 추가해 유효성 검사 빈도를 줄일 수 있습니다. 예:
    ts
     
    import { debounce } from 'lodash'
    watch(
      purchaseOrder,
      debounce((newValue) => {
        errors.value.orderer = validateOrderer(newValue.orderer)
        errors.value.orderDate = validateOrderDate(newValue.orderDate)
        errors.value.items = newValue.items.map((item, index) => validateItem(item, index))
      }, 300),
      { deep: true }
    )
    (lodash 설치 필요: npm install lodash)
  • 컴포넌트 분리: 유효성 검사 로직을 컴포저블(composable)로 분리해 재사용성을 높일 수 있습니다.
이 간소화된 코드는 기존 기능과 동일한 실시간 유효성 검사를 유지하면서도 코드량을 줄이고 관리하기 쉽게 만듭니다. 추가로 특정 부분을 더 최적화하거나 다른 방식으로 간소화하고 싶다면 말씀해주세요!
 

 
 
TypeScript에서 Partial<T>는 유틸리티 타입(Utility Type) 중 하나로, 주어진 타입 T의 모든 속성을 선택적(optional)으로 만드는 역할을 합니다. 즉, 원래 타입의 모든 속성을 포함하지만, 각 속성이 필수(required)가 아니라 선택적으로 변경됩니다.
Partial<T> 설명
  • 용도: 객체 타입의 일부 속성만 사용하거나, 속성을 선택적으로 초기화할 때 유용합니다.
  • 동작: T 타입의 모든 속성을 ?: (선택적 속성)으로 변환합니다.
  • 예시:
    interface User {
      name: string
      age: number
      email: string
    }
    
    // Partial<User>는 모든 속성을 선택적으로 만듦
    type PartialUser = Partial<User>
    
    // PartialUser는 다음과 같은 타입이 됨
    // {
    //   name?: string
    //   age?: number
    //   email?: string
    // }
질문의 맥락에서 Partial<OrderItem>
질문에서 언급된 코드에서 newItem은 다음과 같이 정의되었습니다:
 
const newItem = ref<Partial<OrderItem>>({
  productName: '',
  quantity: 0,
  unitPrice: 0
})
여기서 OrderItem 인터페이스는 다음과 같습니다:
 
interface OrderItem {
  id: number
  productName: string
  quantity: number
  unitPrice: number
}
Partial<OrderItem>을 사용한 이유는 newItem 객체가 OrderItem 타입의 모든 속성을 갖지만, 초기화 시점에서 일부 속성(특히 id)이 아직 설정되지 않을 수 있기 때문입니다. Partial<OrderItem>은 모든 속성을 선택적으로 만들어 다음과 같은 타입을 생성합니다:
ts
 
{
  id?: number
  productName?: string
  quantity?: number
  unitPrice?: number
}
이로 인해 newItemid 없이도 초기화할 수 있으며, 유효성 검사를 통해 필수 속성(productName, quantity, unitPrice)이 채워진 후 OrderItem 타입으로 변환되어 items 배열에 추가됩니다.
실무적 사용 사례
  1. 폼 입력 초기화:
    • 폼에서 사용자가 데이터를 입력하기 전, 일부 속성만 초기화하거나 필수 속성을 제외한 나머지를 선택적으로 처리할 때 사용.
    • 예: 위 코드에서 newItem은 품목 추가를 위한 임시 객체로, id는 추가 시점에 동적으로 생성되므로 초기화하지 않음.
  2. 부분 업데이트:
    • API 호출로 객체의 일부 속성만 업데이트할 때, Partial<T>를 사용해 필요한 속성만 전송.
    • 예: PATCH 요청에서 Partial<User>로 일부 사용자 정보만 업데이트.
  3. 유연한 타입 정의:
    • 복잡한 객체를 단계적으로 구성하거나, 일부 속성이 누락될 수 있는 상황에서 타입 안정성을 유지.
코드에서의 역할
newItemPartial<OrderItem>을 사용함으로써:
  • id 속성을 초기화하지 않아도 TypeScript 오류가 발생하지 않음.
  • 사용자가 입력한 productName, quantity, unitPrice를 유연하게 관리.
  • addItem 함수에서 유효성 검사를 통해 필수 속성을 확인한 후, 완전한 OrderItem 객체로 변환:
 
const addItem = () => {
  const itemError = validateItem(newItem.value, purchaseOrder.value.items.length)
  if (itemError) {
    alert(itemError)
    return
  }
  purchaseOrder.value.items.push({
    id: purchaseOrder.value.items.length + 1,
    productName: newItem.value.productName!, // Non-null assertion
    quantity: newItem.value.quantity!,
    unitPrice: newItem.value.unitPrice!
  })
}
여기서 ! (Non-null assertion)은 유효성 검사를 통과했으므로 productName, quantity, unitPrice가 반드시 존재한다고 TypeScript에 알려줍니다.
요약
  • Partial<T>는 타입 T의 모든 속성을 선택적으로 만들어 유연한 객체 조작을 가능하게 함.
  • 위 코드에서는 newItemOrderItem의 일부 속성만 초기화하도록 허용하며, id와 같은 속성을 동적으로 추가할 때 유용.
  • 실무에서는 폼 입력, 부분 업데이트, 객체 초기화 등에서 자주 사용됨.
추가로 궁금한 점이나 Partial과 관련된 다른 유틸리티 타입(Pick, Omit 등)에 대해 알고 싶다면 말씀해주세요!
TypeScript에서 Partial<T>는 유틸리티 타입(Utility Type) 중 하나로, 주어진 타입 T의 모든 속성을 **선택적(optional)**으로 만드는 역할을 합니다. 즉, 원래 타입의 모든 속성을 포함하지만, 각 속성이 필수(required)가 아니라 선택적으로 변경됩니다.
Partial<T> 설명
  • 용도: 객체 타입의 일부 속성만 사용하거나, 속성을 선택적으로 초기화할 때 유용합니다.
  • 동작: T 타입의 모든 속성을 ?: (선택적 속성)으로 변환합니다.
  • 예시:
     
    interface User {
      name: string
      age: number
      email: string
    }
    
    // Partial<User>는 모든 속성을 선택적으로 만듦
    type PartialUser = Partial<User>
    
    // PartialUser는 다음과 같은 타입이 됨
    // {
    //   name?: string
    //   age?: number
    //   email?: string
    // }
질문의 맥락에서 Partial<OrderItem>
질문에서 언급된 코드에서 newItem은 다음과 같이 정의되었습니다:
 
const newItem = ref<Partial<OrderItem>>({
  productName: '',
  quantity: 0,
  unitPrice: 0
})
여기서 OrderItem 인터페이스는 다음과 같습니다:
 
interface OrderItem {
  id: number
  productName: string
  quantity: number
  unitPrice: number
}
Partial<OrderItem>을 사용한 이유는 newItem 객체가 OrderItem 타입의 모든 속성을 갖지만, 초기화 시점에서 일부 속성(특히 id)이 아직 설정되지 않을 수 있기 때문입니다. Partial<OrderItem>은 모든 속성을 선택적으로 만들어 다음과 같은 타입을 생성합니다:
 
{
  id?: number
  productName?: string
  quantity?: number
  unitPrice?: number
}
이로 인해 newItemid 없이도 초기화할 수 있으며, 유효성 검사를 통해 필수 속성(productName, quantity, unitPrice)이 채워진 후 OrderItem 타입으로 변환되어 items 배열에 추가됩니다.
실무적 사용 사례
  1. 폼 입력 초기화:
    • 폼에서 사용자가 데이터를 입력하기 전, 일부 속성만 초기화하거나 필수 속성을 제외한 나머지를 선택적으로 처리할 때 사용.
    • 예: 위 코드에서 newItem은 품목 추가를 위한 임시 객체로, id는 추가 시점에 동적으로 생성되므로 초기화하지 않음.
  2. 부분 업데이트:
    • API 호출로 객체의 일부 속성만 업데이트할 때, Partial<T>를 사용해 필요한 속성만 전송.
    • 예: PATCH 요청에서 Partial<User>로 일부 사용자 정보만 업데이트.
  3. 유연한 타입 정의:
    • 복잡한 객체를 단계적으로 구성하거나, 일부 속성이 누락될 수 있는 상황에서 타입 안정성을 유지.
코드에서의 역할
newItemPartial<OrderItem>을 사용함으로써:
  • id 속성을 초기화하지 않아도 TypeScript 오류가 발생하지 않음.
  • 사용자가 입력한 productName, quantity, unitPrice를 유연하게 관리.
addItem 함수에서 유효성 검사를 통해 필수 속성을 확인한 후, 완전한 OrderItem 객체로 변환:
const addItem = () => {
  const itemError = validateItem(newItem.value, purchaseOrder.value.items.length)
  if (itemError) {
    alert(itemError)
    return
  }
  purchaseOrder.value.items.push({
    id: purchaseOrder.value.items.length + 1,
    productName: newItem.value.productName!, // Non-null assertion
    quantity: newItem.value.quantity!,
    unitPrice: newItem.value.unitPrice!
  })
}
여기서 ! (Non-null assertion)은 유효성 검사를 통과했으므로 productName, quantity, unitPrice가 반드시 존재한다고 TypeScript에 알려줍니다.
요약
  • Partial<T>는 타입 T의 모든 속성을 선택적으로 만들어 유연한 객체 조작을 가능하게 함.
  • 위 코드에서는 newItemOrderItem의 일부 속성만 초기화하도록 허용하며, id와 같은 속성을 동적으로 추가할 때 유용.
  • 실무에서는 폼 입력, 부분 업데이트, 객체 초기화 등에서 자주 사용됨.
추가로 궁금한 점이나 Partial과 관련된 다른 유틸리티 타입(Pick, Omit 등)에 대해 알고 싶다면 말씀해주세요!
728x90