서버 컴포넌트가 생성되는 과정(2 - 클라이언트)

우혁·2024년 11월 22일
19

React

목록 보기
15/19
post-thumbnail

스트리밍된 데이터를 가져온 후 렌더링 준비

createFromFetch 함수는 서버 컴포넌트에서 서버로부터 데이터를 가져오고 처리하는 핵심 함수이다.

이 함수의 목적은 서버에서 생성된 컴포넌트 트리를 클라이언트에서 재구성하는 것이다.

function createFromFetch<T>(
  promiseForResponse: Promise<Response>, 
  options?: Options,
): Thenable<T> {
  // FlightResponse 객체 생성(서버 응답을 처리하고 저장)
  const response: FlightResponse = createResponseFromOptions(options);
  promiseForResponse.then(
    function (r) {
      // 응답 본문을 읽고 처리
      // 서버에서 전송된 컴포넌트 트리 데이터가 파싱되고 FlightResponse 객체에 저장
      startReadingFromStream(response, (r.body: any));
    },
    function (e) {
      // 오류 처리
      reportGlobalError(response, e);
    },
  );
  // 루트 컴포넌트 추출(비동기 작업의 결과)
  return getRoot(response);
}

FlightResponse 객체 생성

서버 컴포넌트에서 서버와 클라이언트 간의 통신을 위해 사용되는 특별한 객체이다.

클라이언트 측에서 서버 응답을 처리하고 관리하는 데 사용된다.

  • createResponseFromOptions 함수
function createResponseFromOptions(options: void | Options) {
  return createResponse(
    null,
    null,
    options && options.callServer ? options.callServer : undefined,
    undefined, // encodeFormAction
    undefined, // nonce
    options && options.temporaryReferences
      ? options.temporaryReferences
      : undefined,
    __DEV__ && options && options.findSourceMapURL
      ? options.findSourceMapURL
      : undefined,
    __DEV__ ? (options ? options.replayConsoleLogs !== false : true) : false,
  );
}
  1. options에서 필요한 값들을 추출
  2. createResponse 함수를 호출하여 Response 객체 생성
  • createResponse 함수
export function createResponse(
  bundlerConfig: SSRModuleMap,
  moduleLoading: ModuleLoading,
  callServer: void | CallServerCallback,
  encodeFormAction: void | EncodeFormActionCallback,
  nonce: void | string,
  temporaryReferences: void | TemporaryReferenceSet,
  findSourceMapURL: void | FindSourceMapURLCallback,
  replayConsole: boolean,
): Response {
  return new ResponseInstance(
    bundlerConfig,
    moduleLoading,
    callServer,
    encodeFormAction,
    nonce,
    temporaryReferences,
    findSourceMapURL,
    replayConsole,
  );
}
  1. 여러 파라미터를 받아 ResponseInstance 생성
  2. 이 함수는 ResponseInstance 생성자를 호출하는 래퍼 함수이다.
  • ResponseInstance 생성자
function ResponseInstance(
  this: $FlowFixMe,
  bundlerConfig: SSRModuleMap,
  moduleLoading: ModuleLoading,
  callServer: void | CallServerCallback,
  encodeFormAction: void | EncodeFormActionCallback,
  nonce: void | string,
  temporaryReferences: void | TemporaryReferenceSet,
  findSourceMapURL: void | FindSourceMapURLCallback,
  replayConsole: boolean,
) {
  const chunks: Map<number, SomeChunk<any>> = new Map();
  this._bundlerConfig = bundlerConfig; // 번들러 설정
  this._moduleLoading = moduleLoading; // 모듈 로딩 관련
  this._callServer = callServer !== undefined ? callServer : missingCall; // 서버 호출
  this._encodeFormAction = encodeFormAction; // 폼 액션 인코딩
  this._nonce = nonce; // 보안을 위한 난수 값
  this._chunks = chunks; // 청크 데이터 저장
  this._stringDecoder = createStringDecoder(); // 문자열 디코더
  this._fromJSON = (null: any);
  // 데이터 파싱을 위한 상태 변수들
  this._rowState = 0;
  this._rowID = 0;
  this._rowTag = 0;
  this._rowLength = 0;
  this._buffer = []; // 임시 데이터 저장 버퍼
  this._tempRefs = temporaryReferences; // 임시 참조 저장소
  this._fromJSON = createFromJSONCallback(this); // JSON 파싱 콜백
}
  1. Map을 생성하여 청크 데이터를 저장
  2. 여러 내부 속성들을 초기화

스트리밍된 JSON 데이터 파싱

createFromJSONCallback 함수는 JSON.parse()의 reviver 함수를 반환한다.

이 reviver 함수는 스트리밍된 JSON 데이터를 파싱하는 데 사용된다.

function createFromJSONCallback(response: Response) {
  return function (key: string, value: JSONValue) {
    if (typeof value === 'string') {
      return parseModelString(response, this, key, value);
    }
    if (typeof value === 'object' && value !== null) {
      return parseModelTuple(response, value);
    }
    return value;
  };
}
  1. JSON 데이터가 스트리밍 방식으로 서버에서 클라이언트로 전송된다.
  2. 클라이언트에서 JSON.parse()를 사용하여 이 데이터를 파싱한다.
  3. 파싱 과정에서 reviver 함수가 각 key, value 쌍에 대해 호출된다.
  4. reviver 함수 내부에서 parseModelString를 호출하여 특별한 처리가 필요한 데이터(서버 컴포넌트, 지연 로딩 컴포넌트 등)를 변환한다.
function parseModelString(
  response: Response,
  parentObject: Object,
  key: string,
  value: string,
): any {
  if (value[0] === '$') {
    if (value === '$') {
      // 리액트 앨리먼트 처리
      if (initializingHandler !== null && key === '0') {
        initializingHandler = {
          parent: initializingHandler,
          chunk: null,
          value: null,
          deps: 0,
          errored: false,
        };
      }
      return REACT_ELEMENT_TYPE;
    }
    switch (value[1]) {
      case '$': {
        // 이스케이프된 문자열
        return value.slice(1);
      }
      case 'L': {
        // 지연 로딩 처리
        const id = parseInt(value.slice(2), 16);
        const chunk = getChunk(response, id);
        return createLazyChunkWrapper(chunk);
      }
      case '@': {
        // Promise 처리
        if (value.length === 2) {
          // 아직 결과가 없음을 나타냄
          return new Promise(() => {});
        }
        const id = parseInt(value.slice(2), 16);
        const chunk = getChunk(response, id);
        return chunk;
      }
      case 'S': {
        // Symbol 처리
        return Symbol.for(value.slice(2));
      }
      case 'F': {
        // 서버 참조(서버에서 실행되는 함수나 데이터)
        const ref = value.slice(2);
        return getOutlinedModel(
          response,
          ref,
          parentObject,
          key,
          createServerReferenceProxy,
        );
      }
      case 'T': {
        // 임시 참조(서버와 클라이언트 간의 데이터 동기화)
        const reference = '$' + value.slice(2);
        const temporaryReferences = response._tempRefs;
        if (temporaryReferences == null) {
          throw new Error(
            'Missing a temporary reference set but the RSC response returned a temporary reference. ' +
              'Pass a temporaryReference option with the set that was used with the reply.',
          );
        }
        return readTemporaryReference(temporaryReferences, reference);
      }
      case 'Q': {
        // Map 처리
        const ref = value.slice(2);
        return getOutlinedModel(response, ref, parentObject, key, createMap);
      }
      case 'W': {
        // Set 처리
        const ref = value.slice(2);
        return getOutlinedModel(response, ref, parentObject, key, createSet);
      }
      case 'B': {
        // Blob 처리
        if (enableBinaryFlight) {
          const ref = value.slice(2);
          return getOutlinedModel(response, ref, parentObject, key, createBlob);
        }
        return undefined;
      }
      case 'K': {
        // FormData 처리
        const ref = value.slice(2);
        return getOutlinedModel(
          response,
          ref,
          parentObject,
          key,
          createFormData,
        );
      }
      case 'i': {
        // Iterator 처리
        const ref = value.slice(2);
        return getOutlinedModel(
          response,
          ref,
          parentObject,
          key,
          extractIterator,
        );
      }
      case 'I': {
        // $Infinity
        return Infinity;
      }
      case '-': {
        // 특수한 숫자 값 처리($-0 or $-Infinity)
        if (value === '$-0') {
          return -0;
        } else {
          return -Infinity;
        }
      }
      case 'N': {
        // NaN 처리
        return NaN;
      }
      case 'u': {
        // undefined 처리
        return undefined;
      }
      case 'D': {
        // Date 처리
        return new Date(Date.parse(value.slice(2)));
      }
      case 'n': {
        // BigInt 처리
        return BigInt(value.slice(2));
      }
      case 'E': {
        // Fallthrough
      }
      case 'Y': {
        // Fallthrough
      }
      default: {
        // 기본 모델 처리
        const ref = value.slice(1);
        return getOutlinedModel(response, ref, parentObject, key, createModel);
      }
    }
  }
  return value;
}

이전에 봤던 RSC Payload에서 1:"$Sreact.suspense" 이런 값이 있었는데,
이 값도 case "S"에 포함되어 Symbol.for("react.suspense")로 역직렬화가 된다.


서버로부터 받은 응답 스트림을 읽고 처리

startReadingFromStream 함수는 비동기적으로 동작하며, 대량의 데이터를 효율적으로 처리할 수 있게 해준다.

서버 컴포넌트의 데이터를 점진적으로 받아 처리함으로써 초기 데이터가 준비되는 즉시 보여줄 수 있어 사용자 경험을 개선하고, 한번에 모든 데이터를 메모리에 로드하지 않고 청크 단위로 처리하여 메모리 사용을 최적화한다.

function startReadingFromStream(
  response: FlightResponse, // 처리된 데이터를 저장하는 곳
  stream: ReadableStream, // 서버로부터 받은 데이터 스트림
): void {
  const reader = stream.getReader(); // 스트림에서 데이터를 읽기 위한 리더 객체 생성
  function progress({
    done, // 스트림을 다 읽었는지 체크하는 변수
    value, // 스트림에서 읽은 데이터 청크
  }: {
    done: boolean,
    value: ?any,
    ...
  }): void | Promise<void> {
    if (done) { // 스트림 읽기가 끝났다면
      close(response); // 함수 종료
      return;
    }
    const buffer: Uint8Array = (value: any);
    processBinaryChunk(response, buffer); // 데이터 청크 처리
    // 다음 청크가 준비되면 progress 함수 호출
    return reader.read().then(progress).catch(error); 
  }
  function error(e: any) {
    // 스트림 읽기 중 발생한 오류 처리
    reportGlobalError(response, e);
  }
  // 스트림 읽기 시작, 그 결과를 progress 함수로 전달
  reader.read().then(progress).catch(error);
}
  1. 스크림 리더 생성
  2. 스트림에서 데이터 읽기 시작
  3. 각 데이터 청크를 받을 때마다 progress 함수 호출
  4. progress 함수는 데이터를 처리하고 다음 청크를 읽는다.
  5. 모든 데이터를 읽었거나 오류가 발생하면 적절히 처리

각 청크 처리하기

export function processBinaryChunk(
  response: Response, // 처리 중인 응답 객체
  chunk: Uint8Array, // 처리할 바이너리 데이터
): void {
  // 여러 변수 상태 초기화
  let i = 0;
  let rowState = response._rowState;
  let rowID = response._rowID;
  let rowTag = response._rowTag;
  let rowLength = response._rowLength;
  const buffer = response._buffer;
  const chunkLength = chunk.length;
  while (i < chunkLength) { // 청크의 각 바이트를 순회하며 처리
    let lastIdx = -1;
    switch (rowState) {
      // rowState 처리 로직(생략)
    const offset = chunk.byteOffset + i;
    if (lastIdx > -1) {
      // 완성된 행 처리
      const length = lastIdx - i;
      const lastChunk = new Uint8Array(chunk.buffer, offset, length);
      processFullBinaryRow(response, rowID, rowTag, buffer, lastChunk);
      i = lastIdx;
      if (rowState === ROW_CHUNK_BY_NEWLINE) {
        i++;
      }
      rowState = ROW_ID;
      rowTag = 0;
      rowID = 0;
      rowLength = 0;
      buffer.length = 0;
    } else {
      // 다음 청크와 함께 처리 할 수 있도록 미완성 행을 버퍼에 저장
      const length = chunk.byteLength - i;
      const remainingSlice = new Uint8Array(chunk.buffer, offset, length);
      buffer.push(remainingSlice);
      rowLength -= remainingSlice.byteLength;
      break;
    }
  }
  // 모든 처리가 끝난 후 응답 객체의 상태 업데이트
  response._rowState = rowState;
  response._rowID = rowID;
  response._rowTag = rowTag;
  response._rowLength = rowLength;
}

이 함수는 바이너리 형식으로 인코딩된 서버 컴포넌트 데이터를 효율적으로 파싱하는 역할을 한다.

각 행은 ID, 태그, 길이, 데이터로 구성되며 이러한 구조를 파싱하여 적절히 처리한다.

완성된 행 처리

function processFullBinaryRow(
  response: Response,
  id: number,
  tag: number,
  buffer: Array<Uint8Array>,
  chunk: Uint8Array,
): void {
  if (enableBinaryFlight) {
    switch (tag) {
     // ArrayBuffer, Int8Array 등등 처리(생략)
  }

  // 문자열 처리
  const stringDecoder = response._stringDecoder;
  let row = '';
  for (let i = 0; i < buffer.length; i++) {
    row += readPartialStringChunk(stringDecoder, buffer[i]);
  }
  row += readFinalStringChunk(stringDecoder, chunk);
  processFullStringRow(response, id, tag, row); // 디코딩된 문자열 전달
}
  1. 바이너리 데이터를 적절한 JavaScript 객체(TypedArray 등)로 변환
  2. 바이너리 데이터를 문자열로 디코딩
  3. 처리된 데이터를 응답 객체에 연결(processFullStringRow 함수 내부)

이롤 통해 서버에서 전송된 다양한 형식의 데이터를 클라이언트에서 적절히 처리할 수 있다.

  • processFullStringRow 함수
function processFullStringRow(
  response: Response, // 처리 중인 응답 객체
  id: number, // 행의 ID
  tag: number, // 행의 타입을 나타내는 태그(ASCII 코드로 표현)
  row: string, // 처리할 문자열 데이터
): void {
  switch (tag) {
    case 73 /* "I" */: {
      // 모듈
      resolveModule(response, id, row);
      return;
    }
    case 72 /* "H" */: {
      // 힌트
      const code: HintCode = (row[0]: any);
      resolveHint(response, code, row.slice(1));
      return;
    }
    case 84 /* "T" */: {
      // 텍스트
      resolveText(response, id, row);
      return;
    }
    // 에러, 디버그 정보, 개발/프로덕션 버전 불일치, 스트림 관련, Postpone 등을 처리하는 로직(생략)
    default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ {
      // JSON 데이터 처리
      resolveModel(response, id, row);
      return;
    }
  }
}

대부분의 청크 문자열은 모델 청크로 취급되는데 특별한 경우(모듈, 힌트 등)에는 다르게 처리된다.

  • resolveModel ➔ resolveModelChunk 함수
function resolveModel(
  response: Response,
  id: number,
  model: UninitializedModel,
): void {
  const chunks = response._chunks; // 청크 가져오기
  const chunk = chunks.get(id); // 주어진 id에 해당하는 청크가 있는지 체크
  if (!chunk) {
    // 청크가 없는 경우
    // 새로운 해결된 모델 청크 생성 후 저장
    chunks.set(id, createResolvedModelChunk(response, model));
  } else {
    // 기존 청크를 새로운 모델 데이터로 업데이트
    resolveModelChunk(chunk, model);
  }
}

export const enableFlightReadableStream = true;
function resolveModelChunk<T>(
  chunk: SomeChunk<T>,
  value: UninitializedModel,
): void {
  if (chunk.status !== PENDING) {
    // 청크 상태가 PENDING이 아니면 이미 처리된 청크로 간주
    if (enableFlightReadableStream) {
      // enableFlightReadableStream이 활성화되어 있는 경우
      // 추가 데이터를 스트림 청크로 처리
      const streamChunk: InitializedStreamChunk<any> = (chunk: any);
      const controller = streamChunk.reason;
      controller.enqueueModel(value); // 내부 데이터 큐에 추가
    }
    return;
  }
    // 리스너 및 상태 업데이트
  const resolveListeners = chunk.value;
  const rejectListeners = chunk.reason;
  const resolvedChunk: ResolvedModelChunk<T> = (chunk: any);
  resolvedChunk.status = RESOLVED_MODEL;
  resolvedChunk.value = value;
  if (resolveListeners !== null) {
    initializeModelChunk(resolvedChunk);
    wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
  }
}

이 과정들을 통해 비동기적으로 도착하는 컴포넌트 데이터를 효율적으로 처리하고 관련된 부분에 상태 변화를 알릴 수 있다.


서버에서 전송된 React 컴포넌트 트리의 루트 추출

export function getRoot<T>(response: Response): Thenable<T> {
  const chunk = getChunk(response, 0); // 첫 번째 청크 가져오기
  return (chunk: any);
}

function getChunk(response: Response, id: number): SomeChunk<any> {
  const chunks = response._chunks; // 청크 맵 가져오기
  let chunk = chunks.get(id); // 청크 조회
  if (!chunk) {
    // 청크가 존재하지 않는 경우
    // 대기 중인 청크 생성, 추가
    chunk = createPendingChunk(response);
    chunks.set(id, chunk);
  }
  return chunk;
}

getRoot 함수는 서버 컴포넌트 응답의 루트 청크를 추출하여 클라이언트가 서버에서 생성된 컴포넌트 트리를 처리하기 시작할 수 있게 해준다.(반환된 청크는 Thenable 객체로 비동기적으로 해결될 수 있다)

getChunk 함수는 청크의 존재 여부에 관계없이 항상 청크 객체를 반환한다. 이 매커니즘을 통해 비동기적으로 도착하는 데이터를 효율적으로 관리할 수 있다.

  • 대기 중인 청크 생성
function createPendingChunk<T>(response: Response): PendingChunk<T> {
  // 청크 생성(PENDING 상태, value, reson은 null로 설정)
  return new Chunk(PENDING, null, null, response);
}

// 상태, 값, 이유, 응답 객체를 속성을 가지는 객체를 생성하는 함수이다.
function Chunk(status: any, value: any, reason: any, response: Response) {
  this.status = status;
  this.value = value;
  this.reason = reason;
  this._response = response;
}

// prototype을 Promise 기반으로 설정하여 Promise와 유사하게 동작할 수 있게 설정
Chunk.prototype = (Object.create(Promise.prototype): any);
Chunk.prototype.then = function <T>(
  this: SomeChunk<T>,
  resolve: (value: T) => mixed,
  reject?: (reason: mixed) => mixed,
) {
  const chunk: SomeChunk<T> = this;
  switch (chunk.status) {
    // 청크 상태 처리
    case RESOLVED_MODEL:
      // 서버 컴포넌트의 렌더링 결과가 준비되었을 때
      initializeModelChunk(chunk);
      break;
    case RESOLVED_MODULE:
      // 클라이언트 컴포넌트에 대한 참조 정보가 준비되었을 때
      initializeModuleChunk(chunk);
      break;
  }
  switch (chunk.status) {
    case INITIALIZED:
      // 청크 데이터가 완전히 로드되고 초기화되었을 경우(위 상태 처리를 거친 경우)
      resolve(chunk.value);
      break;
    case PENDING: // 아직 데이터가 도착하지 않은 경우
    case BLOCKED: // 아직 완전히 로드되지 않은 경우
      // 나중에 청크가 해결되거나 거부될 때 콜백을 호출하기 위해 배열에 추가
      if (resolve) {
        if (chunk.value === null) {
          chunk.value = ([]: Array<(T) => mixed>);
        }
        chunk.value.push(resolve);
      }
      if (reject) {
        if (chunk.reason === null) {
          chunk.reason = ([]: Array<(mixed) => mixed>);
        }
        chunk.reason.push(reject);
      }
      break;
    default:
      // 오류 상태인 경우 reject 호출
      if (reject) {
        reject(chunk.reason);
      }
      break;
  }
};

청크 객체는 Promise와 유사한 인터페이스를 제공하면서도 서버 컴포넌트의 특수한 요구사항을 처리할 수 있는 추가적인 기능을 가지고 있다.


정리하기

  1. 클라이언트에서의 데이터 요청 및 수신

    • createFromFetch 함수를 사용하여 서버에 데이터를 요청한다.
    • 서버로부터 받은 응답을 처리하기 위해 createResponseFromOptions 함수를 호출한다.
  2. FlightResponse 객체 생성

    • createResponseFromOptions 함수를 통해 FlightResponse 객체를 생성한다.
    • 이 객체는 서버 응답을 처리하고 저장하며 스트리밍 데이터를 점진적으로 처리한다.
  3. 스트리밍된 데이터 읽기 시작

    • startReadingFromStream 함수를 사용하여 서버로부터 스트리밍되는 데이터를 읽기 시작한다.
    • 이 함수는 데이터를 청크 단위로 비동기적으로 읽는다.
  4. 바이너리 청크 처리

    • processBinaryChunk 함수를 통해 각 바이너리 청크를 처리한다.
    • 이 함수는 청크를 행 단위로 분석하고 처리한다.
    • 이 과정에서 불완전한 청크를 버퍼링하여 다음 청크와 결합하는 역할도 수행한다.
  5. 완성된 행 처리

    • processFullBinaryRow 함수를 사용하여 완성된 바이너리 행을 처리한다.
    • 필요한 경우 바이너리 데이터를 문자열로 변환한다.
  6. 문자열 행 처리

    • processFullStringRow 함수를 통해 문자열로 변환된 행을 처리한다.
    • 행의 태그에 따라 다양한 유형을 데이터(모듈, 힌트, 텍스트 등)를 처리한다.
  7. 모델 데이터 처리

    • resolveModel 함수를 사용하여 모델 데이터(React 컴포넌트 트리의 일부)를 처리한다.
    • 필요한 경우 새로운 청크를 생성하거나 기존 청크를 업데이트한다.
  8. 청크 상태 관리

    • resolveModelChunk 함수를 통해 청크의 상태를 관리하고 업데이트한다.
    • 청크의 상태에 따라 적절한 처리를 수행한다.(초기화, 리스너 호출 등)
  9. 청크 가져오기

    • 청크가 존재하는 경우 청크를 반환하고, 존재하지 않는 경우 대기 중인 청크를 생성한다.
  10. 루트 컴포넌트 추출

    • getRoot 함수를 사용하여 서버에서 전송된 React 컴포넌트 트리의 루트를 추출한다.
    • 이 함수는 Thenable 객체를 반환하여 비동기적으로 루트 컴포넌트에 접근할 수 있게 한다.

서버 컴포넌트가 동작하는 전체 플로우

<서버>
1. renderToPipeableStream 함수를 통해 서버에서 React 트리를 렌더링한다.
2. 렌더링 된 결과를 가지고 RSC Payload라는 형태의 데이터 형식으로 직렬화한다.
3. RSC Payload와 함께 초기 HTML(Shell)을 생성한다.
4. 생성된 HTML, RSC Payload, 클라이언트 컴포넌트에 대한 JavaScript 코드를 클라이언트로 스트리밍 방식으로 전송한다.

💡 RSC Payload
JSON과 유사한 특별한 바이너리 형식이고 점진적으로 스트리밍되어 UI를 동적으로 업데이트한다. 클라이언트에서 React 트리를 재구성하는 데 사용된다.

  • 서버 컴포넌트의 렌더링 결과
  • 동적 데이터
  • 클라이언트 컴포넌트에 전달될 props

💡 초기 HTML(Shell)
정적이며 상호작용이 불가능하지만 빠른 초기 렌더링(FCP)을 제공할 수 있다.

  • 페이지의 기본 구조
  • 레이아웃 컴포넌트
  • 클라이언트 컴포넌트의 플레이스 홀더
  • 메타 데이터(<head>, <meta> 등)

<클라이언트>
5. 클라이언트는 먼저 초기 HTML(Shell)을 받아 빠르게 표시한다.
6. 이후 서버로부터 스트리밍되는 RSC Payload를 받는다.
7. 받은 데이터를 처리하기 위해 FlightResponse 객체를 생성한다.
8. 스트리밍된 데이터를 청크 단위로 읽는다.
9. 각 바이너리 청크를 처리하여 문자열로 변환하고, 태그에 따라 다양한 유형의 데이터를 적절히 처리한다.
10. React 컴포넌트 트리의 일부인 모델 데이터를 처리한다.
11. 청크의 상태에 따라 적절한 처리를 한다.(초기화, 리스너 호출 등)
12. 서버에서 전송된 React 컴포넌트 트리의 루트를 추출한다.
13. 추출된 루트 컴포넌트를 기반으로 클라이언트에서 React 트리를 재구성한다.
14. 클라이언트 컴포넌트는 필요에 따라 Hydration 과정을 수행한다.


🙃 도움이 되었던 자료들

React Server Components - React 공식 문서(v18.3.1)
How do React Server Components(RSC) work internally in React?
Understanding React Server Components
AsyncLocalStorage - Node.js 공식 문서
WritableStream
Node.js Stream 당신이 알아야할 모든 것 2편
2024-01-27 Streaming SSR로 비동기 렌더링 하기
2024-02-23 [A는 B] 1. RSC란?
[Deep Dive] React Server Component를 정확히 아시나요? - 1편
How React server components work: an in-depth guide
(번역) React 18이 애플리케이션 성능을 개선하는 방법
React 서버 컴포넌트 작동원리를 아주 쉽게 알아보자
자바스크립트에서 데이터 스트림 읽기 (ReadableStream)
Nextjs 앱라우터 이해해보기

profile
🏁

0개의 댓글