
JSX에서는 두가지 이상의 루트요소를 리턴 할 수 없다.
에러코드
function App() {
return (
<UserInput onAddCalculator={AddHandler}/>
<UserResult users={userData}/>
);
}
해결방법 첫번째 : 인위적인 <div> </div> 를 만들어서 요소들을 감싸준다.
function App() {
return (
<div>
<UserInput onAddCalculator={AddHandler}/>
<UserResult users={userData}/>
</div>
);
}
해결방법 두번째 : 네이티브 자바스크립트 배열을 사용한다.
function App() {
return (
[
<UserInput onAddCalculator={AddHandler}/>
<UserResult users={userData}/>
]
);
}
그렇다면 첫번째 방법인 <div> </div> 로 무작정 감싸주는것이 최선의 방법일까?
작동은 하겠지만 좋다고만 말할수는 없다.
왜냐하면 “<div> soup"상태에 빠질수 있기 떄문이다.
"<div> soup“ 예시를 보여주는 코드
<div>
<div>
<div>
<div>
<h2> soup 예시 </h2>
</div>
</div>
</div>
</div>
프로젝트를 하다보면 리액트 컴포넌트는 많아지는게 필연적이고 div로 감싸주는것이 필요할 것이다.
하지만 불필요한 div가 실제 DOM에 렌더링된다는것이 문제점이다.
사용자의 입장에서는 성능적으로도 불필요한 div가 렌더링되는것을 원하지 않을것이다. 또한 중첩된 CSS 선택자 관련하여 스타일링 할때도 꼬일여지가 있다.
<div> </div>가 아니라 다른 방법으로 감싸는 형식을 만들어보자.
Wrapper 컴포넌트를 인위적으로 만들어서 자식 요소를 리턴.
Wrapper.js
const Wrapper = props => {
return props.children;
};
export default Wrapper;
그다음.. 사용하고자하는 파일에서 div대신 사용해주면 된다!
App.js
function App() {
return (
<Wrapper>
<UserInput onAddCalculator={AddHandler}/>
<UserResult users={userData}/>
</Wrapper>
);
}
위 단원에서는 Wrapper를 인위적으로 만들어서 코드들을 감싸주었다.
하지만 리액트에서 제공하는 “React.Fragment“를 사용하여 더더욱 편리하게 감싸줄수 있다.
App.js
import React, { Fragment } from 'react;
function App() {
return (
<React.Fragment>
<UserInput onAddCalculator={AddHandler}/>
<UserResult users={userData}/>
</React.Fragment>
);
}
아래 사진처럼 잘못된 입력을 INPUT 했을경우 띄우는 “Modal"창을 예시로 Portlas의 필요성을 느껴보자.
위 "Modal"창을 대략적으로 표현하자면 다음과 같다.
작동은 하지만 좋지가않다. 구조적으로 Modal창이 띄워졌을경우 코드로써 맨윗부분에 있는게 맞지 않을까??
위처럼 Form 옆에 있는것이 아니라 아래사진처럼 모달이 개별적으로 존재하여 원하는곳에 렌더링하게끔(DOM 구조상 맨위쪽으로) 한다면 최적의 코드가 될것이다.
특정 컴포넌트를 원하는 DOM위치에 이동하여 렌더링하게끔 하는것이 "Portals“이다.
먼저 실제 렌더링할 장소를 표시해준다.
index.html
<html lang="en">
...
<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>
Portals를 사용할 상수(컴포넌트)를 추가한다.
ErrorModal.js
// ReactDom은 임의의 이름이 가능하다.
// react-dom은 라이브러리로 이것을 명시해야지만 createPortal 메소드를 사용할 수 있다. 즉 Portals를 이용하려면 필수다.
import ReactDOM from "react-dom";
// Backdrop이라는 이름을 가지는 Portals를 사용하는 컴포넌트
const Backdrop = (props) => {
return <div className={classes.backdrop} onClick={props.onErrorHandler} />;
};
// ModalOverlay이라는 이름을 가지는 Portals를 사용하는 컴포넌트
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.onErrorHandler}>닫기</Button>
</footer>
</Card>
);
};
const ErrorModal = (props) => {
return (
<React.Fragment>
{/* 텅텅비는 이곳에서는 무엇을 써야하나?
본격적으로 createPortal 메소드를 사용하자!
createPortal 메소드는 두개의 인수를 취한다.
1. 렌더링되어야하는 리액트 노드.
단, 정말 리액트 노드여야 한다. { }이런식으로 값을 읽어 들이는것은 불가능하다.
2. 포인터 ( 이 요소가 렌더링되어야 하는 실제 DOM 의 컨테이너를 가리키는 포인터 )
*/}
{ReactDOM.createPortal(
<Backdrop onErrorHandler={props.onErrorHandler} />,
document.getElementById("backdrop-root")
)}
{ReactDOM.createPortal(
<ModalOverlay
title={props.title}
message={props.message}
onErrorHandler={props.onErrorHandler}
/>, document.getElementById('overlay-root')
)}
</React.Fragment>
);
};
export default ErrorModal;
잘못된 입력을 하기전(Modal이 띄워지기전) 아무 내용이 렌더링이 되어있지 않은
클래스이름이 "backdrop-root" , "overlay-root" 를 가지는 div부분이
Modal이 띄워지자 해당하는 내용이 채워진것을 볼 수 있다.
Ref : 특정 DOM에서 요소들을 가지고 작업 할 수 있게하는 기능이다.
UserInput.js를 가지고 useState로 상태업데이트 했던것을 Ref를 통해 작업해보자.
UserInput.js
// useRef 를 사용하려면 추가해주어야한다.
import React, { useState, useRef } from "react";
const UserInput = (props) => {
// 해당 상수는 JSX 코드에서 만들 Ref와 연결하게끔 한다.
const nameInputRef = useRef();
const ageInputRef = useRef();
const [error, setError] = useState();
const submitHandler = (event) => {
event.preventDefault();
// 제출 버튼 클릭시 cureent.value를 통해 새로운 이름을 가지는 상수로 저장한다.
const enteredName = nameInputRef.current.value;
const enteredAge = ageInputRef.current.value;
if (enteredName.trim().length === 0 || enteredAge.trim().length === 0) {
setError({
title: "잘못된 이름",
message: "유효한 이름을 입력해주세요. (공백X)",
});
return;
}
if (+enteredAge <1) {
setError({
title: "잘못된 나이",
message: "유효한 나이를 입력해주세요. (음수X)",
});
return;
}
const inputData = {
id: Math.random(),
name: enteredName,
age: enteredAge,
};
props.onAddCalculator(inputData.id, enteredName, enteredAge);
// useState로 상태관리하는대신에 Ref로 관리하므로
// 이때 재설정로직도 바뀌게되는데
// 리액트 없이 DOM을 조작하면된다. (단, DOM을 조작하는것은 올바르지 않기에 이 경우만 해당함)
// 이게 싫으면 그냥 state 기반 해결법을 쓰면됨.
// useState vs Ref
// 값을 읽기만한다면 state는 권장하지않음. 불필요한 코드와작업이 많아서 ref가 나음
// state는 코드가 깔끔하긴함. 많은 리액트 프로젝트에서 ref로 사용하기도함.
// ref는 많은 기능이 있음.
nameInputRef.current.value = '';
ageInputRef.current.value = '';
};
const errorHandler = () => {
setError(null); // null은 falsy값이다.
};
return (
<div>
{error &&(
<ErrorModal
title={error.title}
message={error.message}
onErrorHandler={errorHandler}
/>
)}
<Card>
<form onSubmit={submitHandler}>
<label htmlFor="username" className={classes.label}>
UserName
</label>
<input
id="username"
className={classes.input}
type="text"
// ref 프롭은 내장된요소다. 어떤 HTML 요소에도 추가 가능.
// 해당 input에대한 정보를 nameInputRef와 연결한다.
ref={nameInputRef}
></input>
<label htmlFor="age" className={classes.label}>
Age(Years)
</label>
<input
id="age"
className={classes.input}
type="number"
ref={ageInputRef}
></input>
<Button type="submit" className={classes.button}>
Add User
</Button>
</form>
</Card>
</div>
);
};
리렌더링을 하지 않고 정보 업데이트를 하고자할때 유용하다.
Refs을 사용할때 상호작용하는 이 방식에는 특별한 이름이 있다.
바로 “제어되지 않는 컴포넌트”라는것이다.
왜 제어되지 않는다고 표현할까? 실제로 이것들은 자체 내부의 State이기 때문에 이것들 안으로 반영되는 값들은 리액트에 의해 제어되지 않는다고 표현한다.
엄밀하게 말하면 리액트 기능을 사용하는것은 맞다. 하지만 단지 입력된 값을 가져오는것이지, 인풋에 다시 데이터를 보내는것은 아니기 때문이다.
반대로 Refs를 사용하기전의 방식은 “제어되는 컴포넌트”라고 한다.
내부 state가 리액트에 의해 제어되기 때문이다. 모든 키 입력에서 해당 state를 업데이트했다. 다시 그 값들을 프롭으로 그 state를 인풋에 다시 전달한다.