JavaScript와 TypeScript는 객체 참조를 일회용처럼 다루기 너무나 쉽게 만듭니다. 객체를 생성하고, 함수에 전달하고, 캐시에 저장한 뒤, 나중에 변수를 새로운 인스턴스로 교체해 버립니다. 언어는 아무런 불평도 하지 않습니다. 하지만 오래된 참조는 프로그램의 다른 어딘가에 여전히 존재하며, 더 이상 최신이 아닌 데이터를 가리키고 있습니다. 이것은 크래시(crash)가 아닙니다. 그보다 더 나쁜 상황입니다. 바로 진실을 소유하고 있다고 믿는 코드베이스의 두 부분 사이에서 발생하는 '소리 없는 불일치(silent divergence)'입니다.

이를 방지하는 규율을 Hard Object References라고 부릅니다. 이것은 라이브러리나 컴파일러 기능이 아닙니다. 코드 전반에 걸쳐 적용하는 하나의 약속(contract)입니다.

Stale Alias(오래된 별칭) 문제

Stale Alias는 한 모듈이 객체의 참조를 보유하고 있는 동안, 다른 모듈이 해당 객체를 새로운 객체로 교체할 때 발생합니다. 첫 번째 참조는 여전히 유효한 코드이지만, 더 이상 최신 데이터를 가리키지 않습니다.

일반적인 웹 애플리케이션의 사용자 레코드를 예로 들어보겠습니다:

const user = {
  name: "Alice",
  address: {
    city: "Seoul",
    country: "KR"
  }
};

배송 컴포넌트가 초기에 주소를 캡처합니다:

const shippingAddress = user.address;

나중에 프로필 업데이트가 도착합니다. 리듀서(reducer)나 서비스 핸들러가 객체 전체를 교체하기로 결정합니다:

user.address = { city: "Tokyo", country: "JP" };

이 시점에서 user.address는 도쿄를 가리킵니다. 하지만 shippingAddress는 여전히 서울에 있는 이전 객체를 가리키고 있습니다. 예외(exception)는 발생하지 않습니다. 타입이 여전히 일치하기 때문에 TypeScript도 만족합니다. UI는 프로필 페이지에 업데이트된 도시를 보여줄 수 있지만, 배송 라벨에는 조용히 옛 주소가 인쇄될 수 있습니다. 이 버그는 사용자가 패키지가 잘못된 국가로 배송되었다고 불평할 때 비로소 드러납니다.

이런 일이 발생하는 이유는 JavaScript가 식별성(identity)과 값(value)을 분리하기 때문입니다. 객체 프로퍼티를 새로운 객체 리터럴로 교체하면 연결 고리가 끊어집니다. 이전 객체는 파괴되지 않습니다. 단지 고아(orphan) 상태가 될 뿐입니다. 여전히 그 객체를 들고 있는 사람은 유령(ghost)을 다루고 있는 셈입니다.

Hard Object References의 의미

규칙은 간단합니다. 원시 값(primitive values)은 교체하되, 객체나 배열의 참조는 절대 교체하지 마십시오. 새로운 데이터가 들어오면 컨테이너를 통째로 바꾸는 대신, 기존 컨테이너 안에 데이터를 복사해 넣으십시오.

이를 위해서는 세 가지 구체적인 습관이 필요합니다.

첫째, 객체와 배열을 선언할 때 const를 사용하십시오. 이는 최상위 변수를 새로운 인스턴스로 다시 바인딩하고 싶은 유혹을 제거합니다. 변수는 해당 스코프의 수명 동안 고정되어 있어야 합니다.

둘째, 객체나 배열을 담고 있는 프로퍼티를 새로 생성된 객체로 교체하지 마십시오. 주소를 업데이트해야 한다면, 그 내부의 프로퍼티를 변경(mutate)하십시오.

셋째, 상태를 비우거나 초기화해야 한다면, 새로운 빈 객체나 배열로 버리는 대신 기존 구조를 비우십시오.

주소 예제로 돌아가서, 올바른 업데이트 방식은 다음과 같습니다:

user.address.city = "Tokyo";
user.address.country = "JP";

들어오는 데이터가 일부이거나 동적이라면, Object.assign을 사용하여 기존 타겟에 기록하십시오:

Object.assign(user.address, incomingAddressData);

메모리 상의 정확히 동일한 객체를 가리키는 shippingAddress 변수는 이제 즉시 새로운 필드들을 확인할 수 있습니다. 실시간 진실의 원천(source of truth) 역할을 하는 유일한 표준 객체(canonical object)가 존재하게 됩니다.

이 규칙이 가장 중요한 곳

단순한 설정(configuration) 객체에 이 규율을 적용하는 것은 과해 보일 수 있습니다. 하지만 상태가 여러 하위 시스템이 겹치는 노드들에 대한 포인터를 보유하는 그래프 구조로 성장하면, 이 규칙은 필수적이 됩니다.

리치 텍스트 에디터를 생각해 보십시오. 문서 모델은 노드들의 트리입니다. 선택(selection) 모델은 시작 노드와 끝 노드의 참조를 보유합니다. 히스토리 버퍼는 마지막 작업에서 변경된 노드들의 참조를 보유합니다. 렌더링 레이어는 레이아웃을 위해 측정한 노드들의 참조를 보유합니다. 만약 상태 관리자가 텍스트가 변경되었다는 이유로 단락(paragraph) 노드를 새로운 객체로 교체한다면, 이 모든 하위 시스템은 이제 오래된 별칭(stale alias)을 보유하게 됩니다. 선택 영역은 잘못된 영역을 강조합니다. 히스토리 시스템은 올바르게 되돌릴 수 없습니다. 렌더러는 크래시가 나거나, 더 나쁘게는 유령 커서(phantom cursors)를 표시합니다.

동일한 위험은 UI, 액세스 제어 레이어, 자동 저장 루틴이 참조하는 중첩된 설정과 권한을 가진 사용자 프로필에서도 나타납니다. 부모 컨테이너가 자식 노드의 측정값을 캐싱하는 레이아웃 엔진에서도 나타납니다. 런타임 컨트롤러가 활성 엔티티를 참조로 추적하는 비주얼 에디터나 캔버스 도구에서도 나타납니다. 이 모든 영역에서 컴포넌트는 객체의 핸들(handle)을 잡고, 그 핸들이 진실의 실시간 뷰(live view)로 유지되기를 기대합니다.

Hard Object References는 객체를 안정적인 주소로 취급합니다. 내부의 가구는 바뀔 수 있지만, 문은 같은 자리에 있습니다. 주소를 가진 사람은 누구나 안으로 걸어 들어가 현재의 배치를 볼 수 있습니다.

교체 대신 반응성(Reactivity Instead of Replacement)

Redux나 이와 유사한 불변(immutable) 상태 라이브러리를 사용해 본 적이 있다면, 이 모델이 아마 거꾸로 된 것처럼 느껴질 것입니다. 그런 시스템에서는 새로운 객체를 생성함으로써 변경 사항을 알립니다. 참조(reference)의 변경 자체가 곧 신호입니다. 컴포넌트는 prevProps.data === nextProps.data를 비교하여 리렌더링 여부를 결정합니다.

Hard Object References를 사용하려면 이러한 가정을 뒤집어야 합니다. 참조가 일정하게 유지되기 때문에, 참조의 동일성(reference equality)만으로는 데이터가 변경되었는지 알 수 없습니다. 업데이트를 전파하기 위한 다른 방법이 필요합니다.

실제로 이는 반응형 시스템(reactivity systems), 명시적 옵저버(explicit observers) 또는 더티 플래그(dirty flags)에 의존함을 의미합니다. user.address.city를 변경(mutate)하면 구독자들에게 알리는 세터(setter)가 트리거될 수 있습니다. 객체는 이벤트 버스(event bus)를 통해 변경 이벤트를 방출할 수 있습니다. 게임 루프나 캔버스 도구는 전역 더티 플래그를 설정하고 프레임 끝에서 그래프를 다시 스캔할 수도 있습니다. 참조는 안정적이므로, 다른 메커니즘을 통해 데이터 흐름을 가시화해야 합니다.

이러한 아키텍처의 변화 덕분에 이 접근 방식은 복잡한 프론트엔드 상태, 대규모 컴포넌트 로컬 상태, 비주얼 에디터, 캔버스 도구 및 런타임 컨트롤러에 가장 적합합니다. 이러한 시스템은 이미 세밀한 업데이트(granular updates), 직접적인 변경(direct mutations) 또는 명령형 API(imperative APIs)에 의존하고 있습니다. 여기에 불변성을 강제하면 비례하는 명확성을 얻지 못한 채 과도한 할당 압박(allocation pressure)과 참조 변화(reference churn)만 초래하는 경우가 많습니다. 매 프레임이 중요한 상황에서 슬라이더 하나를 움직이기 위해 새로운 객체 그래프를 할당하는 것은 낭비입니다. 참조를 고정(hard)하고 내부를 변경(mutate)하는 것이 문제의 실제 메커니즘과 일치합니다.

효과적으로 적용하기

이 규칙의 간과하기 쉬운 장점 중 하나는