이효석
Study

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] 쉽게 설명하는 재능은 확실히 없으신듯 합니다