Vue.js 면접 핵심 질문: 합격을 위한 25문항

Vue.js 면접을 위한 25개의 핵심 질문. 반응성부터 composables까지, 다음 면접에서 빛날 핵심 개념을 정리합니다.

코드 블록과 Vue 로고가 등장하는 Vue.js 기술 면접 일러스트

Vue.js 면접에서는 프레임워크 문법보다 훨씬 더 많은 것을 평가합니다. 채용 담당자는 반응성 시스템에 대한 이해도, Composition API를 활용한 코드 구성 능력, 그리고 실제 성능과 아키텍처 문제를 해결할 수 있는 역량을 확인하고 싶어합니다.

준비 팁

모든 문항에는 자세한 답변과 코드 예제가 포함되어 있습니다. 기술 면접에서는 실제 면접처럼 개념을 소리 내어 설명하는 연습이 효과적입니다.

Vue.js 기본 문항

1. Vue 3에서 ref와 reactive의 차이는 무엇입니까

이 질문은 Vue 3의 핵심인 반응성 시스템에 대한 이해를 확인합니다. 주요 차이는 다루는 데이터 타입과 접근 문법에 있습니다.

ref는 원시값(string, number, boolean)에 대한 반응형 참조를 만들고 스크립트에서는 .value로 값에 접근해야 합니다. reactive는 객체에 대한 반응형 프록시를 만들며 속성에 직접 접근할 수 있습니다.

javascript
// ref와 reactive 비교 예시
import { ref, reactive } from 'vue'

// ref: for primitives
// Requires .value in the script
const count = ref(0)
count.value++ // Access with .value

// reactive: for complex objects
// Direct property access
const user = reactive({
  name: 'Alice',
  age: 25
})
user.age++ // No .value needed

// Warning: reactive loses reactivity if reassigned
// user = { name: 'Bob', age: 30 } // ❌ Breaks reactivity
Object.assign(user, { name: 'Bob', age: 30 }) // ✅ Correct

일반 원칙은 단순한 값에는 ref를, 서로 연관된 여러 속성을 가진 객체에는 reactive를 사용하는 것입니다.

2. Vue 3의 반응성 시스템은 어떻게 동작합니까

Vue 3는 반응형 객체에 대한 작업을 가로채기 위해 JavaScript Proxy(ES6)를 사용합니다. Object.defineProperty를 사용한 Vue 2와 달리, 이 방식은 속성의 추가와 제거를 동적으로 감지합니다.

javascript
// Simplified demonstration of the reactivity principle
// Vue uses Proxies to track dependencies

const handler = {
  // Intercept reading
  get(target, key, receiver) {
    track(target, key) // Register the dependency
    return Reflect.get(target, key, receiver)
  },
  // Intercept writing
  set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver)
    trigger(target, key) // Trigger updates
    return result
  }
}

// Creating a reactive proxy
const reactiveObject = new Proxy(originalObject, handler)

Proxy의 장점에는 새로운 속성 감지, Map과 Set 지원, 큰 객체에서의 더 나은 성능이 포함됩니다.

3. computed와 watch의 차이를 설명해 주세요

computedwatch는 반응성 관리에서 서로 다른 역할을 수행합니다.

computed: 다른 반응형 데이터에서 파생된 값을 계산합니다. 값은 캐시되며, 의존성이 변경될 때만 재계산됩니다. 데이터 변환에 적합합니다.

watch: 변경에 응답하여 부수 효과를 실행합니다. API 호출, DOM 조작, 비동기 작업에 유용합니다.

javascript
// Computed vs watch comparison
import { ref, computed, watch } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// computed: derived value with cache
// Recalculates only if firstName or lastName changes
const fullName = computed(() => {
  console.log('Computing full name') // Called only once
  return `${firstName.value} ${lastName.value}`
})

// Multiple accesses = single execution (cached)
console.log(fullName.value) // "John Doe"
console.log(fullName.value) // No recalculation

// watch: side effect without cache
// Executed on every change
watch(firstName, async (newName, oldName) => {
  // Side effect: API call
  await saveToServer({ firstName: newName })
  console.log(`Name changed from ${oldName} to ${newName}`)
})

4. Virtual DOM이 무엇이며 Vue는 이를 어떻게 사용합니까

Virtual DOM은 실제 DOM의 가벼운 JavaScript 표현입니다. Vue는 가상 트리를 메모리에 유지하고, 이전 상태와 새 상태의 차이(diffing)를 계산하여 실제 DOM에는 필요한 변경만 적용합니다.

javascript
// Conceptual representation of the Virtual DOM
// Vue creates this structure internally

const vnode = {
  type: 'div',
  props: {
    class: 'container',
    id: 'app'
  },
  children: [
    {
      type: 'h1',
      props: {},
      children: 'Title'
    },
    {
      type: 'p',
      props: {},
      children: 'Paragraph content'
    }
  ]
}

// When changes occur, Vue compares vnodes
// and updates only the modified elements

Vue 3의 최적화에는 정적 노드의 호이스팅, 변경 유형을 식별하기 위한 패치 플래그, 컴파일러 단계의 트리 셰이킹이 포함됩니다.

5. 직접 관계가 없는 컴포넌트 간 통신은 어떻게 처리합니까

부모-자식 관계가 직접 없는 컴포넌트끼리 통신하기 위한 패턴은 여러 가지가 있습니다.

javascript
// Solution 1: Event Bus (small applications)
// eventBus.js
import { ref } from 'vue'

const bus = ref(new Map())

export function useEventBus() {
  // Emit an event
  const emit = (event, payload) => {
    const callbacks = bus.value.get(event) || []
    callbacks.forEach(cb => cb(payload))
  }

  // Listen to an event
  const on = (event, callback) => {
    if (!bus.value.has(event)) {
      bus.value.set(event, [])
    }
    bus.value.get(event).push(callback)
  }

  return { emit, on }
}
javascript
// Solution 2: Provide/Inject for nested components
// Ancestor
import { provide, ref } from 'vue'

const sharedState = ref('shared value')
provide('stateKey', sharedState)

// Descendant (any level)
import { inject } from 'vue'

const state = inject('stateKey')

복잡한 애플리케이션에서는 전역 상태 관리에 Pinia가 권장됩니다.

Composition API 관련 문항

6. Composition API는 Options API에 비해 어떤 장점이 있습니까

Composition API는 전통적인 Options API에 비해 여러 구조적 장점을 제공합니다.

기능 단위 구성: 같은 기능에 관련된 코드가 한곳에 묶이며, Options API처럼 타입(data, methods, computed)별로 흩어지지 않습니다.

composables를 통한 재사용: 로직을 재사용 가능한 함수로 손쉽게 분리할 수 있습니다.

더 나은 TypeScript 지원: 데코레이터 없이 자연스러운 타입 추론이 가능합니다.

javascript
// Options API: code fragmented by type
export default {
  data() {
    return {
      searchQuery: '',
      results: []
    }
  },
  computed: {
    hasResults() {
      return this.results.length > 0
    }
  },
  methods: {
    async search() {
      this.results = await fetchResults(this.searchQuery)
    }
  },
  watch: {
    searchQuery: 'search'
  }
}

// Composition API: code grouped by feature
import { ref, computed, watch } from 'vue'

export function useSearch() {
  const searchQuery = ref('')
  const results = ref([])

  const hasResults = computed(() => results.value.length > 0)

  const search = async () => {
    results.value = await fetchResults(searchQuery.value)
  }

  watch(searchQuery, search)

  return { searchQuery, results, hasResults, search }
}

7. 재사용 가능한 composable은 어떻게 만듭니까

composable은 반응성 로직을 캡슐화한 함수입니다. 일반적인 규약은 use 접두사 사용, 상태와 메서드를 담은 객체 반환, 정리(cleanup) 처리 등이 있습니다.

composables/useLocalStorage.jsjavascript
import { ref, watch } from 'vue'

// Composable to synchronize state with localStorage
export function useLocalStorage(key, defaultValue) {
  // Retrieve initial value from localStorage
  const storedValue = localStorage.getItem(key)
  const data = ref(
    storedValue ? JSON.parse(storedValue) : defaultValue
  )

  // Synchronize changes to localStorage
  watch(
    data,
    (newValue) => {
      if (newValue === null) {
        localStorage.removeItem(key)
      } else {
        localStorage.setItem(key, JSON.stringify(newValue))
      }
    },
    { deep: true } // Observe nested objects
  )

  return data
}

// Usage in a component
const theme = useLocalStorage('theme', 'light')
const userPrefs = useLocalStorage('prefs', { notifications: true })
명명 규약

composable은 재사용 가능하다는 점을 강조하기 위해 useXxx 규약을 따릅니다. 이 규약은 가독성을 높이고 반응형 의존성을 식별하기 쉽게 만듭니다.

8. watchEffect와 watch를 비교해 설명해 주세요

watchEffectwatch는 모두 변경에 반응하지만 접근 방식이 다릅니다.

watchEffect: 즉시 실행되며 반응형 의존성이 변경될 때 자동으로 재실행됩니다. 의존성 추적은 자동으로 이뤄집니다.

watch: 특정 소스를 관찰하고 이전 값과 새 값을 함께 제공합니다. 트리거 시점에 대한 더 세밀한 제어가 가능합니다.

javascript
// watchEffect vs watch comparison
import { ref, watch, watchEffect } from 'vue'

const userId = ref(1)
const userData = ref(null)

// watchEffect: automatic tracking
// Runs immediately
watchEffect(async () => {
  // userId is automatically tracked
  const response = await fetch(`/api/users/${userId.value}`)
  userData.value = await response.json()
})

// watch: explicit sources with old values
watch(userId, async (newId, oldId) => {
  console.log(`User changed from ${oldId} to ${newId}`)
  const response = await fetch(`/api/users/${newId}`)
  userData.value = await response.json()
}, {
  immediate: true // Run immediately like watchEffect
})

// watchEffect with cleanup
watchEffect((onCleanup) => {
  const controller = new AbortController()

  fetch(`/api/users/${userId.value}`, {
    signal: controller.signal
  }).then(/* ... */)

  // Cleanup: cancel previous request
  onCleanup(() => controller.abort())
})

9. script setup에서 TypeScript와 함께 props를 어떻게 다룹니까

<script setup> 문법은 definePropswithDefaults를 통해 TypeScript와의 기본 통합을 제공합니다.

TypedComponent.vuetypescript
<script setup lang="ts">
// Interface for props
interface Props {
  title: string
  count?: number
  items?: string[]
  onSubmit?: (data: FormData) => void
}

// defineProps with generic typing
// withDefaults for default values
const props = withDefaults(defineProps<Props>(), {
  count: 0,
  items: () => [], // Factory for objects/arrays
  onSubmit: undefined
})

// Props are automatically typed
console.log(props.title) // string
console.log(props.count) // number

// defineEmits with typing
const emit = defineEmits<{
  (e: 'update', value: number): void
  (e: 'delete', id: string): void
}>()

// Typed emit usage
const handleUpdate = () => {
  emit('update', props.count + 1)
}
</script>

Vue.js / Nuxt.js 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

성능 관련 문항

10. 어떤 성능 최적화 기법이 있습니까

Vue 3는 다양한 성능 최적화 메커니즘을 제공합니다.

1. v-once: single render for static contentjavascript
<template>
  <div v-once>
    <!-- This content will never be re-rendered -->
    <ComplexStaticComponent />
  </div>
</template>

// 2. v-memo: conditional memoization
<template>
  <div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
    <!-- Re-renders only if id or selected changes -->
    {{ item.name }}
  </div>
</template>

// 3. shallowRef/shallowReactive: shallow reactivity
import { shallowRef, triggerRef } from 'vue'

// Only tracks ref replacement, not internal mutations
const largeList = shallowRef([/* thousands of elements */])

// Force update after mutation
largeList.value.push(newItem)
triggerRef(largeList) // Manually trigger re-render
4. Async components for code-splittingjavascript
import { defineAsyncComponent } from 'vue'

const HeavyComponent = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  delay: 200, // Delay before showing loading
  errorComponent: ErrorDisplay,
  timeout: 3000
})

// 5. KeepAlive for component caching
<template>
  <KeepAlive :include="['Dashboard', 'UserProfile']" :max="10">
    <component :is="currentView" />
  </KeepAlive>
</template>

11. 불필요한 재렌더를 어떻게 피합니까

불필요한 재렌더는 성능에 악영향을 줍니다. 다음과 같은 전략으로 최소화할 수 있습니다.

javascript
// Problem: function created on each render
<template>
  <!--New function on every render -->
  <ChildComponent @click="() => handleClick(item.id)" />
</template>

// Solution: use a method or ref
<script setup>
const handleItemClick = (id) => {
  // Processing logic
}
</script>

<template>
  <!--Stable reference -->
  <ChildComponent @click="handleItemClick(item.id)" />
</template>
javascript
// Using computed for expensive calculations
import { computed } from 'vue'

// ❌ Recalculated on every render
const getFilteredItems = () => {
  return items.value.filter(/* complex logic */)
}

// ✅ Cached, recalculated only if items changes
const filteredItems = computed(() => {
  return items.value.filter(/* complex logic */)
})

12. 컴포넌트와 라우트의 lazy loading을 설명해 주세요

lazy loading은 코드를 필요할 때 로드하여 초기 번들 크기를 줄입니다.

router/index.jsjavascript
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      // Immediate loading (main bundle)
      component: () => import('@/views/Home.vue')
    },
    {
      path: '/dashboard',
      // Separate chunk with custom name
      component: () => import(
        /* webpackChunkName: "dashboard" */
        '@/views/Dashboard.vue'
      ),
      // Lazy loading child routes
      children: [
        {
          path: 'analytics',
          component: () => import('@/views/Analytics.vue')
        }
      ]
    },
    {
      path: '/admin',
      // Prefetch on link hover
      component: () => import('@/views/Admin.vue'),
      meta: { prefetch: true }
    }
  ]
})

export default router

Vue Router 관련 문항

13. 가드를 사용해 라우트를 어떻게 보호합니까

내비게이션 가드는 라우트 접근을 제어합니다.

router/index.jsjavascript
import { createRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

const router = createRouter({
  routes: [
    {
      path: '/dashboard',
      component: Dashboard,
      meta: { requiresAuth: true, roles: ['admin', 'user'] }
    }
  ]
})

// Global guard: checks authentication
router.beforeEach(async (to, from, next) => {
  const auth = useAuthStore()

  // Public route
  if (!to.meta.requiresAuth) {
    return next()
  }

  // Check authentication
  if (!auth.isAuthenticated) {
    return next({
      path: '/login',
      query: { redirect: to.fullPath }
    })
  }

  // Check roles if specified
  if (to.meta.roles && !to.meta.roles.includes(auth.user.role)) {
    return next('/unauthorized')
  }

  next()
})

// Component-level guard
export default {
  beforeRouteEnter(to, from, next) {
    // No access to this here
    next(vm => {
      // Access component instance via vm
      vm.loadData()
    })
  },
  beforeRouteLeave(to, from, next) {
    // Confirm before leaving if form modified
    if (this.hasUnsavedChanges) {
      const answer = confirm('Leave without saving?')
      next(answer)
    } else {
      next()
    }
  }
}

14. 라우트에 props를 어떻게 전달합니까

Vue Router를 사용하면 컴포넌트를 라우트 매개변수와 분리할 수 있습니다.

javascript
// Route configuration with props
const routes = [
  {
    path: '/user/:id',
    component: UserProfile,
    // Boolean mode: passes params as props
    props: true
  },
  {
    path: '/search',
    component: SearchResults,
    // Function mode: custom transformation
    props: (route) => ({
      query: route.query.q,
      page: parseInt(route.query.page) || 1,
      filters: route.query.filters?.split(',') || []
    })
  },
  {
    path: '/static',
    component: StaticPage,
    // Object mode: static props
    props: { sidebar: true, theme: 'dark' }
  }
]
UserProfile.vuejavascript
<script setup>
// Props are automatically injected
defineProps<{
  id: string
}>()
</script>

// SearchResults.vue
<script setup>
defineProps<{
  query: string
  page: number
  filters: string[]
}>()
</script>

Pinia와 상태 관리 관련 문항

15. Pinia와 Vuex의 차이는 무엇입니까

Pinia는 Vue 3의 공식 상태 관리자이며 단순화된 API로 Vuex를 대체합니다.

| 기능 | Vuex | Pinia | |---------|------|-------| | Mutations | 필수 | 필요 없음 | | 모듈 | 복잡한 설정 | 독립 스토어 | | TypeScript | 제한적 지원 | 네이티브 완전 지원 | | API | Options | Composition + Options | | DevTools | 지원 | 완전 지원 |

javascript
// Pinia Store with Composition API
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  // State
  const items = ref([])
  const discountCode = ref(null)

  // Getters (computed)
  const totalItems = computed(() =>
    items.value.reduce((sum, item) => sum + item.quantity, 0)
  )

  const totalPrice = computed(() => {
    const subtotal = items.value.reduce(
      (sum, item) => sum + item.price * item.quantity, 0
    )
    return discountCode.value ? subtotal * 0.9 : subtotal
  })

  // Actions (direct functions)
  function addItem(product) {
    const existing = items.value.find(i => i.id === product.id)
    if (existing) {
      existing.quantity++
    } else {
      items.value.push({ ...product, quantity: 1 })
    }
  }

  function removeItem(productId) {
    const index = items.value.findIndex(i => i.id === productId)
    if (index > -1) {
      items.value.splice(index, 1)
    }
  }

  async function checkout() {
    const response = await api.createOrder(items.value)
    items.value = []
    return response
  }

  return {
    items, discountCode,
    totalItems, totalPrice,
    addItem, removeItem, checkout
  }
})

16. Pinia 스토어의 상태를 어떻게 영속화합니까

영속화는 사용자 세션 사이에 상태를 유지하도록 도와줍니다.

plugins/piniaPersistedState.jsjavascript
import { watch } from 'vue'

export function createPersistedState(options = {}) {
  const {
    key = 'pinia',
    storage = localStorage,
    paths = null
  } = options

  return ({ store }) => {
    // Restore state on startup
    const savedState = storage.getItem(`${key}-${store.$id}`)
    if (savedState) {
      store.$patch(JSON.parse(savedState))
    }

    // Persist changes
    watch(
      () => store.$state,
      (state) => {
        const toSave = paths
          ? paths.reduce((acc, path) => {
              acc[path] = state[path]
              return acc
            }, {})
          : state

        storage.setItem(
          `${key}-${store.$id}`,
          JSON.stringify(toSave)
        )
      },
      { deep: true }
    )
  }
}

// main.js
import { createPinia } from 'pinia'
import { createPersistedState } from './plugins/piniaPersistedState'

const pinia = createPinia()
pinia.use(createPersistedState({
  key: 'app-state',
  paths: ['user', 'preferences'] // Persist only these keys
}))
민감한 데이터

민감한 데이터(토큰, 비밀번호)는 localStorage에 저장하지 않는 편이 좋습니다. 인증 토큰은 httpOnly 쿠키로 다루는 것이 더 안전합니다.

고급 문항

17. Vue에서 플러그인 시스템은 어떻게 구현합니까

플러그인은 Vue를 전역 기능으로 확장합니다.

plugins/analyticsPlugin.jsjavascript
export const AnalyticsPlugin = {
  install(app, options = {}) {
    const { trackingId, debug = false } = options

    // Global injection available in all components
    const analytics = {
      trackEvent(category, action, label) {
        if (debug) {
          console.log('Analytics:', { category, action, label })
        }
        // Logic to send to analytics service
        window.gtag?.('event', action, {
          event_category: category,
          event_label: label
        })
      },
      trackPage(path) {
        window.gtag?.('config', trackingId, { page_path: path })
      }
    }

    // Make available via inject
    app.provide('analytics', analytics)

    // Add global property (discouraged in Composition API)
    app.config.globalProperties.$analytics = analytics

    // Custom directive for click tracking
    app.directive('track', {
      mounted(el, binding) {
        el.addEventListener('click', () => {
          analytics.trackEvent('click', binding.value, el.textContent)
        })
      }
    })

    // Automatic route change tracking
    app.mixin({
      mounted() {
        if (this.$route) {
          analytics.trackPage(this.$route.path)
        }
      }
    })
  }
}

// main.js
import { AnalyticsPlugin } from './plugins/analyticsPlugin'

app.use(AnalyticsPlugin, {
  trackingId: 'UA-XXXXX-X',
  debug: import.meta.env.DEV
})

18. 렌더 함수와 그 활용을 설명해 주세요

렌더 함수는 렌더링을 완전히 제어할 수 있어 매우 동적인 컴포넌트에 유용합니다.

components/DynamicHeading.jsjavascript
import { h } from 'vue'

// Functional component with render function
export const DynamicHeading = {
  props: {
    level: {
      type: Number,
      default: 1,
      validator: (v) => v >= 1 && v <= 6
    }
  },
  setup(props, { slots }) {
    // h() creates a vnode
    // Arguments: type, props, children
    return () => h(
      `h${props.level}`,
      { class: 'dynamic-heading' },
      slots.default?.()
    )
  }
}

// Component with complex conditional logic
export const ConditionalWrapper = {
  props: ['condition', 'wrapper'],
  setup(props, { slots }) {
    return () => {
      if (props.condition) {
        return h(props.wrapper, null, slots.default?.())
      }
      return slots.default?.()
    }
  }
}

// Usage
<DynamicHeading :level="2">Level 2 Title</DynamicHeading>

<ConditionalWrapper :condition="isLink" wrapper="a">
  Conditional content
</ConditionalWrapper>

19. Vitest로 Vue 컴포넌트를 어떻게 테스트합니까

단위 테스트는 컴포넌트의 격리된 동작을 검증합니다.

components/__tests__/Counter.spec.jsjavascript
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from '../Counter.vue'

describe('Counter', () => {
  it('displays the initial value', () => {
    const wrapper = mount(Counter, {
      props: { initialValue: 5 }
    })

    expect(wrapper.text()).toContain('5')
  })

  it('increments the value on click', async () => {
    const wrapper = mount(Counter)

    await wrapper.find('button.increment').trigger('click')

    expect(wrapper.text()).toContain('1')
  })

  it('emits an event when changed', async () => {
    const wrapper = mount(Counter)

    await wrapper.find('button.increment').trigger('click')

    expect(wrapper.emitted('change')).toBeTruthy()
    expect(wrapper.emitted('change')[0]).toEqual([1])
  })

  it('calls the service on submit', async () => {
    const mockSubmit = vi.fn()
    const wrapper = mount(Counter, {
      global: {
        provide: {
          submitService: mockSubmit
        }
      }
    })

    await wrapper.find('form').trigger('submit')

    expect(mockSubmit).toHaveBeenCalled()
  })
})

20. Vue에서 오류를 전역으로 어떻게 처리합니까

Vue 3는 오류를 잡고 처리하기 위한 여러 가지 메커니즘을 제공합니다.

main.jsjavascript
import { createApp } from 'vue'

const app = createApp(App)

// Global handler for component errors
app.config.errorHandler = (err, instance, info) => {
  // err: the error
  // instance: the component instance
  // info: string describing where the error occurred

  console.error('Vue error:', err)
  console.error('Component:', instance?.$options?.name)
  console.error('Info:', info)

  // Send to monitoring service
  errorTracker.captureException(err, {
    component: instance?.$options?.name,
    info
  })
}

// Handler for warnings (dev only)
app.config.warnHandler = (msg, instance, trace) => {
  console.warn('Vue warning:', msg)
}
javascript
// ErrorBoundary component
<script setup>
import { onErrorCaptured, ref } from 'vue'

const error = ref(null)

// Captures errors from child components
onErrorCaptured((err, instance, info) => {
  error.value = {
    message: err.message,
    component: instance?.$options?.name,
    info
  }

  // Return false to stop propagation
  return false
})

const retry = () => {
  error.value = null
}
</script>

<template>
  <div v-if="error" class="error-boundary">
    <h2>An error occurred</h2>
    <p>{{ error.message }}</p>
    <button @click="retry">Retry</button>
  </div>
  <slot v-else />
</template>

Vue.js / Nuxt.js 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

모범 사례 관련 문항

21. Vue에서 어떤 명명 규약을 따라야 합니까

명명 규약은 코드의 가독성과 유지 보수성을 높입니다.

javascript
// Component naming
// PascalCase for files and names
// BaseButton.vue, AppHeader.vue, TheNavbar.vue

// Props: camelCase in JS, kebab-case in template
defineProps<{
  userName: string        // JS
  isActive: boolean       // JS
}>()

// <UserCard :user-name="name" :is-active="active" />

// Events: camelCase with action prefix
const emit = defineEmits<{
  (e: 'updateValue', value: string): void  // ✅
  (e: 'submit'): void                       // ✅
  (e: 'value-updated'): void               // ❌ Avoid
}>()

// Composables: use prefix
// useAuth.js, useFetch.js, useLocalStorage.js

// Pinia stores: use prefix + Store suffix
// useUserStore, useCartStore, useSettingsStore

// Constants: SCREAMING_SNAKE_CASE
const MAX_RETRY_COUNT = 3
const API_BASE_URL = '/api/v1'

22. 대규모 Vue 프로젝트는 어떻게 구성합니까

모듈 단위 구조는 탐색과 유지 보수를 더 쉽게 만듭니다.

text
src/
├── assets/              # Static files
├── components/
│   ├── ui/              # Generic components (Button, Modal)
│   └── common/          # Reusable business components
├── composables/         # Reusable logic
│   ├── useAuth.js
│   └── useFetch.js
├── layouts/             # Page layouts
│   ├── DefaultLayout.vue
│   └── AuthLayout.vue
├── modules/             # Functional modules
│   ├── auth/
│   │   ├── components/
│   │   ├── composables/
│   │   ├── stores/
│   │   └── views/
│   └── dashboard/
├── plugins/             # Vue plugins
├── router/
│   ├── index.js
│   └── guards.js
├── stores/              # Global Pinia stores
├── types/               # TypeScript types
├── utils/               # Pure utilities
└── views/               # Pages/Routes

23. v-if와 v-show는 언제 선택합니까

v-ifv-show 중 어떤 것을 선택하느냐는 토글 빈도에 달려 있습니다.

javascript
// v-if: low initial cost, expensive toggle
// Removes/adds element from DOM
// Ideal for: rarely modified conditions

<template>
  <!-- v-if: component not created if not admin -->
  <AdminPanel v-if="user.isAdmin" />

  <!-- v-if with v-else-if for multiple conditions -->
  <LoadingSpinner v-if="isLoading" />
  <ErrorMessage v-else-if="error" :message="error" />
  <DataDisplay v-else :data="data" />
</template>

// v-show: higher initial cost, fast toggle
// Uses display: none
// Ideal for: frequent toggles

<template>
  <!-- v-show: always rendered, frequent toggle -->
  <Tooltip v-show="isHovered">
    Contextual information
  </Tooltip>

  <!-- Accordion menu with frequent toggle -->
  <div v-show="isExpanded" class="accordion-content">
    {{ content }}
  </div>
</template>

24. v-for로 만든 리스트를 어떻게 최적화합니까

요소가 많은 리스트에서는 성능을 위해 최적화가 필수입니다.

javascript
// Always use :key with a unique stable identifier
<template>
  <!--Unique and stable key -->
  <li v-for="item in items" :key="item.id">
    {{ item.name }}
  </li>

  <!--Index as key (reordering issues) -->
  <li v-for="(item, index) in items" :key="index">
    {{ item.name }}
  </li>
</template>

// Filtering and sorting: use computed
<script setup>
import { computed } from 'vue'

const sortedAndFilteredItems = computed(() => {
  return items.value
    .filter(item => item.isActive)
    .sort((a, b) => a.name.localeCompare(b.name))
})
</script>

// Virtualization for very long lists
<script setup>
import { useVirtualList } from '@vueuse/core'

const { list, containerProps, wrapperProps } = useVirtualList(
  largeList,
  { itemHeight: 50 }
)
</script>

<template>
  <div v-bind="containerProps" style="height: 400px; overflow: auto">
    <div v-bind="wrapperProps">
      <div v-for="item in list" :key="item.data.id">
        {{ item.data.name }}
      </div>
    </div>
  </div>
</template>

25. Renderless component 패턴을 설명해 주세요

renderless 컴포넌트는 HTML 구조를 강제하지 않고 로직만 캡슐화합니다.

components/MouseTracker.vuejavascript
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const x = ref(0)
const y = ref(0)

const updatePosition = (event) => {
  x.value = event.clientX
  y.value = event.clientY
}

onMounted(() => {
  window.addEventListener('mousemove', updatePosition)
})

onUnmounted(() => {
  window.removeEventListener('mousemove', updatePosition)
})

// Expose state via slot
defineExpose({ x, y })
</script>

<template>
  <!-- Slot with props: parent decides the rendering -->
  <slot :x="x" :y="y" />
</template>
javascript
// Usage: full control over rendering
<template>
  <MouseTracker v-slot="{ x, y }">
    <div class="cursor-display">
      Position: {{ x }}, {{ y }}
    </div>
  </MouseTracker>

  <MouseTracker v-slot="{ x, y }">
    <svg>
      <circle :cx="x" :cy="y" r="10" fill="red" />
    </svg>
  </MouseTracker>
</template>

이 패턴은 로직과 표현을 완전히 분리하여 재사용성을 극대화합니다.

결론

이 25개의 문항은 Vue.js 면접에서 평가되는 핵심 개념을 모두 다룹니다.

  • 반응성: ref, reactive, computed, watch
  • Composition API: composable, script setup, TypeScript
  • 성능: lazy loading, 가상화, 최적화 기법
  • Vue Router: 가드, props, 내비게이션
  • Pinia: 스토어, 영속화, 비동기 액션
  • 모범 사례: 구조, 규약, 고급 패턴

효과적인 준비는 이론적 이해와 코드 연습을 병행하는 데 있습니다. 여기서 다룬 모든 개념은 면접에서 더 깊은 후속 질문으로 이어질 수 있습니다.

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

태그

#vue.js
#interview
#frontend
#javascript
#technical questions

공유

관련 기사