Section 3. Page Router 핵심 정리(4)

OlMinJe·2025년 9월 28일

Next.js

목록 보기
5/20
post-thumbnail

인프런 "한 입 크기로 잘라먹는 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>
  );
}

✅ 정리

  • Next.js는 페이지 컴포넌트를 _app.tsxComponent로 전달하고, 우리가 임의로 부착한 getLayout 메서드도 함께 전달할 수 있다.
  • _app.tsx에서 GlobalLayout으로 한 번 감싸고, 페이지에 getLayout이 있으면 추가로 원하는 레이아웃으로 감싸 최종 UI를 만들 수 있다.
// pages/_app.tsx
const getLayout = Component.getLayout ?? ((page) => page);
return <GlobalLayout>{getLayout(<Component {...pageProps} />)}</GlobalLayout>;
profile
큐트걸

0개의 댓글