17 메인 페이지에 실제 데이터 띄워보기 - 카테고리 생성 및 카테고리별로 포스트 아이템 띄워주기
source: categories/study/gatsby/gatsby_9-08.md
URL로부터 선택된 카테고리 파싱하기
저번에 만든 CategoryList 컴포넌트의 아이템을 선택하면 http://localhost:8000/?category=Web
과 같은 링크로 이동하도록 구현했습니다.
따라서 저희는 URL로부터 카테고리 아이템을 파싱해와야합니다.
기본적으로 Gatsby의 페이지 컴포넌트에서는 URL의 구성요소 중, Query에 대한 부분(?category=Web)을 Props로 받을 수 있습니다.
location 객체 내의 search 프로퍼티에 Query에 대한 부분이 들어있는데, 저희는 해당 부분을 URL Parser 라이브러리인 query-string 라이브러리를 통해 객체로 변환한 후, 해당 값을 저장하겠습니다.
아래의 커맨드를 통해 필요한 라이브러리를 설치해줍시다.
yarn add query-string
그리고, Props를 받는 부분과 타입을 다음과 같이 변경해줍니다.
src/pages/index.tsx
// ...
type IndexPageProps = {
location: {
search: string
}
data: {
allMarkdownRemark: {
edges: PostListItemType[]
}
file: {
childImageSharp: {
gatsbyImageData: IGatsbyImageData
}
}
}
}
// ...
const IndexPage: FunctionComponent<IndexPageProps> = function ({
location: { search },
data: {
allMarkdownRemark: { edges },
file: {
childImageSharp: { gatsbyImageData },
},
},
}) {
// ...
}
이제 실제로 Query를 파싱하는 부분을 구현해봅시다.
해당 기능은 query-string 라이브러리에서 제공하는 parse 함수를 통해 구현할 수 있습니다.
src/pages/index.tsx
// ...
import queryString, { ParsedQuery } from 'query-string'
type IndexPageProps = {
location: {
search: string
}
data: {
allMarkdownRemark: {
edges: PostListItemType[]
}
file: {
childImageSharp: {
gatsbyImageData: IGatsbyImageData
}
}
}
}
// ...
const IndexPage: FunctionComponent<IndexPageProps> = function ({
location: { search },
data: {
allMarkdownRemark: { edges },
file: {
childImageSharp: { gatsbyImageData },
},
},
}) {
const parsed: ParsedQuery<string> = queryString.parse(search)
const selectedCategory: string =
typeof parsed.category !== 'string' || !parsed.category
? 'All'
: parsed.category
return (
<Container>
<GlobalStyle />
<Introduction profileImage={gatsbyImageData} />
<CategoryList
selectedCategory={selectedCategory}
categoryList={CATEGORY_LIST}
/>
<PostList posts={edges} />
<Footer />
</Container>
)
}
// ...
URL의 Query 부분을 그대로 parse 함수를 통해 객체 형태로 변환시켰습니다.
해당 타입은 query-string 라이브러리에서 제공해주는 ParsedQuery 타입을 지정해주어야 하고, 해당 객체 프로퍼티의 타입인 string을 제네릭으로 지정해주어야 합니다.
그리고, category 프로퍼티 값이 문자열 형태가 아니거나 존재하지 않는 경우에는 기본적으로 카테고리 값을 All로 지정하고, 그러지 않은 경우에는 파싱한 값을 지정해주었습니다.
따라서 Query 부분에 카테고리 값이 존재하지 않거나, 타입이 맞지 않는 경우에는 모든 게시글을 조회하도록 의도하여 구현했습니다.
이 과정을 통해 구한 카테고리 값은 CategoryList 컴포넌트에 Props로 넘겨주었습니다.
이 상태에서 로컬 서버를 실행하고, 카테고리 아이템을 선택해보세요.
URL이 잘 변경되고, 해당 카테고리 아이템에 스타일이 잘 적용되는지 확인해주세요.
마크다운 데이터로 카테고리 목록 만들기
저희는 임의적으로 카테고리 목록 객체를 만들어 이를 넘겨주었는데, 형태는 카테고리 이름이 프로퍼티 이름이고 해당 카테고리 포스트 개수가 프로퍼티 값으로 되어있었습니다.
따라서 Props로 받은 edges 데이터를 통해 해당 객체의 형태를 따라 카테고리 목록을 만들어보겠습니다.
이를 위해 reduce 메서드를 사용할 것인데, 해당 메서드의 사용 방법에 대해 잘 모른다면 검색을 통해 먼저 찾아보고 오시는 것을 추천드립니다.
먼저, 구현 결과는 다음과 같습니다.
src/pages/index.tsx
import React, { FunctionComponent, useMemo } from 'react'
// ...
// CATEGORY_LIST 상수 제거
const IndexPage: FunctionComponent<IndexPageProps> = function ({
location: { search },
data: {
allMarkdownRemark: { edges },
file: {
childImageSharp: { gatsbyImageData },
},
},
}) {
const parsed: ParsedQuery<string> = queryString.parse(search)
const selectedCategory: string =
typeof parsed.category !== 'string' || !parsed.category
? 'All'
: parsed.category
const categoryList = useMemo(
() =>
edges.reduce(
(
list: CategoryListProps['categoryList'],
{
node: {
frontmatter: { categories },
},
}: PostType,
) => {
categories.forEach(category => {
if (list[category] === undefined) list[category] = 1;
else list[category]++;
});
list['All']++;
return list;
},
{ All: 0 },
),
[],
)
return (
<Container>
<GlobalStyle />
<Introduction profileImage={gatsbyImageData} />
<CategoryList
selectedCategory={selectedCategory}
categoryList={categoryList}
/>
<PostList posts={edges} />
<Footer />
</Container>
)
}
// ...
edges 배열로 reduce 메서드를 호출하며 카테고리 목록을 만드는 함수와 이름이 All이고 값이 0인 프로퍼티가 존재하는 객체를 디폴트값으로 넘겨주었습니다.
파라미터로 넘긴 함수에서는 edges 배열의 각 요소 내에 있는 카테고리 값을 추출해내고, 반복문을 통해 객체에 값을 추가해주었습니다.
코드 자체는 어렵지 않으니 코드를 꼼꼼하게 읽어보고 오시는 것도 좋을 것 같습니다.
그리고, 카테고리 목록은 처음 생성 후 바뀌는 경우가 존재하지 않기 때문에 useMemo 함수를 통해 감싸줌으로써 불필요하게 재연산되지 않도록 구현했습니다.
그럼 이제 로컬 서버를 실행시켜 카테고리 목록이 잘 나오는지 확인해보세요.
카테고리 별로 포스트 아이템 띄워주기
그럼 이제 파싱한 카테고리 값을 통해 해당 카테고리의 포스트 아이템만 띄워주도록 구현해봅시다.
가장 먼저 PostList 컴포넌트에서 카테고리 값을 받을 수 있도록 아래와 같이 수정해주겠습니다.
src/components/Main/PostList.tsx
// ...
type PostListProps = {
selectedCategory: string
posts: PostListItemType[]
}
// ...
const PostList: FunctionComponent<PostListProps> = function ({
selectedCategory,
posts,
}) {
// ...
}
src/pages/index.tsx
// ...
const IndexPage: FunctionComponent<IndexPageProps> = function ({
location: { search },
data: {
allMarkdownRemark: { edges },
file: {
childImageSharp: { gatsbyImageData },
},
},
}) {
// ...
return (
<Container>
<GlobalStyle />
<Introduction profileImage={gatsbyImageData} />
<CategoryList
selectedCategory={selectedCategory}
categoryList={categoryList}
/>
<PostList selectedCategory={selectedCategory} posts={edges} />
<Footer />
</Container>
)
}
// ...
그럼 이제 PostList 컴포넌트로 이동해봅시다.
여기에서는 Props로 받은 selectedCategory 값을 가지고 있는 포스트 아이템만 필터링하는 기능을 구현해야 하는데, 이를 위해 filter 메서드를 사용하겠습니다.
이 메서드도 마찬가지로 사용 방법이 익숙치 않은 분들은 먼저 찾아보고 오시는 것을 추천드립니다.
해당 부분도 코드가 어렵지 않기 때문에 구현 결과부터 보여드리겠습니다.
src/components/Main/PostList.tsx
import React, { FunctionComponent, useMemo } from 'react'
// ...
const PostList: FunctionComponent<PostListProps> = function ({
selectedCategory,
posts,
}) {
const postListData = useMemo(
() =>
posts.filter(({ node: { frontmatter: { categories } } }: PostListItemType) =>
selectedCategory !== 'All'
? categories.includes(selectedCategory)
: true,
),
[selectedCategory],
)
return (
<PostListWrapper>
{postListData.map(({ node: { id, frontmatter } }: PostListItemType) => (
<PostItem {...frontmatter} link="https://www.google.co.kr/" key={id} />
))}
</PostListWrapper>
)
}
export default PostList
만약 선택된 카테고리가 존재하면서 All이 아닌 경우에는 해당 카테고리 값을 가진 포스트 아이템만 필터링하도록 했고, 그렇지 않은 경우에는 모든 포스트 아이템을 보여주도록 구현했습니다.
이 부분도 마찬가지로 불필요하게 재연산되지 않도록 useMemo 함수로 감싸주었습니다.
그리고 posts 데이터가 아닌 새로 생성한 postListData를 통해 PostItem 컴포넌트를 생성해줌으로써 구현이 끝나게 됩니다.
그럼 이제 임의로 마크다운 문서를 여러 개 생성한 다음, 구현한 기능이 잘 동작하는지 확인해주세요.