로딩 상태 처리

로딩 상태 처리 (Handling Loading State)

AI SDK RSC에서 로딩 상태를 처리하는 세 가지 방법을 설명하는 문서예요.

출처: 문서

본문

경고: AI SDK RSC는 현재 실험 단계(experimental)예요. 프로덕션에서는 AI SDK UI 사용을 권장해요. RSC에서 UI로 마이그레이션하는 방법은 마이그레이션 가이드를 참고하세요.

언어 모델의 응답은 종종 완료되는 데 시간이 오래 걸릴 수 있으므로, 사용자에게 로딩 상태를 보여주는 것이 매우 중요해요. 이는 시스템이 사용자의 요청을 처리 중이라는 시각적 피드백을 제공하고 긍정적인 사용자 경험을 유지하는 데 도움을 줘요.

AI SDK RSC로 로딩 상태를 처리하는 방법은 세 가지가 있어요:

  • 전통적인 Next.js 애플리케이션에서 하는 것처럼 로딩 상태를 관리하기. 클라이언트에서 로딩 상태 변수를 설정하고 응답을 받으면 업데이트하는 방식이에요.
  • 서버에서 클라이언트로 로딩 상태를 스트리밍하기. 이 접근 방식은 더 세밀한 수준에서 로딩 상태를 추적하고 사용자에게 더 상세한 피드백을 제공할 수 있게 해 줘요.
  • 서버에서 클라이언트로 로딩 컴포넌트를 스트리밍하기. 이 접근 방식은 모델의 응답을 기다리는 동안 React Server Component를 클라이언트로 스트리밍할 수 있게 해 줘요.

클라이언트에서 로딩 상태 처리 (Handling Loading State on the Client)

클라이언트 (Client)

폼이 제출될 때 generateResponse 함수를 호출하는 간단한 Next.js 페이지를 만들어볼게요. 이 함수는 사용자의 프롬프트(input)를 받아 응답(response)을 생성해요. 로딩 상태를 처리하려면 loading 상태 변수를 사용해요. 폼이 제출되면 loading을 true로 설정하고, 응답을 받으면 다시 false로 설정해요. 응답이 스트리밍되는 동안 입력 필드는 비활성화돼요.

'use client';

import { useState } from 'react';
import { generateResponse } from './actions';
import { readStreamableValue } from '@ai-sdk/rsc';

// Force the page to be dynamic and allow streaming responses up to 30 seconds
export const maxDuration = 30;

export default function Home() {
  const [input, setInput] = useState<string>('');
  const [generation, setGeneration] = useState<string>('');
  const [loading, setLoading] = useState<boolean>(false);

  return (
    <div>
      <div>{generation}</div>
      <form
        onSubmit={async e => {
          e.preventDefault();
          setLoading(true);
          const response = await generateResponse(input);

          let textContent = '';

          for await (const delta of readStreamableValue(response)) {
            textContent = `${textContent}${delta}`;
            setGeneration(textContent);
          }
          setInput('');
          setLoading(false);
        }}
      >
        <input
          type="text"
          value={input}
          disabled={loading}
          className="disabled:opacity-50"
          onChange={event => {
            setInput(event.target.value);
          }}
        />
        <button>Send Message</button>
      </form>
    </div>
  );
}

서버 (Server)

이제 generateResponse 함수를 구현할게요. 입력에 대한 응답을 생성하려면 streamText 함수를 사용해요.

'use server';

import { streamText } from 'ai';
__PROVIDER_IMPORT__;
import { createStreamableValue } from '@ai-sdk/rsc';

export async function generateResponse(prompt: string) {
  const stream = createStreamableValue();

  (async () => {
    const { textStream } = streamText({
      model: __MODEL__,
      prompt,
    });

    for await (const text of textStream) {
      stream.update(text);
    }

    stream.done();
  })();

  return stream.value;
}

서버에서 로딩 상태 스트리밍 (Streaming Loading State from the Server)

로딩 상태를 더 세밀한 수준에서 추적하고 싶다면, 커스텀 변수를 담을 새 streamable 값을 만들고 이를 프론트엔드에서 읽으면 돼요. 로딩 상태 추적을 위한 새 streamable 값을 만들도록 예제를 업데이트해볼게요:

서버 (Server)

'use server';

import { streamText } from 'ai';
__PROVIDER_IMPORT__;
import { createStreamableValue } from '@ai-sdk/rsc';

export async function generateResponse(prompt: string) {
  const stream = createStreamableValue();
  const loadingState = createStreamableValue({ loading: true });

  (async () => {
    const { textStream } = streamText({
      model: __MODEL__,
      prompt,
    });

    for await (const text of textStream) {
      stream.update(text);
    }

    stream.done();
    loadingState.done({ loading: false });
  })();

  return { response: stream.value, loadingState: loadingState.value };
}

클라이언트 (Client)

'use client';

import { useState } from 'react';
import { generateResponse } from './actions';
import { readStreamableValue } from '@ai-sdk/rsc';

// Force the page to be dynamic and allow streaming responses up to 30 seconds
export const maxDuration = 30;

export default function Home() {
  const [input, setInput] = useState<string>('');
  const [generation, setGeneration] = useState<string>('');
  const [loading, setLoading] = useState<boolean>(false);

  return (
    <div>
      <div>{generation}</div>
      <form
        onSubmit={async e => {
          e.preventDefault();
          setLoading(true);
          const { response, loadingState } = await generateResponse(input);

          let textContent = '';

          for await (const responseDelta of readStreamableValue(response)) {
            textContent = `${textContent}${responseDelta}`;
            setGeneration(textContent);
          }
          for await (const loadingDelta of readStreamableValue(loadingState)) {
            if (loadingDelta) {
              setLoading(loadingDelta.loading);
            }
          }
          setInput('');
          setLoading(false);
        }}
      >
        <input
          type="text"
          value={input}
          disabled={loading}
          className="disabled:opacity-50"
          onChange={event => {
            setInput(event.target.value);
          }}
        />
        <button>Send Message</button>
      </form>
    </div>
  );
}

이렇게 하면 생성 과정에 대해 사용자에게 더 상세한 피드백을 제공할 수 있어요.

streamUI로 로딩 컴포넌트 스트리밍 (Streaming Loading Components with streamUI)

streamUI 함수를 사용한다면, 로딩 상태를 React 컴포넌트 형태로 클라이언트에 스트리밍할 수 있어요. streamUI는 JavaScript 제너레이터 함수 사용을 지원하며, 이를 통해 다른 블로킹 작업이 완료되는 동안 어떤 값(이 경우 React 컴포넌트)을 내보낼(yield) 수 있어요.

서버 (Server)

'use server';

import { openai } from '@ai-sdk/openai';
import { streamUI } from '@ai-sdk/rsc';

export async function generateResponse(prompt: string) {
  const result = await streamUI({
    model: openai('gpt-6-astra'),
    prompt,
    text: async function* ({ content }) {
      yield <div>loading...</div>;
      return <div>{content}</div>;
    },
  });

  return result.value;
}

참고: streamUI 함수에서 React 컴포넌트를 정의하고 있으므로 파일을 .ts에서 .tsx로 바꾸는 것을 잊지 마세요.

클라이언트 (Client)

'use client';

import { useState } from 'react';
import { generateResponse } from './actions';
import { readStreamableValue } from '@ai-sdk/rsc';

// Force the page to be dynamic and allow streaming responses up to 30 seconds
export const maxDuration = 30;

export default function Home() {
  const [input, setInput] = useState<string>('');
  const [generation, setGeneration] = useState<React.ReactNode>();

  return (
    <div>
      <div>{generation}</div>
      <form
        onSubmit={async e => {
          e.preventDefault();
          const result = await generateResponse(input);
          setGeneration(result);
          setInput('');
        }}
      >
        <input
          type="text"
          value={input}
          onChange={event => {
            setInput(event.target.value);
          }}
        />
        <button>Send Message</button>
      </form>
    </div>
  );
}

더 알아보기 (Learn more)