
이 글이 나한테 해당되는지 먼저 확인해보세요
이런 상황이 하나라도 해당된다면, 이 글이 도움이 될 겁니다.
npm run dev에서는 아무 문제 없는데npm run build후 실행하면 오류가 난다Cannot access 'je' before initialization같은 minified 변수명으로 된 오류가 난다- React에서
useCallback또는useMemo를 쓰는데 프로덕션에서만 이상한 오류가 난다 - 개발 모드와 프로덕션 빌드의 동작이 왜 다른지 이해가 안 된다
- 오류 메시지를 봐도 어떤 변수인지 특정을 못 해서 디버깅이 막막하다
처음엔 배포 환경 설정 문제라고 생각했습니다.
환경 변수, API 경로, CORS 설정을 전부 확인했는데 아무것도 아니었어요.
원인은 JavaScript의 const와 번들러의 동작 방식 차이였습니다.
용어 먼저 짚고 넘어갈게요

TDZ (Temporal Dead Zone)란
let과 const로 선언한 변수는 선언 전에 접근하면 오류가 납니다.
선언문이 실행되기 전까지 그 변수는 "일시적 사각지대(TDZ)"에 있다고 합니다.
console.log(x) // ReferenceError: Cannot access 'x' before initialization
const x = 1
var와 달리 const는 선언 전에 접근할 수 없습니다.
이게 JavaScript 명세에 정의된 동작입니다.
Babel이란
JavaScript 코드를 변환해주는 도구입니다.
최신 문법을 구형 브라우저에서도 동작하는 코드로 바꿔줘요.
개발 모드(Vite의 dev server)에서 Babel이 const를 var로 변환하는 경우가 있습니다.
Rollup이란
프로덕션 빌드에서 파일을 묶어주는 번들러입니다.
Vite는 개발 모드와 프로덕션 빌드에서 서로 다른 도구를 씁니다.
개발 모드는 Babel(또는 esbuild), 프로덕션 빌드는 Rollup이에요.
두 도구가 const의 TDZ를 다르게 처리합니다.
useCallback이란
React에서 함수를 메모이제이션하는 훅입니다.
함수와 의존성 배열을 받아서, 의존성이 바뀔 때만 새 함수를 만들어요.
두 번째 인자인 의존성 배열이 이 버그의 핵심입니다.
어떤 증상이었나

PLCLink Phase 3 알람 매뉴얼 기능을 완성하고 프로덕션 빌드로 테스트했습니다.
npm run dev에서는 알람 매뉴얼 업로드, 이미지 표시, 팝업 동작까지 전부 정상이었어요.
그런데 npm run build 후 실행하면 알람 페이지에서 오류가 났습니다.
Cannot access 'je' before initialization
at AlarmPage (AlarmPage.jsx:1:1)
je가 뭔지 알 수가 없습니다.
코드 어디에도 je라는 변수가 없어요.
프로덕션 빌드에서 모든 변수명이 짧게 축약되기 때문에,
빌드된 코드에서 je가 원래 코드의 어떤 변수인지 바로 파악이 안 됩니다.
가장 당황스러운 버그였습니다.
원인 찾기 : minified 변수명을 역추적하는 방법

Source Map을 활성화하면 프로덕션 빌드에서도 원래 변수명으로 오류를 볼 수 있습니다.
vite.config.js에서 빌드 시 source map을 켭니다.
// vite.config.js
export default defineConfig({
build: {
sourcemap: true // 프로덕션 빌드에 source map 포함
}
})
이 상태로 다시 빌드하면 오류가 이렇게 바뀝니다.
Cannot access 'safeAlarms' before initialization
at AlarmPage.jsx:247:18
je가 safeAlarms였습니다. 247번째 줄을 확인했습니다.
원인 코드

247번째 줄 근처 코드입니다.
// AlarmPage.jsx
const handleUpload = useCallback((file, ruleId) => {
const targetAlarm = safeAlarms.find(a => a.id === ruleId) // safeAlarms 사용
if (!targetAlarm) return
uploadManualImage(file, ruleId)
}, [safeAlarms, editingRuleId]) // 의존성 배열에 safeAlarms 포함
// ... 중간에 다른 코드 200줄 ...
// 247번째 줄
const safeAlarms = Array.isArray(alarms) ? alarms : [] // safeAlarms 선언
useCallback이 먼저 선언되어 있고, 그 의존성 배열에 safeAlarms가 있는데, safeAlarms는 200줄 아래에서 선언됩니다.
왜 개발 모드에서는 괜찮았나 : Babel이 const를 var로 바꾼다

이게 핵심입니다.
Babel은 const를 하위 호환성을 위해 var로 변환할 수 있습니다.
var는 TDZ가 없어요. 선언 전에 접근해도 undefined가 반환됩니다.
// 원본 코드
const safeAlarms = Array.isArray(alarms) ? alarms : []
// Babel 변환 후 (var로)
var safeAlarms = Array.isArray(alarms) ? alarms : []
var는 함수 스코프 안에서 호이스팅됩니다.
선언이 어디에 있든 함수 시작 시점에 undefined로 초기화되어 있어요.
그래서 useCallback 의존성 배열에서 safeAlarms를 참조해도 오류가 나지 않습니다.
값이 undefined일 뿐이에요.
Rollup은 const를 그대로 유지합니다.
const는 선언 전에 접근하면 TDZ 오류가 납니다.
프로덕션 빌드는 Rollup으로 하기 때문에 이 오류가 발생합니다.
개발 모드 (Babel 처리)
const → var 변환
var는 TDZ 없음
선언 전 접근 = undefined
오류 안 남
프로덕션 빌드 (Rollup 처리)
const 그대로 유지
const는 TDZ 있음
선언 전 접근 = ReferenceError
오류 남
두 환경이 다르게 동작하는 이유가 여기 있습니다.
의존성 배열이 "즉시 평가"된다는 게 무슨 뜻인가
useCallback을 이해할 때 중요한 포인트입니다.
const handleUpload = useCallback(
() => { ... },
[safeAlarms, editingRuleId] // 이 배열은 useCallback 호출 시 즉시 평가됨
)
두 번째 인자인 의존성 배열은 useCallback을 호출하는 시점에 평가됩니다.
배열 안의 변수들이 이 시점에 이미 선언되어 있어야 합니다.
safeAlarms가 useCallback보다 아래에 선언되어 있으면, useCallback이 실행되는 시점에 safeAlarms는 아직 TDZ 상태입니다.
Rollup 빌드에서는 이게 오류로 이어집니다.
해결 방법 : 선언 순서를 정리한다

간단합니다. useCallback 의존성 배열에 있는 변수들은 반드시 useCallback보다 먼저 선언되어 있어야 합니다.
// 수정 전 : 위험한 패턴
const handleUpload = useCallback((file, ruleId) => {
const targetAlarm = safeAlarms.find(a => a.id === ruleId)
...
}, [safeAlarms, editingRuleId])
// ... 200줄 ...
const safeAlarms = Array.isArray(alarms) ? alarms : [] // 나중에 선언
const [editingRuleId, setEditingRuleId] = useState(null) // 나중에 선언
// 수정 후 : 안전한 패턴
const safeAlarms = Array.isArray(alarms) ? alarms : [] // 먼저 선언
const [editingRuleId, setEditingRuleId] = useState(null) // 먼저 선언
// ... 이제 안전하게 참조 가능 ...
const handleUpload = useCallback((file, ruleId) => {
const targetAlarm = safeAlarms.find(a => a.id === ruleId)
...
}, [safeAlarms, editingRuleId])
같은 패턴이 이 파일에서 두 번 발생했습니다.
두 곳 모두 수정했습니다.
이 버그가 생기기 쉬운 이유
React 컴포넌트 파일은 길어지기 쉽습니다.
이벤트 핸들러가 많은 복잡한 페이지라면 500~700줄이 되는 경우도 있어요.
코드가 길어지면 이런 상황이 생깁니다.
useCallback을 위쪽에 작성하고, 나중에 필요한 state나 변수를 아래쪽에 추가합니다.
그때 의존성 배열에 그 변수를 추가했는데 선언 위치는 신경 쓰지 않는 거예요.
개발 모드에서는 Babel이 const를 var로 바꿔서 오류가 숨겨집니다.
프로덕션 빌드를 해보기 전까지는 이 문제가 있다는 걸 알 수가 없어요.
프로덕션 빌드를 주기적으로 테스트해야 하는 이유
이 버그를 발견하기 전까지 npm run dev에서만 테스트했습니다.
"개발 모드에서 되면 프로덕션에서도 되겠지"라는 가정이 있었어요.
그게 틀렸다는 걸 알았습니다.
개발 모드와 프로덕션 빌드는 다른 도구로 처리됩니다.
Babel과 Rollup의 const 처리 방식이 다른 것처럼, 다른 부분에서도 동작 차이가 생길 수 있습니다.
기능을 완성했을 때, 혹은 기능 추가 후 PR 전에 npm run build && npm run preview로 프로덕션 빌드를 직접 실행해서 테스트하는 게 맞습니다.
개발 모드에서 문제없다고 배포하면, 이 버그처럼 운영 환경에서 터지는 경우가 생깁니다.
간단히 확인하는 방법
useCallback 또는 useMemo를 쓸 때, 의존성 배열을 보고 아래 체크를 합니다.
의존성 배열에 있는 각 변수가
이 useCallback/useMemo보다 위에 선언되어 있는가?
YES → 안전
NO → 선언 순서 수정 필요
ESLint의 react-hooks/exhaustive-deps 규칙은 의존성 누락을 잡아주는데, 선언 순서 문제는 잡아주지 않습니다.
이건 직접 확인해야 합니다.
정리

| 항목 | 내용 |
|---|---|
| 증상 | 개발 모드 정상, 프로덕션 빌드에서 Cannot access 'xx' before initialization |
| 원인 | useCallback 의존성 배열 변수가 선언 전에 참조됨 (TDZ) |
| 왜 개발 모드에서 안 나는가 | Babel이 const를 var로 변환해서 TDZ를 숨김 |
| 왜 프로덕션에서 나는가 | Rollup은 const를 그대로 유지해서 TDZ 오류 발생 |
| 해결 | useCallback보다 먼저 의존성 변수를 선언 |
| 예방 | 기능 완성 후 프로덕션 빌드로 직접 테스트 |
마치며
"개발 모드에서는 됐는데"라는 말이 "프로덕션에서도 된다"는 보장이 아니라는 걸 알았습니다.
이 버그를 처음 만났을 때 오류 메시지가 Cannot access 'je' before initialization이었어요.
je가 뭔지 몰라서 어디서부터 시작해야 할지도 막막했습니다.
source map을 켜서 원래 변수명을 찾고, 그 변수의 선언 위치와 사용 위치를 비교하고, Babel과 Rollup의 동작 차이를 이해하고 나서야 원인이 보였어요.
같은 증상을 겪고 있다면, source map부터 켜보세요.
그게 시작입니다.
'(개인Project)_개발 > PLC-PC 연결' 카테고리의 다른 글
| [Program][Phase3.5] 브라우저가 HMI가 됐다 : PLCLink Phase 3.5 캔버스 HMI 완성기 (0) | 2026.05.31 |
|---|---|
| [Program][보족] 실제 하드웨어로 테스트하기 전까지 발견 못 한 버그 : SLMP 디바이스 주소 오파싱 (0) | 2026.05.29 |
| [Program][보족] PDF 매뉴얼을 알람 팝업에 연결하기까지 (3차 시도와 최종 해결) (0) | 2026.05.27 |
| [Program][보족] 래더가 내 값을 덮어쓴다 (PLC 스캔 사이클을 이해해야 SLMP 쓰기가 보인다) (0) | 2026.05.26 |
| [Program][Phase 3] 알람이 뜨면 매뉴얼이 바로 열린다 (PLCLink Phase 3 완성기) (0) | 2026.05.25 |