[React] 중첩된 커스텀 훅의 리렌더링

Seungrok Yoon (Lethe)·2024년 8월 12일
0

[TIL] 성장 한 스푼

목록 보기
50/53

중첩된 커스텀 훅에서 상태 변화가 일어날 경우, 리렌더링은 어떠한 방식으로 발생하는가?

내 깃헙 이슈
PR

//NestedCustomHook 페이지
import styled from '@emotion/styled';
import useNestedHook from '@Hook/nested-custom-hook/useNestedHook';
import { v4 as uuidv4 } from 'uuid';

export default function NestedCustomHook() {
  const formId = `user_${uuidv4()}`;
  const { formValues, setter } = useNestedHook();

  return (
    <VerticalFlexContainer>
      <form>
        <label htmlFor={`name_${formId}`}>
          Name:&nbsp;
          <input
            id={`name_${formId}`}
            name="name"
            type="text"
            value={formValues.name}
            onChange={(e) => {
              setter.setName(e.target.value);
            }}
          />
        </label>
        <label htmlFor={`email_${formId}`}>
          Email:&nbsp;
          <input
            id={`email_${formId}`}
            name="email"
            type="text"
            value={formValues.email}
            onChange={(e) => {
              setter.setEmail(e.target.value);
            }}
          />
        </label>
      </form>
      <HorizontalFlexContainer gap="20px">
        <CustomSpan>Name: {formValues.name}</CustomSpan>
        <CustomSpan>Email: {formValues.email}</CustomSpan>
      </HorizontalFlexContainer>
    </VerticalFlexContainer>
  );
}

const HorizontalFlexContainer = styled.div<{ gap: string }>`
  display: flex;
  gap: ${(props) => props.gap};
`;

const VerticalFlexContainer = styled.div`
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 12px;
`;

const CustomSpan = styled.span`
  display: inline-block;
  background-color: 'red';
`;
//useNestedCustomHook.tsx
import useEmail from './useEmail';
import useName from './useName';

interface FormValues {
  name: string;
  email: string;
}

interface Setter {
  setName: React.Dispatch<React.SetStateAction<string>>;
  setEmail: React.Dispatch<React.SetStateAction<string>>;
}

export default function useNestedHook(): {
  formValues: FormValues;
  setter: Setter;
} {
  console.log('useNestedHook Rerender');
  const { name, setName } = useName();
  const { email, setEmail } = useEmail();

  return { formValues: { name, email }, setter: { setName, setEmail } };
}
//useName.tsx
import { useState } from 'react';

export default function useName() {
  console.log('useName rerender');
  const [name, setName] = useState('');

  return { name, setName };
}
//useEmail.tsx
import { useState } from 'react';

export default function useEmail() {
  console.log('useEmail rerender');
  const [email, setEmail] = useState('');

  return { email, setEmail };
}

위 예시 코드는 React의 커스텀 훅 동작에 대한 이해를 위해서 예시로 작성한 코드이다.

실행을 하게 되면 아래와같은 리렌더링 양상을 보여준다.

왜 이럴까?

Render and Commit

정답은 리액트의 렌더링 과정에 있었다.

커스텀 훅은 상태 관리 로직을 재사용성을 위해 분리한 것에 지나지 않는다. 중첩된 커스텀훅을 호출하는 컴포넌트는 커스텀 훅의 상태들을 참조(구독)하고 있는 것이다.

Triggering a render

상태가 변경되어 트리거된 리렌더는 큐에 등록이 되고, 이후 상태가 변경되어 리렌더링이 필요한 컴포넌트가 재호출된다. 재호출된다는 것은 함수 내부 라인을 한 줄 한 줄 다 훑으며 실행이 된다는 것이다. 그렇기에 커스텀 훅에서 리턴된 상태값을 참조하는 페이지 컴포넌트가 리렌더링 대상이 되었다.

Rendering the component

첫 렌더링 시에는 루트 컴포넌트를 호출하고,

이후 렌더에서는 렌더링을 트리거한 상태 업데이트를 지닌 컴포넌트를 호출한다. 이 과정은 재귀적이어서, 만약 A컴포넌트가 호출되었다면, 하위 컴포넌트들도 다시 재호출된다.

이 작업은 한 눈에 비효율적임을 알 수 있다.

그래서 리액트는 useMemo, useCallback 등의 불필요한 리렌더링을 방지하는 API를 제공하고 있다.

Committing to the DOM

컴포넌트들을 렌더링(호출)했다면, 이제 DOM을 변경하게 된다.

  • 첫 렌더 시에는 appendChild() DOM API를 통해 화면에 생성한 노드를 추가한다.

  • 리렌더링 시에는, DOM과 최신 렌더링 결과를 맞추기 위해 최소한의 작업을 진행한다.

리액트는 렌더와 렌더 사이에 차이가 생긴 DOM 노드만 변경한다. 컴포넌트 호출은 하지만, Commit 단계에서 컴포넌트와 매칭되는 DOM을 조작하는 건 일부만 진행할 수 있다는 이야기이다.

브라우저 페인트

DOM 업데이트를 하고 나면, 화면을 리페인트한다.

나의 오개념

내부 커스텀 훅의 상태를 바꾸니까 리렌더링이 내부 커스텀 훅에서 먼저 일어날 것이라 생각한 것이 오개념이었다.

리렌더링은 별도의 큐잉으로 진행이된다.

profile
안녕하세요 개발자 윤승록입니다. 내 성장을 가시적으로 기록하기 위해 블로그를 운영중입니다.

0개의 댓글