학습 자료

console.log 가 터미널에 찍힌다


Article

앞 강의에서 만든 페이지에 console.log를 넣어본다.

export default function Home() {
  console.log("여기는 어디인가");
  return <h1></h1>;
}

브라우저 콘솔을 연다. 아무것도 없다.

터미널을 본다. 거기 찍혀 있다.

터미널
  • 여기는 어디인가
  • GET / 200 in 42ms

기본값이 서버다

App Router의 모든 컴포넌트는 기본적으로 서버에서 실행된다. 이걸 서버 컴포넌트라 부른다.

React 편에서 만든 컴포넌트는 전부 브라우저에서 돌았다. 여기서는 반대다. 우리가 아무 표시를 하지 않으면 서버에서 돈다.

서버 컴포넌트가 화면이 되기까지
  1. 요청사용자가 / 을 연다
  2. 서버에서 실행컴포넌트 함수를 부른다
  3. HTML 생성완성된 마크업
  4. 브라우저즉시 보인다

1강에서 말한 SSR이 이것이다. 아무 설정도 하지 않았는데 서버 렌더링이 됐다.

브라우저에서 페이지 소스 보기(Ctrl+U)를 해보면 <h1>홈</h1>이 그대로 들어 있다. React 편 2강의 빈 <div id="root">와 정반대다.

서버라서 할 수 있는 일

컴포넌트가 서버에서 돈다면, 서버에서 할 수 있는 모든 일을 컴포넌트 안에서 할 수 있다.

export default async function Posts() {
  const res = await fetch("https://api.example.com/posts");
  const posts = await res.json();

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

async 함수다. 컴포넌트 안에서 바로 await를 쓴다.

React 편 15강에서 데이터를 받아오던 코드와 비교해보자.

데이터를 받아오는 두 방식

클라이언트에서

useEffect + useState

  • isLoading state 를 만든다
  • error state 를 만든다
  • 경쟁 상태를 막는다
  • 화면이 그려진 뒤에 요청한다

서버 컴포넌트에서

async · await

  • 그냥 await 한다
  • 데이터가 온 뒤에 HTML 을 만든다
  • 로딩 상태가 애초에 없다
  • 요청이 서버에서 나간다

useState도, useEffect도 없다. 데이터가 준비된 뒤에 HTML이 만들어지므로 로딩 상태를 관리할 이유가 없다.

그리고 요청이 서버에서 서버로 나간다. 브라우저에서 지구 반대편 API를 부르는 것보다 대개 빠르다.

브라우저에 코드가 가지 않는다

더 중요한 게 있다. 서버 컴포넌트의 코드는 브라우저로 전송되지 않는다.

import { formatDate } from "heavy-date-library";

export default async function Post({ post }) {
  return <time>{formatDate(post.createdAt)}</time>;
}

heavy-date-library가 200KB짜리라도 사용자는 그걸 내려받지 않는다. 서버에서 이미 문자열로 바꿔 HTML에 넣었기 때문이다.

브라우저가 받는 자바스크립트
클라이언트 렌더링300KB
서버 컴포넌트90KB

서버에서는 할 수 없는 일

대신 잃는 것이 있다. 서버에는 브라우저가 없다.

export default function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
콘솔
  • Error: useState only works in Client Components. Add the "use client" directive at the top of the file to use it.

당연하다. 서버는 HTML을 만들어 보내고 할 일이 끝난다. 버튼을 누를 사용자가 거기 없다.

서버 컴포넌트에서 못 하는 것
useState · useReducer상태가 바뀌어도 다시 그릴 사람이 없다
useEffect화면이 그려진 뒤가 없다
onClick · onChange클릭할 브라우저가 없다
window · document브라우저에만 있는 객체다
localStorage같은 이유

이 목록이 곧 클라이언트 컴포넌트가 필요한 이유다. 8강과 9강에서 경계를 긋는 법을 배운다.

그럼 React 는 어디로 갔나

서버가 완성된 HTML을 보냈다면, 브라우저의 React는 무슨 일을 할까.

1강에서 이름만 언급한 하이드레이션이 여기서 일어난다.

화면이 살아나는 순서
  1. 01

    서버가 HTML 을 보낸다

    사용자는 즉시 글자를 본다. 다만 버튼을 눌러도 아무 일이 없다.

  2. 02

    브라우저가 자바스크립트를 받는다

    클라이언트 컴포넌트의 코드만 온다. 서버 컴포넌트 코드는 오지 않는다.

  3. 03

    React 가 기존 HTML 에 붙는다

    새로 그리지 않는다. 이미 있는 DOM 에 이벤트 핸들러를 연결하고 상태를 복원한다.

  4. 04

    이제 상호작용이 된다

    버튼이 눌리고 입력창이 반응한다.

서버 렌더링일 때의 시간 흐름
서버
컴포넌트 실행 → HTML
화면
흰 화면글자가 보인다 (아직 안 눌림)완전히 동작
브라우저 JS
하이드레이션
요청HTML 도착하이드레이션 완료
글자는 일찍 보이지만 잠깐 동안 버튼이 안 눌린다

1강의 CSR 타임라인과 비교해보면, 흰 화면이 훨씬 짧다.

하이드레이션이 깨질 때

서버가 만든 HTML과 브라우저의 React가 만든 결과가 다르면 에러가 난다.

export default function Clock() {
  return <p>{new Date().toLocaleTimeString()}</p>;
}
콘솔
  • Hydration failed because the server rendered HTML didn't match the client.

서버에서 렌더링한 시각과 브라우저에서 하이드레이션할 때의 시각이 다르다. React 편 13강에서 렌더 함수는 순수해야 한다고 했던 그 규칙이다. 같은 입력에 같은 출력을 내야 하는데, 시간은 매번 다르다.

파일 하나를 열어 확인한다

머리로만 이해하지 말고 눈으로 본다. 페이지에 이렇게 써두고 새로고침한다.

export default async function Home() {
  console.log("서버에서 실행");

  const res = await fetch("https://api.github.com/repos/vercel/next.js");
  const data = await res.json();

  return (
    <div>
      <h1>{data.name}</h1>
      <p>별 {data.stargazers_count.toLocaleString()}개</p>
    </div>
  );
}
직접 확인해볼 것
  1. 01

    터미널에 로그가 찍히는가

    브라우저 콘솔에는 안 나온다. 서버에서 실행됐다는 증거다.

  2. 02

    페이지 소스에 별 개수가 있는가

    Ctrl+U 를 눌러본다. 숫자가 HTML 에 박혀 있다. 크롤러가 이걸 읽는다.

  3. 03

    네트워크 탭에 github.com 요청이 있는가

    없다. 브라우저가 아니라 서버가 요청했기 때문이다.

세 번째가 결정적이다. 브라우저 개발자 도구의 네트워크 탭에 GitHub API 요청이 보이지 않는다. 우리 서버가 대신 다녀왔다.

정리하면

  • App Router의 컴포넌트는 기본값이 서버 컴포넌트다. 표시를 안 하면 서버에서 돈다.
  • 그래서 console.log가 터미널에 찍히고, 페이지 소스에 내용이 들어 있다.
  • 컴포넌트를 async로 만들고 바로 await 할 수 있다. 로딩 상태를 만들 이유가 없다.
  • 서버 컴포넌트의 코드는 브라우저로 전송되지 않는다. 무거운 라이브러리와 API 키를 써도 된다.
  • 대신 useState, useEffect, onClick, window를 쓸 수 없다. 브라우저가 없기 때문이다.
  • 서버가 보낸 HTML에 React가 이벤트를 붙이는 과정이 하이드레이션이다.
  • 서버와 브라우저에서 결과가 달라지면 하이드레이션이 깨진다. new Date()가 대표적이다.

Part 1이 끝났다. 다음 강의부터 라우팅을 제대로 파고든다. layout 말고도 Next.js가 알아보는 파일들이 있다.

console.log 가 터미널에 찍힌다 — 디코드랩(DCODELAB)