본문 바로가기

온라인 강의(유데미, 인프런 등)/React 완벽 가이드(유데미)

[react] forwardRef

반응형

stackoverflow 클론 코딩 프로젝트를 진행하던 중이였다.

질문 등록 페이지를 만들며 input 또는 editor가 focus 될 경우 해당 컴포넌트로 스크롤될 수 있도록 하는 기능을 만들고 싶었다. 그래서 질문 양식에 따라 input 또는 editor 컴포넌트가 담긴 각각의 custom component에 ref를 주어 각 컴포넌트들이 focus됨에 따라 참조되어 스크롤이 해당 컴포넌트로 이동할 수 있도록 해야겠다~~ 싶었는데 왠걸!! 스크롤 이벤트가 전혀 일어나지 않는 것이였다!

그때 당시에는 이유를 찾지못하고 각 custom component를 div 태그로 감싸 그 div에 ref를 주는 것으로 땜빵(?) 해결을 하였고, 여러가지 구글링을 해보았지만 문제 자체를 파악하지 못해 제대로 된 검색 키워드도 찾지 못한 채 넘어갔었다. 

이제서야!!! 오늘 forward ref에 대한 짧은 강의를 듣고 난 후!! 왜 그때 내가 의도한 대로 되지 않았는지 깨달았다.

custom component에 ref를 주었으니 그것은 당연히 props에 포함되는 값이 되었고, 각 custom component는 ref라는 props를 받는 로직도 없었을 뿐더러 그것을 사용하지도 않으니 ref가 undefined가 되면서 의도했던 대로 적용이 안되었을 수 밖에..

그때 당시에 블로그 포스팅으로도 삽질의 기록을 남겨놓았었다...(갑자기 추억돋네)

https://bbeeyaks-moment.tistory.com/209

 

StackOverflow 클론코딩(12/28)

내가 두 번째로 맡게 된 파트는 두둥.... 질문 작성 페이지다!! 도움말 카드?도 조건부 렌더링이 되어야하고, 유효성 검사도 해야하고.. 눈물 쓱.. 제일 어려운 페이지가 되겠구나 싶었는데 내가

bbeeyaks-moment.tistory.com

결론적으로, React 컴포넌트에 ref prop을 넘겨서 그 내부에 있는 HTML 엘리먼트에 접근을 하게 해주려면 forwardRef() 함수를 사용해야하는 것이였다! 이제라도 원인을 알게되어 참 다행이다. 모르고 넘어갈 뻔?!

 

forwardRef

React에서 특수한 목적으로 사용되기 때문에 일반적인 용도로 사용할 수 없는 prop이 몇 가지 있다. 대표적인 예로 루프를 돌면서 동일한 컴포넌트 여러 번 랜더링할 때 사용하는 key prop이 있고,  ref prop도 마찬가지로 HTML 엘리먼트 접근이라는 특수한 용도로 사용되기 때문에 일반적인 prop으로 사용을 할 수 없다.

HTML 엘리먼트가 아닌 React 컴포넌트에서 ref prop을 사용하려면 React에서 제공하는 forwardRef()라는 함수를 사용해야 한다. React 컴포넌트를 forwardRef()라는 함수로 감싸주면, 해당 컴포넌트는 함수는 두 번째 매개 변수를 갖게 되는데, 이를 통해 외부에서 ref prop을 넘길 수 있다.

 

예시를 살펴보자.

아래처럼 Input 컴포넌트를 forwardRef 함수로 감싸주면 부모 컴포넌트로부터 받아온 ref prop을 사용할 수 있게 된다.

 

추가적으로 알게 된 useImperativeHandle 훅에 대해서도 간단하게 살펴보자.

useImperativeHandle 훅은 forwardRef 를 사용해 ref를 사용하는 부모 측에서 커스터마이징된 메서드를 사용할 수 있게 해준다.  useImperativeHandle 첫 번째 인자로는 프로퍼티를 부여할 ref이고, 두 번째 인자는 객체를 리턴하는 함수이다. 이 객체에 추가하고 싶은 프로퍼티를 정의하면 된다.

예시에서는 focus 라는 메서드를 정의하였다. 원래 input 엘리먼트에는 focus라는 메서드가 없지만, useImperativeHandle 에서 정의하면 부모 컴포넌트에서 단순하게 이를 호출하여 사용할 수 있다.

import React, { useRef, useImperativeHandle } from 'react';

import classes from './Input.module.css';

const Input = React.forwardRef((props, ref) => {
  const inputRef = useRef();

  const activate = () => {
    inputRef.current.focus();
  };

  useImperativeHandle(ref, () => {
    return {
      focus: activate,
    };
  });

  return (
    <div
      className={`${classes.control} ${
        props.isValid === false ? classes.invalid : ''
      }`}
    >
      <label htmlFor={props.id}>{props.label}</label>
      <input
        ref={inputRef}
        type={props.type}
        id={props.id}
        value={props.value}
        onChange={props.onChange}
        onBlur={props.onBlur}
      />
    </div>
  );
});

export default Input;

이 메서드를 호출하기 위해 부모 컴포넌트에서는 해당 ref.current에 정의된 메서드를 호출하기만 하면 된다.

// 부모가 자식에게 내려줄 ref
const emailInputRef = useRef();
const passwordInputRef = useRef();

const submitHandler = (event) => {
    event.preventDefault();
    if (formIsValid) {
      authCtx.onLogin(emailState.value, passwordState.value);
    } else if (!emailIsValid) {
      emailInputRef.current.focus();
    } else {
      passwordInputRef.current.focus();
    }
  };
// 부모 컴포넌트 코드 일부
 <Input
      ref={emailInputRef}
      id="email"
      label="E-Mail"
      type="email"
      isValid={emailIsValid}
      value={emailState.value}
      onChange={emailChangeHandler}
      onBlur={validateEmailHandler}
/>
<Input
      ref={passwordInputRef}
      id="password"
      label="Password"
      type="password"
      isValid={passwordIsValid}
      value={passwordState.value}
      onChange={passwordChangeHandler}
      onBlur={validatePasswordHandler}
/>

이렇게 할 때의 장점은 child component에 상태나 로직을 isolate할 수 있다는 점이다. 굳이 Redux나 Context API를 사용할 만큼의 일이 아닐 때 useImperativeHandle을 사용할 수도 있다. 상태나 로직들은 자식 컴포넌트가 갖고 있고, 부모 컴포넌트는 ref.current에서 필요한 프로퍼티를 가져오기만 하면 되기 때문이다.

 

일반적으로 forwardRef() 함수는 HTML 엘리먼트 대신에 사용되는 최말단 컴포넌트(ex. <Input/>, <Button/>)를 대상으로 주로 사용되며, 유지보수의 용이성을 위해 그 보다 상위 컴포넌트에서는 forwardRef() 함수를 사용하는 것이 권장되지 않는다고 한다. 

 

참고한 블로그)

https://www.daleseo.com/react-forward-ref/

https://developer-alle.tistory.com/372

반응형