페이지별 레이아웃 설정은
https://onebite-books-page-woad.vercel.app/
한 입 크기로 잘라먹는 Next.js - 이정환
을 참고하여 레이아웃을 설정하려고 합니다.
먼저 search 부분부터 레이아웃을 별도로 설정하려고 합니다.
먼저 searchable-layout.tsx로 임시 서치 바를 만들어 줍니다.
import { ReactNode } from "react";
export default function SearchableLayout({ children }: { children: ReactNode }) {
return (
<div>
<div>임시 서치바</div>
{children}
</div>
);
}
다음으로는 이렇게 만든 searchable-layout을 index페이지와 search 페이지에 각각 적용 시켜야 합니다.
_app.tsx에 searchable-layOut을 중첩하는 방식을 사용하게 된다면
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import GlobalLayout from "./components/global-layout";
import SearchableLayout from "./components/searchable-layout";
export default function App({ Component, pageProps }: AppProps) {
return (
<GlobalLayout>
<SearchableLayout>
<Component {...pageProps} />
</SearchableLayout>
</GlobalLayout>
);
}
그러면 설정했던 개별 북 페이지에도 서치바가 나타나게 되는 문제점이 존재합니다.
예시로
http://localhost:3000/book/13 들어가면

세부 표시에도 서치바가 나타나게 되는 문제점이 생기는걸 볼 수 있습니다.
그 이유는 일괄적으로 레이아웃이 적영되어 버리는 문제가 발생하기 때문입니다.
특정 페이지에만 적용되길 원하는 레이아웃은 중첩하는 방식으로는 문제 해결을 못하기에
원하는 컴포넌트에 searchable-layout 컴포넌트를 추가해주면 됩니다.
index.tsx로 들어가서 홈 컴퍼넌트에다가 getLayout 메서드를 추가합니다.
Home.getLayout = (page: ReactNode) => {
return <SearchableLayout>{page}</SearchableLayout>;
};
이렇게 되면 홈 컴포넌트에 저장이 된 getLayout이라는 메서드는 page라는 매개변수로 현재의 페이지 역할을 할 컴포넌트를 받아와서 별도의 레이아웃으로 감싸진 형태의 페이지를 return 해주는 함수가 되는 것입니다.
import { ReactNode } from "react";
import SearchableLayout from "./components/searchable-layout";
import style from "./index.module.css";
export default function Home() {
return (
<>
<h1 className={style.h1}>인덱스</h1>
<h2 className={style.h2}>H2</h2>
</>
);
}
Home.getLayout = (page: ReactNode) => {
return <SearchableLayout>{page}</SearchableLayout>;
};
이렇게 설정후 _app.tsx로 가서 console.log(Component.getlayout)으로 로그를 확인해 보면

getLayout으로 설정한 page가 잘 출력되는 것으로 확인할 수있습니다.
또한 getLayout메서드는 page 역할을 하는 컴퍼넌트를 전달 받아서 결과 값으로 SearchAbleLayout이 적용된 새로운 컴포넌트를 return 하도록 만들어져 있어
_app.tsx에서 getLayout으로 변수 설정 후 함수를 실행해 컴포넌트를 전달해주면
SearchableLayout으로 감싸진 형태로 렌더링이 설정 됩니다.
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import GlobalLayout from "./components/global-layout";
export default function App({ Component, pageProps }: AppProps) {
const getLayout = Component.getLayout
return (
<GlobalLayout>
{getLayout(<Component {...pageProps} />)}
</GlobalLayout>
);
}
컴포넌트라는 것은 함수입니다. 함수도 객체이기 때문에 메서드를 추가할 수 있습니다.
그렇기 때문에 홈 컴퍼넌트에 별도의 메서더를 추가해 놓으면
앱 컴포넌트에서 전달을 받았을 때 꺼내와서 사용할 수 있는 것입니다.
Home 컴포넌트 뿐만아니라 이제 search 페이지에도 똑같이 getlayout이라는 메서드를 추가하고 똑같이 searchable-layout이 적용된 페이지를 렌더링 하도록 설정을 해주면
import { useRouter } from "next/router";
import { ReactNode } from "react";
import SearchableLayout from "../components/searchable-layout";
export default function Page() {
const router = useRouter();
const q = router.query.q;
return <h1>search {q}</h1>;
}
Page.getLayout = (page: ReactNode) => {
return <SearchableLayout>{page}</SearchableLayout>;
};
http://localhost:3000/search 접속하면 똑같이 적용된 것을 확인할 수 있습니다.

http://localhost:3000/book/13에 접속해보면 getLayout 메서드가 적용되지 않은 페이지로 접속하게 되면 타입 에러가 발생하게 됩니다.

왜냐하면 book 컴포넌트에는 별도의 메서드를 추가해 놓지 않았기 때문입니다.
그래서 이럴 때에는 예외 처리를 해주셔야 합니다.
_app.tsx로 가서 간단하게
const getLayout = Component.getLayout ?? ((page: ReactNode) => page)
이렇게 처리해주면 됩니다.
만약 getLayout의 함수가 없는 컴포넌트에서는 앞에 있는 Component.getLayout이 undefined가 되기 때문에 뒤에 있는 값 ((page: ReactNode) => page)이 저장이 됩니다.
현재 _app.tsx에서 getLayout함수에 빨간색 오류가 발생하고 있는것을 확인할 수 있습니다.
그 이유는 getLayout메서드는 임의로 만들어둔 메서드이기 때문에 기본적인 타입 정보에는 포함되어 있지 않기 때문입니다.
그렇기에 Component 타입에 getLayout이라는 메서드가 존재할 것이다라고 타입 정보를 추가해 주면 됩니다.
type NextPageWithLayout = NextPage & {
getLayout: (page: ReactNode) => ReactNode;
};
새로운 타입을 만들어주면 이 타입을 App 컴포넌트가 받는 이 Props의 타입에 교집합으로 컴포넌트의 타입을 정의를 하면 됩니다.
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import GlobalLayout from "./components/global-layout";
import { ReactNode } from "react";
import { NextPage } from "next";
type NextPageWithLayout = NextPage & {
getLayout?: (page: ReactNode) => ReactNode;
};
export default function App({ Component, pageProps }: AppProps & { Compnent: NextPageWithLayout }) {
const getLayout = Component.getLayout ?? ((page: ReactNode) => page);
return <GlobalLayout>{getLayout(<Component {...pageProps} />)}</GlobalLayout>;
}
이렇게 되면 타입 정의가 된 오류가 해결 할 수 있습니다.
먼저 searchable-layout.tsx로 와서 input 태그와 button 태그를 만들어주는걸로 시작합니다.
<input placeholder="검색어를 입력해주세요..." />
<button>검색</button>
다음에 현재 input 값을 받는 useState를 만들어 주고 onChange 이벤트 핸들러를 만들어 줍니다.
이때 이벤트 핸들러로 받는 e는 타입을 React.ChangeEvent<HTMLInputElement>로 입력을 해줍니다.
이는 HTML의 input Element에서 발생한 이벤트 타입이다라고 이해 하면 됩니다.
const [search, setSearch] = useState("");
const onChangeSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
};
return (
<div>
<div>
<input value={search} onChange={onChangeSearch} placeholder="검색어를 입력해주세요..." />
<button>검색</button>
</div>
{children}
</div>
);
다음으로는 button 검색 버튼을 입력했을 때 입력한 검색어와 함께 search 페이지로 이동시키는 기능을 구현해야 합니다.
onSubmit의 함수를 만들고 useRouter의 push 메서드를 통해 search 페이지로 보내주면 됩니다.
참고로 search가 없다면 바로 return 해주는 검사문도 추가해주면 좋습니다.
export default function SearchableLayout({ children }: { children: ReactNode }) {
const [search, setSearch] = useState("");
const router = useRouter();
const onChangeSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
};
const onSubmit = () => {
if (!search) return
router.push(`/search?q=${search}`);
};
return (
<div>
<div>
<input value={search} onChange={onChangeSearch} placeholder="검색어를 입력해주세요..." />
<button onClick={onSubmit}>검색</button>
</div>
{children}
</div>
);
}
그다음 검색을 해보면 상세 페이지로 이동하는것을 확인할 수 있습니다.
이때 검색 후 새로고침을 하면 페이지는 검색했던 페이지 그대로인데 현재 input의 search 값이 사라져 있는 것을 확인할 수 있습니다.
이는 useEffect를 통해 해결할 수 있습니다.
먼저 쿼리스트링을 통해 현재 router의 쿼리 큐 값을 가져옵니다.
const q = router.query.q;
useEffect(() => {
setSearch(q || "");
}, [q]);
하지만 오류가 발생하게 됩니다 그 이유는 현재 받아온 쿼리스트링에는 자동적으로

string, string배열, undefined로 추론되고 있기 때문에 간단하게 타입 단원으로 string을 설정해주면 됩니다.
그렇게 하면 새로고침을 눌러도 검색했던 결과가 사라지지 않습니다.
엔터만 눌러도 검색되는 기능을 추가하려면
onKeyDown 함수를 만들어 e 객체를 받아온 후 e.key가 "Enter"이면 onSubmit()을 실행시키는 함수를 만들고 input에 넣어주면 됩니다.
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
onSubmit();
}
search bar에 입력된 검색어가 동일할 때에는 엔터를 누르든 검색 버튼을 누르든 페이지가 이동할 필요가 없어 이동을 방지해주는 기능까지 추가하려면
기존의 onSubmit 함수에서 조건문에 추가로 q가 search이면 바로 return해 주는 조건을 추가하면 됩니다.
const onSubmit = () => {
if (!search || q === search) return;
router.push(`/search?q=${search}`);
};
스타일링은 크게 searchbar_container와 그안에 input 태그, button 태그를 꾸며보겠습니다.
먼저 기존의 파일이름과 똑같이 css파일을 만들어 줍니다. (module 꼭 붙여야 합니다!)
.searchbar_container {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.searchbar_container > input {
flex: 1;
padding: 15px;
border-radius: 5px;
border: 1px solid rgb(220, 220, 220);
}
.searchbar_container > button {
width: 80px;
border-radius: 5px;
border: none;
background-color: rgb(37, 147, 255);
color: white;
cursor: pointer;
}
이렇게 꾸며주시면 됩니다!