자바스크립트에서의 순환 참조 문제
🎯 이 문서를 읽고 난 후의 상태
- 순환 참조 문제가 무엇인지 안다.
- 순환 참조 문제를 해결하는 방법을 안다.
- 순환 참조 문제를 예방하는 방법을 안다.
😭 문제 상황
위의 글에서 확인할 수 있는데, 스토리북에서 스토리를 작성하다가 순환 참조 문제를 마주하게 되었다.
평소 순환 참조 문제라는 키워드는 들어봤는데, 정작 제대로 알지 못하고 있다는 생각이 들었다.
그래서 이번 기회에 순환 참조 문제가 무엇인지 확인하고, 이를 해결하는 방법을 알아보려 한다.
🤔 순환 참조 문제란?
순환 참조(Circular Dependencies)
란 두 개 이상의 모듈이 서로를 직접 또는 간접적으로 참조하는 상황을 의미한다.
위의 예시처럼, ModuleA
와 ModuleB
가 서로를 참조하는 상황이 발생하면, 이를 순환 참조 문제라고 한다.
이러한 상황이 발생하면, 모듈이 무한히 서로를 참조하게 되어, 프로그램이 무한 루프에 빠지는 상황이 발생할 수 있으며, 그렇기 때문에 문제
라고 분류한다.
보면 알겠지만, 의외로 소프트웨어 개발에서 흔히 발생하는 문제이며, 특히 모듈 간의 의존성이 복잡해지면 복잡해질 수록 발생할 확률이 높아진다.
코드를 통해 좀 더 살펴보자.
📝 직접 순환 참조 예시
- ModuleA
- ModuleB
// moduleA.js
import { functionB } from './moduleB.js';
export function functionA() {
console.log('Function A');
functionB();
}
// moduleB.js
import { functionA } from './moduleA.js';
export function functionB() {
console.log('Function B');
functionA();
}
위의 코드에서 ModuleA
와 ModuleB
가 서로를 참조하고 있다.
이렇게 되면, functionA
가 호출되면 functionB
가 호출되고, functionB
가 호출 되면 functionA
가 호출되는 무한 루프에 빠지게 된다.
이러한 상황이 발생하면, 프로그램이 무한 루프에 빠지게 되어, 프로그램이 정상적으로 동작하지 않게 된다.
📝 간접 순환 참조 예시
순환 참조는 두 개 이상의 모듈이 직접 참조하는 것 뿐만 아니라, 간접적으로 참조하는 경우에도 발생할 수 있다.
- ModuleA
- ModuleB
- ModuleC
// moduleA.js
import { functionC } from './moduleC.js';
export function functionA() {
console.log('Function A');
functionC();
}
// moduleB.js
import { functionA } from './moduleA.js';
export function functionB() {
console.log('Function B');
functionA();
}
// moduleC.js
import { functionB } from './moduleB.js';
export function functionC() {
console.log('Function C');
functionB();
}
위의 코드에서 ModuleA
는 ModuleC
를 참조하고, ModuleC
는 ModuleB
를 참조하고, ModuleB
는 ModuleA
를 참조하고 있다.
여기서 moduleA
→ moduleC
→ moduleB
→ moduleA
순으로 순환 참조가 발생한다.
🤔 순환 참조는 왜 문제가 되는가?
단순히 무한 루프에 빠지는 것 말고 좀 더 구체적인 이유를 살펴보자.
문제 | 설명 |
---|---|
모듈 초기화 문제 | JavaScript 모듈 시스템 (특히 ES 모듈)은 모듈을 로드할 때 먼저 의존성을 모두 로드한 후 실행한다.순환 참조가 있을 경우, 모듈이 완전히 초기화되기 전에 다른 모듈을 참조하게 되어 초기화되지 않은 상태의 값을 참조하게 된다. 이는 undefined 또는 예상치 못한 값이 반환되게 만들 수 있다. |
메모리 누수 | 순환 참조는 가비지 컬렉션(garbage collection 을 방해할 수 있다.특히 객체 간 순환 참조는 메모리 누수를 일으킬 수 있다. 가비지 컬렉션은 어떤 변수의 참조 카운트가 0이 될 때 실행 이 되는데, 순환 참조가 발생하면 결코 0이 될 수 없기 때문이다. |
코드 유지보수의 어려움 | 순환 참조는 코드를 이해하고 유지보수하기 어렵게 만든다. 특히 순환 참조가 발생하면, 모듈 간의 의존성이 복잡해지고, 코드를 이해하기 어려워진다. 이는 코드의 재사용성과 확장성을 저해시킨다. |
테스트의 어려움 | 순환 참조는 테스트하기 어렵게 만든다. 특히 모듈 간의 의존성이 복잡해지면, 테스트 코드를 작성하기 어려워진다. 이게 앞서 말한 스토리북 작성 과정에서 내가 겪은 문제기도 했다. |
🤔 순환 참조의 예시와 문제점
간단한 React
컴포넌트를 사용하는 예시를 통해서, 순환 참조 문제를 확인해보자.
- ComponentA
- ComponentB
// ComponentA.tsx
import React from 'react';
import { ComponentB } from './ComponentB';
export const ComponentA: React.FC = () => {
return (
<div>
<h1>Component A</h1>
<ComponentB />
</div>
);
};
// ComponentB.tsx
import React from 'react';
import { ComponentA } from './ComponentA';
export const ComponentB: React.FC = () => {
return (
<div>
<h1>Component B</h1>
<ComponentA />
</div>
);
};
moduleA
→ moduleB
→ moduleA
→ moduleB
→ moduleA
→ moduleB
→ moduleA
→ ... 와 같은 참조가 일어난다.
그리고 이는 다음과 같은 에러를 발생시킬 수 있다.
Uncaught RangeError: Maximum call stack size exceeded
이는 무한 루프가 발생하여 콜 스택이 초과되었기 때문이다.
🤔 순환 참조 문제를 감지하는 방법
사실 제일 좋은 건 의식적으로 매번 점검하면서 넘어가는 것이다.
다만 그렇게 하면, 생산성이 굉장히 떨어지는 문제가 발생한다.
매번 하나하나 발생할지 안할지 고려하기엔.. 우리가 해결해야하는 문제와 신경써야할 것들이 너무나도 많다.
그래서 이러한 문제를 감지하는 도구와 방법을 알아보자.
📝 ESLint
를 사용한 순환 참조 감지
ESLint
를 사용하면 순환 참조를 감지할 수 있다.
eslint-plugin-import
플러그인을 사용하면, 순환 참조를 감지할 수 있다.
npm install eslint eslint-plugin-import --save-dev
그리고 .eslintrc.js
파일에 다음과 같이 설정을 추가한다.
module.exports = {
plugins: ['import'],
rules: {
'import/no-cycle': 'error',
},
};
이렇게 설정하면, ESLint
가 순환 참조를 감지하고, 에러를 발생시킨다.
사실 인터넷 등에서 찾아서 ESLint
를 사용하면 항상 import
모듈을 다운받아서 쓰곤 했었는데.. 이런 목적으로 쓴다는 걸 이번에 처음알았다...
역시 항상 뭐든 제대로 알고 넘어가지 않으면 언젠가 피를 보나보다...
📝 TypeScript
를 사용한 순환 참조 감지
TypeScript
를 사용하면 순환 참조를 감지할 수 있다.
TypeScript
는 기본적으로 순환 참조를 감지하고, 에러를 발생시킨다.
📝 순환 참조 시각화 도구 사용
madge
와 같은 도구를 사용하면, 코드의 의존성을 시각적으로 보여주어 순환 참조를 쉽게 감지할 수 있다.