
이런 경험 있으신가요?
vite dev로 개발하면서 기능을 추가했습니다.
잘 됩니다.
vite build로 빌드해서 배포했어요.
버튼 하나를 클릭하니까 이런 오류가 납니다.
ReferenceError: setPage is not defined
"방금 전까지 잘 됐는데?"
개발 환경에서는 수십 번 눌러봐도 정상이었던 버튼이 빌드 결과물에서는 오류를 냅니다.
콘솔 오류 위치를 따라가 보면 분명히 setPage 함수가 있는 파일인데 없다고 해요.

PLCLink Phase 5에서 헤더의 연결 상태 아이콘을 클릭하면 통신 진단 페이지로 이동하게 만들다가 이 오류를 만났습니다.
처음엔 "왜 개발에서는 됐지?"부터 시작해서 Vite 빌드 설정이 문제인 줄 알고 한참 다른 곳을 뒤졌어요.
원인은 훨씬 단순한 곳에 있었습니다.
용어 먼저 짚고 넘어갈게요

스코프(Scope)란
변수나 함수가 "어디서 보이는가"를 결정하는 범위입니다.
방 비유로 이해하면 쉽습니다. 내 방에서 선언한 물건은 내 방 안에서만 꺼낼 수 있어요.
옆방에 있는 사람은 내 방 물건을 직접 꺼낼 수 없습니다.
가져가려면 "이거 줘"라고 요청해야 해요.
React 컴포넌트란
화면의 한 부분을 담당하는 함수입니다.
App, Header, Sidebar 각각이 별도의 함수예요.
중요한 것은 같은 파일에 있어도 다른 함수면 스코프가 다릅니다.
useState와 setState란
React에서 상태를 저장하고 바꾸는 방법입니다.
const [page, setPage] = useState("home")처럼 쓰면, page는 현재 값이고 setPage는 값을 바꾸는 함수예요.
이 setPage는 선언된 함수 안에서만 직접 쓸 수 있습니다.
prop이란
부모 컴포넌트가 자식 컴포넌트에게 값을 건네주는 방법입니다.
"이 함수 써도 돼"라고 허락서와 함께 넘겨주는 것과 같아요.
버그가 생기는 구조

PLCLink의 상황이 이랬습니다.
// App.jsx
export default function App() {
const [page, setPage] = useState("home")
return (
<div>
<Header /> {/* setPage를 전달하지 않음 */}
<MainContent page={page} />
</div>
)
}
// Header (같은 파일 또는 별도 파일)
function Header() {
return (
<div onClick={() => setPage("diagnostics")}> {/* setPage 없음 */}
연결 상태
</div>
)
}
Header는 App과 별개의 함수입니다.
App 안에서 선언된 setPage는 App 함수의 스코프에 있어요.
Header 함수에서는 보이지 않습니다.
방 비유로 돌아오면, setPage는 App이라는 방에 있는 물건이고 Header는 옆방입니다.
옆방에서 허락도 없이 직접 꺼내려 한 거예요.
왜 개발 환경에서는 됐나

이게 가장 헷갈리는 부분입니다.
vite dev는 코드를 최소화하지 않고 빠른 빌드를 위해 다른 방식으로 번들링합니다.
이 과정에서 우연히 같은 스코프처럼 동작하는 경우가 생길 수 있어요.
vite build는 프로덕션용으로 코드를 최적화하고 압축합니다.
이 과정에서 스코프 분리가 엄격하게 적용되면서 setPage가 Header 스코프에 정말로 없다는 게 드러납니다.
즉, 개발 환경이 "이런 코드도 동작한다"라고 잘못된 안도감을 줬던 겁니다.
이 유형의 버그가 특히 까다로운 이유예요.
개발 중에는 오류가 없으니까 배포하고 나서야 알게 됩니다.
해결 : prop으로 전달

App이 Header에게 setPage의 역할을 하는 함수를 prop으로 건네줍니다.
// App.jsx
export default function App() {
const [page, setPage] = useState("home")
return (
<div>
<Header
onNavDiagnostics={() => setPage("diagnostics")}
/>
<MainContent page={page} />
</div>
)
}
// Header
function Header({ onNavDiagnostics }) {
return (
<div
onClick={() => onNavDiagnostics && onNavDiagnostics()}
>
연결 상태
</div>
)
}
App이 onNavDiagnostics라는 이름으로 함수를 내려줍니다.
Header는 자신의 prop에서 받아서 씁니다.
이제 setPage를 직접 참조하지 않아요.
onNavDiagnostics && onNavDiagnostics()는 prop이 없을 때를 대비한 방어 코드입니다.
prop 없이 Header를 단독으로 테스트하거나 쓸 때 오류가 나지 않아요.
같은 실수가 반복되는 패턴

이 오류는 이런 상황에서 자주 생깁니다.
같은 파일 안에 여러 컴포넌트를 쓸 때요.
// 같은 파일이어도 다른 함수면 스코프가 다름
function App() {
const [count, setCount] = useState(0)
return <Counter />
}
function Counter() {
return (
<button onClick={() => setCount(c => c + 1)}> {/* 없음 */}
{count} {/* 이것도 없음 */}
</button>
)
}
"같은 파일이니까 당연히 보이지 않나?"라는 생각이 드는데, 다른 함수라면 스코프가 다릅니다.
해결 패턴은 항상 같습니다.
function App() {
const [count, setCount] = useState(0)
return (
<Counter
count={count}
onIncrement={() => setCount(c => c + 1)}
/>
)
}
function Counter({ count, onIncrement }) {
return (
<button onClick={onIncrement}>{count}</button>
)
}
자식이 필요한 것을 부모가 prop으로 내려줍니다.
이 원칙 하나만 기억하면 스코프 오류는 나지 않아요.
prop drilling이 너무 깊어진다면
App → Layout → Header → Button처럼 계층이 깊어지면, 중간 단계마다 prop을 계속 내려줘야 합니다.
이를 prop drilling이라고 해요.
Layout은 setPage를 쓰지도 않는데 Header에 전달하기 위해서만 받아야 하는 상황이 생기죠.
이 경우에는 React Context나 Zustand 같은 상태 관리 라이브러리를 씁니다.
const PageContext = createContext()
function App() {
const [page, setPage] = useState("home")
return (
<PageContext.Provider value={{ page, setPage }}>
<Layout /> {/* 중간 계층이 prop을 받아 전달할 필요 없음 */}
</PageContext.Provider>
)
}
function Header() {
const { setPage } = useContext(PageContext) // 어디서든 접근 가능
return (
<div onClick={() => setPage("diagnostics")}>연결 상태</div>
)
}
PLCLink는 이미 useApp() 커스텀 훅으로 앱 전체 상태를 관리하고 있어서, Header에도 동일하게 적용했습니다. setPage 대신 useApp().navigate("diagnostics") 형태로요.

마치며
이 버그를 만난 날 반나절을 Vite 빌드 설정 파일을 뒤졌습니다.
"빌드에서만 나는 오류니까 빌드 설정 문제겠지"라고 생각했거든요.
원인이 컴포넌트 스코프라는 걸 알았을 때 허탈했어요.
코드 두 줄로 해결됐으니까요.
비슷한 상황을 만나면 Vite 설정보다 먼저 컴포넌트 스코프를 확인해보세요.
"개발에서 됐는데 빌드에서 안 된다"는 현상이 오히려 "개발 환경이 잘못 통과시켜 준 것"일 수 있습니다.
그리고 기능을 만들 때마다 vite build로 한 번씩 확인하는 습관을 들이면 이런 버그를 배포 전에 잡을 수 있어요.
'(개인Project)_개발 > PLC-PC 연결' 카테고리의 다른 글
| [Program][보족] 현장 운영자가 볼 로그와 개발자가 볼 로그를 나눈 이유 (1) | 2026.06.13 |
|---|---|
| [Program][보족] FastAPI 서버를 백그라운드로 돌리면서 트레이 아이콘까지 만드는 방법 (0) | 2026.06.12 |
| [Program][보족] PyInstaller EXE가 콘솔 없이 에러 메시지도 없이 꺼진다 : 초기화 로그로 디버깅 (0) | 2026.06.11 |
| [Program][Phase5] "Python 설치해야 해요?" 라는 질문이 없어지는 날 : PLCLink 데스크탑 앱 제작기 (1) | 2026.06.10 |
| [Program][보족] Python OPC-UA 클라이언트 구현 삽질기 : browse path, NodeId, VariantType (0) | 2026.06.08 |