똑같은 삽질은 2번 하지 말자
React vol.5 (portal, ref, forwardRef) 본문
개요
React JS 를 공부하면서 기본을 정리해두고자 작성
Portal
화면을 만들어나감에 있어 모달같은 UI도 필요한 상황이 있을건데 보통 모달은 모달이 필요한 컴포넌트에서 모달의 상태를 관리해야 하기 때문에 해당 컴포넌트에서 모달을 렌더링하는 경우가 있다. 하지만 모달은 HTML구조상 하위 태그에 있다고 하기 보다는 제일 상위태그들과 같은 계층에 두어야 좀더 올바르다고 말할 수 있다.
그런 상황일때 portal을 이용하면 된다.
usage
index.html
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="backdrop-root"></div>
<div id="overlay-root"></div>
<div id="root"></div>
</body>
</html>
ErrorModal.js
import React from 'react';
import ReactDOM from 'react-dom';
import Card from './Card';
import Button from './Button';
import classes from './ErrorModal.module.css';
const Backdrop = (props) => {
return <div className={classes.backdrop} onClick={props.onConfirm} />;
};
const ModalOverlay = (props) => {
return (
<Card className={classes.modal}>
<header className={classes.header}>
<h2>{props.title}</h2>
</header>
<div className={classes.content}>
<p>{props.message}</p>
</div>
<footer className={classes.actions}>
<Button onClick={props.onConfirm}>Okay</Button>
</footer>
</Card>
);
};
const ErrorModal = (props) => {
return (
<React.Fragment>
{ReactDOM.createPortal(
<Backdrop onConfirm={props.onConfirm} />,
document.getElementById('backdrop-root')
)}
{ReactDOM.createPortal(
<ModalOverlay
title={props.title}
message={props.message}
onConfirm={props.onConfirm}
/>,
document.getElementById('overlay-root')
)}
</React.Fragment>
);
};
export default ErrorModal;
ref?
DOM요소에 접근하기 위해 쓰는 친구
함수형 방식에서는 useRef라는 훅이 존재한다.
때로는 state를 쓰지않고 ref만을 써서 로직을 구성해 나갈 수도 있는데 그건 보통 값의 변경이나 감시가 이루어질 필요가 거의 없고 그저 dom요소의 값을 읽어들이기만 해도 되는 경우에는 ref를 추천한다.
usage
import React, { useState, useRef } from 'react';
import Button from '../UI/Button';
const AddUser = (props) => {
const nameInputRef = useRef();
const ageInputRef = useRef();
const addUserHandler = (event) => {
event.preventDefault();
const enteredName = nameInputRef.current.value;
const enteredUserAge = ageInputRef.current.value;
props.onAddUser(enteredName, enteredUserAge);
nameInputRef.current.value = '';
ageInputRef.current.value = '';
};
return (
<form onSubmit={addUserHandler}>
<label htmlFor="username">Username</label>
<input id="username" type="text" ref={nameInputRef} />
<label htmlFor="age">Age (Years)</label>
<input id="age" type="number" ref={ageInputRef} />
<Button type="submit">Add User</Button>
</form>
);
};
export default AddUser;
useRef와 useState는 React에서 상태를 관리하는 데 사용되지만 서로 다른 목적과 특징을 가지고 있습니다. 아래에 이 두 훅의 주요 차이점과 useRef를 사용하는 이유에 대해 봐보자.
재렌더링
useState는 상태가 변경될 때마다 컴포넌트를 재렌더링합니다. 반면에 useRef는 값이 변경되더라도 컴포넌트를 재렌더링하지 않습니다. 이 특징 때문에 useState를 사용하여 타이머 ID를 관리하면 마우스 이동이 감지될 때마다 컴포넌트가 재렌더링됩니다. 이는 성능에 영향을 미칠 수 있으며, 예상치 않은 렌더링 동작을 유발할 수 있습니다
값의 유지
useRef는 .current 프로퍼티를 통해 값을 유지하며, 이 값은 컴포넌트의 라이프 사이클 동안 유지됩니다. 이러한 이유로 useRef는 이전 값에 액세스하거나 변경사항을 추적하기 위해 종종 사용됩니다.
useState는 상태를 변경하기 위해 세터 함수를 제공하며, 이전 상태 값에 액세스하려면 콜백 함수를 사용해야 합니다.
타이머 관리
useRef는 타이머 ID를 저장하고 추후에 clearTimeout을 호출하기 위한 안전한 방법을 제공합니다. useRef의 .current 프로퍼티는 컴포넌트의 라이프 사이클 동안 일관된 값을 유지하므로, 이벤트 핸들러 내에서 타이머 ID에 안전하게 액세스할 수 있습니다. useState를 사용하면 상태 변경과 관련된 복잡성 때문에 예기치 않은 동작이 발생할 수 있으며, 이는 특히 비동기 작업(예: setTimeout 및 clearTimeout)과 관련하여 문제를 유발할 수 있습니다. 이러한 이유로 useRef를 사용하여 타이머 ID를 관리하는 것이 더 안정적이고 예측 가능한 동작을 제공합니다.
이 타이머 관리와 관련으로 어떤 영상 재생 컨트롤러를 영상 부분의 엘리먼트에서 마우스가 호버 + 마지막 움직임이 발생한 3초뒤 컨트롤러를 숨겨야 하는 요건이 있었는데 일부러 마우스를 계속해서 움직이면 사라져야 할 컨트롤러가 사라지지 않고 표시되고 비표시되는 등등 제어하기 힘든 상태가 되었었다. 이 timeout관련의 timeoutId를 useRef로 관리하게 됨으로써 이런 현상이 없어졌다.
forwardRef
부모 컴포넌트에서 자식 컴포넌트로 ref 를 전달하여 부모가 자식의 ref를 사용할 수 있게하는 문법
usage
import React from 'react';
import classes from './Input.module.css';
const Input = React.forwardRef((props, ref) => {
return (
<div className={classes.input}>
<label htmlFor={props.input.id}>{props.label}</label>
<input ref={ref} {...props.input} />
</div>
);
});
export default Input;
import { useRef, useState } from 'react';
import Input from '../../UI/Input';
import classes from './MealItemForm.module.css';
const MealItemForm = (props) => {
const amountInputRef = useRef();
return (
<form className={classes.form}>
<Input
ref={amountInputRef}
label='Amount'
input={{
id: 'amount_' + props.id,
type: 'number',
min: '1',
max: '5',
step: '1',
defaultValue: '1',
}}
/>
<button>+ Add</button>
</form>
);
};
export default MealItemForm;