본문 바로가기
공부

[React] 참조 동일성

by 꾸돼지 2025. 6. 8.
320x100

리액트 컴포넌트의 리렌더링이 발생하는 조건은 내가 알기로는 3가지이다.

 

1. 부모 컴포넌트가 리렌더링 되는 경우, return 부분이 재호출되면서 자식 컴포넌트로 리렌더링 대상이 된다.

 - 이 경우 React.Memo로 감싼 컴포넌트는 리렌더링 대상에서 제외된다.

2. 부모 컴포넌트에게 전달받은 props이 변하는 경우, 해당 상태값을 전달받은 자식 컴포넌트도 리렌더링 대상이 된다.

3. 자신의 상태(State)가 변하는 경우, 리렌더링 대상이 된다.

 - useMemo, useCallback으로 2-3의 리렌더링 조건에서 내부의 변수, 함수를 제외할 수 있다.

 - react-query에서 data 값이 변하는 경우 역시 내부 상태가 변하는 것으로 간주한다.

 


 

리액트를 공부하면 리렌더링 조건은 항상 강조되는 내용이다.

최근 포폴 프로젝트를 진행하면서, 이 부분을 놓쳐 무한 리렌더링을 발생시켰다.

컴포넌트 내부에서 custom-hook을 사용하는 경우, 해당 hook의 결과 역시 컴포넌트의 상태가 된다.

React-Query의 data 값이 변경될 때와 같다.

내가 놓친 부분은 참조 동일성(referential equality) 부분이다.

 

잘못된 코드는 아래와 같다.

  const { user } = useAuth();
  const sseUrl = `${NOTIFICATION_URL}/subscribe/${user?.id}`;
  const { messages, isConnected, clearMessages } = useSseNotifications(sseUrl, {
    onOpen: () => console.log('SSE Connection'),
    onError: (error) => console.error('SSE: Error', error),
    token: user?.access.token,
  });

 

컴포넌트가 호출되며 커스텀 훅을 이용하여 알림 서버와 SSE 연결을 수행하는 로직이다.

messages는 전달받은 알림의 목록이 들어있는 상태값이고, isConnected 는 SSE 연결 상태를 나타내는 상태값이다.

 

위의 로직은 서버로부터 SSE 연결이 된 이후, isConnected의 값이 true로 변경되어 반환된다.

isConnected는 위의 컴포넌트 내부의 상태값으로 취급되며 false에서 true로 값이 변동된다.

동시에 컴포넌트가 리렌더링되고, 위의 커스텀훅이 다시 실행되며 무한 리렌더링이 발생하게 된다.

 

이 때, 리렌더링이 되는 것은 당연하지만, 커스텀 훅이 재실행되면 안된다.

커스텀 훅이 재실행된다는 것은 기존의 sse연결을 삭제하고, 리렌더링 되는 시점에 다시 sse 연결을 서버에 시도하는 작업이다.

동시에 isConnected의 값이 초기화되고, 다시 연결되며 isConnected의 값이 변하게 된다.

 

커스텀 훅의 내부는 useEffect의 dependency로 두번째 인자로 들어온 options를 참조한다.

리렌더링되는 과정에서 options의 값은 변하지 않는다. 하지만 새로운 객체를 생성해서 커스텀 훅의 인자를 제공한다.

이렇게 새로운 객체를 생성하여 함수의 인자로 제공했기 때문에 리렌더링 시 내부 함수를 재시작하는 트리거가 되었던 것이다.

 

수정된 코드는 아래와 같다.

  const { user } = useAuth();

  const sseUrl = useMemo(
    () => `${NOTIFICATION_URL}/subscribe/${user?.id}`,
    [user?.id]
  );
  const options = useMemo(
    () => ({
      onOpen: () => console.log('SSE Connection'),
      onError: (error: string) => console.error('SSE: Error', error),
      token: user?.access.token,
    }),
    [user?.access.token]
  );
  const { messages, isConnected, clearMessages } = useSseNotifications(sseUrl, options);

 

useMemo를 사용하면 명시적으로 사용자 정보가 변하지 않는 경우, 생성되어 있는 객체를 재사용하게 된다.

동일한 객체를 참조하기 때문에 useEffect의 동작 대상에서 제외되어 문제는 해결되었다.

 


요즘 너무 BE 공부만 해서 그런지 가물가물해서 옛날 기록들을 보며 다시 정리했다.

 

1. Browser는 Java의 jvm과 유사한 자바스크립트 엔진이라는 가상 머신 위에서 동작한다.

JS Engine은 js 코드를 실시간으로 번역하고, 메모리 관리(GC)까지 처리하는 실행 환경이다.

 

2. 자바스크립트 엔진은 운영체제로부터 RAM을 할당받아, 크게 2개의 공간으로 나눠 데이터를 관리한다.

 1) 콜스택(Call Stack) : 원시타입의 값과 참조 값이 저장되는 곳으로 빠르고 효율적인 공간이다.

  - jvm의 metaspace(method area)와 유사하다.

 2) 메모리 힙(Memory Heap): 참조 타입(객체, 배열, 함수 등)의 실제 데이터 덩어리가 저장되는 공간으로 크기가 유동적이다.

  - jvm의 heap과 유사하다.

 

const person = {name: 'GG' };

 

- 객체 데이터 { ... }는 힙에 저장된다.

- 변수(person)는 콜 스택에 생성된다.

- person이라는 변수는 힙에 있는 해당 객체의 참조(메모리 주소)를 저장하고 있다.

 

** 자바스크립트 엔진은 참조 동일성을 '===' 연산자를 통해 판단한다.

** 객체나 배열의 비교에서, 두 변수가 가진 메모리 주소가 정확히 동일한지만 확인하고, 내용물은 전혀 신경쓰지 않는다.

** 원시타입을 비교할 땐, 값과 타입까지 모두 확인한다.

 

const personA = { name: '꿀꿀' }; // 힙에 객체 생성 (주소: #101)
const personB = { name: '꿀꿀' }; // 힙에 '새로운' 객체 생성 (주소: #205)

// 주소 #101과 주소 #205는 다르다.
console.log(personA === personB); // false



const personC = { name: '꿀꿀' }; // 힙에 객체 생성 (주소: #301)
const personD = personC;        // personC의 '주소 값(#301)'을 복사

// 두 변수 모두 주소 #301을 가리킨다.
console.log(personC === personD); // true

// 따라서 한쪽을 바꾸면 다른 쪽도 바뀐다.
personC.name = '꿀';
console.log(personD.name); // '꿀'

 

 


React의 useEffect나 useMemo, useCallback 등의 의존성 배열은 위의 참조 동일성으로 변경 여부를 판단한다.

리렌더링 될 때마다 함수나 객체를 새로 만들면, 내용이 같아도 '다른 참조'로 인식되어 불필요한 재실행이 유발된다.

320x100