리액트 컴포넌트는 JSX 코드를 리턴한다. 리액트는 이 코드를 실제 DOM으로 렌더링한다. 하지만 하나의 root JSX 요소만 리턴해야한다는 제약사항이 존재한다. 인접한 구조를 만들기 위해 하나의 root 요소가 필요하다. 이전에도 정리한 내용이지만 JS 함수는 하나의 문자열만 반환할 수 있기 때문이다.
JSX코드는 React.createElement()
함수로 변환된다. 단 1개의 React.createElement()만 반환된다. 이러한 JS의 태생적 한계때문에 root JSX element는 하나만 존재해야한다. 3번째부터 나열되는 전달인자로 인해 root 요소 하위에 나란히 요소들을 추가할 수 있다.
첫 번째 방법은 반환되는 JSX 코드에 div와 같은 하나의 요소를 감싸서 root 요소 구조로 만든다.
두 번째로는 배열 형태로 반환할 수 있지만, key 속성이 필요하다. 이 방식은 매번 배열 형태로 만들어주어야 하며, key 속성의 값을 하드코딩으로 매 번 다르게 세팅해야되서 일반적으로는 wrapping해서 하나의 root 요소로 만든다.
<div> Soup
<div>
<div>
<div>
<h2>어떠한 컨텐츠 내용들</h2>
</div>
</div>
</div>
실제로 DOM으로 렌더링될 때, 리액트 컴포넌트들은 많이 중첩된다. 하지만 JSX 상에 하나의 root 요소만을 위해 div 같은 요소들로 감싸게 되면 위와 같은 문제가 발생한다. 불필요한 div나 기타 요소들이 시멘틱적인 구조적인 의미없이 만들어진다. 이는 스타일링에 문제를 발생시키고 좋지 않은 관행이 된다. 많이 중첩된 HTML 요소는 렌더링 성능을 저하시킨다.
const Wrapper = props => {
return props.children;
};
export default Wrapper;
props.children만 반환하는 래퍼 컴포넌트 생성해보자. 이 컴포넌트를 JSX 코드의 root 요소로 적용시켜주면, 실제 래핑된 요소가 DOM으로 렌더링 되지 않는다. 에러없이 문제를 해결할 수 있으며 불필요한 div soup 현상이 일어나지 않는다.
<React.Fragment>
, <Fragment>
나 <>
를 사용하여 가독성있고 깔끔한 코드를 작성할 수 있다. empty wrapper component는 DOM위에 실제 HTML 요소로 렌더링되지 않는다. React의 JSX에 내장되어있는 wrapper 컴포넌트가 존재하므로, 위에서 만들었던 커스텀 래퍼 컴포넌트를 사용하지 않는다.
Vue 3에서는 multi-root node 컴포넌트를 공식적으로 지원하였다. Vue는 개발자들이 대부분 SFC 구조로 많이 개발하기 때문에, vue파일에 의미없이
<templmate>
요소를 사용하여 개발한다. 하지만,<templmate>
은<React.Fragment>
와 동일한 비어있는 래퍼 컴포넌트이다.
리액트의 portals도 fragments처럼 간결한 코드를 위해 존재하는 문법이다.
주로 modal이 깊게 중첩되는 문제를 해결하기 위해 나온 개념이다. 이전에 작성했던 modal의 구조(도메인 컴포넌트 코드 상위에 modal의 요소 코드를 매 번 추가해서 붙여넣는 구조)는 의미적인 관점에서나 HTML의 구조적 간결성을 볼 때는 좋지 않다.
modal, side drawer, dialog들은 전체 페이지 위에 표시되는 오버레이 컴포넌트이다. 구조적으로 모든 레이어보다 제일 위에 올려져있으므로, 스타일링과 접근성의 관점에서 깊게 중첩되는 위치시키는 것은 좋지 않다.
portal을 사용하면, JSX 코드 상에서는 바로 옆에 위치시키지만 렌더링된 실제 DOM에서는 제일 위에(다른 곳에) 위치시킬 수 있다. body의 직계 자식으로 만들어보자.
body 바로 아래에 portals를 추가하기 위해 index.html에 div를 몇 개 추가한다.
<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>
react-dom은 브라우저에 대한 리액트용 어댑터의 일종이다. react는 웹/브라우저에 대해 모르지만, react-dom 패키지는 웹에 대한 인터페이스라고 볼 수 있다. react-dom은 실제 HTML 요소들을 화면에 표시할 때 사용한다.
import { createPortal } from 'react-dom';
const 컴포넌트이름 = props => {
return (
<>
{createPortal(<렌더링 할 컴포넌트 />, document.getElementById('backdrop-root'))}
</>
);
}
createPortal()의 첫 번째 전달인자는 렌더링하는 React child가 들어가고, 두 번째 인자는 렌더링될 DOM 요소이다. 이는 index.js에 있는 root id에 <App />
컴포넌트가 렌더링되는 로직과 동일하다. portals의 핵심은 렌더링된 HTML 내용을 다른 곳으로 옮기는 것이다.
Vue 3의
Teleport
기능과 동일하다. 다른 DOM의 일부에 slot 컨텐츠를 렌더링하는 기능이다. 타겟 대상의 컨테이너에는 to 키워드를 통해 id나 class명 등의 HTMLElement를 지정하여 실제 요소를 지정할 수 있다.
참조(reference)를 줄여서 ref
라고 한다. 다른 DOM 요소에 접근해서 작업할 수 있도록 해주는 기능이다. 마지막에 렌더링되는 HTML 요소들과 다른 JS 코드와의 연결을 해준다. 컴포넌트 함수 안에서 useRef()를 호출하여 사용한다. 어떠한 HTML 요소라도 ref 속성을 통해 연결할 수 있다.
컴포넌트 함수에 JSX 코드에서 ref를 만나면 useRef()에서 설정된 초기 값을 native DOM 요소에 설정한다.
import { useRef } from "react";
const 컴포넌트명 = props => {
const nameInputRef = useRef();
const addUserHandler = e => {
e.preventDefault();
const nameRefValue = nameInputRef.current.value;
// valid
if (!nameRefValue) return;
// props logic
// ...
// init value
nameInputRef.current.value = '';
};
return (
<form onSubmit={addUserHandler}>
<input id="username" type="text" ref={nameInputRef} />
</form>
);
};
export default 컴포넌트명;
위 코드에서 useRef()의 결과는 항상 current 속성을 가진 객체가 반환된다. 즉, nameInputRef.current.value의 값은 input text의 실제 값을 가진다는 뜻이다. DOM은 리액트에 의해서만 조작되어야 하고, 일반적으로 ref는 단순히 DOM의 값을 읽어올 때 사용한다(아닌 경우도 존재하기 때문에 일반적으로라고 표현함). ref 문법을 사용하여 state 기반 코드를 대체할 수 있다.
값을 빠르게 읽고 싶을 때 값을 readonly하고 싶을 때는 ref 문법을 추천한다. state를 사용하면 오히려 불필요한 코드나 작업이 있을 수 있다. ref는 코드량이 적지만, DOM을 조작하는 부분에 대해 native js로 코딩해야한다.
Vue의 template ref와 동일한 개념이다. Vue 2에서는 $ref라는 방식을 사용하였으나 Vue 3에서 반응형 변수를 선언하는 방식(ref/reative) 중 ref()를 template 코드 상의 ref와 동일하게 사용할 수 있게 개선되었다. 컴포넌트가 mount된 이후에 접근할 수 있다.