📝로딩 (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>
)
}
'[Next.js] > [Next 14]' 카테고리의 다른 글
[Next 14] 정적 데이터, 이미지, 폰트, 스크립트, 정적 메타데이터, Opengraph, 동적 메타데이터 (1) | 2024.01.02 |
---|---|
[Next 14] 외부 라이브러리, 런타임(Runtime), CSS Style (0) | 2024.01.02 |
[Next 14] Server Component (SSR), Client Component (CSR) (1) | 2024.01.02 |
[Next 14] 미들웨어 (Middleware) (0) | 2024.01.02 |
[Next 14] Next.js란, 폴더 및 파일 구조, 라우팅, 링크(페이지 연결), 정적 페이지 생성(SSG), 에러 핸들링 (0) | 2024.01.01 |