Javascript/React

useRef, useImperativeHandle, forwardRef

kyoulho 2024. 7. 20. 15:05

useRef와 useImperativeHandle은 ref라는 속성에 적용하는 값을 만들어 주는 훅이다. 리액트와 리액트 네이티브가 제공하는 컴포넌트는 모두 ref라는 이름의 속성을 가지고 있다.

ref 속성이란?


Ref는 참조를 의미한다.
ref 속성값은 사용자 코드에서 설정하는 것이 아니라, 특정 시점에 React 프레임워크 내부에서 설정해 준다. ref 속성은 초기에는 null이지만, 컴포넌트가 마운트 되는 시점에서 실제 DOM 객체의 값이 된다.
HTML 요소들은 자바스크립트에서 DOM 타입 객체이다. 모든 요소는 HTMLElement 타입이며, click(), blur(), focus() 메서드를 제공한다. 이 메서드들은 가상 DOM 상태에서는 호출할 수 없고, 실제 DOM 상태에서만 호출할 수 있다. ref 속성값은 실제 DOM 상태일 때의 값이므로, ref로 얻은 값(즉, DOM 객체)을 사용하여 click()과 같은 메서드를 호출할 수 있다.

interface RefAttributes<T> extends Attributes {
    ref?: Ref<T> | undefined;
}

interface RefObject<T>{
    // 가상 DOM 타입일 때는 null
    // 또한 current는 리액트에서 설정해주는 값이므로 readonly이다.
    readonly current: T | null;
}

type Ref<T> = RefObject<T> | null;

 

useRef


// useRef
function useRef<T>(initialValue: T): MutableRefObject<T>;

interface MutableRefObject<T> {
    current: T;
}

 

예제 1

만일 useRef가 없었다면 input 요소에 id를 부여하고 onClick 함수에서 input요소를 찾아서 click 함수를 호출했어야 했을 것이다.

export default function Click() {
    const inputRef = useRef<HTMLInputElement>(null)
    const onClick = useCallback(() => inputRef.current?.click(), [])

    return (
        <section className={'mt-4'}>
            <div className={"mt-4 flex justify-center items-center"}>
                <Button className={"btn-primary mr-4"} onClick={onClick}>
                    CLICK ME
                </Button>
                <input ref={inputRef} className={"hidden"} type={"file"} accept={"image/*"}/>
            </div>
        </section>
    )
}

inputRef.current는 초깃값을 null로 설정했다가 리액트가 값을 바꾸므로 useCallback 훅의 의존성 목록에 inputRef.current를 추가해야 할 것 같다. 하지만 useRef 훅 호출로 얻은 inputRef의 current 속성은 그 값이 변해도 다시 렌더링 되지 않도록 설계되었으므로 의존성 목록에 포함하지 않는다.

예제 2

페이지가 열리자마자 입력 상자가 포커싱 된다.

export default function InputFocus() {
    const inputRef = useRef<HTMLInputElement>(null)

    useEffect(() => inputRef.current?.focus(), [])

    return (
        <section className={'mt-4'}>
            <div className={"flex justify-center mt-4"}>
                <input ref={inputRef} className={"input input-primary"} placeholder={"enter some text"}/>
            </div>
        </section>
    )
}

예제 3

리액트에서는 항상 <input>의 value 속성을 얻기 위해서는 useState를 통해 value를 캐싱하고, <input>의 onChange 함수에서 setValue를 해주는 방식을 요구한다. 하지만 ref 속성을 이용하면 바로 value를 얻을 수 있다.

export default function InputValue() {
    const inputRef = useRef<HTMLInputElement>(null)

    const getValue = useCallback(
        () => alert(`input value: ${inputRef.current?.value}`), [])
    
    useEffect(() => inputRef.current?.focus(), [])

    return (
        <section className={'mt-4'}>
            <div className={"flex justify-center mt-4"}>
                <div className={"flex flex-col w-1/3 p-2"}>
                    <input ref={inputRef} className={"input input-primary"} placeholder={"enter some text"}/>
                    <Button onClick={getValue} className={"mt-4 btn-primary"}>
                        GET VALUE
                    </Button>
                </div>
            </div>
        </section>
    )
}

 

forwardRef


forwardRef 함수는 이름대로 부모 컴포넌트에서 생성한 ref를 자식 컴포넌트로 전달해 주는 역할을 한다.

forwarRef 함수가 필요한 이유

import type {DetailedHTMLProps, FC, InputHTMLAttributes} from 'react'

export type ReactInputType = DetailedHTMLProps<
  InputHTMLAttributes<HTMLInputElement>,
  HTMLInputElement
>

export type InputProps = ReactInputType & {}

export const Input: FC<InputProps> = ({className: _className, ...inputProps}) => {
  const className = ['input', _className].join(' ')
  return <input {...inputProps} className={className} />
}

리액트가 제공하는 기본 <input>은 ref 속성에 값을 설정할 수 있지만, 이러한 사용자 컴포넌트에도 똑같이 ref 속성을 설정하기 위해서는 forwardRef가 필요하다.

forwardRef 함수 타입

function forwardRef<T, P = {}>(render: ForwardRefRenderFunction<T,P>): 반환_타입;

T는 ref 대상 컴포넌트(여기서는 input의 타입인 HTMLInputElement), P는 컴포넌트의 속성 타입이다. 그런데 앞서 Input의 속성 타입은 InputProps였다. 따라서 forwardRef 타입 정보에서 타입 변수 P는 InputProps타입이다.

forwardRef를 사용하는 사용자 컴포넌트

import type {DetailedHTMLProps, InputHTMLAttributes} from 'react'
import {forwardRef} from "react";

export type ReactInputType = DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>,
    HTMLInputElement>

export type InputProps = ReactInputType & {}

export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
    const {className: _className, ...inputProps} = props
    const className = ['input', _className].join(' ')
    return <input ref={ref} {...inputProps} className={className}/>
})

 

 

useImperativeHandle


useImperativeHandle 훅은 컴포넌트 내부에서 사용할 기능을 외부에서 타입스크립트 코드로 사용할 수 있도록 해준다. 이 훅을 사용하면 함수나 속성을 자식 컴포넌트의 ref를 통해 부모 컴포넌트에서 호출할 수 있게 된다. useImperativeHandle은 forwardRef와 함께 사용되며, 주로 사용자 정의 메서드를 제공하기 위해 사용된다.

 

useImperativeHandle 함수 타입

function useImperativeHandle<T, R extends T>(
  ref: React.Ref<T> | undefined,
  init: () => R,
  deps?: React.DependencyList
): void;

ref는 forwardRef 호출로 얻는 값이다. 이는 useImperativeHandle 훅은 다른 훅과 달리 사용자 컴포넌트 내부에서만 사용해야 한다는 것을 의미한다. init은 useMemo 훅 때와 유사하게 '() => 메서드_객체' 형태의 함수를 입력하는 용도이다.

 

예제

버튼 클릭시 부모 요소에서 ref를 통해 Input의 value를 가져와서 유효성 검사하는 방식으로 구현할 수도 있을 것이다.

하지만 useImperativeHandle을 이용하면 부모 컴포넌트는 복잡한 검증 로직을 다루지 않고 간결하게 메서드를 호출할 수 있으며, 자식 컴포넌트는 유효성 검사 로직을 중앙집중적으로 관리할 수 있다.

import type { ReactInputProps } from './Input';
import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react';

// ValidatableInputMethods 타입 정의
export type ValidatableInputMethods = {
  validate: () => [boolean, string];
};

// ValidatableInput 컴포넌트 정의
export const ValidatableInput = forwardRef<ValidatableInputMethods, ReactInputProps>(
  ({ type, className: _className, ...inputProps }, methodRef) => {
    // 클래스 이름을 메모이제이션하여 성능을 최적화
    const className = useMemo(() => ['input', _className].join(' '), [_className]);

    // input 요소에 대한 ref 생성
    const inputRef = useRef<HTMLInputElement>(null);

    // useImperativeHandle 훅을 사용하여 부모 컴포넌트에서 사용할 메서드를 정의
    useImperativeHandle(
      methodRef,
      () => ({
        // validate 메서드 정의
        validate: (): [boolean, string] => {
          const value = inputRef.current?.value;
          
          // 입력값이 없으면 오류 메시지 반환
          if (!value || !value.length) return [false, '사용자가 입력한 내용이 없습니다.'];

          // 입력값의 유효성을 검사
          switch (type) {
            case 'email': {
              // 이메일 유효성 검사 정규 표현식
              const regEx =
                /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;
              const valid = regEx.test(value);
              return valid ? [true, value] : [false, '틀린 이메일 주소 입니다.'];
            }
            // 기본 타입이 아닌 경우 오류 메시지 반환
            default:
              return [false, '컴포넌트 타입이 유효하지 않습니다.'];
          }
        }
      }),
      [type]
    );

    return <input {...inputProps} className={className} ref={inputRef} />;
  }
);
import { useRef, useCallback } from 'react';
import { Title } from '../components';
import type { ValidatableInputMethods } from '../theme/daisyui';
import { ValidatableInput } from '../theme/daisyui';
import { Button } from '../theme/daisyui';

/**
 * 부모 요소
 */
export default function ValidatableInputTest() {
  // ValidatableInput 컴포넌트의 메서드에 접근하기 위한 ref 생성
  const methodsRef = useRef<ValidatableInputMethods>(null);

  // 이메일 유효성 검사를 수행하는 콜백 함수
  const validateEmail = useCallback(() => {
    if (methodsRef.current) {
      // validate 메서드를 호출하여 검증 결과를 얻음
      const [valid, valueOrErrorMessage] = methodsRef.current.validate();
      if (valid) {
        alert(`${valueOrErrorMessage}는 유효한 이메일 주소입니다.`);
      } else {
        alert(valueOrErrorMessage);
      }
    }
  }, []);

  return (
    <section className="mt-4">
      <Title>ValidatableInputTest</Title>
      <div className="flex justify-center mt-4">
        <div className="flex flex-col w-1/3 p-2">
          {/* ValidatableInput 컴포넌트, ref를 통해 메서드에 접근 */}
          <ValidatableInput type="email" ref={methodsRef} className="input-primary" />
          {/* 버튼 클릭 시 validateEmail 함수 호출 */}
          <Button onClick={validateEmail} className="mt-4 btn btn-primary">VALIDATE</Button>
        </div>
      </div>
    </section>
  );
}

'Javascript > React' 카테고리의 다른 글

useContext  (0) 2024.07.21
FileReader 클래스  (0) 2024.07.20
컴포넌트 생명 주기와 useEffect, useLayoutEffet  (0) 2024.07.14
깊은 복사와 얕은 복사, 그리고 의존성 목록  (0) 2024.07.13
Form 다루기  (0) 2024.07.13