Ref
useRef에 대해서 설명하기 앞서 Ref가 무엇이고, 사용 목적이 무엇인지에 대해 명확히 짚고 넘어가도록 하겠습니다. Ref는 Ref는 render 메서드에서 생성된 DOM 노드나 React 엘리먼트에 접근하는 방법을 제공합니다. 이는 클래스형 컴포넌트의 this.setState나 함수형 컴포넌트의 useState와 분리되어 생각해야 하며, 선언적으로 해결할 수 있는 부분을 ref를 통해서 React 엘리먼트를 수정하게 된다면 예상과 다른 동작을 만날 수 있고 유지보수가 어려워집니다. 가이드 문서에서는 바람직한 사용 사례를 크게 3가지로 구분 짓고 있습니다.
1. 포커스, 텍스트 선택영역, 혹은 미디어의 재생을 관리할 때
많은 웹 사이트에서 Top Button을 통해서 스크롤을 맨 위로 이동시키는 기능을 탑재하고 있습니다.
useRef를 통해서 스크롤 기능을 쉽게 구현할 수 있습니다.
// App.tsx
export const App: React.FC = () => {
const containerRef = useRef<HTMLElement>(null);
const handleClick = () => {
if(containerRef && containerRef.current)
containerRef.current.scrollTo(0, 0);
// container.current.scrollTop(0);
// container.current.scrollIntoView();
}
return (
<div id="container" ref={containerRef}>
...
<button onClick={handleClick}>Top</button>
</div>
);
}
스크롤 이동에 대해서 많은 방식이 있으며 각 차이에 대한 글을 참조하시면 좋겠습니다. IE 지원 범위에 따라서 선택해야 하는 것이 차이가 생길 수 있습니다.
2. 애니메이션을 직접적으로 실행시킬 때
특정 버튼 클릭으로 애니메이션을 실행시키는 것은 className을 추가해서 해당 CSS를 적용하는 것과 귀결됩니다.
// App.tsx
class App extends Component {
constructor() {
super();
}
componentDidMount() {
this.animateRef = React.createRef();
}
handleClick = () => {
this.animateRef.current.className.add('active');
}
render() {
return (
<div>
...
<div className="animateTarget" ref={this.animateRef}>
target
</div>
<button onClick={this.handleClick}>
trigger-animation
</button>
</div>
);
}
}
/* styles.css */
.animatedTarget {
position: absolute;
visibility: hidden;
opacity: 0;
}
.active {
animation: color-to-red 1.5s 0.5s 3 alternate forwards;
}
@keyframes color-to-red {
from {
opacity: 0;
visibility: hidden;
}
100% {
transform: translate(15%, 0);
opacity: 1;
visibility: visible;
color: red;
}
}
이러한 구현 방식은 classNames와 useState로 해결할 수 있습니다. useState는 V-DOM의 reconciliation의 연산을 한번 수행하게 되지만, ref를 통한 적용은 그렇지 않기에 작게나마 성능상의 이점을 취할 수 있습니다. 다만 직관적이고 독립적인 애니메이션 적용 기능을 사용하는 것이 아니라 연관된 부분이 존재하고 추가 렌더링을 진행해야 한다면, 성능 저하로 이어질 수 있습니다.
3. 서드 파티 DOM 라이브러리를 React와 같이 사용할 때
D3.js와 같이 DOM을 직접 제어하는 라이브러리를 React와 함께 사용할 때 Ref를 사용하는 것으로 DOM control logic을 React에 위임할 수 있습니다. D3는 바닐라 자바스크립트를 대상으로 구현된 라이브러리라 DOM control에 대해서 React와 다르게 동작합니다. useEffect를 함께 사용해서 SVG계산, 레이아웃, 지오매핑 등 시각화하고 싶은 대상에 대한 계산을 D3에 할당하고, useEffect를 통해서 렌더링을 진행하는 것으로 각 기능을 위임할 수 있습니다. 각 모듈 분리 사용이 부담이 되지만, React로 구성된 구조를 최대한 해치지 않기 때문입니다. 또한 D3에서 생각해야 하는 enter(data로 받아들인 요소의 수가 이미 존재하는 객체보다 많을 때 새로 생성된 요소들을 선택해서 attr / append를 사용), update(data로 받아들인 요소의 수가 같은 경우 해당 데이터를 대상으로 attr를 사용), exit(data로 받아들인 요소의 수가 적은 경우 삭제해야 할 요소들을 선택해서 remove를 사용)과 같은 일련의 생명주기를 useEffect가 대체하기 때문에 hooks에 대해서 익숙하다면, 더욱 빠르게 진행을 할 수 있습니다. Murat Kemalar의 영상을 보시면 이해가 잘 가실 것 같아 첨부합니다.
import React. { useRef, useEffect, useState } from 'react';
import { select } from 'd3';
type Props = unknown;
export const App: React.FC<Props> = () => {
const [data, setData] = useState<number[]>(() => ([10, 20, 30, 50, 80, 130]));
const svgRef = useRef<SVGElement>();
const handleDivide = () => {
setData((currentData => currentData.map(value => value / 2));
}
const handleFilter = () => {
setData((currentData => currentData.filter(value => value % 2));
}
useEffect(() => {
const svg = select(svgRef.current);
svg
.selectAll('circle')
.data(data)
.join(
enter => enter.append('circle').attr('class', 'new'),
update => update.attr('class', 'updated')
exit => exit.remove(),
)
.attr('r', value => value)
.attr('cx', value => value * 2)
.attr('cy', value => value * 2)
.attr('stroke', 'blue');
}, [data]);
return (
<>
<svg ref={svgRef} />
<button onClick={handleDivide}>Divide with 2</button>
<button onClick={handleFilter}>Filter to odd number</button>
</>
)
}
이러한 로직을 custom Hooks로 작성할 수도 있습니다. 해당 부분에 대한 좋은 포스팅 소개드립니다. 다만 이러한 custom Hooks를 위해서 dependencies를 같이 passing 해줘야 하는데, eslint plugin의 도움을 받을 수 없어서 의존성 배열이 맞지 않을 경우 이슈 트레킹이 어려울 수 있습니다.
React를 합치는 경우 circle과 같은 간단한 부분은 사실 적용이 쉬우나, scale이 변하는 경우 계산을 다시 하는 것이 소스 코드 작성의 복잡도를 높입니다. Airbnb에서 제공하는 visx라는 라이브러리를 사용하면서 useMemo를 접목시키면 가독성 있는 소스 코드 작성이 가능합니다.
createRef, useRef
ref는 React.createRef()를 통해서 생성되고 ref 어트리뷰트를 통해서 React Element에 부착됩니다. 노드를 향한 참조는 ref의 current 어트리뷰트에 담기게 됩니다.
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
const handleClick = () => {
console.log(this.myRef.current);
}
render() {
return (
<div ref={this.myRef}>
<button onClick={handleClick}>button</button>
</div>
);
}
}
1. ref 어트리뷰트가 HTML 엘리먼트에 사용되었다면, React.createRef()로 생성된 ref는 자신을 전달받은 DOM Element를 current로 할당받습니다.
2. ref 어트리뷰트가 클래스형 컴포넌트의 대상으로 지정했다면 그 인스턴스를 current 프로퍼티로 전달받습니다.
3. 함수형 컴포넌트는 인스턴스가 없기 때문에 함수형 컴포넌트에 ref를 지정할 수 없습니다. 그러나 1에 의거해서 함수형 컴포넌트 내부의 HTML엘리먼트가 존재한다면, 혹은 함수형 컴포넌트 내부에 클래스형 컴포넌트가 존재한다면 각각은 ref 어트리뷰트의 대상이 가능합니다.
createRef()로 생성된 ref는 컴포넌트가 마운트 될 때 React가 current 프로퍼티에 DOM element를 대입하고, 컴포넌트의 마운트가 해제될 때 current 프로퍼티를 다시 null로 대입합니다. 그렇기 때문에 createRef로 ref를 가지고 있는 컴포넌트 대상 혹은 상위 컴포넌트가 렌더링 되는 경우 current 프로퍼티는 초기화됩니다.
createRef와 비슷한 useRef가 있습니다. 함수형 컴포넌트는 createRef, useRef 모두 사용이 가능하며 클래스형 컴포넌트는 rule of Hooks에 의거해 사용해도 기능하지 않습니다. useRef는 createRef와 다르게 리렌더링이 있더라도 current 프로퍼티를 유지하고 있어서 상태를 유지할 수 있습니다.
Form with hooks
React.FormEvent <HTMLFormElement>를 사용하는 것으로 작성된 양식에 따른 값을 조회하는 방식을, useRef와 useState를 사용하는 것으로 대체할 수 있습니다.
import React, { useEffect, useState, useRef } from "react";
type Props = unknown;
export const InputField: React.FC<Props> = () => {
const inputRef = useRef<HTMLInputElement | null>(null);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
let userName = "";
// case 1
userName = document.querySelector("input")?.value;
// case 2
userName = event.target.elements[0].value;
// case 3
userName = event.currentTarget.elements[0].value;
// case 4
userName = event.target.elements.userNameInput.value;
// case 5
userName = inputRef.current!.value as string;
// case 6
userName = (event.currentTarget.elements.namedItem("userNameInput") as HTMLInputElement).value;
}
return (
<>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="userNameInput">UserName: </label>
<input id="userNameInput" type="text" ref={inputRef} />
{/* <input name="userNameInput" type="text" /> */}
</div>
<button type="submit">Submit</button>
</form>
</>
);
};
export default InputField;
import React, { useState } from "react";
type Props = unknown;
export const DynamicInput: React.FC<Props> = () => {
const [nickName, setNickName] = useState<string>("");
const isLowerCase = nickName === nickName.toLowerCase();
const error = isLowerCase ? null : "nickName must be lower case";
const handleSubmit = (event: any) => {
event.preventDefault();
console.log(nickName);
};
const handleChange = (event: any) => {
setNickName(event.target.value.toLowerCase());
console.log(nickName);
};
return (
<div>
<h1 style={{ color: "blue" }}>DynamicInput</h1>
<form onSubmit={handleSubmit}>
<div>
<label>NickName: </label>
<input type="text" onChange={handleChange} value={nickName} />
</div>
<button type="submit">Submit</button>
</form>
<div style={{ color: "red" }}>{error}</div>
</div>
);
};
export default DynamicInput;
입력을 효과적으로 제어(trim() / toLowerCase() / regEx())할 수 있으나, 각 입력마다 setState()를 호출하기 때문에 <input> tag를 가지고 있는 컴포넌트가 렌더 되기 때문에 에디터와 같이 많은 입력값을 가지고 있는 경우 해당 방식에 대해서 고민이 필요합니다. 많은 입력값을 가지고 있는 경우 useContext를 사용해서 특정 상태에 값을 각각 저장하도록 하는 방법도 있지만, reducer함수의 작성이 결국 많은 분기문들을 거치기 때문에 성능이 좋지 않을 수 있습니다. semantic tag의 특성을 살려서 event.currentTarget.elements.namedItem를 사용하는 것이 성능상 이점을 취할 수도 있습니다. Form과 Event에 대한 정리도 한번 보시면 좋겠습니다.
Custom Hooks
useInput
앞서 사용했던 onChange와 value를 useState와 연결했던 것을 Custom Hooks로 따로 작성하고 재활용할 수 있습니다.
import { useState } from "react";
const useInput = (initialValue) => {
const [value, setValue] = useState(initialValue);
const handleChange = (event) => {
setValue(event.target.value);
};
return {
value,
onChange: handleChange
};
};
export default useInput;
직접 작성할 수도 있으며, react-hookedup에서 가져와서 사용할 수도 있습니다.
import { useInput } from "react-hookedup";
function App () {
const { value, onChange } = useInput("");
return (<input value={value} onChange={onChange} />);
};
usePrevious
값의 변경이 있을 경우 useRef는 리렌더를 진행하지 않고, useState는 리렌더를 진행하는 특징을 통해서 입력에 따라서 기존의 값을 기억하는 hooks를 작성할 수 있습니다.
import { useEffect, useRef, useState } from "react";
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
function App() {
const [ count, setCount ] = useState(0);
const prevCount = usePrevious(count);
...
}
useState의 대상이 되는 값을 useRef의 current가 되도록 일치시키는 것은 setCount -> count변경 -> re-render -> ref.current변경 -> no-re-render의 프로세스대로 진행되기에, 이전 값을 계속 기억할 수 있습니다.
이 부분도 직접 작성할 수도 있으며, react-hookedup에서 가져와서 사용할 수도 있습니다.
import React, { useState } from "react";
import { usePrevious } from "react-hookedup";
export default function App () {
const [ count, setCount ] = useState(0);
const prevCount = usePrevious(count);
...
}
※ 잘못된 부분이나 궁금한 점 댓글로 작성해 주시면 최대한 답변하겠습니다. 읽어주셔서 감사합니다.
※ React Hooks #6 - Hooks(useTranslation)에서 웹사이트 다국어 처리를 하는 방식을 소개해 보겠습니다.
'React > Hooks' 카테고리의 다른 글
React Hooks #4 - Hook Flow (0) | 2021.09.22 |
---|---|
React Hooks #3 - Hooks(useContext) (0) | 2021.08.28 |
React Hooks #2 - Hooks(useReducer) (0) | 2021.08.16 |
React Hooks #1 - Motivation (0) | 2021.07.04 |