>웹 프론트엔드 >JS 튜토리얼 >Next.js에서 Cloudflare Workers와 함께 React Edge까지: 해방 이야기

Next.js에서 Cloudflare Workers와 함께 React Edge까지: 해방 이야기

Susan Sarandon
Susan Sarandon원래의
2024-11-20 14:26:17918검색

빠른 색인

  • 마지막 지푸라기
  • Cloudflare의 대안 ?
  • React Edge: 모든 개발자의 고통(또는 거의)에서 탄생한 React 프레임워크
    • 형식 RPC의 마법
    • 사용의 힘Fetch: 마법이 일어나는 곳
  • 쓸모없는 Fetch: 완전한 무기고
    • RPC: 클라이언트-서버 통신의 기술
    • 합리적인 i18n 시스템
    • "작동하는" JWT 인증
    • 공유스토어
    • 우아한 라우팅
    • 에지 캐시를 사용한 분산 캐시
  • 링크: 미리 생각하는 구성요소
  • app.useContext: 엣지로 향하는 포털
  • app.useUrlState: URL과 동기화된 상태
  • app.useStorageState: 지속 상태
  • app.useDebounce: 주파수 제어
  • app.useDistinct: 중복이 없는 상태
  • React Edge CLI: 손끝에서 느껴지는 힘
  • 결론

마지막 밀짚

모든 것은 Vercel 송장으로 시작되었습니다. 아니요, 실제로는 훨씬 일찍 시작되었습니다. 작은 좌절감이 쌓였습니다. DDoS 보호, 더 자세한 로그, 괜찮은 방화벽, 대기열 구축 등과 같은 기본 기능에 대한 비용을 지불해야 하는 필요성. 점점 더 비용이 많이 드는 공급업체 종속에 갇힌 느낌.

"그리고 가장 나쁜 점은 페이지 라우터를 사용하는 애플리케이션의 서버에서 귀중한 SEO 헤더의 렌더링이 중단되었다는 것입니다. 모든 개발자에게 진정한 골칫거리입니다! ?"

그러나 모든 것을 다시 생각하게 만든 것은 Next.js가 향하는 방향이었습니다. 클라이언트 사용, 서버 사용 지시문이 도입되면서 이론적으로는 개발이 단순화되지만 실제로는 관리하기가 더 복잡해졌습니다. 마치 PHP 시절로 돌아가서 파일이 실행되어야 하는 위치를 지시하는 지시어로 파일을 표시하는 것과 같았습니다.

여기서 끝나지 않습니다. App Router - 흥미로운 아이디어지만 Next.js 내에서 거의 완전히 새로운 프레임워크를 생성하는 방식으로 구현되었습니다. 갑자기 동일한 작업을 수행하는 완전히 다른 두 가지 방법('기존'과 '새')이 나타났습니다. 미묘하게 다른 동작과 숨겨진 함정이 있었습니다.

Cloudflare의 대안?

그때 문득 생각났습니다. 에지에서 실행되는 작업자, 스토리지용 R2, 분산 데이터용 KV와 함께 Cloudflare의 놀라운 인프라를 활용해 보는 것은 어떨까요? 물론 놀라운 DDoS 보호, 글로벌 CDN, 방화벽도 함께 제공됩니다. , 페이지 규칙 및 라우팅 등 Cloudflare가 제공하는 모든 기능을 제공합니다.

가장 좋은 점은 사용한 만큼만 비용을 지불하는 공정한 가격 모델입니다.

리액트 엣지는 이렇게 탄생했습니다. 바퀴를 재발명하는 것을 목표로 하지 않고 진정으로 단순하고 현대적인 개발 경험을 제공하는 프레임워크입니다.

React Edge: 모든 개발자의 고통(또는 거의)에서 탄생한 React 프레임워크

React Edge 개발을 시작했을 때 저는 이해하기 쉬운 프레임워크를 만드는 것이라는 명확한 목표를 가지고 있었습니다. 더 이상 혼란스러운 지시문으로 씨름할 필요가 없고, 기본 기능에 대해 더 이상 엄청난 비용을 지불할 필요가 없으며, 가장 중요한 것은 더 이상 클라이언트/서버 분리로 인한 인위적인 복잡성을 처리할 필요가 없다는 것입니다. 저는 단순함을 희생하지 않으면서 성능을 제공하는 프레임워크인 속도를 원했습니다. React의 API에 대한 지식과 JavaScript 및 Golang 개발자로서 수년간의 경험을 활용하여 스트림 및 멀티플렉싱을 처리하여 렌더링 및 데이터 관리를 최적화하는 방법을 정확히 알고 있었습니다.

강력한 인프라와 글로벌 입지를 갖춘 Cloudflare Workers는 이러한 가능성을 탐색할 수 있는 완벽한 환경을 제공했습니다. 저는 진정한 하이브리드를 원했고 이러한 도구와 경험의 조합은 현대적이고 효율적인 솔루션으로 실제 문제를 해결하는 프레임워크인 React Edge에 생명을 불어넣었습니다.

React Edge는 React 개발에 혁신적인 접근 방식을 도입합니다. 완전한 유형 안전성과 제로 구성을 통해 서버에서 클래스를 작성하고 클라이언트에서 직접 호출한다고 상상해 보십시오. 태그나 접두사에 의한 무효화를 허용하여 "작동하는" 분산 캐싱 시스템을 상상해 보십시오. 서버와 클라이언트 간에 상태를 원활하고 안전하게 공유한다고 상상해 보세요. 단순화된 인증, 효율적인 국제화 시스템, CLI 등을 추가하세요.

RPC 통신은 거의 마술처럼 느껴집니다. 클래스에서 메소드를 작성하고 클라이언트에서 마치 로컬인 것처럼 호출합니다. 지능형 다중화 시스템은 여러 구성 요소가 동일한 호출을 하더라도 하나의 요청만 서버로 전송되도록 보장합니다. 임시 캐싱은 불필요한 반복 요청을 방지하며 서버와 클라이언트 모두에서 원활하게 작동합니다.

가장 강력한 기능 중 하나는 데이터 가져오기 환경을 통합하는 app.useFetch 후크입니다. 서버에서는 SSR 중에 데이터를 미리 로드합니다. 클라이언트에서는 해당 데이터를 자동으로 수화하고 주문형 업데이트를 지원합니다. 자동 폴링 및 종속성 기반 반응성을 지원하므로 동적 인터페이스 생성이 그 어느 때보다 쉬워졌습니다.

하지만 그게 다가 아닙니다. 프레임워크는 강력한 라우팅 시스템(환상적인 Hono에서 영감을 얻었음), Cloudflare R2를 사용한 통합 자산 관리, HttpError 클래스를 통해 오류를 처리하는 우아한 방법을 제공합니다. 미들웨어는 공유 저장소를 통해 클라이언트에 쉽게 데이터를 보낼 수 있으며 보안을 위해 모든 것이 자동으로 난독화됩니다.

가장 인상적인 부분은? 프레임워크의 코드는 거의 모두 하이브리드입니다. "클라이언트" 버전과 "서버" 버전이 없습니다. 동일한 코드가 두 환경 모두에서 작동하며 상황에 자동으로 적응합니다. 클라이언트는 필요한 것만 수신하여 최종 번들을 극도로 최적화합니다.

그리고 금상첨화: 이 모든 것이 Cloudflare Workers 엣지 인프라에서 실행되어 합리적인 비용으로 뛰어난 성능을 제공합니다. 예상치 못한 청구서도 없고 값비싼 기업 요금제 뒤에 숨어 있는 기본 기능도 없습니다. 단지 진정으로 중요한 것, 즉 놀라운 응용 프로그램 구축에 집중할 수 있는 견고한 프레임워크일 뿐입니다. 또한 React Edge는 대기열, 내구성 있는 개체, KV 저장소 등을 포함한 Cloudflare의 생태계를 활용하여 애플리케이션을 위한 강력하고 확장 가능한 기반을 제공합니다.

Vite는 개발 환경, 테스트, 빌드 프로세스의 기반으로 사용되었습니다. 인상적인 속도와 현대적인 아키텍처를 갖춘 Vite는 민첩하고 효율적인 작업 흐름을 지원합니다. 개발을 가속화할 뿐만 아니라 빌드 프로세스를 최적화하여 빠르고 정확한 컴파일을 보장합니다. 의심할 여지없이 Vite는 React Edge를 위한 완벽한 선택이었습니다.

엣지 컴퓨팅 시대를 위한 React 개발 재고

클라이언트/서버 장벽을 걱정하지 않고 React 애플리케이션을 개발하는 것이 어떤 것인지 궁금한 적이 있습니까? 클라이언트 사용이나 서버 사용과 같은 수십 개의 지시문을 외우지 않고 계십니까? 더 좋은 점은 전체 입력 및 구성 없이 서버 기능을 마치 로컬인 것처럼 호출할 수 있다면 어떨까요?

React Edge를 사용하면 더 이상 다음을 수행할 필요가 없습니다.

  • 별도의 API 경로 생성
  • 로딩/오류 상태 수동 관리
  • 직접 디바운스 구현
  • 직렬화/역직렬화에 대한 고민
  • CORS 다루기
  • 클라이언트/서버 간 입력 관리
  • 인증 규칙을 수동으로 처리
  • 국제화 설정에 대한 어려움

가장 중요한 점은 이 모든 것이 클라이언트 사용 또는 서버 사용으로 표시하지 않고도 서버와 클라이언트 모두에서 원활하게 작동한다는 것입니다. 프레임워크는 컨텍스트에 따라 수행할 작업을 자동으로 알고 있습니다. 뛰어들어 볼까요?

형식화된 RPC의 마법

이렇게 할 수 있다고 상상해 보세요.

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};

이것을 Next.js/Vercel과 비교해보세요:

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configure CORS
  // Validate request
  // Handle errors
  // Serialize response
  // ...100 lines later...
}

// app/search/page.tsx
'use client';
import { useEffect, useState } from 'react';

export default const SearchPage = () => {
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState({});
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let timeout;
    const doSearch = async () => {
      setLoading(true);
      try {
        const res = await fetch('/api/search?' + new URLSearchParams({
          q: search,
          ...filters
        }));
        if (!res.ok) throw new Error('Search failed');
        setData(await res.json());
      } catch (err) {
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    timeout = setTimeout(doSearch, 300);
    return () => clearTimeout(timeout);
  }, [search, filters]);

  // ... rest of the component
}

사용의 힘Fetch: 마법이 일어나는 곳

데이터 가져오기에 대한 재고

React의 데이터 가져오기에 대해 알고 있는 모든 것을 잊어버리세요. React Edge의 app.useFetch 후크는 완전히 새롭고 강력한 접근 방식을 도입합니다. 다음과 같은 후크를 상상해 보세요.

  • SSR 중에 서버에 데이터를 미리 로드합니다.
  • 클라이언트의 데이터를 깜박임 없이 자동으로 수화합니다.
  • 클라이언트와 서버 간 전체 입력을 유지합니다.
  • 지능형 디바운스를 통해 반응성을 지원합니다.
  • 동일한 호출을 자동으로 다중화합니다.
  • 프로그래밍 방식 업데이트 및 설문조사를 활성화합니다.

실제로 살펴보겠습니다.

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};

멀티플렉싱의 마법

위의 예에는 지능형 멀티플렉싱이라는 강력한 기능이 숨겨져 있습니다. ctx.rpc.batch를 사용하면 React Edge는 호출을 그룹화할 뿐만 아니라 동일한 호출을 자동으로 중복 제거합니다.

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configure CORS
  // Validate request
  // Handle errors
  // Serialize response
  // ...100 lines later...
}

// app/search/page.tsx
'use client';
import { useEffect, useState } from 'react';

export default const SearchPage = () => {
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState({});
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let timeout;
    const doSearch = async () => {
      setLoading(true);
      try {
        const res = await fetch('/api/search?' + new URLSearchParams({
          q: search,
          ...filters
        }));
        if (!res.ok) throw new Error('Search failed');
        setData(await res.json());
      } catch (err) {
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    timeout = setTimeout(doSearch, 300);
    return () => clearTimeout(timeout);
  }, [search, filters]);

  // ... rest of the component
}

SSR 퍼펙트 하이드레이션

// First, define your API on the server
class PropertiesAPI extends Rpc {
  async searchProperties(filters: PropertyFilters) {
    const results = await this.db.properties.search(filters);
    // Automatic caching for 5 minutes
    return this.createResponse(results, {
      cache: { ttl: 300, tags: ['properties'] }
    });
  }

  async getPropertyDetails(ids: string[]) {
    return Promise.all(
      ids.map(id => this.db.properties.findById(id))
    );
  }
}

// Now, on the client, the magic happens
const PropertySearch = () => {
  const [filters, setFilters] = useState<PropertyFilters>({
    price: { min: 100000, max: 500000 },
    bedrooms: 2
  });

  // Reactive search with intelligent debounce
  const {
    data: searchResults,
    loading: searchLoading,
    error: searchError
  } = app.useFetch(
    async (ctx) => ctx.rpc.searchProperties(filters),
    {
      // Re-fetch when filters change
      deps: [filters],
      // Wait 300ms of "silence" before fetching
      depsDebounce: {
        filters: 300
      }
    }
  );

  // Fetch property details for the found results
  const {
    data: propertyDetails,
    loading: detailsLoading,
    fetch: refreshDetails
  } = app.useFetch(
    async (ctx) => {
      if (!searchResults?.length) return null;

      // This looks like multiple calls, but...
      return ctx.rpc.batch([
        // Everything is multiplexed into a single request!
        ...searchResults.map(result =>
          ctx.rpc.getPropertyDetails(result.id)
        )
      ]);
    },
    {
      // Refresh when searchResults change
      deps: [searchResults]
    }
  );

  // A beautiful and responsive interface
  return (
    <div>
      <FiltersPanel
        value={filters}
        onChange={setFilters}
        disabled={searchLoading}
      />

      {searchError && (
        <Alert status='error'>
          Search error: {searchError.message}
        </Alert>
      )}

      <PropertyGrid
        items={propertyDetails || []}
        loading={detailsLoading}
        onRefresh={() => refreshDetails()}
      />
    </div>
  );
};

쓸모없는 Fetch: 완전한 무기고

RPC: 클라이언트-서버 통신 기술

보안 및 캡슐화

React Edge의 RPC 시스템은 보안과 캡슐화를 염두에 두고 설계되었습니다. RPC 클래스의 모든 항목이 자동으로 클라이언트에 노출되는 것은 아닙니다.

const PropertyListingPage = () => {
  const { data } = app.useFetch(async (ctx) => {
    // Even if you make 100 identical calls...
    return ctx.rpc.batch([
      ctx.rpc.getProperty('123'),
      ctx.rpc.getProperty('123'), // same call
      ctx.rpc.getProperty('456'),
      ctx.rpc.getProperty('456'), // same call
    ]);
  });

  // Behind the scenes:
  // 1. The batch groups all calls into ONE single HTTP request.
  // 2. Identical calls are deduplicated automatically.
  // 3. Results are distributed correctly to each position in the array.
  // 4. Typing is maintained for each individual result!

  // Actual RPC calls:
  // 1. getProperty('123')
  // 2. getProperty('456')
  // Results are distributed correctly to all callers!
};

RPC API 계층

RPC의 가장 강력한 기능 중 하나는 API를 계층 구조로 구성하는 기능입니다.

One of the most impressive parts is how useFetch handles SSR:

const ProductPage = ({ productId }: Props) => {
  const { data, loaded, loading, error } = app.useFetch(
    async (ctx) => ctx.rpc.getProduct(productId),
    {
      // Fine-grained control over when to fetch
      shouldFetch: ({ worker, loaded }) => {
        // On the worker (SSR): always fetch
        if (worker) return true;
        // On the client: fetch only if no data is loaded
        return !loaded;
      }
    }
  );

  // On the server:
  // 1. `useFetch` makes the RPC call.
  // 2. Data is serialized and sent to the client.
  // 3. Component renders with the data.

  // On the client:
  // 1. Component hydrates with server data.
  // 2. No new call is made (shouldFetch returns false).
  // 3. If necessary, you can re-fetch with `data.fetch()`.

  return (
    <Suspense fallback={<ProductSkeleton />}>
      <ProductView
        product={data}
        loading={loading}
        error={error}
      />
    </Suspense>
  );
};

계층 구조의 이점

API를 계층 구조로 구성하면 다음과 같은 여러 이점이 있습니다.

  • 논리적 구성: 관련 기능을 직관적으로 그룹화합니다.
  • 자연스러운 네임스페이스: 명확한 경로(예: users.preferences.getTheme)로 이름 충돌을 피하세요.
  • 캡슐화: 각 수준에서 도우미 메서드를 비공개로 유지합니다.
  • 유지관리성: 각 하위클래스는 독립적으로 유지관리되고 테스트될 수 있습니다.
  • 전체 타이핑: TypeScript는 전체 계층 구조를 이해합니다.

React Edge의 RPC 시스템은 클라이언트-서버 통신을 너무나 자연스럽게 만들어 원격 호출을 하고 있다는 사실조차 잊어버릴 정도입니다. API를 계층 구조로 구성하는 기능을 사용하면 코드를 깔끔하고 안전하게 유지하면서 복잡한 구조를 구축할 수 있습니다.

합리적인 i18n 시스템

React Edge는 무거운 라이브러리에 의존하지 않고 가변 보간과 복잡한 서식을 지원하는 우아하고 유연한 국제화 시스템을 도입합니다.

class PaymentsAPI extends Rpc {
  // Properties are never exposed
  private stripe = new Stripe(process.env.STRIPE_KEY);

  // Methods starting with $ are private
  private async $validateCard(card: CardInfo) {
    return await this.stripe.cards.validate(card);
  }

  // Methods starting with _ are also private
  private async _processPayment(amount: number) {
    return await this.stripe.charges.create({ amount });
  }

  // This method is public and accessible via RPC
  async createPayment(orderData: OrderData) {
    // Internal validation using a private method
    const validCard = await this.$validateCard(orderData.card);
    if (!validCard) {
      throw new HttpError(400, 'Invalid card');
    }

    // Processing using another private method
    const payment = await this._processPayment(orderData.amount);
    return payment;
  }
}

// On the client:
const PaymentForm = () => {
  const { rpc } = app.useContext<App.Context>();

  // ✅ This works
  const handleSubmit = () => rpc.createPayment(data);

  // ❌ These do not work - private methods are not exposed
  const invalid1 = () => rpc.$validateCard(data);
  const invalid2 = () => rpc._processPayment(100);

  // ❌ This also does not work - properties are not exposed
  const invalid3 = () => rpc.stripe;
};

코드 사용법:

// Nested APIs for better organization
class UsersAPI extends Rpc {
  // Subclass to manage preferences
  preferences = new UserPreferencesAPI();
  // Subclass to manage notifications
  notifications = new UserNotificationsAPI();

  async getProfile(id: string) {
    return this.db.users.findById(id);
  }
}

class UserPreferencesAPI extends Rpc {
  async getTheme(userId: string) {
    return this.db.preferences.getTheme(userId);
  }

  async setTheme(userId: string, theme: Theme) {
    return this.db.preferences.setTheme(userId, theme);
  }
}

class UserNotificationsAPI extends Rpc {
  // Private methods remain private
  private async $sendPush(userId: string, message: string) {
    await this.pushService.send(userId, message);
  }

  async getSettings(userId: string) {
    return this.db.notifications.getSettings(userId);
  }

  async notify(userId: string, notification: Notification) {
    const settings = await this.getSettings(userId);
    if (settings.pushEnabled) {
      await this.$sendPush(userId, notification.message);
    }
  }
}

// On the client:
const UserProfile = () => {
  const { rpc } = app.useContext<App.Context>();

  const { data: profile } = app.useFetch(
    async (ctx) => {
      // Nested calls are fully typed
      const [user, theme, notificationSettings] = await ctx.rpc.batch([
        // Method from the main class
        ctx.rpc.getProfile('123'),
        // Method from the preferences subclass
        ctx.rpc.preferences.getTheme('123'),
        // Method from the notifications subclass
        ctx.rpc.notifications.getSettings('123')
      ]);

      return { user, theme, notificationSettings };
    }
  );

  // ❌ Private methods remain inaccessible
  const invalid = () => rpc.notifications.$sendPush('123', 'hello');
};

제로 구성

React Edge는 번역을 자동으로 감지하고 로드합니다. 쿠키에 사용자 기본 설정을 쉽게 저장할 수도 있습니다. 하지만 당연히 이런 걸 기대하시겠죠?

// translations/fr.ts
export default {
  'Good Morning, {name}!': 'Bonjour, {name}!',
};

"작동하는" JWT 인증

인증은 항상 웹 애플리케이션의 문제점이었습니다. JWT 토큰, 보안 쿠키 및 재검증을 관리하려면 많은 상용구 코드가 필요한 경우가 많습니다. React Edge는 이것을 완전히 바꿉니다.

전체 인증 시스템을 구현하는 방법은 다음과 같습니다.

const WelcomeMessage = () => {
  const userName = 'John';

  return (
    <div>
      {/* Output: Good Morning, John! */}
      <h1>{__('Good Morning, {name}!', { name: userName })}</h1>
    </div>
  );
};

클라이언트 사용법: 제로 구성

// worker.ts
const handler = {
  fetch: async (request: Request, env: types.Worker.Env, context: ExecutionContext) => {
    const url = new URL(request.url);

    const lang = (() => {
      const lang =
        url.searchParams.get('lang') || worker.cookies.get(request.headers, 'lang') || request.headers.get('accept-language') || '';

      if (!lang || !i18n[lang]) {
        return 'en-us';
      }

      return lang;
    })();

    const workerApp = new AppWorkerEntry({
      i18n: {
        en: await import('./translations/en'),
        pt: await import('./translations/pt'),
        es: await import('./translations/es')
      }
    });

    const res = await workerApp.fetch();

    if (url.searchParams.has('lang')) {
      return new Response(res.body, {
        headers: worker.cookies.set(res.headers, 'lang', lang)
      });
    }

    return res;
  }
};

이것이 왜 혁명적인가?

  1. 제로 상용구

    • 수동 쿠키 관리 없음
    • 요격기가 필요하지 않습니다
    • 수동 새로 고침 토큰 없음
  2. 기본 보안

    • 토큰은 자동으로 암호화됩니다
    • 쿠키는 안전하며 httpOnly
    • 자동 재검증
  3. 전체 입력

    • JWT 페이로드가 입력되었습니다
    • 통합 Zod 검증
    • 입력된 인증 오류
  4. 완벽한 통합

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};

공유 매장

React Edge의 가장 강력한 기능 중 하나는 작업자와 클라이언트 간에 상태를 안전하게 공유하는 기능입니다. 작동 방식은 다음과 같습니다.

미들웨어 및 스토어 활용 예시

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configure CORS
  // Validate request
  // Handle errors
  // Serialize response
  // ...100 lines later...
}

// app/search/page.tsx
'use client';
import { useEffect, useState } from 'react';

export default const SearchPage = () => {
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState({});
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let timeout;
    const doSearch = async () => {
      setLoading(true);
      try {
        const res = await fetch('/api/search?' + new URLSearchParams({
          q: search,
          ...filters
        }));
        if (!res.ok) throw new Error('Search failed');
        setData(await res.json());
      } catch (err) {
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    timeout = setTimeout(doSearch, 300);
    return () => clearTimeout(timeout);
  }, [search, filters]);

  // ... rest of the component
}

작동 방식

  • 공개 데이터: 공개로 표시된 데이터는 클라이언트와 안전하게 공유되므로 구성요소에서 쉽게 액세스할 수 있습니다.
  • 개인 데이터: 민감한 데이터는 작업자의 환경 내에 유지되며 고객에게 절대 노출되지 않습니다.
  • 미들웨어와의 통합: 미들웨어는 공개 데이터와 비공개 데이터 모두로 저장소를 채울 수 있어 서버측 로직과 클라이언트측 렌더링 간의 원활한 정보 흐름을 보장합니다.

이익

  1. 보안: 공개 및 비공개 데이터 범위를 분리하여 민감한 정보를 안전하게 보호합니다.
  2. 편의성: 저장소 데이터에 대한 투명한 액세스로 작업자와 클라이언트 전체의 상태 관리가 단순화됩니다.
  3. 유연성: 스토어는 미들웨어와 쉽게 통합되므로 요청 처리에 따라 동적 상태 업데이트가 가능합니다.

우아한 라우팅

React Edge의 라우팅 시스템은 Hono에서 영감을 얻었지만 SSR의 기능이 향상되었습니다.

// First, define your API on the server
class PropertiesAPI extends Rpc {
  async searchProperties(filters: PropertyFilters) {
    const results = await this.db.properties.search(filters);
    // Automatic caching for 5 minutes
    return this.createResponse(results, {
      cache: { ttl: 300, tags: ['properties'] }
    });
  }

  async getPropertyDetails(ids: string[]) {
    return Promise.all(
      ids.map(id => this.db.properties.findById(id))
    );
  }
}

// Now, on the client, the magic happens
const PropertySearch = () => {
  const [filters, setFilters] = useState<PropertyFilters>({
    price: { min: 100000, max: 500000 },
    bedrooms: 2
  });

  // Reactive search with intelligent debounce
  const {
    data: searchResults,
    loading: searchLoading,
    error: searchError
  } = app.useFetch(
    async (ctx) => ctx.rpc.searchProperties(filters),
    {
      // Re-fetch when filters change
      deps: [filters],
      // Wait 300ms of "silence" before fetching
      depsDebounce: {
        filters: 300
      }
    }
  );

  // Fetch property details for the found results
  const {
    data: propertyDetails,
    loading: detailsLoading,
    fetch: refreshDetails
  } = app.useFetch(
    async (ctx) => {
      if (!searchResults?.length) return null;

      // This looks like multiple calls, but...
      return ctx.rpc.batch([
        // Everything is multiplexed into a single request!
        ...searchResults.map(result =>
          ctx.rpc.getPropertyDetails(result.id)
        )
      ]);
    },
    {
      // Refresh when searchResults change
      deps: [searchResults]
    }
  );

  // A beautiful and responsive interface
  return (
    <div>
      <FiltersPanel
        value={filters}
        onChange={setFilters}
        disabled={searchLoading}
      />

      {searchError && (
        <Alert status='error'>
          Search error: {searchError.message}
        </Alert>
      )}

      <PropertyGrid
        items={propertyDetails || []}
        loading={detailsLoading}
        onRefresh={() => refreshDetails()}
      />
    </div>
  );
};

주요 특징

  • 그룹화된 경로: 공유 경로 및 미들웨어 아래 관련 경로를 논리적으로 그룹화합니다.
  • 유연한 핸들러: 페이지를 반환하거나 API 응답을 전달하는 핸들러를 정의합니다.
  • 경로별 헤더: 개별 경로에 대한 HTTP 헤더를 사용자 정의합니다.
  • 내장 캐싱: ttl 및 태그로 캐싱 전략을 단순화합니다.

이익

  1. 일관성: 관련 경로를 그룹화하면 일관된 미들웨어 애플리케이션 및 코드 구성이 보장됩니다.
  2. 확장성: 이 시스템은 대규모 애플리케이션을 위한 중첩 및 모듈식 라우팅을 지원합니다.
  3. 성능: 기본 캐싱 지원으로 수동 구성 없이 최적의 응답 시간을 보장합니다.

엣지 캐시가 포함된 분산 캐시

React Edge에는 JSON 데이터와 전체 페이지 모두에 대해 원활하게 작동하는 강력한 캐싱 시스템이 포함되어 있습니다. 이 캐싱 시스템은 지능형 태그 지정 및 접두사 기반 무효화를 지원하므로 다양한 시나리오에 적합합니다.

예: 태그를 사용하여 API 응답 캐싱

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};

주요 특징

  • 태그 기반 무효화: 캐시 항목을 태그를 사용하여 그룹화할 수 있으므로 데이터 변경 시 쉽고 선택적으로 무효화할 수 있습니다.
  • 접두사 일치: 공통 접두사를 사용하여 여러 캐시 항목을 무효화합니다. 이는 검색어나 계층적 데이터와 같은 시나리오에 이상적입니다.
  • TTL(Time-to-Live): 캐시 항목의 만료 시간을 설정하여 고성능을 유지하면서 데이터를 최신 상태로 유지합니다.

이익

  1. 향상된 성능: 자주 액세스하는 데이터에 대해 캐시된 응답을 제공하여 API의 부하를 줄입니다.
  2. 확장성: 분산 캐싱 시스템으로 대규모 데이터세트와 높은 트래픽을 효율적으로 처리합니다.
  3. 유연성: 캐싱을 세밀하게 제어하여 개발자가 데이터 정확성을 희생하지 않고도 성능을 최적화할 수 있습니다.

링크: 앞서 생각하는 구성 요소

Link 구성 요소는 클라이언트 측 리소스를 미리 로드하기 위한 지능적이고 성능 지향적인 솔루션으로, 사용자에게 더 부드럽고 빠른 탐색 환경을 보장합니다. 사용자가 링크 위로 마우스를 가져가면 미리 가져오기 기능이 실행되어 유휴 순간을 활용하여 대상 데이터를 미리 요청합니다.

작동 방식

  1. 조건부 프리페칭: 프리페치 속성(기본적으로 활성화됨)은 프리로드 실행 여부를 제어합니다.
  2. 지능형 캐시: 세트는 중복 가져오기 호출을 방지하면서 이미 프리페치된 링크를 저장하는 데 사용됩니다.
  3. 마우스 Enter 이벤트: 사용자가 링크 위로 마우스를 가져가면 handlerMouseEnter 함수가 미리 로드가 필요한지 확인하고 필요한 경우 대상에 대한 가져오기 요청을 시작합니다.
  4. 오류 복원력: 요청 중 모든 실패가 억제되어 구성 요소의 동작이 일시적인 네트워크 문제에 영향을 받지 않도록 합니다.

사용 예

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configure CORS
  // Validate request
  // Handle errors
  // Serialize response
  // ...100 lines later...
}

// app/search/page.tsx
'use client';
import { useEffect, useState } from 'react';

export default const SearchPage = () => {
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState({});
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let timeout;
    const doSearch = async () => {
      setLoading(true);
      try {
        const res = await fetch('/api/search?' + new URLSearchParams({
          q: search,
          ...filters
        }));
        if (!res.ok) throw new Error('Search failed');
        setData(await res.json());
      } catch (err) {
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    timeout = setTimeout(doSearch, 300);
    return () => clearTimeout(timeout);
  }, [search, filters]);

  // ... rest of the component
}

사용자가 '회사 소개' 링크 위로 마우스를 가져가면 구성 요소가 /about 페이지에 대한 데이터를 미리 로드하기 시작하여 거의 즉각적인 전환을 보장합니다. 천재적인 아이디어죠? React.dev 문서에서 영감을 얻었습니다.

app.useContext: 엣지로 향하는 포털

app.useContext 후크는 작업자의 전체 컨텍스트에 대한 원활한 액세스를 허용하는 React Edge의 초석입니다. 라우팅, 상태, RPC 호출 등을 관리하기 위한 강력한 인터페이스를 제공합니다.

예: 대시보드에서 app.useContext 사용

// First, define your API on the server
class PropertiesAPI extends Rpc {
  async searchProperties(filters: PropertyFilters) {
    const results = await this.db.properties.search(filters);
    // Automatic caching for 5 minutes
    return this.createResponse(results, {
      cache: { ttl: 300, tags: ['properties'] }
    });
  }

  async getPropertyDetails(ids: string[]) {
    return Promise.all(
      ids.map(id => this.db.properties.findById(id))
    );
  }
}

// Now, on the client, the magic happens
const PropertySearch = () => {
  const [filters, setFilters] = useState<PropertyFilters>({
    price: { min: 100000, max: 500000 },
    bedrooms: 2
  });

  // Reactive search with intelligent debounce
  const {
    data: searchResults,
    loading: searchLoading,
    error: searchError
  } = app.useFetch(
    async (ctx) => ctx.rpc.searchProperties(filters),
    {
      // Re-fetch when filters change
      deps: [filters],
      // Wait 300ms of "silence" before fetching
      depsDebounce: {
        filters: 300
      }
    }
  );

  // Fetch property details for the found results
  const {
    data: propertyDetails,
    loading: detailsLoading,
    fetch: refreshDetails
  } = app.useFetch(
    async (ctx) => {
      if (!searchResults?.length) return null;

      // This looks like multiple calls, but...
      return ctx.rpc.batch([
        // Everything is multiplexed into a single request!
        ...searchResults.map(result =>
          ctx.rpc.getPropertyDetails(result.id)
        )
      ]);
    },
    {
      // Refresh when searchResults change
      deps: [searchResults]
    }
  );

  // A beautiful and responsive interface
  return (
    <div>
      <FiltersPanel
        value={filters}
        onChange={setFilters}
        disabled={searchLoading}
      />

      {searchError && (
        <Alert status='error'>
          Search error: {searchError.message}
        </Alert>
      )}

      <PropertyGrid
        items={propertyDetails || []}
        loading={detailsLoading}
        onRefresh={() => refreshDetails()}
      />
    </div>
  );
};

app.useContext의 주요 기능

  • 경로 관리: 일치하는 경로, 해당 매개변수 및 쿼리 문자열에 쉽게 액세스할 수 있습니다.
  • RPC 통합: 추가 구성 없이 클라이언트에서 직접 입력된 보안 RPC 호출을 수행합니다.
  • 공유 스토어 액세스: 가시성(공개/비공개)을 완벽하게 제어하여 공유 작업자-클라이언트 상태에서 값을 검색하거나 설정합니다.
  • 범용 URL 액세스: 동적 렌더링 및 상호 작용에 대한 현재 요청의 전체 URL에 쉽게 액세스할 수 있습니다.

강력한 이유

app.useContext 후크는 작업자와 클라이언트 사이의 격차를 해소합니다. 이를 통해 상용구 없이 공유 상태, 보안 데이터 가져오기 및 상황별 렌더링에 의존하는 기능을 구축할 수 있습니다. 이는 복잡한 애플리케이션을 단순화하여 유지 관리가 더 쉽고 개발 속도가 빨라집니다.

app.useUrlState: URL과 동기화된 상태

app.useUrlState 후크는 애플리케이션 상태를 URL 쿼리 매개변수와 동기화하여 URL에 포함되는 내용, 상태 직렬화 방법 및 업데이트 시기에 대한 세부적인 제어를 제공합니다.

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};

매개변수

  1. 초기상태

    • 상태의 기본 구조와 값을 정의하는 객체입니다.
  2. 옵션:

    • 디바운스: 상태 변경 후 URL이 업데이트되는 속도를 제어합니다. 과도한 업데이트를 방지하는데 유용합니다.
    • kebabCase: URL로 직렬화할 때 상태 키를 kebab-case로 변환합니다(예: filter.locations → filter-locations).
    • omitKeys: URL에서 제외할 키를 지정합니다. 예를 들어 민감한 데이터나 큰 개체는 생략될 수 있습니다.
    • 생략값: 존재하는 경우 URL에서 관련 키를 제외하는 값입니다.
    • pickKeys: 지정된 키만 포함하도록 직렬화된 상태를 제한합니다.
    • 접두사: 네임스페이스를 위한 모든 쿼리 매개변수에 접두사를 추가합니다.
    • url: 동기화할 기본 URL이며 일반적으로 앱 컨텍스트에서 파생됩니다.

이익

  • 동일한 useState API: 기존 구성요소와 쉽게 통합됩니다.
  • SEO 친화적: 상태 의존적 뷰가 공유 가능하고 북마크 가능한 URL에 반영되도록 합니다.
  • 디바운스된 업데이트: 슬라이더나 텍스트 상자와 같이 빠르게 변화하는 입력에 대한 과도한 쿼리 업데이트를 방지합니다.
  • URL 정리: kebabCase 및 omitKeys와 같은 옵션은 쿼리 문자열을 읽기 쉽고 관련성 있게 유지합니다.
  • 상태 하이드레이션: 구성요소 마운트 시 URL에서 상태를 자동으로 초기화하여 딥링킹을 원활하게 만듭니다.
  • 어디서나 작동: 서버측 렌더링 및 클라이언트측 탐색을 지원하여 애플리케이션 전체에서 일관된 상태를 보장합니다.

실제 응용

  • 부동산 목록용 필터: 공유 가능한 검색을 위해 목록 유형 및 지도 경계와 같은 사용자 적용 필터를 URL에 동기화합니다.
  • 동적 보기: 페이지를 새로 고치거나 링크를 걸어도 지도 확대/축소, 중심점 또는 기타 보기 설정이 유지되도록 합니다.
  • 사용자 기본 설정: 쉽게 공유하거나 북마크할 수 있도록 사용자가 선택한 설정을 URL에 저장합니다.

app.useStorageState: 지속 상태

app.useStorageState 후크를 사용하면 완전한 TypeScript 지원과 함께 localStorage 또는 sessionStorage를 사용하여 브라우저에서 상태를 유지할 수 있습니다.

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};

지속성 옵션

  • 디바운스: 저장소에 저장되는 빈도를 제어합니다.
  • 저장소: localStorage와 sessionStorage 중에서 선택하세요.
  • omitKeys/pickKeys: 어떤 데이터가 유지되는지 세밀하게 제어합니다.

성능

  • 디바운스로 최적화된 업데이트.
  • 자동 직렬화/역직렬화.
  • 인메모리 캐싱.

일반적인 사용 사례

  • 검색기록
  • 즐겨찾기 목록
  • 사용자 기본 설정
  • 필터 상태
  • 임시 장바구니
  • 초안 양식

app.useDebounce: 주파수 제어

반응형 값을 손쉽게 디바운스:

// pages/api/search.ts
export default async handler = (req, res) => {
  // Configure CORS
  // Validate request
  // Handle errors
  // Serialize response
  // ...100 lines later...
}

// app/search/page.tsx
'use client';
import { useEffect, useState } from 'react';

export default const SearchPage = () => {
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState({});
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let timeout;
    const doSearch = async () => {
      setLoading(true);
      try {
        const res = await fetch('/api/search?' + new URLSearchParams({
          q: search,
          ...filters
        }));
        if (!res.ok) throw new Error('Search failed');
        setData(await res.json());
      } catch (err) {
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    timeout = setTimeout(doSearch, 300);
    return () => clearTimeout(timeout);
  }, [search, filters]);

  // ... rest of the component
}

app.useDistinct: 중복이 없는 상태

유형 안전성을 유지하면서 고유한 값을 갖는 배열을 유지하세요.

app.useDistinct 후크는 심층 비교 및 ​​디바운스를 지원하여 값이 실제로 변경된 시기를 전문적으로 감지합니다.

// On the server
class UserAPI extends Rpc {
  async searchUsers(query: string, filters: UserFilters) {
    // Validation with Zod
    const validated = searchSchema.parse({ query, filters });
    return this.db.users.search(validated);
  }
}

// On the client
const UserSearch = () => {
  const { rpc } = app.useContext<App.Context>();

  // TypeScript knows exactly what searchUsers accepts and returns!
  const { data, loading, error, fetch: retry } = app.useFetch(
    async (ctx) => ctx.rpc.searchUsers('John', { age: '>18' })
  );
};

주요 특징

  1. 고유값 감지:
    • 현재 및 이전 값을 추적합니다.
    • 기준에 따라 변경 사항이 의미가 있는지 자동으로 감지합니다.
  2. 심층 비교:
    • 복잡한 객체에 대해 깊은 수준에서 값 동등성 검사를 활성화합니다.
  3. 맞춤 비교:
    • "뚜렷한" 변화를 구성하는 요소를 정의하는 사용자 정의 기능을 지원합니다.
  4. 디바운스됨:
    • 변경이 너무 자주 발생하여 불필요한 업데이트를 줄입니다.

이익

  • 동일한 useState API: 기존 구성요소와 쉽게 통합됩니다.
  • 성능 최적화: 값이 의미 있게 변경되지 않은 경우 불필요한 다시 가져오기 또는 재계산을 방지합니다.
  • 향상된 UX: 과도한 UI 업데이트를 방지하여 보다 원활한 상호작용을 유도합니다.
  • 단순화된 논리: 상태 관리에서 동일성 또는 중복에 대한 수동 검사를 제거합니다.

React Edge의 후크는 조화롭게 작동하도록 설계되어 유연하고 강력한 유형의 개발 경험을 제공합니다. 이들의 조합을 통해 훨씬 적은 코드로 복잡하고 반응적인 인터페이스를 만들 수 있습니다.

React Edge CLI: 손끝에서 느껴지는 힘

React Edge용 CLI는 필수 도구를 직관적인 단일 인터페이스에 모아 개발자의 삶을 단순화하도록 설계되었습니다. 초보자이든 숙련된 개발자이든 CLI를 사용하면 프로젝트를 효율적이고 쉽게 구성, 개발, 테스트 및 배포할 수 있습니다.

주요 특징

모듈식 및 유연한 명령:

  • 빌드: 환경과 모드(개발 또는 프로덕션)를 지정하는 옵션을 사용하여 앱과 작업자를 모두 빌드합니다.
  • dev: 로컬 또는 원격 개발 서버를 시작하여 앱이나 작업자에서 별도의 작업을 허용합니다.
  • 배포: Cloudflare Workers와 Cloudflare R2의 결합된 기능을 활용하여 빠르고 효율적인 배포를 지원하여 엣지 인프라의 성능과 확장성을 보장합니다.
  • 로그: 작업자 로그를 터미널에서 직접 모니터링합니다.
  • lint: 자동 수정을 지원하여 Prettier 및 ESLint 실행을 자동화합니다.
  • 테스트: Vitest를 사용하여 선택적 적용 범위로 테스트를 실행합니다.
  • type-check: 프로젝트 전체에서 TypeScript 입력을 검증합니다.

실제 사용 사례

React Edge를 사용한 첫 번째 프로덕션 애플리케이션이 이미 출시되었다는 소식을 공유하게 되어 자랑스럽습니다! 브라질의 부동산 회사인 Lopes Imóveis는 이미 프레임워크의 성능과 유연성의 이점을 누리고 있습니다.

웹사이트에서는 검색을 최적화하고 보다 원활한 사용자 경험을 제공하기 위해 속성이 캐시에 로드됩니다. 매우 동적인 사이트이기 때문에 경로 캐싱은 재검증 중 오래된 전략과 결합하여 단 10초의 TTL을 사용합니다. 이를 통해 사이트는 백그라운드 재검증 중에도 탁월한 성능으로 업데이트된 데이터를 제공할 수 있습니다.

또한 유사한 속성에 대한 권장 사항은 백그라운드에서 효율적이고 비동기적으로 계산된 다음 통합 RPC 캐싱 시스템을 사용하여 Cloudflare의 캐시에 직접 저장됩니다. 이 접근 방식은 후속 요청에 대한 응답 시간을 줄이고 권장 사항 쿼리를 거의 즉각적으로 수행합니다. 모든 이미지는 Cloudflare R2에 저장되어 외부 공급자에 의존하지 않고도 확장 가능하고 분산된 저장소를 제공합니다.

From Next.js to React Edge with Cloudflare Workers: A Story of Liberation
From Next.js to React Edge with Cloudflare Workers: A Story of Liberation

곧 Easy Auth를 위한 대규모 자동 마케팅 프로젝트를 시작하여 이 기술의 잠재력을 더욱 보여줄 예정입니다.

결론

그래서 독자 여러분, 우리는 React Edge 세계를 통한 이 여정의 끝에 도달했습니다! Basic 및 Bearer와 같은 더 간단한 인증 옵션과 개발자의 하루를 훨씬 더 행복하게 만들어주는 기타 트릭과 같이 탐색해야 할 놀라운 기능이 여전히 많다는 것을 알고 있습니다. 하지만 잠깐만요! 앞으로 이러한 각 기능에 대해 자세히 알아보기 위한 더 자세한 기사를 제공할 계획입니다.

스포일러 경고: 곧 React Edge가 오픈 소스로 공개되고 적절하게 문서화될 예정입니다! 개발, 작업, 글쓰기 및 약간의 사회 생활의 균형을 맞추는 것은 쉽지 않지만, 특히 Cloudflare의 인프라가 제공하는 터무니없는 속도와 함께 이 경이로운 작업을 보는 즐거움은 나를 계속 움직이게 하는 원동력입니다. 최고의 순간은 아직 오지 않았으니 계속 지켜봐 주시기 바랍니다! ?

그동안 지금 바로 탐색하고 테스트하고 싶다면 NPM에서 이미 패키지를 사용할 수 있습니다: NPM의 React Edge..

제 이메일 주소는 feliperohdee@gmail.com이며, 항상 피드백에 열려 있습니다. 이것은 이 여정의 시작일 뿐입니다. 제안과 건설적인 비판을 환영합니다. 읽으신 내용이 마음에 드셨다면 친구 및 동료와 공유하시고, 추가 업데이트를 계속 지켜봐 주시기 바랍니다. 여기까지 읽어주셔서 감사하고, 다음에 또 만나요! ???

위 내용은 Next.js에서 Cloudflare Workers와 함께 React Edge까지: 해방 이야기의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.