ref 속성이란?
모든 리액트 컴포넌트는 reference의 앞 3글자를 따 ref 속성을 제공한다. ref 속성은 초기에는 null 값 이였다가 마운트 되는 시점에 물리DOM객체의 값으로 변경된다. 즉 ref는 물리 DOM 객체의 참조이다.
HTML 요소는 자바스크립트에선 DOM 타입 객체이며 <div>, <button> 같은 요소는 HTMLElement 상속타입인 HTMLDivElement, HTMLButtonElement 타입 객체이다.
HTMLElement 타입은 click(), blur(), focus() 메서드를 제공하며 리액트 요소가 물리 DOM 상태일때만 호출할 수 있다.
ref 속성값은 물리 DOM 상태일 때의 값으로 ref로 얻은 값(DOM 객체)을 사용해 click()과 같은 이벤트를 호출할 수 있다.
다음 ref속성의 정의 코드에서 타입이 Ref<T>인걸 알 수 있고 여기서 타입 변수 T는 HTMLElement와 같은 DOM타입이다.
Ref<T>는 current라는 읽기 속성을 가진 RefObject<T> 타입이며 값이 null일 수 있다. 왜냐하면 리액트 요소가 마운트 되기 전인 가상 DOM타입일 때는 값이 null이기 때문이다. 또한 current는 리액트 내부에서 설정하는 값이므로 사용자가 임의로 변경해선 안되므로 리액트에서 값을 읽기전용(readonly)으로 설정했다.
// ref 속성 정의
interface RefAttributes<T> extends Attributes {
ref?: Ref<T> | undefined;
}
// RefObject<T> 타입 정의
interface RefObject<T> {
readonly current: T | null;
}
type Ref<T> = RefObject<T> | null;
useRef 훅 알아보기
react 패키지에서는 useRef 훅 제공하며 useRef의 반환값 타입은 MutaleRefObject<T>이다. MutableRefObject<T> 타입은 ref 속성의 타입인 RefObject<T> 처럼 current라는 속성을 가지고 있다.
// react패키지에서 제공하는 useRef 훅 임포트
import {useRef} from 'react'
// useRef 훅의 반환값 타입
function useRef<T>(initialValue: T): MutableRefObject<T>;
// MutableRefObject<T> 타입 정의
interface MutableRefObject<T> {
current: T;
}
useRef훅을 사용하기 위한 코드를 추가한다.
import {useRef, useCallback} from 'react'
import {Title} from '../components'
export default function ClickTest() {
const inputRef = useRef<HTMLInputElement>(null)
// ?. 널 병합 연산자 사용 -> 마운트 이전에는 current의 값은 null이기 때문이다.
const onClick = useCallback(() => inputRef.current?.click(), [])
return (
<section className="mt-4">
<Title>ClickTest</Title>
<div className="mt-4 flex justify-center items-center">
<button className="btn btn-primary mr-4" onClick={onClick}>
Click Me
</button>
<input ref={inputRef} className="hidden" type="file" accept="image/*" />
</div>
</section>
)
}
FileList 클래스와 Array.from() 함수
FileList는 File 타입의 리스트이며 자바스크립트 배열이 아닌 유사 배열 객체(array-like objects)이다. 자바스크립트에서는 유사 배열 객체를 Array.from() 함수를 사용해 배열로 변환할 수 있다.
const fileArray: File[] = Array.from(files) // 배열로 변환 처리
FileReader 클래스로 File 타입 객체 읽기
자바스크립트 엔진은 File 타입 객체를 읽을 수 있도록 FileReader라는 클래스를 기본으로 제공한다. FileReader 클래스가 제공하는 readAsDataURL() 메서드는 File 타입 객체를 읽어 문자열로 된 이미지를 Base64 인코딩으로 제공한다.
다만 File 타입 객체에 담긴 데이터는 바이너리 데이터이므로 base64 인코딩 시 시간이 걸리므로 FileReader 클래스에서는 onload 이벤트 속성을 제공해 인코딩 된 이후 작업이 처리되도록 도움을 준다.
const file : File
const fileReader = new FileReader
fileReader.onload = (e: ProgressEvent<FileReader>) => {
if (e.target) {
const result = e.target.result
}
}
filereader.readAsDataURL(file)
imageFileReaderP 유틸리티 함수 만들기
imageFileReader의 패턴은 매번 개발하기 번거로우기 때문에 유틸리티 함수를 만들기 위해 코드를 추가한다.
export const imageFileREaderP = (file: Blob) =>
new Promise<string>((resolve, reject) => {
const fileReader = new FileReader()
fileReader.onload = (e: ProgressEvent<FileReader>) => {
const result = e.target?.result
if (result && typeof result === 'string') resolve(result)
else reject(new Error(`imageFileReaderP: can't read image file`))
}
fileReader.readAsDataURL(file)
})
FileDrop 컴포넌트 만들기
imageFileReaderP 유틸리티 함수를 사용하는 FileDrop컴포넌트 코드를 추가한다.
onInputChange, onDivDrop의 useCallback 훅 makeImageUrls가 의존성 배열에 포함되지 않으면, makeImageUrls가 변경되었을 때 해당 변경 사항이 반영되지 않고 이전 버전의 makeImageUrls를 계속 사용하게 되므로 추가한 것이다.
하단의 소스에서 map(imageFileREaderP) 과 map((file) => imageFileREaderP(file))는 같은방식으로 동작하며 이유는 자바스크립트의 함수는 일급 객체이기 때문에 함수 이름을 그대로 전달 시 자동으로 배열의 요소를 함수의 인자로 전달한다.
import type {ChangeEvent, DragEvent} from 'react'
import {useState, useRef, useCallback, useMemo} from 'react'
import {useToggle} from '../hook'
import {Title, Div} from '../components'
import {imageFileREaderP} from '../utils'
export default function FileDrop() {
const [imageUrls, setImageUrls] = useState<string[]>([])
const [error, setError] = useState<Error | null>(null)
const [loading, toggleLoading] = useToggle(false)
const inputRef = useRef<HTMLInputElement>(null)
const onDivClick = useCallback(() => inputRef.current?.click(), [])
// File배열을 imageFileREaderP Util을 통해 base64 인코딩 후
// 추가된 urls 배열을 기존 imageUrls배열에 병합한다.
const makeImageUrls = useCallback(
(files: File[]) => {
const promises = Array.from(files).map(imageFileREaderP)
toggleLoading()
Promise.all(promises)
.then(urls => setImageUrls(imageUrls => [...urls, ...imageUrls]))
.catch(setError)
.finally(toggleLoading)
},
[toggleLoading]
)
// input의 onchange이벤트를 통해 makeImageUrls(Array.from(files)) 호출
const onInputChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setError(null)
const files = e.target.files
files && makeImageUrls(Array.from(files))
},
[makeImageUrls]
)
const ondivDragOver = useCallback((e: DragEvent) => e.preventDefault(), [])
const onDivDrop = useCallback( // 드롭한 파일이 있을경우 makeImageUrls(Array.from(files)) 호출
(e: DragEvent) => {
e.preventDefault()
setError(null)
const files = e.dataTransfer?.files
files && makeImageUrls(Array.from(files))
},
[makeImageUrls]
)
// prettier-ignore
// imagesUrls로 jsx생성
const images = useMemo(() => {
return imageUrls.map((url,index) => (
<Div key={index} src={url}
className='m-2 bg-transparent bg-center bg-no-repeat bg-contain'
width='5rem' height='5rem' />
))
},[imageUrls])
// prettier-ignore
return (
<section className="mt-4">
<Title>FileDrop</Title>
{error && (
<div className='p-4 mt-4 bg-red-200'>
<p className='text-3xl text-red-500 text-bold'>{error.message}</p>
</div>
)}
<div onClick={onDivClick} className='w-full mt-4 bg-gray-200 border border-gray-500'>
{loading && (
<div className='flex items-center justify-center'>
<button className='btn btn-circle loading'></button>
</div>
)}
<div onDragOver={ondivDragOver} onDrop={onDivDrop} className='flex flex-col items-center justify-center h-40 cursor-pointer'>
<p className='text-3xl font-bold'>drop images or click me</p>
</div>
<input ref={inputRef} onChange={onInputChange} multiple className='hidden' type='file' accept='images/*' />
</div>
<div className='flex flex-wrap justify-center'>{images}</div>
</section>
)
}
<input> 요소의 ref 속성 사용하기
<input>의 ref 속성의 inputRef.current값이 물리 DOM 객체이므로 foucs() 메서드를 호출한 코드를 추가한다.
import {useRef, useEffect} from 'react'
import {Title} from '../components'
export default function InputFocusTest() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => inputRef.current?.focus(), [])
// prettier-ignore
return (
<section className="mt-4">
<Title>InputFocusTest</Title>
<div className="flex justify-center mt-4">
<input ref={inputRef} className='input input-primary' placeholder='enter som text' />
</div>
</section>
)
}
useState 호출 없이 <input>의 value 속성값 얻기
리액트의 <input>의 value 속성 코드 패턴의 작성은 매번 작성하기엔 번거로운 부분이 있다. 이런 패턴을 요구하는 이유는 가상 DOM환경에서 빠른 리렌더링을 위해서이다.
// <input>의 value 속성 코드 패턴
const [value, setValue] = useState<string>('')
const onChangeValue = (e: ChangeEvent<HTMLInputElement>) => setValue(notUsed => e.target.value)
<input value={value} onChange={onChangeValue}
하지만 ref 속성이 유효한 값, 즉 물리DOM 객체가 만들어지면 value속성 값을 통해 값을 얻을 수 있으므로 useState훅을 사용하지 않고서 input value 속성값을 가져올 수 있다. ref를 통한 value값을 얻어오는 코드를 추가하자.
import {useRef, useEffect, useCallback} from 'react'
import {Title} from '../components'
export default function InputValueTest() {
const inputRef = useRef<HTMLInputElement>(null)
const getValue = useCallback(() => alert(`input value:${inputRef.current?.value}`), [])
useEffect(() => inputRef.current?.focus(), [])
// prettier-ignore
return (
<section className="mt-4">
<Title>InputValueTest</Title>
<div className="flex justify-center mt-4">
<div className='flex flex-col w-1/3 p-2'>
<input ref={inputRef} className='input input-primary' />
<button onClick={getValue} className='mt-4 btn btn-primary'>get value</button>
</div>
</div>
</section>
)
}
forwardRef 함수 이해하기
forwardRef 함수는 부모 컴포넌트에서 생성한 ref를 자식 컴포넌트로 전달해 주는 역할을 한다.
forwardRef 함수가 필요한 이유 알기
예를들어 사용자 컴포넌트를 통해 <input> 요소를 사용하는 Input을 만들었다고 가정해보자. 이럴경우 <input>은 ref 속성값을 사용할 수 있지만 Input은 ref 속성값을 사용할 수 없다.
import {Input} from '../theme/daisyui'
<Input ref={inputRef} className="input-primary" />
왜냐하면 Input은 사용자 컴포넌트이므로 물리 DOM 객체를 얻을 수 없기 때문이다. 해당 내용을 확인할 수 있는 코드를 추가하자. 정상적으로 값을 ref를 통해 전달받기 위해서는 forwardRef를 사용해야 한다.
import {useRef, useEffect, useCallback} from 'react'
import {Title} from '../components'
import {Input} from '../theme/daisyui'
export default function ForwardRefTest() {
const inputRef = useRef<HTMLInputElement>(null)
const getValue = useCallback(() => alert(`input value:${inputRef.current?.value}`), [])
useEffect(() => inputRef.current?.focus(), [])
// prettier-ignore
return (
<section className="mt-4">
<Title>ForwardRefTest</Title>
<div className="flex justify-center mt-4">
<div className='flex flex-col w-1/3 p-2'>
<Input ref={inputRef} className='input input-primary' />
<button onClick={getValue} className='mt-4 btn btn-primary'>get value</button>
</div>
</div>
</section>
)
}
forwardRef 함수의 타입
아래의 타입정보 중 타입 변수 T(input의 타입인 HTMLInputElement)는 ref 대상 컴포넌트의 타입이고 P는 컴포넌트의 속성 타입입니다. 앞서 본 사용자 컴포넌트인 Input 컴포넌트의 속성 타입은 InputProps이므로 forwardRef 타입정보에서 타입변수 P는 InputProps이다.
// forwardRef 타입 정보
function forwardRef<T, P = {}> (render: ForwardRefRenderfunction<T, P>): 반환_타입;
// Input의 속성타입
export type ReactInputProps = DetailedHTMLProps<
InputHTMLAttributes<HTMLInputElement>,HTMLInputElement>
export type InputProps = ReactInputProps & {}
이를 참고해 ref 관련 부분을 forwardRef 함수로 전달하는 형태로 완성된 Input.tsx 컴포넌트 파일을 변경하자.
import {type DetailedHTMLProps, type InputHTMLAttributes} from 'react'
import {forwardRef} from 'react'
export type ReactInputProps = DetailedHTMLProps<
InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
>
export type InputProps = ReactInputProps & {}
export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
const {className: _className, ...inputProps} = props
const className = ['input', _className].join(' ')
return <input ref={ref} {...inputProps} className={className} />
})
useImperativeHandle 훅이란?
useImperativeHandle 훅은 컴포넌트 기능을 JSX가 아닌 타입스크립트 코드에서 사용한다. useImperativeHandle 훅의 탄생 배경에 대해 살펴보자.
이 코드가 성립하는건 textInput 코어 컴포넌트에 focus메서드를 제공하기 때문이다. 근데 생각을 달리해 아래와 같이 TextInputMethod 타입의 객체를 useRef<TextInput | null> 대신 useRef<TextInputMethods | null>로 사용하고자 하는 것에서 시작 되었다.
const textInputRef = useRef<TextInput | null>(null)
const setFocus = () => textInputRef.current?.focus()
// 이 타입을 useRef<TextInputMethods | null>로 사용하는건 어떨까?
export type TextInputMethods = {
focus: () => void
dismiss: () => void
}
useImperativeHandle 훅의 타입
useImperativeHandle 훅의 타입 정의이다. 매개변수 Ref는 forwardRef 호출로 얻는 값을 입력하는 용도이고, init은 useMemo 훅과 유사하게 '() => 메서드_객체' 형태의 함수를 입력하는 용도이다.
function useImperativeHandle<T, R extends T>(ref: Ref<T> | undefined, init: () => R, detps?:
DependencyList): void;
// useMemo 훅과 useImperativeHandle 훅 비교
const object = useMemo(() => ({}), [])
const handle = useImperativeHandle(ref, () => ({}), [])
useImperativeHandle의 매개변수 ref는 forwardRef 함수를 호출해 얻은 ref를 사용해야 한다. 즉 Input 과 같이 커스텀 태그의 컴포넌트 내부에서만 사용이 가능함을 의미한다.
ValidatableInput 컴포넌트 만들기
import type {ReactInputProps} from './Input'
import {forwardRef, useImperativeHandle, useMemo, useRef} from 'react'
export type ValidatableInputMethods = {
validate: () => [boolean, string]
}
export const ValidatableInput = forwardRef<ValidatableInputMethods, ReactInputProps>(
({type, className: _className, ...inputProps}, methodsRef) => {
const className = useMemo(() => ['input', _className].join(' '), [_className])
// input 객체의 ref를 얻어온다.
const inputRef = useRef<HTMLInputElement>(null)
useImperativeHandle(
methodsRef,
() => ({
validate: (): [boolean, string] => {
const value = inputRef.current?.value
if (!value || !value.length) return [false, '사용자가 입력한 낸용이 없습니다.']
switch (type) {
case 'email': {
const regEx =
/^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i
const valid = regEx.test(value)
// 값이 참이면 value를 반환, false이면 '틀린 이메일 주소입니다' string을 반환
return valid ? [true, value] : [false, '틀린 이메일 주소입니다.']
}
}
return [true, value]
}
}),
[type]
)
return <input {...inputProps} className={className} ref={inputRef} />
}
)
import {useRef, useCallback} from 'react'
import {Title} from '../components'
import type {ValidatableInputMethods} from '../theme/daisyui'
import {ValidatableInput} from '../theme/daisyui'
export default function ValidatableInputTest() {
const methodsRef = useRef<ValidatableInputMethods>(null)
const validateEmail = useCallback(() => {
if (methodsRef.current) {
const [valid, valueOrErrorMessage] = methodsRef.current.validate()
if (valid) alert(`${valueOrErrorMessage}는 유효한 이메일 주소입니다.`)
else alert(valueOrErrorMessage)
}
}, [])
// prettier-ignore
return (
<section className="mt-4">
<Title>ValidatableInputTest</Title>
<div className="flex justify-center mt-4">
<div className='flex flex-col w-1/3 p-2'>
<ValidatableInput type='email' ref={methodsRef} className='input-primary' />
<button onClick={validateEmail} className='mt-4 btn btn-primary'>validate</button>
</div>
</div>
</section>
)
}
'FrontEnd > React' 카테고리의 다른 글
[React] 리덕스 기본 개념 이해하기 (2) | 2024.12.10 |
---|---|
[React] useContext 훅 이해하기 (0) | 2024.12.10 |
[React] useEffect와 useLayoutEffect훅 이해하기 (1) | 2024.12.01 |
[React] useState 훅 이해하기 (1) | 2024.11.28 |
[React] useMemo와 useCallback 훅 이해하기 (0) | 2024.11.27 |