Back
Featured image of post [React] Ref의 개념과 useImperativeHandle, Uncontrolled form

[React] Ref의 개념과 useImperativeHandle, Uncontrolled form

React에서 DOM에 접근하는 방법 소개

Ref

ref는 render 메서드에서 생성된 DOM 노드나 React 엘리먼트에 접근하는 방법을 제공합니다.

  • 포커스, 텍스트 선택, 혹은 미디어의 재생을 관리할 때
  • 애니메이션을 직접적으로 실행시킬 때
  • React와 같이 서드 파티 DOM 라이브러리를 사용할 때

순수 자바스크립트에서는 UI를 변경하기 위해 DOM API를 사용하여 DOM에 직접적으로 접근하였습니다. React에서는 이러한 방식을 지양하고, ref 객체를 생성하여 접근하도록 권고합니다.

DOM API를 이용하여 접근하기

DOM에 접근하기 위해서는 기존의 DOM API를 사용할 수 있습니다.

import React from 'react';

const Component = () => {
    const hadleClick = () => {
        document.getElementById('inputText').style.color = 'green';
    };

    return (
        <div>
            <input id="inputText" />
            <button onClick={hadleClick}>ChangeStyle</button>
        </div>
    );
};

export default Component;

React에서 DOM API 사용을 지양해야 하는 이유?

  1. 명확한 Element 구별이 어려움

    DOM 요소의 id는 유일해야 합니다. 컴포넌트를 재사용할 때 중복 문제가 발생할 수 있으며, class를 사용한다고 해도 특정 요소를 구별하기 어렵습니다.

  2. 신뢰할 수 있는 React 생명 주기

    React에서는 명확한 생명주기 환경을 제공합니다. 컴포넌트의 componentDidMount, componentDidUpdate 상황을 발생하는 메서드를 실행하기 전에 ref를 수정합니다. 따라서 React에서 공식적으로 제공하는 ref를 사용하는 것이 더욱 신뢰성이 보장됩니다.

createRef

리액트 v16.3부터 제공하는 createRef 메서드를 사용하여 ref를 설정할 수 있습니다. current 프로퍼티에 접근하여 Document API를 사용할 수 있습니다.

import React from 'react';

const Component = () => {
    const inputRef = React.createRef();

    const onChangeStyle = () => {
        inputRef.current = inputRef.current.value;
    };

    return (
        <div>
            <input ref={inputRef} onFocus={onFocusInput} onChange={onChangeInput} />
            <button onClick={onChangeStyle}>ChangeStyle</button>
        </div>
    );
};

export default Component;

useRef

useRef는 함수형 컴포넌트에서 ref를 사용할 수 있도록 제공되는 hook입니다. 기존에 createRef로 생성된 객체는 리렌더링이 발생할 때마다 current 값을 null로 초기화 합니다.
use-ref

import React, { createRef, useState } from 'react';

const Component = () => {
    const inputRef = React.createRef();
    const [_, setState] = useState(null);

    const handleChangeInput = (e) => {
        inputRef.current = e.target.value;
        //강제 리렌더링
        setState({});
    };

    return (
        <div>
            <input ref={inputRef} onChange={handleChangeInput} />
            <button>ChangeStyle</button>
            <p>내용 :{inputRef?.current ?? ''}</p>
        </div>
    );
};

export default Component;

use-ref

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

const Component = () => {
    const inputRef = useRef(null);
    const [_, setState] = useState(null);

    const handleChangeInput = (e) => {
        inputRef.current = e.target.value;
        setState({});
    };

    return (
        <div>
            <input type="text" ref={inputRef} onChange={handleChangeInput} />
            <button>ChangeStyle</button>
            <p>내용 :{inputRef?.current ?? ''}</p>
        </div>
    );
};

export default Component;

useRef() 훅을 사용하면 렌더링이 발생할 때마다 동일한 ref 객체를 제공받을 수 있습니다. 따라서 함수형 컴포넌트 생명 주기 내에서 ref를 사용하고 싶다면 useRef()를 사용하는 것이 옳습니다. 또한 DOM에 직접적으로 접근하는 경우 외에도 리렌더링이 필요하지 않은 독립적인 값 유지에도 활용할 수 있습니다.

useImperativeHandle

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

forwardRef

자식 컴포넌트에 ref를 전달할 때 사용하는 메서드이다. 함수형 컴포넌트에서는 ref 인스턴스 속성이 존재하지 않기 때문에 다른 속성으로 ref를 넘겨주거나, 공식적으로 React에서 제공하는forwardRef 를 사용해 ref를 전달받을 수 있다.

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

//Child
const Input = forwardRef((props, ref) => {
    return <input type="text" ref={ref} />;
});

//Parent
const Component = () => {
    const inputRef = useRef(null);

    const changeStyle = () => {
        inputRef.current.style.border = '1px solid hotpink';
    };
    return (
        <div>
            <Input ref={inputRef} />
            <button onClick={changeStyle}>Change Style</button>
        </div>
    );
};

export default Component;

forwardRef에 랩핑하면 두 번째 인자로 상위에서 선언한 ref를 전달받을 수 있다.

useImperativeHandle

다시 돌아와서, React에서는 *“부모 컴포넌트에 노출되는 인스턴스 값을 사용자화”*할 때 useImprativeHandle을 사용할 수 있다고 안내하고있다. 자식 컴포넌트에서 ref를 생성하고 useImperativeHandle 을 사용해 부모에서 보낸 ref와 커스터마이징 객체를 넘겨주면 부모에서 참조하고 있는 ref에서 작성한 여러 메서드 및 값들에 접근이 가능하게된다.

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

    useImperativeHandle(ref, () => {
        return {
            focus: () => {
                inputRef.current.focus();
            },
            inputValue: inputRef.current.value
        };
    });

    return <input type="text" ref={inputRef} />;
});

const Component = () => {
    const inputRef = useRef(null);

    return (
        <div>
            <Input ref={inputRef} />
            <button
                onClick={() => {
                    //자식에서 선언한 메서드와 값을 참조가 가능하다.
                    const { focus, inputValue } = inputRef.current;
                    console.log(inputValue);
                    focus();
                }}
            >
                changeStyle
            </button>
        </div>
    );
};

useImperativeHandle을 사용하면 다음과 같은 이점을 얻을 수 있습니다.

  • 부모에서 접근할 수 없는 자식 컴포넌트에 작성된 메서드와 값을 참조할 수 있습니다.
  • forwardRef를 사용하면 자식의 DOM에 직접 접근하게 되면서 관련 메서드들을 직접 작성해주어야 했습니다. useImperativeHandle을 사용함으로써 자식 컴포넌트에서 정의된 객체를 통해 접근이 가능합니다. (캡슐화)

UncontrolledForm

form Element가 React 외부에서 작동하는 값을 사용하는 Form입니다. 구체적으로 말하면, React에서 제공하는 state를 사용하지 않고 ref를 사용해 DOM Api에 접근하여 값을 사용하는 형태입니다.

import React, { useRef } from 'react';

const Form = () => {
    const inputRef = useRef(null);
    let value = '';
    const handleSubmit = () => {
        //DOM에 직접 접근하여 value
        alert(inputRef.current.value);
        value = '';
    };

    return (
        <form onSubmit={handleSubmit}>
            <input ref={inputRef} type="text" />
            <button type="submit">Submit</button>
        </form>
    );
};

export default Form;

controlledComponent (제어 컴포넌트)

React 공식 문서에서는 Form 제어 시 controlledComponent 방식으로 작성하길 권고합니다. 제어 컴포넌트는 state 속성을 기반으로 Form을 제어하는 방식을 말합니다. 공식 문서에 따르면 state 사용 시 “신뢰 가능한 단일 출처(single source of truth)”로 관리한다고 합니다.

신뢰 가능한 단일 출처?

사용하는 데이터를 오직 하나의 출처에서 사용하고 수정해야 한다는 규칙입니다. React에서는 useState를 통해 설정한 변수를 여러 곳에서 사용할 수 있습니다.

import React, { useState } from 'react';

const Form = () => {
    const [id, setId] = useState(null);
    const handleForm = () => {
        alert(id);
    };

    const handleUSerId = (e) => {
        setId(e.target.value);
    };
    return (
        <form onSubmit={handleForm}>
            <input type="text" onChange={handleUSerId} />
            <button type="submit">submit</button>
        </form>
    );
};

export default Form;

비교

  • 사용자 입력 값을 실시간으로 접근해야 할 때는 제어 컴포넌트를 사용하는 것이 적합합니다.
  • state 동기화하는 과정에서 리렌더링이 발생하는데 많은 연산이 필요한 경우 비제어 컴포넌트를 사용하는 것이 적합합니다.

참고 자료