0%

useRef를 이용해 여러 ref object 관리하기

createRef()와 useRef()의 차이점

createRef : 클래스형 컴포넌트에서 사용

useRef : 함수형 컴포넌트에서 사용

  • 클래스형 컴포넌트는 인스턴스를 생성 후 render 코드 블록 쪽만 리랜더링후 다시 실행
  • 함수형 컴포넌트는 함수 블록 안에 있는 모든 것을 리랜더링시 마다 다시 실행
  • 함수형 컴포넌트에서 createRef를 사용 할 시 ref 값이 초기화 되어서 원하는 값을 얻지 못하기 떄문에, 리액트 훅인 useRef를 사용: 리액트 훅을 사용하면 useState 값을 리랜더링 할 때 내부적으로 값을 기억하듯이 useRef도 내부적으로 ref 값을 기억함.

useRef Hook

React 컴포넌트는 기본적으로 내부 상태(state)가 변할 때 마다 다시 랜더링(rendering)이 된다. 컴포넌트 함수가 다시 호출이 된다는 것은 함수 내부의 변수들이 모두 초기화가 되고 함수의 모든 로직이 다시 실행된다는 것을 의미한다.


다시 렌더링 되어도 동일한 참조값을 유지하려면?

리렌더링 시 함수 내부의 변수들이 기존에 저장하고 있는 값들을 잃어버리고 초기화되는데, 간혹 다시 랜더링이 되더라도 기존에 참조하고 있던 컴포넌트 함수 내의 값이 그대로 보존되야 하는 경우가 있다.

useRef 는 current 속성을 가지고 있는 객체를 반환하는데, 인자로 넘어온 초기값을 current 속성에 할당한다. 이 current 속성은 값을 변경해도 상태를 변경할 때처럼 React 컴포넌트가 다시 랜더링되지 않는다. React 컴포넌트가 다시 랜더링될 때도 마찬가지로 이 current 속성의 값이 유실되지 않는다.

useRef()를 사용하는 경우

  • DOM 노드나 React 엘리먼트에 직접 접근하기 위해서

    React의 ref prop은 HTML 엘리먼트의 레퍼런스를 변수에 저장하기 위해서 사용한다.

    예를 들어, 다음과 같이 <input> 엘리먼트에 ref prop으로 inputRef라는 변수를 넘기게 되면, 우리는 이 inputRef 객체의 current 속성을 통해서 <input> 엘리먼트에 접근할 수 있고, DOM API를 이용하여 제어할 수 있습니다.

    1
    <input ref={inputRef} />
    • input 엘리먼트 제어
    • audio 엘리먼트 제어

  1. useRef에 ref element들을 할당해야 하므로 배열로 초기값 선언

    객체를 할당해도 된다.

    1
    const refs = useRef([]);
  2. 위와 같이 빈 배열(객체)을 전달하면, 여러개의 DOM element를 refs에 담을 수 있다.

    1
    2
    3
    4
    5
    6
    <div>
    <div ref={el => (refs.current[0] = el)} />
    <div ref={el => (refs.current[1] = el)} />
    <div ref={el => (refs.current[2] = el)} />
    <div ref={el => (refs.current[3] = el)} />
    </div>
    • 각 div의 ref에는 useRef 자체가 아니라 useRef 배열의 index를 설정해주어야 한다.
    • 객체를 할당할 때에는 property로 접근하면 된다.
  3. 할당한 객체에 맞게 접근하기 위해, ref.current에서 배열의 index나 객체의 property를 이용하여 접근한다.

    1
    refs.current[currentInput]

Uncontrolled Component 문제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function CustomTextInput(props) {
// textInput must be declared here so the ref can refer to it
const textInput = useRef(null);
const numberInput = useRef(null);

const handleTextInputClick = () => {
textInput.current.focus();
}
const handleNumberInputClick = () => {
numberInput.current.focus();
}

return (
<div>
<input
type="text"
ref={textInput} />
<input
type="number"
ref={numberInput} />
<input
type="button"
value="Focus the text input"
onClick={handleTextInputClick}
/>
<input
type="button"
value="Focus the number input"
onClick={handleNumberInputClick}
/>
</div>
);
}
  • 관리해야 할 엘리먼트들이 늘어날수록, 정의해야 할 useRef 또한 정비례하여 증가하게 된다.

useRef를 정의하는 곳과 사용하는 곳이 한 곳에 존재하면, react hooks의 특성상 동적으로 그 개수를 늘려가며 선언할 수 없다. 반면 방법1과 방법2에서는 동적으로 늘어나도 동일한 매커니즘으로 동작할 수 있다.

해결 방안1. Controlled Component + Uncontrolled Component

  • 리액트스러움: DOM을 제어하는 부분을 가려둔채 컴포넌트로만 보게 되면, CustomTextInput 를 사용하는 입장에서 내부 구현을 알 수 없는채로 “무엇”이 나타나길 기대하며 그 방법을 알지 못해도 된다는 점에서 “선언적”이라고 볼 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function CustomTextInput({focused}) {
// textInput must be declared here so the ref can refer to it
const textInput = useRef(null);

const handleClick = () => {
textInput.current.focus();
}

useEffect(()=>{
if(!focused){
return
}
textInput.current.focus();
},[])

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

const InputID = {
first:'first',
second:'second'
}

function Form(){
const [focused, setFocused] = useState('')
const changeFocus = (id) => { setFocused(id) }

return <>
<CustomTextInput focused={focused===InputID.first}/>
<CustomTextInput focused={focused===InputID.second}/>
<button type="button" onClick={()=>changeFocus(InputID.first)} >focus first</button>
<button type="button" onClick={()=>changeFocus(InputID.second)} >focus second</button>
</>
}
  • useRef를 관리할 엘리먼트 수만큼 정의해야 하는 단점이 있지만, 이 경우에 그 기능이 각 컴포넌트와 props로 가려지기 때문에 복잡한 상태를 최소한의 노력으로 관리할 수 있다.

해결 방안2. Uncontrolled Component

  • 한정된 기능만 제공: 해당 컴포넌트를 “어떻게” 제어해야 하는지 그대로 노출하고 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Form({focused}) {
const inputRefs = useRef([]);

const handleFocus = (index) => {
inputRefs.current[index]?.focus();
}

return (
<>
<input type="text" ref={(elem) => inputRefs.current[0] = elem} />
<input type="text" ref={(elem) => inputRefs.current[1] = elem} />

<button type="button" onClick={()=>handleFocus(0)} >focus first</button>
<button type="button" onClick={()=>handleFocus(1)} >focus second</button>
</>
);
}