Skip to main content

[Javascript] 순환 참조 (Circular Import)

오늘은 Javascript, Python같은 언어에서 import시 발생할 수 있는 순환 참조 에러에 대한 포스트를 작성하고자 한다.

순환 참조

메모리 관점에서의 순환 참조 ( In Javascript )

  • 간단히 요약하자면, 서로 다른 두 객체가 서로를 참조하여, 가비지 컬렉션 알고리즘의 대상에 포함되지 않아 해당 객체들이 불필요해져도 메모리 회수되지 않고 존속하게되는 문제이다.
  • Mozilla Developer Network의 문서의 예시를 드는게 이해하기 쉽다.
import b from "b";
function f() {
  var x = {};
  var y = {};
  x.a = y;         // x는 y를 참조합니다.
  y.a = x;         // y는 x를 참조합니다.

  return "azerty";
}

f();

다음 예제에서는 두 객체가 서로 참조하는 속성으로 생성되어 순환 구조를 생성합니다.
함수 호출이 완료되면 이 두 객체는 스코프를 벗어나게 될 것이며, 그 시점에서 두 객체는 불필요해지므로 할당된 메모리는 회수되어야 합니다.
그러나 두 객체가 서로를 참조하고 있으므로, 참조-세기 알고리즘은 둘 다 가비지 컬렉션의 대상으로 표시하지 않습니다.

  • 참조-세기(Reference-counting) 알고리즘 : 가비지 컬렉션시 사용하는 알고리즘으로, “어떤 다른 객체도 참조하지 않는 객체"를 더 이상 필요없는 객체로 인식하고 가비지 컬렉션을 수행.

import에서의 순환 참조(Circular Dependencies) (In Javascript)

  • 2가지 이상의 모듈에서 import가 꼬리의 꼬리를 물어 순환 구조를 이루어 에러가 발생하는 경우.
  • 예시 circular import
  • 위와 같이 참조의 순환 고리가 발생하게 될 경우, 그 고리의 어느 한 모듈(순환 고리안의 다른 모듈을 참조하는)을 이용하려 하면
    ReferenceError: Cannot access '모듈 이름' before initialization
    라며 에러가 발생한다.

나의 사례

  • 본인은 Axios 모듈, Redux 객체(store)를 export 하는 모듈, Redux Toolkit의 slice 객체를 export하는 모듈를 사용하며 React 프로젝트를 개발하고 있었다.
  • 개발 초-중반기에만 해도 ‘Redux 객체 -> Redux Toolkit의 Slice 객체’, ‘Slice 객체 -> Axios 모듈’ 로의 참조 외에는 모듈간의 연관관계가 존재하지 않아 순환 참조가 발생하지 않아 에러가 발생하지 않았다. before_circular_import
    • ‘Redux 객체 -> Redux Toolkit의 Slice 객체’
      • Redux Toolkit Slice들의 Reducer를 모은 rootReducer를 메인 Redux객체(store)에 할당하기 위해 Slice 객체를 참조.
    • ‘Slice 객체 -> Axios 모듈’
      • 비동기 작업 후 state를 update하는 로직을 위해, Redux 액션 생성함수(Action Creator)를 만들어서 사용하였다.
      • 이 때, 비동기 작업을 위해 Axios 모듈을 사용했다.

사건의 발단

  • 그러다 Axios 모듈에 interceptor를 설정해, request를 보내기 전 ‘유저 Slice’에 저장된 AccessToken을 불러와 Header에 첨부해주려 했는데 순환 참조 에러가 발생했다.
    Uncaught ReferenceError: Cannot access '__WEBPACK_DEFAULT_EXPORT__' before initialization
    • 해당 에러를 맞닥들인 당시, 나는 ‘순환 참조’라는 개념도 잘 알지 못했고 이러한 행위가 에러를 발생시킨다는 것 역시 알지 못했다.
    • 그리고 여러 글들을 찾아 보다가, 관련된 stackoverflow 글을 보고 어느정도 왜 에러를 발생시키는 행위인지 알 수 있었다.

원인

  • request를 보내기 전에 Redux Store를 확인하고 만약 AccessToken이 존재하면 해당 값 가져와서 Header의 Authorization으로 설정한 뒤 요청을 보내는 로직을 수행하는 interceptor를 Axios 모듈에다 달아주고 싶었다.
  • React에서 자주 쓰던것 처럼, useDispatch()훅을 통해 dispatch 함수를 반환 받아, Action Creator를 통해 만든 Action을 dispatch하는 방식으로 AccessToken을 가져오는 것이 원래 계획이었다.
  • 그러나 Axios 모듈은 React Component가 아니기 때문에, React Component 외부에서는 Redux Store 모듈을 불러와 Store 객체가 갖고있는 dispatch 함수를 호출해야 했다.
  • 그래서 Redux Store 모듈을 불러와 dispatch를 함수를 호출하려 했는데, Redux Store를 불러오는 로직을 추가하니 상기한 내용의 에러를 내놓고 있었다.
  • 즉, Axios 모듈에서 Redux Store 모듈을 import함으로써 아래와 같은 구조가 형성되어 버린것이었다. after_circular_import
  1. store 객체를 생성하려면 user-slice의 reducer가 필요user-slice.js를 import
  2. user-slice.js 내부에 정의된 Action Creator 함수는 비동기 작업을 처리하기 위해 Axios 모듈이 필요http.js를 import
  3. Axios 모듈에서는 store 객체 안에 저장된 AccessToken을 가져오기 위해 store 객체가 필요store.js를 import

해결?

  • Action Creator 함수, Store에 접근하는 interceptor, Slice들의 reducer를 combine해 사용하는 store 객체 모두 놓치고 싶지 않았지만, 에러를 해결해야 해 어쩔 수 없이 Action Creator 함수를 사용하지 않는 방향으로 코드를 수정했다.
    • Axios 모듈을 사용하는 Action Creator 함수로 로그인, 로그아웃이 있었는데, 해당 작업을 각 기능을 사용하는 component(로그인 - 로그인 페이지, 로그아웃 - 네비게이션 바)에서 정의하도록 하여 순환 관계를 끊었다.

결론?

  • 순환 참조가 발생할 경우, 순환 고리에 포함된 모듈(객체)들은 생성이 안 되어 사용이 불가능하므로 순환 참조가 발생하지 않도록 코드를 짜는것이 중요하다는걸 몇 시간의 실랑이 끝에 알 수 있었다….