완벽 가이드 - Refs & Portals

primav·2024년 11월 27일

React

목록 보기
22/35
post-thumbnail

📌 Refs

이름 저장

useState란?

  • 리액트 컴포넌트의 상태를 관리하기 위한 훅이다.
  • 상태가 변경되면 컴포넌트가 리렌더링된다.
  • 즉, 값이 바뀔 때 화면에 즉시 반영된다.

✔️ useState 사용

export default function Player() {
  const [enteredPlayerName, setEnteredPlayerName] = useState("");
  const [submitted, setSubmitted] = useState(false);

  const handleChange = (e) => {
    setSubmitted(false);
    setEnteredPlayerName(e.target.value);
  };
  const handleSubmit = () => {
    setSubmitted((submitted) => !submitted);
  };
  return (
    <section id="player">
      <h2>Welcome {submitted ? enteredPlayerName : "unknown entity"}</h2>
      <p>
        <input type="text" onChange={handleChange} value={enteredPlayerName} />
        <button onClick={handleSubmit}>Set Name</button>
      </p>
    </section>
  );
}

작동 방식

  1. input에 값을 입력하면 setEnteredPlayerName으로 상태를 업데이트한다.
  2. handleSubmit 호출 시 submitted 상태를 true로 변경한다.
  3. enteredPlayerNamesubmitted 값에 따라 화면이 즉시 업데이트된다.
  4. 상태 변경이 발생하면 컴포넌트가 다시 렌더링된다.

장점

  • 화면과 상태가 항상 동기화된다.
  • 상태관리가 명확하고, 리액트 철학에 맞는 방식이다.

단점

  • 입력값이 바뀔때마다 컴포넌트가 리렌더링된다.

useRef란?

  • 컴포넌트 내에서 값을 저장하는 데 사용하는 훅이다.
  • 값이 변경되어도 컴포넌트가 리렌더링되지 않는다.
  • 일반적으로 DOM 요소를 직접 참조하거나, 상태와는 별개로 리렌더링과 상관없는 값을 저장할 때 사용한다.

✔️ useRef 사용

import { useState, useRef } from "react";

export default function Player() {
  const playerName = useRef;

  const [enteredPlayerName, setEnteredPlayerName] = useState(null);

  const handleSubmit = () => {
    setEnteredPlayerName(playerName.current.value);
  };

  return (
    <section id="player">
      <h2>Welcome {enteredPlayerName ?? "unknown entity"}</h2>
      <p>
        <input ref={playerName} type="text" />
        <button onClick={handleSubmit}>Set Name</button>
      </p>
    </section>
  );
}

작동 방식

  1. refinput 요소에 연결하여 입력 값을 직접 참조한다.
  2. handleSubmit 호출 시 playerName.current.value로 값을 가져온다.
  3. setEnteredPlayerName으로 상태를 업데이트하지만, 입력값 변경만으로는 리렌더링이 발생하지 않는다.

장점

  • 입력값이 바뀌어도 불필요한 리렌더링이 발생하지 않으므로 성능 최적화에 유리하다.
  • DOM 요소에 직접 접근 가능하여 상태 관리가 필요 없는 단순 입력값 저장에 적합하다.

단점

  • ref 로 값을 관리하기 때문에 입력값이 즉시 화면에 반영되지 않는다.
  • 컴포넌트와 입력값이 비동기적으로 동작할 수 있다.

🤔 언제 useState와 useRef를 사용할까?

  • useState
    화면에 즉각적으로 반영되어야 하는 데이터(상태)를 관리할 때 사용한다.
    ex) 사용자 입력, 버튼 클릭 등으로 상태가 변화하며 UI를 업데이트할 때

  • useRef
    DOM 접근이 필요하거나 리렌더링 없이 값을 추적해야 할 때 사용한다.
    ex) 입력 필드 참조, 컴포넌트 내부에서 유지해야 하는 값 관리

💡 요약

useState는 리렌더링을 유발하여 상태와 UI를 동기화한다.
useRef는 리렌더링 없이 값을 유지하거나, DOM 요소를 참조할 때 사용된다.
화면과 상호작용하는 상태는 useState, 단순히 값을 저장하거나 DOM 참조가 필요한 경우는 useRef를 활용하면 된다.

리액트는 선언적인 코드를 사용한다.

DOM을 직접 조작하는 것이 아니다. DOM을 조작하느 것은 리액트가 알아서 해준다!

📌 setTimeout

setTimeout

지정된 시간 후에 한 번 실행될 콜백 함수를 예약한다.
(호출 시 변수에 반환 가능 --> 이 변수를 통해 타이머를 추적/취소할 수 있다.)

const timer = setTimeout(() => {
  console.log("This will run after 3 seconds");
}, 3000);

clearTimeout

예약된 타이머를 취소한다.
clearTimeout(timer)를 호출하면, 해당 타이머가 실행되지 않는다.
(이미 실행된 타이머는 취소할 수 없다!)

clearTimeout(timer);

📌 타이머 구현

사진과 같이 여러개의 타이머를 구현하려면 각 타이머마다 값을 관리해야 한다.
하지만 밑의 변수를 사용하여 timer를 저장하는 방식은 하나의 타이머 데이터만 관리할 수 있으므로 여러개의 타이머를 작동시킬 때 오류가 난다.

이것을 해결하기 위해 useRef를 사용하면 된다.

✔️ 변수 사용

let timer;

function handleStart() {
  timer = setTimeout(() => {
    setTimerExpired(true);
  }, targetTime * 1000);

  setTimerStarted(true);
}

function handleStop() {
  clearTimeout(timer);
}

문제점

  • 컴포넌트가 리렌더링되면 타이머는 초기화된다.
  • 따라서, 리렌더링 이후에 이전 타이머를 참조할 수 있다.
  • 리렌더링시 데이터가 손실된다!

✔️ useRef 사용

const timer = useRef();

const [timerStarted, setTimerStarted] = useState(false);
const [timerExpired, setTimerExpired] = useState(false);

function handleStart() {
  timer.current = setTimeout(() => {
    setTimerExpired(true);
  }, targetTime * 1000);

  setTimerStarted(true);
}

function handleStop() {
  clearTimeout(timer.current);
}

해결

  • 리렌더링 간에 값이 유지된다.
  • time.current에 저장된 값은 컴포넌트가 리렌더링 되어도 초기화되지 않는다..

let vs useRef

특징let 사용useRef 사용
값의 유지 여부리렌더링 시 초기화됨리렌더링 간에도 값 유지
UI 리렌더링 영향로컬 변수는 리렌더링을 발생시키지 않음useRef도 리렌더링을 발생시키지 않음
타이머 ID 관리 안정성리렌더링 후 이전 타이머 ID 접근 불가리렌더링 후에도 타이머 ID 접근 가능
사용 목적단순히 리렌더링 없는 경우에만 적합타이머, DOM 참조, 상태와 무관한 값 관리에 적합

➡️ 타이머처럼 리렌더링 이후에도 값을 유지해야 하는 경우, useRef를 사용하는 것이 가장 안전하다.

.current 란?

.currentuseRef 로 생성된 객체의 속성이다.
useRef를 호출하면 { current: ... } 형태의 객체를 반환하며, .current는 해당 객체의 실제 값을 담고 있다.

const myRef = useRef(initialValue);
  • useRef{ current: initialValue } 형태의 객체를 반환한다.
  • .current 속성을 통해 값에 접근하거나 변경할 수 있다.

.current의 역할

  • 값의 저장소 역할을 한다.
  • useRef는 값 변경 시 리렌더링을 발생시키지 않으므로, 리렌더링과 독립적인 데이터 저장소로 사용된다.
  • **값을 읽거나 쓸 때 항상 .current를 통해 접근한다.

🤔 왜 .current를 사용해야 할까?

리액트에서useRef 로 생성한 참조(ref)는 단순한 변수와 다르다.
useRef는 리액트가 관리하는 특수한 객체를 반환하며, .current 속성을 통해 해당 값을 저장/조회할 수 있다.

💡 결론 : .current를 쓰는 이유

  • 값에 접근하거나 변경하기 위해
    useRef로 반환된 객체는 항상 { current: 값 } 구조를 가지므로, .current를 통해 값을 읽거나 써야 한다.

  • 리렌더링과 독립적인 값 관리
    useState는 값 변경 시 리렌더링이 발생하지만, useRef.current는 리렌더링 없이 값을 유지하고 관리한다.

📌 forwardRef

forwardRef 란?

부모 컴포넌트가 자식 컴포넌트의 DOM 요소 또는 다른 참조(ref)를 직접 조작할 수 있도록 ref를 전달 (forward) 해주는 고차 함수이다.

왜 forwardRef 를 사용할까?

기본적으로 리액트의 컴포넌트는 자신이 관리하는 DOM요소에만 접근할 수 있다.
부모 컴포넌트에서 자식 컴포넌트 내부의 특정 DOM 요소를 제어하려면 ref를 전달할 필요가 있다.

사용방법

✔️ 부모 컴포넌트

const modalRef = useRef();

  const openModal = () => {
    modalRef.current.showModal(); // 모달 열기
  };

return (
    <div>
      <button onClick={openModal}>Open Modal</button>
      <ResultModal ref={modalRef} result="Win" targetTime={10} />
    </div>
  );

✔️ 자식 컴포넌트

const ResultModal = forwardRef(function ResultModal(
  { result, targetTime }, // Props
  ref // 부모로부터 전달받은 ref
) {
  return (
    <dialog ref={ref} className="result-modal" open> </dialog>
  );

결론

  • forwardRef는 부모가 자식 컴포넌트의 DOM 요소에 직접 접근하도록 해준다.
  • React의 forwardRef를 사용하면 부모가 자식의 내부 DOM 구조를 몰라도 간단히 조작할 수 있다.

➡️ 이 구조는 네이티브 DOM 조작이 필요하거나, 특정 상황에서 외부에서 DOM 요소를 제어해야 할 때 유용하다.

📌 useImperativeHandle

useImperativeHandle 란?

리액트 훅 중 하나로, 자식 컴포넌트가 부모 컴포넌트에 전달되는 ref 동작을 커스터마이징하거나 특정 메서드를 노출할 때 사용된다. 주로 forwardRef와 함께 사용한다.

일반적인 ref 사용 방식

ref는 보통 리액트 컴포넌트나 DOM 요소에 직접 접근하기 위해 사용된다.
부모 컴포넌트가 자식 컴포넌트의 DOM 노드에 접근하려면 useRef를 통해 연결된 ref를 이용할 수 있다.

useImperative로 동작 제어

위 방식은 자식 컴포넌트의 내부 DOM 구조에 의존하므로 캡슐화가 부족하다.
useImperativeHandle은 부모가 자식의 내부 구조를 몰라도 제공된 메서드만 사용할 수 있게 한다.

사용 방법

useImperativeHandle(ref, createHandle, [dependencies]);
  • ref - forwardRef 를 통해 부모로부터 전달된 ref
  • reateHandle 객체를 반환하는 함수. 이 객체는 부모가
    ref.current를 통해 사용할 수 있는 메서드나 값을 정의
  • [dependencies] - 선택적 / createHandle 함수가 재실행될 조건을 지정하는 의존성 배열

📌 react vs react-dom

항목ReactReactDOM
역할React 컴포넌트를 작성하고 상태 관리React 컴포넌트를 실제 DOM에 렌더링
제공 기능컴포넌트 및 상태 관리, Hooks 제공 등DOM에 React 앱을 렌더링Portal, SSR 지원
초점UI 설계와 컴포넌트의 동작React 컴포넌트와 브라우저 DOM의 연결
사용 예JSX, Hooks, Context 등을 처리ReactDOM.createRoot 또는 render 호출
서버 사이드 렌더링관련 없음ReactDOMServer를 통해 구현
  • React - 애플리케이션의 UI와 그 동작을 정의한다.
  • ReactDOM - React가 정의한 UI를 실제 브라우저 DOM에 렌더링한다.

이 두 패키지를 분리하는 이유가 뭘까?

React를 다양한 환경에서 사용할 수 있도록 하기 위해서이다.

예를 들어, React Native는 ReactDOM 대신 모바일 앱의 네이티브 환경에 맞는 렌더러를 사용한다.
ReactDOM은 브라우저 환경에서 동작하도록 설계된 렌더러이다.

📌 createPortal

리액트의 portal 을 생성하는 메서드이다.
기본적으로 리액트의 컴포넌트는 부모 DOM 노드에 렌더링되지만, portal을 사용하면 부모 컴포넌트 계층 구조를 벗어나서 다른 DOM 노드에 리액트 컴포넌트를 렌더링할 수 있다.

사용방법

createPortal(child, container)
  • child - 렌더링할 리액트 노드 (컴포넌트, JSX 등)
  • container - 렌더링할 DOM 엘리먼트 (일반적으로 document.getElemenyById('id')로 가져옴)

왜 createPortal을 사용하는가?

  1. DOM 구조와 스타일 분리
    모달, 툴팁, 드롭다운과 같은 컴포넌트를 만들때, 렌더링 위치를 루트 노드로 이동하여 스타일과 동작을 제대로 처리할 수 있다.

  2. z-index 문제 해결
    모달이나 팝업이 부모 컴포넌트 내부에 렌더링되면 부모의 overflow: hidden 이나 z-index 등의 스타일이 영향을 미칠 수 있다.
    이를 방지 하기 위해 모달을 DOM 계층 밖에 렌더링 해야된다.

💡 주요 포인트

  • createPortal은 DOM 계층 구조를 무시하고 리액트 컴포넌트를 다
    른 DOM 노드에 렌더링할 수 있게 한다.
  • 주로 모달, 툴팁, 드롭다운과 같은 UI 요소를 구현할 때 사용된다.

0개의 댓글