본문 바로가기

Front-End: Web/React.js

forwardRef 사용법 (+여러 ref들 전달하기)

반응형

공부하게된 계기

커스텀 훅인 useInput을 사용하여 인풋을 관리하니 편하긴 하지만, value가 달라질 때마다 인풋 컴포넌트를 포함한 대부분의 모든 컴포넌트들이 리렌더링된다. 왜냐하면 로그인 페이지에서 header, body, footer로 나뉘는데 email, password 인풋의 value들을 header, body, footer를 묶어주는 로그인페이지 컴포넌트에서 관리하기 때문이다. header에서는 memo를 사용해서 리렌더링이 안되지만, body와 footer 그리고 modal, modal background는 인풋값을 입력할 때마다 리렌더링된다.

만약에 이 인풋들을 useInput으로 관리하지 않고 useRef로 관리하게 된다면, 로그인 버튼을 눌렀을 때만 useRef.current.value를 확인하면 되므로 인풋값을 입력할 때마다 리렌더링이 일어나지 않을 것으로 예상된다. 따라서 로그인페이지 컴포넌트에서 emailRef, passwordRef를 정의하고 forwoardRef를 활용하여 이 두 값을 body에 전달하려 한다.

그래서 forwardRef 기본 사용법과 typescript 타입 주는 방법, 그리고 프로젝트에 적용하는 방법을 알아보았다.

 

🔹 인풋값을 입력할 때마다 모든 컴포넌트 리렌더링됨 (리렌더링 박스 생성)

 


 

forwardRef란?

🧸 Ref 전달하기는 일부 컴포넌트가 수신한 ref를 받아 조금 더 아래로 전달(즉, “전송”)할 수 있는 옵트인 기능입니다.

useRef로 정의한 ref를 props를 사용하여 자식 컴포넌트로 전달할 때 사용한다.

 

forwardRef 사용법

상위 컴포넌트에서 FancyButton 컴포넌트로 ref를 전달한다 하자.

const FancyButton = React.**forwardRef**((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// 이제 DOM 버튼으로 ref를 직접 받을 수 있습니다.
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
  • 첫 번째 인자(props) : 일반적으로 전달하는 props들
  • 두 번째 인자(ref) : forwardRef와 같이 호출된 컴포넌트를 정의했을 때에만 생성되는 인자. 일반 함수나 클래스 컴포넌트는 ref 인자를 받지도 않고 props에서 사용할 수도 없다!

(+) ref 전달할 때 상위 컴포넌트의 속성명은 반드시 ref 여야 한다!

 

TypeScript에서 사용하기

const Component = React.forwardRef**<RefType, PropsType>((props, ref)** => {
  return someComponent;
});

제너릭 파라미터(ref와 props)와 이의 타입 순서가 반대라서 혼란스러울 수 있다!

만약 전달할 props가 없다면, type을 생략해도 된다.

const Search = React.forwardRef<HTMLInputElement>((props, ref) => {
  return <input ref={ref} type="search" />;
});

 


프로젝트에 적용하기

여러 ref들 전달하기

useRef로 ref들을 묶어서 refs를 생성하고, child component로 보낸다. (구글링 참고하였음)

// LoginPage.tsx

import LoginModalBody from "@components/molecules/Div/LoginModalBody";
import LoginModalFooter from "@components/molecules/Div/LoginModalFooter";
import LoginModalHeader from "@components/molecules/Div/LoginModalHeader";
import AuthModal from "@components/organisms/Modal/AuthModal";
import useInput from "@hooks/common/useInput";
import useLogin from "@hooks/query/useLogin";
import validateEmail from "@utils/validateEmail";
import { useCallback, useRef, useState } from "react";

const LoginPage = () => {
  **const emailRef = useRef<HTMLInputElement>(null);
  const passwordRef = useRef<HTMLInputElement>(null);
  const refs = useRef<any>({ emailRef, passwordRef });**

  const [errorMessage, setErrorMessage] = useState("");

  const { mutate: login } = useLogin(setErrorMessage);

  const onLogin = useCallback(() => {
    if (!emailRef.current || !passwordRef.current) {
      return setErrorMessage("모든 값을 입력해주세요.");
    }
    if (!validateEmail(emailRef.current.value)) {
      return setErrorMessage("유효하지 않은 아이디입니다.");
    }
    if (passwordRef.current.value.length < 8) {
      return setErrorMessage("비밀번호는 8자리 이상이어야 합니다.");
    }
    setErrorMessage("");
    login({
      email: emailRef.current.value,
      password: passwordRef.current.value,
    });
  }, [emailRef, passwordRef]);

  return (
    <AuthModal width={480}>
      <>
        <LoginModalHeader errorMessage={errorMessage} />
        **<LoginModalBody ref={refs} />**
        <LoginModalFooter onLogin={onLogin} />
      </>
    </AuthModal>
  );
};

export default LoginPage;
// LoginModalBody.tsx
interface RefHandler {
  emailRef: RefObject<HTMLInputElement>;
  passwordRef: RefObject<HTMLInputElement>;
}

const LoginModalBody = forwardRef<RefHandler>((props, refs) => {
  console.log(1, props, refs);

  ...

});

 

두 개의 ref(emailRef, passwordRef)를 성공적으로 잘 받아왔다!

 

 

에러 직면

위의 에러가 발생해서 refs의 타입을 RefHandler → any로 줘도 여전히 에러가 발생한다.

 

문제가 뭐지? → 전달 방식 변경

해당 에러를 구글링해서 계속 찾던 와중에, 생각해보니까 “여러 ref들을 굳이 ref로 묶어서 보내줄 필요가 있나?” 하는 의문이 들었다.

그냥 ref를 묶어서 props로 전달해주기만 하면 되니까, const로 정의해서 전달해봤다.

// LoginPage.tsx

const LoginPage = () => {
  const emailRef = useRef<HTMLInputElement | null>(null);
  const passwordRef = useRef<HTMLInputElement | null>(null);
  const refs = { emailRef, passwordRef };

	...

	return (
    <AuthModal width={480}>
      <>
        <LoginModalHeader errorMessage={errorMessage} />
        <LoginModalBody refs={refs} />
        <LoginModalFooter onLogin={onLogin} />
      </>
    </AuthModal>
  );
};

export default LoginPage;

 

// LoginModalBody.tsx

import LinkText from "@components/atoms/Text/LinkText";
import { Ref } from "react";
import styled from "styled-components";
import LoginForm from "../Form/LoginForm";

interface LoginModalBodyProps {
  refs: {
    emailRef: Ref<HTMLInputElement>;
    passwordRef: Ref<HTMLInputElement>;
  };
}

const LoginModalBody = ({ refs }: LoginModalBodyProps) => {
  return (
    <>
      <LoginForm ref={refs.emailRef} text="이메일" type="email" />
      <LoginForm ref={refs.passwordRef} text="비밀번호" type="password" />
      <LinkTextContainer>
        <LinkText text="비밀번호를 잊으셨나요?" onClick={() => null} />
      </LinkTextContainer>
    </>
  );
};

const LinkTextContainer = styled.div`
  margin: -1rem 0 1.25rem;
`;

export default LoginModalBody;

 

이렇게 하니 더이상 에러가 발생하지 않고, 예상했듯이 인풋에 값을 입력할 때마다 리렌더링 박스가 발생하지 않는다. 로그인 버튼을 누르거나 가입하기 버튼을 누를 때만 발생한다.

 

이제 forwardRef의 사용법을 잘 알았으니 useInput을 사용하고 있는 모든 컴포넌트들의 코드도 변경해서 최적화해주자..(으악)

 

반응형