김용현
Study

정리하기 파일

리액트 17 버전 살펴보기

1. 리액트의 점진적인 업그레이드

  • React 16과 호환 가능
    • React 17 애플리케이션 내부에 React 16을 게으르게(lazy) 불러온다. 이 과정에서 React 16을 위한 별도의 루트 요소를 만들고, 여기에 불러온 React 16 모듈을 렌더링하는 구조

2. 이벤트 위임 방식의 변경

  • document -> 리액트 최상단 요소 !https://ko.legacy.reactjs.org/static/bb4b10114882a50090b8ff61b3c4d0fd/31868/react_17_delegation.png (opens in a new tab)
  • 각 이벤트는 해당 리액트 컴포넌트 트리 수준으로 격리되므로 이벤트 버블링으로 인한 혼선을 방지할 수 있다.
    • stopPropagation를 호출해도 같은 요소에 묶인 다른 이벤트 리스너들은 그대로 호출된다. 따라서 React 16에서 컴포넌트 루트가 아래처럼 나뉘었고, '특정 루트'에 대한 이벤트 전파를 막고 싶을지라도 모든 이벤트가 document에 등록되므로 불가능하다. 스크린샷 2024-03-16 오후 9.24.57.png

3. import React from 'react'가 더이상 필요 없다.

  • 기존에는 JSX 변환을 위해 import React from 'react'가 필요했으나 리액트 17부터는 필요없다.
  • React 17부터 바벨 변환시 React.createElement가 사라졌고, jsx 변환에 필요한 react/jsx-runtime 모듈을 내부적으로 호출한다.
  • npx react-codemod update-react-imports: 기존 소스코드의 import React from 'react' 삭제
  • 추후 쓰이지 않게 하려면 ESLint로 강제할 수도 있음
  • tsconfig.json의 jsx를 react-jsx로 변경하면 해당 방식으로 JSX 변환

4. 그 밖의 주요 변경 사항

  • 이벤트 풀링 제거
    • 이벤트 풀링은 이벤트가 발생할 때마다 새로운 이벤트 객체를 생성하는 대신, 이미 사용된 이벤트 객체를 '풀(pool)'에 저장하고 필요할 때 재사용하는 것을 의미. 이벤트 객체를 재사용하기 위해 사용된 개념
    • 해당 시스템에서는 이벤트 핸들러 함수가 호출되고 나면 해당 이벤트 객체의 모든 속성이 null로 설정되는데, 비동기적으로 이벤트 객체에 접근하려고 하면 문제가 발생해 e.persist() 같은 처리가 필요했음
  • useEffect 클린업 함수의 비동기 실행
  • 컴포넌트의 undefined 반환에 대한 일관적인 처리
    • 컴포넌트, forwardRef, memo에서 undefined 반환시 에러
    • React 18에서는 에러 발생 x

리액트 18 버전 살펴보기

1. 새로 추가된 훅 살펴보기

  • useId: 고유한 키값 생성
    • 같은 컴포넌트여도 인스턴스 단위로 다른 값 생성
    • 클라이언트와 서버 불일치 해결 (Math.random의 hydration issue)
  • useTransition: UI의 변경을 가로막지 않고 상태를 업데이트 할 수 있는 함수
    • 동시성(concurrency) 개념이 적용된 훅. 진행 중인 렌더링을 버리고 새로운 상태값으로 다시 렌더링 할 수 있음
    • state 업데이트 함수를 감싸서 사용
  • useDeferredValue: 디바운스와 유사
    • state 자체를 감싸서 사용
    • 상태를 업데이트할 수 있는 코드에 접근할 수 있다면 useTransition, props와 같이 값만 받아와야 하는 상황이라면 useDeferredValue
  • useSyncExternalStore -> 중요해 보이네요!!
    • 상태관리 라이브러리를 위한 훅
    • 동시성 이슈, 하나의 state 값이 있음에도 서로 다른 값을 기준으로 렌더링되는 tearing 현상을 막기 위함
    • 리액트에서 관리할 수 없는 외부 데이터 소스에 대한 동시성 처리 추가 필요
    • useSyncExternalStore(callbackToSubscribeExternalState, externalState, defaultValueForServerside)
  • useInsertionEffect
    • css-in-js 라이브러리를 위한 훅
    • 실행 순서: 렌더링 -> useInsersionEffect -> 레이아웃 계산 -> useLayoutEffect -> 페인팅 -> useEffect

2. react-dom/client

3. react-dom/server

  • renderToPipeableStream
    • hydrateRoot를 호출하면 서버에서는 HTML을 렌더링하고, 클라이언트의 리액트에서는 여기에 이벤트만 추가하여 첫 번째 로딩을 매우 빠르게 수행한다.
    • 이때 renderToPipeableStream을 쓰면 최초에 브라우저는 아직 불러오지 못한 데이터 부분을 Suspense의 fallback으로 받는다.
  • renderToReadableStream
    • renderToPipeableStream는 Node.js 환경, renderToReadableStream은 웹 스트림 기반으로 작동. 서버 환경이 아닌 클라우드플레어(Cloudflare)나 디노(Deno) 같은 웹 스트림을 사용하는 모던 엣지 런타임 환경에서 사용되는 메서드.

4. 자동 배치(Automatic Batchinig)

  • 여러 상태 업데이트를 하나의 리렌더링으로 묶어서 처리
  • 루트 컴포넌트를 createRoot로 생성하면 동기, 비동기, 이벤트 핸들러 등에 관계 없이 배치 수행됨
  • 배치를 피하고 싶은 경우 flushSync 사용

5. 더욱 엄격해진 엄격 모드

  • 더 이상 안전하지 않은 특정 생명주기를 사용하는 컴포넌트에 대한 경고
    • 더 이상 componentWillMount, componentWillReceiveProps, componentWillUpdate를 사용할 수 없음
  • 문자열 ref 사용 금지
    • 문자열이라 참조 파악이 어려운 것과 관련된 문제 발생 가능
  • findDOMNode에 대한 경고 출력
    • findDOMNode: 클래스형 컴포넌트 인스턴스에서 실제 DOM 요소에 대한 참조를 가져오는 데 사용
    • 클래스 내부에서 돔을 임의로 조작하면서 '컴포넌트의 렌더링을 위해서는 부모 컴포넌트의 렌더링이 이렁나야 한다'는 리액트의 추상화를 무너뜨림
  • 구 Context API 사용 시 발생하는 경고
    • childContextAPI, getChildContext
  • 예상치 못한 부작용(side-effects) 검사
    • strict mode에서 이중 호출되는 경우
      • 클래스형 컴포넌트의 constructor, render, shouldComponentUpate, getDerivedStateFromProps
      • 클래스형 컴포넌트의 setState의 첫 번째 인수
      • 함수형 컴포넌트의 body
      • useState, useMemo, useReducer에 전달되는 함수
    • console.log에 회색으로 출력됨
  • React 18에서 추가된 엄격 모드
    • 차후 마운트 해제된 상태에서도 상태값을 유지할 수 있는 기능이 제공될 예정이므로 이를 대비해 useEffect가 두번씩 실행된다.
    • 이를 고려해 적절한 cleanup 함수를 배치하는 것이 좋다.

6. Suspense 기능 강화

  • 수정된 버그
    • Suspense 내부에 출력된 컴포넌트가 실제로는 렌더링되지 않았음에도 effect 함수들이 실행되는 이슈
    • 서버에서 사용할 수 없는 문제
  • fallback 컴포넌트에 자동 스로틀링 지원
  • 한계

7. 인터넷 익스플로러 지원 중단에 따른 추가 폴리필 필요

  • Promise, Symbol, Object.assign

1. app 디렉터리의 등장

  • _document: 페이지에 쓰이는 html과 body 태그를 수정하거나, 서버 사이드 렌더링 시 styled-components 같은 일부 CSS-in-JS를 지원하기 위한 코드를 삽입하는 제한적인 용도로 사용된다.
  • _app: app은 페이지를 초기화하기 위한 용도로 사용되며, 다음과 같은 작업이 가능하다.
    • 페이지 변경 시에 유지하고 싶은 레이아웃
    • 페이지 변경 시 상태 유지
    • componentDidCatch를 활용한 에러 핸들링
    • 페이지간 추가적인 데이터 삽입
    • global CSS 주입

즉 12버전까지는 페이지 공통 레이아웃을 유지할 수 있는 방법은 _app이 유일했다.

라우팅

/app 디렉토리로 라우팅 방식이 변경되었다.

  • 12 이하: /pages/a/b.tsx 또는 /pages/a/b/index.tsx 모두 동일한 주소
  • 13: /app/a/b 는 /a/b 로 변환되며 파일명은 무시

layout.js

스크린샷 2024-03-16 오후 9.25.46.png

_app과 _document를 대신해 공통 코드를 삽입할 수도 있게 되었다.

주의할점

  • layout은 app 디렉터리 내부에서는 예약어이다. 무조건 layout을 써야하며 다른 목적으로는 사용할 수 없다.
  • layout 내부에는 export default가 반드시 있어야 한다.

pages.js

스크린샷 2024-03-16 오후 9.25.51.png

page도 예약어이며, 다음과 같은 props를 받는다.

  • params: 옵셔널 값으로 동적 라우트 파라미터를 사용할 경우 해당 파라미터에 값이 들어온다. ex) […id]
  • searchParams: URL에서 ?a=1 과 같은 query에 해당한다.

주의 사항

  • 무조건 page.(js,ts,jsx,tsx)을 써야하며 다른 목적으로는 사용할 수 없다.

  • export default가 반드시 있어야 한다

    .

error.js

스크린샷 2024-03-16 오후 9.25.55.png

에러 바운더리는 클라이언트에서만 작동하므로 클라리언트 컴포넌트여야한다.

(https://nextjs.org/docs/app/building-your-application/routing/error-handling#handling-errors-in-layouts (opens in a new tab))

리액트 서버 컴포넌트

기존 리액트 컴포넌트와 서버 사이드 렌더링의 한계

한계점

  • 자바스크립트 번들 크기가 0인 컴포넌트를 만들 수 없다: 게시판 등 사용자가 작성한 HTML에 위험한 태그를 제거하기 위해 사용되는 유명한 sanitize-html라이브러리를 사용한다고 가정하자 만약 저 라이브러리를 렌더링한 결과만 클라이언트에 제공하게 된다면 라이브러리를 다운로드 해도 되지 않아도 된다. 스크린샷 2024-03-16 오후 9.26.00.png
  • 백엔드 리소스에 대한 직접적인 접근이 불가능하다: 클라이언트에서 백엔드 데이터에 접근하려면 REST API와 같은 방법이 일반적이다. 이는 항상 백엔드에서 데이터를 제공해줘야 한다. 그러나 직접 가져올 수 있다면 수고로움이 줄어들 것이다. (Q. 프론트에서 DB에 직접 접근하는 것이 맞을까?) 성능 관점에서 볼 때 단계가 하나 줄어든 셈이므로 이점을 가지고 있다.
  • 자동 코드 분할이 불가능하다: 개발자는 항상 코드 분할을 해도 되는 컴포넌트인지를 유념하고 개발해야 하기 때문에 이를 누락하는 경우가 발생할 수 있고 if문 전까지 어떤 컴포넌트를 불러올지 결정할 수 없다.
  • 연쇄적으로 발생하는 클라이언트와 서버의 요청을 대응하기 어렵다: 요청이 연달아 컴포넌트를 렌더링한다고 할때 최초 컴포넌트의 요청과 렌더링이 끝나기 전까지는 하위 컴포넌트의 요청과 렌더링이 끝나지 않는다는 큰 단점이 있다. 서버에서 수행한다면 모두 서버에서 렌더링이 일어나기에 클라이언트에서는 반복적으로 요청을 수행하지 않아도 된다. 그에 따라 효율적으로 컴포넌트를 렌더링할 수 있다.
  • 추상화에 드는 비용이 증가한다: 리액트는 탬플릿 언어로 설계되지 않았다. 탬플릿 언어란 HTML에 특정 언어의 문법을 집어넣어 사용할 수 있는 것을 의미한다. 복잡한 추상화에 따른 결과물을 연산하는 작업을 서버에서 수행하게 된다면, 클라이언트에서는 속도가 빨라지고 서버에서는 클라이언트로 전송되는 결과물도 가벼워질 것이다.

서버 컴포넌트란?

서버 컴포넌트란 하나의 언어, 하나의 프레임워크, 그리고 하나의 API와 개념을 사용하면서 서버와 클라이언트 모두에서 컴포넌트를 렌더링할 수 있는 기법을 의미한다.

클라이언트 컴포넌트는 서버 컴포넌트를 import 할 수 없다. 그 이유는 서버 컴포넌트를 불러오게 된다면 서버 컴포넌트를 실행할 방법이 없기 때문이다.

스크린샷 2024-03-16 오후 9.26.05.png

각 컴포넌트의 차이와 제약사항은 다음과 같다.

  • 서버 컴포넌트
    • 요청이 오면 그 순간 서버에서 딱 한 번 실행될 뿐이므로 상태를 가질 수 없다. (useState, useReducer 등의 훅을 사용할 수 없다.)
    • 렌더링 생명주기도 사용할 수 없다. (useEffect, useLayoutEffect를 사용할 수 없다.)
    • 위의 훅을 사용한 커스텀훅은 사용할 수 없다.
    • 서버에서만 실행되기 떄문에 window, document 등에 접근할 수 없다.
    • 데이터베이스, 내부 서비스, 파일 시스템 등 서버에만 있는 데이터를 async/await으로 접근할 수 있다. 컴포넌트 자체가 async한 것이 가능하다.
    • 다른 서버 컴포넌트를 렌더링하거나 div, span, p 같은 요소를 렌더링하거나, 혹은 클라이언트 컴포넌트를 렌더링할 수 있다.
  • 클라이언트 컴포넌트
    • 브라우저 환경에서만 실행되므로 서버 컴포넌트를 불러오거나, 서버 전용 훅이나 유틸리티를 불러올 수 없다.
    • 클라이언트 컴포넌트가 자식으로 서버 컴포넌트를 갖는 구조는 가능하다. 서버 컴포넌트는 이미 서버에서 만들어진 트리를 가지고 있고, 클라이언트 컴포넌트는 이미 서버에서 만들어진 그 트리를 삽입해서 보여주기 때문에 가능하다.
  • 공용 컴포넌트
    • 서버와 클라이언트에서 모두 사용 가능하다. 이는 서버, 클라이언트의 제약사항을 모두 받는 컴포넌트가 된다.

리액트는 기본적으로 다 공용 컴포넌트로 파악하고 대신 클라이언트 컴포넌트를 명시하려면 ‘use client’라고 작성해두면 된다.

서버 사이드 렌더링과 서버 컴포넌트의 차이

서버 사이드 렌더링의 목적은 초기에 인터랙션은 불가능하지만 정적인 HTML을 빠르게 내려주는 데 초점을 두고 있다. 그렇기에 여전히 HTML이 로딩된 이후에는 자바스크립트 코드를 다운로드하고 파싱하고 실행하는데 비용이 든다.

서버 컴포넌트를 활용해 서버에서 렌더링할 수 있는 컴포넌트는 서버에서 완성해 제공받은 다음, 클라이언트에서 서버 사이드 렌더링으로 초기 HTML을 전달받을 수 있다. 이는 자바스크립트의 양도 줄어들어 브라우저의 부담을 덜 수 있다.

서버 컴포넌트는 어떻게 작동하는가?

https://github.com/reactjs/server-components-demo/blob/29ec74365084e1921c6bf49c03266788099449a3/server/api.server.js#L74-L87 (opens in a new tab)

스크린샷 2024-03-16 오후 9.26.10.png

  1. 서버가 렌더링 요청을 받는다. 서버가 렌더링 과정을 수행해야 하므로 서버에서 시작된다.
  2. 서버에서 받은 요청에 따라 컴포넌트를 JSON으로 직렬화 한다. 이때 서버에서 렌더링할 수 있는 것은 직렬화해서 내보내고, 클라이언트로 표시된 부분은 해당 공간을 플레이스홀더 형식으로 비워두고 나타낸다. 브라우저는 이를 역질렬화해서 렌더링을 수행한다.
  3. 브라우저가 리액트 컴포넌트 트리를 구성한다. 브라우저가 서버로 스트리밍으로 JSON 결과물을 받았다면 이 구문을 다시 파싱한 결과물을 바탕으로 트리를 재구성해 컴포넌트를 만들어 나간다.

Next.js에서의 리액트 서버 컴포넌트

새로운 fetch 도입과 getServerSideProps, getStaticProps, getInitialProps의 삭제

getServerSideProps, getStaticProps, getInitialProps 는 fetch 기반으로 요청하도록 변경되었다.

이에 서버 컴포넌트 트리내에 동일한 요청이 있다면 재요청이 발생하지 않도록 요청 중복을 방지했다.

정적 렌더링과 동적 렌더링

정적인 라우팅에 대해서는 기본적으로 빌드 임에 렌더링을 미리 해두고 캐싱해 재사용할 수 있게끔 해뒀고, 동적인 라우팅에 대해서는 서버에 매번 요청이 올 때마다 컴포넌트를 렌더링하도록 변경했다.

스크린샷 2024-03-16 오후 9.26.14.png

동적인 렌더링을 원할 경우 { cache: 'no-cache' } 옵션을 추가해서 요청이 올때 마다 렌더링을 수행하게 하면되고, 정적일 경우엔 제거하면 된다.

만약 동적인 주소이지만 특정 주소에 대해서만 캐싱하고 싶을 경우에는 generateStaticParams 를 사용하면 된다.

https://nextjs.org/docs/app/api-reference/functions/generate-static-params (opens in a new tab)

스크린샷 2024-03-16 오후 9.26.19.png

캐시와 mutating, 그리고 revalidating

페이지에 revalidate라는 변수를 선언해서 페이지 레벨로 정의하는 것도 가능하다.

  1. 최초로 해당 라우트 요청이 올 때는 미리 정적으로 캐시해 둔 데이터를 보여준다.
  2. 이 캐시된 초기 요청은 revalidate에 선언된 값만큼 유지된다.
  3. 만약 해당 시간이 지나도 일단은 캐시된 데이터를 보여준다.
  4. Next.js는 캐시된 데이터를 보여주는 한편, 시간이 경과했으므로 백그라운드에서 다시 데이터를 불러온다.
  5. 4번의 작업이 성공적으로 끝나면 캐시된 데이터를 갱신하고, 그렇지 않다면 과거 데이터를 보여준다.

서버 액션

Data Fetching: Server Actions and Mutations (opens in a new tab)

Redirecting (opens in a new tab)

If you would like to redirect the user to a different route after the completion of a Server Action, you can use [redirect](<https://nextjs.org/docs/app/api-reference/functions/redirect>) API. redirect needs to be called outside of the try/catch block:

app/actions.ts

스크린샷 2024-03-16 오후 9.26.24.png

서버 액션 사용 시 주의할 점

  • 서버 액션은 클라이언트 컴포넌트 내에서 정의될 수 없다. 클라이언트 컴포넌트에서 서버 액션을 쓰고 싶을 때는 ‘use server’로 서버 액션만 모여있는 파일을 별도로 import 해야 한다.
  • 서버 액션으 import하는 것 뿐만 아니라, props 형태로 서버 액션을 클라이언트 컴포넌트에 넘기는 것 또한 가능하다. 즉 서버에서만 실행될 수 있는 자원은 반드시 파일 단위로 분리해야 한다.

Next.js 13코드 맛보기

getStaticProps와 비슷한 정적인 페이지 렌더링 구현해보기

스크린샷 2024-03-16 오후 9.26.28.png

generateStaticParams를 통해 [id]로 사용할 값을 객체 배열로 모아두고 별다른 옵션 없이 fetch하게 되면 미리 모든 html이 빌드 타임에 생성된다.

revalidate를 써주게 되면 캐시 유효시간이 지나게 되면 이전의 캐시를 무효화하고 새로운 페이지를 빌드하게 된다.