지난 글에 이어서 리액트 18의 변경점을 알아보자.
리액트 18에서 클라이언트에서 리액트 트리를 만들때 사용되는 API가 변경되었다.
createRoot
기존 react-dom의 render를 대체할 메서드이다. 리액트 18 기능 사용을 위해서는 createRoot와 render를 함께 사용해야 한다.
//before
import ReactDOM fro 'react-dom'
import App from 'App'
const container = document.getElementById('root')
ReactDOM.render(<App/>,container)
//after
import ReactDOM fro 'react-dom'
import App from 'App'
const container = document.getElementById('root')
const root = ReactDOM.createRoot(container)
root.render(<App />)
따라서 리액트 18버전으로 업그레이드를 하려는 경우 해당 부분의 코드를 위와 같이 변경해야 한다.
hydrateRoot
서버사이드 렌더링 애플리케이션에서 하이드레이션을 하기 위한 새로운 메서드이다. React DOM 서버 API와 함께 사용된다.
//before
import ReactDOM fro 'react-dom'
import App from 'App'
const container = document.getElementById('root')
ReactDOM.hydrate(<App/>,container)
//after
import ReactDOM fro 'react-dom'
import App from 'App'
const container = document.getElementById('root')
const root = ReactDOM.hydrateRoot(container,<App/>)
물론 대부분 서버사이드 렌더링은 프레임워크에 의존하고 있기 때문에 해당 코드를 직접 수정할 일은 별로 없을 것 같다.
react-dom/server
클라이언트와 같이 서버쪽에서도 컴포넌트를 생성하는 API에 변경이 있었다.
renderToPipeableStream
리액트 컴포넌트를 HTML로 렌더링하는 메서드이다. 스트림을 지원하는 메서드로 HTML을 점진적으로 렌더링 하고 클라이언트에서 중간에 script를 삽입하는 등의 작업이 가능하다.
기존 renderToNodeStream은 렌더링이 무조건 순차적으로 이뤄지기 때문에 이전 렌더링이 완료되어야지만 렌더링이 끝나는 문제가 있따. 이를 활용하면 해당 문제점을 해결할 수 있다.
renderToReadableStream
renderToPipeableStream이 Node.js환경에서의 렌더링을 위한다면 해당 메서드는 웹 스트림을 기반으로 작동한다.
자동 배치(Automatic Batching)
리액트가 여러 상태 업데이트를 하나의 리렌더링으로 묶어서 성능을 향상시키는 방법이다.
import { Profiler, useCallback, useEffect, useState } from "react";
const sleep = (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
const MyComponent = () => {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const callback = useCallback(
(
id: any,
phase: any,
actualDuration: any,
baseDuration: any,
startTime: any,
commitTime: any
) => {
console.group(phase);
console.table({ id, phase, commitTime });
console.groupEnd();
},
[]
);
useEffect(() => {
console.log("render되었다미");
});
function handleClick() {
sleep(3000).then(() => {
setCount((c) => c + 1);
setFlag((f) => !f);
});
}
return (
<Profiler id="React18" onRender={callback}>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</Profiler>
);
};
/* STYLE */
export default MyComponent;
리액트 18에서는 자동배치 덕분에 리렌더링이 한번만 일어난다.
사실 Sleep과 같은 함수를 호출하지 않으면 굳이 리액트 18버전을 사용하지 않아도 자동 배치가 이뤄진다. 즉, 리액트 18버전에서는 setTimeout과 같은 비동기 이벤트에서도 자동배치가 이뤄지는 패치가 일어난 것을 확인할 수 있다.
이러한 자동배치로 인해 기존 코드에 영향이 미칠것 같다면 flushSync를 사용해볼 수 있다.
function handleClick() {
sleep(3000).then(() => {
flushSync(() => {
setCount((c) => c + 1);
});
flushSync(() => {
setFlag((f) => !f);
});
});
}
더 엄격해진 엄격 모드
리액트의 엄격 모드는 잠재적인 버그를 찾는데 도움이 되는 컴포넌트다. 해당 엄격모드는 개발자 모드에서만 작동한다.
더이상 안전하지 않은 생명주기를 사용하는 컴포넌트에 대한 경고
리액트 클래스형 컴포넌트에서 사용되는 생명주기 메서드 중 일부를 현재 사용할 수 없다. 16.3 버전부터 UNSAFE_로 시작하게 되었다. 해당 메서드를 사용한다면 사용하지 말라는 로그를 띄워준다.
문자열 ref 사용금지
과거 리액트에서는 레거시 문자열 ref라 해서 createRef없이 문자열로 ref를 생성하여 DOM 노드 참조하는 것이 가능했다.
class UnsafeClassComponent extends Component{
componentDidMount(){
console.log(this.refs.myInput)
}
render(){
return (
<div>
<input type="text" ref="myInput"/>
</div>
)
}
}
위처럼 ref에 단순한 문자열을 할당할 수 있다. 이런 방식은 ref값의 충돌, 성능이슈 등등의 문제로 금지되어 엄격모드에서 경고를 출력해주게 되었다.
구 ContextAPI 사용
childContextTypes과 getChildContext를 사용하는 구 리액트 ContextAPI를 사용하면 엄격모드에서 에러를 출력해준다.
SideEffect 검사 ( 부작용 검사 )
- 클래스형 컴포넌트의 constructor , render , getDerivedStateFromProps , shouldComponentUpdate
- 클래스형 컴포넌트의 setState 첫번째 인수
- 함수형 컴포넌트의 body
- useState,useMemo,useReducer에 전달되는 함수
위 내용을 엄격모드에서는 의도적으로 이중으로 호출한다.
함수형 프로그래밍의 원칙에 따라 리액트의 모든 컴포넌트는 순수하다고 가정한다. 따라서 해당 결과가 잘 지켜지고 있는지 의도적으로 두번 실행한다.
콘솔 등을 통해서 해당 컴포넌트가 순수한지 쉽게 파악할 수 있게 해준다.
리액트 18에서 추가된 엄격 모드
향후 리액트에서 컴포넌트가 마운트 해제된 상태에서도 컴포넌트 내부의 값을 유지할 수 있는 기능을 제공할 것이라고 리액트 팀에서 밝혔다. (! 전역상태로 따로 저장을 안해도 되는가?)
이 기능을 향후 지원하기 위해 엄격모드의 개발모드에 새로운 기능을 도입했다.
import {useEffect} from 'react'
let count = 0
export default function App(){
useEffect(() => {
count +=1
console.log('mount App, ${count})
return () => {
console.log('unmount app ${count})
}
})
return <h1>hello</h1>
}
위 코드를 각각 리액트 18,17 에서 실행한 결과는 아래와 같다.
//react 18
// mount App, 1
// unmount app 1
// mount App,2
//react 17
//mount App,1
즉 useEffect가 두번 작동된것처럼 보인다. 이런 방식은 의도된 것으로 보이며 리액트팀은 아래와 같이 입장을 발표했다 하니 한번 봐보자.
https://react.dev/blog/2022/03/08/react-18-upgrade-guide#updates-to-strict-mode
정리
리액트 18의 핵심은 지난 글에서 말했듯이 동시성 렌더링이다. 과거 렌더링은 한번시작하면 멈출 수 없고 무조건 결과를 봐야 끝나는 작업이었지만 리액트 18부터는 렌더링을 일시 중지할수도 있고 아예 중간에 멈추고 새로 시작하게 하는것 또한 가능하다.
리액트팀의 목표는 리액트 컴포넌트가 별다른 노력 없이도 동시성 렌더링이 작동하는것이며 이는 긍정적이다. 물론 이 동시성 렌더링을 지원하기 위해 useStyncExternalStore의 도움이 필수적인데 외부 라이브러리가 해당 부분을 지원하는지는 꼭 살펴보는 것이 좋다.
'FrontEnd > Deep Dive' 카테고리의 다른 글
[React] Deep Dive 모던 리액트(29) Next.js 서버 컴포넌트 (0) | 2024.02.17 |
---|---|
[React] Deep Dive 모던 리액트(28) Next.js13 라우팅 (1) | 2024.02.16 |
[React] Deep Dive 모던 리액트(26) 리액트 18 추가된 훅 (1) | 2024.02.07 |
[React] Deep Dive 모던 리액트(25) 리액트 17 변경점 (2) | 2024.02.06 |
[React] Deep Dive 모던 리액트(24) 리액트 도커라이즈 (1) | 2024.02.06 |