본문 바로가기
  • 기록
React/(Udemy)React-The Complete Guide

[React] Fragments, Portals & "Refs"(섹션9)

by juserh 2022. 6. 20.
  • JSX Limitations & Fragments
  • Getting a cleaner DOM with Portals
  • Working with Refs

1. JSX 제한사항 및 해결방법

<JSX 제한사항>
반환되는 "root" JSX element는 단 하나여야만 한다. 여러 개의 JSX element들을 인접하게 반환할 수 없다. 그러나, 루트 element 안에는 여러 개의 JSX element가 인접할 수 있다.

해결법 1) 인접한 요소들을  <div> 등의 element 하나로 감싸서 반환

해결법 2) native javascript array: JSX  element들을 자바스크립트 배열 []에 담아서 반환, 그런데 이때는 모든 element들에 "key" props를 주어야 함.

 

-> 모든 element들에 key속성을 주는 것은 귀찮으므로 보통은 <div>로 감싸는 방법을 사용한다.

    그런데, 여기서 또 다른 문제가 발생한다. 바로 "<div> soup"이다.

<div>
    <div>
        <div>
            <div>
            	<h2>이런 게 div soup다!</h2>
            </div>
        </div>
    </div>
</div>

아무 이유 없이 그저 JSX코드 제한사항을 해결하기 위해 감싸는 용도의 <div>를 사용하다보면, 코드가 위와 같이 될 때가 있다.  그러면 리액트는 의미 없는 element들까지 모두 렌더링해야 하므로 프로그램 속도나 성능이 떨어지게 된다.

 


2. 컴포넌트 감싸는 Wrapper 생성

그럼 "<div> soup"를 피해갈 다른 방법이 없을까....? 하면 컴포넌트를 감싸는 다른 Wrapper 컴포넌트를 만드는 것이다.

const Wrapper = (props) => {
  return props.children;
};

export default Wrapper;

Wrapper 컴포넌트 코드를 살펴보면, JSX코드를 반환하고 있지 않다. 그냥 감싸고 있는 내용들을 그대로 출력한다.

 

그리고 위 Wrapper 컴포넌트로 의미 없는 <div>를 없애보자.

먼저 기존의 AddUser 컴포넌트의 JSX 반환 부분 코드를 살펴보면,

  return (
    <div>
      {error && (
        <ErrorModal
          title={error.title}
          message={error.message}
          onConfirm={errorHandler}
        />
      )}
      <Card className={classes.input}>
        <form onSubmit={addUserHandler}>
          <label htmlFor="username">Username</label>
          <input
            id="username"
            type="text"
            onChange={usernameChangeHandler}
            value={enteredUsername}
          />
          <label htmlFor="age">Age (Years)</label>
          <input
            id="age"
            type="number"
            onChange={ageChangeHandler}
            value={enteredAge}
          />
          <Button type="submit">Add User</Button>
        </form>
      </Card>
    </div>
  );

<div>태그로 내용들을 감싸고 있다. root element를 하나로 하기 위해서이다.

여기서 <div> 대신에 Wrapper 컴포넌트를 써서 수정하면,

  return (
    <Wrapper>
      {error && (
        <ErrorModal
          title={error.title}
          message={error.message}
          onConfirm={errorHandler}
        />
      )}
      <Card className={classes.input}>
        <form onSubmit={addUserHandler}>
          <label htmlFor="username">Username</label>
          <input
            id="username"
            type="text"
            onChange={usernameChangeHandler}
            value={enteredUsername}
          />
          <label htmlFor="age">Age (Years)</label>
          <input
            id="age"
            type="number"
            onChange={ageChangeHandler}
            value={enteredAge}
          />
          <Button type="submit">Add User</Button>
        </form>
      </Card>
    </Wrapper>
  );

Wrapper 컴포넌트는 JSX코드 사용 없이 그저 props.children으로 내용만 반환하고 있으므로,

개발자도구를 열어서 elements로 html 코드를 확인해보면 의미 없는 <div>가 더이상 없는 것을 확인할 수 있다.

 


3. React Fragment

그런데 위 Wrapper 컴포넌트는 사실 리액트에서 제공하고 있다. Fragment 컴포넌트가 바로 그것인데, <React.Fragment></React.Fragment>나 <></>로 사용할 수 있다. 이 컴포넌트는 html의 특정 DOM이 아니라 그냥 빈 Wrapper를 렌더링한다.

 

그러면 이를 이용해서 App.js을 수정해보자.

  return (
    <div>
      <AddUser onAddUser={addUserHandler} />
      <UsersList users={usersList} />
    </div>
  );

기존에 위와 같았던 코드를 수정하여,

  return (
    <>
      <AddUser onAddUser={addUserHandler} />
      <UsersList users={usersList} />
    </>
  );

이렇게 만들어 주어도 브라우저에서 잘 실행된다.

  return (
    <React.Fragment>
      <AddUser onAddUser={addUserHandler} />
      <UsersList users={usersList} />
    </React.Fragment>
  );

혹은 <React.Fragment>로 작성해도 되고,

import React, { useState, Fragment } from "react";
 .
 .
 .
 
 return (
    <Fragment>
      <AddUser onAddUser={addUserHandler} />
      <UsersList users={usersList} />
    </Fragment>
  );
  .
  .
  .

혹은 Fragment를 import하여 <Fragment>로 작성해도 된다.

 


4. React Portals

Portals 역시 코드를 깔끔하게 표현하기 위해 사용한다.

return(
    <React.Fragment>
    	<MyModal />
        <MyInputForm />
    </React.Fragment>
);

우리가 JSX코드를 위와 같이 작성하여 모달을 브라우저 위에 오버레이하여 보여준다고 해보자.

이때 위 코드가 실제로 DOM에는 아래와 같이 나타는데,

<section>
    <h2>Some other content ...</h2>
    <div class="my-modal">
        <h2>A Modal Title!</h2>
    </div>
    <form>
    	<label>Username</label>
        <input type="text" />
    </form>
<section

언뜻 보면 잘못된 부분은 없다.

다만 의미적으로(?), 구조적으로(?) 살펴보면 "my-modal"은 전체 페이지에 오버레이되어서 출력되는데 실제 DOM에 구조를 살펴보면, 그런 구조가 반영되지 않았다(모달이랑 폼이 나란히 배치되는 것으로 해석됨).

 

때문에 Portals로 개선할 수 있는데, 그러면 

<div class="my-modal"> 
    <h2>A Modal Title!</h2>
</div>
<section>
    <h2>Some other content ...</h2>
    <form>
        <label>Username</label>
        <input type="text" />
    </form>
</section>

실제 DOM이 위와 같이 나타난다.

모달이 폼과 나란히가 아닌 더 상위에 표시되는 것을 확인할 수 있다.

이럴 때 React Portals가 사용되는 것이다.

 


5. Portals 작업

이전에 작업한 것을 실행하고 DOM을 살펴보면,

이렇게 에러모듈과 폼이 나란히 렌더링 되고 있는 것을 볼 수 있다.

 

이것을 Portals로 더 보기 좋게 바꿔볼 것이다.

현재는 root 안에 위치하고 있는 "ErrorModal_backdrop"과 "ErrorModal"을 <body> 바로 안으로 옮겨 root와 나란히 위치하도록 할 것이다(왜냐하면 모달은 페이지 전체에 오버레이 되는 것이므로 의미상 이렇게 랜더링 하는 것이 맞으니까..!).

 

<Portal을 사용 시 주의할 점>
1. 컴포넌트를 이동시킬 장소가 있어야 한다.
2. 그 컴포넌트에게 해당 장소로 포털을 가져가야 함을 알려야 한다.

 

1. 먼저, public/index.html를 수정해준다.

  <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>

기존 코드에서 body의 바로 아래, root와 나란한 위치에 모달의 뒷배경과 오버레이 모달이 렌더링될 위치를 지정해주는 것이다.

 

2. ErrorModal 컴포넌트에서 코드 수정한다.

먼저 backdrop과 modal창을 다른 컴포넌트로 빼주려고 한다.

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>
  );
};

ErrorModal컴포넌트 내에서 만들었던 backdrop과 modal창을 위와 같이 따로 꺼내어 선언해주고,

 

import ReactDOM from "react-dom";

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>
  );
};

react-dom을 import하고,

createPortal 메소드를 이용하여 특정 부분에 랜더링할 element(첫번째 인수)와 그걸 넣어줄 부분(두번째 인수)를 지정해준다.

ReactDOM.createPortal(
        <Backdrop onConfirm={props.onConfirm} />,
        document.getElementById("backdrop-root")
)

 

이렇게 코드를 수정하고 나면,

모달창 뜨기 전
모달창 뜬 상태

개발자 도구로 html 코드를 살펴보면 모달창이 뜨면 <body> 바로 아래, 즉 root와 나란하게 렌더링되는 것을 확인할 수 있다.

이렇게 Portal을 이용하면 특정 컴포넌트를 다른 위치로 렌더링할 수 있다.

 


6. Ref 작업

ref를 사용하면 html DOM에 접근할 수 있다.

 

1. 먼저, useRef 훅을 import 해준다.

import React, { useRef } from "react";

2. ref를 선언해준다: useRef()의 인수로 초기값을 지정할 수 있다.

  const nameInputRef = useRef();
  const ageInputRef = useRef();

3. ref로 접근할 html DOM에 ref 속성을 지정해준다.

          <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} />

이렇게 하면 useRef로 html DOM에 접근할 수 있다.

 

여기까지 하고, AddUserHandler에

    console.log(nameInputRef);

 위 코드를 추가하여 console에 출력해보면,

이렇게 useRef를 이용해 선언한 변수 nameInputRef에 <input> DOM객체가 저장된 것을 볼 수 있다.

그리고 이번엔 위 코드를 

    console.log(nameInputRef.current.value);

이렇게 수정하면,

해당하는 html element에 입력된 value값도 알 수 있다.

 

그래서 useState이 아닌 useRef로 입력된 값에 접근하여 데이터를 저장하는 방식으로 수정해보자.

  const addUserHandler = (event) => {
    event.preventDefault();
    console.log(nameInputRef.current.value);
    const enteredName = nameInputRef.current.value;
    const enteredUserAge = ageInputRef.current.value;

    if (enteredName.trim().length === 0 || enteredUserAge.trim().length === 0) {
      setError({
        title: "InValid input",
        message: "Please enter a valid name and age (non-empty values)",
      });
      return;
    }
    if (+enteredUserAge < 1) {
      setError({
        title: "InValid age",
        message: "Please enter a valid age (>0)",
      });
      return;
    }
    props.onAddUser(enteredName, enteredUserAge);
    nameInputRef.current.value = "";
    ageInputRef.current.value = "";
  };

이렇게 하면 이전에 state을 사용할 때보다 코드가 더 짧고 간단한 것을 볼 수 있다.

그런데 useRef는 html DOM에 직접 접근하는 방법으로 사용에 유의해야 한다.

 

값만 빠르게 읽고 그걸 변경할 계획이 없다면, useRef 사용(ex. 키 로그 출력 시 등)

 


7. Ref: 제어되는 컴포넌트와 제어되지 않는 컴포넌트

앞에 ref로 <input>에 접근하여 데이터에 접근하였는데,

이런 경우 해당 컴포넌트를 "uncontroled component"라고 한다.

왜 uncontroled component 일까?

 

이전에 state을 통해 <input>을 관리했을 때는 입력값이 변경된 경우(onChange)엔 리액트로 state을 변경, 또 제출 버튼을 눌러 데이터가 제출되고 난 후에는 state의 값을 empty로 변경하여 <input> value에 적용되도록 하는 식으로 진행하였다. 이런 경우는 리액트에 의해 element가 제어되므로, state을 이용한 경우는 controled component라고 할 수 있다.

 

그러나 ref를 이용할 경우에는 물론 react의 훅을 이용하는 것이긴 하지만, ref에 html DOM을 저장하여 DOM에 직접 접근하는 것이므로 리액트를 통한 관리가 아니게 된다. 그래서 ref를 이용한 경우에는 uncontroled component라고 하는 것이다.

 


섹션9 끝!