인프런 "한 입 크기로 잘라먹는 Next.js" 수강
글로벌 레이아웃은 앱의 모든 페이지를 감싸는 루트 컴포넌트에 적용한다. Page Router 기준으로 실습 파일의 루트는 _app.tsx다.
// pages/_app.tsx
import '@/styles/globals.css';
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
return (
<div>
<header>header</header>
<main>
<Component {...pageProps} />
</main>
<footer>footer</footer>
</div>
);
}
이제 레이아웃을 컴포넌트로 분리해보자.
src/components/global-layout.tsx를 만들고 아래처럼 작성한다.
// src/components/global-layout.tsx
import { ReactNode } from 'react';
export default function GlobalLayout({ children }: { children: ReactNode }) {
return (
<div>
<header>header</header>
<main>{children}</main>
<footer>footer</footer>
</div>
);
}
그리고 _app.tsx에서 사용한다.
// pages/_app.tsx
import GlobalLayout from '@/components/global-layout';
import '@/styles/globals.css';
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
return (
<GlobalLayout>
<Component {...pageProps} />
</GlobalLayout>
);
}
특정 페이지군에만 검색 UI를 붙이고 싶다면, 전역이 아닌 페이지별 레이아웃을 만든다.
// src/components/searchable-layout.tsx
import { ReactNode } from 'react';
export default function SearchableLayout({ children }: { children: ReactNode }) {
return (
<div>
<div>임시 서치바</div>
{children}
</div>
);
}
이걸 전역 _app.tsx에 바로 끼우면 모든 페이지에 서치 바가 생긴다.
필요한 페이지에서만 쓰려면
per-page layout패턴을 쓰자.
// pages/index.tsx
import SearchableLayout from '@/components/searchable-layout';
import type { ReactElement } from 'react';
import styles from './index.module.css';
export default function Home() {
return (
<>
<h1 className={styles.h1}>인덱스</h1>
<h2 className={styles.h2}></h2>
</>
);
}
// getLayout: 페이지 컴포넌트에 메서드로 선언
Home.getLayout = (page: ReactElement) => {
return <SearchableLayout>{page}</SearchableLayout>;
};
getLayout 메서드는 페이지 렌더링 시 호출되어, 해당 페이지를 원하는 레이아웃으로 감싼다.
그리고 _app.tsx에서 이 메서드를 인식하도록 처리하면 된다.
// pages/_app.tsx
import GlobalLayout from '@/components/global-layout';
import '@/styles/globals.css';
import type { AppProps, NextPage } from 'next';
import type { ReactElement, ReactNode } from 'react';
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
export default function App({ Component, pageProps }: AppPropsWithLayout) {
// getLayout이 없으면 기본적으로 "그냥 페이지"를 반환
const getLayout = Component.getLayout ?? ((page: ReactElement) => page);
return <GlobalLayout>{getLayout(<Component {...pageProps} />)}</GlobalLayout>;
}
이렇게 하면 getLayout이 없는 페이지도 타입 오류 없이 안전하게 렌더링된다.
검색 입력 /search?q=...로 이동하는 간단한 예시에
trim()encodeURIComponent을 추가하여 구현하기
// src/components/searchable-layout.tsx
import { useRouter } from 'next/router';
import { ChangeEvent, KeyboardEvent, ReactNode, useEffect, useState } from 'react';
export default function SearchableLayout({ children }: { children: ReactNode }) {
const router = useRouter();
const [search, setSearch] = useState('');
// 현재 URL의 쿼리(q)와 상태를 동기화
const q = (router.query.q as string) ?? '';
useEffect(() => {
setSearch(q);
}, [q]);
const onChangeSearch = (e: ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
};
const submit = () => {
const next = search.trim();
if (!next || next === q) return;
router.push(`/search?q=${encodeURIComponent(next)}`);
};
const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') submit();
};
return (
<div>
<form
onSubmit={(e) => {
e.preventDefault();
submit();
}}
>
<input
value={search}
onChange={onChangeSearch}
onKeyDown={onKeyDown}
placeholder="검색어를 입력해주세요..."
aria-label="검색어"
/>
<button type="submit">검색</button>
</form>
{children}
</div>
);
}
_app.tsx의 Component로 전달하고, 우리가 임의로 부착한 getLayout 메서드도 함께 전달할 수 있다._app.tsx에서 GlobalLayout으로 한 번 감싸고, 페이지에 getLayout이 있으면 추가로 원하는 레이아웃으로 감싸 최종 UI를 만들 수 있다.// pages/_app.tsx
const getLayout = Component.getLayout ?? ((page) => page);
return <GlobalLayout>{getLayout(<Component {...pageProps} />)}</GlobalLayout>;