JSX는 return할 때 한 개의 root를 가지고 return 되어야 하는 문법을 가지고 있다. 즉 중첩된 component를 그냥 return 하게 되면,
Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment
이와 같은 에러를 마주하게 된다.
이에 대한 흔한 해결법으로 사용하는 것은 <div> </div>
를 사용해 return하려는 component를 감싸는 것이다. wrapper 역할을 하지만 구조적,의미적으로는 무의미한 <div\>
를 사용해서 하나의 root를 가질 수 있도록 하는 것이다.
const nestedComponent = () => {
return (
<div>
<Component1 />
<Component2 />
</div>
)
}
하지만 이러한 방식의 해결은 아주 작은 단위의 리액트 프로젝트에서는 문제가 되지 않을지 몰라도, 큰 프로젝트에서는 <div> Soup
라고도 불리는 문제점을 발생시키기도 한다. 여러개의 component를 rendering하다보면, 각 component별로 <div> </div>
를 사용하여 JSX문법의 한계를 해결하다보니
<div>
<div>
<div>
<div>
<Component1 />
</div>
</div>
</div>
</div>
이와 같이 너무 많은 <div> </div>
들이 중첩되어 rendering되는 경우가 생긴다. 이는 성능면에서 비효율적일 뿐더러 css가 꼬이게 되는 상황도 발생할 수 있다.
Wrapper의 역할을 하는 component를 따로 만들어준다! Wrapper역할을 하기 위해 children을 이용한다. children은 여닫는 태그 사이에 있는 모든 코드를 props.children안에 담게되고, 이를 사용할 수 있도록 하는 역할을 한다. 이러한 특성을 사용하여 단순하게 props.children을 반환하는 사용자 정의 component를 만들어준다는 의미이다!
// Wrapper.js
const Wrapper = props => {
return props.children;
};
export default Wrapper;
이렇게 만들어 준
Wrapper component
를 다른 component파일에 import해서div
대신에 사용하여 해결할 수 있다.
// nestedComponent.js
import Wrapper from './Wrapper.js'
const nestedComponent = () => {
return (
<Wrapper>
<Component1 />
<Component2 />
</Wrapper>
)
}
<Component1 />
과 <Component2 />
가 Wrapper
의 prop.children으로 들어가게 되고, 결론적으로 Wrapper
는 props.chldren
이라는 하나의 값을 return하게 되므로 하나의 root
를 가지고 반환해야 한다는 JSX의 문법 구조를 만족한다.
위에서 살펴본 Wrapper component는 사용자가 직접 만든 component이다. 하지만 역시나! React에는 이러한 Wrapper component의 역할을 하는 component가 따로 정의되어있다. 그것이 바로
Fragment Component
이다.
React.Fragment
또는Fragment
를 그냥 import해서 사용할 수 있다. 이 Fragment Component는 빈 구문을 rendering한다. 즉 추가적인<div>
와 같은 html element를 생성하지 않는다.추가적으로 프로젝트의 설정이 되어있다면 fragment가 아닌
<> </>
와 같이 빈 태그로 사용할 수도 있다.
1.
const nestedComponent = () => {
return (
<React.fragment>
<Component1 />
<Component2 />
</React.fragment>
)
}
2.
import { Fragment } from 'react';
const nestedComponent = () => {
return (
<Fragment>
<Component1 />
<Component2 />
</Fragment>
)
}
3.
const nestedComponent = () => {
return (
<>
<Component1 />
<Component2 />
</>
)
}
오버레이 코드 (모달, 사이드 드로어, 다이얼로그)의 경우 하나의 root component안에서 rendering된다면, css styling으로 인해 그 본연의 기술처럼 동작할 순 있지만, 사실상 그 본연의 기술을 한다고 볼 수 없다. 왜냐하면 오버레이 코드같은 경우에는 말 그대로 모든 html 코드들의 상단에 위치해야 하므로 어떠한 Component내부에 들어가 있으면 안되기 때문이다.
이러한 방식은 버튼을 만들 때 <button>
을 사용하는게 아니라 <div>
을 사용하는 것과 같다. 동작에는 크게 문제가 없지만 의미적으로나 구조적으로나 알맞은 코드가 아니기 때문이다.
이때 기존처럼 하나의 component 내부에 오버레이를 담당하는 component를 중첩시켜서 작성하지만 실제 rendering 될 때는 해당 오버레이 component를 html의 가장 상단으로 빼서 rendering되도록 하는 기능이 바로
React Portals
이다.
1️⃣ index.html에 새로운 Root 요소 만들기
<!--index.html --> <!-- 오버레이 component(ex.모달)를 위치시키기 위한 root--> <div id="backdrop-root"></div> <div id="overlay-root"></div> <!-- 기존에 사용하던 root--> <div id="root"></div>
2️⃣ 내가 이동시키려는 component 혹은 코드 부분을 새로운 함수에 담아서 return
// ErrorModal.js // 이동시키려는 backdrop 관련 코드 const Backdrop = props => { return <div className={classes.backdrop} onClick={props.onConfirm}/> } // 이동시키려는 Modal 관련 코드 const ModalOverlay = props => { return ( ...생략 (모달 관련 코드) ) } // 기존에는 backdrop과 modal관련 코드가 들어있었지만 들어있었지만 이제 없음 const ErrorModal = props => { return ( <React.Fragment> </React.Fragment > ) }; export default ErrorModal;
3️⃣ React DOM import 해오고 사용하려는 위치에
ReactDOM.createPortal
선언하기
ReactDOM.createPortal( 1 , 2 )
은 2개의 인자를 받는다.
1. 렌더링되어야 하는 리액트 노드
2️⃣ 에서 만든 새로운 component를 첫 번째 인자로 받는다. 이때 기존의 component와 같이 내부에 props를 같은 것을 전달할 수 있다.
2. 이 요소가 실제로 렌더링 되어야 하는 DOM의 컨테이너를 가리키는 포인터
Backdrop component
의 경우 1️⃣ 에서 만든 backdrop-root
의 위치에 rendering해야하므로, 해당 위치를 가지고 온다.
// ErrorModal.js // import해오기 import ReactDOM from 'react-dom'; const Backdrop = props => { return <div className={classes.backdrop} onClick={props.onConfirm}/> } const ModalOverlay = props => { return ( ...생략 (모달 관련 코드) ) } const ErrorModal = props => { return ( <React.Fragment> // portal 선언하기! (인자 2개) {ReactDOM.createPortal( // 1. 렌더링되어야 하는 리액트 노드 <Backdrop onConfirm={props.onConfirm}/>, // 2. 실제 렌더링되어야 하는 DOM 위치 document.getElementById('backdrop-root') )} </React.Fragment > ) }; export default ErrorModal;
모달과 같은 component를 기존과 같이 rendering할 경우 표면적으로는 아무 문제 없이 동작하는 듯 하나, 성능상이나 구조적으로 옳지 않은 코드이다.
따라서 index.html
에 하나의 root div
를 만들어두고 거기서 rendering하던 기존의 방법과 다르게, 화면의 가장 최상단에 위치하고싶은 경우처럼 위치가 정해져있는 경우, 해당 위치에 새로운 div
를 만들어둔다.
<!--index.html-->
<body>
...생략
<!-- 새롭게 만든 root (modal이 위치해야 하는 곳) -->
<div id="new-root"></div>
<!-- 기존 root -->
<div id="root"></div>
</body>
기존 하나의 component에 담겨있던 모달 관련 코드를 기존의 component 파일 내부에 새로운 component로 만든다. (새롭게 만들어진 이 component가 해당 파일에서만 사용되는 경우(export 안 되는 경우) 하나의 파일에서 여러개의 component가 정의되어도 괜찮다.)
새롭게 만들어진 component를 return하는 부분에서 ReacDOM.createPortal
문법에 따라 사용한다.
const newModal = props => {
return (기존 모달 관련 코드)
}
const originModal = props => {
return (
<React.Fragment>
{ReactDOM.createPortal(
<newModal onConfirm={props.onConfirm}/>,
document.getElementById('new-root')
)}
</React.Fragment>
)
}
이렇게 하면 알맞은 html 위치에 모달 띄우기 성공~!
🧐 useRef()?
useRef()
는 기본적으로 component가 return하는 jsx코드의 DOM 요소에 접근해서 사용할 수 있도록 한다.
useRef()
가 반환하는 값은 항상 객체이고,current
라는 prop을 기본으로 가지고있다. 이때current
prop에는ref
로 연결된 html의 DOM node값이 저장된다. 따라서 DOM요소를 가지고 할 수 있는 많은 것들을 ref를 통해서 작업할 수 있다.
하지만 ref를 이용해서 DOM 요소를 수정하거나 조작하는 것은 추천되지 않는다. 그저 값을 읽어오는 것과 같은 작업을 하기엔 용이하다.
1️⃣ 사용하려는
ref
를useRef
를 사용하여 미리 선언import React, { useRef } from 'react'; const AddUser = props => { <!-- useRef로 미리 선언해두기--> const nameInputRef = useRef(); const ageInputRef = useRef(); ...생략 return ( ...생략 ); } export default AddUser;
2️⃣ 선언한
useRef
와html
을 연결1️⃣ 에서 선언한
useRef
와html element
를 연결하려면, 연결하려는html element
에ref prop
을 넣어주면 된다. 보통은<input>
과 연관되어 많이 사용되지만 모든 html element는 ref값을 가질 수 있음을 기억하자.const AddUser = props => { const nameInputRef = useRef(); const ageInputRef = useRef(); ...생략 return ( <Wrapper> <form onSubmit={addUserHandler}> <label htmlFor="username">Username</label> <input id="username" type='text' // ref를 prop으로 주어서 만들어둔 useRef() 연결하기! ref={nameInputRef} /> <label htmlFor="age">Age (Years)</label> <input id="age" type='number' // ref를 prop으로 주어서 만들어둔 useRef() 연결하기! ref={ageInputRef} /> <Button type='submit'>Add User</Button> </form> </Wrapper> ) }
ref를 사용하지 않던 기존 코드는 state를 선언해서 input에 입력이 될 때마다 state를 업데이트하는 방식이었다. 하지만, ref값
을 이용해서 input의 value를 읽어온다면, input에 값이 입력될 때마다 state를 업데이트하던 방식의 기존 코드를 개선할 수 있다! 따라서 input의 value와 관련된 state값을 없애고, ref를 선언하여 input과 연결한 후, 해당 ref에 반환된 객체의 current.value를 통해 input의 value를 읽는 코드를 form이 submit되는 버튼이 눌릴 때 실행되는 함수안에 정의해둔다면, input의 value값을 읽어올 수 있다.
// AddUser.js
const AddUser = props => {
// 1. useRef선언
const nameInputRef = useRef();
const ageInputRef = useRef();
// 3. 연결된 함수에서 ref값 이용하기
const addUserHandler = (event) => {
event.preventDefault();
// ref가 가진 current prop을 이용해서 input의 value에 접근
const enteredName = nameInputRef.current.value;
const enteredUserAge = ageInputRef.current.value;
...생략
props.onAddUser(enteredName, enteredUserAge);
// ref로 DOM 요소 수정하기..
nameInputRef.current.value = '';
ageInputRef.current.value = '';
}
return (
<Wrapper>
<!--3. 연결된 함수 -->
<form onSubmit={addUserHandler}>
<label htmlFor="username">Username</label>
<input
id="username"
type='text'
// 2. ref 연결
ref={nameInputRef}
/>
<label htmlFor="age">Age (Years)</label>
<input
id="age"
type='number'
// 2. ref 연결
ref={ageInputRef}
/>
<Button type='submit'>Add User</Button>
</form>
</Wrapper>
)
}
submit버튼이 눌렸을 때 input의 요소를 reset하는 경우에는 DOM요소 조작을 하는 것이 효율적이기도 하다. 물론 state값을 둬서 submit되면 빈값으로 input의 value를 update할 수 있지만 ref의 DOM을 조작해서 하는 방법도 이 경우에는 효율적이다!
(하지만 정말 아주 드물게 ref를 이용해서 DOM조작을 한다고 하니 state를 쓰는 방법을 더 손호해야 할 듯 하다)
보기에는 state을 이용한 방법과 ref를 이용한 방법에 큰 차이가 없어보인다. 어떤 때에 ref를 쓰고 state를 쓰는걸까?
ref
값을 빠르게 읽고 그 값을 바꾸거나 하지 않을 경우
state
그 외에 값에 접근하여 조작도 해야하는 경우
이렇게 보면 될 듯 하다 ㅎㅎ
ref
를 통해 Input의 값에 접근하고, input의 value를 빈 값으로 업데이트 하는 방식은 제어되지 않는 component이다. 이름에서도 알 수 있듯, ref를 사용하는 경우 우리는 예외적으로 ref와 DOM API를 이용하여 component를 조작한다. 이때 물론 useRef라는 react요소를 사용하긴 하지만 이것은 단순히 HTML 값에 접근하기 위해서 사용하는 것이고, 실제로 해당 html의 값을 바꾸는 것은 DOM API를 이용하므로 해당 방법은 제어되지 않는 component이다.
반면 이전까지 사용해온 state
를 이용해서 input의 value를 읽고 업데이트 하는 방식은 제어되는 component이다. ref와 달리 DOM API를 이용하는 것이 아닌 useState의 함수를 사용하여 input의 value값을 업데이트한다.
이러한 개념은 현업에서 많이 쓰인다고 하니.. 알아두자!
좋은 글이네요