🧩 TypeScript + React에서 ref 정리
✅ 전체 요약
리스트를 렌더링할 때 ref를 map() 안에서 동적으로 관리하고자 할 때,
배열 방식과 객체(Record) 방식의 차이, 그리고 ref 함수의 타입에 대한 이해가 중요함.
🔹 Q1. ref={el => itemRefs.current[item.id] = el} 에서 el의 타입은?
- HTMLDivElement | null
- 마운트 시에는 실제 DOM 엘리먼트 (HTMLDivElement), 언마운트 시에는 null
🔹 Q2. 컴포넌트에 ref를 쓰면 el의 타입은?
- 일반 컴포넌트는 ref를 직접 받을 수 없음
- forwardRef로 감싸야 함
- 그 경우 el의 타입은 ref가 노출하는 객체 (T | null)
🔹 Q3. useRef<Record<number, HTMLDivElement | null>>({})의 의미?
- Record<K, V>는 key가 K, value가 V인 객체 타입
- 즉, number 키를 가진 HTMLDivElement | null 값의 객체
import React, { useEffect, useState } from "react";
export interface propsType {
name: string;
}
export default function ProductDetail({ name }: propsType) {
const [productImage, setProductImage] = useState<string | undefined>();
useEffect(() => {
const productMap: Record<string, string> = {
basic: basicImg,
standard: standardImg,
premium: premiumImg,
};
setProductImage(productMap[name]); // 유효한 name일 때만 이미지 설정
}, [name]);
return <div className="productBox">{productImage ? <img src={productImage} alt={name} /> : null}</div>;
}
조건 추천 방식 이유
| 리스트가 고정된 순서로만 처리됨 | 배열 (Array<...>) | 간단하고 직관적 |
| 리스트가 동적으로 변경되거나 key 기반 접근 필요 | 객체 (Record<...>) | 참조 일관성 유지, 안전함 |
| 요소를 item.id로 직접 찾아야 할 경우 | 객체 (Record<...>) | key 기반 접근이 쉬움 |
✅ 객체 방식 (Record 사용, id 기반)
tsx
const itemRefs = useRef<Record<number, HTMLDivElement | null>>({});
"key가 number이고, value가 HTMLDivElement 또는 null인 객체"
✅ Record<K, V>란?
Record<K, V>는 **"key가 K이고, value가 V인 객체"**를 만드는 타입이다.
📌 특징
- item.id 같은 고유 식별자 기반으로 ref 관리.
- 리스트 순서가 바뀌거나 일부 요소가 삭제돼도 ref가 올바르게 유지됨.
- 더 안정적이고 명시적임.
예시
tsx
{items.map(item =>(
<div ref={el => {itemRefs.current[item.id] = el}} key={item.id}>
{item.name}
</div>
))}
ref={el => {itemRefs.current[item.id] = el}} 이 부분은 callback ref라고 불리는 방식
필요하다면 Record<string, T>도 가능하고, 심지어 유니온 키에도 쓸 수 있다:
ts
type MyRecord = Record<'a' | 'b' | 'c', number>;
이건 { a: number, b: number, c: number }와 같다.
🔍 실전에서 언제 뭘 써야 할까?
🔹 Q4. 배열 방식 (useRef<(HTMLDivElement | null)[]>([]))도 되나?
- 가능은 함
- 단, 리스트 순서가 바뀌거나 삭제/삽입이 발생할 경우 index로 관리하면 버그 유발 가능성 있음
- 이럴 땐 Record<number, ...> 방식이 더 안전
🔹 Q5. 배열 방식에서 ref={(el) => itemRefs.current[index] = el}의 el 타입은?
- 여전히 HTMLDivElement | null
- DOM 요소가 ref 콜백으로 전달되며, 언마운트 시 null 전달됨
🔹 Q6. 에러 발생: 콜백에서 값을 반환했기 때문
ts
ref={el => itemRefs.current[index] = el}
❌ 에러 메시지:
bash
'(el: HTMLDivElement | null) => HTMLDivElement | null' 형식은 'Ref<HTMLDivElement>' 형식에 할당할 수 없습니다
✅ 해결 방법:
ts
ref={(el) => { itemRefs.current[index] = el; }}
중괄호 {}로 감싸서 반환값이 없도록 (void) 만들어야 함
💡 결론
| 고정된 리스트, 단순 렌더링 | 배열 ref (useRef<HTMLElement[]>([])) |
| 동적 리스트, id 기반 접근 | 객체 ref (`useRef<Record<number, HTMLElement |
| ref 콜백 함수 | 항상 반환값이 없어야 함 (void) |
- DOM ref → HTMLDivElement | null, HTMLInputElement | null 등 명확히 지정
- 컴포넌트 ref → 반드시 forwardRef + useImperativeHandle을 사용해야 함
- useRef<Record<...>>는 key 기반 접근 시 아주 유용함
- ref 콜백은 항상 void를 반환해야 함
🧠 TypeScript: Record<K, V> vs Map<K, V> 비교
✅ 개요
Record와 Map은 모두 "키-값 쌍" 구조를 만들기 위한 수단이지만,
용도, 키 타입, 런타임 특성에서 차이가 있습니다.
📌 핵심 비교
| 구조 | JS 객체 ({}) | ES6 Map 클래스 |
| 키 타입 | string, number, symbol (실제는 문자열) | 모든 타입 가능 (객체, 함수 포함) |
| 런타임 | 일반 객체 | Map 인스턴스 |
| 순회 방법 | Object.entries, for...in | for...of, map.forEach |
| 직렬화 | 가능 (JSON.stringify 사용) | 불가능 (결과: {}) |
| 타입 안정성 | 매우 강함 (유니온 제한 등) | 있음, 그러나 덜 직관적 |
| 성능 | 빠름 (정적, 단순 구조) | 더 유연하지만 오버헤드 존재 |
📘 사용 예제
Record 예제
type Status = 'idle' | 'loading' | 'error';
const statusText: Record<Status, string> = {
idle: '대기 중',
loading: '로딩 중',
error: '에러 발생',
};
- 유니온 키를 통해 타입 안정성 확보
- 오타 방지, 자동 완성 지원
Map 예제
const statusMap = new Map<string, string>();
statusMap.set('idle', '대기 중');
statusMap.set('loading', '로딩 중');
statusMap.set('error', '에러 발생');
- 동적으로 값 추가/삭제 가능
- 객체, 배열 등 다양한 타입을 키로 사용 가능
🔍 사용 패턴 가이드
| 고정된 키 집합 | Record<K, V> | 타입 제한 가능, 빠름 |
| 키가 런타임에 결정됨 | Map<K, V> | 유연한 삽입/삭제 |
| 키로 객체를 사용해야 함 | Map<K, V> | 객체 비교 및 참조 가능 |
| JSON 저장이 필요함 | Record<K, V> | JSON.stringify 가능 |
💡 기타 참고
- Map은 size, clear(), has() 등 유용한 API 제공
- Record는 객체기 때문에 Object.keys, Object.entries 등으로 다루기 쉬움
- Record<number, T> 사용 시 주의: 키는 자동으로 문자열로 변환됨 ("1", "2" 등)
✅ 결론
| 정적, 고정된 키와 타입 안전성이 중요 | ✅ Record<K, V> |
| 동적 키, 객체 기반 키 사용이 필요 | ✅ Map<K, V> |
| JSON 직렬화가 필요하거나 간단한 상태 저장 | ✅ Record<K, V> |
| 복잡한 런타임 조작 및 성능이 중요 | ✅ Map<K, V> |
'typescript' 카테고리의 다른 글
| partial (0) | 2025.10.30 |
|---|---|
| 여러 이미지를 호버하여 이미지를 변경할때 type 정의 (0) | 2025.05.20 |
| 타입스크립트_2(feat.노마드코더) (0) | 2023.03.17 |
| 타입스크립트_1(feat.노마드코더) (0) | 2023.02.22 |
| typescript (0) | 2023.01.11 |