Next.js로 다중 작성자 블로그 만들기
게시 됨: 2022-03-10이 기사에서는 두 명 이상의 작성자를 지원하는 Next.js로 블로그를 구축할 것입니다. 우리는 각 게시물을 작성자에게 표시하고 게시물과 함께 이름과 사진을 표시합니다. 또한 각 작성자는 자신이 기여한 모든 게시물을 나열하는 프로필 페이지를 얻습니다. 다음과 같이 보일 것입니다.
우리는 모든 정보를 로컬 파일 시스템의 파일에 보관할 것입니다. 게시물과 작성자의 두 가지 콘텐츠 유형은 서로 다른 유형의 파일을 사용합니다. 텍스트가 많은 게시물은 마크다운을 사용하여 편집 프로세스가 더 쉬워집니다. 작성자에 대한 정보가 더 가볍기 때문에 JSON 파일에 보관합니다. 도우미 기능을 사용하면 다양한 파일 형식을 읽고 내용을 더 쉽게 결합할 수 있습니다.
Next.js를 사용하면 다양한 소스와 다양한 유형의 데이터를 손쉽게 읽을 수 있습니다. 동적 라우팅 및 next/link
덕분에 사이트의 다양한 페이지를 빠르게 구축하고 탐색할 수 있습니다. next/image
패키지를 사용하면 이미지 최적화도 무료로 제공됩니다.
"배터리 포함" Next.js를 선택하면 애플리케이션 자체에 집중할 수 있습니다. 우리는 새로운 프로젝트에서 자주 발생하는 반복적인 기초 작업에 시간을 할애할 필요가 없습니다. 모든 것을 손으로 구축하는 대신 테스트되고 입증된 프레임워크에 의존할 수 있습니다. Next.js 뒤에 있는 크고 활동적인 커뮤니티를 통해 문제가 발생할 경우 도움을 쉽게 얻을 수 있습니다.
이 글을 읽고 나면 하나의 Next.js 프로젝트에 많은 종류의 콘텐츠를 추가할 수 있을 것입니다. 그들 사이에 관계를 만들 수도 있습니다. 이를 통해 작가와 게시물, 코스와 수업, 배우와 영화 등을 연결할 수 있습니다.
이 기사에서는 Next.js에 대한 기본적인 지식이 있다고 가정합니다. 이전에 사용하지 않았다면 페이지를 처리하고 페이지에 대한 데이터를 먼저 가져오는 방법에 대해 읽어볼 수 있습니다.
이 기사에서는 스타일 지정을 다루지 않고 대신 모든 것이 작동하도록 하는 데 중점을 둡니다. GitHub에서 결과를 얻을 수 있습니다. 이 기사를 따르고 싶다면 프로젝트에 넣을 수 있는 스타일시트도 있습니다. 탐색을 포함하여 동일한 프레임을 얻으려면 pages/_app.js
를 이 파일로 바꾸십시오.
설정
create-next-app
을 사용하여 새 프로젝트를 설정하고 해당 디렉토리로 변경하여 시작합니다.
$ npx create-next-app multiauthor-blog $ cd multiauthor-blog
나중에 Markdown 파일을 읽어야 합니다. 이를 쉽게 하기 위해 시작하기 전에 몇 가지 종속성을 추가합니다.
multiauthor-blog$ yarn add gray-matter remark remark-html
설치가 완료되면 dev
스크립트를 실행하여 프로젝트를 시작할 수 있습니다.
multiauthor-blog$ yarn dev
이제 사이트를 탐색할 수 있습니다. 브라우저에서 https://localhost:3000을 엽니다. create-next-app에 의해 추가된 기본 페이지가 표시되어야 합니다.
잠시 후 페이지에 도달하려면 탐색이 필요합니다. 페이지가 존재하기 전에도 pages/_app.js
에 추가할 수 있습니다.
import Link from 'next/link' import '../styles/globals.css' export default function App({ Component, pageProps }) { return ( <> <header> <nav> <ul> <li> <Link href="/"> <a>Home</a> </Link> </li> <li> <Link href="/posts"> <a>Posts</a> </Link> </li> <li> <Link href="/authors"> <a>Authors</a> </Link> </li> </ul> </nav> </header> <main> <Component {...pageProps} /> </main> </> ) }
이 문서 전체에서 탐색이 가리키는 누락된 페이지를 추가합니다. 블로그 개요 페이지에서 작업할 수 있도록 먼저 몇 가지 게시물을 추가해 보겠습니다.
게시물 작성
콘텐츠를 코드와 분리하여 유지하기 위해 게시물을 _posts/
라는 디렉토리에 넣습니다. 작성 및 편집을 쉽게 하기 위해 각 게시물을 Markdown 파일로 생성합니다. 각 게시물의 파일 이름은 나중에 경로에서 슬러그 역할을 합니다. 예를 들어 _posts/hello-world.md
파일은 /posts/hello-world
아래에서 액세스할 수 있습니다.
전체 제목 및 짧은 발췌문과 같은 일부 정보는 파일의 시작 부분에 표시됩니다.
--- title: "Hello World!" excerpt: "This is my first blog post." createdAt: "2021-05-03" --- Hey, how are you doing? Welcome to my blog. In this post, …
블로그가 비어 있는 상태로 시작되지 않도록 다음과 같은 파일을 몇 개 더 추가합니다.
multi-author-blog/ ├─ _posts/ │ ├─ hello-world.md │ ├─ multi-author-blog-in-nextjs.md │ ├─ styling-react-with-tailwind.md │ └─ ten-hidden-gems-in-javascript.md └─ pages/ └─ …
자신의 게시물을 추가하거나 GitHub 리포지토리에서 이러한 샘플 게시물을 가져올 수 있습니다.
모든 게시물 나열
이제 몇 개의 게시물이 있으므로 이를 블로그에 올릴 방법이 필요합니다. 블로그의 색인 역할을 하는 모든 항목을 나열하는 페이지를 추가하는 것으로 시작하겠습니다.
Next.js에서는 pages/posts/index.js
아래에 생성된 파일이 사이트에서 /posts
로 액세스할 수 있습니다. 파일은 해당 페이지의 본문 역할을 할 함수를 내보내야 합니다. 첫 번째 버전은 다음과 같습니다.
export default function Posts() { return ( <div className="posts"> <h1>Posts</h1> {/* TODO: render posts */} </div> ) }
아직 Markdown 파일을 읽을 수 있는 방법이 없기 때문에 멀리 가지 못합니다. 이미 https://localhost:3000/posts로 이동할 수 있지만 제목만 표시됩니다.
우리는 이제 거기에 우리의 게시물을 올릴 방법이 필요합니다. Next.js는 getStaticProps()
라는 함수를 사용하여 페이지 구성 요소에 데이터를 전달합니다. 이 함수는 반환된 객체의 props
를 props로 구성 요소에 전달합니다.
getStaticProps()
에서 post 라는 prop으로 구성 요소에 posts
을 전달할 것입니다. 이 첫 번째 단계에서 두 개의 자리 표시자 게시물을 하드코딩할 것입니다. 이 방법으로 시작하여 나중에 실제 게시물을 받을 형식을 정의합니다. 도우미 함수가 이 형식으로 반환하면 구성 요소를 변경하지 않고 해당 형식으로 전환할 수 있습니다.
게시물 개요에는 게시물의 전체 텍스트가 표시되지 않습니다. 이 페이지의 경우 각 게시물의 제목, 발췌문, 퍼머링크 및 날짜로 충분합니다.
export default function Posts() { … } +export function getStaticProps() { + return { + props: { + posts: [ + { + title: "My first post", + createdAt: "2021-05-01", + excerpt: "A short excerpt summarizing the post.", + permalink: "/posts/my-first-post", + slug: "my-first-post", + }, { + title: "My second post", + createdAt: "2021-05-04", + excerpt: "Another summary that is short.", + permalink: "/posts/my-second-post", + slug: "my-second-post", + } + ] + } + } +}
연결을 확인하기 위해 소품에서 게시물을 가져와 Posts
구성 요소에 표시할 수 있습니다. 제목, 작성 날짜, 발췌문 및 게시물 링크를 포함합니다. 현재로서는 그 링크가 아직 아무데도 연결되지 않을 것입니다.
+import Link from 'next/link' -export default function Posts() { +export default function Posts({ posts }) { return ( <div className="posts"> <h1>Posts</h1> - {/* TODO: render posts */} + {posts.map(post => { + const prettyDate = new Date(post.createdAt).toLocaleString('en-US', { + month: 'short', + day: '2-digit', + year: 'numeric', + }) + + return ( + <article key={post.slug}> + <h2> + <Link href={post.permalink}> + <a>{post.title}</a> + </Link> + </h2> + + <time dateTime={post.createdAt}>{prettyDate}</time> + + <p>{post.excerpt}</p> + + <Link href={post.permalink}> + <a>Read more →</a> + </Link> + </article> + ) + })} </div> ) } export function getStaticProps() { … }
브라우저에서 페이지를 다시 로드하면 이제 다음 두 개의 게시물이 표시됩니다.
우리는 getStaticProps()
에 있는 모든 블로그 게시물을 영원히 하드코딩하고 싶지 않습니다. 결국, 그것이 우리가 이전에 _posts/
디렉토리에 이 모든 파일을 만든 이유입니다. 이제 이러한 파일을 읽고 해당 콘텐츠를 페이지 구성 요소에 전달할 방법이 필요합니다.
우리가 할 수 있는 몇 가지 방법이 있습니다. getStaticProps()
에서 바로 파일을 읽을 수 있습니다. 이 함수는 클라이언트가 아닌 서버에서 실행되기 때문에 fs
와 같은 기본 Node.js 모듈에 액세스할 수 있습니다. 페이지 구성 요소를 유지하는 동일한 파일에서 로컬 파일을 읽고, 변환하고, 조작할 수도 있습니다.
파일을 짧게 유지하고 하나의 작업에 집중하기 위해 대신 해당 기능을 별도의 파일로 이동합니다. 그렇게 하면 Posts
구성 요소는 데이터 자체를 읽을 필요 없이 데이터만 표시하면 됩니다. 이것은 우리 프로젝트에 약간의 분리와 조직을 추가합니다.
관례에 따라 lib/api.js
라는 파일에 데이터를 읽는 함수를 넣을 것입니다. 이 파일에는 콘텐츠를 표시하는 구성 요소에 대한 콘텐츠를 가져오는 모든 기능이 들어 있습니다.
게시물 개요 페이지의 경우 모든 게시물을 읽고 처리하고 반환하는 기능이 필요합니다. 우리는 그것을 getAllPosts()
라고 부를 것입니다. 여기에서 먼저 path.join()
을 사용하여 _posts/
디렉토리에 대한 경로를 빌드합니다. 그런 다음 fs.readdirSync()
를 사용하여 해당 디렉토리를 읽고 그 안에 있는 모든 파일의 이름을 제공합니다. 이 이름을 매핑한 다음 각 파일을 차례로 읽습니다.
import fs from 'fs' import path from 'path' export function getAllPosts() { const postsDirectory = path.join(process.cwd(), '_posts') const filenames = fs.readdirSync(postsDirectory) return filenames.map(filename => { const file = fs.readFileSync(path.join(process.cwd(), '_posts', filename), 'utf8') // TODO: transform and return file }) }
파일을 읽은 후 내용을 긴 문자열로 얻습니다. 게시물의 텍스트에서 전면 내용을 분리하기 위해 해당 문자열을 gray-matter
를 통해 실행합니다. 또한 파일 이름 끝에서 .md
를 제거하여 각 게시물의 슬러그를 가져올 것입니다. 나중에 게시물에 액세스할 수 있는 URL을 빌드하려면 해당 슬러그가 필요합니다. 이 함수에 대한 포스트의 Markdown 본문이 필요하지 않으므로 나머지 내용은 무시할 수 있습니다.
import fs from 'fs' import path from 'path' +import matter from 'gray-matter' export function getAllPosts() { const postsDirectory = path.join(process.cwd(), '_posts') const filenames = fs.readdirSync(postsDirectory) return filenames.map(filename => { const file = fs.readFileSync(path.join(process.cwd(), '_posts', filename), 'utf8') - // TODO: transform and return file + // get frontmatter + const { data } = matter(file) + + // get slug from filename + const slug = filename.replace(/\.md$/, '') + + // return combined frontmatter and slug; build permalink + return { + ...data, + slug, + permalink: `/posts/${slug}`, + } }) }
여기에서 ...data
를 반환된 객체에 어떻게 퍼뜨렸는지 주목하세요. 이를 통해 나중에 { {post.title}
대신 {post.data.title}
post.title}로 앞부분의 값에 액세스할 수 있습니다.
게시물 개요 페이지로 돌아가서 이제 자리 표시자 게시물을 이 새로운 기능으로 교체할 수 있습니다.
+import { getAllPosts } from '../../lib/api' export default function Posts({ posts }) { … } export function getStaticProps() { return { props: { - posts: [ - { - title: "My first post", - createdAt: "2021-05-01", - excerpt: "A short excerpt summarizing the post.", - permalink: "/posts/my-first-post", - slug: "my-first-post", - }, { - title: "My second post", - createdAt: "2021-05-04", - excerpt: "Another summary that is short.", - permalink: "/posts/my-second-post", - slug: "my-second-post", - } - ] + posts: getAllPosts(), } } }
브라우저를 다시 로드하면 이전에 있던 자리 표시자 대신 실제 게시물이 표시됩니다.
개별 게시물 페이지 추가
각 게시물에 추가한 링크는 아직 아무데도 연결되지 않습니다. 아직 /posts/hello-world
와 같은 URL에 응답하는 페이지가 없습니다. 동적 라우팅을 사용하면 이와 같이 모든 경로와 일치하는 페이지를 추가할 수 있습니다.
pages/posts/[slug].js
로 생성된 파일은 /posts/abc
와 같은 모든 URL과 일치합니다. URL에서 [slug]
대신 표시되는 값은 페이지에서 쿼리 매개변수로 사용할 수 있습니다. 해당 페이지의 getStaticProps()
에서 params.slug
로 사용하여 도우미 함수를 호출할 수 있습니다.
getAllPosts()
의 대응물로서 해당 도우미 함수 getPostBySlug(slug)
를 호출합니다. 모든 게시물 대신 우리가 전달한 슬러그와 일치하는 단일 게시물을 반환합니다. 게시물 페이지에서 기본 파일의 Markdown 콘텐츠도 표시해야 합니다.
개별 게시물에 대한 페이지는 게시물 개요에 대한 페이지와 같습니다. getStaticProps()
에서 페이지에 posts
을 전달하는 대신 단일 post
만 전달합니다. 게시물의 Markdown 본문을 사용 가능한 HTML로 변환하는 방법을 살펴보기 전에 먼저 일반 설정을 수행해 보겠습니다. 다음 단계에서 즉시 추가할 도우미 기능을 사용하여 여기에서 자리 표시자 게시물을 건너뛸 것입니다.
import { getPostBySlug } from '../../lib/api' export default function Post({ post }) { const prettyDate = new Date(post.createdAt).toLocaleString('en-US', { month: 'short', day: '2-digit', year: 'numeric', }) return ( <div className="post"> <h1>{post.title}</h1> <time dateTime={post.createdAt}>{prettyDate}</time> {/* TODO: render body */} </div> ) } export function getStaticProps({ params }) { return { props: { post: getPostBySlug(params.slug), }, } }
이제 헬퍼 파일 lib/api.js
에 getPostBySlug(slug)
함수를 추가해야 합니다. 몇 가지 주목할 만한 차이점이 있는 getAllPosts()
와 비슷합니다. 슬러그에서 게시물의 파일 이름을 얻을 수 있기 때문에 전체 디렉토리를 먼저 읽을 필요가 없습니다. 슬러그가 'hello-world'
인 경우 _posts/hello-world.md
라는 파일을 읽게 됩니다. 해당 파일이 없으면 Next.js는 404 오류 페이지를 표시합니다.
getAllPosts()
의 또 다른 차이점은 이번에는 게시물의 Markdown 콘텐츠도 읽어야 한다는 것입니다. 먼저 주석으로 처리하여 원시 remark
대신 렌더링 준비 HTML로 반환할 수 있습니다.
import fs from 'fs' import path from 'path' import matter from 'gray-matter' +import remark from 'remark' +import html from 'remark-html' export function getAllPosts() { … } +export function getPostBySlug(slug) { + const file = fs.readFileSync(path.join(process.cwd(), '_posts', `${slug}.md`), 'utf8') + + const { + content, + data, + } = matter(file) + + const body = remark().use(html).processSync(content).toString() + + return { + ...data, + body, + } +}
이론적으로 getPostBySlug(slug)
내부에서 getAllPosts getAllPosts()
함수를 사용할 수 있습니다. 먼저 모든 게시물을 가져와서 주어진 슬러그와 일치하는 게시물을 검색할 수 있습니다. 그것은 우리가 하나의 게시물을 얻기 전에 항상 모든 게시물을 읽어야 한다는 것을 의미합니다. 이는 불필요한 작업입니다. getAllPosts()
는 게시물의 마크다운 콘텐츠도 반환하지 않습니다. 그렇게 하도록 업데이트할 수 있습니다. 이 경우 현재 필요한 것보다 더 많은 작업을 수행합니다.
두 도우미 함수는 다른 작업을 수행하므로 별도로 유지합니다. 그렇게 하면 각 기능이 수행해야 하는 작업에만 기능에 집중할 수 있습니다.
동적 라우팅을 사용하는 페이지는 getStaticPaths()
옆에 getStaticProps()
() 를 제공할 수 있습니다. 이 함수는 페이지를 빌드할 동적 경로 세그먼트의 값을 Next.js에 알려줍니다. getAllPosts()
를 사용하고 각 게시물의 slug
를 정의하는 객체 목록을 반환하여 이를 제공할 수 있습니다.
-import { getPostBySlug } from '../../lib/api' +import { getAllPosts, getPostBySlug } from '../../lib/api' export default function Post({ post }) { … } export function getStaticProps({ params }) { … } +export function getStaticPaths() { + return { + fallback: false, + paths: getAllPosts().map(post => ({ + params: { + slug: post.slug, + }, + })), + } +}
getPostBySlug(slug)
에서 Markdown 콘텐츠를 구문 분석하므로 이제 페이지에서 렌더링할 수 있습니다. Next.js가 post.body
뒤에서 HTML을 렌더링할 수 있도록 이 단계에서 dangerouslySetInnerHTML
하게SetInnerHTML을 사용해야 합니다. 이름에도 불구하고 이 시나리오에서는 속성을 사용하는 것이 안전합니다. 우리는 우리의 게시물에 대한 완전한 통제권을 가지고 있기 때문에 그들이 안전하지 않은 스크립트를 주입할 가능성은 거의 없습니다.
import { getAllPosts, getPostBySlug } from '../../lib/api' export default function Post({ post }) { const prettyDate = new Date(post.createdAt).toLocaleString('en-US', { month: 'short', day: '2-digit', year: 'numeric', }) return ( <div className="post"> <h1>{post.title}</h1> <time dateTime={post.createdAt}>{prettyDate}</time> - {/* TODO: render body */} + <div dangerouslySetInnerHTML={{ __html: post.body }} /> </div> ) } export function getStaticProps({ params }) { … } export function getStaticPaths() { … }
게시물 개요의 링크 중 하나를 따라가면 이제 해당 게시물의 자체 페이지로 이동합니다.
작성자 추가
이제 게시물이 연결되었으므로 작성자에 대해 동일한 단계를 반복해야 합니다. 이번에는 Markdown 대신 JSON을 사용하여 설명하겠습니다. 의미가 있을 때마다 이와 같이 동일한 프로젝트에서 서로 다른 유형의 파일을 혼합할 수 있습니다. 파일을 읽는 데 사용하는 도우미 함수는 차이점을 처리합니다. 페이지는 콘텐츠를 저장하는 형식을 알지 못해도 이러한 기능을 사용할 수 있습니다.
먼저 _authors/
라는 디렉토리를 만들고 여기에 몇 개의 작성자 파일을 추가합니다. 게시물과 마찬가지로 각 작성자의 슬러그로 파일 이름을 지정합니다. 나중에 작성자를 찾는 데 사용할 것입니다. 각 파일에서 JSON 개체에 작성자의 전체 이름을 지정합니다.
{ "name": "Adrian Webber" }
현재로서는 우리 프로젝트에 두 명의 저자가 있으면 충분합니다.
그들에게 좀 더 개성을 주기 위해 각 작성자의 프로필 사진도 추가해 보겠습니다. 이러한 정적 파일을 public/
디렉터리에 넣습니다. 동일한 슬러그로 파일 이름을 지정하면 묵시적 규칙만 사용하여 파일을 연결할 수 있습니다. 각 작성자의 JSON 파일에 그림의 경로를 추가하여 둘을 연결할 수 있습니다. 모든 파일의 이름을 슬러그로 지정하면 기록하지 않고도 이 연결을 관리할 수 있습니다. JSON 객체는 코드로 작성할 수 없는 정보만 보유하면 됩니다.
완료되면 프로젝트 디렉토리가 다음과 같아야 합니다.
multi-author-blog/ ├─ _authors/ │ ├─ adrian-webber.json │ └─ megan-carter.json ├─ _posts/ │ └─ … ├─ pages/ │ └─ … └─ public/ ├─ adrian-webber.jpg └─ megan-carter.jpg
게시물과 마찬가지로 이제 모든 작성자를 읽고 개별 작성자를 가져오는 도우미 함수가 필요합니다. 새로운 함수 getAllAuthors()
및 getAuthorBySlug(slug)
도 lib/api.js
에 들어갑니다. 그들은 포스트 대응과 거의 똑같은 일을 합니다. 저자를 설명하기 위해 JSON을 사용하기 때문에 여기에서 주석이 있는 remark
을 구문 분석할 필요가 없습니다. 우리는 또한 gray-matter
이 필요하지 않습니다. 대신 JavaScript의 내장 JSON.parse()
를 사용하여 파일의 텍스트 내용을 객체로 읽을 수 있습니다.
const contents = fs.readFileSync(somePath, 'utf8') // ⇒ looks like an object, but is a string // eg '{ "name": "John Doe" }' const json = JSON.parse(contents) // ⇒ a real JavaScript object we can do things with // eg { name: "John Doe" }
이러한 지식을 바탕으로 도우미 기능은 다음과 같습니다.
export function getAllPosts() { … } export function getPostBySlug(slug) { … } +export function getAllAuthors() { + const authorsDirectory = path.join(process.cwd(), '_authors') + const filenames = fs.readdirSync(authorsDirectory) + + return filenames.map(filename => { + const file = fs.readFileSync(path.join(process.cwd(), '_authors', filename), 'utf8') + + // get data + const data = JSON.parse(file) + + // get slug from filename + const slug = filename.replace(/\.json/, '') + + // return combined frontmatter and slug; build permalink + return { + ...data, + slug, + permalink: `/authors/${slug}`, + profilePictureUrl: `${slug}.jpg`, + } + }) +} + +export function getAuthorBySlug(slug) { + const file = fs.readFileSync(path.join(process.cwd(), '_authors', `${slug}.json`), 'utf8') + + const data = JSON.parse(file) + + return { + ...data, + permalink: `/authors/${slug}`, + profilePictureUrl: `/${slug}.jpg`, + slug, + } +}
애플리케이션에 작성자를 읽는 방법을 사용하여 이제 작성자를 모두 나열하는 페이지를 추가할 수 있습니다. pages/authors/index.js
아래에 새 페이지를 만들면 사이트에 /authors
페이지가 제공됩니다.
도우미 함수는 우리를 위해 파일 읽기를 처리합니다. 이 페이지 구성 요소는 작성자가 파일 시스템의 JSON 파일임을 알 필요가 없습니다. 데이터를 가져오는 위치 또는 방법을 모른 채 getAllAuthors()
를 사용할 수 있습니다. 도우미 함수가 작업할 수 있는 형식으로 데이터를 반환하는 한 형식은 중요하지 않습니다. 이와 같은 추상화를 통해 애플리케이션 전체에서 다양한 유형의 콘텐츠를 혼합할 수 있습니다.
작성자의 색인 페이지는 게시물의 색인 페이지와 매우 유사합니다. getStaticProps()
에서 모든 작성자를 가져오고 Authors
구성 요소에 전달합니다. 해당 구성 요소는 각 작성자를 매핑하고 이에 대한 일부 정보를 나열합니다. 슬러그에서 다른 링크나 URL을 만들 필요가 없습니다. 도우미 함수는 이미 사용 가능한 형식으로 작성자를 반환합니다.
import Image from 'next/image' import Link from 'next/link' import { getAllAuthors } from '../../lib/api/authors' export default function Authors({ authors }) { return ( <div className="authors"> <h1>Authors</h1> {authors.map(author => ( <div key={author.slug}> <h2> <Link href={author.permalink}> <a>{author.name}</a> </Link> </h2> <Image alt={author.name} src={author.profilePictureUrl} height="40" width="40" /> <Link href={author.permalink}> <a>Go to profile →</a> </Link> </div> ))} </div> ) } export function getStaticProps() { return { props: { authors: getAllAuthors(), }, } }
사이트에서 /authors
를 방문하면 이름과 사진과 함께 모든 저자 목록이 표시됩니다.
저자 프로필에 대한 링크는 아직 아무데도 연결되지 않습니다. 프로필 페이지를 추가하기 위해 pages/authors/[slug].js
아래에 파일을 만듭니다. 작성자에게는 텍스트 콘텐츠가 없기 때문에 지금 추가할 수 있는 것은 이름과 프로필 사진뿐입니다. 또한 Next.js에 페이지를 빌드할 슬러그를 알려주는 또 다른 getStaticPaths()
가 필요합니다.
import Image from 'next/image' import { getAllAuthors, getAuthorBySlug } from '../../lib/api' export default function Author({ author }) { return ( <div className="author"> <h1>{author.name}</h1> <Image alt={author.name} src={author.profilePictureUrl} height="80" width="80" /> </div> ) } export function getStaticProps({ params }) { return { props: { author: getAuthorBySlug(params.slug), }, } } export function getStaticPaths() { return { fallback: false, paths: getAllAuthors().map(author => ({ params: { slug: author.slug, }, })), } }
이를 통해 이제 정보에 대해 매우 가벼운 기본 작성자 프로필 페이지가 생겼습니다.
이 시점에서 작성자와 게시물은 아직 연결되지 않았습니다. 다음에는 각 작성자의 게시물 목록을 프로필 페이지에 추가할 수 있도록 해당 다리를 구축하겠습니다.
게시물과 작성자 연결
두 콘텐츠를 연결하려면 서로를 참조해야 합니다. 우리는 이미 게시물과 작성자를 슬러그로 식별하므로 이를 참조합니다. 게시물에 작성자를 추가하고 작성자에게 게시물을 추가할 수 있지만 한 방향으로만 연결하면 충분합니다. 우리는 게시물을 작성자에게 귀속시키고 싶기 때문에 각 게시물의 전면에 작성자의 슬러그를 추가할 것입니다.
--- title: "Hello World!" excerpt: "This is my first blog post." createdAt: "2021-05-03" +author: adrian-webber --- Hey, how are you doing? Welcome to my blog. In this post, …
그것을 유지하면 gray-matter
을 통해 게시물을 실행하면 작성자 필드가 문자열로 게시물에 추가됩니다.
const post = getPostBySlug("hello-world") const author = post.author console.log(author) // "adrian-webber"
작성자를 나타내는 객체를 가져오기 위해 해당 슬러그를 사용하고 getAuthorBySlug(slug)
를 호출할 수 있습니다.
const post = getPostBySlug("hello-world") -const author = post.author +const author = getAuthorBySlug(post.author) console.log(author) // { // name: "Adrian Webber", // slug: "adrian-webber", // profilePictureUrl: "/adrian-webber.jpg", // permalink: "/authors/adrian-webber" // }
단일 게시물의 페이지에 작성자를 추가하려면 getStaticProps()
에서 getAuthorBySlug(slug)
를 한 번 호출해야 합니다.
+import Image from 'next/image' +import Link from 'next/link' -import { getPostBySlug } from '../../lib/api' +import { getAuthorBySlug, getPostBySlug } from '../../lib/api' export default function Post({ post }) { const prettyDate = new Date(post.createdAt).toLocaleString('en-US', { month: 'short', day: '2-digit', year: 'numeric', }) return ( <div className="post"> <h1>{post.title}</h1> <time dateTime={post.createdAt}>{prettyDate}</time> + <div> + <Image alt={post.author.name} src={post.author.profilePictureUrl} height="40" width="40" /> + + <Link href={post.author.permalink}> + <a> + {post.author.name} + </a> + </Link> + </div> <div dangerouslySetInnerHTML={{ __html: post.body }}> </div> ) } export function getStaticProps({ params }) { + const post = getPostBySlug(params.slug) return { props: { - post: getPostBySlug(params.slug), + post: { + ...post, + author: getAuthorBySlug(post.author), + }, }, } }
getStaticProps()
에서 post
라고도 하는 객체에 ...post
를 어떻게 퍼뜨렸는지 주목하세요. 그 줄 뒤에 author
를 배치함으로써 우리는 저자의 문자열 버전을 전체 객체로 대체하게 됩니다. 이를 통해 Post
구성 요소의 post.author.name
을 통해 작성자의 속성에 액세스할 수 있습니다.
이러한 변경으로 이제 게시물 페이지에 작성자의 이름과 사진이 포함된 프로필 페이지 링크가 표시됩니다.
게시물 개요 페이지에 작성자를 추가하려면 비슷한 변경이 필요합니다. getAuthorBySlug(slug)
를 한 번 호출하는 대신 모든 게시물을 매핑하고 각 게시물에 대해 호출해야 합니다.
+import Image from 'next/image' +import Link from 'next/link' -import { getAllPosts } from '../../lib/api' +import { getAllPosts, getAuthorBySlug } from '../../lib/api' export default function Posts({ posts }) { return ( <div className="posts"> <h1>Posts</h1> {posts.map(post => { const prettyDate = new Date(post.createdAt).toLocaleString('en-US', { month: 'short', day: '2-digit', year: 'numeric', }) return ( <article key={post.slug}> <h2> <Link href={post.permalink}> <a>{post.title}</a> </Link> </h2> <time dateTime={post.createdAt}>{prettyDate}</time> + <div> + <Image alt={post.author.name} src={post.author.profilePictureUrl} height="40" width="40" /> + + <span>{post.author.name}</span> + </div> <p>{post.excerpt}</p> <Link href={post.permalink}> <a>Read more →</a> </Link> </article> ) })} </div> ) } export function getStaticProps() { return { props: { - posts: getAllPosts(), + posts: getAllPosts().map(post => ({ + ...post, + author: getAuthorBySlug(post.author), + })), } } }
그러면 게시물 개요의 각 게시물에 작성자가 추가됩니다.
작성자의 게시물 목록을 JSON 파일에 추가할 필요가 없습니다. 그들의 프로필 페이지에서 우리는 먼저 getAllPosts()
가 있는 모든 게시물을 얻습니다. 그런 다음 이 작성자에게 귀속된 항목의 전체 목록을 필터링할 수 있습니다.
import Image from 'next/image' +import Link from 'next/link' -import { getAllAuthors, getAuthorBySlug } from '../../lib/api' +import { getAllAuthors, getAllPosts, getAuthorBySlug } from '../../lib/api' export default function Author({ author }) { return ( <div className="author"> <h1>{author.name}</h1> <Image alt={author.name} src={author.profilePictureUrl} height="40" width="40" /> + <h2>Posts</h2> + + <ul> + {author.posts.map(post => ( + <li> + <Link href={post.permalink}> + <a> + {post.title} + </a> + </Link> + </li> + ))} + </ul> </div> ) } export function getStaticProps({ params }) { const author = getAuthorBySlug(params.slug) return { props: { - author: getAuthorBySlug(params.slug), + author: { + ...author, + posts: getAllPosts().filter(post => post.author === author.slug), + }, }, } } export function getStaticPaths() { … }
이것은 우리에게 모든 저자의 프로필 페이지에 있는 기사 목록을 제공합니다.
작성자 개요 페이지에서는 인터페이스를 복잡하게 하지 않기 위해 작성한 게시물 수만 추가합니다.
import Image from 'next/image' import Link from 'next/link' -import { getAllAuthors } from '../../lib/api' +import { getAllAuthors, getAllPosts } from '../../lib/api' export default function Authors({ authors }) { return ( <div className="authors"> <h1>Authors</h1> {authors.map(author => ( <div key={author.slug}> <h2> <Link href={author.permalink}> <a> {author.name} </a> </Link> </h2> <Image alt={author.name} src={author.profilePictureUrl} height="40" width="40" /> + <p>{author.posts.length} post(s)</p> <Link href={author.permalink}> <a>Go to profile →</a> </Link> </div> ))} </div> ) } export function getStaticProps() { return { props: { - authors: getAllAuthors(), + authors: getAllAuthors().map(author => ({ + ...author, + posts: getAllPosts().filter(post => post.author === author.slug), + })), } } }
이를 통해 작성자 개요 페이지는 각 작성자가 기여한 게시물 수를 보여줍니다.
그리고 그게 다야! 이제 게시물과 작성자가 완전히 연결되었습니다. 우리는 게시물에서 작성자의 프로필 페이지로, 거기에서 다른 게시물로 이동할 수 있습니다.
요약 및 전망
이 기사에서는 고유한 슬러그를 통해 두 가지 관련 유형의 콘텐츠를 연결했습니다. 게시물에서 작성자까지의 관계를 정의하면 다양한 시나리오를 사용할 수 있습니다. 이제 각 게시물에 작성자를 표시하고 프로필 페이지에 게시물을 나열할 수 있습니다.
이 기술을 사용하여 다른 많은 종류의 관계를 추가할 수 있습니다. 각 게시물에는 작성자 위에 검토자가 있을 수 있습니다. reviewer
필드를 게시물의 전면에 추가하여 설정할 수 있습니다.
--- title: "Hello World!" excerpt: "This is my first blog post." createdAt: "2021-05-03" author: adrian-webber +reviewer: megan-carter --- Hey, how are you doing? Welcome to my blog. In this post, …
파일 시스템에서 검토자는 _authors/
디렉토리의 다른 작성자입니다. getAuthorBySlug(slug)
를 사용하여 정보도 얻을 수 있습니다.
export function getStaticProps({ params }) { const post = getPostBySlug(params.slug) return { props: { post: { ...post, author: getAuthorBySlug(post.author), + reviewer: getAuthorBySlug(post.reviewer), }, }, } }
한 사람이 아닌 게시물에 두 명 이상의 작성자를 지정하여 공동 작성자를 지원할 수도 있습니다.
--- title: "Hello World!" excerpt: "This is my first blog post." createdAt: "2021-05-03" -author: adrian-webber +authors: + - adrian-webber + - megan-carter --- Hey, how are you doing? Welcome to my blog. In this post, …
이 시나리오에서는 더 이상 게시물의 getStaticProps()
에서 단일 작성자를 찾을 수 없습니다. 대신 우리는 이 저자 배열을 매핑하여 모두 가져옵니다.
export function getStaticProps({ params }) { const post = getPostBySlug(params.slug) return { props: { post: { ...post, - author: getAuthorBySlug(post.author), + authors: post.authors.map(getAuthorBySlug), }, }, } }
이 기술을 사용하여 다른 종류의 시나리오도 생성할 수 있습니다. 모든 종류의 일대일, 일대다 또는 다대다 관계를 가능하게 합니다. 프로젝트에 뉴스레터와 사례 연구도 포함되어 있는 경우 각각에 저자를 추가할 수도 있습니다.
Marvel 유니버스에 관한 모든 사이트에서 캐릭터와 그들이 등장하는 영화를 연결할 수 있습니다. 스포츠에서는 플레이어와 현재 플레이하는 팀을 연결할 수 있습니다.
도우미 함수는 데이터 원본을 숨기기 때문에 콘텐츠가 다른 시스템에서 올 수 있습니다. 파일 시스템에서 기사를 읽고 API에서 주석을 읽고 이를 코드에 병합할 수 있습니다. 일부 콘텐츠가 다른 유형의 콘텐츠와 관련된 경우 이 패턴으로 연결할 수 있습니다.
추가 리소스
Next.js는 데이터 가져오기 페이지에서 사용한 기능에 대한 더 많은 배경 지식을 제공합니다. 여기에는 다양한 유형의 소스에서 데이터를 가져오는 샘플 프로젝트에 대한 링크가 포함되어 있습니다.
이 스타터 프로젝트를 더 진행하려면 다음 기사를 확인하십시오.
- Strapi 및 Next.js를 사용하여 CSS 트릭 웹사이트 클론 구축
로컬 파일 시스템의 파일을 Strapi 기반 백엔드로 교체하십시오. - Next.js의 스타일링 방법 비교
이 스타터의 스타일을 변경하기 위해 사용자 정의 CSS를 작성하는 다양한 방법을 살펴보십시오. - Next.js를 사용한 마크다운/MDX
Markdown에서 JSX 및 React 구성 요소를 사용할 수 있도록 프로젝트에 MDX를 추가합니다.