Next.js에서 CSS를 사용해서 스타일링을 적용해보자
export default function Home() {
return <h1 style={{ color: "blue" }}>index</h1>;
}
인라인으로 스타일 설정을 할 수도 있지만 아무래도 코드가 길어질수록 가독성이 좋지 않다
그렇다면 css 파일을 import 하면?
import "./index.css";
export default function Home() {
return <h1>index</h1>;
}
Global CSS cannot be imported from files other than your Custom <App>.이라는 에러가 뜬다.
Next.js에서는 _app.tsx가 아닌 별도의 페이지 파일에서 css 파일을 import 할 수 없다. (클래스 네임 충돌 방지)
대신 CSS 파일을 모듈처럼 사용할 수 있도록 해주는 CSS module을 사용하면 된다.
import style from "./index.module.css";
export default function Home() {
return (
<>
<h1 className={style.h1}>index</h1>
<h2 className={style.h2}>test - h2</h2>
</>
);
}
.h1 {
color: blue;
}
.h2 {
color: aqua;
}
다음과 같이, 클래스 네임이 중복되지 않도록 자동으로 유니크한 이름으로 변환을 시켜준다.
_app.tsx 파일의 코드가 글로벌 레이아웃으로 적용된다.
그렇다면 글로벌 레이아웃을 변경하려면 _app.tsx 파일의 코드를 변경하면 될까?
import "@/styles/globals.css";
import type { AppProps } from "next/app";
export default function App({ Component, pageProps }: AppProps) {
return (
<div>
<header>헤더</header>
<main>
<Component {...pageProps} />
</main>
<footer>푸터</footer>
</div>
);
}
이런 식으로 _app.tsx 파일에 코드를 모두 작성하게 되면 가독성이 뭔가 별로다!
대신 src/components/global-layout.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>
);
}
import { ReactNode } from "react";
export default function GlobalLayout({ children }: { children: ReactNode }) {
return (
<div>
<header>헤더</header>
<main>{children}</main>
<footer>푸터</footer>
</div>
);
}
모든 페이지에 적용되는 CSS 파일은, _app.tsx에 import되는 global.css 파일이다.
html,
body {
margin: 0px;
padding: 0px;
background-color: rgb(250, 250, 250);
}
margin,padding을 0px로 해서 여백이 없게 하고,
background-color를 연한 회색으로 설정해준다.
그리고 global-layout.tsx에 import할 css 모듈 파일을 만들어준다.
.container {
background-color: white;
max-width: 600px;
min-height: 100vh;
margin: 0 auto;
box-shadow: rgba(100, 100, 100, 0.2) 0px 0px 29px 0px;
padding: 0px 15px; /* 상하, 좌우 */
}
auto : 자동 크기 조정
vh : Viewport Height, 뷰포트의 높이(브라우저 창의 높이)를 기준으로 한 단위
min-height: 100vh : 화면 높이의 100%를 차지
box-shadow : 요소에 그림자 효과를 추가하는 속성
box-shadow: rgba(100, 100, 100, 0.2) 0px 0px 29px 0px
rgba(100, 100, 100, 0.2) : 어두운 회색(100,100,100) 투명도 20%X-offset: 0px (가로 방향 이동 없음) Y-offset: 0px (세로 방향 이동 없음) Blur Radius: 29px (흐림 정도) Spread Radius: 0px (그림자의 크기 변화 없음).header {
height: 60px;
font-weight: bold;
font-size: 18px;
line-height: 60px;
}
.header > a {
color: black;
text-decoration: none;
}
.main {
padding-top: 10px;
}
.footer {
padding: 100px 0px; /* 상하, 좌우 */
color: gray;
}
line-height : line-box의 높이를 설정 (줄 간격을 늘이거나 줄일 수 있다)
.header > a : .header 내부의 직계 자식인 a 태그만 선택
import Link from "next/link";
import { ReactNode } from "react";
import style from "./global-layout.module.css";
export default function GlobalLayout({ children }: { children: ReactNode }) {
return (
<div className={style.container}>
<header className={style.header}>
<Link href={"/"}>📚 ONEBITE BOOKS</Link>
</header>
<main className={style.main}>{children}</main>
<footer className={style.footer}>제작 @hohobooks</footer>
</div>
);
}
검색 기능을 담당하는 search를 만든다면 어떻게 할까?
search바는 home, search 결과 페이지에만 존재해야 하고, book 상세보기 페이지에서는 없어야 한다.
이렇게 일부 페이지에만 존재하는 요소를 만들려면 어떻게 해야 할까?
src/components 디렉터리에
searchable-layout.tsx 파일을 만든다.
import { ReactNode } from "react";
export default function SearchableLayout({
children,
}: {
children: ReactNode;
}) {
return (
<div>
<div>서치바가 들어갈 자리</div>
{children}
</div>
);
}
상단에 적용될 서치바와 home, search 페이지를 props로 받아서 넣을 children을 넣어준다.
서치바가 존재하는 페이지에서 export default로 내보낸 함수 객체에 메서드를 추가해준다.
자바스크립트 함수는 객체이기 때문에 메서드를 추가할 수 있다. (참고 : 객체 자료형 by 한 입 북스)
import SearchableLayout from "@/components/searchable-layout";
import { ReactNode } from "react";
...
Page.getLayout = (page: ReactNode) => {
return <SearchableLayout>{page}</SearchableLayout>;
};
getLayout 메서드는 ReactNode 타입인 page를 인자로 받아, page를 children으로 전달한 SearchableLayout 컴포넌트를 리턴한다.
_app.tsx 파일에서 메서드 호출import GlobalLayout from "@/components/global-layout";
import "@/styles/globals.css";
import { NextPage } from "next";
import type { AppProps } from "next/app";
import { ReactNode } from "react";
type NextPageWithLayout = NextPage & {
getLayout?: (page: ReactNode) => ReactNode;
};
export default function App({
Component,
pageProps,
}: AppProps & { Component: NextPageWithLayout }) {
const getLayout = Component.getLayout ?? ((page: ReactNode) => page);
return <GlobalLayout>{getLayout(<Component {...pageProps} />)}</GlobalLayout>;
}
한 줄씩 알아보자!
NextPageWithLayout 타입 정의type NextPageWithLayout = NextPage & {
getLayout?: (page: ReactNode) => ReactNode;
};
NextPage 타입을 확장한 타입으로, 페이지 컴포넌트가 getLayout 메서드를 선택적으로 가진다.
export default function App({
Component,
pageProps,
}: AppProps & { Component: NextPageWithLayout }) {...
Component가 NextPageWithLayout이라는 추가적인 속성을 가질 수 있게 한다.
AppProps는 App 컴포넌트에 기본적으로 전달되는 props 타입을 정의하며, Component와 pageProps를 포함한다.
const getLayout = Component.getLayout ?? ((page: ReactNode) => page);
현재 페이지의 getLayout 메서드를 찾아서 저장한다.
만약 현재 페이지가 getLayout을 제공하지 않으면, 기본적으로 page를 그대로 반환한다.
??는 널 병합 연산자로 a ?? b는 a가 null || undefined가 아니라면 a가, 맞다면 b가 반환된다.
return <GlobalLayout>{getLayout(<Component {...pageProps} />)}</GlobalLayout>;
getLayout 함수를 호출한 뒤, 인자로 현재 페이지 컴포넌트 ( <Component {...pageProps} /> )를 전달한다.
- 현재 페이지가
NextPageWithLayout타입의component로서 App 함수의 인자로 들어온다.- App 함수 내부에
getLayout함수를 정의한다. 현재 페이지가 getLayout 메서드를 가졌다면 getLayout 메서드를 저장하고, 아니라면 page를 그대로 반환하는 함수를 저장한다.getLayout함수를 호출하며 인자로현재 페이지를 전달해서 받은 리턴값(컴포넌트)를 최종적으로 리턴한다.
import { useState } from "react";
export default function SearchableLayout(...) {
const [search, setSearch] = useState("");
const onChangeSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
};
return (
<div>
<div className={style.searchbar_container}>
<input
value={search}
onChange={onChangeSearch}
placeholder="검색어를 입력하세요 ..."
/>
<button>검색</button>
</div>
{children}
</div>
);
}
사용자가 입력중인 검색어를 search state에 저장하자.
input에 입력된 값이 변할 때 onChange 이벤트 핸들링을 위한 함수를 정의한다.onChangeSearch 함수의 인자인 이벤트 객체 e는 React.ChangeEvent<HTMLInputElement> 타입으로 정의한다.e.target.value를 setState의 인자로 전달해서 state를 갱신한다.import { useRouter } from "next/router";
export default function SearchableLayout(...) {
const router = useRouter();
const onSubmit = () => {
if (!search) return;
router.push(`search?q=${search}`);
};
return (
<div>
...
<button onClick={onSubmit}>검색</button>
...
);
}
사용자가 검색어를 입력하고 검색 버튼을 눌렀을 때, search 결과 페이지로 이동시키자.
onSubmit 함수를 실행한다.onSubmit 함수에서는 검색어가 없으면( !search ) 그냥 리턴시키고, 검색어가 있다면export default function SearchableLayout(...) {
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
onSubmit();
}
};
return (
...
<input
value={search}
onKeyDown={onKeyDown}
onChange={onChangeSearch}
placeholder="검색어를 입력하세요 ..."
/>
...
);
}
검색 버튼을 눌렀을 때 뿐만 아니라, 엔터키를 눌렀을 때에도 submit이 동작하게 하자.
onKeyDown 함수를 정의한다. 인자인 이벤트 객체 e는 React.KeyboardEvent<HTMLInputElement> 타입으로 정의한다.e.key가 Enter라면 onSubmit 함수를 실행한다.import { useState, useEffect } from "react";
export default function SearchableLayout(...) {
const q = router.query.q as string;
useEffect(() => {
setSearch(q || "");
}, [q]);
...
}
새로고침시 페이지는 그대로지만, 검색창의 검색어가 사라지는 문제를 해결하자.
router.query.q는 현재 URL의 쿼리 파라미터 q의 값을 가져온다.useEffect를 사용해서 q의 값이 변할 때마다 setSearch를 호출한다.q가 있다면 q로, 없다면 빈 문자열""로 search 상태를 설정한다.router.query.q는 기본적으로 string | string[] | undefined 타입이다. 하지만 setState의 인자로는 string만 전달할 수 있어서 에러가 난다. 이 문제는 타입 단언 as string으로 해결할 수 있다.
const onSubmit = () => {
if (!search || q === search) return;
router.push(`search?q=${search}`);
};
q === search : 만약 URL의 쿼리 파라미터 q와 현재 search 상태가 같다면, 페이지를 새로고침하거나 이동할 필요가 없으므로 리턴한다.
.searchbar_container {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
display: flex : 내부 요소들이 수평으로 나열
gap : 내부 요소들 간의 간격
.searchbar_container > input {
flex: 1;
padding: 15px;
border-radius: 5px;
border: 1px solid rgb(220, 220, 220);
}
.searchbar_container > input : searchbar_container 내부의 직계 자식인 input 태그만 선택
flex: 1 : 가능한 공간을 모두 차지한다
border-radius : 모서리가 둥근 정도
.searchbar_container > button {
width: 80px;
border-radius: 5px;
border: none;
background-color: rgb(37, 147, 255);
color: white;
cursor: pointer;
}
return (
<div>
<div className={style.searchbar_container}>
...
</div>
{children}
</div>
);
2.8강 다시 듣기! 라고 메모해두려고 했는데 페이지 라우터였구나..😂