TechFeedTechFeed
Frontend

React invalid hook call, 훅 규칙, 중복 리액트 | 패키지가 리액트를 두 개 들고 있으면?

Invalid hook call이 패키지 쪽에서 나면 앱과 react-dom이 서로 다른 리액트 파일을 읽고 있는 상태다. npm ls react로 트리를 보고, npm link·별칭·overrides로 한 복사본에 맞춘다. 훅을 조건 안에 넣은 줄과 자리를 나눈다. React, Next.js, webpack, 피어 의존성, 프론트엔드, 개발자 기준. 2026년 9월 리액트·npm 공식 문서.

by

Invalid hook call에서 패키지가 리액트를 두 개 들고 있으면, 앱 코드와 react-dom이 서로 다른 react 파일을 읽고 있는 상태입니다. 훅을 조건 안에 넣었는지부터 고치지 말고, npm ls react로 트리가 갈라졌는지부터 보세요.


넥스트 앱에 로컬 UI 패키지를 npm link로 붙이면 개발 서버는 켜지는데, 버튼을 누르는 순간 콘솔이 빨개집니다. 훅 코드는 멀쩡하고, 패키지가 자기 node_modules/react를 따로 들고 있을 때 자주 납니다.


트리가 두 갈래면 번들러 별칭이나 피어 의존성으로 한 복사본에 맞춥니다. 근거는 리액트 공식 경고축소 빌드 오류 321에 있습니다.


두 개면 훅 규칙보다 트리를 먼저 본다

콘솔 세 줄 중 3번이 중복 리액트입니다. 훅 순서부터 고치지 마세요.


리액트는 이 경고를 세 갈래로 안내합니다. 훅 규칙을 어긴 경우, 리액트와 렌더러 버전이 어긋난 경우, 같은 앱에 리액트 복사본이 둘 이상인 경우입니다. 세 번째가 이 글의 칸입니다. 훅을 if 안에 넣었을 때와 콘솔 문장은 같아 보여도, 고치는 자리가 완전히 다릅니다.


훅이 동작하려면 앱에서 불러온 reactreact-dom 안쪽에서 불러온 react가 같은 모듈이어야 합니다. 두 경로가 다른 파일을 가리키면, 훅 디스패처가 비어 있어 함수 컴포넌트 밖처럼 보입니다. 그래서 메시지가 "함수 컴포넌트 본문에서만 호출하라"로 나옵니다. 조건문 버그가 아닌데도 그 문장이 뜹니다.


제가 쓰는 넥스트 사이트에 형제 폴더의 UI 패키지를 붙일 때도 같은 줄이 났습니다. 패키지 쪽 useState는 멀쩡했고, 앱과 패키지가 각자 리액트를 들고 있었습니다. 프로덕션 축소 빌드에서는 같은 내용이 오류 321로만 찍히니, 개발 서버에서 전체 문장을 먼저 읽는 게 빠릅니다.


먼저 기억할 것 | 콘솔에 "more than one copy of React"가 있으면 훅 코드를 고치지 않습니다. npm ls react로 경로가 둘인지부터 확인하세요.


npm ls react로 갈라진 복사본을 찾는다

같은 버전이 두 경로면 이미 두 복사본입니다. 숫자만 같다고 한 개가 아닙니다.


공식 문서가 시키는 첫 명령은 앱 폴더에서 npm ls react입니다. 트리가 한 줄이고 루트 node_modules/react만 가리키면 중복이 아닙니다. 아래에 다른 패키지가 자기 경로의 리액트를 또 들고 있으면, 버전이 같아도 모듈 객체가 둘입니다.


화면에 보이는 것다음 행동
루트에 react 한 줄만복사본은 하나훅 규칙·eslint로 자리를 옮긴다
패키지 아래에 react가 또 있음의존이 피어가 아니라 일반 의존그 패키지 package.json을 연다
버전이 18과 19로 갈림렌더러와 앱이 다른 메이저react와 react-dom을 같은 줄로 맞춘다
deduped만 찍힘설치기는 한 칸으로 묶었다고 봄번들러가 심볼릭 링크를 따라가는지 본다

npm ls react-dom도 같이 보세요. 훅을 지원하지 않는 옛 렌더러는 이 경고의 두 번째 갈래입니다. 지금은 거의 16.8 미만이 남아 있지 않지만, 모노레포에서 한 패키지만 옛 버전을 고정한 경우는 아직 있습니다. 명령 해석은 npm ls 문서를 따릅니다.


복사본이 둘인지 확인
# 앱 루트에서 npm ls react npm ls react-dom # 트리가 두 갈래면 경로가 다르게 찍힌다 # my-ui-kit@0.1.0 # └── react@19.0.0 ← 앱과 다른 폴더
패키지 트리가 갈라져 리액트 복사본이 두 개가 된 개발 환경
npm ls react에 경로가 둘이면 훅 코드보다 의존 트리를 먼저 고친다

링크된 패키지가 자기 리액트를 들고 있으면 번들러가 둘 다 봅니다.


공식 경고 문서가 따로 짚는 장면이 npm link입니다. 앱 폴더와 라이브러리 폴더가 형제일 때, 라이브러리를 링크하면 그 안의 node_modules/react가 그대로 따라옵니다. 앱은 자기 복사본을, 라이브러리는 자기 복사본을 불러서, 화면은 뜨는데 훅만 깨집니다.


문서가 제안하는 우회는 라이브러리 쪽에서 앱의 리액트를 다시 링크하는 것입니다. 형제 폴더가 myappmylib이면, mylib에서 npm link ../myapp/node_modules/react를 실행합니다. 이렇게 하면 라이브러리가 앱과 같은 파일을 봅니다. 워크스페이스로 올리면 설치기가 루트로 끌어올려 한 칸만 남기는 경우가 많습니다.


한국에서 1인으로 넥스트 여러 사이트를 돌리면, 공통 헤더를 로컬 패키지로 빼는 일이 잦습니다. 그때 패키지 dependencies에 리액트를 넣어 두면 링크하는 순간 두 벌이 됩니다. 패키지에는 피어로만 적고, 앱이 버전을 책임지는 쪽이 맞습니다. 설치 로그가 초록이어도 런타임 훅만 깨질 수 있으니, 링크한 직후 버튼을 한 번 눌러 콘솔을 확인하세요.


공식 문서의 동일 객체 확인
// node_modules/react-dom/index.js 맨 위에 window.React1 = require('react'); // 내 컴포넌트 파일 require('react-dom'); window.React2 = require('react'); console.log(window.React1 === window.React2); // false면 복사본이 둘이다

주의 | window.React1 === window.React2false면 훅 규칙을 고친 커밋은 원인과 무관합니다. 개발 서버를 끈 뒤 링크와 별칭부터 다시 보세요.


번들러 별칭으로 한 복사본에 묶는다

resolve.alias로 앱의 node_modules/react에 고정합니다. 심볼릭 링크를 따라가면 다시 갈라집니다.


웹팩은 resolve.alias가 다른 해석보다 앞섭니다. react 요청을 앱 루트의 node_modules/react 절대 경로로 고정하면, 링크된 패키지도 그 파일을 씁니다. 넥스트는 next.config.jswebpack 함수에서 같은 별칭을 넣습니다. 웹팩 기본값은 심볼릭 링크를 실제 경로로 풀어 npm link와 충돌하니, 별칭을 걸었는데도 둘이면 resolve.symlinks도 같이 보세요.


수단언제한계
webpack resolve.alias로컬 패키지·링크 직후설정 파일을 고친 뒤 개발 서버 재시작
넥스트 webpack 훅앱 라우터·페이지 라우터 공통서버 컴포넌트 경로는 별도로 확인
npm link 앱의 react형제 폴더 라이브러리폴더를 옮기면 링크가 끊김
워크스페이스 끌어올리기한 저장소 여러 패키지피어를 일반 의존으로 두면 또 갈림

별칭은 우회입니다. 라이브러리가 리액트를 일반 의존으로 들고 있으면, 설치할 때마다 트리가 다시 갈라질 수 있습니다. 별칭으로 화면을 살린 뒤에는 피어 칸을 고치는 커밋을 남기세요. 근거는 웹팩 resolve.alias넥스트 webpack 설정입니다.


넥스트에서 리액트를 한 경로로 고정
const path = require('path'); /** @type {import('next').NextConfig} */ const nextConfig = { webpack: (config) => { config.resolve.alias = { ...config.resolve.alias, react: path.resolve(__dirname, 'node_modules/react'), 'react-dom': path.resolve(__dirname, 'node_modules/react-dom'), }; return config; }, }; module.exports = nextConfig;
번들러가 리액트 요청을 앱 폴더 한 경로로 묶는 설정 장면
별칭은 앱의 node_modules/react로 고정하고, 고친 뒤에는 개발 서버를 다시 켠다

피어 의존성과 overrides로 트리를 고친다

라이브러리는 peerDependencies, 앱은 overrides로 한 버전을 강제합니다.


UI 패키지가 리액트를 dependencies에 넣으면 설치기가 패키지 전용 복사본을 만듭니다. 올바른 자리는 peerDependencies입니다. 호스트 앱이 리액트를 제공하고, 패키지는 그 복사본을 쓰겠다는 뜻입니다. npm 7부터 피어는 기본으로 설치되니, 범위를 ^18처럼 넓게 적지 않고 패치까지 잠그면 앱과 충돌합니다.


고칠 수 없는 서드파티가 리액트를 일반 의존으로 들고 있으면, 앱 루트 package.jsonoverrides로 한 버전을 강제합니다. 직접 의존과 스펙이 다를 때는 "react": "$react"처럼 앱이 이미 가진 줄을 가리키는 문법이 안전합니다. 얀은 같은 일을 resolutions로 합니다. 공식 경고 문서가 얀 쪽 우회로 적어 둔 칸입니다.


피어 충돌로 설치 자체가 멈추면 그건 ERESOLVE입니다. 설치는 끝났는데 런타임만 훅 경고면 이 글의 칸입니다. 두 줄을 한 커밋에서 섞지 마세요. overrides 문법은 npm package.json overrides를 따릅니다.


패키지는 피어, 앱은 overrides
// 라이브러리 package.json { "peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" } } // 앱 package.json - 서드파티가 두 벌을 만들 때 { "dependencies": { "react": "^19.0.0" }, "overrides": { "react": "$react", "react-dom": "$react-dom" } }
피어 의존성과 overrides로 리액트 버전을 한 줄로 맞추는 작업
라이브러리에는 피어만 두고, 고칠 수 없는 패키지는 앱 overrides로 한 버전을 가리킨다

훅을 조건 안에 넣은 줄과 자리 분리

eslint-plugin-react-hooks가 잡는 줄은 여기 답이 아닙니다. 트리가 한 줄이면 그쪽으로 가세요.


훅을 반복문·조건·이벤트 핸들러·클래스 안에서 호출하면 같은 경고가 납니다. 그건 규칙 문서의 첫 갈래이고, eslint-plugin-react-hooks가 저장소에서 바로 표시합니다. 이 글은 그 칸을 고치지 않습니다. npm ls react가 한 줄인데도 경고면 훅 호출 위치를 옮기세요.


반대로 린트가 깨끗하고 로컬 패키지를 붙인 뒤에만 나면 중복입니다. 하이드레이션 미스매치와도 다릅니다. 서버 HTML과 브라우저 첫 그림이 다른 줄은 콘솔 문장부터 다릅니다. 날짜 로케일 불일치는 다음 에러 큐의 칸이니, 여기 결론과 섞지 마세요.


  • [ ] 개발 모드에서 전체 경고 문장을 읽었다
  • [ ] npm ls react에 경로가 둘인지 확인했다
  • [ ] React1 === React2 로그가 false인지 봤다
  • [ ] 로컬 패키지라면 피어로 옮기거나 앱 리액트를 링크했다
  • [ ] 별칭을 넣었으면 개발 서버를 다시 켰다
  • [ ] 린트가 잡는 조건 훅과 커밋을 나누었다

참고 자료


내부 연계: 하이드레이션 미스매치, Cannot find module, npm ERESOLVE 피어 충돌, Invalid src와 remotePatterns, headers already sent


인용한 동작은 2026년 9월 공개 문서 기준입니다.


자주 묻는 질문

훅 코드는 안 건드렸는데 Invalid hook call이 납니다.

중복 리액트일 때가 많습니다. npm ls react에 경로가 둘이면 앱과 패키지가 다른 파일을 읽습니다. 조건문 훅과 콘솔 문장은 같아도 고치는 자리는 트리입니다.


버전이 둘 다 19인데도 두 개로 나옵니다.

메이저가 같아도 폴더가 다르면 모듈 객체가 둘입니다. 훅 디스패처는 버전 문자열이 아니라 같은 파일을 요구합니다. 경로를 한 칸으로 묶으세요.


npm link만 끊으면 사라집니다. 배포는 괜찮은가요?

배포 아티팩트에 패키지 전용 node_modules가 안 실리면 프로덕션은 조용할 수 있습니다. 그래도 피어로 고치지 않으면 다음 링크에서 재발합니다. 로컬만 우회하지 마세요.


별칭을 넣었는데도 경고가 남습니다.

개발 서버를 끄지 않았거나, 심볼릭 링크를 실제 경로로 푸는 설정이 별칭을 이깁니다. next.config.js를 저장한 뒤 프로세스를 재시작하고, React1 === React2를 다시 찍으세요.


eslint가 exhaustive-deps만 말하고 이 줄은 안 잡습니다.

중복 복사본은 정적 규칙이 못 봅니다. 린트는 조건·반복 안의 훅을 잡고, 두 모듈 객체는 런타임에서만 갈라집니다. npm ls와 동일 객체 로그가 그 칸입니다.


하이드레이션 미스매치와 무엇이 다른가요?

하이드레이션은 서버 HTML과 브라우저 첫 그림이 다른 줄입니다. Invalid hook call 중복은 렌더 전에 훅 디스패처가 비어 있는 줄입니다. 날짜 포맷 불일치와 슬러그를 섞지 마세요.


Invalid hook call에서 패키지가 리액트를 두 개 들고 있으면 앱과 react-dom이 서로 다른 react 파일을 읽고 있는 상태입니다. npm ls react로 경로를 확인하고, 링크·별칭·피어로 한 복사본에 맞추세요. 관련 글: 하이드레이션 미스매치, Cannot find module, ERESOLVE.


Invalid hook call중복 리액트훅 규칙npm ls reactnpm linkwebpack alias피어 의존성overridesNext.js프론트엔드개발자react-dom

함께 보면 좋은 문제 해결

EXPLORE / Frontend

이어서 읽어보기

전체 토픽 둘러보기