본문 바로가기

카테고리 없음

Refs & Portals [React - The Complete Guide 2024]

Resf & Portals

  • Accessing DOM Elements with Refs
  • Managing Values with Refs
  • Exposing API Functions from Components
  • Detaching DOM Rendering from JSX Structure with Portals

Refs

Refs를 쓰기 전 코드

import {useState} from "react";

export default function Player() {

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

    function handleChange(event){
        setSubmitted(false);
        setEnteredPlayerName(event.target.value)
    }

    function handleClick(){
        setSubmitted(true);
    }

  return (
    <section id="player">
      <h2>Welcome {submitted ? enteredPlayerName :  'unknown entity' } </h2>
      <p>
        <input type="text" onChange={handleChange} value={enteredPlayerName}/>
        <button onClick={handleClick}>Set Name</button>
      </p>
    </section>
  );
}

submit 후 타이핑시 unknown entity로 표시되는 문제점이 있음.

코드가 김.

 

Refs 적용 코드

import {useState, useRef} from "react";

export default function Player() {
    const playerName  = useRef();
    const[enteredPlayerName, setEnteredPlayerName] = useState(null);

    function handleClick(){
        setEnteredPlayerName(playerName.current.value);
    }

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

Refs를 통해 JSX요소와 연결하기 (ref 속성 사용)

useRef()를 통해 받는 참조값은 항상 자바스크립트 객체이며, current속성을 가지고 있음. 

.current를 통해 실제 참조값에 접근

 

`??`구문 : 자바스크립트의 "null 병합 연산자"

좌측 피연산자가 `null` 또는 `undefined`일 경우 우측 피연산자를 반환. 그렇지 않으면 좌측 피연산자를 반환

 

DOM을 직접 조작하기보다는 선언적 코드를 사용하도록 하자! React가 수행하도록!

 

State vs Refs

상태를 왜 사용할까?

상태가 없다면(Ref만 사용한다면) 초기에 연결값이 없는 것은 물론, 참조값이 바뀔때마다 컴포넌트가 재실행되지 않음.

상태를 업데이트하면, 컴포넌트를 재실행함!

 

State

  • Causes component re-evaluation (re-execution) with change
  • Should be used for values that are directly reflected in the UI
  • Should not be used for "behind the scenes" values that have no direct UI impact

 

Refs

  • Do not cause component re-evaluation when changed
  • Can be used to gain direct DOM element access (-> great for reading values or accessing certain browser APIs)

 

className = " " 와 className = { } 의 차이점.

정적 문자열로 클래스를 설정 / JavaScript 표현식을 사용하여 동적으로 클래스 이름을 설정

""로 비워두는 것은 괜찮지만, {} 로 비워주면 정의되지 않았다는 에러가 발생!

 

TimerChallenge.jsx

import {useState} from "react";

let timer; // 상태 업데이트 시 컴포넌트 재실행 => timer 변수 재생성 -> 컴포넌트 바깥에서 정의
// timer가 현재 모든 인스턴스에 공유되고 있으므로, 1초,5초 게임 실행시 덮어씌워짐.

export default function TimerChallenge({title, targetTime}) {
    const [timerExpired, setTimerExpired] = useState(false);
    const [timerStarted, setTimerStarted] = useState(false);



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

    function handleStop(){
        clearTimeout(timer) // 포인터 : timer의 id를 input으로 필요
    }

    return (<section className="challenge">
        <h2>{title}</h2>
            {timerExpired && <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>
    );
}

 

timer 객체를 여러 인스턴스에서 공유하는 문제점 발생

 

const timer = useRef();

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

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

    function handleStop(){
        clearTimeout(timer.current) // 포인터 : timer의 id를 input으로 필요
    }

각자의 timer를 가지고, 기억

 

dialog backdrop이란?

HTML에서 <dialog> 요소가 활성화될 때 나타나는 배경

 

Ref를 다른 컴포넌트로 전달할 수 없다. 또한 해당 컴포넌트의 요소로도 전달할 수 없다.

forwardRef를 통해서 컴포넌트에서 컴포넌트로 참조를 전달해 사용할 수 있다.

이를 사용하려면 컴포넌트 함수를 감싸주어야함.

참조 속성을 받을 때에는

첫번째 매개변수로로 구조 분해 할당가능한 속성이 오며, 두 번째 매개변수로 ref 매개변수를 받는다.

 

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 formattedRemainingTime = (remainingTime / 1000).toFixed(2);
    const score = Math.round((1 - remainingTime / (targetTime*1000))*100);

    useImperativeHandle(ref, () => {
        return {
            open(){
                dialog.current.showModal(); // TimerChalleng 와 ResultModal 컴포넌트를 분리하기 위해
            }
        }
    });
    // ref, 객체를 반환하는 함수를 인자로 가짐. (컴포넌트에 노출되어야 하는 속성 및 메소드)

    return createPortal(
        <dialog ref = {dialog} className="result-modal">
        {userLost &&  <h2> You lost </h2>} {!userLost &&  <h2> Your Score : {score} </h2>}
        <p>The target time was <strong>{targetTime} seconds.</strong></p>
        <p>You stopped the timer with <strong> {formattedRemainingTime} seconds left.</strong></p>
        <form method = 'dialog' onSubmit = {onReset}>
            <button>Close</button>
        </form>
    </dialog>,
        document.getElementById('modal')
    );
})

export default ResultModal;

 

시간관련 브라우저의 내장 기능

  • setTimeout: 일정 시간 후 한 번 실행.
  • setInterval: 일정 간격으로 반복 실행.
  • clearTimeout: setTimeout으로 설정된 타이머를 취소.
  • clearInterval: setInterval로 설정된 반복 작업을 취소.

최종 TimerChalleng.jsx 코드

import {useRef, useState} from "react";
import ResultModal from "./ResultModal.jsx";

export default function TimerChallenge({title, targetTime}) {
    const timer = useRef();
    const dialog = useRef();

    const [timeRemaining, setTimeRemaining] = useState(
        targetTime * 1000
    );


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

    const timerIsActive = timeRemaining > 0 && timeRemaining < targetTime * 1000;

    if(timeRemaining <= 0 ){
        clearInterval(timer.current);
        dialog.current.open();
    }

    function handleReset(){
        setTimeRemaining(targetTime*1000);
    }

    function handleStart() {
        timer.current = setInterval(() => {
            setTimeRemaining(prevTimeRemaining => prevTimeRemaining - 10);
            // setTimerExpired(true);
            // dialog.current.open(); // dialog의 내장요소
        }, 10);
    }

    function handleStop(){
        dialog.current.open();
        clearTimeout(timer.current) // 포인터 : timer의 id를 input으로 필요
    }

    return (
        <>
            <ResultModal
                ref ={dialog}
                targetTime = {targetTime}
                remainingTime={timeRemaining}
                onReset = {handleReset}
            />
        <section className="challenge">
        <h2>{title}</h2>
        <p className="challenge-time">
            {targetTime} second{targetTime > 1 ? 's' : ''}
        </p>
        <p>
            <button onClick={timerIsActive ? handleStop : handleStart}>
                {timerIsActive ? 'Stop' : 'Start'} Challenge
            </button>
        </p>
            <p className={timerIsActive ? 'active' : undefined}>
                {timerIsActive ? "Time is running..." : "Timer inactive"}
            </p>
    </section>
        </>
    );
}

 

dialog를 ESC를 눌러 닫게되면, 버튼클릭과 달리 onReset을 트리거 하지 않는다.

이를 위해서는 onClose속성에 onReset을 바인딩해주어야 한다. 

<dialog onClose={onReset}/> 

 

Portals

현재 이전 코드에서 dialog의 html위치를 찾아보면,

timechallenge의 section 이전에 함께 위치해있는 것을 볼 수 있다.

시각적으로는 문제가 없지만, 기술적으로는 옳지 않다.

(body나 div의 바로 밑에 있는 것이 더 바람직)

시각적으로 보고, html구조를 떠올렸을 때 그 구조가 서로 일치하도록...!

 

 => 이러한 문제들을 Portal을 통해 해결할 수 있다!

컴포넌트에 렌더링될 html코드를 DOM내 다른 곳으로 옮기는 것!

첫 번째 인수 : jsx 코드

두 번째 인수 : html 요소 (index.html에 존재하는)

 

iindex.html에서 body바로 밑에 <div id='modal'/>을 추가.

그 후 createPortal을 사용

 return createPortal(
        <dialog ref = {dialog} className="result-modal">
        {userLost &&  <h2> You lost </h2>} {!userLost &&  <h2> Your Score : {score} </h2>}
        <p>The target time was <strong>{targetTime} seconds.</strong></p>
        <p>You stopped the timer with <strong> {formattedRemainingTime} seconds left.</strong></p>
        <form method = 'dialog' onSubmit = {onReset}>
            <button>Close</button>
        </form>
    </dialog>,
        document.getElementById('modal')
    );
})

 

JSX 코드 렌더링이 현재 어플에서 사용하고 있는 곳이 아니라,
웹페이지의 다른 곳에  가야하는 모달이나 비슷한 시나리오가 있을 때 포탈을 자주 사용함.

 


 

GitHub - codrae/React-The-Complete-Guide

Contribute to codrae/React-The-Complete-Guide development by creating an account on GitHub.

github.com