함수형 컴포넌트에서 'ref'를 생성하고 관리하는 hook으로 useRef를 사용하면 일반적인 'ref'와 마찬가지로 DOM 노드나 다른 리액트 컴포넌트를 참조할 수 있다.
'useRef'의 핵심 개념은 ref객체를 생성하고 해당 ref객체를 통해 컴포넌트 내에서 DOM노드나 다른 React컴포넌트를 참조하는 것이다. 이러한 ref객체는 함수형 컴포넌트에서도 동일하게 작동하며 useRef를 사용하면 쉽게 생성하고 관리할 수 있다.
useRef에는 두 가지 주요 용도가 있는데 첫쨰, ref객체를 생성하고 둘째, 컴포넌트가 업데이트될 때 이전 ref값을 유지한다. 이러한 기능을 통해 useRef는 컴포넌트에서 DOM요소의 크기, 위치 등을 측정하거나 이전 값과 현재 값을 비교하여 변경 여부를 결정하는 작업 등에 사용될 수 있다.
import { useRef, useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
}, [count]);
const prevCount = prevCountRef.current;
useEffect(() => {
if (prevCount !== undefined) {
console.log(`count changed from ${prevCount} to ${count}`);
}
}, [count, prevCount]);
return (
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
<p>Count: {count}</p>
</div>
);
}
위의 코드는 useRef를 사용해 이전 count 값과 현재 count 값을 비교해 count값이 변경될 때마다 console.log를 호출하는 컴포넌트다. 이 예제에서 prevCountRef.current를 사용해 이전 count 값을 가져와 현재 count값과 비교해 변경 여부를 결정하고 이를 통해 console.log를 호출해 변경된 count값을 콘솔에 출력할 수 있다.
또 useRef는 HTML 파일 태그의 사용에서 쓰이는데...
이 못생긴 기본 input 태그를 숨기고 꾸며주는 방법은 크게 2가지가 있다.
useRef
HTML태그를 선택할 때 보통 getElementId 사용, react에서는 html태그에 접근을 도와주는 역할을 useRef가 하고 있다. 간단한 사용 방법은?
import { useRef } from 'react';
export default function ImageRefPage(): JSX.Element {
const fileRef = useRef<HTMLInputElement>(null);
}
Import로 useRef를 가져오고 constfileRef = useRef() 작성하는 것이 가장 처음 Ref를 불러오고 사용하는 기본 설정이다. useState나 useEffect처럼 react에서 가져와야 사용할 수 있다. 이렇게 하고 fileRef를 태그에 연결해주면 해당 태그는 fileRef로 불러서 사용할 수 있다.
<input
style={{ display: "none" }}
onChange={onChangeFile}
type="file"
ref={fileRef}
/>
이렇게 input태그에 ref={fileRef}를 작성하면 이제 input 태그를 fileRef를 이용해 사용할 수 있다. 그리고 나서, useRef에 있는 기능을 이용하면 된다. 참고로 useRef에는 다양한 기능이 있다.
const onClickImage = (): void => {
// 기존 방식: document.getElementById("파일태그ID")?.click();
fileRef.current?.click();
};
onClick함수의 Ref에는 current안에 click이라는 기능이 있는데 이는 이름 그대로 current는 fileRef에 들어온 태그를 뜻하고 그 태그를 click하겠다는 기능이다.
<button onClick={onClickImage}>이미지 등록 버튼</button>
그리고 새로운 버튼을 하나 만들어 해당 기능을 넣어준다.
이러면 버튼을 클릭했을 때 fileRef.current.click()이 실행될 것이고 그건 useRef에 넣어두었던 input 태그를 클릭한 것과 같은 결과가 나올 것이다.
응용
return (
<>
<div>
<img onClick={onClickImage} style={{ width: '500px' }} id="image" />
<input
hidden={true}
ref={fileRef}
type="file"
onChange={readImage}
></input>
<button onClick={onClickImage}>이미지 등록 버튼</button>
</div>
</>
);
위 코드에선 미리보기 이미지에도 onClickimage를 넣어주었는데 이 기능이 어떤 기능인지는 위에서 이야기했고 input태그에는 hidden기능을 이용해 태그를 숨겨주었다. 이러면 미리보기 이미지를 클릭할 때, 이미지 등록 버튼을 클릭할 때 모두 input type=file태그를 클릭한 것과 동일한 결과를 볼 수있다.
import { gql, useMutation } from "@apollo/client";
import { ChangeEvent, useRef, useState } from "react";
import {
IMutation,
IMutationUploadFileArgs,
} from "../../../src/commons/types/generated/types";
const UPLOAD_FILE = gql`
mutation uploadFile($file: Upload!) {
uploadFile(file: $file) {
url
}
}
`;
export default function ImageRefPage(): JSX.Element {
const [imageUrl, setImageUrl] = useState("");
const fileRef = useRef<HTMLInputElement>(null);
const [uploadFile] = useMutation<
Pick<IMutation, "uploadFile">,
IMutationUploadFileArgs
>(UPLOAD_FILE);
const onChangeFile = async (
event: ChangeEvent<HTMLInputElement>
): Promise<void> => {
const file = event.target.files?.[0]; // 배열로 들어오는 이유: <input type="file" multiple/>일때, 여러 개 업로드 가능하기 때문입니다.
const result = await uploadFile({ variables: { file } });
setImageUrl(result.data?.uploadFile.url ?? "");
};
const onClickImage = (): void => {
// 기존 방식 : document.getElementById("파일태그ID")?.click();
fileRef.current?.click();
};
return (
<>
<div
style={{ width: "50px", height: "50px", backgroundColor: "gray" }}
onClick={onClickImage}
>
이미지 선택
</div>
<input
style={{ display: "none" }}
onChange={onChangeFile}
type="file"
ref={fileRef}
/>
<img src={`https://storage.googleapis.com/${imageUrl}`} />
</>
);
}
위는 전체 코드이다.
(참고)label태그와 htmlFor 이용하기
Label태그에는 htmlFor이라는 속성이 있는데 이속성에 값을 넣으면 갑소가 똑같은 id를 찾아 그 태그의 기능과 연결해준다.
<div>
<label htmlFor="fileTag">이거 눌러도 실행돼요!</label>
<img style={{ width: '500px' }} id="image" />
<input id="fileTag" type="file" onChange={readImage}></input>
</div>
htmlFor='fileTag'와 input id="filetag'의 값이 똑같다.
결과적으로 label태그를 클릭해도 똑같이 파일을 올릴 수 있다. 그러면 label태그를 원하는 디자인으로 꾸미고 기존 input태그는 안보이도록 css작업하면 된다.