diff --git a/package.json b/package.json
index 8bffbc8..33f6df7 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
},
"dependencies": {
"@actions/core": "^1.10.1",
+ "@giscus/react": "^2.3.0",
"@mdx-js/loader": "3.0.0",
"@mdx-js/react": "3.0.0",
"@next/mdx": "14.0.3",
diff --git a/src/app/AnalyticsScript.tsx b/src/app/AnalyticsScript.tsx
new file mode 100644
index 0000000..6a72944
--- /dev/null
+++ b/src/app/AnalyticsScript.tsx
@@ -0,0 +1,16 @@
+import Script from "next/script";
+
+export const AnalyticsScript = () => (
+ <>
+
+
+ >
+);
diff --git a/src/app/Providers.tsx b/src/app/Providers.tsx
new file mode 100644
index 0000000..f18a517
--- /dev/null
+++ b/src/app/Providers.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+import React, { ReactNode, useState } from "react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { MDXProvider } from "@mdx-js/react";
+import ThemeProvider from "@src/utils/context/ThemeProvider";
+import DarkModeProvider from "@src/utils/context/DarkModeContext";
+import components from "@src/components/mdx/MDXComponents";
+import StyledComponentsRegistry from "@src/registry/StyledComponentsRegistry";
+import GlobalStyle from "@src/styles/GlobalStyle";
+
+export const Providers = ({ children }: { children: ReactNode }) => {
+ const [queryClient] = useState(
+ () =>
+ new QueryClient({
+ defaultOptions: {
+ queries: {
+ refetchOnWindowFocus: false,
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ refetchInterval: false,
+ },
+ },
+ }),
+ );
+
+ return (
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+};
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
new file mode 100644
index 0000000..3bc5cf1
--- /dev/null
+++ b/src/app/layout.tsx
@@ -0,0 +1,36 @@
+import { PropsWithChildren } from "react";
+import { Metadata } from "next"; // import "data://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css";
+import Header from "@src/components/Header";
+import { Providers } from "@src/app/Providers";
+import { AnalyticsScript } from "@src/app/AnalyticsScript";
+
+export const metadata: Metadata = {
+ title: { template: "%s | oooooroblog", default: "oooooroblog" },
+ description: "웹 프론트엔드 개발 블로그",
+ icons: {
+ icon: "/favicon-32x32.png",
+ shortcut: "/favicon-32x32.png",
+ apple: "/apple-touch-icon.png",
+ },
+ manifest: "/site.webmanifest",
+ openGraph: {
+ type: "website",
+ url: "https://www.oooooroblog.com",
+ title: "oooooroblog",
+ description: "웹 프론트엔드 개발 블로그",
+ },
+};
+
+export default function RootLayout({ children }: PropsWithChildren) {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
new file mode 100644
index 0000000..675a0e9
--- /dev/null
+++ b/src/app/page.tsx
@@ -0,0 +1,40 @@
+"use client";
+import styled from "styled-components";
+import { useEffect } from "react";
+import { getAllPostMeta } from "@src/business/post";
+import PostList from "@src/components/main/PostList";
+import { StorageKey } from "@src/constants/constants";
+import Profile from "@src/components/main/Profile";
+
+export default function Page() {
+ const posts = getAllPostMeta();
+
+ useEffect(() => {
+ const scroll = parseInt(
+ sessionStorage.getItem(StorageKey.MAIN_SCROLL_Y) ?? "0",
+ );
+ window.scrollTo({ top: scroll, behavior: "auto" });
+ }, []);
+
+ const onClickPost = () => {
+ sessionStorage.setItem(StorageKey.MAIN_SCROLL_Y, window.scrollY.toString());
+ };
+
+ return (
+
+
+
+
+ );
+}
+
+const Wrapper = styled.div`
+ margin: 2.5rem auto;
+ max-width: 760px;
+ padding: 0 1rem;
+ display: flex;
+ flex-direction: column;
+ row-gap: 20px;
+`;
diff --git a/src/app/posts/[slug]/layout.tsx b/src/app/posts/[slug]/layout.tsx
new file mode 100644
index 0000000..7937e01
--- /dev/null
+++ b/src/app/posts/[slug]/layout.tsx
@@ -0,0 +1,18 @@
+import { PropsWithChildren } from "react";
+import { Metadata } from "next";
+import { getPostDetail } from "@src/business/post";
+import { PostPageProps } from "@src/app/posts/[slug]/page";
+
+export function generateMetadata({
+ params: { slug },
+}: PostPageProps): Metadata {
+ const { detail: post } = getPostDetail(slug);
+ return {
+ title: post.title,
+ description: post.description,
+ };
+}
+
+export default function PostLayout({ children }: PropsWithChildren) {
+ return <>{children}>;
+}
diff --git a/src/pages/posts/[slug].tsx b/src/app/posts/[slug]/page.tsx
similarity index 51%
rename from src/pages/posts/[slug].tsx
rename to src/app/posts/[slug]/page.tsx
index 11e397d..7705773 100644
--- a/src/pages/posts/[slug].tsx
+++ b/src/app/posts/[slug]/page.tsx
@@ -1,76 +1,32 @@
-import { GetStaticPropsContext } from "next";
-import { useRouter } from "next/router";
-import { NextSeo } from "next-seo";
-import styled from "styled-components";
+"use client";
import { useMDXComponent } from "next-contentlayer/hooks";
-import { allPosts, type Post } from "contentlayer/generated";
-
-import components from "@src/components/mdx/MDXComponents";
-import WavyLine from "@src/components/WavyLine";
+import styled from "styled-components";
import Meta from "@src/components/post/Meta";
import PostTitle from "@src/components/post/PostTitle";
+import WavyLine from "@src/components/WavyLine";
+import components from "@src/components/mdx/MDXComponents";
import Profile from "@src/components/main/Profile";
+import SidePost from "@src/components/post/SidePost";
import Comment from "@src/components/post/Comment";
+import { getPostDetail, getPostMeta } from "@src/business/post";
import { FadeIn } from "@src/styles/animation";
-import SidePost from "@src/components/post/SidePost";
-
-export async function getStaticPaths() {
- // Get a list of valid post paths.
- const paths = allPosts.map((post) => ({
- params: { slug: post._raw.flattenedPath },
- }));
-
- return { paths, fallback: false };
-}
-
-export async function getStaticProps(context: GetStaticPropsContext) {
- // Find the post for the current page.
- const postIdx = allPosts.findIndex(
- (post) => post._raw.flattenedPath === context.params?.slug
- );
- const post = allPosts[postIdx];
- const prevPost = allPosts[postIdx - 1] ?? null,
- nextPost = allPosts[postIdx + 1] ?? null;
-
- // Return notFound if the post does not exist.
- if (!post) return { notFound: true };
-
- // Return the post as page props.
- return { props: { post, prevPost, nextPost } };
-}
+export type PostPageProps = { params: { slug: string } };
const codePrefix = `
if (typeof process === 'undefined') {
globalThis.process = { env: {} }
}
`;
-export default function Page({
- post,
- prevPost,
- nextPost,
-}: {
- post: Post;
- prevPost: Post;
- nextPost: Post;
-}) {
- // Parse the MDX file via the useMDXComponent hook.
+export default function PostPage({ params: { slug } }: PostPageProps) {
+ const { postIdx, detail: post } = getPostDetail(slug);
+ const prevPost = getPostMeta(postIdx + 1),
+ nextPost = getPostMeta(postIdx - 1);
+
const MDXContent = useMDXComponent(codePrefix + post.body.code);
- const router = useRouter();
return (
<>
-
-
+
+
+
>
);
}
@@ -105,7 +63,7 @@ const PostHeader = styled.div`
margin-bottom: 3rem;
`;
-export const Article = styled.article`
+const Article = styled.article`
animation: ${FadeIn("0%")} 1.4s;
max-width: 760px;
diff --git a/src/business/post.ts b/src/business/post.ts
new file mode 100644
index 0000000..e8f306d
--- /dev/null
+++ b/src/business/post.ts
@@ -0,0 +1,43 @@
+import { compareDesc } from "date-fns";
+import { allPosts } from "contentlayer/generated";
+import { PostListElement } from "@src/types/post";
+
+const getSortedPosts = () => {
+ return allPosts.sort((a, b) => {
+ const compareByDate = compareDesc(new Date(a.date), new Date(b.date));
+ if (compareByDate !== 0) return compareByDate;
+ return b.id - a.id;
+ });
+};
+
+export const getAllPostMeta = (): PostListElement[] => {
+ return getSortedPosts().map(
+ (meta) =>
+ ({
+ slug: meta.url,
+ meta: {
+ index: meta.id,
+ title: meta.title,
+ description: meta.description || "",
+ postedAt: meta.date,
+ category: "",
+ series: "",
+ tags: [],
+ keywords: [],
+ },
+ }) satisfies PostListElement,
+ );
+};
+
+export const getPostDetail = (slug: string) => {
+ const sorted = getSortedPosts();
+ const postIdx = sorted.findIndex((post) => post._raw.flattenedPath === slug);
+ return {
+ postIdx,
+ detail: sorted[postIdx],
+ };
+};
+
+export const getPostMeta = (idx: number) => {
+ return getSortedPosts()[idx];
+};
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 0e13af4..0145ab1 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -1,34 +1,20 @@
+"use client";
import styled from "styled-components";
import Link from "next/link";
-import { useContext } from "react";
-import { DarkModeContext } from "@src/utils/context/DarkModeContext";
+import { ThemeToggleButton } from "@src/components/header/ThemeToggleButton";
export default function Header() {
- const { isDarkMode, toggleDarkMode } = useContext(DarkModeContext);
-
return (
-
-
- ooooorobo
-
-
-
-
-
+ ooooorobo
+
);
}
-const Background = styled.div`
+const Background = styled.nav`
background-color: ${(props) => props.theme.colors.bg.secondary};
`;
diff --git a/src/components/header/ThemeToggleButton.tsx b/src/components/header/ThemeToggleButton.tsx
new file mode 100644
index 0000000..0482336
--- /dev/null
+++ b/src/components/header/ThemeToggleButton.tsx
@@ -0,0 +1,17 @@
+"use client";
+import { useContext } from "react";
+import { DarkModeContext } from "@src/utils/context/DarkModeContext";
+
+export const ThemeToggleButton = () => {
+ const { isDarkMode, toggleDarkMode } = useContext(DarkModeContext);
+
+ return (
+
+ );
+};
diff --git a/src/components/mdx/CodeBlock.tsx b/src/components/mdx/CodeBlock.tsx
index a0ced7b..bc482f9 100644
--- a/src/components/mdx/CodeBlock.tsx
+++ b/src/components/mdx/CodeBlock.tsx
@@ -1,20 +1,19 @@
-import Highlight, { defaultProps } from "prism-react-renderer";
-import theme from "prism-react-renderer/themes/oceanicNext";
+import {Highlight, themes} from "prism-react-renderer";
import styled from "styled-components";
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function CodeBlock(props: any) {
- const className = props.children.props.className || "";
+ const className = props.children?.props.className || "";
const matches = className.match(/language-(?.*)/);
return (
{({ className, style, tokens, getLineProps, getTokenProps }) => (
@@ -22,15 +21,15 @@ export default function CodeBlock(props: any) {
(line, i) =>
// 마지막 줄은 무조건 \n만 있음
i < tokens.length - 1 && (
-
+
{i + 1}
{line.map((token, key) => (
-
+
))}
- )
+ ),
)}
)}
diff --git a/src/components/post/Comment.tsx b/src/components/post/Comment.tsx
index e7cb107..5ad4c01 100644
--- a/src/components/post/Comment.tsx
+++ b/src/components/post/Comment.tsx
@@ -1,26 +1,23 @@
-import Script from "next/script";
+import Giscus from "@giscus/react";
/**
* Giscus comment
*/
export default function Comment() {
return (
-
+
);
}
diff --git a/src/components/post/SidePost.tsx b/src/components/post/SidePost.tsx
index 21a6e37..f0d2098 100644
--- a/src/components/post/SidePost.tsx
+++ b/src/components/post/SidePost.tsx
@@ -5,11 +5,9 @@ import { type Post } from "contentlayer/generated";
const Post = ({ title, url }: { title: string; url: string }) => {
return (
- (
-
+
{title}
-
- )
+
);
};
export default function SidePost({
@@ -21,7 +19,7 @@ export default function SidePost({
}) {
return (
-
+
{prevPost && (
<>
이전 포스트
@@ -29,7 +27,7 @@ export default function SidePost({
>
)}
-
+
{nextPost && (
<>
다음 포스트
@@ -54,9 +52,9 @@ const Wrapper = styled.div`
`)}
`;
-const PostWrapper = styled.div<{ align: string }>`
+const PostWrapper = styled.div<{ $align: string }>`
width: 50%;
- text-align: ${({ align }) => align};
+ text-align: ${({ $align }) => $align};
${({ theme }) =>
theme.media.mobile(`
width: auto;
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
deleted file mode 100644
index 1a0abba..0000000
--- a/src/pages/_app.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import type { AppProps } from "next/app";
-import { useEffect, useState } from "react";
-import { MDXProvider } from "@mdx-js/react";
-import {
- Hydrate,
- QueryClient,
- QueryClientProvider,
-} from "@tanstack/react-query";
-import { Nanum_Gothic_Coding, Noto_Sans_KR } from "next/font/google";
-
-import ThemeProvider from "@src/utils/context/ThemeProvider";
-import DarkModeProvider from "@src/utils/context/DarkModeContext";
-import Header from "@src/components/Header";
-import components from "@src/components/mdx/MDXComponents";
-import GlobalStyle from "@src/styles/GlobalStyle";
-
-const defaultFont = Noto_Sans_KR({
- subsets: ["latin"],
- display: "swap",
- weight: ["300", "400", "700"],
- variable: "--font-default",
-});
-
-const codingFont = Nanum_Gothic_Coding({
- subsets: ["latin"],
- display: "swap",
- weight: ["400", "700"],
- variable: "--font-coding",
-});
-
-function MyApp({ Component, pageProps }: AppProps) {
- const [queryClient] = useState(
- () =>
- new QueryClient({
- defaultOptions: {
- queries: {
- refetchOnWindowFocus: false,
- refetchOnMount: false,
- refetchOnReconnect: false,
- refetchInterval: false,
- },
- },
- }),
- );
- useEffect(() => {
- console.log(
- `
-%c
- _ _
- | | | |
- ___ ___ ___ ___ ___ _ __ ___ | |__ | | ___ __ _
- / _ \\ / _ \\ / _ \\ / _ \\ / _ \\| '__/ _ \\| '_ \\| |/ _ \\ / _\` |
-| (_) | (_) | (_) | (_) | (_) | | | (_) | |_) | | (_) | (_| |
- \\___/ \\___/ \\___/ \\___/ \\___/|_| \\___/|_.__/|_|\\___/ \\__, |
- __/ |
- |___/
-` +
- `
- _ _
-| | | |
-| |__ _ _ ___ ___ ___ ___ ___ _ __ ___ | |__ ___
-| '_ \\| | | | / _ \\ / _ \\ / _ \\ / _ \\ / _ \\| '__/ _ \\| '_ \\ / _ \\
-| |_) | |_| | | (_) | (_) | (_) | (_) | (_) | | | (_) | |_) | (_) |
-|_.__/ \\__, | \\___/ \\___/ \\___/ \\___/ \\___/|_| \\___/|_.__/ \\___/
- __/ |
- |___/
-
-방문해 주셔서 감사합니다 :>`,
- "color: #fe5000",
- );
- }, []);
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default MyApp;
diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx
deleted file mode 100644
index 840fde5..0000000
--- a/src/pages/_document.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import Document, {
- DocumentContext,
- DocumentInitialProps,
- Head,
- Html,
- Main,
- NextScript,
-} from "next/document";
-import { ServerStyleSheet } from "styled-components";
-import Script from "next/script";
-
-export default class MyDocument extends Document {
- static async getInitialProps(
- ctx: DocumentContext
- ): Promise {
- const sheet = new ServerStyleSheet();
- const originalRenderPage = ctx.renderPage;
-
- try {
- ctx.renderPage = () =>
- originalRenderPage({
- enhanceApp: (App) => (props) =>
- sheet.collectStyles(),
- });
-
- const initialProps = await Document.getInitialProps(ctx);
- return {
- ...initialProps,
- styles: [
- <>
- {initialProps.styles}
- {sheet.getStyleElement()}
- >,
- ],
- };
- } finally {
- sheet.seal();
- }
- }
-
- render() {
- return (
-
-
-
-
-
-
-
- {process.env.NODE_ENV === "production" && (
- <>
-
-
- >
- )}
-
-
-
-
-
-
- );
- }
-}
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
deleted file mode 100644
index 105a242..0000000
--- a/src/pages/index.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import type { NextPage } from "next";
-import { useEffect } from "react";
-import { NextSeo } from "next-seo";
-import styled from "styled-components";
-import { compareDesc } from "date-fns";
-import { allPosts } from "contentlayer/generated";
-
-import { PostListElement } from "@src/types/post";
-import Profile from "@src/components/main/Profile";
-import PostList from "@src/components/main/PostList";
-import { StorageKey } from "@src/constants/constants";
-
-type HomeProps = { posts: PostListElement[] };
-
-const Home: NextPage = ({ posts }: HomeProps) => {
- useEffect(() => {
- const scroll = parseInt(
- sessionStorage.getItem(StorageKey.MAIN_SCROLL_Y) ?? "0"
- );
- window.scrollTo({ top: scroll, behavior: "auto" });
- }, []);
-
- const onClickPost = () => {
- sessionStorage.setItem(StorageKey.MAIN_SCROLL_Y, window.scrollY.toString());
- };
-
- return (
-
-
-
-
-
- );
-};
-
-export const getStaticProps = () => {
- const posts = allPosts
- .sort((a, b) => {
- const compareByDate = compareDesc(new Date(a.date), new Date(b.date));
- if (compareByDate !== 0) return compareByDate;
- return b.id - a.id;
- })
- .map(
- (meta) =>
- ({
- slug: meta.url,
- meta: {
- index: meta.id,
- title: meta.title,
- description: meta.description || "",
- postedAt: meta.date,
- category: "",
- series: "",
- tags: [] as string[],
- },
- } as PostListElement)
- );
- return { props: { posts } };
-};
-
-export default Home;
-
-const Wrapper = styled.div`
- margin: 2.5rem auto;
- max-width: 760px;
- padding: 0 1rem;
- display: flex;
- flex-direction: column;
- row-gap: 20px;
-`;
diff --git a/src/posts/60-starting-geultto-8.mdx b/src/posts/60-starting-geultto-8.mdx
index 42273d2..ba4b066 100644
--- a/src/posts/60-starting-geultto-8.mdx
+++ b/src/posts/60-starting-geultto-8.mdx
@@ -7,13 +7,17 @@ date: 2023-02-10
---
+
+
+
+
+
+
+
import WavyLine from "../components/WavyLine";
> 네트워크를 통해 내가 얻을 수 있는 것과 줄 수 있는 것이 무엇인지를 구분하는 것이 중요하다.
-우미영, \
- <나를 믿고 일한다는 것>
-
-
+ 우미영, \<나를 믿고 일한다는 것>
최근 다양한 커뮤니티에 참여해서 네트워킹을 하고 있는데, 내가 얻을 수 있는 것은 생각해 본 적이 있지만 줄 수 있는 것을 생각해 본 적은 없었던 것 같다.
diff --git a/src/posts/63-test-async-with-jest.mdx b/src/posts/63-test-async-with-jest.mdx
index 92984d9..fd47b15 100644
--- a/src/posts/63-test-async-with-jest.mdx
+++ b/src/posts/63-test-async-with-jest.mdx
@@ -298,10 +298,7 @@ test("비동기 작업이 완료되기 이전에는 onBeforeExecute는 실행되
1. execute를 실행하면, 자바스크립트 콜 스택에 execute 함수가 쌓이고, 주도권이 execute 함수로 넘어갑니다.
2. 이때 `await fetchList`를 만나면, WebAPI가 setTimeout의 처리를 맡게 되고 콜 스택에서 execute는 빠져나옵니다.
3. 주도권이 테스트로 넘어옵니다.
-4. (여기까지 동일) `jest.advanceTimersByTime`으로 3.1초가 흐르고, execute는 태스크 큐로 이동해 실행 대기 상태가 됩니다.
+4. (여기까지 동일) `jest.advanceTimersByTime`으로 3.1초가 흐르고, execute는 태스크 큐로 이동해 실행 대기 상태가 됩니다.
5. `await flushPromises()` 처리로 인해, 테스트도 콜 스택에서 빠져나와 태스크 큐로 이동합니다.
6. 콜 스택이 비었기 때문에, 태스크 큐에서 더 앞에 있던 `execute`가 콜 스택으로 들어가 마저 실행됩니다. 이 시점에 `onAfterExecute`가 실행됩니다.
7. execute 실행이 모두 끝나면, 다시 테스트를 실행합니다. 이때는 `expect(onAfterExecute).toHaveBeenCalledTimes(1);`를 통과할 수 있습니다.
diff --git a/src/registry/StyledComponentsRegistry.tsx b/src/registry/StyledComponentsRegistry.tsx
new file mode 100644
index 0000000..a4865af
--- /dev/null
+++ b/src/registry/StyledComponentsRegistry.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import { ReactNode, useState } from "react";
+import { useServerInsertedHTML } from "next/navigation";
+import { ServerStyleSheet, StyleSheetManager } from "styled-components";
+
+export default function StyledComponentsRegistry({
+ children,
+}: {
+ children: ReactNode;
+}) {
+ const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
+
+ useServerInsertedHTML(() => {
+ const styles = styledComponentsStyleSheet.getStyleElement();
+ styledComponentsStyleSheet.instance.clearTag();
+ return <>{styles}>;
+ });
+
+ if (typeof window !== "undefined") return <>{children}>;
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/styles/theme.ts b/src/styles/theme.ts
index b53aab6..0ca19cd 100644
--- a/src/styles/theme.ts
+++ b/src/styles/theme.ts
@@ -1,4 +1,4 @@
-import { DefaultTheme, css, CSSProp } from "styled-components";
+import { css, CSSProp, DefaultTheme, RuleSet } from "styled-components";
declare module "styled-components" {
export interface DefaultTheme {
@@ -66,7 +66,7 @@ const dark: ColorPreset = {
...primary,
};
-const defaultTheme: Omit = {
+const defaultTheme = {
fontSizes: {
tiny: "12px",
s: "14px",
@@ -90,18 +90,16 @@ const defaultTheme: Omit = {
code: 1.6,
},
media: {
- mobile: (...args) =>
- css`
- @media only screen and (max-width: 800px) {
- ${args}
- }
- `,
- desktop: (...args) =>
- css`
- @media only screen and (min-width: 800px) {
- ${args}
- }
- `,
+ mobile: (...args: RuleSet