Home
home
🏡 홈
home

Node.js 순환참조 이슈

분류
개발지식
태그
Javascript
작성자
작성일
2025/07/05 02:28
순환참조는 누가 먼저 실행될지 몰라서, 책임을 서로 미루는 상황이죠.
Node.js를 사용한 개발에서는 순환참조 이슈가 발생할 수 있습니다.
순환참조는 A와 B라는 파일이 있을 때 서로 모듈을 import하여 사용하는 것을 의미합니다.
A에서 B를 import하고, B에서는 A를 import 합니다.
가장 대표적으로 많이 나타나는 현상은,
분명히 정상적으로 module.exports로 객체를 내보냈고, 이를 정상적으로 가져와 사용하였지만, 함수 같은 경우는 A is not a function. 과 같이 계속 존재하지 않는다고 오류가 발생합니다.
그러면, 개발자는 단순히 저런 오류를 보면,
내가 함수명을 잘못 정의했나? 문법이 틀렸나? 이런 생각을 할 수 있게 됩니다.
물론 실제로 그랬을 수도 있습니다.  (저도 그랬거든요..)
하지만, 두 세번 검토한 결과, 아무런 문제가 없었다면? 바로 순환참조를 의심해볼 수 있습니다.
서로 상호간의 모듈을 참조해서, 서로 정의되지 않은 상태에서 참조하려고 하니 아직 함수 같은 것들이 정의되지 않았다고 오류가 표출되는 것이죠!
A.js
const {createBObject} = require('./B'); const createAObject = () => { return new A(); }; const B = createBObject(); module.exports = { createAObject: createAObject };
JavaScript
복사
B.js
const {createAObject} = require('./A'); const createBObject = () => { return new B(); }; const A = createAObject(); module.exports = { createBObject: createBObject };
JavaScript
복사
이와 같이 서로 상호적으로 참조해버리면,
서로가 서로를 필요해져버리는 상황이 되어버리면서, 코드가 정신을 못차리게 되어,
import해온 모듈 자체를 정의해버리지 않게 됩니다.
그리고 그결과가 createBObject is not a function, createAObject is not function 과 같은 너무 친절하지 못한 결과가 되어버립니다.
이것만 보고, 어떻게 유추할까요..?
개념을 몰랐다면, 당연히 유추하지도 못할 겁니다.

해결방법 - 1, 파일 분리

근본적으로는 상호참조가 일어나지 않도록 코드를 작성해야 합니다.
상호참조가 일어났다는 것은 코드분리를 명확히 하지 않았다는 것입니다.
A, B 오브젝트를 생성하는 코드가 있고, 이를 둘 다 가져와서 작업이 필요하다면,
애초에 중간 파일을 하나 추가해서, 관리하게 만드는 것이죠.
objectManager.js
const {createAObject} = require('./A'); const {createBObject} = require('./B'); const A = createAObject(); const B = createBObejct(); A.~(); B.~();
JavaScript
복사
이렇게 중간 모듈을 하나 두어,
상호참조가 일어나지 않도록 개발하는 방식이 가장 좋은 방법입니다.

해결방법 - 2, 모듈 임포트 지연

A.js
const createAObject = () => { return new A(); }; module.exports = { createAObject: createAObject }; const {createBObject} = require('./B'); const B = createBObject();
JavaScript
복사
B.js
const createBObject = () => { return new B(); }; module.exports = { createBObject: createBObject }; const {createAObject} = require('./A'); const A = createAObject();
JavaScript
복사
이처럼, 사실 module을 내보낸 이후에 다른 모듈을 import하는 방식으로 작성하면,
모듈을 import하는 시점에서는 이미 정의된 상태 이므로, 사용에 있어 아무런 문제가 없게 됩니다.

해결방법 - 3, 이벤트 기반 통신

Node.js 환경에서는 event 통신을 통해 코드 간의 결합도를 낮출 수 있습니다.
만약 특정 단위별로 확실히 구역을 나눠서 설계하고 싶을 때는 매우 좋은 선택지가 될 수 있습니다.
단, 이는 단순히 event가 trigger되는 형태가, 응답을 기대하는 코드에서는 사용하기가 어렵습니다.
eventBus.js
const EventEmitter = require('events'); module.exports = new EventEmitter();
JavaScript
복사
A.js
const eventBus = require('./eventBus'); const createAObject = () => { return new A(); }; eventBus.on('create-a', () => { const A = createAObject(); A.~(); }); eventBus.emit('create-b');
JavaScript
복사
B.js
const eventBus = require('./eventBus'); const createBObject = () => { return new B(); }; eventBus.on('create-b', () => { const B = createBObject(); B.~(); }); eventBus.emit('create-a');
JavaScript
복사
이런식으로 event를 on()으로 listen하고, emit()으로 신호를 보내서 이벤트를 발동 시킬 수 있습니다.
이런 방식은 역할을 구분하는데 확실한 전략이 될 수 있습니다.
다만, 응답을 기대하여 이를 사용해야 한다면, 별도의 모듈을 만들어서 상호참조를 없애는 방식이 가장 좋은 방식입니다.
단순히 관심사를 분리하기 위함이고, void형태의 함수 호출이 필요하다면, eventEmitter를 통한 코드 분리 방식도 좋은 선택입니다.
다만, event방식은 코드의 연결점을 없애는 방식이기 때문에 디버깅이 어려워 진다는 리스크가 존재하기 때문에,
코드를 어디까지 분리할지, 정책을 명확히 해야 합니다.

마무리

이번에, electron.js 를 통해 데스크탑앱을 개발하던 와중에,
가장 당황했던 부분이 바로 “순환 참조” 문제 였습니다.
ChatGPT도 같이 문제점을 찾는데 일조해주었지만, GPT님 조차도 힘들어하셨던 문제였습니다.
Java Spring 개발에서는 순환참조 오류가 나면,
Framework단에서 그려주면서, 순환참조가 났다고 에러를 표출해주었지만,
Node.js 세계에서는 오류가 나면, 그냥 없이 실행하지 뭐.. 이런 느낌으로 흘러가서
더욱 찾기가 어려웠었습니다.
1시간 넘게 오타만 찾고있었으니 말이죠..
글 읽어주셔서 감사합니다.