개발 블로그를 직접 만들고 싶어서 시작한 프로젝트입니다.
읽고 싶으면 펴기 / 닫고 싶으면 접기
-
Next.JS
react에서 SSR방식을 사용해 성능을 향상시키고 SEO를 유리하게 하기 위해서 사용했습니다.
-
contentlayer
Next.JS와 호환이 되면서 블로그 기능을 구현할 수 있는 SDK로 Next.js 단독 또는 Gatsby와 같은 다른 프레임워크보다 빌드 시간이 빠릅니다.
-
tailwind
기존 css보다 사용하기 편리하고 컴포넌트에서 스타일 유추가 쉽기 때문에 사용했습니다.
-
rehype-prism-plus
코드 블록을 사용할 때 tailwind를 사용하면 스타일 적용이 안 되기 때문에 코드 블록에 클래스를 추가 하기 위해 사용했습니다.
-
next-themes
라이트모드와 다크모드를 구현하기 위해 현재 테마 정보를 가져오기위해 사용했습니다.
-
cheerio
OpenGraphPreview를 생성할떄 필요한 og정보를 html에서 더 쉽게 추출하기 위해서 사용했습니다.
재사용 가능한 [category].tsx
제 블로그는 크게 home, category, slug 3가지 부분으로 이루어져 있습니다.
category 페이지에는 해당 카테고리의 모든 글을 card 형태로 모아두는 페이지입니다.
처음에는 pages/js, pages/react... 이런 식으로 각각 폴더별로 만들었지만
카테고리의 페이지들은 거의 흡사하게 생겼기 때문에 재사용성을 높이기 위해서 하나의 컴포넌트로 구현했습니다.
|
export const getStaticPaths = async () => { |
|
const links = navlinks.map((navlink: Navlinks) => navlink.link); |
|
const paths = links.map((link: string) => ({ |
|
params: { category: link.slice(1) }, |
|
})); |
|
return { |
|
paths, |
|
fallback: false, |
|
}; |
|
}; |
|
|
|
type PageParams = { |
|
category: string; |
|
}; |
|
|
|
export const getStaticProps = async ( |
|
context: GetStaticPropsContext<PageParams> |
|
) => { |
|
if (!context.params) { |
|
return; |
|
} |
|
const { category } = context.params; |
|
|
|
let posts: Post[] | undefined; |
|
let curDocs: DocumentTypes[] | undefined; |
|
|
|
const handelSortDocs = (curDos: DocumentTypes[]) => { |
|
curDocs = curDos; |
|
posts = curDos.sort( |
|
(a: Post, b: Post) => Number(new Date(b.date)) - Number(new Date(a.date)) |
|
); |
|
}; |
|
|
|
checkCategory(category, handelSortDocs); |
|
|
|
const structuredData = handleStructuredData(); |
|
return { |
|
props: { |
|
posts: posts ? posts : null, |
|
structuredData, |
|
curDocs: curDocs ? curDocs : null, |
|
}, |
|
}; |
|
}; |
- 63번줄에 checkCategory함수는 category, slug 둘다에 쓰이는 스위치 함수입니다.
두번째 인자에 함수를 주입함으로써 category에서는 날짜순으로 문서를 정렬합니다.
|
import * as Articles from "contentlayer/generated"; |
|
import type { DocumentTypes } from ".contentlayer/generated/types"; |
|
|
|
export const checkCategory = ( |
|
category: string, |
|
sortDocsOrgetArticle: (docs: DocumentTypes[]) => void |
|
) => { |
|
switch (category) { |
|
case "js": |
|
sortDocsOrgetArticle(Articles.allJs); |
|
break; |
|
case "types": |
|
sortDocsOrgetArticle(Articles.allTypes); |
|
break; |
|
case "next": |
|
sortDocsOrgetArticle(Articles.allNexts); |
재사용 가능한 [...slug].tsx
제 블로그는 크게 home, category, slug 3가지 부분으로 이루어져 있습니다.
slug 페이지에는 해당 카테고리의 특정 글을 보여주는 페이지입니다.
category 페이지와 마찬가지로 재사용성을 높이기 위해서 구현했습니다.
|
export const getStaticPaths = async () => { |
|
const paths = Articles.allDocuments.map((p: Post) => ({ |
|
params: { slug: [p._raw.sourceFileDir, p.slug] }, |
|
})); |
|
return { |
|
paths, |
|
fallback: false, |
|
}; |
|
}; |
|
|
|
type PageParams = { |
|
slug: string[]; |
|
}; |
|
|
|
export const getStaticProps = async ( |
|
context: GetStaticPropsContext<PageParams> |
|
) => { |
|
if (!context.params) { |
|
return; |
|
} |
|
|
|
const { slug } = context.params; |
|
const category = slug[0]; |
|
let post: Post | undefined; |
|
let structuredData: StructuredDataType | undefined; |
|
|
|
const getArticle = (curDos: DocumentTypes[]) => { |
|
post = curDos.find((p: Post) => p.slug === slug[1]); |
|
if (post) { |
|
const customMeta = makeMeta(post); |
|
structuredData = handleStructuredData(customMeta); |
|
return; |
|
} |
|
}; |
|
|
|
checkCategory(category, getArticle); |
|
|
|
return { |
|
props: { |
|
post: post ? post : null, |
|
structuredData: structuredData ? structuredData : null, |
|
}, |
|
}; |
|
}; |
URL 미리보기 구현 (OpenGraphPreview)
다른 분들의 블로그들을 읽던 도중에 공유된 URL이 멋지게 꾸며져 있는 부분을 발견했습니다.
저는 a 태그로 언더라인 하나가 그어져 있는 텍스트로 보여주었기 때문에 멋진 URL card를 만들었습니다.
|
const makeURLPreview = async (el: HTMLAnchorElement) => { |
|
try { |
|
const urlPath = el.href; |
|
const response = await axios.get( |
|
`/api/proxy?url=${encodeURIComponent(urlPath)}` |
|
); |
|
const html = response.data; |
|
const $ = cheerio.load(html); |
|
|
|
const jsonLD = $('script[type="application/json"]').text(); |
|
const structuredData = jsonLD |
|
? JSON.parse(jsonLD)?.props?.pageProps?.structuredData |
|
: {}; |
|
|
|
const meta = (property: string) => |
|
$(`meta[property="${property}"]`).attr("content"); |
|
const title = $("title").text(); |
|
|
|
const ogTitle = structuredData?.ogTitle || meta("og:title") || title; |
|
const ogDescription = |
|
structuredData?.ogDescription || meta("og:description"); |
|
const ogImage = structuredData?.ogImage || meta("og:image"); |
|
|
|
const aHtml = ReactDOMServer.renderToString( |
|
<OpenGraphPreview |
|
urlPath={urlPath} |
|
ogTitle={ogTitle} |
|
ogDescription={ogDescription} |
|
ogImage={ogImage} |
|
URL={urlPath} |
|
/> |
|
); |
|
el.outerHTML = aHtml; |
|
} catch (err) { |
|
console.error(err); |
|
} |
|
}; |
초기 html에 서버에서 동적으로 생성된 메타태그가 주입되지 않는 문제
카카오톡 같은 다른 플랫폼에서 제 블로그가 공유될 때 og태그를 가지고 카드를 만드는데 제 블로그 글마다 다른 카드를 형성하고 싶어서
getStacticProps를 사용해서 서버에서 메타정보를 가진 객체를 container 컴포넌트에 props를 내려준 뒤
메타 정보를 바탕으로 메타태그를 형성하는 방법을 사용했지만
초기 html에 동적인 메타태그가 주입되지 않는 문제가 있습니다.
- 서버에서 메타 객체를 생성후 props로 내려줍니다.
|
export const getStaticProps = async ( |
|
context: GetStaticPropsContext<PageParams> |
|
) => { |
|
if (!context.params) { |
|
return; |
|
} |
|
|
|
const { slug } = context.params; |
|
const category = slug[0]; |
|
let post: Post | undefined; |
|
let structuredData: StructuredDataType | undefined; |
|
|
|
const getArticle = (curDos: DocumentTypes[]) => { |
|
post = curDos.find((p: Post) => p.slug === slug[1]); |
|
if (post) { |
|
const customMeta = makeMeta(post); |
|
structuredData = handleStructuredData(customMeta); |
|
return; |
|
} |
|
}; |
|
|
|
checkCategory(category, getArticle); |
|
|
|
return { |
|
props: { |
|
post: post ? post : null, |
|
structuredData: structuredData ? structuredData : null, |
|
}, |
|
}; |
|
}; |
- structuredData 객체가 메타 정보를 가진 객체입니다.
|
interface ContainerProps { |
|
structuredData?: StructuredDataType; |
|
children?: React.ReactNode; |
|
className?: string | undefined | null | false | Record<string, boolean>; |
|
} |
|
|
|
export default function Container({ |
|
structuredData, |
|
children, |
|
className, |
|
}: ContainerProps) { |
|
const widthSize = useResizeWidth(); |
|
|
|
return ( |
|
<main className={`w-full flex flex-col items-center p-3 relative`}> |
|
<Head> |
|
{structuredData && ( |
|
<React.Fragment> |
|
<title>{structuredData.headline}</title> |
|
|
|
<meta name="description" content={structuredData.description} /> |
|
<meta property="og:title" content={structuredData.headline} /> |
|
<meta |
|
property="og:description" |
|
content={structuredData.description} |
|
/> |
|
<meta property="og:url" content={structuredData.url} /> |
|
<meta property="og:image" content={structuredData.image} /> |
|
|
|
<meta name="twitter:title" content={structuredData.headline} /> |
|
<meta |
|
name="twitter:description" |
|
content={structuredData.description} |
|
/> |
|
|
|
<meta |
|
property="article:published_time" |
|
content={structuredData.datePublished} |
|
></meta> |
|
<script |
|
type="application/ld+json" |
|
dangerouslySetInnerHTML={{ |
|
__html: JSON.stringify(structuredData), |
|
}} |
|
/> |
|
</React.Fragment> |
|
)} |
|
</Head> |
결국 문제를 고치지 못해서 _documents.tsx에 대표적인 메타 태그를 주입했었습니다.
그때 어떤 귀인분이 문제 해결의 실마리를 알려주셨습니다.
#27 fix: next/head가 적용되지 않는 이슈
|
const [showChild, setShowChild] = useState(false); |
|
useEffect(() => { |
|
setShowChild(true); |
|
}, []); |
|
|
|
if (!showChild) { |
|
return null; |
|
} |
|
|
|
if (typeof window === "undefined") { |
|
return <React.Fragment></React.Fragment>; |
이 코드는 _app.tsx에서 실행되기 때문에 모든 페이지와 컴포넌트에 영향을 줍니다.
showChild 상태가 false일 때 렌더링을 하지 않도록 설정하고 있습니다.
그러나 이렇게 설정하면 페이지 렌더링이 늦어지고 동적 메타 태그가 정상적으로 삽입되지 않을 수 있습니다.
따라서 해당부분을 삭제하고 window, document 객체를 사용하는 로직들을 브라우저 상태에서만 로직이 동작하도록 변경했습니다.
마운트시 True Boolean값을 전달하는 hook 👇
|
function useIsBrowser() { |
|
const [isBrowser, setIsBrowser] = useState(false); |
|
|
|
useEffect(() => { |
|
setIsBrowser(true); |
|
}, []); |
|
|
|
return isBrowser; |
|
} |
|
|
|
export default useIsBrowser; |
isBrowser값이 false일 경우 로직을 실행하지 않는 코드 👇
|
if (!isBrowser) { |
|
return null; |
|
} |
실제로 까똑 og card를 잘 불러옵니다.