
객체 내부에서 자기 자신을 참조하면 벌어지는 일들
개발을 하다 보면 객체나 컴포넌트에서 자기 자신을 참조해야 하는 상황이 생각보다 자주 발생합니다. 상태를 갱신한다거나, 자기 자신을 다른 함수에 전달해야 할 때 말이죠.
처음엔 별 생각 없이 "이렇게 쓰면 되겠지"라고 생각했는데, 막상 해보니 예상과 다른 결과가 나온 적이 있었습니다.
간단한 퀴즈부터 내보겠습니다.
먼저 이런 코드를 봤을 때 어떤 결과가 나올지 예상해보세요.
const obj = {
name: 'test',
getName: () => obj.name,
}
console.log(obj.getName())
이 코드는 과연 에러 없이 동작할까요?
A) test가 출력된다.
B) undefined가 출력된다.
C) ReferenceError가 발생한다.
정답은… A) test입니다. 쉽게 맞추신 분도 많으시겠지만
저는 처음 봤을 때
어? 객체가 선언되면서 자기 자신을 참조하는데 어떻게 에러가 안 날까?
라는 생각을 했습니다. 직관적으로 뭔가 이상해 보였습니다. 객체가 만들어지는 도중에 자기 자신을 참조하고 있으니까요.
왜 안 터질까?
핵심은 "함수는 정의되는 시점에 실행되지 않는다." 였습니다.
getName은 단지 화살표 함수일 뿐이고, 실제 실행은 obj.getName() 호출 시점입니다. 이때는 이미 obj가 완전히 메모리에 올라와 있기 때문에, 자기 자신을 참조해도 문제가 없습니다.
즉, 지연 실행(deferred execution) 덕분에 아무 문제 없이 동작하는 구조입니다.
JavaScript의 실행 순서를 보면:
- obj 객체 생성 시작
- name 프로퍼티에 'test' 할당
- getName 프로퍼티에 함수 할당 (이때 obj는 아직 미완성이지만 함수는 실행되지 않음)
- obj 객체 생성 완료
obj.getName()호출 시점에 비로소 함수 실행 → 이때는 obj가 완전히 존재함
비슷해 보이지만 실제론 터지는 코드도 있습니다
그런데 조금만 다르게 쓰면 문제가 발생합니다.
const obj = {
name: 'test',
getName: createGetter(obj), // ❌ ReferenceError
}
function createGetter(ctx) {
return () => ctx.name
}
여기서는 ReferenceError가 발생합니다. 이유는 간단한데요
createGetter(obj)는 객체가 아직 만들어지기도 전에 즉시 실행됩니다. 이 시점에서 obj는 아직 존재하지 않거나, 정확히는 **TDZ(Temporal Dead Zone)**에 있어서 접근이 불가능한 상태입니다.
실제로 콘솔에서 실행해보면 이런 에러가 나옵니다:
ReferenceError: Cannot access 'obj' before initialization
실제로 겪을 수 있는 문제: 상태 객체 조립하다가 생긴 일
이런 구조는 특히 상태 객체나 로직 분리 같은 걸 할 때 흔하게 발생합니다.
function createIncrement(state) {
return () => {
state.count++
console.log(`현재 카운트: ${state.count}`)
}
}
const state = {
count: 0,
increment: createIncrement(state), // ❌ ReferenceError
}
여기서도 똑같은 오류가 발생합니다. state는 아직 초기화되지 않았고, 그 상태에서 함수 안에서 state.count++ 같은 걸 하려다 보니 에러가 나는 거죠.
찬찬히 실행 순서를 따져보니 이해가 되었습니다.
이럴 땐 어떻게 구조를 잡아야 할까?
다행히 해결 방법은 명확합니다. 핵심은 평가 시점을 조절하는 것입니다.
1. 평가 시점을 나중으로 미루기
const state = {
count: 0,
}
state.increment = () => {
state.count++
console.log(`현재 카운트: ${state.count}`)
}
이 방식은 안전합니다. state가 완전히 초기화된 뒤에 increment를 정의하니까요.
2. 외부 함수를 쓰되, 객체가 완성된 이후에 전달하기
function createIncrement(ctx) {
return () => {
ctx.count++
console.log(`현재 카운트: ${ctx.count}`)
}
}
const state = {
count: 0,
}
state.increment = createIncrement(state) // ✅ 안전
이 방식도 문제없습니다. state는 이미 존재하고, 그걸 함수에 넘기기 때문에 어떤 클로저도 안정적으로 작동합니다.
3. 초기화 함수를 따로 만들기
function createState() {
const state = {
count: 0,
}
state.increment = () => {
state.count++
console.log(`현재 카운트: ${state.count}`)
}
return state
}
const state = createState()
이런 패턴도 자주 사용됩니다. 객체 생성과 메서드 바인딩을 한 번에 처리할 수 있어서 깔끔하죠.
실무에서 마주한 비슷한 상황들
이런 참조 문제는 여러 상황에서 발생했습니다.
React 컴포넌트에서
// ❌ 이렇게 하면 문제가 생길 수 있음
const MyComponent = {
state: { count: 0 },
handleClick: () => {
MyComponent.state.count++ // 컴포넌트가 아직 완성되지 않았을 수도...
},
}
이벤트 핸들러 바인딩에서
// ❌ 위험한 패턴
const eventManager = {
handlers: [],
subscribe: addHandler(eventManager), // 즉시 실행되어 문제 발생
}
이런 경우들을 겪으면서, 항상 "이 참조는 언제 평가되는가?"를 먼저 생각하게 되었습니다.
결국 핵심은 "언제 평가되는가?"
객체가 자기 자신을 참조해도 괜찮은지 아닌지는, 언제 그 참조가 평가되느냐에 달려 있습니다.
함수 안에서 객체를 참조하는 경우: 대부분 괜찮습니다. → 함수는 호출 시점까지 실행되지 않으니까요.
즉시 실행되거나 외부 함수로 전달되는 참조: 위험합니다. → 객체가 아직 초기화되지 않았기 때문이죠.
이건 단순한 문법 문제가 아니라, JavaScript의 실행 흐름과 스코프 체인을 이해하고 있어야만 예측 가능한 동작입니다.
마무리하며
개발하다 보면 객체 내부에서 자기 자신을 참조해야 할 일이 생각보다 자주 생깁니다.
예를 들어, 상태를 갱신한다든지, 자기 자신을 다른 컴포넌트에 전달한다든지 말이죠.
이럴 땐 단순히 "이렇게 쓰면 되겠지"가 아니라, 항상 스스로 물어봐야 합니다:
"이 참조는 언제 평가되는가?"
그 질문 하나만 꾸준히 던져도, 애매한 ReferenceError는 꽤 많이 줄일 수 있습니다.
특히 복잡한 상태 관리나 이벤트 시스템을 구축할 때는 더더욱 중요한 관점이라고 생각합니다. 언제 어떤 순서로 코드가 실행되는지 정확히 파악하고 있어야, 예상치 못한 오류를 피할 수 있으니까요.