JiSoo's Devlog

[모던 리액트 Deep Dive] 4장 본문

Frontend/React

[모던 리액트 Deep Dive] 4장

지숭숭숭 2024. 5. 9. 20:52

싱글 페이지 애플리케이션

렌더링과 라우팅에 필요한 대부분의 기능을 서버가 아닌 브라우저의 자바스크립트에 의존하는 방식

최초에 서버에서 최소한의 데이터를 불러온 이후부터는 이미 가지고 있는 자바스크립트 리소스와 브라우저 API를 기반으로 모든 작동이 이뤄진다

최초에 로딩해야 할 리소스가 커지는 단점이 있지만 한번 로딩되면 이후 서버를 거쳐 필요한 리소스를 받아올 일이 적어져 사용자에게 훌륭한 UI/UX 제공 가능

 

전통적인 방식의 애플리케이션

과거 서버 사이드에서 작동하던 애플리케이션은 페이지 전환이 발생할 때마다 새롭게 페이지를 요청하고 HTML 페이지를 다운로드해 파싱하는 작업을 거친다

 

웹페이지의 모든 영역(페이지 렌더링부터 사용자 인터랙션까지)을 담당하며 아우를 수 있는 싱글 페이지 렌더링이 인기를 얻었다

과거에는 자바스크립트에서 할 수 있는 일이 제한적이었기에 대부분의 처리를 서버에서 해야만 했다

서버 의존적인 문제는 웹 애플리케이션의 확장성에도 걸림돌로 작용했다

 

JAM 스택이 등장하고 대부분의 작업을 자바스크립트에서 수행할 수 있었기 때문에 자바스크립트와 마크업을 미리 빌드해 두고 정적으로 사용자에게 제공하면 이후 작동은 모두 사용자의 클라이언트에서 실행되기 때문에 서버 확장성 문제에서 자유로워질 수 있게 되었다

 

자바스크립트 코드의 규모가 커지면서 웹 애플리케이션에서 제공하는 자바스크립트 리소스의 크기와 수가 모두 증가하기 시작했다

 

 

서버 사이드 렌더링

최초에 사용자에게 보여줄 페이지를 서버에서 렌더링해 빠르게 사용자에게 화면을 제공하는 방식

싱글 페이지 애플리케이션은 사용자에게 제공되는 자바스크립트 번들에서 렌더링 담당

서버 사이드 방식은 렌더링에 필요한 작업을 모두 서버에서 수행

클라이언트 렌더링은 사용자 기기의 성능에 영향을 받지만 서버 사이드 렌더링은 비교적 안정적인 렌더링 가능

 

장점

◦ 최초 페이지 진입이 비교적 빠르다

사용자가 최초 페이지에 진입했을 때 페이지에 유의미한 정보가 그려지는 시간이 더 빨라질 수 있다

화면 렌더링이 HTTP 요청에 의존적이거나 렌더링해야 할 HTML의 크기가 커진다면 상대적으로 서버 사이드 렌더링이 더 빠를 수 있다

하지만 서버가 사용자를 감당 못하고 리소스 확보가 어렵다면 오히려 느려질 수도 있다

 

  검색 엔진과 SNS 공유 등 메타데이터 제공이 쉽다

서버 사이드 렌더링은 검색 엔진 최적화에 유용하다

검색 엔진에 제공할 정보를 서버에서 가공해 HTML 응답으로 제공할 수 있어 검색 엔진 최적화에 대응하기 유용하다

 

  누적 레이아웃 이동이 적다

누적 레이아웃 이동이란 사용자에게 페이지를 보여준 이후에 뒤늦게 HTML 정보가 추가되거나 삭제되어 마치 화면이 덜컥거리는 것과 같은 부정적인 사용자 경험을 말한다

즉, 사용자가 예상치 못한 시점에 페이지가 변경되어 불편을 초래하는 것

싱글 페이지 애플리케이션에서는 페이지 콘텐츠가 API 요청에 의존하고 응답 속도가 제각각이며 이를 적절히 처리해놓지 않으면 누적 레이아웃 이동 문제가 발생할 수 있다

서버 사이드 렌더링은 이런 요청이 완전히 완료된 이후에 완성된 페이지를 제공하므로 이런 문제에 비교적 자유롭다

 

◦  사용자의 디바이스 성능에 비교적 자유롭다

 자바스크립트 리소스 실행은 절대적으로 사용자 디바이스 성능에 의존적이다

서버 사이드 렌더링을 수행하면 이런 부담을 서버에 나눌 수 있어 사용자의 디바이스 성능으로부터 좀 더 자유로워질 수 있다

 

◦ 보안에 좀 더 안전하다

인증 혹은 민감한 작업을 서버에서 수행하고 그 결과만 브라우저에 제공해 보안 위협을 피할 수 있다

 

단점

◦ 소스코드를 작성할 때 항상 서버를 고려해야 한다

◦ 적절한 서버가 구축돼 있어야 한다

◦ 서비스 지연에 따른 문제

 

가장 뛰어난 싱글 페이지 애플리케이션은 가장 뛰어난 멀티 페이지 애플리케이션보다 낫다

평균적인 싱글 페이지 애플리케이션은 평균적인 멀티 페이지 애플리케이션보다 느리다

페인트 홀딩 : 같은 출처에서 라우팅이 일어날 경우 화면을 잠깐 하얗게 띄우는 대신 이전 페이지 모습을 잠깐 보여주는 기법

back forward cache : 브라우저 앞으로 가기, 뒤로 가기 실행 시 캐시된 페이지를 보여주는 기법

Shared Element Transitions : 페이지 라우팅이 일어났을 때 두 페이지에 동일 요소가 있다면 해당 콘텍스트를 유지해 부드럽게 전환되게 하는 기법

평균적인 사용자 경험을 제공한다는 가정하에 별도의 최적화를 거쳐야 하는 싱글 페이지 애플리케이션보다 서버에서 렌더링 되는 멀티 페이지 애플리케이션이 더 나은 경험을 제공한다고 볼 수 있다

 

두 방법론 모두 상황에 따라 유효한 방법

싱글 페이지 애플리케이션이 제공하는 보일러플레이트나 라이브러리가 점차 완벽해지면서 잠재적인 모든 위험을 제거할 수 있고 멀티 페이지 애플리케이션이 브라우저 API 도움을 받아 싱글 페이지 애플리케이션과 같은 끊김 없는 사용자 경험을 제공할 수도 있다

 

현대의 서버 사이드 렌더링

최초 웹사이트 진입 시에는 서버 사이드 렌더링이 방식으로 서버에서 완성된 HTML을 제공받고

이후 라우팅에서는 서버에서 내려받은 자바스크립트를 바탕으로 마치 싱글 페이지 애플리케이션처럼 작동

 

두 렌더링을 모두 이해해야 두 가지 장점을 완벽하게 취하는 제대로 된 웹서비스를 구축할 수 있다

 


서버 사이드 렌더링을 위한 리액트 API

리액트에서 서버 사이드 렌더링을 실행할 때 사용되는 API를 확인하려면 리액트 저장소의 react-dom/server.js 확인

react-dom이 렌더링 하기 위한 다양한 메서드를 제공하고 server.node.js에 있는 함수를 export 하고 있다

 

renderToString

인수로 넘겨받은 리액트 컴포넌트를 렌더링해 HTML 문자열로 반환하는 함수

import ReacrDOMServer from 'react-com/server'

function ChildrenComponent({ fruits }: { fruits: Array<string> }) {
  useEffect(() => {
    cnsole.log(fruits)
  }, [fruits])
  
  function handleClick() {
    console.log('hello')
  }
  
  return (
    <ul>
      {fruits.map((fruit) => (
        <li key={fruit} onClick={handleClick}>
          {fruit}
        </li>
      ))}
    </ul>
  )
}

function SampleComponent() {
  return (
    <>
      <div>hello</div>
      <ChildrenComponent fruits={['apple', 'banana', 'peach']} />
    </>
  )
}

const result = ReactDOMServer.renderToString(
  React.createElement('div', { id: 'root' }, <SampleComponent />),
)

↑ result는 아래와 같은 문자열 반환

<div id="root" data-reactroot="">
  <div>hello</hello>
  <ul>
    <li>apple</li>
    <li>banana</li>
    <li>peach</li>
  </ul>
</div>

 

renderToString은 단순히 '최초 HTML 페이지를 빠르게 그려주는 데'에 목적이 있다

클라이언트에서 실행되는 자바스크립트 코드를 포함시키거나 렌더링 하는 역할까지 해주지는 않는다

renderToString을 사용하면 클라이언트에서 실행되지 않고 일단 먼저 완성된 HTML을 서버에서 제공할 수 있어 초기 렌더링에서 뛰어난 성능을 보일 것이다

실제 웹페이지가 사용자와 인터랙션 할 준비가 되기 위해서는 이와 관련된 별도 자바스크립트 코드를 모두 다운로드, 파싱, 실행하는 과정을 거쳐야 한다

data-reactroot 속성은 리액트 컴포넌트의 루트 엘리먼트가 무엇인지 식별하는 역할로 이후 자바스크립트를 실행하기 위한 hydrate 함수에서 루트를 식별하는 기준점이 된다

 

renderToStaticMarkup

리액트 컴포넌트를 기준으로 HTML 문자열을 만든다는 점에서 renderToString과 유사하다

data-reactroot와 같은 리액트에서만 사용하는 추가적인 DOM 속성을 만들지 않는다

리액트에서만 사용하는 속성을 제거하면 결과물인 HTML의 크기를 약간이라도 줄일 수 있다

이 함수를 실행한 결과로 렌더링 수행 시 클라이언트에서는 리액트에서 제공하는 useEffect 같은 브라우저 API를 절대 실행할 수 없다

리액트의 이벤트 리스너가 필요 없는 완전히 순수한 HTML을 만들 때만 사용된다

블로그 글, 상품 약관 정보와 같이 아무런 브라우저 액션이 없는 정적인 내용만 필요한 경우에 유용하다

 

renderToNodeStream

renderToString과 결과물이 완전히 동일하지만 두 가지 차이점이 있다

첫 번째는 renderToString과 renderToStaticMarkup은 브라우저에서 실행 가능하지만 renderToNodeStream은 브라우저에서 사용하는 것이 불가능하다

두 번째는 결과물의 타입이다

renderToNodeStream의 결과물은 Node.js의 ReadableStream이다

ReadableStream은 utf-8로 인코딩 된 바이트 스트림으로 서버 환경에서만 사용 가능하다

궁극적으로 브라우저가 원하는 결과물인 string을 얻기 위해서 추가적인 처리가 필요하다

ReadableStream자체는 브라우저에서 사용 가능한 객체이지만 만드는 과정이 브라우저에서 불가능하게 구현돼 있다

 

스트림은 큰 데이터를 다룰 때 데이터를 청크로 분할해 조금씩 가져오는 방식을 의미한다

renderToString으로 생성해야 하는 HTML의 크기가 매우 크면 서버에 큰 부담이 될 수 있는데 스트림을 사용하면 큰 크기의 데이터를 청크 단위로 분리해 순차적으로 처리할 수 있다는 장점이 있다

대부분의 널리 알려진 리액트 서버 사이드 렌더링 프레임워크는 모두 renderToNodeStream을 채택하고 있다

 

renderToStaticNodeStream

renderToNodeStream과 제공하는 결과물은 동일

리액트 자바스크립트에 필요한 리액트 속성이 제공되지 않는다

hydrate를 할 필요가 없는 순수 HTML 결과물이 필요할 때 사용하는 메서드

 

 

render

render 함수는 컴포넌트와 HTML 요소를 인수로 받는다

두 정보를 바탕으로 HTML의 요소에 해당 컴포넌트를 렌더링 하며 이벤트 핸들러를 붙이는 작업까지 한 번에 수행한다

클라이언트에서만 실행되는 렌더링과 이벤트 핸들러 추가 등 리액트를 기반으로 한 온전한 웹페이지를 만드는 데 필요한 모든 작업을 수행한다

import * as ReactDOM from 'react-dom'
import App from './App'

const rootElement = document.getElementById('root')

ReactDOM.render(<App />, rootElement)

 

hydrate

renderToString과 renderToNodeStream으로 생성된 HTML 콘텐츠에 자바스크립트 핸들러나 이벤트를 붙이는 역할

정적으로 생성된 HTML에 이벤트와 핸들러를 붙여 완전한 웹페이지 결과물을 만든다

import * as ReactDOM from 'react-dom'
import App from './App'

// containerId를 가리키는 element는 서버에서 렌더링된 HTML의 특정 위치를 의미
const element = document.getElementById(containerId)

// 해당 element를 기준으로 리액트 이벤트 핸들러를 붙인다
ReactDOM.render(<App />, element)

hydrate는 기본적으로 이미 렌더링 된 HTML이 있다는 가정하에 작업이 수행되고 렌더링 된 HTML을 기준으로 이벤트를 붙이는 작업만 실행한다

hydrate로 넘겨준 두 번째 인수에는 반드시 렌더링 된 정적인 HTML 정보가 담겨 있어야 한다

 

 


 

 

Next.js

풀스택 웹 애플리케이션을 구축하기 위한 리액트 기반 프레임워크

다른 프레임워크에 비해 사용자도 많고 꾸준히 새로운 기능을 추가해 릴리스하고 있어 Next.js를 선택하는 것이 합리적

 

Next.js 프로젝트 만들기

npx create-next-app@latest --ts

 

next  : Next.js의 기반이 되는 패키지

eslint-config-next : 기반 프로젝트에서 사용하도록 만들어진 ESLint 설정

 

next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
}

module.exports = nextConfig

@type으로 시작하는 주석은 타입스크립트의 타입 도움을 받기 위해 추가된 코드

reactStrictMode : 리액트의 엄격 모드와 관련된 옵션, 리액트 애플리케이션 내부에서 잠재적인 문제를 알리기 위한 도구

swcMinify : SWC를 기반으로 코드 최소화 작업을 할 것인지 여부를 설정하는 속성

SWC는 번들링과 컴파일을 더욱 빠르게 수행하기 위해 만들어진 바벨의 대안

 

pages/_app.tsx

에러 바운더리를 사용해 애플리케이션 전역에서 발생하는 에러 처리

reset.css 같은 전역 CSS 선언

모든 페이지에 공통으로 사용 또는 제공해야 하는 데이터 제공

 

pages/_document.tsx

create-next-app으로 생성했다면 존재하지 않는다

_app.tsx가 애플리케이션 전체를 초기화하는 곳이라면 _document.tsx는 애플리케이션의 HTML을 초기화하는 곳

CSS-in-JS의 스타일을 서버에서 모아 HTML로 제공하는 작업을 한다

웹사이트의 뼈대가 되는 HTML 설정과 관련된 코드를 추가하는 곳으로 반드시 서버에서만 렌더링 된다

 

pages/_error.tsx

기본적으로 생성해 주는 파일은 아니다

Next.js 프로젝트 전역에서 발생하는 에러를 적절하게 처리하고 싶다면 활용하면 된다

 

pages/404.tsx

404 페이지를 정의할 수 있는 파일

기본 404 페이지를 볼 수 있고 원하는 404 페이지를 만들 수 있다

 

pages/500.tsx

서버에서 발생하는 에러를 핸들링하는 페이지

_error.tsx와 500.tsx 둘 다 있다면 500.tsx가 먼저 실행된다

export default function My500Page() {
  return <h1>서버에서 에러가 발생했습니다</h1>
}

 

 

Next.js에서 react-pages처럼 라우팅 구조는 /pages 디렉터리를 기초로 구성되며 각 페이지에 있는 default export로 내보낸 함수가 해당 페이지의 루트 컴포넌트가 된다

 

/pages/index.tsx

웹사이트의 루트이며 localhost:3000과 같은 루트 주소를 의미

 

/pages/hello.tsx

/pages가 생략되고 파일명이 주소가 된다 localhost:3000/hello

 

/pages/hello/[greeting].tsx

[ ] 여기에 어떠한 문자도 올 수 있다는 뜻

localhost:3000/hello/1, localhost:3000/hello/greeting 모두 유효

 

/pages/hi/[...props].tsx

/hi를 제외한 /hi 하위의 모든 주소가 여기로 온다

 

[ ] 안의 내용은 변수로 처리된다

주소에 숫자를 입력했다고 해서 숫자로 형변환되지 않음에 주의해야 한다

 

Next.js는 서버 사이드 렌더링과 동시에 싱글 페이지 애플리케이션과 같이 클라이언트 라우팅 또한 수행한다

최초 페이지 렌더링이 서버에서 수행된다

 

next/link는 Next.js에서 제공하는 라우팅 컴포넌트로 <a/> 태그와 비슷한 동작을 한다

next/link로 이동하는 경우 서버 사이드 렌더링이 아닌 클라이언트에서 필요한 자바스크립트만 불러온 뒤 라우팅하는 클라이언트 라우팅/렌더링 방식으로 작동한다

Next.js는 서버 사이드 렌더링의 장점인 사용자가 빠르게 볼 수 있는 최초 페이지를 제공한다는 점과 싱글 페이지 애플리케이션의 장점인 자연스러운 라우팅이라는 두 장점을 모두 살리는 방식으로 작동한다

 

내부 페이지 이동 시 규칙

◦ <a> 대신 <Link>를 사용한다

◦ window.location.push 대신 router.push를 사용한다

 

/pages/api/hello.ts

api 폴더는 서버의 API를 정의하는 폴더

HTML 요청을 하는 게 아니라 단순히 서버 요청을 주고받게 된다

 

 

Data Fetching

pages/의 폴더에 있는 라우팅이 되는 파일에서만 사용할 수 있고 예약어로 지정되어 반드시 정해진 함수명으로 export를 사용해 함수를 파일 외부로 내보내야 한다

이걸 활용해 서버에서 미리 필요한 페이지를 만들어 제공하거나 해당 페이지에 요청이 있을 때마다 서버에서 데이터를 조회해 미리 페이지를 만들어 제공할 수 있다

 

getStaticPaths와 getStaticProps

어떤 페이지를 블로그, 게시판과 같이 사용자와 관계없이 정적으로 결정된 페이지를 보여주고자 할 때 사용되는 함수

반드시 둘이 함께 있어야 사용할 수 있다

getStaticPaths는 접근 가능한 주소를 정의하는 함수

getStaticProps는 페이지로 요청이 왔을 때 제공할 props를 반환하는 함수

getStaticPaths 함수의 번호나 값 중 하나인 fallback 옵션은 미리 빌드해야 할 페이지가 너무 많은 경우 사용 가능

 

getServerSideProps

서버에서 실행되는 함수이며 해당 함수가 있으면 무조건 페이지 진입 전 이 함수를 실행한다

응답값에 따라 페이지의 루트 컴포넌트에 props를 반환할 수도, 다른 페이지로 리다이렉트 시킬 수도 있다

props의 결과를 HTML에 정적으로 작성해 내려주기 때문에 JSON으로 직렬화할 수 없는 값은 props로 제공 X

무조건 서버에서만 실행된다!

그렇기 때문에 window.document 같은 브라우저에서만 접근할 수 있는 객체에는 접근 불가

API 호출 시 프로토콜과 도메인 없이 fetch 요청 불가

사용자가 매 페이지를 호출할 때마다 실행되고 이 실행이 끝나기 전까지는 사용자에게 어떠한 HTML도 보여줄 수 없다

 

getServerSideProps에서 어떤 조건에 따라 다른 페이지로 보내고 싶다면 redirect 사용 가능

export const getServerSideProps: GetServerSideProps = async (context) => {
  const {
    query: { id = ''},
  } = context
  const post = await fetchPost(id.toString())
  
  if (!post) {
    redirect: {
      destination: '/404'
    }
  }
  
  return {
    props: { post },
  }
}

 

getInitialProps

getStaticProps나 getServerSideProps가 나오기 전에 사용할 수 있었던 유일한 페이지 데이터 불러오기 수단이었다

_app.tsx와 같이 일부 페이지에서는 getInitialProps밖에 사용할 수 없다

페이지의 루트 함수에 정적 메서드로 추가하고 props 객체를 반환하는 게 아니라 바로 객체를 반환한다

라우팅에 따라 서버와 클라이언트 모두에서 실행 가능한 메서드

Todo.getInitialProps = async (context) => {
  const isServer = context.req
  console.log(`${isServer ? '서버' : '클라이언트'}에서 실행`)
}

 

 

 

스타일 적용하기

 

전역 스타일

CSS Reset이라 불리는 애플리케이션 전체에 공통으로 적용하고 싶은 스타일이 있다면 _app.tsx를 활용

글로벌 스타일은 다른 페이지나 컴포넌트와 충돌할 수 있으므로 반드시 _app.tsx에서만 제한적으로 작성해야 한다

 

컴포넌트 레벨 CSS

[name].module.css와 같은 명명 규칙만 준수하면 되며 다른 컴포넌트의 클래스명과 겹쳐 스타일에 충돌이 일어나지 않도록 고유한 클래스명을 제공한다

 

SCSS와 SASS

sass 패키지를 npm install --save-dev sass 명령어로 설치하면 별도 설정 없이 동일하게 스타일 사용 가능

 

CSS-in-JS

자바스크립트 내부에 스타일시트 삽입

styled-component가 가장 많은 사용자들이 사용하고 있다

CSS-in-JS를 서버 사이드 렌덜이 프레임워크에서 사용할 때는 반드시 초기화 과정을 서버에서 거쳐야 한다

 

 

next.config.js

Next.js 실행에 필요한 설정을 추가할 수 있는 파일

 

basePath : url의 접두사라 볼 수 있다. basePath: 'docs'처럼 문자열을 추가하면 localhost:3000/docs에 서비스가 시작된다

swcMinify : swc를 이용해 코드를 압축할지를 나타낸다. 기본값은 true

poweredByHeader : Next.js는 응답 헤더에 X-Power-by: Next.js 정보를 제공하는데 false를 선언하면 이 정보가 사라진다

redirects : 특정 주소를 다른 주소로 보내고 싶을 때 사용된다

reactStrictMode : 리액트에서 제공한느 엄격 모드 설정 여부

assetPrefix : 만약 next에서 빌드된 결과물을 동일한 호스트가 아닌 다른 CDN 등에 업로드하고 싶다면 이 옵션에 해당 CDN 주소를 명시하면 된다

assetPrefix 설정이 활성화되면 static 리소스들은 해당 주소에 있다고 가정하고 해당 주소로 요청하게 된다

 

 

 

728x90

'Frontend > React' 카테고리의 다른 글

[모던 리액트 Deep Dive] 6장  (0) 2024.05.19
[모던 리액트 Deep Dive] 5장  (0) 2024.05.18
[모던 리액트 Deep Dive] 3장  (1) 2024.05.03
[모던 리액트 Deep Dive] 2장  (2) 2024.04.27
[모던 리액트 Deep Dive] 1장  (0) 2024.04.08