ref, useRef, forwardRef, DOM

강연주·2025년 5월 13일

📚 TIL

목록 보기
160/186

모임 생성

🖥️ MeetupForm.tsx

const LabeledInput = React.forwardRef<HTMLInputElement, LabeledInputProps>(({ id, name, label, type, placeholder, value, defaultValue, disabled, required, checked, onChange }, ref) => {
  return (
    <>
      <div>
        <label htmlFor={id}>{label}</label>
        <input
          id={id}
          name={name}
          type={type}
          placeholder={placeholder}
          disabled={disabled}
          value={value}
          defaultValue={defaultValue}
          required={required}
          checked={checked}
          onChange={onChange}
          ref={ref}
        />
      </div>
    </>
  );
});

...

const MeetupForm = () => {
  const router = useRouter();
  const queryClient = useQueryClient();

  // Ref
  const organizerNicknameRef = useRef<HTMLInputElement>(null);
  const organizerProfileImageRef = useRef<HTMLInputElement>(null);
  const nameRef = useRef<HTMLInputElement>(null);
  const startedAtRef = useRef<HTMLInputElement>(null);
  const endedAtRef = useRef<HTMLInputElement>(null);
  const placeRef = useRef<HTMLSelectElement>(null);
  const placeDescriptionRef = useRef<HTMLInputElement>(null);
  const adTitleRef = useRef<HTMLInputElement>(null);
  const adEndedAtRef = useRef<HTMLInputElement>(null);
  const descriptionRef = useRef<HTMLTextAreaElement>(null);
  const isPublicRef = useRef<HTMLInputElement>(null); //초기값 왜 null
  const categoryRef = useRef<HTMLSelectElement>(null);
  const imageRef = useRef<HTMLInputElement>(null);
  
...

 return (
    <>
      <div>
        <form onSubmit={handleMeetupFormSubmit}>
          <div>
            <LabeledSelect id="category" name="category" label="모임 성격" options={categoryOptions} ref={categoryRef} required />

            <LabeledInput id="name" name="name" label="모임 이름(랜덤 생성 버튼 필요)" type="text" ref={nameRef} required />
            
...
            
  • 중복 코드 줄이라고 지피티가 코드를 하나 알려줌.
  • 프로젝트 극초반에 무지성으로 가져다 씀.
    • label을 매번 쓰는 수고를 줄인다는 목적은 이해 완료.
  • 읽어보는데 forwardRef가 뭐야?
  • 찬찬히 뜯어보니 애가 그냥 매개변수 가져다 화살표 함수 선언하는 거랑 모양이 다름.
    • ()안에 함수가 들어가 있다.
  • 부모 - 자식 컴포넌트 간에 ref를 전달하고 접근할 수 있게 해준다.
  • 그럼 초기값은 왜 null이야?
    • 모임 생성은 그렇다 치고, 왜 수정도?
  • useRef를 useState처럼 초기값 설정해주고 바꾸는 거라고 생각.
    • 물론 얘도 DOM과 엮여서 어떻게 다뤄지는지는 알아보지 않았으나 초기값이 그대로 렌더링되고 슉슉 바꿀 수 있는데...
  • ref를 그냥 초기값과 함께 선언, 접근, 변경하는 식으로 생각하면 안 됐다.
    • .current, 참조 객체, 가상 DOM...


🖥️ JavaScript

// 사용 흐름
// 부모 컴포넌트에서
const nameRef = useRef<HTMLInputElement>(null);

// 자식 컴포넌트 사용
<LabeledInput id="name" label="모임 이름" ref={nameRef} />

// 나중에 값 접근
const name = nameRef.current?.value;

useRef의 초기값이 null인 이유

1. DOM 요소 참조 메커니즘

  • React에서 useRef를 사용해 DOM 요소를 참조할 때,
    초기에는 해당 요소가 아직 렌더링되지 않은 상태이다.
  • 컴포넌트가 처음 실행될 때는 DOM이 아직 생성되지 않았기 때문에,
    ref가 가리킬 실제 DOM 요소가 존재하지 않는다.
  • 따라서 초기값은 null로 설정하고, 컴포넌트가 렌더링된 후에 React가 자동으로 ref의 .current 속성에 실제 DOM 요소를 할당한다.

2. TypeScript와의 관계

  • TypeScript를 사용할 때 useRef<HTMLInputElement>(null)과 같은 형태로 작성하는 것은, ref가 가리킬 DOM 요소의 타입을 알려주는 것.
  • 이 타입 정의는 .current가 null이거나 HTMLInputElement 타입의 객체일 수 있음을 의미한다.
  • 이는 타입 안전성을 위한 것으로, ref가 실제 DOM 요소를 참조하기 전에는
    null이라는 것을 명시적으로 표현한다.

3. 생성 컴포넌트와 수정 컴포넌트 모두 null인 이유

  • 새 모임을 생성하는 컴포넌트든, 기존 모임을 수정하는 컴포넌트든 상관없이
    모든 useRef는 초기에 null로 설정한다.
  • 수정 컴포넌트에서는 기존 데이터를 표시하기 위해 defaultValue 속성을 사용하지만, 이것은 ref의 초기값과는 별개.
  • defaultValue는 DOM 요소의 초기 표시 값을 설정하는 것이고,
    useRef의 초기값은 DOM 참조를 위한 것이다.

  • forwardRef를 왜 썼느냐?

    • ref를 부모 컴포넌트(MeetupForm)에서 자식 컴포넌트(LabeldInput, LabeledSelect)로 전달하고 접근 가능케 하기 위함
    • forwardRef는 함수를 매개변수로 갖고 그 함수는 2개의 파라미터를 가진다.
  • 그럼 LabeledInput, LabeledSelect는 애초에 왜 만들었느냐?

    • input이나 select 태그마다 매번 label 태그를 적는 중복을 방지하려고 사용!
    • 즉, 매번 label 태그를 적는 식으로 코딩한다면 forwardRef같은 고차원 함수도 필요가 없었겠지만, 그 경우 코드가 길고 지저분해진다.
  • 그럼 useRef의 초기값은 왜 null이냐? 심지어 기존 모임 정보를 가져와 수정하는 코드에서도?

    • DOM 어쩌고 하는 내용을 얼핏 봤는데 DOM과 연관 있느냐?
      • 바로 DOM 때문이다.

네, 정확합니다! 아주 정확하게 이해하셨습니다. 🥹🥹🥹🥹🥹🥹🥹
클로드랑 나랑 내 머리끄덩이 잡고 나아가는 중


profile
아무튼, 개발자

0개의 댓글