10. 리액트 17과 18의 변경 사항 살펴보기
10.1 리액트 17 버전 살펴보기
10.1.1 리액트의 점진적인 업그레이드
- 리액트 17버전 부터는 하나의 어플리케이션에 2개 이상의 버전이 존재가 가능하도록 업데이트
- 리액트 17 버전에서 16 버전을 lazy 하게 불러오고, 리액트 16을 위한 별도의 루트 요소를 만들어주면 문제 없이 구동 된다
10.1.2 이벤트 위임 방식의 변경
- 기존의 16 버전의 리액트는 이벤트를 document 레벨에서 관리하며 필요한 이벤트를 각각의 컴포넌트에 위임하는 방식으로 작동
- 하지만, 이렇게 할 경우 16과 17 버전이 혼재될 경우 하나의 document 에서 다른 버전의 이벤트 핸들러가 존재하게 되므로 문제가 발생할 가능성이 존재
- 이를 해결하고, 점진적인 업그레이드가 가능하도록 17 버전에서는 이벤트 위임을 document 레벨이 아닌 컴포넌트의 최상위 루트에서 처리 하도록 변경 -> 따라서 16 버전은 16 버전의 컴포넌트 루트에서 이벤트를 위임 / 17 버전은 17 버전의 컴포넌트 루트에서 이벤트를 위임하게 되므로 문제 해결이 가능하다
- 또한, JQeury 등의 타 라이브러리와의 충돌도 예방한다
import React, { MouseEvent, useEffect } from "react";
import ReactDOM from "react-dom";
export default function App() {
useEffect(() => {
document.addEventListener("click", (e) => {
console.log("이벤트가 document까지 올라옴");
});
}, []);
function 안녕하세요(e: MouseEvent<HTMLButtonElement>) {
e.stopPropagation();
alert("안녕하세요!");
}
return <button onClick={안녕하세요}>리액트 버튼</button>;
}
ReactDOM.render(<App />, document.getElementById("root"));
- 위의 코드는 리액트 16과 17에서 다르게 동작
- 16 에서는 모든 이벤트가 document 에 달려 있기 때문에 e.stopPropagation() 이 의미가 없으므로, 이벤트가 작동
- 17 에서는 이벤트가 컴포넌트 루트에 달려 있으므로 e.stopPropagation() 가 동작하여, 이벤트가 작동하지 않는다
10.1.3 import React from 'react' 가 더 이상 필요 없다 : 새로운 JSX transform
- 17 버전 부터는 바벨과 협력하여 import React from 'react' 없이도 JSX 를 해석이 가능하여 에러가 발생하지 않는다
- 불필요한 import 를 삭제해 번들링 크기를 줄이고, 컴포넌트 작성을 더 간결하게 해준다
10.1.4 그 밖의 주요 변경 사항
이벤트 풀링 제거
- 리액트 16버전 까지의 이벤트는 이벤트 풀링이라는 기능을 이용하여, 기본 이벤트를 한번 더 감싸서 처리한다.
- 여러 이벤트가 모두 래핑된 이벤트 풀을 만들어 결과적으로 이벤트를 필요에 따라 재사용이 가능해 보이는 장점이 있지만, 이벤트를 받아오고 이벤트가 종료 되면 이벤트를 초기화 하기 위해 null 로 지정하는 과정에서 에러가 발생한다
- 위의 코드는 handleChange 사용 되고 나서 null 로 초기화가 되는데, 그 후에 e 객체에 접근을 하려고 하기 때문에 에러가 발생
- e.persist() 같은 명령어로 해결을 해줘야 했음
export default function App() {
const [value, setValue] = useState("");
function handleChange(e: ChangeEvent<HTMLInputElement>) {
// null 로 초기화를 막아 에러를 해결
e.persist();
setValue(() => {
return e.target.value;
});
}
return <input onChange={handleChange} value={value} />;
}
- 17 부터는 이벤트 풀링이 사라져서 문제 해결 + 모든 브라우저의 이벤트 처리 능력이 좋아져서 필요가 없어짐
useEffect 클린업 함수의 비동기 실행
- useEffect 의 클린업은 16 까지는 동기적으로 처리, 따라서 클린업 동작 전까지는 다른 작업이 방해되는 문제가 발생
- 17 부터는 화면이 완전히 업데이트 된 이후 비동기적으로 클린업이 실행되어 성능이 향상
컴포넌트의 undefined 반환에 대한 일관적인 처리
- 16 에서는 실수로 컴포넌트가 undefined 를 반환하는 문제를 막기 위해, 에러를 발생
- 대신 forwardRef 나 memo 에서는 에러가 발생하지 않던 문제를 17 부터 픽스
10.2 리액트 18 버전 살펴보기
10.2.1 새로 추가된 훅 살펴보기
useId
- 컴포넌트별로 유니크한 값을 생성하는 훅
export function UniqueComponent() {
return <div>{Math.random()}</div>;
}
- 특히 SSR 의 경우, 컴포넌트 렌더링 시의 random 값과 클라이언트가 서버에서 결과물을 받아서 다시 random 을 돌렸을 때(하이드레이션) 값이 다른 문제가 발생.
- 따라서 17 까지는 해당 부분을 처리하기 어려웠다
- 18 에 추가된 useId 를 사용하면 이와 같은 문제를 해결할 수 있다
- useId 로 생성된 값은 서로 다른 인스턴스(선언)에 따라 유니크한 랜덤 값을 만들어 내며, SSR 에서도 동일하게 작동된다
useTansition
- UI 변경을 가로막지 않고 상태를 업데이트하는 리액트 훅
- 상태 업데이트를 긴급하지 않은 것으로 간주하여, 무거운 렌더링 작업을 미루어 더 나은 사용자 경험 제공이 가능하다
- useTransition 은 isPending 과 startTranstion 이 담긴 배열을 반환, isPending 은 boolean 값이고 startTranstion 급하지 않은 상태 업데이트 값의 배열
import { useState, useTransition } from "react";
// ...
export default function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState<Tab>("about");
function selectTab(nextTab: Tab) {
startTransition(() => {
setTab(nextTab);
});
}
return (
<>
{/* ... */}
{isPending ? (
"로딩 중"
) : (
<>
{tab === "about" && <About />}
{tab === "posts" && <Posts />}
{tab === "contact" && <Contact />}
</>
)}
</>
);
}
- 위의 코드에서
<About />
,<Posts />
,<Contact />
등이 로딩이 오래 걸리는 컴포넌트였다면 tab 상태 값에 따라 서로 다른 탭을 불러올 때, 기존 탭의 렌더링이 완료되기 전 까지는 새로운 탭의 랜더링이 동기적으로 처리되어 느린 문제점이 발생 - 하지만 위와 같이 useTransition 을 사용하면, 탭이 변경되면 기존 랜더링을 후순위로 미루고 지금 필요한 컴포넌트 랜더링을 시작하므로 문제를 해결할 수 있다
- 단, useTransition 는 상태 업데이트에만 사용이 가능하며 값에 대해서 사용하고 싶으면 useDefferdValue 를 사용
- startTranstion 내부는 상태 업데이트 함수만 넘길 수 있다. 그리고 또한 동기 함수만 넘겨야 한다.
- setTimeOut 같은 비동기 함수를 넘기면 정상적으로 작동하지 않는다
useDefferedValue
- 리랜더링이 급하지 않는 부분을 지연시키는 훅이며, 특정시간 동안 발생하는 이벤트를 하나로 인식해 한 번만 실행하게 해주는 디바운스와 비슷하나 장점이 몇가지 존재
- useTransition 과는 달리 함수를 넘기는 것이 아니라 값으로만 처리가 가능하다. 상황에 맞는 것을 사용하면 된다
export default function Input() {
const [text, setText] = useState("");
const deferredText = useDeferredValue(text);
const list = useMemo(() => {
const arr = Array.from({ length: deferredText.length }).map(
(_) => deferredText
);
return (
<ul>
{arr.map((str, index) => (
<li key={index}>{str}</li>
))}
</ul>
);
}, [deferredText]);
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setText(e.target.value);
}
return (
<>
<input value={text} onChange={handleChange} />
{list}
</>
);
}
- text 는 바로바로 state 로서 업데이트 하지만, 시간이 걸리는 list 는 업데이트 기준을 defferdText 로 설정하여 후순위로 렌더링하여 더 나은 사용자 경험 제공
useSyncExternalStore
- 17 까지 존재하던 useSubscription 구현을 18 대체한 훅
- 리액트 18에서 useTranstion, useDefferdValue 와 같은 랜더링을 후순위로 처리하는 최적화가 가능해 지면서 동시성 이슈가 발생
- 즉, 렌더링이 후순위로 밀리면서 같은 값을 바라보지만 컴포넌트의 렌더링 결과물이 달라지는 tearing 이슈가 발생
- 기존 리액트는 렌더링이 동기적으로 한번에 발생해서 위와 같은 이슈가 없었지만, 리액트 18 부터는 발생 가능성이 존재
- 위와 같은 문제를 해결하기 위한 훅이 useSyncExternalStore
import { useSyncExternalStore } from "react";
// useSyncExternalStore(
// subscribe: (callback) => Unsubscribe
// getSnapshot: () => State
// ) => State
- subscribe : 콜백 함수를 받아 스토어에 등록하는 용도, 스토어의 값이 변경되면 해당 콜백이 호출되어 해당 컴포넌트를 리랜더링
- 두번째 인수 : 스토어에 데이터를 반환하는 함수. 스토어 변경이 없다면 동일한 값을 반환해야만 한다
- 세번째 인수 : SSR 에서만 사용되는 값으로, 내부 리액트 하이드레이션 도중에만 사용
import { useSyncExternalStore } from "react";
function subscribe(callback: (this: Window, ev: UIEvent) => void) {
window.addEventListener("resize", callback);
return () => {
window.removeEventListener("resize", callback);
};
}
function useWindowWidth() {
return useSyncExternalStore(
subscribe,
() => window.innerWidth,
() => 0 // 서버 사이드 렌더링 시 제공되는 기본값
);
}
export default function UseSyncExternalStore() {
const windowSize = useWindowWidth();
return <>{windowSize}</>;
}
- useSyncExternalStore 를 사용하여 innerWidth 를 확인하고, innerWidth 가 변경 되면 리렌더링을 발생시키는 코드
- useTransition 과 혼합하여 사용하는 경우, 렌더링이 후순위로 밀리게 되므로 기존의 useEffect 를 사용하여 window.innerWidth 값을 가져오는 경우는 후순위로 밀리기 전의 값을 가져오기 때문에 현재의 정확한 값을 가져오지 못하고 초깃값을 가져오지만 useSyncExternalStore 를 사용하면 정확하게 값을 가져온다.
- 상태 관리 라이브러리 등을 사용하여 상태 값을 외부에서 관리하고 있으면서, useTransition 등을 사용하여 렌더링 최적화를 하는 훅을 사용하면 useSyncExternalStore 를 이용하여 렌더링을 처리해여 tearing 현상에서 자유로울 수 있다
useInsertionEffect
- CSS-in-js 라이브러리를 위한 훅으로, useEffect 와 매우 유사하게 작동한다
- useInsertionEffect 는 DOM 이 실제로 변경되기 전에 동기적으로 실행되어, 레이아웃을 계산하기 전에 실행되어 자연스러운 스타일 삽입이 가능해 진다
import { useEffect, useInsertionEffect, useLayoutEffect } from "react";
export default function useEffectSeries() {
useEffect(() => {
console.log("useEffect!"); // 3
});
useLayoutEffect(() => {
console.log("useLayoutEffect!"); // 2
});
useInsertionEffect(() => {
console.log("useInsertionEffect!"); // 1
});
return <></>;
}
- 순서는 useInsertionEffect -> useEffectLayout -> useEffect 순으로 일어난다
- DOM 변경이 일어나기 전에 스타일을 입혀야 DOM 에 따른 Layout 재계산이 필요 없으므로, 효율성에서 차이를 보인다
10.2.2. react-dom/client
- 클라이언트에서 리액트 트리를 만들 때 사용하는 API 변경으로 리액트 18로 이전 버전을 업그레이드 할 경우 index.jsx 또는 tsx 파일을 변경
createRoot
- 기존의 render 메서드를 대체하는 메서드
// before
import ReactDOM from "react-dom";
import App from "App";
const container = document.getElementById("root");
ReactDOM.render(<App />, container);
// after
import ReactDOM from "react-dom";
import App from "App";
const container = document.getElementById("root");
const root = ReactDOM.createRoot(container);
root.render(<App />);
hydrateRoot
- SSR 에서 하이드레이션을 하기 위한 메서드
// before
import ReactDOM from "react-dom";
import App from "App";
const container = document.getElementById("root");
ReactDOM.hydrate(<App />, container);
// after
import ReactDOM from "react-dom";
import App from "App";
const container = document.getElementById("root");
const root = ReactDOM.hydrateRoot(container, <App />);
10.2.3 react-dom/server
renderToPipeableStream
- 리액트 컴포넌트를 HTML 렌더링하는 메서드
- renderToPipeableStream 을 활용하면 무거운 작업의 경우 Suspense 를 이용하여 필요한 부분부터 렌더링하고, 느린 부분은 이후에 렌더링이 가능하도록 설정이 가능
- 기존 renderToNodeStream 은 렌더링이 반드시 순서대로 일어나서 무거운 작업이 있으면 먼저 보여줘야 할 부분도 지연되는 문제가 발생
renderToReadableStream
- renderToPipeableStream 는 Node 환경 / renderToReadableStream 는 클라우드플레어나 Deno 같은 웹 스트림 환경
10.2.4 자동 배치
- 여러 상태 업데이트를 하나의 렌더링으로 묶어서 성능을 향상시키는 방법
- 기존에는 이벤트 핸들러 내부에서만 제공하던 기능이었지만, 18에 이르러서는 비동기 이벤트에서도 자동 배치를 지원한다
- 이러한 자동 배치를 리액트 18에서도 사용하지를 원치 않을 경우, flushSync 를 사용
10.2.5 더욱 엄격해진 엄격 모드
리액트의 엄격 모드
더 이상 안전하지 않은 특정 생명주기를 사용하는 컴포넌트에 대한 경고
- componentWillMount, componentWillReceiveProps, componentWillUpdate 를 사용하면 에러 발생
문자열 ref 사용 금지
- 문자열로 특정 node 에 접근하는 것 금지
- 실제로 어떤 ref 에서 참조하는지 파악하기 어렵다, 문자열과 객체 접근 혼용 문제, 성능 이슈 문제
class UnsafeClassComponent extends Component {
componentDidMount() {
// 요렇게 편법으로 접근하는 것 막음
console.log(this.refs.myInput);
}
render() {
return (
<div>
<input type="text" ref="myInput" />
</div>
);
}
}
findDOMNode 에 대한 경고 출력
- 컴포넌트 인스턴스에서 실제 DOM 요소 참조 금지
- 특정 부모가 특정 자식만 렌더링이 가능, 이는 리액트가 추구하는 트리 추상화 구조에 위배
class UnsafeClassComponent extends Component {
componentDidMount() {
const node = ReactDOM.findDOMNode(this);
if (node) {
(node as HTMLDivElement).style.color = "red";
}
}
render() {
return <div>UnsafeClassComponent</div>;
}
}
구 Context API 사용 시 발생하는 경고
- childContextTypes, getChildContext 사용 시 에러
예상치 못한 부작용 검사
- StrictMode 사용 시, 부작용 검사를 위해 2번씩 실행
- 클래스 컴포넌트의 constructor, render, shouldComponentUpdate, getDerivedStateFromProps, setState 의 첫번째 인수
- 함수 컴포넌트의 body, useState / useMemo / useReducer 에 전달되는 함수
리액트 18에 추가된 엄격 모드
- 컴포넌트의 마우튼가 해제된 상태에서도 컴포넌트의 내부 상태값을 유지할 수 있는 기능을 제공하기 위해, 엄격 모드에는 컴포넌트가 최초 마운트 시 바로 마운트를 전부 해제하고 이전의 상태를 복원하는 기능이 추가
- 이러한 엄격 모드에서의 효율성을 위해 useEffect 사용시 클린업 함수를 적절히 사용 하는 것이 중요하다
10.2.6 Suspense 기능 강화
- 컴포넌트를 동적으로 가저올 수 있게 도와주는 기능
- Suspense 는 컴포넌트를 미처 불러오지 못했을 때 보여주는 fallback 과, children 으로 로딩 완료시 보여주는 컴포넌트를 인자로 받는다
// Sample Component
export default function SampleCompnent() {
return <>동적으로 가져오는 컴포넌트</>;
}
// app.tsx
import { Suspense, lazy } from "react";
const DynamicSampleComponent = lazy(() => import("./SampleComponent"));
export default function App() {
return (
<Suspense fallback={<>로딩중</>}>
<DynamicSampleComponent />
</Suspense>
);
}
18 버전에서 해결 된 Suspense 의 문제점
- 컴포넌트가 보이기도 전에 useEffect 가 먼저 작동하는 문제
- SSR 에서는 제대로 작동하지 않는 문제
10.2.7 인터넷 익스플로러 지원 중단에 따른 추가 폴리필 필요
- 리액트에서는 이제 ES6 에서 지원하는 문법(Promise, Symbol, Object.assign)을 당연하게 지원한다는 가정하에 배포
- 익스플로러에 배포시 반드시 폴리필 제공 필요
10.2.8 그 밖의 변경 사항
- 컴포넌트에서 undefined 반환해도 에러 발생 X, 자동으로 null 처리
- Suspense 의 fallback 도 undefined 를 null 로 자동 처리
- renderToNodeStream 지원 중단
10.2.9 정리
- 리액트 18 은 동시성 렌더링을 위해 업데이트가 이루어 졌다
11. Next.js 13 과 리액트 18
11.1 app 디랙터리의 등장
- next 는 모든 페이지가 폴더와 파일로 구분되어 있어 레이아웃 적용 부분에서 문제가 있었다
- 페이지 공통으로 무언가를 집어 넣을 수 있는 곳은 _document 와 _app 이 유일
11.1.1 라우팅
- next 12 이하 버전에서는 /pages/a/b.tsx 와 /pages/a/b/index.tsx 가 동일한 주소로 변환
- next 13 에서는 /app/a/b 로 폴더명 까지만 주소로 변환되며, 파일명은 무시
layout.js
- 페이지의 전체적인 레이아웃을 구성하는 요소로 폴더에 해당 파일이 존재하면, 하위 폴더 및 주소에 모두 영향을 준다
- 해당 내용 적용으로 주소별 공통 UI 뿐 아니라, 웹페이지에 필요한 공통 코드도 삽입할 수 있다
page.js
- 기존 next 의 페이지 개념을 담당
error.js
- 라우팅 영역에서 사용하는 공통 에러 컴포넌트로, 해당 파일을 사용하면 라우팅 별로 서로 다른 에러를 발생 시킬 수 있다
not-found.js
- 404 페이지 전용 파일
loading.js
- Suspense 를 기반으로 로딩 중에 보여줄 파일
route.js
11.2 리액트 서버 컴포넌트
11.2.1 기존 리액트 컴포넌트와 서버 사이드 렌더링의 한계
- 기존 SSR 의 방식, 서버에서 DOM 을 불러오고 클라이언트는 만들어진 DOM 을 기준으로 하이드레이션을 진행하여 이벤트 핸들러를 DOM 에 추가하고 요청에 따라 응답하는 방식은 한계점을 지닌다
- 자바스크립트 번들크기가 0인 컴포넌트 생성이 불가능. 타 라이브러리 사용시 서버와 클라이언트 모두 해당 라이브러리를 로딩해야 하는 단점이 발생
- 백엔드 리소스에 직접적 접근이 불가능
- 자동 코드 분할이 불가능. 코드를 여러 단위로 나누어 필요할 때만 동적으로 로딩하는 기능을 리액트에서는 lazy 로 구현이 가능하지만 SSR 에서는 항상 lazy 로 감싸줘야만 하며 동적 판단에 따라 어떤 컴포넌트를 미리 그릴지 판단이 어렵다
- 연쇄적으로 발생하는 클라이언트와 서버의 요청을 대응하기 어렵다.
- 추상화에 드는 비용이 증가. 템플릿 언어에 비해 다양한 언어적 기능을 제공하지만 이에 따른 비용이 증가
11.2.2 서버 컴포넌트란?
-
서버에서 구동이 가능한 컴포넌트를 말한다. 서버에서 만들어서 빠르게 HTML 을 내려주는 SSR 과는 다른 개념이다
-
서버 컴포넌트 문제점
- 요청되면 딱 한번 생성되어 전달 되므로 상태 및 생명주기 활용이 불가능
- 브라우저에서 사용되는 DOM API, window, document 등에 접근이 불가능
-
클라이언트 컴포넌트 문제점
- 서버 컴포넌트를 불러올 수 없다
-
공용 컴포넌트
- 위에 기술한 컴포넌트의 문제점을 동시에 가지는 컴포넌트
-
리액트는 기본적으로 모든 컴포넌트를 공용 컴포넌트로 판단하며 "use client" 라고 작성한 컴포넌트만 클라이언트 컴포넌트로 판단
-
클라이언트 컴포넌트에서 서버 컴포넌트를 import 하면 에러 발생
** [p. 736] 너네 이거 알지? 하면서 부수적인 것만 설명 하는데... 흐름이 너무 구립니다. 리액트 내용 쭉하고 넥스트 같이 나왔으면 이해가 훨 잘되었을거 같은데, 리액트 하다 넥스트하다 리액트 하다 넥스트하다 하다보니 흐름이 영...
11.2.3 서버 사이드 렌더링과 서버 컴포넌트의 차이
11.2.4 서버 컴포넌트는 어떻게 작동하는가?
- 서버에서 스트리밍 형태로 페이지 정보를 보낸다. 따라서, 클라이언트는 줄 단위로 JSON 을 읽어서 컴포넌트 렌더링이 가능하여 되도록 빠르게 사용자에게 결과물을 보여줄 수 있다
- 컴포넌트 별로 번들링이 가능하여, 필요에 따라 특정 컴포넌트를 지연하여 받는 등의 처리가 가능하다
- JSON 으로 직렬화 된 데이터를 받아 빠르게 컴포넌트 트리의 구성을 한다. 즉, SSR 이 단순히 HTML 을 그리는 것에 비해 더 고도화된 작업을 빠르게 수행할 수 있다
- 이 모든 내용은 SSR 의 단점을 극복하기 위해서 적용 된 내용이다
11.3 Next.js 에서의 리액트 서버 컴포넌트
- 13 버전에 들어서 서버 컴포넌트 기능이 적용 되었다
- page.js 와 layout.js 는 반드시 서버 컴포넌트로 적용되기 때문에 제약 사항을 반드시 따라야 한다
- 서버 컴포넌트는 클라이언트 컴포넌트를 children props 로 받는 것만 가능한데, page.js 와 layout.js 는 DOM 트리의 최상단에 위치하므로 서버 컴포넌트의 장점만 가질 수 있다.
- 이를 이용하여 page.js 와 layout.js 는 클라이언트에 부담을 주지 않고 서버에서 빠르게 생성이 가능하며 직렬화된 JSON 을 통해 클라이언트에 전달 되므로 빠르게 최초 페이지와 레이아웃을 사용자에게 제공이 가능해진다
- 이렇게 그려진 페이지와 레이아웃에 클라이언트에서 그린 클라이언트 컴포넌트가 얹어지는 개념이므로, 모든 작업을 클라이언트에서 수행하던 것에 비해 효율성과 사용성 제공이 가능해 진다
** [p. 741] 이런건 설명을 해야하지 않겠니?
11.3.1 새로운 fetch 동비과 getServerSideProps, getStaticProps, getInitialProps 의 삭제
- 서버 컴포넌트의 도입으로 SSR 에 서만 필요한 특수한 기능이 삭제 되었다
- 최근 유행인 SWR 와 React-Query 처럼 특정 fetch 요청을 렌더링이 끝날 때까지 캐싱을 하여 중복된 요청을 방지하는 역할을 한다
11.3.2 정적 렌더링과 동적 렌더링
- Next 13 버전에서는 정적인 라우팅에 대해서는 기본적으로 빌드 타임에 미리 렌더링을 생성하여 캐싱해서 제공하는 기능 추가
- cache 옵션으로 설정이 가능
async function fetchData() {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts`,
// no-cache 옵션을 추가했다.
{ cache: "no-cache" }
// Next.js에서 제공하는 옵션을 사용해도 동일하다.
{ next: { revalidate: 0 }}
);
const data = await res.json();
return data;
}
export default async function Page() {
const data: Array<any> = await fetchData();
return (
<ul>
{data.map((item, key) => (
<li key={key}>{item.id}</li>
))}
</ul>
);
}
- 특정 주소에 대한 캐싱의 경우는 generateStaticParams 를 사용하면 된다
11.3.3 캐시와 mutationg, 그리고 revalidating
{ next: { revalidate: 0 }}
와 같은 옵션을 사용해서 시간을 정해두고 해당 시간이 지나면 데이터를 불러와서 페이지를 렌더링 하는 것이 가능- 혹은, 페이지에 revalidate 라는 변수를 선언해서 정의도 가능
// app/page.tsx
export const revalidate = 60;
- 캐시를 리프레시하고 싶다면 router.refresh() 로 가능하다
11.3.4 스트리밍을 활용한 점진적인 페이지 불러오기
- HTML 을 작은 단위로 쪼개서 완성되는 대로 클라이언트로 보내는 스트리밍이 도입
- 폴더 경로에 loading.tsx 를 배치해서 로딩 중에 보여줄 컴포넌트를 예약
<Layout>
<Header />
<SideNav />
<!-- 여기에 로딩이 온다. -->
<Suspense fallback={<Loading />}>
<Page />
</Suspense>
</Layout>
- 혹은 직접 리액트의 Suspense 를 사용도 가능하다
11.4 웹팩의 대항마, 터보팩의 등장(beta)
- 요즘 뜨는 라이브러리인 Rome, SWC, esbuild 의 공통점은 기존 JS 로 제공되던 기능을 Rust 혹은 Go 를 사용하여 제공하여 상대적으로 빠른 속도와 성능을 제공한다
- Next 13 에서서는 웹팩의 후계자를 자처하는 터보팩(Turbopack)이 출시, 웹팩 대비 700배, vite 대비 10배 빠르다고 한다
- 현재는 베타로 개발 모드에서만 제한적으로 사용이 가능
11.5 서버 액션(alpha)
- API 생성 없이 함수 수준에서 직접 서버에 접근에 데이터를 요청하는 기능을 수행 가능
- next.config.js 에서 실험 기능을 활성화 해야만 사용 가능
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true,
},
};
module.exports = nextConfig;
- 파일 상단에 "use server" 지시자 선언 필요
11.5.1 form 의 action
- 클라이언트에서 이벤트를 발생시키지만 실제로 수행은 서버에서 하는 form action 코드
export default function Page() {
async function handleSubmit() {
"use server";
console.log(
"해당 작업은 서버에서 수행합니다. 따라서 CORS 이슈가 없습니다."
);
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "post",
body: JSON.stringify({ title: "foo", body: "bar", userId: 1 }),
headers: { "Content-type": "application/json; charset=UTF-8" },
});
const result = await response.json();
console.log(result);
}
return (
<form action={handleSubmit}>
<button type="submit">form 요청 보내보기</button>{" "}
</form>
);
}
- 이러한 액션은 기존의 PHP 와 크게 다르지 않아 보이지만, 이 모든 기능을 스트리밍 형태로 제공하기 때문에 페이지 새로고침 없이 수행할 수 있는 장점이 존재
11.5.2 input 의 submit 과 image 릐 formAction
11.5.3 startTransition 과의 연동
- startTransition 을 통해 데이터가 갱신 되었을 경우 페이지 단위의 로딩이 아닌 컴포넌트 단위의 로딩으로 처리도 가능하다
11.5.4 server mutation 이 없는 작업
11.5.5 서버 액션 사용 시 주의할 점
- 클라이언트 컴포넌트 내부에서는 정의가 불가능
- 서버에서만 실행될 수 있는 자원은 반드시 파일 단위로 분리해야 한다
** [p.760] 이건 next deep dive 아니냐...... 진짜.... 이 책의 정체성은 무엇인지 의문이 든다
11.6 그 밖의 변화
- 라우트 미들웨어 강화, SEO 기능을 쉽게 작성 가능, 정적으로 내부 링크 분석 기능 등 추가
- 자세히 알아보고 싶으면 공식 문서 봐라....
** [p.760] 설명 실화냐?
11.7 Next.js 13 코드 맛보기
11.7.1 getServerSideProps 와 비슷한 서버 사이드 렌더링 구현해 보기
** [p.760] 이걸 먼저 보여주거나 이전 버전 코드랑 비교하고 차이점에서 들어나는 부분에서 저 위에 내용이 나오는게 맞지 않을까?
** [p.760] 아니... 이 책 사는 사람은 next 도 이미 다 아는걸 기준으로 설명하는거냐? 진짜 이럴 페이지에 리액트 관련 심도 있는거나 좀 다루거나 하지.... 진짜 이도 저도 아니네 갈수록
- 기존에는 서버 데이터를 불러와서 하이드레이션 할 수 있는 방법은 getServerSideProps 를 비롯한 몇 가지 방법으로 제한되어 잇었다
- 리액트 18과 넥스트 13에 들어서는 서버 컴포넌트의 경우 어디서든 서버 관련 코드를 추가할 수 있게 변경
- 서버 컴포넌트에서 fetch 를 수행하고 별다른 cache 를 설정 안해주면 기존의 getServerSideProps 와 유사하게 동작
import { ReactNode } from "react";
import { fetchPostById } from "#services/server";
export default async function Page({
params,
}: {
params: { id: string };
children?: ReactNode;
}) {
// const response = await fetch(
// `https://jsonplaceholder.typicode.com/posts/${id}`,
// options,
// )
// const data = await response.json()
// 와 같다.
const data = await fetchPostById(params.id, { cache: "no-cache" });
return (
<div>
<h1>{data.title}</h1>
<p>{data.body}</p>
</div>
);
}
- 위 컴포넌트의 실행 결과는 기존의 getServerSideProps 와 마찬가지로 미리 렌더링되어 완성된 HTML 이 전달된다
- 그리고 그 후에 JS 관련 번들은 직렬화 된 데이터로 스트리밍 되어 하이드레이션이 진행된다
- 기존의 getServerSideProps 실행 결과를 JSON 으로 받았다면, 이제는 서버 컴포넌트의 렌더링 결과룰 JSON 형태의 직렬화 된 데이터로 스트리밍을 받아 하이드레이션을 진행한다. 따라서, 결과물을 더 빠르게 그릴 수 있게 되었다
11.7.2 getStaticProps 와 비슷한 정적인 페이지 렌더링 구현해 보기
- 서버 컴포넌트의 fetch 에 cache 를 사용하면 구현이 가능하다
// /app/ssg/[id]/page.tsx
import { fetchPostById } from "#services/server";
export async function generateStaticParams() {
return [{ id: "1" }, { id: "2" }, { id: "3" }, { id: "4" }];
}
export default async function Page({ params }: { params: { id: string } }) {
// const response = await fetch(
// `https://jsonplaceholder.typicode.com/posts/${id}`,
// options,
// )
// const data = await response.json() // 와 같다.
const data = await fetchPostById(params.id);
return (
<div>
<h1>{data.title}</h1>
<p>{data.body}</p>
</div>
);
}
- force-cache : 캐시가 존재하면 해당 캐시 값을 반환하고, 존재하지 않으면 서버에서 데이터를 가져온다 (기본값)
- no-store : 캐시를 절대 사용하지 않고, 매번 새롭게 불러온다
- 위의 코드처럼 cache 옵션을 주지 않으면 모든 cache 값을 사용하도록 설정한 것과 같다
** [p.766] 뭔뜻인지 아시는 분? 두 옵션은 서로 상충하는데, 모든 cache 값을 사용한다는게 뭔말이죠?
- 또한 revalidate 값을 설정하여 제한 시간을 설정 할 수 있다
11.7.3 로딩, 스트리밍, 서스펜스
- Suspense 는 loading.tsx 파일과 달리 쪼개서 사용이 가능
// [id]/page.tsx
import { Suspense } from "react";
export default async function Page({ params }: { params: { id: string } }) {
return (
<div className="space-y-8 lg:space-y-14">
<Suspense fallback={<div>유저 목록을 로딩 중입니다.</div>}>
{/* 타입스크립트에서 Promise 컴포넌트에 대해 에러를 내기 때문에 임시 처리 */}
{/* @ts-expect-error Async Server Component */}
<Users />
</Suspense>
<Suspense
fallback={<div>유저 {params.id}의 작성 글을 로딩 중입니다.</div>}
>
{/* @ts-expect-error Async Server Component */}
<PostByUserId userId={params.id} />
</Suspense>
</div>
);
}
export async function Users() {
// Suspense를 보기 위해 강제로 지연시킨다.
await sleep(3 * 1000);
const users = await fetchUsers();
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export async function PostByUserId({ userId }: { userId: string }) {
// Suspense를 보기 위해 강제로 지연시킨다.
await sleep(5 * 1000);
const allPosts = await fetchPosts();
const posts = allPosts.filter((post) => post.userId === parseInt(userId, 10));
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
- 콘솔 도구에서 Node 환경 서버 컴포넌트를 실행하면 Chuck 단위로 파일이 내려오는 것도 확인이 가능하다
11.8 정리 및 주의사항
- create-react-app 시대의 종말이 다가온다 ㄷㄷㄷㄷ
** [p.774] 쉽게 설명하는 재능은 확실히 없으신듯 합니다