리액트에서의 react-router
페이지 컴포넌트 함수는 반드시 JSX를 반환해야 함
=> 그래야 서버에서 함수를 실행하고 렌더링한 HTML을 브라우저로 전송 가능
http://localhost:3000
에 접근했을 때 반환된 JSX 확인 가능http://localhost:3000/contacts
로 접근하면 Contack 페이지 확인 가능http://localhost:3000/contact-us
로 이동 시 확인 가능http://localhost:3000/posts
주소로 posts/ 폴더의 index.js 페이지 함수에 접근 가능pages/ -> pages 폴더
- index.js
- contact-us.js
- posts/ -> pages/ 안에 posts 폴더
- index.js
- [slug].js
경로 매개변수(route variable)란?
- 대괄호로 감싸진 폴더명 또는 파일명 (예: [date], [slug].js)
- 활용 에: 블로그 게시글을 구분할 때
- 경로 매개변수는 중첩 가능
- 예: pages/posts/[date]/[slug].js
=> http://localhost:3000/posts/2023-05-05/four-hours-lecture 로 이동 가능
params를 getServerSideProps 등을 통해 props로 받아서 컴포넌트에 전달
JSX(뷰)에 props 내용을 끼워넣으면 동일한 뷰로 내용만 바뀐 화면 구현 가능
export async function getServerSideProps({ params }) {
const { name } = params;
return {
props: {
name,
},
};
}
function Greet(props) {
return (
<h1> Hello, {props.name}! </h1>
);
}
export default Greet;
=> http://localhost:3000/greet/Sheryl
주소로 가면 화면에서 'Hello, Sheryl!' 이라는 문구 확인
import { useRouter } from 'next/router';
function Greet() {
const { query } = useRouter();
return <h1>Hello {query.name}!</h1>;
}
export default Greet;
useRouter의 query로 경로 매개변수를 가져올 수 있음
query를 console.log로 출력
http://localhost:3000/greet/Sheryl?learning_next.js=true
에 접근하면{ learning_next.js: true, name: Sheryl }
경로 매개변수와 같은 key 이름의 쿼리 매개변수도 지정 가능
<a>
태그 대신 사용되는 내장 컴포넌트Link 안에
<a>
태그를 꼭 넣어야 하나요?
Next.js 13 버전부터 Link 컴포넌트 안의 string을<a>
태그로 감싸지 않아도
자동으로<a>
태그로 감싸짐
import Link from 'next/link';
function Navbar() {
return (
<div>
<Link href='/'>Home</Link>
<Link href='/about'>About</Link>
<Link href='/contacts'>Contacts</Link>
</div>
);
}
export default Navbar;
기본적으로 현재 화면에 표시되는 페이지의 모든 Link로 연결된 페이지를 미리 읽어옴 (preload 기능)
preload={false}
를 전달import Link from 'next/link';
function Navbar() {
return (
<div>
<Link href='/' preload={false}>Home</Link>
...
</div>
);
}
/posts/[date]/[slug].js
페이지를 Link에 연결하기import Link from 'next/link';
function Posts() {
return (
<div>
<Link href='/posts/2023-05-21/queen-charlotte-is-attractive' preload={false}>
Read Post
</Link>
...
</div>
);
}
export default Posts;
<Link
href={{
pathname: 'posts/[date]/[slug].js'
query: {
date: '2023-04-24',
slug: 'start-course',
foo: 'bar'
}
}}
>
Read Post
</Link>
http://localhost:3000/posts/2023-04-24/start-course?foo=bar
로 이동예: 로그인한 사용자만 접근 가능한 페이지를 위해 useAuth 훅 생성
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import PrivateComponent from '../components/Private';
import useAuth from '../hooks/auth';
function MyPage() {
const router = useRouter(); // useRouter 훅으로 router 생성
const { isLogin } = useAuth();
// 로그인이 되지 않았을 때 로그인 페이지로 이동
// useEffect 훅을 사용하여 useEffect 내부 코드가 클라이언트에서만 실행되도록 함
useEffect(() => {
if (!isLogin) {
router.push('/login');
}
}, [isLogin]);
return isLogin ? <PrivateComponent /> : null;
}
router.push({
pathname: 'posts/[date]/[slug].js'
query: {
date: '2023-05-19',
slug: 'end-course',
foo: 'bar'
}
});
http://localhost:3000/posts/2023-05-19/end-course?foo=bar
로 이동누적 레이아웃 이동 (Cumulative Layout Shift, CLS)
이미지를 불러온 뒤 페이지의 레이아웃이 변경되는 현상
예: 이미지 로딩 후 이미지의 높이만큼 문자 영역이 아래로 밀림
<img
src='https://images.unsplash.com/photo-115jlljffaseh3232k'
alt='강아지'
/>
Next.js에서 다음 2가지 방법으로 쉽게 외부 서비스로 이미지 제공 가능
module.exports = {
images: {
domains: ['images.unsplash.com'] // 'http://~' 뒷 부분 작성
}
};
import Image from 'next/image';
function IndexPage() {
return (
<div>
<Image
src='https://images.unsplash.com/photo-115jlljff32k'
width={500}
height={200}
alt='강아지'
/>
</div>
);
}
export default IndexPage;
이미지 크기를 원하는 대로 조절하기
import Image from 'next/image';
function IndexPage() {
return (
<div>
<div
style={{
width: 500,
height: 200,
position: 'relative',
}}
>
<Image
src='https://images.unsplash.com/photo-115jlljff32k'
layout='fill'
objectFit='cover'
alt='강아지'
/>
</div>
</div>
);
}
export default IndexPage;
objectFit: cover에 대해 찾아보다가 알게 된 사실:
next/image 13 버전에서 objectFit: cover => deprecated
참고 링크
기존에 이미지 비율을 맞추던 방법
이제는 next/Image의 새로운 props 중 하나인 fill 속성을 사용
<div style={{position:"relative"}}>
<Image
src={source}
alt=""
fill
style={{ objectFit:"cover" }} // objectFit은 style에 선언
/>
</div>
=> next/Image가 더 이상 css역할을 담당하지 않게 됨
화면 별로 여러 가지 크기의 이미지를 생성하기 위해 기본적으로 img 태그의 srcset 속성 사용
구글 크롬이나 파이어폭스 프라우저에서 이미지 정보를 확인하거나 다른 이름으로 저장하면 원래 이미지가 jpeg 포맷이어도 WebP 포맷으로 제공
=> 이미지 최적화를 Next.js가 아닌 외부 서비스에서 처리하는 방법 필요
next.config.js에 loader 속성을 지정
외부 서비스를 통해 자동 이미지 최적화 작업 처리
예: 외부 서비스 Akamai 사용하기
module.exports = {
images: {
loader: 'akamai',
domains: ['images.unsplash.com']
}
};
import Image from 'next/image';
const loader = ({ src, width, quality }) => {
return `https://example.com/${src}?w=${width}&q=${quality || 75}`;
}
function CustomImage () {
return (
<Image
loader={loader}
src="/myimage.png"
alt="내 이미지"
width={350}
height={540}
/>
);
}
<head>
내부 데이터를 변경할 수 있게 해줌import Head from 'next/head';
function PostHead(props) {
return (
<Head>
<title>{props.title}</title>
<meta name="description" content={props.subtitle} />
{/* og 메타데이터 */}
<meta property="og:title" content={props.title} />
<meta property="og:description" content={props.subtitle} />
<meta property="og:image" content={props.image} />
{/* twitter 카드 메타데이터 */}
<meta name="twitter:card" content="summary" />
<meta name="twitter:card" content={props.title} />
<meta name="twitter:card" content={props.description} />
<meta name="twitter:card" content={props.image} />
</Head>
);
}
export default PostHead;
export default [
{
id: 'cho-co-late',
slug: 'i-love-chocolate',
title: '생초콜릿 맛있어',
subtitle: '김보람초콜릿 짱짱',
image: 'http://images.unsplash.com/아마-초콜릿-사진
},
{
id: 'cat-cat',
slug: 'i-love-cat',
title: '고양이 키우고 싶어',
subtitle: '간택 당해버려',
image: 'http://images.unsplash.com/검은-옷에-고양이-안은-사진'
}
];
import PostHead from '../../components/PostHead';
import posts from '../.../data/posts';
export function getServerSideProps({ parmas }) {
const { slug } = params;
const selectedPost = posts.find((post) => post.slug === slug);
return {
props: {
post: selectedPost,
},
};
}
function Post({ post }) {
return (
<div>
<PostHead {...post } /> // 경로 params로 선택된 post 데이터는 전부 props로 전달
<h1>{post.title}</h1>
<p>{post.subtitle}</p>
</div>
);
}
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp;
import Link from 'next/link';
function Navbar() {
return (
<div>
<div>나의 웹 사이트</div>
<div>
<Link href='/'>Home</Link>
<Link href='/about'>About</Link>
<Link href='/contacts'>Contacts</Link>
</div>
</div>
);
}
export default Navbar;
import Navbar from '../components/Navbar';
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return (
<>
<Navbar />
<Component {...pageProps} />
</>
;
}
export default MyApp;
=> Navbar 컴포넌트가 모든 페이지에 공통으로 들어감
import { createContext } from 'react';
const ThemeContext = createContext({
theme: 'light',
toggleTheme: () => null
});
export default ThemeContext;
import { useState } from 'react';
import ThemeContext from '../components/themeContext';
import Navbar from '../components/Navbar';
import '../styles/globals.css';
const themes = {
dark: {
background: 'black',
color: 'white'
},
light: {
background: 'white',
color: 'black'
}
};
function MyApp({ Component, pageProps }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(theme === 'dark' ? 'light' : 'dark');
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<div
style={{
width: '100%',
minHeight: '100vh',
...themes[theme] // themes 스타일 객체에서 theme 번째 스타일을 가져옴
}}
>
<Navbar />
<Component {...pageProps} />
</div>
</ThemeContext.Provider>
;
}
export default MyApp;
import { useContext } from 'react';
import Link from 'next/link';
import ThemeContext from '../components/themeContext';
function Navbar() {
const { theme, toggleTheme } = useContext(ThemeContext);
const newThemeName = theme === 'dark' ? 'light' : 'dark';
return (
<div>
<div>나의 웹 사이트</div>
<div>
<Link href='/'>Home</Link>
<Link href='/about'>About</Link>
<Link href='/contacts'>Contacts</Link>
<button onClick={toggleTheme}>
{newThemeName} 모드로 변경
</button>
</div>
</div>
);
}
export default Navbar;
// _app.js
import App from 'next/app'; // next/app에서 App 컴포넌트 임포트
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
// 컴포넌트 위쪽이 아닌 밑에서 선언
MyApp.getInitialProps = async (appContext) => {
const appProps = await App.getInitialProps(appContext);
const additionalProps = await fetch(...); // fetch API로 불러온 값
return {
...appProps, // 기본 App 컴포넌트의 props
...additionalProps // fetch로 추가된 props
};
}
<head>
, <html>
, <body>
와 같은 태그를 다루지 않음<head>
태그는 공통 컴포넌트로 만들어서 페이지 별로 메타데이터를 추가했었음<html>
과 <body>
태그는 _document.js 파일에서 처리import Document, { Html, Head, Main, NextScript } from 'next/document';
class MyDocument extends Document { // 불러온 Document를 상속
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
}
render() {
return (
<Html> // * 추가
<Head /> // * 추가
<body>
<Main /> // * 추가
<NextScript /> // * 추가
</body>
</Html>
);
}
}
export default MyDocument;
<html>
태그에 해당