📌 Layouts - next.js 공식
React 모델을 사용하면 페이지를 일련의 컴포넌트로 분해할 수 있다. 이러한 컴포넌트 중 많은 부분이 페이지 간에 재사용되는 경우가 많은데, 예를 들어 모든 페이지에 동일한navigation
과footer
가 있을 수 있다.
Next.js에서 앱 컴포넌트를 커스터마이징하기 위한 파일이며, 앱 전역에서 사용되는 리소스를 로드하거나 설정할 수 있는 중요한 역할을 한다.
_app.js 파일에서는 전역으로 사용되는 CSS, 모든 페이지에서 공통으로 사용되는 컴포넌트, 그리고 추가적인 설정 등을 정의할 수 있다.
Google Analytics와 같은 서비스를 사용하기 위한 스크립트나, 검색 엔진 최적화(SEO)를 위한 메타 태그를 추가하거나, 서버사이드 렌더링(SSR)에 필요한 작업을 수행하는 스크립트 등을 구성할 수도 있다.
✍️ 따라서 UI를 구성하고자 할 때 이러한 커다란 react.js component 를 사용하는 대신 Layout component를 활용할 수도 있다는 것 같다. 🤔
import NavBar from './NavBar';
export default function Layout({ children }) {
return (
<>
<NavBar />
<div>{children}</div>
</>
);
}
<title>
태그를 추가하고 원하는 타이틀을 작성할 수 있다. 또한, <meta>
태그를 추가하여 페이지의 메타 정보를 설정하거나, <link>
태그를 추가하여 favicon과 같은 리소스를 페이지에 추가할 수도 있다.// components / Seo.js
import Head from 'next/head';
export default function Seo({ title }) {
return (
<Head>
<title>{`${title} | Seul's Movies`}</title>
<meta content="This is Seul's Movies website" />
// <link rel='icon' href='/favicon.ico' />
</Head>
);
}
❓link
태그를 넣지 않아도 잘 적용되는 것을 확인, 이유가 뭘까? 🤔
✍️ public 폴더 내부에 favicon.ico
파일을 위치시키면, Next.js는 자동으로 해당 파일을 head
요소에 link
태그로 추가해준다. 이는 Next.js가 내부적으로 next/head 모듈의 Head 컴포넌트에서 자동으로 처리하는 것인데, 그래서 별도로 Head 컴포넌트를 사용하여 link
태그를 추가해주지 않아도 자동으로 favicon이 적용된다고 한다. 😀
1. public 폴더 안의 svg 파일을 가져오기
ex.) public / vercel.svg
파일 가져오기
아래처럼 작성했더니 eslint 에 노란줄이 떴다.😮
// _app.js
<img src='/vercel.svg' alt='vercel_logo' />
2. 👉 <Image>
활용하기
import Image from 'next/image';
...
<Image src='/vercel.svg' alt='vercel_logo' width={200} height={200} />
3. Image 스타일링
<div className='container'>
<Seo title='Home' />
{movies?.map((movie) => (
<div className='movie' key={movie.id}>
{/* <img src={`https://image.tmdb.org/t/p/w300${movie.poster_path}`} /> */}
<div className='image-wrapper'>
<Image
src={`https://image.tmdb.org/t/p/w300${movie.poster_path}`}
width={300}
height={400}
alt='movie_img'
></Image>
</div>
<h4>{movie.original_title}</h4>
</div>
))}
</div>
<style jsx>{`
...
.container {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 25px;
gap: 30px;
}
.image-wrapper {
margin: auto;
max-width: 300px; // max-width:100% 하면 가운데 정렬이 되지만 border-radius가 잘 적용 안됨
max-height: 400px;
margin-bottom: 10px;
border-radius: 12px; // 1)
overflow: hidden; // 2) 이렇게 1)과 같이 설정해 줌으로써 속성을 적용시킬 수 있다.
transition: transform 0.2s ease-in-out;
}
.image-wrapper:hover {
transform: scale(1.02) translateY(-5px);
}
...
`}</style>
img
태그 대신 next.js에서 제공하는 Image
컴포넌트를 활용한 스타일링이다.Image
컴포넌트에 스타일을 적용하기 위해서는 먼저 꼭 필요한 프로퍼티를 지정해주고 wrapper
class를 만들어 준 뒤 스타일을 적용해줄 수 있다.next.config.js 에서 설정하기
const nextConfig = {
reactStrictMode: true,
async redirects() {
...
},
async rewrites() {
...
},
// 다른 설정 추가 가능
images: { // 1)
domains: ['image.tmdb.org'],
},
};
module.exports = nextConfig; // 위에서 정의한 nextConfig 객체 내보내기
@next/font 이용하기
📌 Optimizing Fonts - next.js 공식
@next/font
패키지를 설치한다. (추후 안정화가 진행되면 next.js 팀에서 안정 패키지를 추가해서 별도 설치를 하지 않아도 된다고 한다.😮)// app.js
import { Poppins } from '@next/font/google';
const title = Poppins({
weight: ['400', '600'],
subsets: ['latin'],
});
const text = Ubuntu({
weight: ['400', '700'],
subsets: ['latin'],
});
export default function App({ Component, pageProps }) {
return (
<>
<h1 className={title.className}>title </h1>
<p className={text.className}>description</p>
</>
);
}
❗️ 폰트가 적용되지 않았던 문제 해결하기
✍️ props 전달 태그 위치와 global.css의 지정값 때문이었다.
// globals.css
* {
box-sizing: border-box;
padding: 0;
margin: 0;
/* font-family: 'Poppins', sans-serif; */
}
// app.js
export default function App({ Component, pageProps }) {
return (
<div className={poppins.className}>
<Layout> // 이곳에 className을 넣었더니 적용되지 않았다.
<Component {...pageProps} />
<span>🌈 안녕 🌈</span>
</Layout>
</div>
);
}
_document.js 파일 활용하기
_document.js
파일은 Next.js에서 HTML 문서를 커스터마이징하기 위해 사용되는 특수한 파일로, 이 파일은 pages 폴더 안에 위치시켜야 한다. 🤔
_document.js
파일에 Google Fonts 코드를 추가하면 전체 애플리케이션에서 Google Fonts를 사용할 수 있다.
import Document, { Html, Head, Main, NextScript } from 'next/document';
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
}
render() {
return (
<Html>
<Head>
<link rel='preconnect' href='https://fonts.googleapis.com' />
<link rel='preconnect' href='https://fonts.gstatic.com' crossorigin />
<link
href='https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500&display=swap'
rel='stylesheet'
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
영화정보 제공 사이트에서 API 키를 발급받고 데이터 받아오기
👉 The Movie Database API
1) 리액트 훅 useEffect
와 fetch()
const API_KEY = 'd43... (내 API_KEY)';
useEffect(() => {
fetch(`https://api.themoviedb.org/3/movie/popularapi_key=${API_KEY}`)
.then((response) => response.json())
.then((data) => console.log(data.results));
}, []);
2) 데이터가 비어있을 때 에러 대비하기
// const [movies, setMovies] = useState([]);
const [movies, setMovies] = useState();
return (
...
// {movies.map((movie) => (
{movies?.map((movie) => (
<div key={movie.id}>{movie.original_title}</div>
</div>
useState([])
의 형태를 변경하면서 return도 같이 수정해준다.2) async await 로 변경하기
추가로 참고한 문서
useEffect(() => {
async function fetchMovies() {
const response = await fetch(
`https://api.themoviedb.org/3/movie/popular?api_key=${API_KEY}`
);
const data = await response.json();
return data.results; // return값을 줘야
}
fetchMovies().then((movieData) => setMovies(movieData)); // 여기서 받아옴
}, []);
+) 에러 핸들링 (tyr ...catch)
useEffect(() => {
async function fetchMovies() {
try {
const response = await fetch(
`https://api.themoviedb.org/3/movie/popularf?api_key=${API_KEY}`
);
if (!response.ok) {
const message = ` 에러 상태 👉 : ${response.status}`;
throw new Error(message);
}
const data = await response.json();
return data.results;
} catch (error) {
console.error(error);
}
}
fetchMovies().then((movieData) => setMovies(movieData));
}, []);
→ fetch
메서드를 사용, REST API를 호출하고 HTTP 응답 코드가 성공적인지 확인한다.
→ 응답이 실패하면 throw new Error()
문을 사용, 에러를 던진다.
→ 그리고 try
블록 안에서 response.json()
을 사용해서 응답 데이터를 JSON 형식으로 변환한다.
→ 만약 JSON 파싱이 실패하면 catch
블록으로 제어가 이동한다.
(이것보다 더 나은 방법이 있는지, 다른 식으로 작성할 수 있는지 열심히 찾는중.. 🧐)
✍️ Redirects & Rewrites
Redirects와 Rewrites 는 Next.js의 라우팅 기능 중 하나이다.
1) Redirects (URL변경됨)
: 클라이언트가 요청한 URL을 다른 URL로 자동으로 리디렉션하는 방법
→ 이를 통해서 사용자 경험을 향상 시키는 데 도움을 줄 수 있다. 예를 들어, 사용자가 이전 URL을 북마크 해두었다면 URL이 새 URL로 리디렉션되어 페이지를 올바르게 표시할 수 있기 때문 😎
async redirects() {
return [
{
source: '/movies',
destination: 'https://google.com',
permanent: false,
},
];
},
2) Rewrites (URL변경되지 않음)
: 클라이언트가 요청한 URL을 다른 URL로 대체하는 방법
→ 이를 통해 클라이언트는 실제로는 다른 URL에 접근하고 있지만, URL이 변경되지 않은 것처럼 보이게 된다.
(이를 URL 프록시 역할이라고 부르는데, 이는 프록시 서버와 유사한 개념이다. 클라이언트가 요청한 URL을 중계하여 다른 URL로 변경해주는 것이 프록시 서버의 역할과 유사하기 때문이다.)
따라서 URL의 보안성을 강화하거나, 더 나은 사용자 경험을 제공할 수 있게된다.✨
async rewrites() {
return [
{
source: '/blog/:path*',
destination: 'https://blog/news/:path*',
permanent: true,
},
]
},
👉 이 특징을 이용해서 API key를 숨길 수 있다.
// next.config.js
const API_KEY = process.env.API_KEY;
async rewrites() {
return [
{
source: '/api/movies',
destination: `https://api.themoviedb.org/3/movie/popular?api_key=${API_KEY}`,
},
];
},
+) getServerSideProps 를 사용해서 숨길 수도 있다. 😮
// ex.)
export default function Home({ data }) {
// 데이터 랜더링
}
// 매 request마다 실행
export async function getServerSideProps() {
const res = await fetch(`https://.../data`);
const data = await res.json();
// props를 통해 page에 data전달
return { props: { data } }
}
+) ✍️ 기존 데이터 패치를 이곳에서 작성해보기
export async function getServerSideProps() {
// 1)
const response = await fetch('http://localhost:3000/api/movies'); // 2)
const data = await response.json();
return {
props: {
data: data.results,
},
};
}
1) 이 안의 코드는 server에서 돌아가게 된다.(백엔드에서만 실행)
2) 클라이언트에서 API를 받아오는 것이 아니므로 http...을 다 기재해줘야 정상적으로 작동한다.
여기서 에러 핸들링 처리는 어떤식으로 해야할까? 🧐
✍️ 영화 포스터나 제목을 클릭했을 때 /movies/[id]
로 라우팅 해보기
ex.) http://localhost:3000/movies/315162
주소의 파일 위치 : pages/movies/[id].js
1. Link 태그를 통해서 라우팅하기
<Link href={`/movies/${movie.id}`} legacyBehavior>
<a>{movie.original_title}</a>
</Link>
1-1. Link 태그와 as 속성
<Link
href={{
pathname: `/movies/${movie.id}`,
query: {
title: movie.original_title,
},
}}
as={`/movies/${movie.id}`}
legacyBehavior
>
<a>{movie.original_title}</a>
</Link>
href
: 클릭 시 이동할 페이지 경로를 설정. 여기서는 /movies/${movie.id}
로 설정했다.query
: 페이지로 전달할 쿼리 파라미터를 설정. 여기서는 title이라는 쿼리 파라미터에 movie.original_title
값을 전달하도록 설정했다.as
: 브라우저 주소창에 표시될 경로를 설정. 이는 href에 설정한 경로에 대응되는 서버 측 경로로 연결된다. 여기서는 /movies/${movie.id}
를 설정했다.legacyBehavior
: 이전 버전의 Next.js에서 사용하던 URL을 사용할 때 설정하는 옵션.👉 즉 /movies/${movie.id}
로 링크를 설정하되, title
쿼리 파라미터를 movie.original_title
값으로 전달하고, 브라우저 주소창에는 /movies/${movie.id}
가 표시된다.
2. useRouter()
훅을 사용해서 라우팅하기
import { useRouter } from 'next/router';
const router = useRouter();
const onClick = (id) => {
console.log(id);
router.push(`/movies/${id}`);
};
2-1. router.push 메서드
const onClick = (id, title) => {
router.push(
{
// URL 설정, 정보
pathname: `/movies/${id}`,
query: {
title,
},
},
// as : 브라우저에 보이는 URL을 마스킹
`/movies/${id}`
);
};
pathname
과 query
프로퍼티가 있다.pathname
은 요청할 페이지의 경로를 나타내며, query
는 해당 페이지에 전달할 쿼리 매개변수를 나타낸다.as
라고 부른다.as
를 사용하여 브라우저의 URL을 마스킹할 수 있다. 즉, pathname
은 서버에서 실제 페이지 경로를 나타내고, as
는 클라이언트에서 보여지는 URL을 나타낸다.3. getServerSideProps 이용하기
export default function Detail({ params }) {
const [title, id, src] = params || []; // 1)
return (
<div>
{/* 검색엔진최적화(SEO)에 도움이 될 수 있다. */}
<Seo title={title}></Seo>
<h4>{title}</h4>
</div>
);
}
// fetch를 위한 것이 아니라 조금 더 빠르게 데이터를 가져오기 위해서 쓸 경우
export function getServerSideProps({ params: { params } }) { // 2)
// console.log(params);
return {
props: { params },
};
}
1) []
를 넣지않으면 undefined 때문에 에러가 뜨는 듯하다. 🤔
+)
아직 js들이 다운로드가 안된상태이므로 useRouter()로 정보를 제대로 못가져오고 있다.
따라서 초기에는 빈 배열을 추가해줘서 오류가 발생하지 않도록 해주고, js가 내려가서 다시 렌더링하게되면 그 때는 빈 배열이 아닌 router.query.params
에서 값을 가져올 수 있게 된다고 한다. 😮
2) next.js가 server-side context를 제공해준다.