Child에서 Parent에게 props로 전달 받은 setState() 함수를 실행하면 둘 다 새로 re-rendering이 되는 것을 확인할 수 있습니다.
re-rendering은 이전과 완전히 다른 새로운 메모리 공간에 새로운 컴포넌트를 만드는 것을 뜻합니다.
개발 모드에서만 활성화되는 StrictMode 때문에 2번씩 렌더링 되는 것으로 콘솔에 표시되지만 프로덕션 모드에서는 1번만 찍힌다고 합니다.
https://codesandbox.io/s/tender-tree-92hx3d?file=/src/App.js
const Parent = () => {
// 오류 발생
// 한 번에 1개의 컴포넌트만 return 가능
return (
<Child/> // createElement();
<Child/> // createElement();
);
}
React에서 위와 같이 하나의 컴포넌트에서 여러 개의 컴포넌트를 return하는 것은 불가능합니다.
function add(a, b) {
return a + b, a + c;
}
<Child />는 React.createElement(); 함수로 치환되어 최종적으로는 각각 하나의 컴포넌트로 evaluate 됩니다.
결국 위 코드는 값을 2개 return 하겠다는 뜻인데,
JS에서 Returning Multiple Values을 지원하지 않기 때문에 컴포넌트도 JS의 규칙을 따라서 반드시 하나의 컴포넌트만을 return해야 합니다.
만약 하나의 컴포넌트에서 여러 개의 컴포넌트를 return 하고 싶다면,
아래와 같이 props.children 을 반환하는 Wrapper 컴포넌트를 만들어서 모든 컴포넌트를 Wrapper의 props.children 으로 전달하면 됩니다.
// <Wrapper> ... </Wrapper>
// 사이의 모든 컴포넌트들을 props.children으로 받아서 return
const Wrapper = (props) => {
return props.children;
}
const Component = () => {
return (
<Wrapper>
<div></div>
<div></div>
<div></div>
</Wrapper>
);
}
그런데 이러한 Wrapper 컴포넌트는 꽤나 자주 사용되기 때문에 React에서 자체적으로 비슷한 기능을 내장하고 있습니다.
React.Fragment라는 컴포넌트를 사용하면 비슷한 느낌으로 여러 개의 컴포넌트를 하나로 묶어서 return 할 수 있습니다.
const Component = () => {
return (
<React.Fragment>
<div></div>
<div></div>
<div></div>
</React.Fragment>
);
}
<React.Fragment> ... </React.Fragment>의 짧은 버전으로 빈 컴포넌트를 사용하셔도 됩니다.
<> ... </>와 같이 기입하면 되며, 기능은 React.Fragment와 동일합니다.
const Component = () => {
return (
<>
<div></div>
<div></div>
<div></div>
</>
);
}
Portal에 대해 이야기하기 전에, 먼저 Modal이라는 것에 대해 알아보겠습니다.

로그인 후 이용해주세요라는 메세지와 취소, 로그인 버튼이 담긴, 위와 같은 경고 창을 Modal Window라고 합니다.
주로 추가적인 절차나 추가적인 확인이 필요할 때, 오류가 발생했을 때 나타납니다.
Modal창의 특징은 Modal 창이 나타났을 때, 이 창을 없애기 전까지는 웹페이지의 다른 부분을 클릭해도 동작하지 않는다는 것입니다.
마치 프로그래밍 언어에서 while (1)의 무한 루프에 빠진 것처럼요.
Modal창에서 요구하는 조건을 수행해야 하거나 사용자가 해당 경고를 확인해야 하는 경우에 띄워주기 때문에 다른 일을 모두 멈추고 Modal에 집중하도록 최우선 순위를 주는 것 같습니다.
잘 보면 Modal 창 뒤의 웹페이지가 약간 어두운 것을 볼 수 있는데,
Modal 창이 최상단에 위치하고 있고, 그림자에 가려진 웹페이지는 기능이 중단 되었다는 것을 사용자에게 시각적으로, 매우 직관적으로 전달하게 됩니다.
남자 화장실 소변기에 파리 그림을 넣으면 무의식적으로 파리를 맞추려고 하는 것처럼, Modal 창의 이러한 시각적 효과도 무의식적으로 사용자에게 이러한 정보들을 전달하는 것 같습니다.
React.Portal은 이러한 Modal 기능을 구현할 때 상당히 유용합니다.
.modal {
z-index: 9999;
}
Modal은 시각적으로 모든 HTML Element의 최상단에 위치하도록 만들어야 하는데 z-index 속성을 이용한 방법도 가능하지만, 이러한 경우 시각적으로는 같지만 HTML 구조적으로는 다른 모양이 됩니다.

Chrome Developer Tools를 통해 살펴보면 Modal이 다른 Element들의 아래에 위치한 것을 알 수 있습니다.
분명 시각적으로는 ErrorModal이 최상단에 있는데,
구조적으로는 아래에 있어 서로 매치가 되지 않습니다.
이를 매치시키기 위해 Portal을 사용합니다.
이전에 React의 최상단 컴포넌트는 반드시 하나이며, 해당 컴포넌트를 관례상 App이라고 명명한다고 설명 하였습니다.
React는 결국 JS이기 때문에 최종적으로 이 최상단 컴포넌트를 HTML에 삽입하기 위해서는 삽입할 위치를 찾아서 렌더링 해야 합니다.
<!-- index.html -->
<div id="root"></div>
// root라는 id를 가진 Element를 찾아서 그 아래 <App /> 렌더링
ReactDOM.render(<App/>, document.getElementById("root"));
위와 같은 방식으로 최상위 컴포넌트를 렌더링 하는데, Portal도 비슷한 방식으로 동작합니다.
사용할 컴포넌트를 지정하고, 렌더링 할 위치를 document.getElementById()로 찾아서 쌍으로 전달하면 됩니다.
아래에서 코드를 통해 자세히 살펴보겠습니다.
React.createPortal()로 Modal 구현하기<!-- index.html -->
<div id="backdrop-root"></div>
<div id="overlay-root"></div>
<div id="root"></div>
index.html 파일, root Element 위에 backdrop-root, overlay-root라는 id를 가진 Element 2개를 만들어 줍니다.
2개로 나누는 이유는 위에서 보았듯이 Modal이 모든 Element의 최상단에 위치하며 그 아래의 Element들이 비활성화 되었다는 정보를 시각적으로 전달하기 위해 Backdrop이라 부르는 어두운 패널을 하나 깔아주고 그 위에 경고창인 Modal을 띄워주는데, 이 Backdrop과 Modal을 분리하기 위해서입니다.
이 둘을 분리함으로 재사용성 을 높일 수 있기 때문인 것 같습니다.
하나의 웹사이트에 ErrorModal, LogInModal, PaymentModal 등 여러 개의 Modal창이 있을 것이고, 매번 각 Modal을 만들 때마다 Backdrop을 따로 구현하는 것이 아니라 Backdrop은 #backdrop-root Element 아래에 렌더링하고, Modal은 #overlay-root Element 아래에 렌더링 하면 하나의 Backdrop 컴포넌트만 만들면 됩니다.
const ErrorModal = props => {
return (
<>
<!-- 1. Backdrop Component -->
{ReactDOM.createPortal(
<!-- 1. 렌더링 할 컴포넌트 -->
<Backdrop onClick={props.onConfirm} />,
<!-- 2. 렌더링 될 위치 (id가 "backdrop-root"인 Element 아래) -->
document.getElementById("backdrop-root")
)}
<!-- 2. Modal Component -->
{ReactDOM.createPortal(
<ModalOverlay
title={props.title}
message={props.message}
onConfirm={props.onConfirm}
/>,
document.getElementById("overlay-root"),
)}
</>
);
};
사용법은 ReactDOM.render()과 유사하기 때문에 이해하는데 어렵지 않을 것입니다.
Modal로 사용할 컴포넌트 내부에서 ReactDOM.createPortal()로 렌더링 할 컴포넌트를 첫번째 인자로, 렌더링 될 위치를 index.html 내에 있는 Element를 찾아서 두번째 인자로 전달합니다.

이제 Chrome Dev Tools로 Modal의 위치를 확인해보면, #root Element와 같은 레벨인 별개의 #overlay-root 아래에 위치한 것을 확인할 수 있습니다.
Element로 변환된 Modal을 찾기도 훨씬 수월할 것이고 시각적인 위치도 일치하기 때문에 코드를 이해도 더 쉬워졌습니다.

React 공식 문서를 보면 Ref를 데이터를 저장하고 싶지만 해당 데이터를 변경할 때 re-render는 하고 싶지 않은 경우 사용할 수 있다고 설명하고 있습니다.
import { useRef } from "react";
const count = useRef(0);
앞서 위와 같이 일반적인 JS 변수들은 변경해도 React가 re-render을 해주지 않는다고 설명했습니다.
그러나 일반적인 JS 변수와 useRef()로 만든 데이터는 차이점이 있습니다.
일반적인 JS 변수, useState(), useRef()로 구현한 Counter 컴포넌트를 각각 살펴보면서 차이를 알아보도록 하겠습니다.
Counter 컴포넌트 만들기const Counter = () => {
// 일반적인 JS 변수, count
let count = 0;
// count를 1 증가시킨다.
const handleAdd = () => {
count += 1;
}
return (
<div>
<!-- button을 누를 때마다 count를 1 증가시킨다. -->
<button onClick={handleAdd}>Add</button>
<h1>{count}</h1>
</div>
);
}
https://codesandbox.io/s/sweet-keller-nyt9pl?file=/src/App.js
위 링크로 들어가서 확인해보시면 버튼을 눌러도 count가 증가하는 것이 화면에 re-render 되지 않는 것을 볼 수 있습니다.
분명히 버튼을 누를 때마다 count의 값은 1씩 증가합니다.
그러나 일반 변수를 변경해도 React에 re-render하라는 명령은 전달할 수 없다는 것을 저번 시간에 함께 알아보았습니다.
그래서 아래와 같이 useState()로부터 받은 setState() 함수를 사용해서 데이터 변경과 함께 re-render를 할 수 있었습니다.
useState()로 Counter 컴포넌트 만들기import { useState } from "react";
const Counter = () => {
// useState로부터 state, setState() 반환 받아서 사용
const [count, setCount] = useState(0);
const handleAdd = () => {
// 이전 count에 1을 더한 값을 새로운 count로 re-render 요청
setCount(count + 1);
}
return (
<div>
<button onClick={handleAdd}>Add</button>
<h1>{count}</h1>
</div>
);
}
https://codesandbox.io/s/hungry-yonath-jxmwi6?file=/src/App.js
useRef()로 Counter 컴포넌트 만들기import { useRef } from "react";
const Counter = () => {
// useRef의 초기값으로 0 설정
const countRef = useRef(0);
const handleAdd = () => {
countRef.current += 1;
}
return (
<div>
<button onClick={handleAdd}>Add</button>
<h1>{countRef.current}</h1>
</div>
);
}
useRef() 함수를 초기화 할 때 default 값을 인자로 주며, 객체 하나를 반환합니다.
반환 받은 객체 안에 current라는 속성이 존재하는데, 이것이 우리가 처음에 넣었던 초기값을 담고 있는 변수입니다.
즉, countRef.current는 0입니다.
값을 변경하고 싶으면 일반 JS 변수처럼 사용하면 됩니다. countRef.current += 1 처럼요.
https://codesandbox.io/s/wild-silence-fn9clo?file=/src/App.js
하지만 위 링크에 접속해 버튼을 클릭해서 handleAdd()를 실행해 countRef.current에 1을 더해도 아무런 변화가 없을 것입니다.
사실 생각해보면 당연한 이야기입니다.
useState()로부터 반환받은 setState() 함수를 통해 re-render를 요청한 것도 아니고,
앞서 React에서 객체 내부의 속성을 업데이트해도 React는 객체의 주소값을 비교하기 때문에, 아래와 같이 전혀 다른 메모리 공간에 위치한, 새로운 객체를 대입하지 않는 한 같은 주소를 비교하게 되어 React가 변화를 인지하지 못하여 만약 setState()를 사용했더라도 re-render가 일어나지 않습니다.
re-render를 요청하기 위한 2가지 조건인
setState()로 re-render를 요청할 것,
주소값이 다른 객체를 다음 State로 설정하여 React에게 상태가 변했음을 알릴 것
그 무엇도 충족하고 있지 않습니다.
그럼 대체 useRef()를 사용하는 이유가 무엇일까요.
우선 useRef()로 만든 변수와 일반 JS 변수의 차이부터 살펴보겠습니다.
https://codesandbox.io/s/musing-dan-hngykw?file=/src/App.js
import { useRef } from "react";
const Counter = () => {
const countRef = useRef(0);
let count = 0;
const handleAdd = () => {
countRef.current += 1;
count += 1;
};
return (
<div>
<button onClick={handleAdd}>Add</button>
<h1>countRef: {countRef.current}</h1>
<h1>count: {count}</h1>
</div>
);
}
일반 JS 변수, Ref 변수 둘 다 re-render를 일으키지 않기 때문에
당연히 버튼을 아무리 눌러도 화면은 업데이트 되지 않습니다.
그러나 아래와 같이 setState() 함수를 하나 만들어서 re-render를 요청하게 된다면...
(나머지 코드는 변동 없음)
https://codesandbox.io/s/lucid-ace-7ype3x?file=/src/App.js
import { useRef, useState } from "react";
const Counter = () => {
// re-render를 요청하기 위해
// 의미 없는 setState 변수를 하나 만듬
const [something, setSomething] = useState(0);
const countRef = useRef(0);
let count = 0;
const handleAdd = () => {
countRef.current += 1;
count += 1;
};
// 아무 값이나 setState 하여
// re-render 요청
const reRender = () => {
setSomething(something + 1);
};
return (
<div>
<button onClick={handleAdd}>Add</button>
<button onClick={reRender}>re-render</button>
<h1>countRef: {countRef.current}</h1>
<h1>count: {count}</h1>
</div>
);
}
Add 버튼을 누르면 화면에 전혀 변화가 없습니다.
그러나 reRender 버튼을 누르면 count는 변하지 않는데, countRef만 증가하는 것을 볼 수 있습니다.
이유를 간단하게 생각해보면, setState() 함수로 re-render를 일으키면 변화가 일어난 컴포넌트들은 폐기되고, 완전히 새로 재생성됩니다.
다
따라서 그 안에 위치한 변수들도 전부 폐기되고 재생성 되는 것입니다.
let count = 0;이라는 변수가 컴포넌트 안에 있는데 count += 1;로 1을 더하고 re-render 한다고 해도 count는 증가하지 않고 재정의 되어 0이 됩니다.
그러나 useRef()로 만든 변수는 컴포넌트가 재생성 되더라도 내부의 값이 유지되는 것을 확인했습니다.
이것이 useRef()와 일반 JS 변수와의 결정적인 차이입니다.
Ref는 객체, 즉 포인터 타입의 변수입니다.
내부적으로 React에서 관리하는 특정 메모리 주소를 가리키고 있습니다.
만약 그 주소가 0번지라고 하고, 0번지에 아래의 ref를 저장했다고 생각해봅시다.
const ref = useRef(0); // ref는 0번지를 가리킴
// 0번지에 저장된 객체
// {
// current: 0,
// }
이 상태에서 ref.current += 1;로 값을 증가시켰다고 해봅시다.
// 0번지에 저장된 객체
// {
// current: 1,
// }
그 상태에서 re-render를 하게 된다면,
// ref는 새로 재생성되었지만, 여전히 0번지를 가리킴
const ref = useRef(0);
// 0번지에 저장된 객체
// {
// current: 1,
// }
ref는 분명 새로운 객체로, 다른 메모리 공간에 재생성될 것입니다.
하지만 가리키는 곳은 여전히 같습니다.
만약 ref에 객체가 아닌 1과 같은 값을 담았다면,
그래서 re-render를 하더라도 이전값을 갖고 있을 수 있는 것입니다.
여기서 한가지 의문이 생길 수 있습니다.
전역 변수를 사용해도 re-render에 영향을 받지 않을테니, 같은 결과가 나오는 것이 아닌가
https://codesandbox.io/s/busy-stonebraker-lw5eh3?file=/src/App.js
확인 해보면 useRef()와 결과가 같은 것을 알 수 있습니다.
const ref = useRef(0);
React의 공식 문서에서는 ref 변수를 설명할 때 const로 정의하고 있습니다.
const LogIn = () => {
const inputRef = useRef();
return <input type="text" ref={inputRef} />;
}
위와 같이 useRef() 함수로부터 새로운 Ref를 반환 받아서 input 등의 Element에 등록하여 사용할 수 있습니다.
React에는 Controlled, Uncontrolled Component가 존재하는데, Ref가 등록된 컴포넌트가 바로 Uncontrolled Component입니다.
이 둘의 차이를 알아보기 위해 useState()로 input 값을 관리하는 예제를 다시 살펴봅시다.
import { useState } from "react";
const LogIn = () => {
const [name, setName] = useState("");
const handleChangeName = (event) => {
// 타이핑 할 때마다 event.target.value로부터
// 새로운 값을 받아와 setName을 호출하며
// 컴포넌트는 새롭게 받아온 값으로 re-render 됩니다.
setName(event.target.value);
}
return <input type="text" value={name} />;
}
https://codesandbox.io/s/magical-chandrasekhar-g1z39j?file=/src/App.js
DOM을 직접 조작
useState(Controlled)
State Change -> React가 Diff 확인 -> re-render
useRef(Uncontrolled)
Access to DOM -> Get Value when you Access to DOM
inputRef.current.value = '';
React에 의해 조작되는 것이 아닌 DOM API를 사용해서 value를 ''로 설정
Evaluate & Render JSX
Manage State & Props
React to (User) Events & Input
Re-evaluate Component upon State & Prop Changes
App에서 일어나는 SetState 이외의 다른 모든 것
These tasks must happend outside of the normal component evaluation and render cycle. since they might block/delay rendering
Send Http Req
Store Data in Browser Storage
Set & Manage Timers
만약 어떤 Component가 re-render을 할 때마다 http req를 보내는 함수가 실행한다면, res를 받으면 re-render -> req를 다시 보내고 -> res -> re-render의 루프가 무한히 반복될 것
useEffect()useEffect(() => { ... }, [dependencies]);
() => { ... }
A function that should be executed AFTER every component evaluation IF the specificed dependencies changed
[dependencies]
Dependencies of this effect the function only runs if the dependencies changed
useEffect(() => // fn()
);
// fn()
의존성이 없는 경우 fn을 그냥 Component 안에 작성한 것과 같다.
그래서 컴포넌트가 re-render 될 때 마다 re-run
useEffect(() => {
setFormIsValid(
enteredEmail.includes('@') && enteredPassword.trim().length > 6
);
// 셋 중 하나라도 변경되면 useEffect 콜백 fn rerun
}, [setFormIsValid, enteredEmail, enteredPassword]);
// setFormIsValid는 함수 포인터
// 이는 생략 가능
//
// setState fn들은 기본적으로
// React에 의해 re-render가 일어나도 렌더링 사이클에 절대 변경되지 않는 것이 보장되기 때문
CLEANUP FN이 실행되는 시점 1.: useEffect()가 다음 번에 실행될 때
즉, 이전 useEffect()가 return한 CLEANUP FN이 다음번 useEffect()가 실행되기 직전에 실행됨 -> 그 후 useEffect() 로직 실행
CLEANUP FN이 실행되는 시점 2: useEffect()가 선언된 컴포넌트가 DOM에서 UNMOUNT 되기 직전에.=컴포넌트가 재사용 될 때마다
첫 번째로 useEffect() 사이클에는 실행되지 않음.
그 후 UNMOUNT 전까지 계속 useEffect() 실행 직전에 실행
useEffect(() => {
return () => {};
},);
let EXECUTED = 0;
let CLEANUP = 0;
useEffect(() => {
const timerID = setTimeout(() => {
console.log(`EXECUTED: ${EXECUTED}`);
});
return () => {
console.log(`CLEANUP: ${CLEANUP}`);
clearTimeout(timerID);
};
},);
useReducer()useState()와 마찬가지로 상태 관리를 할 때 사용하지만, 좀 더 복잡하고 여러 개의 상태를 관리해야 하거나 State 간의 의존성이 있는 경우 사용합니다.
서로 다른 State 간에 의존성이 있는 경우
const [state1, setState1] = useState(0);
const [state2, setState2] = useState(0);
const handleState1 = () => {
// state1은 state2에 의존하고 있다.
setState1(state2 + 1);
}
위와 같이 state1의 값을 변경할 때, state2의 값에 의존한다면 대부분의 경우에는 잘 작동하겠지만, 제대로 동작하지 않는 경우가 종종 생긴다.
예를 들어 setState2() -> setState1() 순으로 실행했는데 state1이 먼저 바뀌고 그 후에 state2가 바뀐다면, state1은 잘못된 값에 의존하게 된다.
const [state1, setState1] = useState(0);
const [state2, setState2] = useState(1);
setState2(10000);
setState1(state2 * 2);
// 순차적으로 실행한다면 state2 = 10000, state1 = 20000이 되겠지만,
// 만약 예상과 다르게 setState1() -> setState2() 순으로 실행된다면, state2 = 10000, state1 = 2가 된다.
// 실행 순서에 따라 전혀 다른 값을 갖게 될 수도 있다는 것이다.
만약 금융 등의 중요한 정보를 이런 식으로 덮어쓰게 되면 문제가 생길 수 있다.
이러한 경우 useState()에 state1, state2를 배열, 객체 형태로 묶어서 관리하거나 혹은 useReducer()를 사용하는 방법이 있다.
(useState()는 간단하지만 error-prone, 오류를 유발하기 쉽습니다.)
const [state, dispatchState] = useReducer();