본문 바로가기
코딩 정보/React

[React] useRef의 응용과 createPortal 에 대해 알아보자

by 꽁이꽁설꽁돌 2024. 7. 4.
728x90
반응형

목차

     

    useRef를 통해 코드 간소화하기

    아래처럼 input에 값을 넣고 변경하는 것을 만들려면 불필요하게 상태와 코드가 많이 필요하다.

    import { useState } from "react";
    
    export default function Player() {
      const [Name, setName] = useState("");
      const [edit, setEdit] = useState(true);
    
      function handleChangeNmae(e) {
        setName(e.target.value);
      }
      function handleSubmit() {
        setEdit(false);
      }
      return (
        <section id="player">
          <h2> {edit ? "Welcome unknown entity" : `Welcome ${Name}`}</h2>
          <p>
            <input type="text" onChange={handleChangeNmae} value={Name} />
            <button onClick={handleSubmit}>Set Name</button>
          </p>
        </section>
      );
    }

     

    ref는 html요소 접근을 도와주는 참조로 current를 통해 모든 html속성에 접근 가능하게 해준다.

    아래와 같이 훨씬 더 간결하게 만들어 줄 수 있다.

    import { useState, useRef } from "react";
    
    export default function Player() {
      const [Name, setName] = useState("");
      const playerName = useRef();
    
      function handleSubmit() {
        setName(playerName.current.value);
      }
      return (
        <section id="player">
          <h2> {Name ?? "Welcome unknown entity"}</h2>
          <p>
            <input
              ref={playerName}
              type="text"
            />
            <button onClick={handleSubmit}>Set Name</button>
          </p>
        </section>
      );
    }

     

    useRef vs useState

    아래 코드는 실행이 될까? 정답은 x이다. 값의 변경이 이루어지지 않는다.

    그 이유는 state는 컴포넌트들의 재실행을 야기하지만 참조는 야기하지 않는다.

    그래서 useRef는 보통 dom에 직접 접근할때 이용한다.

    import { useState, useRef } from "react";
    
    export default function Player() {
      //const [Name, setName] = useState("");
      const playerName = useRef();
    
      
      function handleSubmit() {
        //setName(playerName.current.value);
        //dom상호작용은 리액트가 해야된다는 규칙 위반
        playerName.current.value = "";
      }
      return (
        <section id="player">
          <h2>
            {playerName.current
              ? "Welcome unknown entity"
              : playerName.current.value}
          </h2>
          <p>
            <input ref={playerName} type="text" />
            <button onClick={handleSubmit}>Set Name</button>
          </p>
        </section>
      );
    }

     

    다음 코드는 timer가 올바르게 작동하지 않는다. 무엇이 문제일까?

    바로 timer를 참조하지 않고 전역변수로 쓴 것이다 저렇게 되면 timer는 머추지 않고 다른 중지 버튼에 덮어 씌어지게 된다. 

    import { useState } from "react";
    
    function TimerChallenge({ title, targetTime }) {
      const [timerStarted, setTimerStarted] = useState(false);
      const [expired, setExpired] = useState(false);
    
      let timer;
      function handleStop() {
        clearTimeout(timer);
      }
      function handleStart() {
        timer = setTimeout(() => {
          setExpired(true);
        }, targetTime * 1000);
        setTimerStarted(true);
      }
    
      return (
        <section className="challenge">
          <h2>{title}</h2>
          {expired && <p>You Lost!!</p>}
          <p className="challenge-time">
            {targetTime} second{targetTime > 1 ? "s" : ""}
          </p>
          <p>
            <button onClick={timerStarted ? handleStop : handleStart}>
              {timerStarted ? "Stop" : "Start"} Challenge
            </button>
          </p>
          <p className={timerStarted ? "active" : undefined}>
            {timerStarted ? "Time is running... " : "Timer inactive"}
          </p>
        </section>
      );
    }
    
    export default TimerChallenge;

     

    따라서 useRef를 통해 모든 컴포넌트 인스턴트들은 독립적인 timer를 갖게 된다.

    import { useRef, useState } from "react";
    
    function TimerChallenge({ title, targetTime }) {
      const [timerStarted, setTimerStarted] = useState(false);
      const [expired, setExpired] = useState(false);
    
      //let timer;
      const timer = useRef();
      function handleStop() {
        clearTimeout(timer.current);
      }
      function handleStart() {
        timer.current = setTimeout(() => {
          setExpired(true);
        }, targetTime * 1000);
        setTimerStarted(true);
      }
    
      return (
        <section className="challenge">
          <h2>{title}</h2>
          {expired && <p>You Lost!!</p>}
          <p className="challenge-time">
            {targetTime} second{targetTime > 1 ? "s" : ""}
          </p>
          <p>
            <button onClick={timerStarted ? handleStop : handleStart}>
              {timerStarted ? "Stop" : "Start"} Challenge
            </button>
          </p>
          <p className={timerStarted ? "active" : undefined}>
            {timerStarted ? "Time is running... " : "Timer inactive"}
          </p>
        </section>
      );
    }
    
    export default TimerChallenge;

     

    forwardRef

    ref를 다른 컴포넌트에서 쓰기위한 방법중 하나인 함수이다.

    import { forwardRef, useImperativeHandle, useRef } from "react";
    
    //ref는 따로 뺴준다.
    const ResultModal = forwardRef(function ResultModal(
      { result, targetTime },
      ref
    ) {
    
      return (
        <dialog ref={ref} className="result-modal">
          <h2>You {result}</h2>
          <p>
            The target time was <strong>{targetTime} seconds.</strong>
          </p>
          <p>
            You stopped the timer with <strong>X seconds left.</strong>
          </p>
          <form method="dialog">
            <button>Close</button>
          </form>
        </dialog>
      );
    });
    
    export default ResultModal;
    
    
    
    //이런식으로 다른 컴포넌트에서 ref를 활용할 수 있게된다.
      const dialog = useRef();
      <ResultModal
              ref={dialog}
              result={"lose"}
              targetTime={targetTime}
            ></ResultModal>

     

    useImperativeHandle

    부모 컴포넌트가 자식 컴포넌트의 내부 인스턴스를 직접 제어할 수 있게 해준다. 이를 통해 자식 컴포넌트의 특정 기능을 외부에서 사용할 수 있도록 노출할 수 있다.

    import { forwardRef, useImperativeHandle, useRef } from "react";
    
    const ResultModal = forwardRef(function ResultModal(
      { result, targetTime },
      ref
    ) {
      const dialog = useRef();
     //컴포넌트 함수에서호출하여속성과메소드를 정의할 수 있다.-> 재사용이 가능하게 만들어준다.
      useImperativeHandle(ref, () => {
        return {
          open() {
            dialog.current.showModal();
          },
        };
      });
    
      return (
        <dialog ref={dialog} className="result-modal">
          <h2>You {result}</h2>
          <p>
            The target time was <strong>{targetTime} seconds.</strong>
          </p>
          <p>
            You stopped the timer with <strong>X seconds left.</strong>
          </p>
          <form method="dialog">
            <button>Close</button>
          </form>
        </dialog>
      );
    });
    
    export default ResultModal;

     

    createPortal

    내가 만든 모달의 위치를 확인해보면 body의 하위요소와 가깝다.

    나는 html의 상위 요소에 위치시키고 싶다 이럴 때 createPortal을 써주면 된다.

     

    이런식으로 html의 상위 요소에 들어간 것을 볼 수 있다.

     

    사용방법은 아래와 같다.

    modal component

    import { forwardRef, useImperativeHandle, useRef } from "react";
    import {createPortal} from "react-dom";
    const ResultModal = forwardRef(function ResultModal(
      { targetTime, remainingTime, onReset },
      ref
    ) {
      const dialog = useRef();
      const userLost = remainingTime <= 0;
      const score = Math.round((1 - remainingTime / (targetTime * 1000)) * 100);
      const formmatRemainingTime = (remainingTime / 1000).toFixed(2);
      useImperativeHandle(ref, () => {
        return {
          open() {
            dialog.current.showModal();
          },
        };
      });
    //createPortal써주고 getElementById로 modal요소 접근
      return createPortal(
        <dialog ref={dialog} className="result-modal">
          {!userLost && <h2>Score: {score}</h2>}
          {userLost && <h2>You Lost</h2>}
          <p>
            The target time was <strong>{targetTime} seconds.</strong>
          </p>
          <p>
            You stopped the timer with{" "}
            <strong>{formmatRemainingTime} seconds left.</strong>
          </p>
          <form method="dialog" onSubmit={onReset}>
            <button>Close</button>
          </form>
        </dialog>,
        document.getElementById('modal')
      );
    });
    
    export default ResultModal;

     

    index.html 

    <body>
        <div id="modal"></div>
        <noscript>You need to enable JavaScript to run this app.</noscript>
        <div id="root"></div>
      </body>

     

    반응형