0%

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

createRef()와 useRef()의 차이점

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

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

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

useRef Hook

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


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

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

useRefcurrent 속성을 가지고 있는 객체를 반환하는데, 인자로 넘어온 초기값을 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>
</>
);
}