프론트엔드 첫걸음

useImperativeHandle 과 forwardRef 본문

개발 공부/React

useImperativeHandle 과 forwardRef

차정 2022. 7. 27. 02:39

/* 본인의 이해를 돕기 위해 적혀진 글입니다. 오류가 있을 수 있습니다. */

사용상황


아래와 같이 공통으로 사용할 Input 컴포넌트 만들어준 다음에  Login 컴포넌트에서 재사용하려고 한다.


Input.js

import React, { useEffect, useRef } from 'react';
import classes from './Input.module.css';

const Input = (props) => {
  const inputRef = useRef();

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

  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;

 


Login.js

import React, {
  useState,
  useEffect,
  useReducer,
  useContext,
  useRef,
} from 'react';


import Input from '../UI/Input/Input';

const Login = (props) => {

  const emailInputRef = useRef();
  const passwordInputRef = useRef();

  const submitHandler = (event) => {
    event.preventDefault();
    if (formIsValid) {
      authCtx.onLogin(emailState.value, passwordState.value);
    } else if (!emailValid) {
      emailInputRef.current.activate();
    } else {
      passwordInputRef.current.activate();
    }
  };

  return (
    <Card className={classes.login}>
      <form onSubmit={submitHandler}>
        <Input
          ref={emailInputRef}
          id='email'
          label='E-mail'
          type='email'
          isValid={emailValid}
          value={emailState.value}
          onChange={emailChangeHandler}
          onBlur={validateEmailHandler}
        />
        <Input
          ref={passwordInputRef}
          id='password'
          label='Password'
          type='password'
          isValid={passwordValid}
          value={passwordState.value}
          onChange={passwordChangeHandler}
          onBlur={validatePasswordHandler}
        />

        <div className={classes.actions}>
          <Button type='submit' className={classes.btn}>
            Login
          </Button>
        </div>
      </form>
    </Card>
  );
};

export default Login;

(포스팅 목적에 맞지않는 코드들은 생략함) 

이렇게 Input 컴포넌트에  Login 컴포넌트에서 만들어준 InputRef를 넘겨서
inputRef가 가리키는 dom을 focus하려고 했지만 실행되지 않는다.
props는 ref를 받을 수 없기 때문이다. 
useRef가 사용하는 focus함수를 하위 컴포넌트에서 사용하게 할 수는 없을까?

이 때 useImperativeHandle 훅을 사용하면 해결된다. 
useImperativeHandle 훅을 사용하면 컴포넌트나 컴포넌트 내부에서 오는 기능들을
명령적으로(Imperative) 사용할 수 있게 한다.
일반적인 state, props 상태관리로 컴포넌트를 제어하거나 
부모컴포넌트의 state 통해서 컴포넌트를 제어하지 않고, 
컴포넌트에서 무언가를 직접 호출하거나 조작하여 사용하게 한다.

 

 

사용방법 

1. 자식컴포넌트에서 useImperativeHandle 호출 +  React.forwardRef로 컴포넌트 감싸주기
  useImperativeHandle의 첫번째 매개변수는 ref 로 자식컴포넌트의 ref 와 부모컴포넌트의 ref를 바인딩 시킨다.
두번째 매개변수는 객체를 반환하는 함수로 이를 활성화 하려면 컴포넌트 함수를 React.forwardRef 로 감싸줘야한다. 
컴포넌트 함수 자체가 forwardRef의 첫번째 인수가 된다.
forwardRef가 반환한 리액트 컴포넌트는 ref에 바인딩 될 수 있는 리액트 컴포넌트다.

Input.js

import React, { useEffect, 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;

 이렇게 하면 이 컴포넌트를 사용하는 부모컴포넌트는 inputRef.current.focus()를 호출할 수 있다.

 

2. 부모컴포넌트에서 ref와 연결된 useImperativeHandle의 함수(노출되는 인스턴스값) 사용하기 

  const submitHandler = (event) => {
    event.preventDefault();
    if (formIsValid) {
      authCtx.onLogin(emailState.value, passwordState.value);
    } else if (!emailValid) {
      emailInputRef.current.focus();
    } else {
      passwordInputRef.current.focus();
    }
  };

 

 

useImperativeHandle은 ref를 사용할 때 부모 컴포넌트에 노출되는 인스턴스 값을 사용자화(customizes)합니다.

 

자식컴포넌트에서 커스터마이징해서 return 한 { focus : activate }  객체를
부모컴포넌트의 inputRef가  .current.focus() 로 갖다 쓸 수 있게끔
자식컴포넌트의 ref를 부모 컴포넌트의 ref에 연결시킨다.  인 것 같다.

 

https://reactjs.org/docs/hooks-reference.html#useimperativehandle