Dev Diary

React.memo()로 불필요한 렌더링 줄이기 본문

Trouble Shooting

React.memo()로 불필요한 렌더링 줄이기

sik9252 2024. 10. 8. 12:57
SMALL

사이드 프로젝트에서 회원가입을 만들며 서버에 회원가입 요청 시 에러가 발생하면 Dialog(모달창)를 통해 알려주기 위해 이전에 만들어둔 디자인 시스템 중 Dialog를 사용해서 RegisterAlarmDialog 라는 컴포넌트를 추가했다.

 

1. 기존 RegisterAlarmDialog 컴포넌트

import {
  Button,
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/_shared/ui";
import { useDialogContext } from "@/providers";
import React from "react";

interface RegisterAlarmDialogProps {
  message: string | undefined;
}

const RegisterAlarmDialog = (({ message }: RegisterAlarmDialogProps) => {
  const { isDialogOpen, dialogClose } = useDialogContext();

  return (
    <Dialog open={isDialogOpen} onOpenChange={dialogClose}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>알림</DialogTitle>
          <DialogDescription>{message}</DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <DialogClose asChild>
            <Button variant="navy" onClick={dialogClose}>
              확인
            </Button>
          </DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
});

export { RegisterAlarmDialog };

 

2. 회원가입 요청을 관리하는 useRegister 훅에서 API 요청 후 Error 발생 시 Dialog를 Open 상태로 변경한다.

// useRegister.tsx

"use client";

import { useEffect } from "react";
import { useRegisterApi } from "../../api";
import { useDialogContext } from "@/providers";

const useRegister = () => {
  const { dialogOpen } = useDialogContext();

  const { mutate: register, data: registerData, error: registerError, isPending: isRegisterPending } = useRegisterApi();

  생략 ...

  // 에러가 발생하면 Dialog를 Open 상태로 변경
  useEffect(() => {
    if (registerError) {
      dialogOpen();
    }
  }, [registerError]);

  return {
    registerError,
    isRegisterPending,
  };
};

export { useRegister };

 

3. register 화면에서 Dialog가 보일 수 있게 RegisterAlarmDialog 컴포넌트를 추가한다.

// register.tsx

"use client";

import React from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button, Input } from "@/components/_shared/ui";
import { registerSchema } from "../../schemas";
import { RegisterFormValues } from "../../api";
import { useRegister } from "./useRegister";
import { RegisterAlarmDialog } from "../../components";

const Register = () => {

  const {
    register,
    handleSubmit,
    formState: { errors, isValid },
  } = useForm<RegisterFormValues>({
    resolver: zodResolver(registerSchema),
    mode: "onChange",
    reValidateMode: "onChange",
  });

  const { handleRegister, registerError, isRegisterPending } = useRegister();

  return (
    <>
      <div className="space-y-[32px] w-[620px] m-auto">
        <div className="flex justify-center heading1">회원가입</div>
        <form onSubmit={handleSubmit(handleRegister)} className="space-y-[24px]">
          <Input
            label="닉네임"
            isRequired
            type="text"
            placeholder="닉네임을 입력해주세요"
            error={!!errors.name}
            errorMessage={errors.name && errors.name.message}
            {...register("name")}
            autoComplete="new-password"
          />
          생략 ...
          <Button variant="navy" size="large" type="submit" disabled={!isValid || isRegisterPending}>
            {isRegisterPending ? "이메일 전송중" : "회원가입"}
          </Button>
        </form>
      </div>
      {/* 알림을 위한 Dialog 컴포넌트 추가 */}
      <RegisterAlarmDialog message={registerError?.message} />
    </>
  );
};

export { Register };

 

적용해보니 실제로 잘 작동했다.

 

 

이대로 사용해도 문제가 없긴하다! 하지만, 회원가입에 필요한 데이터를 입력하면서 상태가 바뀌고 그럼 화면이 분명 다시 렌더링 될텐데 그럼 register 컴포넌트에 같이 있는 RegisterAlarmDialog도 계속 렌더링 되고 있을것이란 생각이 들었다.

따라서 RegisterAlarmDialog 내부에 console.log(isDialogOpen);을 출력해본 결과 역시 회원가입 정보를 입력하는 Input에 내용을 적고 내용에 대한 검증을 수행하며 화면이 다시 렌더링 되어 RegisterAlarmDialog도 같이 계속 렌더링이 되고 있었다.

 

false가 계속 출력되는걸 볼 수 있다

 


React.memo를 사용해보자

React.memo는 props가 변경되지 않으면 해당 컴포넌트의 리렌더링을 막아준다. 즉, props가 변경되는 것이 아닌 이상 부모 컴포넌트가 리렌더링될 때 자식 컴포넌트도 함께 리렌더링 되는 것을 막아줄 수 있다는 것이다.

import {
  Button,
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/_shared/ui";
import { useDialogContext } from "@/providers";
import React from "react";

interface RegisterAlarmDialogProps {
  message: string | undefined;
}

// React.memo를 추가한다.
const RegisterAlarmDialog = React.memo(({ message }: RegisterAlarmDialogProps) => {
  const { isDialogOpen, dialogClose } = useDialogContext();

  return (
    <Dialog open={isDialogOpen} onOpenChange={dialogClose}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>알림</DialogTitle>
          <DialogDescription>{message}</DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <DialogClose asChild>
            <Button variant="navy" onClick={dialogClose}>
              확인
            </Button>
          </DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
});

export { RegisterAlarmDialog };

 

아까랑 다르게 처음 화면이 렌더링 될 때만 출력되고, 그 이후로 텍스트를 아무리 변경해도 다시 렌더링되지 않는 것을 볼 수 있다.

React.memo 적용 후

 

추가로 알게 된 사항

(1) React.memo는 얕은 비교를 하기 때문에 객체 타입 props가 전달 되었을때는 의도한대로 동작하지 않을 수 있다.

자바스크립트에서 객체, 함수 배열 등 primitive 타입이 아닌 reference 타입의 자료형을 비교할 때는 값에 의한 비교가 아니라 참조에 의한 비교를 하기 때문에 아래와 같이 두 a, b를 선언하면 두 값은 서로 다른 값으로 인식된다는 것을 알고 있을 것이다.

let a = { count: 1 }
let b = { count: 2 }

console.log(a === b); // false

 

따라서, 객체가 props로 전달 되는 경우 새로운 메모리 주소를 가지게 되어 비교를 할 때 서로 다른 값으로 인식되어 memo를 사용했음에도 리렌더링이 발생할 수 있다. 이때는 별도의 검증 함수를 만들어 memo의 두 번째 인수로 전달해 깊은 비교를 할 수 있게 해줘야 한다.

 

(2) useMemo와 React.memo의 차이

useMemo: 두 번째 인수로 전달하는 의존 배열을 기반으로 변경을 감지한다.

React.memo: props를 기반으로 변경을 감지한다.

const memoizedValue = useMemo(() => expensiveComputation(a, b), [a, b])
const ChildComponent = React.memo(({name, value}) => {
  ...
})

 

 

(3) memo한 컴포넌트에 onChange와 같은 함수를 props로 전달해야 하는 경우

memo를 통해 값을 기억하게 했지만 함수는 여전히 리렌더링 시 재생성 되고 있기 때문에 의도한대로 작동하지 않을 수 있다. 따라서 아래와 같은 2가지 방법을 사용해 리렌더링을 방지할 수 있다.

  • 컴포넌트 외부에 함수를 선언해 변경되지 않도록 한다.
  • useCallback()을 사용해 props로 넘길 함수를 감싸 해당 함수를 재생성하지 않도록 한다.

값의 메모이제이션: useMemo

함수의 메모이제이션: useCallback

 

참고 자료

https://ko.react.dev/reference/react/memo

 

memo – React

The library for web and native user interfaces

ko.react.dev

LIST