React는 Virtual DOM을 사용해 요소를 한꺼번에 렌더링하므로 DOM을 직접 조작할 때보다 속도가 빠르지만, <input> 요소 focus, 미디어 조작, 애니메이션 적용 등 DOM 요소의 주소값을 활용해야 하는 경우 DOM을 조작할 수밖에 없음. 그럴 때 사용 가능한 Hook이 useRef().
A. 특정 DOM 요소 선택하기
javascript에서 DOM 요소를 선택하기 위해 Element.querySelector(), Element.getElementById()같은 메서드를 사용하는데 React에서도 동일한 개념으로 ref라는 것을 사용함.
useRef(null)을 통해 ref 객체를 초기화 해줄 수 있으며, 이때 변수 checkBox에는{ current: null }객체가 할당됨. 해당 컴포넌트의 JSX 표현식에서 참조할 요소의 속성으로 'ref'를 지정해주면 해당 컴포넌트가 마운트된 이후에 해당 요소가 current 속성에 저장됨. 이후 checkBox.current와 같은 식으로 해당 요소를 조작할 수 있음.
DOM 요소를 선택해서 사용할 때는 그 전에 자식 컴포넌트 및 HTML 요소가 마운트되어 있어야 하므로 해당 컴포넌트가 최초로 렌더링될 때에는 useEffect() 안에서 해당 요소를 사용할 수 있음에 주의!
데이터를 추가할 때 해당 데이터의 id값을 컴포넌트 내부에 정의된 nextId 값으로 지정하려고 함. 하지만 위 코드처럼 단순하게 지역변수를 정의하면컴포넌트의 Props, State가 변경될 때마다 해당 컴포넌트가 호출되고, 지역 변수가 초기화됨. 따라서 nextId 값이 항상 0으로 초기화되어 모든 데이터의 id가 0이 되어버됨.
// 2. useRef() 이용
function App() {
let nextId = useRef(0);
const onCreate = () => {
...
nextId.current++;
};
};
이처럼 해당 컴포넌트가 지속적으로 참조할 변수를 만들 때는 useRef()를 이용할 수 있음. useRef()를 이용해 만들어진 객체는 React의 전역 저장소에 저장되기 때문에 컴포넌트를 재호출하더라도 nextId 변수가 초기화되지 않고 마지막으로 업데이트한 current 속성값이 유지됨. 단, Props, State와 달리 ref 객체의 값이 변하더라도 컴포넌트는 재렌더링되지 않으므로 주의!
Click to Edit과 같은 기능을 구현할 때 <div>, <span> 요소를 <input> 요소로 바꾸면 포커싱된 상태가 아니기 때문에 내용을 편집하기 위해서는 요소를 두번 클릭해야 하는 불편함이 있음. DOM에 HTMLElement.focus() 메서드가 있지만 React는 가상 DOM을 이용하기 때문에 해당 메서드를 직접 사용할 수 없음. 이때, useRef()를 써서 <input> 요소를 포커싱해줄 수 있음.
React에서 <input> 요소의 값을 참조하고 싶을 때, 보통 useState()를 이용해 값을 저장하는 방법이 많이 쓰임. 자동 완성 등 값이 입력될 때마다 컴포넌트를 재렌더링해야 하는 경우에는 적절한 방법이 될 수 있으나, 그렇지 않은 경우에는 오히려 불필요한 렌더링만 발생시켜 성능 저하 문제가 발생할 수 있음. 이런 경우에는 useRef()로 <input> 요소를 참조하고, 전송 등 버튼을 눌렀을 때에서야 State를 변경하거나 서버에 데이터를 보내도록 하면 불필요한 렌더링을 방지할 수 있음.
※ ref로 DOM 요소를참조했을 때, ref.current === event.target 이므로 <input>의 값은 ref.current.value로 접근할 수 있음.
const App = () => {
const [input, setInput] = useState(null);
const ref = useRef(null); // ref 객체를 이용
const onSubmit = () => {
setInput(ref.current.value); // 전송 버튼을 눌렀을 때만 State 변경
};
console.log("재렌더링");
return (
<>
<label>
<input type="text" ref={ref} />
<button onClick={onSubmit}>전송</button>
</label>
</>
);
};
useRef(), useState()를 사용했을 때 차이점
useRef()를 사용하면 전송 버튼을 눌렀을 때만 컴포넌트 재렌더링이 되지만, useState()를 사용하면 값을 입력할 때마다 컴포넌트가 재렌더링되어 성능 문제가 발생할 수 있음. <input> 요소에 꼭 useState()를 사용해야 하는 상황이 아니라면 useRef()를 사용하는 것이 좋음.
D. 비동기적으로 반환되는 객체의 이벤트 제거하기
useEffect()에서 비동기 작업의 결과로 반환되는 객체를 async 함수 블록 또는 then 블록의 외부 스코프에서 접근해야 할 때가 있음. 하지만, 외부 스코프에서 내부 스코프의 변수에 접근하는 것은 불가능하기 때문에 Ref 객체에 해당 변수를 할당하고, 필요할 때 Ref 객체를 사용하는 방법이 있음.
예를 들어, Navigator 객체로 특정 권한이 부여되어있는지 확인해야 할 때, navigator.permissions.query() 메서드를 사용할 수 있음. 이 메서드는 Promise를 반환하기 때문에 async 함수 블록 또는 then 블록 내부에서 이벤트 핸들러를 추가해줘야 함.
useEffect(() => {
(async () => {
const permissionStatus = await navigator.permissions.query({
name: "geolocation",
});
// 비동기 결과에 이벤트 핸들러 추가
permissionStatus.addEventListener("change", permissionHandler);
})();
}, []);
하지만, permissionStatus가 async 함수 내부에서 선언되었기 때문에 useEffect()에서 클린업 함수로 이벤트 핸들러를 제거해줄 수가 없음.
Ref 객체에 하나의 요소만 저장할 경우에는 문제가 없지만, 리스트처럼 여러 요소를 하나의 Ref에 담아야할 때는 useRef()를 여러 번 쓰기에는 복잡해짐. 이럴 때는 컴포넌트의 ref 속성으로 (element) => void 형식의 함수를 할당해서 elelmentRef.current[i] = element처럼 Ref 객체를 할당해줄 수 있음.
4. useEffect()와 함께 사용하기
ref 객체 또한 컴포넌트 내부에 존재하는 변수이므로 당연히 useEffect()에서도 참조할 수 있음. 하지만, ref를 useEffect()의 dependency로 지정하게 되면 얘기가 조금 달라짐.
버튼을 눌렀을 때 countUpRef()를 실행해 ref.current 값을 1만큼 증가시키는 상황을 생각해보자. ref가 변경되어도 컴포넌트는 재렌더링되지 않으므로 그에 따른 Effect 실행도 불가능함. 따라서, ref 값을 변경하는 작업만 하는 게 아니라면 State도 같이 변경해줘야 함.
"State까지 같이 변경"해 어떠한 Effect를 실행하려고 할 때, ref 객체는 React에서 유일하게 mutable한 변경이 허용되므로 ref 객체 자체를 useEffect()의 dependency로 사용할 수 없음.(ref 객체의 주소값은 컴포넌트가 재렌더링되어도 변하지 않으므로) 즉, 매번 주소 또는 값이 달라지는 ref.current를 dependency로 사용해야 함.
5. useRef() 구현해보기
컴포넌트가 재렌더링되어도 ref 객체는 이전의 값을 유지해야 한다는 특징은 클로저의 특징과 같음. 따라서, useRef()를 즉시실행함수 표현과 클로저를 활용해 아래와 같이 만들어 줄 수 있음. 한편, 컴포넌트가 재렌더링될 때마다 이렇게 만든 ref() 함수 또한 다시 실행됨. 함수가 다시 실행되는 것 자체를 막는 것은 불가능하지만 매번 초기화를 하는 것은 막을 수 있음. ref()가 매번 실행될 때 ref(value)에서 value 값이 동일한 값으로 들어온다는 의미이므로 initial 변수를 만들어 초기값을 저장한 후, ref()가 실행될 때마다 두번째, 세번째, ...로 실행된 것인지 판별하면 컴포넌트가 재렌더링될 때마다 초기화되어 버리는 문제를 해결할 수 있음.
const ref = (() => {
let initial;
const res = { current: null };
return (value) => {
if (initial !== value) { // ref()를 처음 설정할 때만 실행되는 부분
initial = value;
res.current = value;
}
return res;
};
})();
useRef()를 이용해 어떠한 값이나 DOM 요소를 저장하면 current 속성에 저장됨. 하지만, 매번 current 속성으로 값에 접근하는 것이 번거롭다고 해서 아래와 같이 구조 분해 할당을 사용하면 useRef()를 제대로 사용할 수 없음.
const { current: value } = useRef(0);
// useRef()를 잘못 사용하는 예
const App = () => {
const [state, setState] = useState(0);
let { current } = useRef(0);
useEffect(() => {
current += 1;
console.log("렌더링 완료 후", current);
}, []);
return (
<button onClick={() => setState((prev) => prev + 1)}>재렌더링</button>
);
};
App 컴포넌트가 마운트되었을 때는 콘솔창에 current 값이 1로 출력되어 잘 작동하는 것처럼 보임. 하지만, State가 변경되어 App 컴포넌트가 재렌더링되면 함수 본문이 다시 실행되어 변수 current도 초기화 후 재설정되므로 current가 1로 유지되는 게 아니라 계속 0이 출력됨. useRef()는 메모이제이션을 통해 변수를 지속적으로 참조할 수 있도록 하는데, 이렇게 구조 분해 할당으로 컴포넌트 본문의 일반 변수로 설정을 하면 useRef()를 사용하는 의미가 없음.
useRef()
1. useRef() 사용하기
React는 Virtual DOM을 사용해 요소를 한꺼번에 렌더링하므로 DOM을 직접 조작할 때보다 속도가 빠르지만, <input> 요소 focus, 미디어 조작, 애니메이션 적용 등 DOM 요소의 주소값을 활용해야 하는 경우 DOM을 조작할 수밖에 없음. 그럴 때 사용 가능한 Hook이 useRef().
A. 특정 DOM 요소 선택하기
javascript에서 DOM 요소를 선택하기 위해 Element.querySelector(), Element.getElementById()같은 메서드를 사용하는데 React에서도 동일한 개념으로 ref라는 것을 사용함.
useRef(null)을 통해 ref 객체를 초기화 해줄 수 있으며, 이때 변수 checkBox에는 { current: null } 객체가 할당됨. 해당 컴포넌트의 JSX 표현식에서 참조할 요소의 속성으로 'ref'를 지정해주면 해당 컴포넌트가 마운트된 이후에 해당 요소가 current 속성에 저장됨. 이후 checkBox.current와 같은 식으로 해당 요소를 조작할 수 있음.
DOM 요소를 선택해서 사용할 때는 그 전에 자식 컴포넌트 및 HTML 요소가 마운트되어 있어야 하므로 해당 컴포넌트가 최초로 렌더링될 때에는 useEffect() 안에서 해당 요소를 사용할 수 있음에 주의!
B. 컴포넌트 내부의 변수 관리하기
데이터를 추가할 때 해당 데이터의 id값을 컴포넌트 내부에 정의된 nextId 값으로 지정하려고 함. 하지만 위 코드처럼 단순하게 지역변수를 정의하면 컴포넌트의 Props, State가 변경될 때마다 해당 컴포넌트가 호출되고, 지역 변수가 초기화됨. 따라서 nextId 값이 항상 0으로 초기화되어 모든 데이터의 id가 0이 되어버됨.
이처럼 해당 컴포넌트가 지속적으로 참조할 변수를 만들 때는 useRef()를 이용할 수 있음. useRef()를 이용해 만들어진 객체는 React의 전역 저장소에 저장되기 때문에 컴포넌트를 재호출하더라도 nextId 변수가 초기화되지 않고 마지막으로 업데이트한 current 속성값이 유지됨. 단, Props, State와 달리 ref 객체의 값이 변하더라도 컴포넌트는 재렌더링되지 않으므로 주의!
2. 활용 예시
A. <input> 요소 포커싱
Click to Edit과 같은 기능을 구현할 때 <div>, <span> 요소를 <input> 요소로 바꾸면 포커싱된 상태가 아니기 때문에 내용을 편집하기 위해서는 요소를 두번 클릭해야 하는 불편함이 있음. DOM에 HTMLElement.focus() 메서드가 있지만 React는 가상 DOM을 이용하기 때문에 해당 메서드를 직접 사용할 수 없음. 이때, useRef()를 써서 <input> 요소를 포커싱해줄 수 있음.
B. 미디어 제어
C. <input> 요소 값 참조하기
React에서 <input> 요소의 값을 참조하고 싶을 때, 보통 useState()를 이용해 값을 저장하는 방법이 많이 쓰임. 자동 완성 등 값이 입력될 때마다 컴포넌트를 재렌더링해야 하는 경우에는 적절한 방법이 될 수 있으나, 그렇지 않은 경우에는 오히려 불필요한 렌더링만 발생시켜 성능 저하 문제가 발생할 수 있음. 이런 경우에는 useRef()로 <input> 요소를 참조하고, 전송 등 버튼을 눌렀을 때에서야 State를 변경하거나 서버에 데이터를 보내도록 하면 불필요한 렌더링을 방지할 수 있음.
※ ref로 DOM 요소를 참조했을 때, ref.current === event.target 이므로 <input>의 값은 ref.current.value로 접근할 수 있음.
useRef()를 사용하면 전송 버튼을 눌렀을 때만 컴포넌트 재렌더링이 되지만, useState()를 사용하면 값을 입력할 때마다 컴포넌트가 재렌더링되어 성능 문제가 발생할 수 있음. <input> 요소에 꼭 useState()를 사용해야 하는 상황이 아니라면 useRef()를 사용하는 것이 좋음.
D. 비동기적으로 반환되는 객체의 이벤트 제거하기
useEffect()에서 비동기 작업의 결과로 반환되는 객체를 async 함수 블록 또는 then 블록의 외부 스코프에서 접근해야 할 때가 있음. 하지만, 외부 스코프에서 내부 스코프의 변수에 접근하는 것은 불가능하기 때문에 Ref 객체에 해당 변수를 할당하고, 필요할 때 Ref 객체를 사용하는 방법이 있음.
예를 들어, Navigator 객체로 특정 권한이 부여되어있는지 확인해야 할 때, navigator.permissions.query() 메서드를 사용할 수 있음. 이 메서드는 Promise를 반환하기 때문에 async 함수 블록 또는 then 블록 내부에서 이벤트 핸들러를 추가해줘야 함.
하지만, permissionStatus가 async 함수 내부에서 선언되었기 때문에 useEffect()에서 클린업 함수로 이벤트 핸들러를 제거해줄 수가 없음.
이때, useRef()를 이용해 permissionStatus를 저장해두고 컴포넌트가 언마운트되는 등 "change" 이벤트를 더이상 구독할 필요가 없을 때 이벤트 핸들러를 제거해줄 수 있음.
navigator.permissions.query()가 Promise를 반환하고 이벤트 핸들러가 추가되는 시점과 permissionRef.current으로 이벤트 핸들러를 제거할 수 있는 시점이 다르기 때문에 위와 같이 useEffect()를 분리해서 사용함.
3. HTML 요소의 리스트를 저장하기
Ref 객체에 하나의 요소만 저장할 경우에는 문제가 없지만, 리스트처럼 여러 요소를 하나의 Ref에 담아야할 때는 useRef()를 여러 번 쓰기에는 복잡해짐. 이럴 때는 컴포넌트의 ref 속성으로 (element) => void 형식의 함수를 할당해서 elelmentRef.current[i] = element처럼 Ref 객체를 할당해줄 수 있음.
4. useEffect()와 함께 사용하기
ref 객체 또한 컴포넌트 내부에 존재하는 변수이므로 당연히 useEffect()에서도 참조할 수 있음. 하지만, ref를 useEffect()의 dependency로 지정하게 되면 얘기가 조금 달라짐.
버튼을 눌렀을 때 countUpRef()를 실행해 ref.current 값을 1만큼 증가시키는 상황을 생각해보자. ref가 변경되어도 컴포넌트는 재렌더링되지 않으므로 그에 따른 Effect 실행도 불가능함. 따라서, ref 값을 변경하는 작업만 하는 게 아니라면 State도 같이 변경해줘야 함.
"State까지 같이 변경"해 어떠한 Effect를 실행하려고 할 때, ref 객체는 React에서 유일하게 mutable한 변경이 허용되므로 ref 객체 자체를 useEffect()의 dependency로 사용할 수 없음.(ref 객체의 주소값은 컴포넌트가 재렌더링되어도 변하지 않으므로) 즉, 매번 주소 또는 값이 달라지는 ref.current를 dependency로 사용해야 함.
5. useRef() 구현해보기
컴포넌트가 재렌더링되어도 ref 객체는 이전의 값을 유지해야 한다는 특징은 클로저의 특징과 같음. 따라서, useRef()를 즉시실행함수 표현과 클로저를 활용해 아래와 같이 만들어 줄 수 있음. 한편, 컴포넌트가 재렌더링될 때마다 이렇게 만든 ref() 함수 또한 다시 실행됨. 함수가 다시 실행되는 것 자체를 막는 것은 불가능하지만 매번 초기화를 하는 것은 막을 수 있음. ref()가 매번 실행될 때 ref(value)에서 value 값이 동일한 값으로 들어온다는 의미이므로 initial 변수를 만들어 초기값을 저장한 후, ref()가 실행될 때마다 두번째, 세번째, ...로 실행된 것인지 판별하면 컴포넌트가 재렌더링될 때마다 초기화되어 버리는 문제를 해결할 수 있음.
6. 주의할 점
A. useRef()는 객체 형태 그대로 사용해야 함
useRef()를 이용해 어떠한 값이나 DOM 요소를 저장하면 current 속성에 저장됨. 하지만, 매번 current 속성으로 값에 접근하는 것이 번거롭다고 해서 아래와 같이 구조 분해 할당을 사용하면 useRef()를 제대로 사용할 수 없음.
App 컴포넌트가 마운트되었을 때는 콘솔창에 current 값이 1로 출력되어 잘 작동하는 것처럼 보임. 하지만, State가 변경되어 App 컴포넌트가 재렌더링되면 함수 본문이 다시 실행되어 변수 current도 초기화 후 재설정되므로 current가 1로 유지되는 게 아니라 계속 0이 출력됨. useRef()는 메모이제이션을 통해 변수를 지속적으로 참조할 수 있도록 하는데, 이렇게 구조 분해 할당으로 컴포넌트 본문의 일반 변수로 설정을 하면 useRef()를 사용하는 의미가 없음.
[참고]
Why react `useRef` hook stores the objects in `current` property? Why cant it store directly in the ref object?
useState vs. useRef: Similarities, differences, and use cases
How can I use multiple refs for an array of elements with hooks?
'JavaScript > React' 카테고리의 다른 글