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

Vue.js 면접에서는 프레임워크 문법보다 훨씬 더 많은 것을 평가합니다. 채용 담당자는 반응성 시스템에 대한 이해도, Composition API를 활용한 코드 구성 능력, 그리고 실제 성능과 아키텍처 문제를 해결할 수 있는 역량을 확인하고 싶어합니다.
모든 문항에는 자세한 답변과 코드 예제가 포함되어 있습니다. 기술 면접에서는 실제 면접처럼 개념을 소리 내어 설명하는 연습이 효과적입니다.
Vue.js 기본 문항
1. Vue 3에서 ref와 reactive의 차이는 무엇입니까
이 질문은 Vue 3의 핵심인 반응성 시스템에 대한 이해를 확인합니다. 주요 차이는 다루는 데이터 타입과 접근 문법에 있습니다.
ref는 원시값(string, number, boolean)에 대한 반응형 참조를 만들고 스크립트에서는 .value로 값에 접근해야 합니다. reactive는 객체에 대한 반응형 프록시를 만들며 속성에 직접 접근할 수 있습니다.
// 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와 달리, 이 방식은 속성의 추가와 제거를 동적으로 감지합니다.
// 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의 차이를 설명해 주세요
computed와 watch는 반응성 관리에서 서로 다른 역할을 수행합니다.
computed: 다른 반응형 데이터에서 파생된 값을 계산합니다. 값은 캐시되며, 의존성이 변경될 때만 재계산됩니다. 데이터 변환에 적합합니다.
watch: 변경에 응답하여 부수 효과를 실행합니다. API 호출, DOM 조작, 비동기 작업에 유용합니다.
// 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에는 필요한 변경만 적용합니다.
// 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 elementsVue 3의 최적화에는 정적 노드의 호이스팅, 변경 유형을 식별하기 위한 패치 플래그, 컴파일러 단계의 트리 셰이킹이 포함됩니다.
5. 직접 관계가 없는 컴포넌트 간 통신은 어떻게 처리합니까
부모-자식 관계가 직접 없는 컴포넌트끼리 통신하기 위한 패턴은 여러 가지가 있습니다.
// 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 }
}// 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 지원: 데코레이터 없이 자연스러운 타입 추론이 가능합니다.
// 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) 처리 등이 있습니다.
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를 비교해 설명해 주세요
watchEffect와 watch는 모두 변경에 반응하지만 접근 방식이 다릅니다.
watchEffect: 즉시 실행되며 반응형 의존성이 변경될 때 자동으로 재실행됩니다. 의존성 추적은 자동으로 이뤄집니다.
watch: 특정 소스를 관찰하고 이전 값과 새 값을 함께 제공합니다. 트리거 시점에 대한 더 세밀한 제어가 가능합니다.
// 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> 문법은 defineProps와 withDefaults를 통해 TypeScript와의 기본 통합을 제공합니다.
<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는 다양한 성능 최적화 메커니즘을 제공합니다.
<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-renderimport { 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. 불필요한 재렌더를 어떻게 피합니까
불필요한 재렌더는 성능에 악영향을 줍니다. 다음과 같은 전략으로 최소화할 수 있습니다.
// 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>// 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은 코드를 필요할 때 로드하여 초기 번들 크기를 줄입니다.
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 routerVue Router 관련 문항
13. 가드를 사용해 라우트를 어떻게 보호합니까
내비게이션 가드는 라우트 접근을 제어합니다.
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를 사용하면 컴포넌트를 라우트 매개변수와 분리할 수 있습니다.
// 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' }
}
]<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 | 지원 | 완전 지원 |
// 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 스토어의 상태를 어떻게 영속화합니까
영속화는 사용자 세션 사이에 상태를 유지하도록 도와줍니다.
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를 전역 기능으로 확장합니다.
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. 렌더 함수와 그 활용을 설명해 주세요
렌더 함수는 렌더링을 완전히 제어할 수 있어 매우 동적인 컴포넌트에 유용합니다.
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 컴포넌트를 어떻게 테스트합니까
단위 테스트는 컴포넌트의 격리된 동작을 검증합니다.
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는 오류를 잡고 처리하기 위한 여러 가지 메커니즘을 제공합니다.
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)
}// 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에서 어떤 명명 규약을 따라야 합니까
명명 규약은 코드의 가독성과 유지 보수성을 높입니다.
// 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 프로젝트는 어떻게 구성합니까
모듈 단위 구조는 탐색과 유지 보수를 더 쉽게 만듭니다.
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/Routes23. v-if와 v-show는 언제 선택합니까
v-if와 v-show 중 어떤 것을 선택하느냐는 토글 빈도에 달려 있습니다.
// 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로 만든 리스트를 어떻게 최적화합니까
요소가 많은 리스트에서는 성능을 위해 최적화가 필수입니다.
// 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 구조를 강제하지 않고 로직만 캡슐화합니다.
<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>// 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 3 Composition API 완벽 가이드: 리액티비티 마스터하기
Vue 3 Composition API 실전 가이드입니다. ref, reactive, computed, watch, 컴포저블을 배워 고성능 Vue 애플리케이션을 구축하십시오.

Vue 3 Pinia vs Vuex 완벽 비교: 2026년 상태 관리 전략과 면접 핵심 질문
Vue 3 생태계에서 Pinia와 Vuex를 비교 분석합니다. Options Store와 Setup Store 패턴, TypeScript 통합, 크로스 스토어 구성, SSR 지원, Vuex에서 Pinia로의 마이그레이션 전략, 그리고 2026년 면접에서 자주 출제되는 상태 관리 질문을 코드 예제와 함께 정리합니다.

Node.js 백엔드 면접 질문: 완벽 가이드 2026
Node.js 백엔드 면접에서 가장 자주 나오는 25가지 질문. Event loop, async/await, streams, 클러스터링, 성능을 상세한 답변과 함께 설명합니다.