blog

useI18n()은 setup 밖에서 안 되는데 Pinia store는 왜 될까?

Front-end / Vue#Vue#vue-i18n#Pinia#Composition API

API 모듈이나 유틸 같은 일반 .ts 파일에서 useI18n()을 호출하면 에러가 나는데, useCounterStore()는 잘 동작한다. 둘 다 use로 시작하고 둘 다 app.use()로 등록하는데 왜 다를까?

결론부터 말하면 기준은 ".ts 파일이냐 .vue 파일이냐"가 아니라 "현재 컴포넌트 인스턴스가 필요한가" 다.

useI18n(): 컴포넌트 setup 안에서만 호출 가능

useI18n()은 내부에서 getCurrentInstance()로 지금 setup 중인 컴포넌트를 찾고, 그 컴포넌트의 앱 컨텍스트에서 i18n 인스턴스를 꺼낸다(컴포넌트별 로컬 스코프 메시지도 지원하기 때문). 컴포넌트 setup 밖에서는 현재 인스턴스가 없으므로 에러가 난다.

// api/error-message.ts
import { useI18n } from 'vue-i18n';

const { t } = useI18n(); // ❌ Must be called at the top of a `setup` function

같은 코드라도 .ts 파일에 있는 composable을 컴포넌트의 setup에서 호출하면 정상 동작한다. 파일 확장자는 상관없다.

컴포넌트 밖에서 번역하기: i18n.global

i18n 인스턴스를 모듈로 내보내고 global composer를 직접 사용한다.

// i18n.ts
import { createI18n } from 'vue-i18n';

export const i18n = createI18n({
  legacy: false,
  locale: 'ko',
  fallbackLocale: 'en',
  messages: {
    en: { welcome: 'Welcome' },
    ko: { welcome: '환영합니다' },
  },
});
// main.ts
app.use(i18n);
// api/error-message.ts
import { i18n } from '@/i18n';

export const getErrorMessage = (key: string) => i18n.global.t(key); // ✅

Pinia store: 활성 Pinia만 있으면 어디서든 호출 가능

Pinia의 useStore()는 컴포넌트 인스턴스 대신 활성 Pinia(active pinia) 를 찾는다. app.use(pinia)를 실행하면 그 Pinia가 활성 Pinia로 설정되므로, 그 이후에 실행되는 코드라면 컴포넌트 밖(라우터 가드, API 모듈 등)에서도 store를 쓸 수 있다.

// api/client.ts
import { useAuthStore } from '@/stores/auth';

export async function request(url: string) {
  const auth = useAuthStore(); // ✅ 함수 실행 시점에는 app.use(pinia)가 끝나 있음
  return fetch(url, { headers: { Authorization: `Bearer ${auth.token}` } });
}

다만 Pinia도 완전히 독립적인 것은 아니다. 모듈 최상단처럼 app.use(pinia) 이전에 실행되는 위치에서 호출하면 에러가 난다.

// api/client.ts
import { useAuthStore } from '@/stores/auth';

const auth = useAuthStore(); // ❌ getActivePinia() was called but there was no active Pinia

그래서 store는 모듈 최상단이 아니라 함수 안에서 호출하는 것이 안전하다. SSR에서는 요청마다 Pinia가 달라야 하므로 컴포넌트 밖 사용에 더 주의해야 한다.

커스텀 composable은?

ref, computed만 쓰는 composable은 어디서 호출해도 동작한다. 하지만 내부에서 onMounted 같은 생명주기 훅, inject, useRoute, useI18n 등 컴포넌트 인스턴스가 필요한 API를 쓰면 그 composable도 setup 안에서만 호출할 수 있다.

정리

함수필요한 것컴포넌트 밖에서
useI18n()현재 컴포넌트 인스턴스불가 → i18n.global.t() 사용
useStore() (Pinia)활성 Pinia(app.use(pinia) 이후)가능, 단 함수 안에서 호출
ref/computed만 쓰는 composable없음가능
생명주기 훅·inject를 쓰는 composable현재 컴포넌트 인스턴스불가