본문으로 건너뛰기

Import 보호

실험적 기능: Import 보호는 실험적 기능이며 변경될 수 있습니다.

Import 보호는 서버 전용 코드가 클라이언트 번들로 유출되거나 클라이언트 전용 코드가 서버 번들로 유출되는 것을 방지합니다. 이 기능은 TanStack Start 내부에서 실행되며 기본적으로 활성화됩니다.

작동 방식

TanStack Start는 애플리케이션을 클라이언트서버라는 두 환경용으로 빌드합니다. 일부 코드는 한 환경에서만 실행되어야 합니다. Import 보호는 개발 및 빌드 중 소스 파일의 모든 import를 검사하고, 환경 경계를 넘는 import를 차단하거나 모킹합니다.

import가 거부되는 방식은 두 가지입니다:

  • 파일 패턴은 해석된 파일 경로와 일치합니다. 기본적으로 클라이언트 환경에서는 *.server.* 파일이 거부되고 서버 환경에서는 *.client.* 파일이 거부됩니다.
  • 지정자 패턴은 원시 import 문자열과 일치합니다. 기본적으로 클라이언트 환경에서는 @tanstack/react-start/server이 거부됩니다.

기본 규칙

Import 보호는 다음 기본값으로 즉시 활성화됩니다:

설정기본값
behavior (개발)'mock' -- 경고하고 모의 모듈로 대체합니다
behavior (빌드)'error' -- 빌드를 실패 처리합니다
log'once' -- 반복되는 위반을 중복 제거합니다
범위Start의 srcDirectory 내부 파일

클라이언트 환경 거부 규칙:

  • **/*.server.*과 일치하는 파일
  • 지정자 @tanstack/react-start/server
  • 파일 검사에서 제외: **/node_modules/**

서버 환경 거부 규칙:

  • **/*.client.*과 일치하는 파일
  • 파일 검사에서 제외: **/node_modules/**

기본적으로 node_modules 내부의 파일은 excludeFiles 옵션을 통해 해석된 대상에 대한 거부 검사에서 제외됩니다. 이를 통해 해석된 파일 이름에 .client. 또는 .server.이 포함된 서드 파티 패키지에서 오탐이 발생하는 것을 방지합니다. 서드 파티 파일을 검사해야 한다면 관련 환경에서 excludeFiles: []을 설정하세요. 자세한 내용은 거부 규칙 구성을 참고하세요.

이러한 기본값 덕분에 별도의 구성 없이 .server.ts / .client.ts 명명 규칙을 사용하여 파일을 단일 환경으로 제한할 수 있습니다. 전체 디렉터리(예: server/ 또는 client/)도 거부하려면 거부 규칙 구성files을 통해 추가하세요. 예를 들어 클라이언트 환경에는 files: ['**/*.server.*', '**/server/**']을 사용합니다.

타입 전용 import

타입 전용 import와 re-export는 런타임 번들에서 제거되어 환경별 코드를 유출할 수 없으므로 import 보호에서 무시됩니다.

import type { User } from './db.server'
import { type RequestHandler } from '@tanstack/react-start/server'

export type { User } from './db.server'

혼합 import에 런타임 값이 하나 이상 포함되면 여전히 검사 대상이 됩니다. 타입만 환경 경계를 넘어도 안전하다면 타입 import와 값 import를 분리하세요.

// This is still checked because `getUsers` is a runtime value.
import { type User, getUsers } from './db.server'

파일 마커

파일 상단에 부수 효과 import를 추가하여 모듈을 서버 전용 또는 클라이언트 전용으로 명시적으로 표시할 수 있습니다:

// src/lib/secrets.ts
import '@tanstack/react-start/server-only'

export const API_KEY = process.env.API_KEY
// src/lib/local-storage.ts
import '@tanstack/react-start/client-only'

export function savePreferences(prefs: Record<string, string>) {
localStorage.setItem('prefs', JSON.stringify(prefs))
}

플러그인은 마커 import를 발견하면 해당 파일을 제한된 파일로 기록합니다. 이후 잘못된 환경에서 해당 파일을 import하면 import가 거부됩니다. 동일한 파일에 두 마커가 모두 있으면 항상 오류가 발생합니다.

파일이 .server.* / .client.* 명명 규칙을 따르지 않지만 환경별 코드를 포함하는 경우 마커가 유용합니다.

동작 모드

behavior 옵션은 위반이 감지될 때의 동작을 제어합니다:

  • 'error' -- 상세한 오류 메시지와 함께 빌드가 실패합니다. 프로덕션 빌드의 기본값입니다.
  • 'mock' -- import가 안전한 프록시 값을 반환하는 모의 모듈로 대체됩니다. 경고가 기록되지만 빌드는 계속됩니다. 개발 중 기본값입니다.

모의 모드는 import 그래프에 위반이 있어도 작업을 계속할 수 있으므로 개발 중에 유용합니다. 모의 모듈은 재귀 Proxy를 반환하므로, 모의 처리된 import에서 속성에 접근하거나 함수를 호출해도 충돌하는 대신 또 다른 모의 객체가 반환됩니다.

기본값을 재정의할 수 있습니다:

Vite

vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'

export default defineConfig({
plugins: [
tanstackStart({
importProtection: {
// Always error, even in dev
behavior: 'error',
},
}),
],
})

Rsbuild

rsbuild.config.ts
import { defineConfig } from '@rsbuild/core'
import { tanstackStart } from '@tanstack/react-start/plugin/rsbuild'

export default defineConfig({
plugins: [
tanstackStart({
importProtection: {
// Always error, even in dev
behavior: 'error',
},
}),
],
})

또는 모드별로 서로 다른 동작을 설정합니다:

importProtection: {
behavior: {
dev: 'mock',
build: 'error',
},
}

거부 규칙 구성

기본값에 자체 거부 규칙을 추가할 수 있습니다. 규칙은 glob 패턴(picomatch 사용) 또는 정규 표현식을 사용하여 환경별로 지정합니다.

Vite

vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'

export default defineConfig({
plugins: [
tanstackStart({
importProtection: {
client: {
// Block specific npm packages from the client bundle
specifiers: ['@prisma/client', 'bcrypt'],
// Block files in a custom directory
files: ['**/db/**'],
},
server: {
// Block browser-only libraries from the server
specifiers: ['localforage'],
},
},
}),
],
})

Rsbuild

rsbuild.config.ts
import { defineConfig } from '@rsbuild/core'
import { tanstackStart } from '@tanstack/react-start/plugin/rsbuild'

export default defineConfig({
plugins: [
tanstackStart({
importProtection: {
client: {
// Block specific npm packages from the client bundle
specifiers: ['@prisma/client', 'bcrypt'],
// Block files in a custom directory
files: ['**/db/**'],
},
server: {
// Block browser-only libraries from the server
specifiers: ['localforage'],
},
},
}),
],
})

서드 파티 패키지 검사

기본적으로 node_modules 내부에서 확인된 파일은 확인된 대상에 대한 거부 검사(파일 패턴 및 마커 검사)에서 제외됩니다. 이렇게 하면 배포 파일명에 우연히 .client. 또는 .server.을 사용하는 패키지에서 발생하는 오탐을 방지할 수 있습니다. 특정 환경에서 검사를 다시 활성화하려면 excludeFiles을 빈 배열로 설정합니다:

importProtection: {
server: {
// Re-enable file-pattern checking for node_modules in the server environment
excludeFiles: [],
},
}

excludeFiles을 제공하면 기본값(['**/node_modules/**'])을 완전히 대체합니다. node_modules을 계속 건너뛰면서 추가 경로를 제외하려면 둘 다 포함합니다:

importProtection: {
client: {
excludeFiles: ['**/node_modules/**', '**/vendor/**'],
},
}

범위 지정 및 제외

기본적으로 import 보호는 Start의 srcDirectory 내부에 있는 파일만 검사합니다. include, excludeignoreImporters을 사용하여 범위를 변경할 수 있습니다:

importProtection: {
// Only check files matching these patterns
include: ['src/**'],
// Skip checking these files
exclude: ['src/generated/**'],
// Ignore violations when these files are the importer
ignoreImporters: ['**/*.test.ts', '**/*.spec.ts'],
}

위반 추적 정보 읽기

위반이 감지되면 플러그인은 위반으로 이어진 전체 import 체인, 문제가 있는 줄을 강조하는 코드 스니펫, 실행 가능한 제안이 포함된 진단 메시지를 표시합니다.

클라이언트의 서버 전용 코드

이 예제에서는 클라이언트 환경에서 *.server.* 파일을 전이적으로 import하는 경우를 보여 줍니다:

[import-protection] Import denied in client environment

Denied by file pattern: **/*.server.*
Importer: src/features/auth/session.ts:5:27
Import: "../db/queries.server"
Resolved: src/db/queries.server.ts

Trace:
1. src/routes/index.tsx:2:34 (entry) (import "../features/auth/session")
2. src/features/auth/session.ts:5:27 (import "../db/queries.server")

Code:
3 | import { logger } from '../utils/logger'
4 |
> 5 | import { getUsers } from '../db/queries.server'
| ^
6 |
7 | export function loadAuth() {

src/features/auth/session.ts:5:27

Suggestions:
- Wrap in createServerFn().handler(() => ...) to make it callable from the client via RPC
- Wrap in createServerOnlyFn(() => ...) if it should not be callable from the client
- Use createIsomorphicFn().client(() => ...).server(() => ...) for environment-specific implementations
- Split the file so client-safe exports are separate

서버의 클라이언트 전용 코드

이 예제에서는 SSR 환경에서 *.client.* 파일을 import하는 경우를 보여 줍니다. 코드 스니펫에 JSX가 포함되어 있으므로 <ClientOnly> 제안이 먼저 표시됩니다:

[import-protection] Import denied in server environment

Denied by file pattern: **/*.client.*
Importer: src/components/dashboard.tsx:3:30
Import: "./browser-widget.client"
Resolved: src/components/browser-widget.client.tsx

Trace:
1. src/routes/dashboard.tsx:1:32 (entry) (import "../components/dashboard")
2. src/components/dashboard.tsx:3:30 (import "./browser-widget.client")

Code:
1 | import { BrowserWidget } from './browser-widget.client'
2 |
> 3 | export function Dashboard() { return <BrowserWidget /> }
| ^
4 |

src/components/dashboard.tsx:3:30

Suggestions:
- Wrap in <ClientOnly fallback={...}>...</ClientOnly> to render only after hydration
- Wrap in createClientOnlyFn(() => ...) if it should only run in the browser
- Use createIsomorphicFn().client(() => ...).server(() => ...) for environment-specific implementations
- Split the file so server-safe exports are separate

출력 읽는 방법

각 위반 메시지는 다음 섹션으로 구성됩니다:

섹션설명
헤더위반이 발생한 환경 유형("client" 또는 "server")
차단 기준일치한 규칙: 파일 패턴, 지정자 패턴 또는 마커
가져오는 파일 / Import / 확인된 경로가져오는 파일(file:line:col 포함), 원시 import 문자열 및 확인된 대상 경로
추적진입점부터 차단된 import까지의 전체 import 체인입니다. 각 단계에는 file:line:col와 사용된 import 지정자가 표시됩니다. 1단계는 항상 진입점입니다
코드문제가 있는 줄의 > 마커와 정확한 열을 가리키는 ^ 캐럿이 포함된 소스 코드 조각
제안 사항방향(클라이언트 내 서버 코드 또는 서버 내 클라이언트 코드)에 맞춰 위반을 해결할 수 있는 실행 가능한 단계

추적은 진입점부터 차단된 모듈까지 위에서 아래로 읽습니다. 이를 통해 체인이 시작되는 위치를 찾아 코드를 재구성할 수 있습니다.

흔한 함정: 일부 Import가 계속 유지되는 이유

Start가 "해당 서버 전용 import를 제거했어야 한다"고 생각할 수 있습니다. 중요한 점은 이 작업이 Start 컴파일러에서 처리된다는 것입니다:

  1. 컴파일러는 현재 대상(클라이언트 또는 서버)에 맞게 환경별 _구현_을 다시 작성합니다.
  2. 이 컴파일 과정에서 코드를 가지치기하고 다시 작성된 후 사용되지 않게 된 import를 제거합니다.

실제로 컴파일러가 createServerFn() 핸들러를 클라이언트 RPC 스텁으로 교체할 때, 제거된 구현에서만 사용되던 서버 전용 import도 제거할 수 있습니다.

예시(클라이언트 빌드):

import { getUsers } from './db/queries.server'
import { createServerFn } from '@tanstack/react-start'

export const fetchUsers = createServerFn().handler(async () => {
return getUsers()
})

개념적으로 클라이언트 빌드 출력은 다음과 같이 됩니다(단순화됨):

import { createClientRpc } from '@tanstack/react-start/client-rpc'
import { createServerFn } from '@tanstack/react-start'

// Compiler replaces the handler with a client RPC stub.
// (The id is generated by the compiler; treat it as an opaque identifier.)
export const fetchUsers = TanStackStart.createServerFn({
method: 'GET',
}).handler(createClientRpc('sha256:deadbeef...'))

// The server-only import is removed by the compiler.

import가 컴파일 후에도 남는 코드로 "유출"되면 계속 활성 상태로 유지되며, import 보호 기능은 여전히 이를 표시합니다:

import { getUsers } from './db/queries.server'
import { createServerFn } from '@tanstack/react-start'

// This is fine -- the server implementation is removed for the client build
export const fetchUsers = createServerFn().handler(async () => {
return getUsers()
})

// This keeps the import alive in the client build
export function leakyHelper() {
return getUsers() // referenced outside server boundary
}

이 경우 leakyHelper이 어떤 형태이기를 원하는지에 따라 몇 가지 선택지가 있습니다:

옵션 A: 클라이언트 코드가 유출 코드를 실수로 import하지 못하도록 파일을 분리합니다

// src/users.server.ts
import { getUsers } from './db/queries.server'
import { createServerFn } from '@tanstack/react-start'

// Safe to import from client code (compiler rewrites the handler)
export const fetchUsers = createServerFn().handler(async () => {
return getUsers()
})
// src/users-leaky.server.ts
import { getUsers } from './db/queries.server'

// Server-only helper; do not import this from client code
export function leakyHelper() {
return getUsers()
}

옵션 B: 같은 파일에 유지하되 헬퍼를 createServerOnlyFn로 감쌉니다

헬퍼는 존재해야 하지만 클라이언트에서는 절대 실행되면 안 되는 경우에 유용합니다. 서버 전용 import가 createServerOnlyFn(() => ...) 콜백 내부에서만 참조되는지 확인합니다:

import { createServerOnlyFn } from '@tanstack/react-start'
import { getUsers } from './db/queries.server'

export const leakyHelper = createServerOnlyFn(() => {
return getUsers()
})

클라이언트에서 컴파일러 출력은 사실상 다음과 같습니다:

export const leakyHelper = () => {
throw new Error(
'createServerOnlyFn() functions can only be called on the server!',
)
}

createServerOnlyFn import가 사라졌으며, 컴파일 후 더 이상 참조되지 않으므로 서버 전용 getUsers import도 사라졌다는 점에 유의합니다.

동일한 개념이 createIsomorphicFn()에도 적용됩니다. 컴파일러는 대상이 아닌 구현을 제거하고 사용되지 않게 된 모든 항목을 가지치기합니다.

"컴파일 과정에서 제거"될 것으로 예상한 파일에서 import 보호 위반이 표시되면, 해당 import가 컴파일러가 인식하는 환경 경계 외부에서 참조되는지(또는 살아남은 코드로 인해 계속 유지되는지) 확인합니다.

오탐: 개발 모드와 빌드 모드

빌드 모드에서 import 보호는 트리 셰이킹이 끝날 때까지 위반 검사를 연기합니다. import가 최종 번들에서 제거되면 위반이 보고되지 않습니다. 빌드 시점의 위반은 확정적입니다. 즉, 빌드에서 위반으로 표시되면 해당 import가 실제로 살아남은 것입니다.

개발 모드에서는 트리 셰이킹이 불완전하거나 제공되지 않을 수 있으므로, 나중에 컴파일 과정에서 제거될 import에 대해 import 보호가 오탐을 보고할 수 있습니다.

개발 모드에서는 경고가 표시되지만 빌드 모드에서는 표시되지 않는다면, 일반적으로 안전하지 않은 연결이 트리 셰이킹 전에는 존재했지만 최종 번들에는 남지 않았음을 의미합니다. 그렇더라도 처음부터 해당 연결이 존재하지 않도록 import 구조를 변경하는 것이 좋습니다.

혼합 배럴과 분리된 진입점

배럴 자체가 반드시 오탐인 것은 아닙니다. 위험한 패턴은 동일한 진입점에서 안전한 export와 환경 제한이 적용된 export를 혼합하는 배럴입니다.

컴파일과 트리 셰이킹 후 무엇이 남는지에 따라 실제 위반 또는 개발 모드에서만 발생하는 오탐으로 나타날 수 있습니다. 더 안전한 구조는 안전한 export와 제한된 export를 별도의 진입점으로 분리하는 것입니다.

예를 들어 다음과 같은 혼합 배럴은 피합니다:

// src/lib/index.ts
export { fetchUsers } from './fetchUsers'
export { getDb } from './db.server'
// src/routes/users.tsx
import { fetchUsers } from '../lib'

클라이언트가 fetchUsers만 사용하더라도 해당 import 경로는 여전히 getDb을 다시 export하는 모듈을 거칩니다.

안전한 export와 서버 전용 export를 별도의 진입점으로 분리하는 것이 좋습니다:

// src/lib/index.ts
export { fetchUsers } from './fetchUsers'
// src/lib/server.ts
export { getDb } from './db.server'
// src/routes/users.tsx
import { fetchUsers } from '../lib'
// src/server/worker.ts
import { getDb } from '../lib/server'

마커로 보호된 파일(import '@tanstack/react-start/server-only')에도 동일하게 적용됩니다. 표시된 파일이 혼합 배럴을 통해 다시 export되지만 클라이언트 코드에서 전혀 사용되지 않는 경우, 프로덕션에서는 트리 셰이킹 후 경고가 억제될 수 있습니다. 하지만 더 나은 해결 방법은 해당 서버 전용 연결이 클라이언트에서 도달 가능한 코드에 전혀 노출되지 않도록 하는 것입니다.

onViolation 콜백

사용자 지정 보고를 수행하거나 판정을 재정의하도록 위반 처리에 연결할 수 있습니다:

importProtection: {
onViolation: async (info) => {
// info.env -- environment name (e.g. 'client', 'ssr', ...)
// info.envType -- 'client' or 'server'
// info.type -- 'specifier', 'file', or 'marker'
// info.specifier -- the raw import string
// info.importer -- absolute path of the importing file
// info.resolved -- absolute path of the resolved target (if available)
// info.trace -- array of { file, line?, column?, specifier? } objects
// info.snippet -- { lines, highlightLine, location } with the source code snippet (if available)

// Return false (or Promise<false>) to allow this specific import (override the denial)
if (info.specifier === 'some-special-case') {
return false
}
},
}

Import 보호 비활성화

Import 보호를 완전히 비활성화하려면 다음과 같이 설정합니다:

importProtection: {
enabled: false,
}

전체 설정 레퍼런스

interface ImportProtectionOptions {
enabled?: boolean
behavior?:
| 'error'
| 'mock'
| { dev?: 'error' | 'mock'; build?: 'error' | 'mock' }
log?: 'once' | 'always'
include?: Array<string | RegExp>
exclude?: Array<string | RegExp>
ignoreImporters?: Array<string | RegExp>
maxTraceDepth?: number
client?: {
specifiers?: Array<string | RegExp>
files?: Array<string | RegExp>
excludeFiles?: Array<string | RegExp>
}
server?: {
specifiers?: Array<string | RegExp>
files?: Array<string | RegExp>
excludeFiles?: Array<string | RegExp>
}
onViolation?: (
info: ViolationInfo,
) => boolean | void | Promise<boolean | void>
}
옵션타입기본값설명
enabledbooleantrue플러그인을 비활성화하려면 false로 설정합니다.
behaviorstring | object{ dev: 'mock', build: 'error' }위반 발생 시 수행할 작업
log'once' | 'always''once'반복되는 위반 사항의 중복을 제거할지 여부
includePattern[]Start의 srcDirectory이러한 패턴과 일치하는 importer만 검사합니다
excludePattern[][]이러한 패턴과 일치하는 importer를 건너뜁니다
ignoreImportersPattern[][]이러한 importer에서 발생한 위반 사항을 무시합니다
maxTraceDepthnumber20import 추적의 최대 깊이
clientobject위의 기본값 참조클라이언트 환경을 위한 추가 거부 규칙
client.specifiersPattern[]프레임워크 서버 지정자클라이언트 환경에서 거부되는 지정자 패턴(기본값에 추가됨)
client.filesPattern[]['**/*.server.*']클라이언트 환경에서 거부되는 파일 패턴(기본값을 대체함)
client.excludeFilesPattern[]['**/node_modules/**']이러한 패턴과 일치하는 확인된 파일은 확인된 대상 검사를 건너뜁니다(파일 패턴 + 마커)(기본값을 대체함)
serverobject위의 기본값 참조서버 환경을 위한 추가 거부 규칙
server.specifiersPattern[][]서버 환경에서 거부되는 지정자 패턴(기본값을 대체함. server.specifiers의 기본값은 []이므로 client.specifiers과 달리 추가 방식이 아님)
server.filesPattern[]['**/*.client.*']서버 환경에서 거부되는 파일 패턴(기본값을 대체함)
server.excludeFilesPattern[]['**/node_modules/**']이 패턴과 일치하는 해석된 파일은 해석된 대상 검사를 건너뜁니다(파일 패턴 + 마커)(기본값 대체)
onViolationfunctionundefined위반이 발생할 때마다 호출되는 콜백