반응형

📝로딩 (Loading)

콘텐츠가 로드되는 동안 서버에서 즉시 로드 상태를 표시할 수 있습니다. 렌더링이 완료되면 새 콘텐츠가 자동으로 교체됩니다

 

📝즉각 로딩 상태

라우팅 폴더에 loading.tsx를 넣을 경우 하위에서 로딩이 일어났을 때 해당 tsx파일을 보여줍니다 로딩의 경우 스켈레톤 및 스피너와 같은 걸 많이 사용합니다 (참고로 로딩이 일어난 건 Fetch한 데이터를 화면에 노출시킬 때만 해당 됩니다)

 

📝서스펜스 로딩 (Suspense)

Suspense 컴포넌트를 이용할 수 있습니다 fallback에는 로딩 시킬 컴포넌트를 적어주고 하위 컴포넌트를 감싸주면 됩니다

 

Suspense다양한 이점이 있는데 SSR의 경우 위의 사진 처럼 모든 데이터를 가져온 후에 서버가 페이지의 HTML을 렌더링 할 수 있습니다 그 후 클라이언트에서 구성요소에 대한 인터렉티브한 이벤트 요소의 코드가 다운 되어야 UI의 기능을 사용할 수 있습니다(Hydration) 이럴 경우 Blokcing 처리로 지연이 될 수 있습니다

 

 

분리전 청크 사이즈                                                                         분리 후 청크 사이즈

 

 

 

Suspense를 이용하면 페이지의 각 컴포넌트를 청크를 나눠서 서버에서 클라이언트로 보낼 수 있습니다 그렇게 되면 NonBlokcking처럼 병렬처리가 되어서 더 빠른 페이지를 제공 및 더 빠른 Hydrating이 가능해 사용자가 페이지를 빨리 이용할 수 있습니다 (스트리밍)

 

SSR에서는 스트리밍을 기본적으로 지원하기 때문에 적용 안 시킨 버전하고 어떤 차이가 있는지는 테스트 못 함 하지만 병렬로 요청이 들어가는 것은 확인했고 Suspense를 사용하면 각각 Loading 보여주다가 먼저 들어오는 것부터 보여준다 아마 이렇게 사용해야 청크를 나눠서 하는 것 같다 마치 CSR처럼 데이터 패치 이후에 그려지는 것처럼 보여진다 → Suspense 필수로 적용

 

물론 Suspense는 Client에서도 사용이 가능하고 청크를 나누진 않기 때문에 다이나믹으로 청크로 나눠서 Lazy Load를 시키면 로딩 성능을 개선 시킬 수 있다 → 서버에서는 자동으로 청크를 나누기 때문에 필요 없다고 한다

 

프론트 코드 (SSR로 테스트)

'use server';

/** /test/page.tsx **/
export default function Page() {
	return (
		<div>
        <Suspense fallback={<div className="text-amber-900">Loading</div>}>
            <Results num={1}/>
        </Suspense>
        <Suspense fallback={<div className="text-amber-900">Loading</div>}>
            <Results2 num={2}/>
        </Suspense>
		</div>
	)
}

/** Results.tsx **/
export default async function Results({num}: { num: number }) {

    const now = new Date();

    let year = now.getFullYear();
    let month = String(now.getMonth() + 1).padStart(2, '0');
    let day = String(now.getDate()).padStart(2, '0');

    let hours = String(now.getHours()).padStart(2, '0');
    let minutes = String(now.getMinutes()).padStart(2, '0');
    let seconds = String(now.getSeconds()).padStart(2, '0');
    let milliseconds = String(now.getMilliseconds()).padStart(3, '0');

    const startDateTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
    console.log(`[${num}] ` + startDateTime);

    const getData = await fetch('<http://localhost:8080/next>', {cache: 'force-cache'});
    let result = await getData.json();

    const end = new Date();

    year = end.getFullYear();
    month = String(end.getMonth() + 1).padStart(2, '0');
    day = String(end.getDate()).padStart(2, '0');

    hours = String(end.getHours()).padStart(2, '0');
    minutes = String(end.getMinutes()).padStart(2, '0');
    seconds = String(end.getSeconds()).padStart(2, '0');
    milliseconds = String(end.getMilliseconds()).padStart(3, '0');

    const endDateTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
    console.log(`[${num} end] ` + endDateTime);
    console.log(`[${num}] interval ` + (end.getTime() - now.getTime()));

    return <div>
        {result.id}
    </div>
}

/** Results2.tsx **/
export default async function Results2({num}: { num: number }) {

    const now = new Date();

    let year = now.getFullYear();
    let month = String(now.getMonth() + 1).padStart(2, '0');
    let day = String(now.getDate()).padStart(2, '0');

    let hours = String(now.getHours()).padStart(2, '0');
    let minutes = String(now.getMinutes()).padStart(2, '0');
    let seconds = String(now.getSeconds()).padStart(2, '0');
    let milliseconds = String(now.getMilliseconds()).padStart(3, '0');

    const startDateTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
    console.log(`[${num}] ` + startDateTime);

    const getData = await fetch('<http://localhost:8080/next2>', {cache: 'force-cache'});
    let result = await getData.json();

    const end = new Date();

    year = end.getFullYear();
    month = String(end.getMonth() + 1).padStart(2, '0');
    day = String(end.getDate()).padStart(2, '0');

    hours = String(end.getHours()).padStart(2, '0');
    minutes = String(end.getMinutes()).padStart(2, '0');
    seconds = String(end.getSeconds()).padStart(2, '0');
    milliseconds = String(end.getMilliseconds()).padStart(3, '0');

    const endDateTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
    console.log(`[${num} end] ` + endDateTime);
    console.log(`[${num}] interval ` + (end.getTime() - now.getTime()));

    return <div>
        {result.id}
    </div>
}

 

 

백엔드 코드

@CrossOrigin(origins = "*")
@GetMapping("/next")
public @ResponseBody HashMap<String, Object> next() throws InterruptedException {

	System.out.println("next call!!");
	HashMap<String, Object> map = new HashMap<String, Object>();
	map.put("id", "Next 테스트 1");
	
	Thread.sleep(6000);

	
	return map;
}

@CrossOrigin(origins = "*")
@GetMapping("/next2")
public @ResponseBody HashMap<String, Object> next2() throws InterruptedException {

	System.out.println("next2 call!!");
	HashMap<String, Object> map = new HashMap<String, Object>();
	map.put("id", "Next 테스트 2");
	
	Thread.sleep(3000);

	
	return map;
}

결과 → /next2를 호출한 곳이 먼저 그려지고 3초 후에 /next를 호출한 곳이 그려진다

 

📝스트리밍

스트리밍을 사용하면 서버에서 UI를 점진적으로 렌더링할 수 있습니다

작업은 여러 단위로 분할되어 준비가 되면 클라이언트로 스트리밍됩니다 이를 통해 사용자는 전체 콘텐츠의 렌더링이 완료되기 전에 페이지의 일부를 즉시 볼 수 있습니다

 

스트리밍은 기본적으로 Next.js 앱 라우터에 내장되어 있습니다 이는 초기 페이지 로딩 성능뿐만 아니라 전체 경로 렌더링을 차단하는 느린 데이터 가져오기에 의존하는 UI를 모두 개선하는 데 도움이 됩니다

 

사용 예제 코드

import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'
 
export default function Posts() {
  return (
    <section>
      <Suspense fallback={<p>Loading feed...</p>}>
        <PostFeed />
      </Suspense>
      <Suspense fallback={<p>Loading weather...</p>}>
        <Weather />
      </Suspense>
    </section>
  )
}

 

 

 

 

반응형