- Published on
TS 5.5+ - noUncheckedSideEffectImports 해결 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서드파티 라이브러리나 레거시 코드베이스를 TypeScript 5.5 이상으로 올리면, 갑자기 import 라인에서 경고나 에러가 쏟아지는 경우가 있습니다. 그중에서도 noUncheckedSideEffectImports는 “사이드 이펙트만 기대하는 import”를 더 엄격하게 다루도록 만든 옵션이라, 기존에 암묵적으로 동작하던 초기화 코드나 폴리필 로딩 방식이 한꺼번에 문제로 드러납니다.
이 글에서는 noUncheckedSideEffectImports가 정확히 무엇을 막는지, 어떤 패턴이 위험한지, 그리고 실제로 팀 코드에 적용할 때 어떤 순서로 고치면 충돌을 최소화할 수 있는지 정리합니다.
또한 TS 5.5 마이그레이션 과정에서 함께 자주 엮이는 설정 이슈는 별도 글인 TS 5.5 useDefineForClassFields로 this undefined 해결도 참고하면 좋습니다.
noUncheckedSideEffectImports는 무엇을 해결하려 하나
자바스크립트에서 다음과 같은 import는 값 바인딩을 가져오지 않고, “모듈을 실행하기 위해서만” 로딩합니다.
- 폴리필 로딩:
import "core-js/stable" - 전역 초기화:
import "./init" - CSS 로딩(번들러 의존):
import "./styles.css" - 라이브러리 전역 패치:
import "reflect-metadata"
이런 패턴은 편리하지만, 문제가 생겼을 때 추적이 어렵습니다.
- 실제로는 아무 효과가 없는데도 남아 있을 수 있음(죽은 코드)
- 트리 셰이킹이 예상과 다르게 동작할 수 있음
- 타입 시스템이 “이 import가 왜 필요한지”를 검증하기 어려움
- 특정 빌드 타겟(SSR, 테스트, 워커 등)에서만 런타임 부작용이 터짐
noUncheckedSideEffectImports는 이런 “의도가 불명확한 사이드 이펙트 import”를 더 보수적으로 취급해서, 코드베이스가 모듈 경계를 명확히 유지하도록 유도합니다.
언제 문제가 터지나: 대표적인 실패 패턴
1) 타입만 쓰는데 값 import로 가져오는 경우
타입만 필요한데 값 import로 가져오면 번들러는 해당 모듈을 런타임에 포함시킬 수 있고, 그 모듈의 사이드 이펙트가 실행될 수 있습니다.
// before
import { User } from "./types";
export function greet(u: User) {
return `hello ${u.name}`;
}
위 코드는 TS 설정에 따라 ./types가 런타임에 import 되어버릴 수 있습니다. 해결은 type import로 의도를 명확히 하는 것입니다.
// after
import type { User } from "./types";
export function greet(u: User) {
return `hello ${u.name}`;
}
2) “실행만 필요”한 import가 여기저기 흩어져 있는 경우
예를 들어 전역 초기화가 여러 엔트리포인트에서 중복 로딩되면, 개발 환경에서는 우연히 동작하지만 프로덕션 번들 최적화나 코드 스플리팅에서 순서가 바뀌며 깨질 수 있습니다.
// a.ts
import "./init";
// b.ts
import "./init";
이런 경우 init를 “명시적 초기화 함수”로 바꾸고, 앱 엔트리에서 한 번만 호출하는 구조가 더 안전합니다.
// init.ts
export function initApp() {
// 전역 이벤트 바인딩, 설정 로딩 등
}
// main.ts
import { initApp } from "./init";
initApp();
3) CSS import 같은 번들러 전용 사이드 이펙트
import "./styles.css";
이 패턴은 Vite, Webpack 같은 환경에서는 자연스럽지만, Node.js 런타임 실행(테스트, 스크립트, SSR)에서는 바로 터질 수 있습니다. 해결책은 “런타임 JS 모듈”과 “번들러 엔트리”를 분리하는 것입니다.
main.ts(런타임 진입점)에서는 CSS를 import 하지 않음main.client.ts또는entry.browser.ts같은 브라우저 전용 엔트리에서만 CSS import
// entry.browser.ts
import "./styles.css";
import { bootstrap } from "./bootstrap";
bootstrap();
TS 5.5 이상에서의 권장 해결 전략(우선순위)
현업에서는 옵션을 켰다가 한 번에 다 고치려 하면 충돌이 큽니다. 다음 우선순위로 접근하면 리스크를 줄일 수 있습니다.
1) 먼저 type import 정리로 노이즈를 줄인다
가장 비용 대비 효과가 큰 작업입니다.
import { Foo } from "..."인데Foo가 타입으로만 쓰이면import type로 변경isolatedModules나verbatimModuleSyntax를 함께 쓰는 프로젝트라면 특히 중요
대규모 코드에서는 ESLint 룰로 자동화하는 편이 좋습니다.
// eslint 예시(개념)
// @typescript-eslint/consistent-type-imports: "error"
2) 사이드 이펙트 import를 “엔트리포인트로 모은다”
전역 초기화, 폴리필, 메트릭 설정 같은 코드는 “앱이 시작할 때 한 번만 실행”되어야 합니다. 따라서 다음처럼 구조를 정리합니다.
src/entry.ts: 사이드 이펙트 import 허용 구역src/lib/*: 순수 모듈(사이드 이펙트 최소화)
// src/entry.ts
import "./polyfills";
import "./observability/setup";
import { start } from "./app";
start();
이렇게 하면 noUncheckedSideEffectImports가 의미하는 바(사이드 이펙트가 통제된 위치에 존재)를 코드 구조로 보장할 수 있습니다.
3) “사이드 이펙트 모듈”을 명시적 API로 바꾼다
사이드 이펙트를 import에 숨기지 말고, 호출로 드러내는 것이 유지보수에 유리합니다.
// before
import "./register";
// after
import { register } from "./register";
register();
이 방식의 장점은 다음과 같습니다.
- 호출 순서를 코드로 표현 가능
- 테스트에서 특정 초기화를 끄기 쉬움
- dead code 제거가 쉬움
4) 정말 필요한 전역 패치라면 “왜 필요한지”를 주석으로 고정
reflect-metadata나 특정 런타임 패치처럼, import 자체가 의도인 경우도 있습니다. 이 경우에는 “어디에서, 왜 필요한지”를 문서화해두지 않으면 나중에 삭제되며 장애로 이어집니다.
// entry.ts
// NOTE: 데코레이터 기반 DI 컨테이너가 런타임 메타데이터를 요구함
import "reflect-metadata";
운영 장애 대응 관점에서 “원인 추적이 가능한 힌트”를 남기는 습관은 중요합니다. 비슷한 맥락으로 장애 원인 추적을 체계화하는 방법은 Rust Tokio에서 thread panicked 원인 추적법 같은 글의 접근도 참고할 만합니다.
tsconfig.json에서 함께 점검할 옵션들
noUncheckedSideEffectImports는 단독으로만 움직이지 않습니다. 아래 옵션들과 결합될 때 체감 변화가 커집니다.
module및moduleResolutionverbatimModuleSyntaximportsNotUsedAsValuesisolatedModules
예시 구성입니다(프로젝트 상황에 맞게 조정 필요).
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"verbatimModuleSyntax": true,
"noUncheckedSideEffectImports": true
}
}
verbatimModuleSyntax를 켜면 타입 import와 값 import의 구분이 더 중요해집니다.moduleResolution이Bundler인지NodeNext인지에 따라 해석이 달라질 수 있으니, 번들러/런타임 환경과 맞춰야 합니다.
실전 리팩터링 체크리스트
대규모 레포에서 noUncheckedSideEffectImports를 켰을 때, 다음 순서로 진행하면 롤백 비용을 줄일 수 있습니다.
import type자동 변환(린트/코드모드)으로 1차 정리- 사이드 이펙트 import 목록을 검색해서 파일별로 분류
- 폴리필
- 전역 초기화
- 스타일/에셋
- 런타임 패치
- 엔트리포인트로 이동 가능한 것은 이동
- 이동 불가능한 것은 명시적 함수 호출로 변경
- 테스트 환경에서만 필요한 초기화는 테스트 setup 파일로 격리
CI에서 이런 류의 설정 변경은 캐시나 빌드 산출물 차이로 증상이 가려지기도 합니다. TS 버전 업과 설정 변경을 동시에 할 때 캐시 미스/히트가 결과를 바꾸는 경우가 있어, 필요하면 GitHub Actions 캐시 미적중 원인 - key·restore-keys·락파일처럼 캐시 전략도 같이 점검하는 편이 안전합니다.
자주 묻는 케이스별 처방
Q1. import "./init"를 없애면 앱이 동작하지 않는다
그렇다면 ./init는 실제로 중요한 전역 초기화를 하고 있는 것입니다. 해결은 “없애는 것”이 아니라 “위치를 통제”하거나 “API화”하는 것입니다.
- 엔트리포인트에서만 import
- 또는
init()함수로 바꾸고 호출
Q2. 테스트에서만 깨진다
테스트 러너는 브라우저 번들러가 아니기 때문에, CSS/에셋 import나 특정 전역 패치가 실패할 수 있습니다.
- 테스트 전용 setup 파일로 이동
- 모킹(예: CSS 모듈 스텁)
- 런타임과 번들러 엔트리를 분리
Q3. SSR에서만 깨진다
SSR은 Node.js에서 실행되므로 브라우저 전용 사이드 이펙트(예: window 접근, CSS import)가 그대로 터집니다.
entry.server.ts와entry.client.ts분리- 브라우저 전용 초기화는 클라이언트 엔트리로 이동
마무리: 이 옵션을 켜야 하는 이유
noUncheckedSideEffectImports는 단순히 “불편한 규칙”이 아니라, 모듈 로딩 순서/환경 차이에서 발생하는 잠복 버그를 조기에 드러내는 장치입니다. 특히 다음 조건에 해당하면 도입 가치가 큽니다.
- 코드 스플리팅, 동적 import를 적극 사용
- SSR과 CSR을 함께 운영
- 테스트/스크립트/배치 등 다양한 실행 환경이 공존
- 레거시 전역 초기화가 많아 의존성이 불명확
결국 목표는 사이드 이펙트를 없애는 것이 아니라, “어디에서 발생하는지 통제하고, 의도를 코드로 표현하는 것”입니다. 위에서 제시한 import type 정리, 엔트리포인트 집중, API화 리팩터링을 순서대로 적용하면 TS 5.5 이상에서도 안정적으로 마이그레이션할 수 있습니다.