├── .eslintrc.json
├── .github
├── ISSUE_TEMPLATE
│ └── 이슈-생성-템플릿.md
└── pull_request_template.md
├── .gitignore
├── .gitmessage.txt
├── .husky
├── pre-commit
└── pre-push
├── .prettierrc
├── README.md
├── contentlayer.config.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── posts
├── blog
│ ├── frontend
│ │ ├── 1-dom.mdx
│ │ ├── 10-cors.mdx
│ │ ├── 11-graphql.mdx
│ │ ├── 12-suspense-errorboundary.mdx
│ │ ├── 2-browser-rendering.mdx
│ │ ├── 3-reflow-repaint.mdx
│ │ ├── 4-module-bundler.mdx
│ │ ├── 5-csr-ssr.mdx
│ │ ├── 7-yarn-npm.mdx
│ │ ├── 8-monorepo.mdx
│ │ ├── 9-optimizing-loading-speed.mdx
│ │ └── index.mdx
│ ├── javascript
│ │ ├── 1-var.mdx
│ │ ├── 2-var-let-const.mdx
│ │ ├── 3-javascript-single-thread.mdx
│ │ ├── 4-event-loop.mdx
│ │ ├── 5-this.mdx
│ │ ├── 6-closure.mdx
│ │ └── index.mdx
│ ├── nextjs
│ │ ├── 1-ssr-ssg-isr.mdx
│ │ ├── 2-lighthouse.mdx
│ │ ├── 3-performance.mdx
│ │ ├── 4-nextjs-13ver.mdx
│ │ └── index.mdx
│ └── retrospect
│ │ ├── 1-devcourse-MIL-1.mdx
│ │ ├── 2-abstract-architecture.mdx
│ │ ├── 3-devcourse-MIL-2.mdx
│ │ ├── 4-devcourse-MIL-3.mdx
│ │ ├── 5-daangn-interview.mdx
│ │ ├── 6-devcourse-MIL-4.mdx
│ │ ├── 7-toss-interview.mdx
│ │ └── index.mdx
└── snippets
│ ├── git
│ ├── alias.mdx
│ ├── hist-commit.mdx
│ └── issue-template.mdx
│ └── settings
│ ├── eslint.mdx
│ ├── husky.mdx
│ └── vscode-code.mdx
├── public
├── favicon.ico
├── gif
│ ├── sunflowers.webp
│ └── truck.webp
├── images
│ ├── base.jpeg
│ ├── blog
│ │ └── 2023
│ │ │ └── 06
│ │ │ └── darkmode
│ │ │ └── cover.png
│ ├── favicon
│ │ ├── apple-icon-180x180.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── manifest.json
│ │ └── ms-icon-144x144.png
│ └── samples
│ │ ├── draft-cover.png
│ │ ├── unsplash-1.jpg
│ │ ├── unsplash-10.jpg
│ │ ├── unsplash-11.jpg
│ │ ├── unsplash-12.jpg
│ │ ├── unsplash-13.jpg
│ │ ├── unsplash-14.jpg
│ │ ├── unsplash-15.jpg
│ │ ├── unsplash-16.jpg
│ │ ├── unsplash-17.jpg
│ │ ├── unsplash-18.jpg
│ │ ├── unsplash-19.jpg
│ │ ├── unsplash-2.jpg
│ │ ├── unsplash-20.jpg
│ │ ├── unsplash-21.jpg
│ │ ├── unsplash-3.jpg
│ │ ├── unsplash-4.jpg
│ │ ├── unsplash-5.jpg
│ │ ├── unsplash-6.jpg
│ │ ├── unsplash-7.jpg
│ │ ├── unsplash-8.jpg
│ │ └── unsplash-9.jpg
└── posts
│ ├── 1-devcourse-MIL-1
│ ├── 231116-184408.png
│ └── featured.png
│ ├── 1-dom
│ ├── 230706-200314.png
│ ├── 230706-201501.png
│ ├── 230706-201703.png
│ ├── cover.png
│ └── new_cover.png
│ ├── 1-ssr-ssg-isr
│ ├── 230821-131324.png
│ ├── 230821-132345.png
│ ├── cover.png
│ └── featured.png
│ ├── 1-var
│ ├── 230706-214734.png
│ ├── 230706-215336.png
│ ├── 230707-040145.png
│ ├── 230707-040809.png
│ └── cover.png
│ ├── 10-cors
│ ├── 230929-232531.png
│ ├── 231006-041732.png
│ ├── 231006-041812.png
│ ├── 231006-041831.png
│ ├── 231006-041846.png
│ ├── 231006-042607.png
│ ├── 231006-042636.png
│ ├── 231006-042908.png
│ ├── cover.png
│ └── featured.png
│ ├── 11-graphql
│ ├── 231020-182629.png
│ ├── 231020-193016.png
│ ├── cover.png
│ └── featured.png
│ ├── 12-suspense-errorboundary
│ ├── 240304-045441.png
│ └── 240304-055427.png
│ ├── 2-abstract-architecture
│ ├── 231119-171803.png
│ ├── 231119-190234.png
│ ├── 231119-190440.png
│ ├── 231119-191922.png
│ ├── 231119-205440.png
│ └── cover.png
│ ├── 2-browser-rendering
│ ├── 230724-143333.png
│ ├── 230724-143341.png
│ ├── 230724-143409.png
│ ├── 230724-143539.png
│ ├── 230724-145527.png
│ ├── 230724-145656.png
│ ├── 230724-145700.png
│ ├── 230724-145856.png
│ ├── 230724-151608.png
│ └── cover.png
│ ├── 2-lighthouse
│ ├── 230907-212702.png
│ ├── 230907-212718.png
│ ├── 230907-212811.png
│ ├── 230907-214124.png
│ ├── 230907-224248.png
│ ├── 230907-224425.png
│ ├── 230907-224948.png
│ ├── 230907-231118.png
│ ├── 230907-231135.png
│ ├── 230907-231302.png
│ ├── 230907-231348.png
│ └── cover.png
│ ├── 2-var-let-const
│ ├── 230713-211114.png
│ ├── 230713-220042.png
│ ├── 230713-225458.png
│ └── cover.png
│ ├── 3-devcourse-mil-2
│ ├── 231123-001702.png
│ ├── 231123-011602.png
│ ├── 231123-013002.png
│ ├── 231123-013131.png
│ ├── 231123-022919.png
│ ├── 231123-030343.png
│ └── cover.png
│ ├── 3-javascript-single-thread
│ ├── 230810-211104.png
│ ├── 230810-211629.png
│ ├── 230810-211811.png
│ └── cover.png
│ ├── 3-performance
│ ├── 230911-093453.png
│ ├── 230911-093523.png
│ ├── 230911-093645.png
│ ├── 230911-093849.png
│ ├── 230911-094750.png
│ ├── 230911-095031.png
│ ├── 230911-095116.png
│ ├── 230911-100033.png
│ ├── 230911-102823.png
│ ├── 230911-103155.png
│ ├── 230911-103739.png
│ ├── 230911-104422.png
│ ├── 230911-104707.png
│ └── cover.png
│ ├── 3-reflow-repaint
│ ├── 230726-000206.png
│ ├── 230726-001638.png
│ ├── 230726-145258.png
│ └── cover.png
│ ├── 4-devcourse-MIL-3
│ ├── 231229-131323.png
│ ├── 231229-133437.png
│ └── 231229-140518.png
│ ├── 4-event-loop
│ ├── 230811-213055.png
│ ├── 230811-213225.png
│ ├── 230811-213538.png
│ ├── 230811-215226.png
│ ├── 230811-215357.png
│ └── cover.png
│ ├── 4-module-bundler
│ ├── 230818-152415.png
│ ├── 230818-153420.png
│ ├── 230818-163527.png
│ ├── 230818-172206.png
│ ├── 230818-172731.png
│ ├── 230818-173707.png
│ ├── 230818-173834.png
│ ├── 230818-174028.png
│ ├── 230824-202514.png
│ ├── 230824-203200.png
│ └── featured.png
│ ├── 4-nextjs-13ver
│ ├── 230919-235424.png
│ ├── 230921-235500.png
│ ├── 230921-235647.png
│ ├── cover.png
│ └── featured.png
│ ├── 5-csr-ssr
│ ├── 230820-234931.png
│ ├── 230820-235218.png
│ ├── 230820-235249.png
│ ├── 230824-202351.png
│ └── cover.png
│ ├── 5-daangn-interview
│ ├── 240114-205712.png
│ ├── 240114-205938.png
│ ├── 240114-234758.png
│ ├── 240115-010259.png
│ ├── 240115-014008.png
│ ├── 240115-022047.png
│ └── 240115-034801.png
│ ├── 5-this
│ └── cover.png
│ ├── 6-closure
│ └── cover.png
│ ├── 6-devcourse-MIL-4
│ ├── 240123-220544.png
│ ├── 240123-220636.png
│ ├── 240123-220732.png
│ ├── 240123-220751.png
│ ├── 240123-221914.png
│ ├── 240123-222026.png
│ ├── 240124-135607.png
│ ├── 240124-135656.png
│ ├── 240124-140313.png
│ ├── 240124-140437.png
│ ├── 240124-153555.png
│ ├── 240124-154318.png
│ ├── 240124-154437.png
│ └── 240124-154803.png
│ ├── 7-javascript-performance
│ ├── 230915-203115.png
│ ├── 230915-203119.png
│ └── 230915-203619.png
│ ├── 7-toss-interview
│ ├── 240601-214439.png
│ ├── 240601-214833.png
│ ├── 240601-230525.png
│ ├── 240601-233844.png
│ ├── 240602-001419.png
│ ├── 240602-232049.png
│ ├── 240603-010120.png
│ ├── 240603-010322.png
│ ├── 240603-022016.png
│ ├── 240603-032933.png
│ ├── 240603-034045.png
│ └── 240603-034219.png
│ ├── 7-yarn-npm
│ ├── 230904-010059.png
│ ├── 230904-115613.png
│ ├── 230904-122752.png
│ ├── 230904-122931.png
│ ├── 230904-123103.png
│ └── cover.png
│ ├── 8-monorepo
│ ├── 230904-231926.png
│ ├── 230904-234635.png
│ ├── 230905-150550.png
│ ├── 230905-153434.png
│ ├── 230905-165659.png
│ ├── 230905-165945.png
│ ├── 230905-170049.png
│ ├── 230905-170430.png
│ └── cover.png
│ ├── 9-optimizing-loading-speed
│ ├── 230915-213601.png
│ ├── 230915-223120.png
│ ├── 230915-223127.png
│ ├── cover.png
│ └── featured.png
│ ├── alias
│ ├── 230706-180256.png
│ └── 230706-180514.png
│ ├── commit
│ ├── 230706-182223.png
│ ├── 230706-182303.png
│ ├── 230706-182532.png
│ └── 230706-182842.png
│ ├── husky
│ ├── 230706-185053.png
│ ├── 230706-185355.png
│ └── 230706-185406.png
│ └── issue-template
│ ├── 230706-183626.png
│ ├── 230706-183643.png
│ ├── 230706-183658.png
│ ├── 230706-183812.png
│ ├── 230706-183823.png
│ ├── 230706-183836.png
│ └── 230706-184024.png
├── scripts
└── generateSitemap.ts
├── src
├── components
│ ├── AuthorContacts.tsx
│ ├── SEO.tsx
│ ├── ThemeSwitch.tsx
│ ├── common
│ │ ├── AnimatedContainer.tsx
│ │ ├── CopyLinkButton.tsx
│ │ ├── Fonts.tsx
│ │ ├── HeaderNavigation.tsx
│ │ ├── HoverCard.tsx
│ │ ├── IconButton.tsx
│ │ ├── IconText.tsx
│ │ ├── LinkArrow.tsx
│ │ ├── LinkExternal.tsx
│ │ ├── LinkHover.tsx
│ │ ├── LogoIcon.tsx
│ │ ├── NavItem.tsx
│ │ ├── Pill.tsx
│ │ ├── PlainText.tsx
│ │ ├── PressedEffect.tsx
│ │ ├── SearchInput.tsx
│ │ ├── SectionBorder.tsx
│ │ ├── SiteFooter.tsx
│ │ ├── SubTitle.tsx
│ │ ├── Tag.tsx
│ │ ├── Title.tsx
│ │ └── index.ts
│ ├── contents
│ │ ├── ContentsBanner.tsx
│ │ ├── ContentsTable.tsx
│ │ ├── Giscus.tsx
│ │ ├── ReadingProgressBar.tsx
│ │ └── index.ts
│ ├── home
│ │ ├── FeaturedPosts.tsx
│ │ ├── IntroduceDescription.tsx
│ │ └── index.ts
│ ├── icons
│ │ ├── CalendarIcon.tsx
│ │ ├── ChatIcon.tsx
│ │ ├── CheckIcon.tsx
│ │ ├── ClockIcon.tsx
│ │ ├── ContactsIcon.tsx
│ │ ├── GithubIcon.tsx
│ │ ├── LinkIcon.tsx
│ │ ├── ListIcon.tsx
│ │ ├── MailIcon.tsx
│ │ ├── TagIcon.tsx
│ │ ├── UpIcon.tsx
│ │ ├── VelogIcon.tsx
│ │ └── index.ts
│ ├── index.ts
│ ├── mdx
│ │ ├── CodeBlock.tsx
│ │ ├── ZoomImage.tsx
│ │ └── index.ts
│ ├── post
│ │ ├── PostContent.tsx
│ │ ├── PostFooter.tsx
│ │ ├── PostHeader.tsx
│ │ ├── PostItem.tsx
│ │ ├── PostList.tsx
│ │ ├── PostNavigation.tsx
│ │ ├── PostPressedCard.tsx
│ │ └── index.ts
│ ├── series
│ │ └── SeriesCard.tsx
│ └── snippet
│ │ ├── SnippetItem.tsx
│ │ ├── SnippetList.tsx
│ │ └── index.ts
├── constants
│ ├── animations.ts
│ ├── config.ts
│ ├── contents.ts
│ ├── fonts.ts
│ └── image.ts
├── layouts
│ ├── Layout.tsx
│ ├── PostLayout.tsx
│ └── SearchLayout.tsx
├── libs
│ ├── core.ts
│ ├── dataset.ts
│ ├── gtag.ts
│ ├── mdx.ts
│ ├── post.ts
│ ├── rehypeCodeWrap.js
│ ├── useDarkMode.ts
│ ├── useDebounce.ts
│ ├── useRouteChange.ts
│ └── useWatchTimeout.ts
├── pages
│ ├── 404.tsx
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── blog
│ │ ├── [...slugs].tsx
│ │ ├── [slug].tsx
│ │ └── index.tsx
│ ├── index.tsx
│ ├── search
│ │ └── index.tsx
│ └── snippets
│ │ ├── [...slugs].tsx
│ │ └── index.tsx
├── styles
│ ├── globals.css
│ ├── intellij-prism.css
│ └── prose.css
└── types
│ └── post.ts
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.node.json
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true
6 | },
7 | "parser": "@typescript-eslint/parser",
8 | "parserOptions": {
9 | "ecmaVersion": "latest",
10 | "sourceType": "module",
11 | "project": ["tsconfig.json"],
12 | "createDefaultProgram": true
13 | },
14 | "plugins": ["unused-imports", "simple-import-sort"],
15 | "extends": ["plugin:prettier/recommended", "next/core-web-vitals"],
16 | "settings": {
17 | "react": {
18 | "version": "detect"
19 | }
20 | },
21 | "rules": {
22 | "no-unused-vars": "off",
23 | "unused-imports/no-unused-imports": "error",
24 | "unused-imports/no-unused-vars": [
25 | "warn",
26 | {
27 | "vars": "all",
28 | "varsIgnorePattern": "^_",
29 | "args": "after-used",
30 | "argsIgnorePattern": "^_"
31 | }
32 | ],
33 | "react-hooks/exhaustive-deps": 0,
34 | "react/display-name": 0,
35 | "react/no-unknown-property": "off",
36 | "@next/next/no-img-element": 0,
37 | "simple-import-sort/imports": "error",
38 | "simple-import-sort/exports": "error"
39 | },
40 | "globals": {
41 | "React": "writable"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/이슈-생성-템플릿.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 이슈 생성 템플릿
3 | about: 해당 템플릿을 이슈 생성시 사용해주세요 :)
4 | title: "[TAG] 이슈 이름"
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## 🌱 만들고자 하는 기능
11 | 기능 이름
12 |
13 | ## 🌱 구현 내용
14 | - [ ] todo1
15 | - [ ] todo2
16 |
17 | ## 🌱 예상기간
18 | 0월00일 ~ 0월00일
19 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## 🌱 개요
2 |
3 | ## 🌱 작업사항
4 | *
5 | *
6 |
7 | ## ✅ check list
8 | - [ ] 이슈 내용을 전부 적용했나요?
9 | - [ ] 산정한 작업 기간 내에 개발했나요?
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 | .vscode
37 | .eslintcache
38 | .env
39 |
40 | # production
41 | /build
42 | /public/robots.txt
43 | /public/sitemap*.xml
44 |
45 | .contentlayer
--------------------------------------------------------------------------------
/.gitmessage.txt:
--------------------------------------------------------------------------------
1 | # <타입> : <제목> 형식으로 작성하며 제목은 최대 50글자 정도로만 입력
2 | # 제목을 아랫줄에 작성, 제목 끝에 마침표 금지, 무엇을 했는지 명확하게 작성
3 |
4 | ################
5 | # [feat] : 새로운 기능 추가
6 | # [fix] : 버그 수정
7 | # [docs] : 문서 수정
8 | # [test] : 테스트 코드 추가
9 | # [refactor] : 코드 리팩토링
10 | # [style] : 코드 의미에 영향을 주지 않는 변경사항
11 | # [chore] : 빌드 부분 혹은 패키지 매니저 수정사항
12 | ################
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | yarn lint-staged
5 |
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | yarn lint
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": true,
4 | "useTabs": false,
5 | "tabWidth": 2,
6 | "trailingComma": "all",
7 | "printWidth": 80
8 | }
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Next.js로 제작한 기술 블로그
2 |
3 | 글쓰기와 공유하는 활동을 좋아하는 성향을 바탕으로 개발 생태계에 기여하고자 기술 블로그를 시작하게 되었습니다 🌻
4 |
5 | ### ✔️ Next.js 선택 이유
6 |
7 | - **`SEO 최적화`** 를 통해 더 많은 사용자들에게 블로그를 노출하여 정보 전달
8 | - 정적 컨텐츠를 SSG를 통해 렌더링하여 **`초기 로딩 속도`** 를 개선
9 | - Next.js의 Image 컴포넌트, webp 변환, priority 등의 기능을 활용하여 **`이미지 최적화`** 및 **`LCP 5초 개선`**
10 | - 성능 개선 과정을 블로그에 기록 [Lighthouse로 Next.js 성능 44% 개선하기](https://enjoydev.life/blog/nextjs/3-performance)
11 |
12 |
13 |
14 | ## 🌐 URL
15 |
16 | https://enjoydev.life
17 |
18 |
19 |
20 | ## 🛠️ Tech Stack
21 |
22 |
29 |
30 |
31 |
32 | ## ✨ 주요 기능
33 |
34 | - **`contentlayer`** 를 이용한 mdx 파일 관리 및 포스팅
35 | - **`TOC(Table of Contents)`** 구현
36 | - Giscus를 이용한 **`댓글`** 기능
37 | - **`포스트 검색`** 기능
38 | - **`다크 모드`** 지원
39 | - **`반응형 디자인`**
40 | - 사이트맵 생성 및 구글 검색 엔진 등록
41 | - 구글 Analytics 연동
42 |
43 |
44 |
45 | ## 📦 Project Structure
46 |
47 | ```
48 | 📦 src
49 | ├── 📂 components
50 | │ ├── 📂 common
51 | │ ├── 📂 contents
52 | | ├── 📂 home
53 | │ ├── 📂 icons
54 | │ ├── 📂 mdx
55 | │ ├── 📂 post
56 | │ ├── 📂 series
57 | │ └── 📂 snippet
58 | ├── 📂 constants
59 | ├── 📂 layouts
60 | ├── 📂 libs
61 | ├── 📂 pages
62 | │ ├── 📂 blog
63 | │ ├── 📂 search
64 | │ └── 📂 snippets
65 | ├── 📂 styles
66 | └── 📂 types
67 | ```
68 |
69 | ## 🚀 Getting Started
70 |
71 | ```bash
72 | yarn install
73 | ```
74 |
75 | ```bash
76 | yarn dev
77 | ```
78 |
--------------------------------------------------------------------------------
/contentlayer.config.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defineDocumentType,
3 | FieldDefs,
4 | makeSource,
5 | } from 'contentlayer/source-files';
6 | import readingTime from 'reading-time';
7 | import rehypeAutolinkHeadings from 'rehype-autolink-headings';
8 | import rehypeExternalLinks from 'rehype-external-links';
9 | import rehypePrism from 'rehype-prism-plus';
10 | import rehypeSlug from 'rehype-slug';
11 | import remarkBreaks from 'remark-breaks';
12 | import remarkGfm from 'remark-gfm';
13 |
14 | import rehypeCodeWrap from './src/libs/rehypeCodeWrap';
15 |
16 | const fields: FieldDefs = {
17 | title: { type: 'string', required: true },
18 | description: { type: 'string', required: true },
19 | date: { type: 'date', required: true },
20 | tags: { type: 'list', required: true, of: { type: 'string' } },
21 | draft: { type: 'boolean' },
22 | image: { type: 'string' },
23 | icon: { type: 'string' },
24 | };
25 |
26 | export const Post = defineDocumentType(() => ({
27 | name: 'Post',
28 | contentType: 'mdx',
29 | filePathPattern: `**/*.mdx`,
30 | fields,
31 | computedFields: {
32 | slug: {
33 | type: 'string',
34 | resolve: (post) => `/${post._raw.flattenedPath}`,
35 | },
36 | readingMinutes: {
37 | type: 'string',
38 | resolve: (post) => Math.ceil(readingTime(post.body.raw).minutes),
39 | },
40 | wordCount: {
41 | type: 'number',
42 | resolve: (post) => post.body.raw.split(/\s+/gu).length,
43 | },
44 | },
45 | }));
46 |
47 | const contentSource = makeSource({
48 | contentDirPath: 'posts',
49 | documentTypes: [Post],
50 | mdx: {
51 | remarkPlugins: [remarkGfm, remarkBreaks],
52 | rehypePlugins: [
53 | rehypeSlug,
54 | rehypeCodeWrap,
55 | rehypePrism,
56 | [
57 | rehypeAutolinkHeadings,
58 | {
59 | properties: {
60 | className: ['anchor'],
61 | ariaLabel: 'anchor',
62 | },
63 | },
64 | ],
65 | [
66 | rehypeExternalLinks,
67 | {
68 | target: '_blank',
69 | rel: ['noopener noreferrer'],
70 | },
71 | ],
72 | ],
73 | },
74 | });
75 |
76 | export default contentSource;
77 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const { withContentlayer } = require('next-contentlayer');
2 |
3 | /** @type {import('next').NextConfig} */
4 | module.exports = withContentlayer({
5 | reactStrictMode: true,
6 | swcMinify: false,
7 | experimental: {
8 | fontLoaders: [
9 | { loader: '@next/font/google', options: { subsets: ['latin'] } },
10 | ],
11 | },
12 | images: {
13 | formats: ['image/avif', 'image/webp'],
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-blog",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build && next export",
8 | "postbuild": "yarn sitemap",
9 | "sitemap": "ts-node --project tsconfig.node.json ./scripts/generateSitemap.ts",
10 | "start": "next start",
11 | "postinstall": "husky install && contentlayer build",
12 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --cache",
13 | "lint-staged": "lint-staged"
14 | },
15 | "lint-staged": {
16 | "**/*.{tsx,ts,jsx,js}": [
17 | "prettier --write"
18 | ]
19 | },
20 | "dependencies": {
21 | "@next/font": "^13.4.7",
22 | "autoprefixer": "10.4.14",
23 | "dayjs": "^1.11.8",
24 | "detect-touch": "^1.1.2",
25 | "framer-motion": "^8.4.2",
26 | "gray-matter": "^4.0.3",
27 | "gsap": "^3.12.2",
28 | "medium-zoom": "^1.0.8",
29 | "next": "^13.1.6",
30 | "next-mdx-remote": "^4.4.1",
31 | "next-seo": "^6.1.0",
32 | "next-themes": "^0.2.1",
33 | "react": "18.2.0",
34 | "react-dom": "18.2.0",
35 | "react-hot-toast": "^2.4.1",
36 | "title": "^3.5.3"
37 | },
38 | "devDependencies": {
39 | "@tailwindcss/typography": "^0.5.9",
40 | "@types/detect-touch": "^1.1.0",
41 | "@types/glob": "^8.1.0",
42 | "@types/gtag.js": "^0.0.12",
43 | "@types/node": "18.11.9",
44 | "@types/react": "18.0.24",
45 | "@types/react-dom": "18.0.8",
46 | "@types/title": "^3.4.1",
47 | "@typescript-eslint/eslint-plugin": "^5.60.1",
48 | "@typescript-eslint/parser": "^5.60.1",
49 | "clsx": "^1.2.1",
50 | "contentlayer": "^0.3.4",
51 | "eslint": "^8.43.0",
52 | "eslint-config-next": "12.1.0",
53 | "eslint-config-prettier": "^8.8.0",
54 | "eslint-plugin-prettier": "^4.2.1",
55 | "eslint-plugin-simple-import-sort": "^10.0.0",
56 | "eslint-plugin-unused-imports": "^2.0.0",
57 | "glob": "^10.3.1",
58 | "husky": "^8.0.3",
59 | "lint-staged": "^13.2.3",
60 | "next-contentlayer": "^0.3.4",
61 | "postcss": "8.4.24",
62 | "prettier": "^2.8.8",
63 | "reading-time": "^1.5.0",
64 | "rehype-autolink-headings": "^6.1.1",
65 | "rehype-code-titles": "^1.2.0",
66 | "rehype-external-links": "^2.1.0",
67 | "rehype-prism-plus": "^1.6.1",
68 | "rehype-slug": "^5.1.0",
69 | "remark-breaks": "^3.0.3",
70 | "remark-gfm": "^3.0.1",
71 | "shiki": "^0.14.3",
72 | "tailwind-merge": "^1.13.2",
73 | "tailwindcss": "3.3.2",
74 | "ts-node": "^10.9.1",
75 | "typescript": "4.8.4"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/posts/blog/frontend/2-browser-rendering.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 브라우저 렌더링 과정 쉽게 이해하기
3 | description: 우리가 지금 보고 있는 웹사이트 화면은 어떻게 만들어진 걸까요? 브라우저 렌더링 과정과 그 원리에 대해 알아보겠습니다.
4 | icon: ''
5 | image: '/posts/2-browser-rendering/cover.png'
6 | tags:
7 | - 브라우저 렌더링
8 | date: 2023-07-24 13:30:11
9 | ---
10 |
11 | # 브라우저 렌더링
12 |
13 |
14 |
15 | 브라우저 렌더링 과정은 프론트엔드 기술 면접에서 자주 나오는 중요한 주제입니다!
16 | 그렇다면 왜 브라우저 렌더링 과정을 중요하게 여기고, 우리가 그에 대해서 알아야 할까요?
17 |
18 | ## 브라우저 렌더링 원리를 알아야 하는 이유
19 |
20 | 웹 브라우저는 뼈대를 이루는 HTML, 살을 붙여 꾸며주는 CSS, 그리고 그것의 동작을 수행하는 자바스크립트가 함께 실행됩니다.
21 |
22 | 따라서 의도대로 코드를 실행시키려면 브라우저가 우리가 작성한 코드를 어떤 순서대로 읽어주는지
23 | 즉, 브라우저가 어떠한 순서로 렌더링 되는지를 파악해야 더 효율적인 코드를 작성할 수 있습니다.
24 |
25 | > ## Okay!
26 |
27 | 이제부터 우리가 지난 시간에 배웠던 DOM이라는 개념을 활용해서
28 | 브라우저가 어떻게 HTML, CSS, JS 코드들을 파싱하고 브라우저에 렌더링을 하는지 알아보겠습니다.
29 |
30 | ---
31 |
32 | ## Parsing
33 |
34 |
35 |
36 | 브라우저가 페이지를 렌더링하려면 가장 먼저 받아온 HTML 파일을 해석해야 합니다.
37 | 그러나 HTML 파일은 그저 문자열로 이루어진 순수한 텍스트이기 때문에 브라우저가 이해할 수 없습니다 😫
38 |
39 | 따라서 `번역`이 필요합니다.
40 | **브라우저는 파싱을 통해 HTML 파일을 해석**하여 DOM Tree를 구성합니다.
41 |
42 |
43 |
44 | 지난 시간에 배운 내용을 활용할 때가 되었군요!
45 | `DOM Tree`가 무엇이었죠?
46 |
47 | > DOM Tree는 브라우저가 이해할 수 있는 노드들로 구성된 트리 자료구조를 뜻합니다.
48 |
49 | 
50 |
51 | 파싱 중 HTML에 CSS가 포함되어 있다면 파싱을 통해 CSSOM(CSS Object Model) Tree 구성 작업도 함께 진행합니다.
52 | 
53 |
54 | > ## Okay!
55 |
56 | 파싱을 통해 HTML과 CSS 파일을 브라우저가 이해할 수 있는 트리 구조로 변환하는군요!
57 |
58 | ---
59 |
60 | ## Style
61 |
62 | Style 단계에서는 DOM Tree와 CSSOM Tree를 결합시켜서 렌더링을 위해 렌더 트리(Render Tree)를 구성합니다.
63 |
64 | 여기서 흥미로운 사실은 렌더 트리는 **브라우저 화면에 보여지지 않는 것들은 포함하지 않습니다.**
65 |
66 | 
67 | 예를 들어서, `visibility: hidden` 속성은 요소가 공간을 차지하고 보이지만 않기 때문에 렌더 트리에 포함이 됩니다.
68 |
69 | 하지만 `display: none` 의 경우는 **공간을 차지하지 않기 때문에 렌더 트리에서 제외**됩니다.
70 |
71 | ## Layout
72 |
73 | 
74 |
75 | Layout 단계에서는 렌더 트리를 화면에 배치하기 위해 정확한 위치와 크기를 계산합니다.
76 | 루트부터 노드를 순회하면서 `노드의 위치와 크기를 계산`하고 렌더 트리에 반영합니다.
77 |
78 | ## Paint
79 |
80 | 
81 |
82 | Paint 단계에서는 Layout 단계에서 계산된 값을 이용하여 각 노드들을 화면 상의 실제 픽셀로 변환합니다.
83 | 이때 픽셀로 변환된 결과는 하나의 레이어가 아니라 여러 개의 레이어로 관리됩니다.
84 |
85 | 브라우저 화면에 `픽셀을 렌더링` 하는 과정이라고 이해하면 좋습니다.
86 |
87 | ## Composite
88 |
89 | Composite 단계에서는 Paint 단계에서 생성된 레이어를 합성하여 `실제 화면에 나타냅니다.`
90 | 이제 우리는 화면에서 웹 페이지를 볼 수 있습니다 ✨
91 |
92 | ---
93 |
94 | > ## Okay!
95 |
96 | 그래프를 보면서 과정을 정리해봅시다.
97 |
98 | 
99 |
100 | ---
101 |
102 | > ## 과정을 한 번에 확인해 봅시다
103 |
104 |
105 |
106 | 페이지가 로드 되었을 때의 네트워크 동작 순서입니다.
107 | 숫자를 천천히 따라가면서 앞의 내용을 떠올려보도록 하겠습니다.
108 |
109 | 1. `Send Request` : 서버에게 HTML, CSS, JS 파일을 요청합니다.
110 | 2. `Parse HTML` : 서버로부터 받아온 HTML 파일을 파싱합니다. DOM Tree를 구축합니다.
111 | 3. `Parse Stylesheet` : CSS 파일을 파싱하여 CSSOM Tree를 구축합니다.
112 | 4. `Evaluate Script` : JS 파일을 실행합니다.
113 | 5. `Layout` : 렌더링 트리의 노드들의 위치와 크기를 계산합니다.
114 | 6. `Paint` : 렌더링 트리의 노드들의 픽셀을 렌더링 합니다.
115 | 7. `Composite Layers` : Paint 단계의 레이어를 합성하여 화면에 나타냅니다. 우리는 실제 화면을 볼 수 있습니다 ✨
116 |
117 | ---
118 |
119 | ## 글을 마치며
120 |
121 | 지금까지 브라우저 렌더링 과정에 대해서 알아봤습니다.
122 | 이제 웹 사이트들이 어떻게 그려지는지 이해할 수 있게 되었습니다!
123 | 그렇다면 이 웹 사이트들을 더 빠르게 로딩하고 버벅거리지 않게 하는 방법은 없을까요?
124 |
125 | 이번에 배운 지식을 활용하면 렌더링을 최적화하는 방법도 금방 배울 수 있습니다.
126 | 다음 포스팅에서 배워봅시다!
127 |
128 |
129 |
--------------------------------------------------------------------------------
/posts/blog/frontend/3-reflow-repaint.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 매끄럽게 동작하는 브라우저 만들기 - 렌더링 최적화 Reflow, Repaint
3 | description: 어떻게 하면 웹 사이트를 더 매끄럽게 동작시킬 수 있을까요? Reflow와 Repaint에 집중해서 최적화 방법을 알아봅시다.
4 | icon: ''
5 | image: '/posts/3-reflow-repaint/cover.png'
6 | tags:
7 | - 브라우저 렌더링 최적화
8 | - Reflow
9 | - Repaint
10 | date: 2023-07-26 15:27:32
11 | ---
12 |
13 | # 브라우저 렌더링 최적화
14 |
15 | 브라우저 렌더링이 언제 실행되는지 아시나요?
16 | 우리가 웹 사이트를 처음 마주했을 때 딱 한 번만 실행되는 것일까요? 🤔
17 |
18 |
19 |
20 | `브라우저 렌더링`은 특정 조건에 따라 `반복적으로 실행`이 됩니다.
21 | 그렇기 때문에 렌더링이 언제 발생하는지 알게되면 우리는 불필요한 렌더링을 막을 수가 있습니다.
22 |
23 | 지금부터 렌더링이 언제 발생되는지 알아보고 불필요한 렌더링을 최적화 할 방법에 대해 알아보겠습니다.
24 |
25 | ---
26 |
27 | ## 렌더링이 반복되는 경우
28 |
29 | > 1. 자바스크립트에 의한 노드 추가 또는 삭제
30 | >
31 | > 2. 브라우저 창의 리사이징에 의한 뷰포트 크기 변경
32 | >
33 | > 3. HTML 요소의 레이아웃에 변경을 발생시키는 스타일 변경
34 |
35 | 위의 경우에는 Layout 계산과 Paint 과정이 재차 실행되어 리렌더링이 발생하게 됩니다.
36 |
37 | 이 과정에서 Layout 계산을 다시 수행하는 것을 `Reflow`, 픽셀을 렌더링하는 Paint 작업을 다시 하는 것을 `Repaint`라고 합니다.
38 |
39 | 두 용어에 대해 조금 더 자세히 알아볼까요?
40 |
41 | ---
42 |
43 | ## Reflow
44 |
45 | - DOM 요소의 `위치와 크기를 다시 계산`하는 Layout 과정입니다.
46 | - 렌더링 트리를 다시 생성하므로 부하가 크고, 상위 엘리먼트가 변경되면 하위 엘리먼트에도 영향을 끼칩니다.
47 | - 예시
48 | - 노드의 추가, 제거
49 | - 위치 변경, 크기 변경, 폰트 변경, 윈도우 리사이징
50 |
51 | ### Reflow는 비싼 작업입니다 💸
52 |
53 |
54 |
55 | 여기서 주의해야할 점은 `Reflow가 발생하면 Repaint까지 발생`하게 되므로 큰 비용이
56 | 발생한다는 것입니다.
57 |
58 | 지난 시간에 배웠던 **브라우저 렌더링 과정**을 한 번 떠올려볼까요?
59 |
60 | - JS, CSS 파싱 -> 렌더 트리 구축 -> **Layout(Reflow) -> Paint(Repaint) -> 레이어 업데이트 -> Composite**
61 |
62 | 해당 흐름에 따라서 Reflow가 발생하면 Repaint와 Composite 과정을 모두 수행하게 됩니다.
63 | 따라서 성능에 문제가 있다면 가장 먼저 Reflow가 발생하고 있진 않는지 찾아보는게 필요합니다.
64 |
65 | ---
66 |
67 | ## Repaint
68 |
69 | - Layout 과정에는 영향을 미치지 않고 `변경된 요소를 화면에 그려줄 때` 발생합니다.
70 | - 예시
71 | - visibility, background-color, color
72 |
73 | 만약 Repaint만 발생하게 된다면 Repaint와 Composite 과정만 거치게 되므로 Reflow 보다는 상대적으로 훨씬 가벼운 작업이라고 할 수 있습니다.
74 |
75 | > ## Okay!
76 |
77 |
78 |
79 | 아하, 정리하자면 `Reflow`는 위치와 크기를 계산하는 `기하학적인 변화`를 담당한다면
80 | `Repaint`는 기하학적 변화 외의 `눈으로 보여지는 부분`만 변경되는 작업이군요!
81 |
82 | 그리고 Reflow는 Repaint까지 발생시키는 비싼 작업이니까, 되도록 Reflow가 발생하지 않도록 주의하는게 필요하겠네요.
83 |
84 | 거의 다 왔습니다!
85 |
86 | 이제 Reflow, Repaint를 최소화 하는 방법을 알아보면서 불필요한 렌더링을 줄여봅시다.
87 |
88 | ---
89 |
90 | ## Reflow와 Repaint 최소화 하기
91 |
92 | ### 1. 영향받는 노드 최소화하기 (position fixed, absolute)
93 |
94 | - 상위 노드의 스타일을 변경하면 하위 노드에 모두 영향을 주게 됩니다.
95 | - 따라서 다른 엘리먼트 레이아웃에 영향을 주지 않는 fixed와 absolute 속성을 사용하면 비용을 줄일 수 있습니다.
96 |
97 | ### 2. 숨겨진 엘리먼트 수정
98 |
99 | - `display: none`의 경우 Reflow와 Repaint가 발생하지 않습니다.
100 | - 많은 수의 엘리먼트를 변경해야 할 경우, 숨겨진 상태에서 변경하고 다시 보이도록 `display: none` 속성을 설정하여 레이아웃 발생을 최대한 줄일 수 있습니다.
101 | - 주의: `visibility: hidden`의 경우 보이지 않기 때문에 Repaint는 발생하지 않지만 공간을 차지하기 때문에 Layout은 발생하게 됩니다.
102 |
103 | ### 3. transform, opacity 속성 사용하기
104 |
105 | - 두 속성은 reflow와 repaint가 일어나지 않는 속성입니다.
106 | - left, right 대신 `transform` 속성을 사용하고
107 | - visibility, display 대신 `opacity` 속성을 사용하면 reflow, repaint 발생을 최소화 할 수 있습니다.
108 |
109 | ### 4. 애니메이션 최적화
110 |
111 | - 한 프레임의 처리는 16ms(`60fps`) 내로 완료되어야 끊기는 현상 없이 자연스럽게 처리됩니다.
112 | - 자바스크립트에서 setTimeout을 이용하여 애니메이션을 구현한다면 이벤트 루프에 의해 딜레이가 생길 수 있고, 16ms 안에 실행하지 못 한다면 해당 프레임은 유실됩니다 😳
113 | - 이를 도와주는 것이 `requestAnimationFrame()` 메서드입니다.
114 | - 장점: 지연 및 블로킹 없이 일정한 간격으로 애니메이션을 수행할 수 있습니다.
115 | - 브라우저 프레임 속도(60fps)에 맞춰 애니메이션을 실행할 수 있도록 합니다.
116 | - 또 다른 장점으로는 페이지가 비활성 상태일 때 렌더링을 중지하는 것이 있습니다. 덕분에 CPU의 리소스와 배터리 수명을 낭비하지 않게 되죠.
117 | - 이와 달리 setTimeout은 백그라운드 상태일 때도 계속 실행됩니다.
118 |
119 | ---
120 |
121 | ## 글을 마치며
122 |
123 | 지금까지 Reflow, Repaint 발생을 최소화 하여 브라우저 렌더링을 최적화 하는 방법에 대해서 알아봤습니다.
124 | 앞으로 개발을 하면서 성능 최적화가 필요할 때 앞서 배웠던 렌더링 과정을 떠올리면 리렌더링 원인을 파악하는게 수월해지실 겁니다!
125 |
126 |
127 |
--------------------------------------------------------------------------------
/posts/blog/frontend/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: '웹 개발 지식 쉽게 배우기 🤗'
3 | description: '브라우저 렌더링, 최적화, 네트워크, 비동기 처리 등 웹 개발에 필요한 개념들을 쉽게 배워봅시다!'
4 | tags:
5 | - web
6 | draft: false
7 | date: 2023-07-06 21:10:17
8 | ---
9 |
--------------------------------------------------------------------------------
/posts/blog/javascript/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: '자바스크립트 개념 자바드림 🤝'
3 | description: '자바스크립트의 기본 개념부터 고급 개념까지 쉽게 배워봅시다!'
4 | tags:
5 | - JavaScript
6 | draft: false
7 | date: 2023-07-06 20:47:08
8 | ---
9 |
--------------------------------------------------------------------------------
/posts/blog/nextjs/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Next.js 쉽게 이해하기 🔍'
3 | description: 'Next.js를 더 잘 활용할 수 있도록 렌더링 방식의 특징들과 성능 개선 방법에 대해 알아보겠습니다!'
4 | tags:
5 | - Next.js
6 | draft: false
7 | date: 2023-09-06 22:28:22
8 | ---
9 |
--------------------------------------------------------------------------------
/posts/blog/retrospect/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: '고찰 및 회고 😌'
3 | description: '성장하기 위해 그동안의 활동들을 돌아보며 배운 점을 기록하였습니다.'
4 | tags:
5 | - 회고
6 | draft: false
7 | date: 2023-10-23 20:28:28
8 | ---
9 |
--------------------------------------------------------------------------------
/posts/snippets/git/alias.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: '(git) Git alias로 단축키 등록하기'
3 | description: 'Git alias를 이용해서 자주 사용하는 명령어를 단축키로 등록합니다.'
4 | icon: ''
5 | image: ''
6 | tags:
7 | - git 단축키 등록
8 | - git alias
9 | draft: false
10 | date: 2023-07-06 18:33:03
11 | ---
12 |
13 | ## Git alias란?
14 |
15 | Git 명령어를 단축키를 등록해서 간단하게 사용할 수 있게 해주는 기능입니다.
16 |
17 | ### 1. **Git Commands로 설정하기**
18 |
19 | - **git config**를 사용하여 각 명령의 Alias을 쉽게 만들 수 있습니다.
20 |
21 | ```bash
22 | git config --global alias.st 'status -s'
23 | ```
24 |
25 | git st를 입력하면 git status -s 명령어가 실행됩니다.
26 |
27 | ### 2. **gitconfig 파일에 직접 설정하기**
28 |
29 | .gitconfing 파일을 vim을 통해 직접 설정이 가능합니다.
30 |
31 | ```bash
32 | vim ~/.gitconfig
33 | ```
34 |
35 | 
36 |
37 | 1. i를 눌러 입력 모드로 전환합니다.
38 | 2. [alias] 탭의 내용을 수정합니다.
39 | 3. 저는 push origin과 status를 등록했습니다.
40 | 4. 저장하기 위해 `:`를 누르고 `wq`를 입력합니다.
41 | 5. 터미널에서 git p 또는 git st가 정상 동작하는 것을 확인합니다.
42 |
43 | ## git log 꿀팁 🍯
44 |
45 | - git log를 그래프, 작성시간, 작성자가 포함된 형태로 예쁘게 볼 수 있는 단축키를 등록해보겠습니다.
46 | - 2번 방법에 따라 vim을 실행하고 아래의 내용을 입력합니다.
47 |
48 | ```bash
49 | lg = log --graph --abbrev-commit --decorate --format=format:'%C(cyan)%h%C(reset) - %C(green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(yellow)%d%C(reset)' --all
50 | ```
51 |
52 | 
53 |
54 | git lg를 입력했을 때 아래의 로그가 보이는 것을 확인합니다 😄
55 |
--------------------------------------------------------------------------------
/posts/snippets/git/hist-commit.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: '(git) Commit Template으로 commit 시간 단축하기'
3 | description: '커밋 타자 치는 시간을 줄여드립니다! Template을 이용하여 커밋 실수를 방지하고 시간을 단축하는 법에 대해 알아보겠습니다.'
4 | icon: ''
5 | image: ''
6 | tags:
7 | - git hist
8 | - git commit
9 | - vscode commit
10 | draft: false
11 | date: 2023-07-06 19:08:42
12 | ---
13 |
14 | ## VSCode에서 commit하기
15 |
16 | - 일일이 feat:... 의 커밋 규칙을 입력하기 귀찮은 분들 혹은 오타로 인해 정해놓은 커밋 규칙을 지키지 못해서 불편함을 겪었던 분들이라면, 이번 과정에 주목해주시기 바랍니다!
17 | - 터미널에서 커밋 메세지를 입력하지 않아도 **vscode에서 텍스트 파일을 수정하는 방식으로 커밋**할 수 있도록 설정해보겠습니다.
18 |
19 | ---
20 |
21 | ### 1. git commit 입력 시에 vscode에서 커밋 파일이 열리도록 설정하기
22 |
23 | - 혹시 code 명령어를 찾을 수 없다는 에러가 보인다면 [해당 포스트](https://www.enjoydev.life/snippets/settings/vscode-code)를 통해 설치해주세요!
24 |
25 | ```bash
26 | git config --global core.editor "code --wait"
27 | ```
28 |
29 | ### 2. git message 템플릿 파일 설정하기
30 |
31 | ```bash
32 | git config --global commit.template .gitmessage.txt
33 | ```
34 |
35 | ### 3. .gitmessage.txt 파일을 루트 폴더에 만들기
36 |
37 | 형식은 자유롭게 수정하셔도 좋습니다.
38 |
39 | ```txt
40 | # <타입> : <제목> 형식으로 작성하며 제목은 최대 50글자 정도로만 입력
41 | # 제목을 아랫줄에 작성, 제목 끝에 마침표 금지, 무엇을 했는지 명확하게 작성
42 |
43 | ################
44 | # [feat] : 새로운 기능 추가
45 | # [fix] : 버그 수정
46 | # [docs] : 문서 수정
47 | # [test] : 테스트 코드 추가
48 | # [refactor] : 코드 리팩토링
49 | # [style] : 코드 의미에 영향을 주지 않는 변경사항
50 | # [chore] : 빌드 부분 혹은 패키지 매니저 수정사항
51 | ################
52 | ```
53 |
54 | 
55 |
56 | ### 4. git commit을 입력
57 |
58 | 터미널에서 `git commit`을 입력하면 vscode에서 `.gitmessage.txt` 파일이 열립니다.
59 | 
60 |
61 | ### 5. 커밋하기
62 |
63 | 필요한 명령어 부분의 `주석을 지우고 내용을 입력`한뒤 txt 파일을 닫으면 (X 버튼 클릭) 커밋이 완료됩니다! 👏
64 | 
65 |
66 | ### 6. 커밋 확인
67 |
68 | git log를 보면 커밋이 잘 된 것을 확인할 수 있습니다.
69 |
70 | 개인적으로 협업할 때 적용 후 가장 편리하다고 느꼈던 기능이었습니다.
71 | 커밋에서의 실수도 줄일 수 있고 커밋을 여러번 할 경우 시간을 단축할 수 있어서
72 | (특히나 깃모지를 사용하신다면 이모지 찾는 시간을 획기적으로 줄일 수 있습니다!)
73 |
74 | 많은 분들이 사용해보셨으면 좋겠습니다 😄
75 |
--------------------------------------------------------------------------------
/posts/snippets/git/issue-template.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: '(git) 협업 시 유용한 Issue Template 만들기'
3 | description: 'Git Issue와 PR 템플릿을 만드는 법을 배워보겠습니다.'
4 | icon: ''
5 | image: ''
6 | tags:
7 | - git
8 | - Issue Template
9 | - PR Template
10 | draft: false
11 | date: 2023-07-06 14:33:01
12 | ---
13 |
14 | ## Git 템플릿
15 |
16 | Git 템플릿을 이용해서 통일된 형식으로 이슈와 PR 작성을 가능하게 해보겠습니다.
17 | 개발 내용을 명확하게 전달하고 공유할 수 있고 체크리스트로 실수를 방지할 수 있기 때문에 추천드립니다 😄
18 |
19 | ### Issue 템플릿 적용
20 |
21 | 1. 프로젝트에 들어가서 톱니바퀴 모양의 settings 탭을 누릅니다.
22 | 2. General 탭의 Features > Issues> Set up Templates를 누릅니다.
23 |
24 | 
25 |
26 | 3. Custom Template을 클릭하고 아래와 같이 내용을 채워줍니다.
27 |
28 | 
29 | 
30 |
31 | 4. Propose changes를 누르고 커밋을 하면 .github 폴더 안에 아래와 같은 파일이 생성됩니다.
32 |
33 | 
34 |
35 | 5. 이슈를 생성해보겠습니다. 방금 만들었던 템플릿을 확인할 수 있습니다.
36 |
37 | 
38 |
39 | 6. 시작 버튼을 누르면 템플릿에 맞춰 내용을 작성할 수 있습니다.
40 |
41 | 
42 |
43 | ---
44 |
45 | ### PR 템플릿
46 |
47 | 1. PR 템플릿은 pull_request_template.md 파일을 직접 만들어야 적용이 됩니다.
48 | 2. 위치는 다음 중 한 곳을 선택하면 됩니다.
49 | - .github/pull_request_template.md
50 | - docs/pull_request_template.md
51 | - root directory 아래 /pull_request_template.md
52 | 3. 방금 이슈 템플릿 생성으로 인해 .github가 생겼으니 .github 아래에 만들어보겠습니다.
53 |
54 | 
55 |
56 | 4. 해당 파일을 커밋하면 PR 생성 시에 템플릿 적용이 되는걸 확인할 수 있습니다 👍
57 |
--------------------------------------------------------------------------------
/posts/snippets/settings/husky.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: '(husky) Husky와 Lint-staged를 이용한 Lint 자동화'
3 | description: 'husky와 lint-staged를 이용해서 git hook을 적용하고 lint, prettier를 자동화 하는 법을 배워보겠습니다.'
4 | icon: ''
5 | image: ''
6 | tags:
7 | - husky
8 | - lint-staged
9 | - git hook
10 | draft: false
11 | date: 2023-07-06 18:15:38
12 | ---
13 |
14 | ## Husky
15 |
16 | Husky란 eslint나 prettier와 같은 설정을 자동화를 통해 특정 상황에서 무조건 적용이 되도록 도와주는 도구입니다.
17 | 아무리 eslint, prettier와 같은 규칙을 설정해도 작업자가 사용을 하지 않으면 소용이 없습니다 🥲
18 |
19 | 우리는 지금부터 husky를 적용해서 다음 과정을 자동화 할 것입니다.
20 |
21 | ### 1. commit 전에 prettier formatting이 잘 적용 되어있는지 검사
22 |
23 | ### 2. push 전에 eslint 규칙이 적용 되었는지 검사
24 |
25 | - pass 되었을 때만 push 하도록 함
26 |
27 | ---
28 |
29 | ## Husky의 역할
30 |
31 | - git hook 역할
32 | - git에서 **특정 이벤트 발생하기 전, 후로 특정 hook 동작을 실행**할 수 있게 합니다. (ex. commit, push)
33 | - git hook 설정은 까다로우며 모든 팀원들이 사전에 repo를 클론받고 메뉴얼하게 사전 과정을 수행해야지만 hook이 실행됨을 보장할 수 있습니다.
34 | - 실수로라도 사전 과정을 시행하지 않는다면 hook이 실행되지 않습니다.
35 | - 해결법은? **husky**
36 | - husky를 통해서 pre-commit, pre-push hook을 설정할 수 있습니다.
37 |
38 | ---
39 |
40 | ## Husky를 통한 Git Hook 적용
41 |
42 | ### 1. `npm install husky --save-dev`
43 |
44 | ### 2. (처음 husky 세팅하는 사람만 실행 필요)
45 |
46 | 1. `npx husky install` : husky에 등록된 hook을 실제 .git에 적용시키기 위한 스크립트
47 | 2. `add postinstall script` in package.json
48 |
49 | - 이후에 clone 받아서 사용하는 사람들은 npm install후에 자동으로 `husky install` 이 될 수 있도록 하는 설정입니다.
50 |
51 | ```json
52 | // package.json
53 |
54 | {
55 | "scripts": {
56 | "postinstall": "husky install"
57 | }
58 | }
59 | ```
60 |
61 | ### 3. scripts 설정
62 |
63 | ```json
64 | // package.json
65 |
66 | {
67 | "scripts": {
68 | "postinstall": "husky install",
69 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --cache",
70 | "lint-staged": "lint-staged"
71 | },
72 | "lint-staged": {
73 | "**/*.{tsx,ts,jsx,js}": ["prettier --write"]
74 | }
75 | }
76 | ```
77 |
78 | ### 4. add pre-commit, pre-push hook
79 |
80 | 1. `npx husky add .husky/pre-commit "yarn lint-staged"`
81 | 2. `npx husky add .husky/pre-push "yarn lint"`
82 |
83 | 해당 명령어를 입력하면 루트 위치에 .husky 폴더가 생성됩니다.
84 | 
85 |
86 | ### 5. prettier 포맷 검사를 해주는 lint-staged 설치
87 |
88 | - lint-staged의 장점
89 | - 포맷팅을 수행 한 뒤 git add 명령어를 자동으로 수행되게 할 수 있습니다.
90 | - 포맷팅을 전체 파일 대상이 아닌 현재 git stage에 올라온 변경사항 대상으로만 수행할 수 있습니다.
91 |
92 | ```bash
93 | yarn add -D lint-staged
94 | ```
95 |
96 | ### 6. .gitignore에 .eslintcache 파일 추가
97 |
98 | ```
99 | .eslintcache
100 | ```
101 |
102 | ---
103 |
104 | ## Husky를 적용해서 commit, push 하기
105 |
106 | ### 1. commit: 커밋할 파일이 prettier가 적용이 되었는지 검사합니다.
107 |
108 | - 강제성은 없기 때문에 적용이 안 되어있더라도 commit은 가능합니다.
109 |
110 | ```bash
111 | git add 파일
112 | git commit -m "내용"
113 | ```
114 |
115 | 
116 |
117 | ### 2. push: push할 파일들이 eslint 규칙에 어긋나지 않는지 검사합니다.
118 |
119 | - 강제성이 있기 때문에 규칙에 어긋난다면 push를 할 수 없습니다.
120 |
121 | ```bash
122 | git push origin main
123 | ```
124 |
125 | 
126 |
127 | (규칙에 어긋난 경우 에러와 워닝을 표시해줍니다.)
128 |
129 | ---
130 |
131 | ## 과정 요약
132 |
133 | 1. package.json에 있는 `postinstall`로 인해서, yarn install만 해도 모든 사람이 husky 설치를 하게 됩니다.
134 | 2. `pre-commit` git hook이 적용되어서 commit 할 때 package.json에 있는 “lint-staged”가 실행됩니다. 이것은 prettier 포맷팅을 검사합니다.
135 | 3. `pre-push` git hook이 적용되어서 push 할 때 package.json에 있는 “lint”가 실행됩니다. 이 때 commit과 다르게 lint 규칙이 맞지 않으면 push가 되지 않습니다. **(강제성)**
136 |
--------------------------------------------------------------------------------
/posts/snippets/settings/vscode-code.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: '(vscode) code 명령어 등록하기'
3 | description: ''
4 | icon: ''
5 | image: ''
6 | tags:
7 | - settings
8 | - vscode
9 | - code
10 | draft: false
11 | date: 2023-07-06 15:05:38
12 | ---
13 |
14 | ## VSCode의 code 명령어
15 |
16 | VSCode에서 개발할 때 code 명령어가 왜 필요할까요?
17 |
18 | - 터미널에서 프로젝트 폴더에 들어간 다음 `code .` 명령어를 입력하면 프로젝트 폴더에 대한 vscode가 바로 열리기 때문에 편리합니다.
19 | - 또한, git hist 또는 git commit 템플릿을 등록할 때 code 명령어가 필요합니다.
20 |
21 | 지금부터 1분만 투자해서 더 편한 개발 환경을 만들어보겠습니다! 👏
22 |
23 | ---
24 |
25 | ### code 명령어 등록
26 |
27 | 1. vscode에서 `command+shift+p` 를 누릅니다.
28 | 2. 입력창에 shell command를 입력하고 `Shell Command: Install ‘code’ command ~` 를 클릭합니다.
29 | 3. code command 등록이 완료 되었습니다. 터미널에서 code를 입력하면 실행됩니다.
30 |
31 | ---
32 |
33 | ### 권한이 없다고 뜨거나 재부팅시에 명령어가 사라진다면?
34 |
35 | - #### 방법 1.
36 |
37 | Downloads에 있는 Visual Studio Code를 Applications 폴더로 이동시킵니다.
38 |
39 | - finder를 이용하여 드래그 앤 드롭을 하면 재부팅을 안 해도 됩니다.
40 | - 터미널을 이용해 이동할 경우 재부팅이 필요합니다.
41 |
42 | ```bash
43 | mv Visual\ Studio\ Code.app /Applications
44 | ```
45 |
46 | - #### 방법2.
47 |
48 | - `rm -rf /usr/local/bin/code` 로 code 명령어를 삭제합니다. (권한이 없다면 sudo를 맨 앞에 붙여보기)
49 | - vscode에서 위의 방법에 따라 code 명령어를 다시 설치합니다.
50 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/favicon.ico
--------------------------------------------------------------------------------
/public/gif/sunflowers.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/gif/sunflowers.webp
--------------------------------------------------------------------------------
/public/gif/truck.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/gif/truck.webp
--------------------------------------------------------------------------------
/public/images/base.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/base.jpeg
--------------------------------------------------------------------------------
/public/images/blog/2023/06/darkmode/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/blog/2023/06/darkmode/cover.png
--------------------------------------------------------------------------------
/public/images/favicon/apple-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/favicon/apple-icon-180x180.png
--------------------------------------------------------------------------------
/public/images/favicon/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/favicon/favicon-16x16.png
--------------------------------------------------------------------------------
/public/images/favicon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/favicon/favicon-32x32.png
--------------------------------------------------------------------------------
/public/images/favicon/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "App",
3 | "icons": [
4 | {
5 | "src": "/android-icon-36x36.png",
6 | "sizes": "36x36",
7 | "type": "image/png",
8 | "density": "0.75"
9 | },
10 | {
11 | "src": "/android-icon-48x48.png",
12 | "sizes": "48x48",
13 | "type": "image/png",
14 | "density": "1.0"
15 | },
16 | {
17 | "src": "/android-icon-72x72.png",
18 | "sizes": "72x72",
19 | "type": "image/png",
20 | "density": "1.5"
21 | },
22 | {
23 | "src": "/android-icon-96x96.png",
24 | "sizes": "96x96",
25 | "type": "image/png",
26 | "density": "2.0"
27 | }
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/public/images/favicon/ms-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/favicon/ms-icon-144x144.png
--------------------------------------------------------------------------------
/public/images/samples/draft-cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/draft-cover.png
--------------------------------------------------------------------------------
/public/images/samples/unsplash-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-1.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-10.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-11.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-12.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-12.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-13.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-13.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-14.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-14.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-15.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-15.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-16.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-16.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-17.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-17.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-18.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-18.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-19.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-19.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-2.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-20.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-20.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-21.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-21.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-3.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-4.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-5.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-6.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-7.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-8.jpg
--------------------------------------------------------------------------------
/public/images/samples/unsplash-9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/images/samples/unsplash-9.jpg
--------------------------------------------------------------------------------
/public/posts/1-devcourse-MIL-1/231116-184408.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/1-devcourse-MIL-1/231116-184408.png
--------------------------------------------------------------------------------
/public/posts/1-devcourse-MIL-1/featured.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/1-devcourse-MIL-1/featured.png
--------------------------------------------------------------------------------
/public/posts/1-dom/230706-200314.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/1-dom/230706-200314.png
--------------------------------------------------------------------------------
/public/posts/1-dom/230706-201501.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/1-dom/230706-201501.png
--------------------------------------------------------------------------------
/public/posts/1-dom/230706-201703.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/1-dom/230706-201703.png
--------------------------------------------------------------------------------
/public/posts/1-dom/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/1-dom/cover.png
--------------------------------------------------------------------------------
/public/posts/1-dom/new_cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/1-dom/new_cover.png
--------------------------------------------------------------------------------
/public/posts/1-ssr-ssg-isr/230821-131324.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/1-ssr-ssg-isr/230821-131324.png
--------------------------------------------------------------------------------
/public/posts/1-ssr-ssg-isr/230821-132345.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/1-ssr-ssg-isr/230821-132345.png
--------------------------------------------------------------------------------
/public/posts/1-ssr-ssg-isr/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/1-ssr-ssg-isr/cover.png
--------------------------------------------------------------------------------
/public/posts/1-ssr-ssg-isr/featured.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/1-ssr-ssg-isr/featured.png
--------------------------------------------------------------------------------
/public/posts/1-var/230706-214734.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/1-var/230706-214734.png
--------------------------------------------------------------------------------
/public/posts/1-var/230706-215336.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/1-var/230706-215336.png
--------------------------------------------------------------------------------
/public/posts/1-var/230707-040145.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/1-var/230707-040145.png
--------------------------------------------------------------------------------
/public/posts/1-var/230707-040809.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/1-var/230707-040809.png
--------------------------------------------------------------------------------
/public/posts/1-var/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/1-var/cover.png
--------------------------------------------------------------------------------
/public/posts/10-cors/230929-232531.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/10-cors/230929-232531.png
--------------------------------------------------------------------------------
/public/posts/10-cors/231006-041732.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/10-cors/231006-041732.png
--------------------------------------------------------------------------------
/public/posts/10-cors/231006-041812.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/10-cors/231006-041812.png
--------------------------------------------------------------------------------
/public/posts/10-cors/231006-041831.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/10-cors/231006-041831.png
--------------------------------------------------------------------------------
/public/posts/10-cors/231006-041846.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/10-cors/231006-041846.png
--------------------------------------------------------------------------------
/public/posts/10-cors/231006-042607.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/10-cors/231006-042607.png
--------------------------------------------------------------------------------
/public/posts/10-cors/231006-042636.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/10-cors/231006-042636.png
--------------------------------------------------------------------------------
/public/posts/10-cors/231006-042908.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/10-cors/231006-042908.png
--------------------------------------------------------------------------------
/public/posts/10-cors/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/10-cors/cover.png
--------------------------------------------------------------------------------
/public/posts/10-cors/featured.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/10-cors/featured.png
--------------------------------------------------------------------------------
/public/posts/11-graphql/231020-182629.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/11-graphql/231020-182629.png
--------------------------------------------------------------------------------
/public/posts/11-graphql/231020-193016.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/11-graphql/231020-193016.png
--------------------------------------------------------------------------------
/public/posts/11-graphql/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/11-graphql/cover.png
--------------------------------------------------------------------------------
/public/posts/11-graphql/featured.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/11-graphql/featured.png
--------------------------------------------------------------------------------
/public/posts/12-suspense-errorboundary/240304-045441.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/12-suspense-errorboundary/240304-045441.png
--------------------------------------------------------------------------------
/public/posts/12-suspense-errorboundary/240304-055427.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/12-suspense-errorboundary/240304-055427.png
--------------------------------------------------------------------------------
/public/posts/2-abstract-architecture/231119-171803.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-abstract-architecture/231119-171803.png
--------------------------------------------------------------------------------
/public/posts/2-abstract-architecture/231119-190234.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-abstract-architecture/231119-190234.png
--------------------------------------------------------------------------------
/public/posts/2-abstract-architecture/231119-190440.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-abstract-architecture/231119-190440.png
--------------------------------------------------------------------------------
/public/posts/2-abstract-architecture/231119-191922.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-abstract-architecture/231119-191922.png
--------------------------------------------------------------------------------
/public/posts/2-abstract-architecture/231119-205440.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-abstract-architecture/231119-205440.png
--------------------------------------------------------------------------------
/public/posts/2-abstract-architecture/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-abstract-architecture/cover.png
--------------------------------------------------------------------------------
/public/posts/2-browser-rendering/230724-143333.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-browser-rendering/230724-143333.png
--------------------------------------------------------------------------------
/public/posts/2-browser-rendering/230724-143341.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-browser-rendering/230724-143341.png
--------------------------------------------------------------------------------
/public/posts/2-browser-rendering/230724-143409.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-browser-rendering/230724-143409.png
--------------------------------------------------------------------------------
/public/posts/2-browser-rendering/230724-143539.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-browser-rendering/230724-143539.png
--------------------------------------------------------------------------------
/public/posts/2-browser-rendering/230724-145527.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-browser-rendering/230724-145527.png
--------------------------------------------------------------------------------
/public/posts/2-browser-rendering/230724-145656.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-browser-rendering/230724-145656.png
--------------------------------------------------------------------------------
/public/posts/2-browser-rendering/230724-145700.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-browser-rendering/230724-145700.png
--------------------------------------------------------------------------------
/public/posts/2-browser-rendering/230724-145856.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-browser-rendering/230724-145856.png
--------------------------------------------------------------------------------
/public/posts/2-browser-rendering/230724-151608.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-browser-rendering/230724-151608.png
--------------------------------------------------------------------------------
/public/posts/2-browser-rendering/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-browser-rendering/cover.png
--------------------------------------------------------------------------------
/public/posts/2-lighthouse/230907-212702.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-lighthouse/230907-212702.png
--------------------------------------------------------------------------------
/public/posts/2-lighthouse/230907-212718.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-lighthouse/230907-212718.png
--------------------------------------------------------------------------------
/public/posts/2-lighthouse/230907-212811.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-lighthouse/230907-212811.png
--------------------------------------------------------------------------------
/public/posts/2-lighthouse/230907-214124.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-lighthouse/230907-214124.png
--------------------------------------------------------------------------------
/public/posts/2-lighthouse/230907-224248.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-lighthouse/230907-224248.png
--------------------------------------------------------------------------------
/public/posts/2-lighthouse/230907-224425.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-lighthouse/230907-224425.png
--------------------------------------------------------------------------------
/public/posts/2-lighthouse/230907-224948.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-lighthouse/230907-224948.png
--------------------------------------------------------------------------------
/public/posts/2-lighthouse/230907-231118.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-lighthouse/230907-231118.png
--------------------------------------------------------------------------------
/public/posts/2-lighthouse/230907-231135.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-lighthouse/230907-231135.png
--------------------------------------------------------------------------------
/public/posts/2-lighthouse/230907-231302.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-lighthouse/230907-231302.png
--------------------------------------------------------------------------------
/public/posts/2-lighthouse/230907-231348.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-lighthouse/230907-231348.png
--------------------------------------------------------------------------------
/public/posts/2-lighthouse/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-lighthouse/cover.png
--------------------------------------------------------------------------------
/public/posts/2-var-let-const/230713-211114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-var-let-const/230713-211114.png
--------------------------------------------------------------------------------
/public/posts/2-var-let-const/230713-220042.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-var-let-const/230713-220042.png
--------------------------------------------------------------------------------
/public/posts/2-var-let-const/230713-225458.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-var-let-const/230713-225458.png
--------------------------------------------------------------------------------
/public/posts/2-var-let-const/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/2-var-let-const/cover.png
--------------------------------------------------------------------------------
/public/posts/3-devcourse-mil-2/231123-001702.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-devcourse-mil-2/231123-001702.png
--------------------------------------------------------------------------------
/public/posts/3-devcourse-mil-2/231123-011602.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-devcourse-mil-2/231123-011602.png
--------------------------------------------------------------------------------
/public/posts/3-devcourse-mil-2/231123-013002.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-devcourse-mil-2/231123-013002.png
--------------------------------------------------------------------------------
/public/posts/3-devcourse-mil-2/231123-013131.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-devcourse-mil-2/231123-013131.png
--------------------------------------------------------------------------------
/public/posts/3-devcourse-mil-2/231123-022919.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-devcourse-mil-2/231123-022919.png
--------------------------------------------------------------------------------
/public/posts/3-devcourse-mil-2/231123-030343.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-devcourse-mil-2/231123-030343.png
--------------------------------------------------------------------------------
/public/posts/3-devcourse-mil-2/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-devcourse-mil-2/cover.png
--------------------------------------------------------------------------------
/public/posts/3-javascript-single-thread/230810-211104.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-javascript-single-thread/230810-211104.png
--------------------------------------------------------------------------------
/public/posts/3-javascript-single-thread/230810-211629.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-javascript-single-thread/230810-211629.png
--------------------------------------------------------------------------------
/public/posts/3-javascript-single-thread/230810-211811.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-javascript-single-thread/230810-211811.png
--------------------------------------------------------------------------------
/public/posts/3-javascript-single-thread/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-javascript-single-thread/cover.png
--------------------------------------------------------------------------------
/public/posts/3-performance/230911-093453.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-performance/230911-093453.png
--------------------------------------------------------------------------------
/public/posts/3-performance/230911-093523.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-performance/230911-093523.png
--------------------------------------------------------------------------------
/public/posts/3-performance/230911-093645.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-performance/230911-093645.png
--------------------------------------------------------------------------------
/public/posts/3-performance/230911-093849.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-performance/230911-093849.png
--------------------------------------------------------------------------------
/public/posts/3-performance/230911-094750.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-performance/230911-094750.png
--------------------------------------------------------------------------------
/public/posts/3-performance/230911-095031.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-performance/230911-095031.png
--------------------------------------------------------------------------------
/public/posts/3-performance/230911-095116.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-performance/230911-095116.png
--------------------------------------------------------------------------------
/public/posts/3-performance/230911-100033.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-performance/230911-100033.png
--------------------------------------------------------------------------------
/public/posts/3-performance/230911-102823.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-performance/230911-102823.png
--------------------------------------------------------------------------------
/public/posts/3-performance/230911-103155.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-performance/230911-103155.png
--------------------------------------------------------------------------------
/public/posts/3-performance/230911-103739.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-performance/230911-103739.png
--------------------------------------------------------------------------------
/public/posts/3-performance/230911-104422.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-performance/230911-104422.png
--------------------------------------------------------------------------------
/public/posts/3-performance/230911-104707.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-performance/230911-104707.png
--------------------------------------------------------------------------------
/public/posts/3-performance/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-performance/cover.png
--------------------------------------------------------------------------------
/public/posts/3-reflow-repaint/230726-000206.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-reflow-repaint/230726-000206.png
--------------------------------------------------------------------------------
/public/posts/3-reflow-repaint/230726-001638.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-reflow-repaint/230726-001638.png
--------------------------------------------------------------------------------
/public/posts/3-reflow-repaint/230726-145258.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-reflow-repaint/230726-145258.png
--------------------------------------------------------------------------------
/public/posts/3-reflow-repaint/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/3-reflow-repaint/cover.png
--------------------------------------------------------------------------------
/public/posts/4-devcourse-MIL-3/231229-131323.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-devcourse-MIL-3/231229-131323.png
--------------------------------------------------------------------------------
/public/posts/4-devcourse-MIL-3/231229-133437.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-devcourse-MIL-3/231229-133437.png
--------------------------------------------------------------------------------
/public/posts/4-devcourse-MIL-3/231229-140518.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-devcourse-MIL-3/231229-140518.png
--------------------------------------------------------------------------------
/public/posts/4-event-loop/230811-213055.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-event-loop/230811-213055.png
--------------------------------------------------------------------------------
/public/posts/4-event-loop/230811-213225.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-event-loop/230811-213225.png
--------------------------------------------------------------------------------
/public/posts/4-event-loop/230811-213538.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-event-loop/230811-213538.png
--------------------------------------------------------------------------------
/public/posts/4-event-loop/230811-215226.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-event-loop/230811-215226.png
--------------------------------------------------------------------------------
/public/posts/4-event-loop/230811-215357.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-event-loop/230811-215357.png
--------------------------------------------------------------------------------
/public/posts/4-event-loop/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-event-loop/cover.png
--------------------------------------------------------------------------------
/public/posts/4-module-bundler/230818-152415.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-module-bundler/230818-152415.png
--------------------------------------------------------------------------------
/public/posts/4-module-bundler/230818-153420.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-module-bundler/230818-153420.png
--------------------------------------------------------------------------------
/public/posts/4-module-bundler/230818-163527.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-module-bundler/230818-163527.png
--------------------------------------------------------------------------------
/public/posts/4-module-bundler/230818-172206.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-module-bundler/230818-172206.png
--------------------------------------------------------------------------------
/public/posts/4-module-bundler/230818-172731.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-module-bundler/230818-172731.png
--------------------------------------------------------------------------------
/public/posts/4-module-bundler/230818-173707.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-module-bundler/230818-173707.png
--------------------------------------------------------------------------------
/public/posts/4-module-bundler/230818-173834.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-module-bundler/230818-173834.png
--------------------------------------------------------------------------------
/public/posts/4-module-bundler/230818-174028.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-module-bundler/230818-174028.png
--------------------------------------------------------------------------------
/public/posts/4-module-bundler/230824-202514.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-module-bundler/230824-202514.png
--------------------------------------------------------------------------------
/public/posts/4-module-bundler/230824-203200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-module-bundler/230824-203200.png
--------------------------------------------------------------------------------
/public/posts/4-module-bundler/featured.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-module-bundler/featured.png
--------------------------------------------------------------------------------
/public/posts/4-nextjs-13ver/230919-235424.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-nextjs-13ver/230919-235424.png
--------------------------------------------------------------------------------
/public/posts/4-nextjs-13ver/230921-235500.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-nextjs-13ver/230921-235500.png
--------------------------------------------------------------------------------
/public/posts/4-nextjs-13ver/230921-235647.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-nextjs-13ver/230921-235647.png
--------------------------------------------------------------------------------
/public/posts/4-nextjs-13ver/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-nextjs-13ver/cover.png
--------------------------------------------------------------------------------
/public/posts/4-nextjs-13ver/featured.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/4-nextjs-13ver/featured.png
--------------------------------------------------------------------------------
/public/posts/5-csr-ssr/230820-234931.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/5-csr-ssr/230820-234931.png
--------------------------------------------------------------------------------
/public/posts/5-csr-ssr/230820-235218.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/5-csr-ssr/230820-235218.png
--------------------------------------------------------------------------------
/public/posts/5-csr-ssr/230820-235249.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/5-csr-ssr/230820-235249.png
--------------------------------------------------------------------------------
/public/posts/5-csr-ssr/230824-202351.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/5-csr-ssr/230824-202351.png
--------------------------------------------------------------------------------
/public/posts/5-csr-ssr/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/5-csr-ssr/cover.png
--------------------------------------------------------------------------------
/public/posts/5-daangn-interview/240114-205712.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/5-daangn-interview/240114-205712.png
--------------------------------------------------------------------------------
/public/posts/5-daangn-interview/240114-205938.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/5-daangn-interview/240114-205938.png
--------------------------------------------------------------------------------
/public/posts/5-daangn-interview/240114-234758.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/5-daangn-interview/240114-234758.png
--------------------------------------------------------------------------------
/public/posts/5-daangn-interview/240115-010259.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/5-daangn-interview/240115-010259.png
--------------------------------------------------------------------------------
/public/posts/5-daangn-interview/240115-014008.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/5-daangn-interview/240115-014008.png
--------------------------------------------------------------------------------
/public/posts/5-daangn-interview/240115-022047.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/5-daangn-interview/240115-022047.png
--------------------------------------------------------------------------------
/public/posts/5-daangn-interview/240115-034801.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/5-daangn-interview/240115-034801.png
--------------------------------------------------------------------------------
/public/posts/5-this/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/5-this/cover.png
--------------------------------------------------------------------------------
/public/posts/6-closure/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/6-closure/cover.png
--------------------------------------------------------------------------------
/public/posts/6-devcourse-MIL-4/240123-220544.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/6-devcourse-MIL-4/240123-220544.png
--------------------------------------------------------------------------------
/public/posts/6-devcourse-MIL-4/240123-220636.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/6-devcourse-MIL-4/240123-220636.png
--------------------------------------------------------------------------------
/public/posts/6-devcourse-MIL-4/240123-220732.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/6-devcourse-MIL-4/240123-220732.png
--------------------------------------------------------------------------------
/public/posts/6-devcourse-MIL-4/240123-220751.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/6-devcourse-MIL-4/240123-220751.png
--------------------------------------------------------------------------------
/public/posts/6-devcourse-MIL-4/240123-221914.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/6-devcourse-MIL-4/240123-221914.png
--------------------------------------------------------------------------------
/public/posts/6-devcourse-MIL-4/240123-222026.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/6-devcourse-MIL-4/240123-222026.png
--------------------------------------------------------------------------------
/public/posts/6-devcourse-MIL-4/240124-135607.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/6-devcourse-MIL-4/240124-135607.png
--------------------------------------------------------------------------------
/public/posts/6-devcourse-MIL-4/240124-135656.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/6-devcourse-MIL-4/240124-135656.png
--------------------------------------------------------------------------------
/public/posts/6-devcourse-MIL-4/240124-140313.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/6-devcourse-MIL-4/240124-140313.png
--------------------------------------------------------------------------------
/public/posts/6-devcourse-MIL-4/240124-140437.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/6-devcourse-MIL-4/240124-140437.png
--------------------------------------------------------------------------------
/public/posts/6-devcourse-MIL-4/240124-153555.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/6-devcourse-MIL-4/240124-153555.png
--------------------------------------------------------------------------------
/public/posts/6-devcourse-MIL-4/240124-154318.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/6-devcourse-MIL-4/240124-154318.png
--------------------------------------------------------------------------------
/public/posts/6-devcourse-MIL-4/240124-154437.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/6-devcourse-MIL-4/240124-154437.png
--------------------------------------------------------------------------------
/public/posts/6-devcourse-MIL-4/240124-154803.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/6-devcourse-MIL-4/240124-154803.png
--------------------------------------------------------------------------------
/public/posts/7-javascript-performance/230915-203115.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-javascript-performance/230915-203115.png
--------------------------------------------------------------------------------
/public/posts/7-javascript-performance/230915-203119.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-javascript-performance/230915-203119.png
--------------------------------------------------------------------------------
/public/posts/7-javascript-performance/230915-203619.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-javascript-performance/230915-203619.png
--------------------------------------------------------------------------------
/public/posts/7-toss-interview/240601-214439.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-toss-interview/240601-214439.png
--------------------------------------------------------------------------------
/public/posts/7-toss-interview/240601-214833.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-toss-interview/240601-214833.png
--------------------------------------------------------------------------------
/public/posts/7-toss-interview/240601-230525.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-toss-interview/240601-230525.png
--------------------------------------------------------------------------------
/public/posts/7-toss-interview/240601-233844.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-toss-interview/240601-233844.png
--------------------------------------------------------------------------------
/public/posts/7-toss-interview/240602-001419.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-toss-interview/240602-001419.png
--------------------------------------------------------------------------------
/public/posts/7-toss-interview/240602-232049.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-toss-interview/240602-232049.png
--------------------------------------------------------------------------------
/public/posts/7-toss-interview/240603-010120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-toss-interview/240603-010120.png
--------------------------------------------------------------------------------
/public/posts/7-toss-interview/240603-010322.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-toss-interview/240603-010322.png
--------------------------------------------------------------------------------
/public/posts/7-toss-interview/240603-022016.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-toss-interview/240603-022016.png
--------------------------------------------------------------------------------
/public/posts/7-toss-interview/240603-032933.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-toss-interview/240603-032933.png
--------------------------------------------------------------------------------
/public/posts/7-toss-interview/240603-034045.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-toss-interview/240603-034045.png
--------------------------------------------------------------------------------
/public/posts/7-toss-interview/240603-034219.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-toss-interview/240603-034219.png
--------------------------------------------------------------------------------
/public/posts/7-yarn-npm/230904-010059.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-yarn-npm/230904-010059.png
--------------------------------------------------------------------------------
/public/posts/7-yarn-npm/230904-115613.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-yarn-npm/230904-115613.png
--------------------------------------------------------------------------------
/public/posts/7-yarn-npm/230904-122752.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-yarn-npm/230904-122752.png
--------------------------------------------------------------------------------
/public/posts/7-yarn-npm/230904-122931.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-yarn-npm/230904-122931.png
--------------------------------------------------------------------------------
/public/posts/7-yarn-npm/230904-123103.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-yarn-npm/230904-123103.png
--------------------------------------------------------------------------------
/public/posts/7-yarn-npm/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/7-yarn-npm/cover.png
--------------------------------------------------------------------------------
/public/posts/8-monorepo/230904-231926.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/8-monorepo/230904-231926.png
--------------------------------------------------------------------------------
/public/posts/8-monorepo/230904-234635.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/8-monorepo/230904-234635.png
--------------------------------------------------------------------------------
/public/posts/8-monorepo/230905-150550.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/8-monorepo/230905-150550.png
--------------------------------------------------------------------------------
/public/posts/8-monorepo/230905-153434.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/8-monorepo/230905-153434.png
--------------------------------------------------------------------------------
/public/posts/8-monorepo/230905-165659.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/8-monorepo/230905-165659.png
--------------------------------------------------------------------------------
/public/posts/8-monorepo/230905-165945.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/8-monorepo/230905-165945.png
--------------------------------------------------------------------------------
/public/posts/8-monorepo/230905-170049.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/8-monorepo/230905-170049.png
--------------------------------------------------------------------------------
/public/posts/8-monorepo/230905-170430.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/8-monorepo/230905-170430.png
--------------------------------------------------------------------------------
/public/posts/8-monorepo/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/8-monorepo/cover.png
--------------------------------------------------------------------------------
/public/posts/9-optimizing-loading-speed/230915-213601.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/9-optimizing-loading-speed/230915-213601.png
--------------------------------------------------------------------------------
/public/posts/9-optimizing-loading-speed/230915-223120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/9-optimizing-loading-speed/230915-223120.png
--------------------------------------------------------------------------------
/public/posts/9-optimizing-loading-speed/230915-223127.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/9-optimizing-loading-speed/230915-223127.png
--------------------------------------------------------------------------------
/public/posts/9-optimizing-loading-speed/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/9-optimizing-loading-speed/cover.png
--------------------------------------------------------------------------------
/public/posts/9-optimizing-loading-speed/featured.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/9-optimizing-loading-speed/featured.png
--------------------------------------------------------------------------------
/public/posts/alias/230706-180256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/alias/230706-180256.png
--------------------------------------------------------------------------------
/public/posts/alias/230706-180514.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/alias/230706-180514.png
--------------------------------------------------------------------------------
/public/posts/commit/230706-182223.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/commit/230706-182223.png
--------------------------------------------------------------------------------
/public/posts/commit/230706-182303.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/commit/230706-182303.png
--------------------------------------------------------------------------------
/public/posts/commit/230706-182532.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/commit/230706-182532.png
--------------------------------------------------------------------------------
/public/posts/commit/230706-182842.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/commit/230706-182842.png
--------------------------------------------------------------------------------
/public/posts/husky/230706-185053.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/husky/230706-185053.png
--------------------------------------------------------------------------------
/public/posts/husky/230706-185355.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/husky/230706-185355.png
--------------------------------------------------------------------------------
/public/posts/husky/230706-185406.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/husky/230706-185406.png
--------------------------------------------------------------------------------
/public/posts/issue-template/230706-183626.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/issue-template/230706-183626.png
--------------------------------------------------------------------------------
/public/posts/issue-template/230706-183643.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/issue-template/230706-183643.png
--------------------------------------------------------------------------------
/public/posts/issue-template/230706-183658.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/issue-template/230706-183658.png
--------------------------------------------------------------------------------
/public/posts/issue-template/230706-183812.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/issue-template/230706-183812.png
--------------------------------------------------------------------------------
/public/posts/issue-template/230706-183823.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/issue-template/230706-183823.png
--------------------------------------------------------------------------------
/public/posts/issue-template/230706-183836.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/issue-template/230706-183836.png
--------------------------------------------------------------------------------
/public/posts/issue-template/230706-184024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pySoo/Next-blog/3cdf6866cce5a0505b51db19d6eaa4ed2771f583/public/posts/issue-template/230706-184024.png
--------------------------------------------------------------------------------
/scripts/generateSitemap.ts:
--------------------------------------------------------------------------------
1 | import { writeFileSync } from 'fs';
2 |
3 | import PostJson from '../.contentlayer/generated/Post/_index.json';
4 | import { siteConfig } from '../src/constants/config';
5 |
6 | const createSiteMap = () => {
7 | const siteUrl = siteConfig.url;
8 |
9 | const sitemap = `
10 |
11 | ${siteUrl}daily
12 | ${siteConfig.menus
13 | .map(
14 | ({ path }) =>
15 | `${siteUrl}${path}daily`,
16 | )
17 | .join('\n')}
18 | ${PostJson.map(
19 | (series) =>
20 | `${siteUrl}${series.slug}daily`,
21 | ).join('\n')}
22 | `;
23 |
24 | writeFileSync('out/sitemap.xml', sitemap, 'utf-8');
25 | };
26 |
27 | const createRobotsTxt = () => {
28 | const siteUrl = siteConfig.url;
29 |
30 | const text = `
31 | User-agent: *
32 | Allow: /
33 | Sitemap: ${siteUrl}/sitemap.xml
34 | Host: ${siteUrl}
35 | `;
36 |
37 | writeFileSync('out/robots.txt', text.trim(), 'utf-8');
38 | };
39 |
40 | (() => {
41 | createSiteMap();
42 | createRobotsTxt();
43 | })();
44 |
--------------------------------------------------------------------------------
/src/components/AuthorContacts.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { siteConfig } from '@/constants/config';
4 | import { $ } from '@/libs/core';
5 |
6 | import LinkExternal from './common/LinkExternal';
7 | import ContactsIcon from './icons/ContactsIcon';
8 |
9 | export default function AuthorContacts({
10 | className,
11 | ...props
12 | }: React.ComponentProps<'div'>) {
13 | return (
14 |
15 | {Object.keys(siteConfig.author.contacts).map((sns) => {
16 | let contact =
17 | siteConfig.author.contacts[
18 | sns as keyof typeof siteConfig.author.contacts
19 | ];
20 |
21 | if (sns === 'email') contact = `mailto:${contact}`;
22 | else if (sns === 'github') contact = `https://github.com/${contact}`;
23 | else if (sns === 'velog') contact = `https://velog.io/${contact}`;
24 |
25 | return !contact ? null : (
26 |
27 |
28 |
29 | );
30 | })}
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/SEO.tsx:
--------------------------------------------------------------------------------
1 | import { ArticleJsonLd, NextSeo } from 'next-seo';
2 |
3 | import { siteConfig } from '@/constants/config';
4 |
5 | const getTitle = (title?: string) => {
6 | if (!title) return siteConfig.title;
7 | if (title.length > 10) return title;
8 |
9 | return `${title} - pySoo`;
10 | };
11 |
12 | const getRelativeUrl = (url?: string) => {
13 | if (!url) return siteConfig.url;
14 |
15 | return `${siteConfig.url}/${url.replace(/^\/+/g, '')}`;
16 | };
17 |
18 | const DEFAULT_IMAGE = `${siteConfig.url}/images/base.jpeg`;
19 |
20 | const getImageUrl = (img?: string) => {
21 | if (!img) return DEFAULT_IMAGE;
22 | if (img.startsWith('https://')) return img;
23 |
24 | return `${siteConfig.url}/${img}`;
25 | };
26 |
27 | /**
28 | * use in Normal Page SEO
29 | */
30 |
31 | type PageSEOType = {
32 | title?: string;
33 | description?: string;
34 | url?: string;
35 | image?: string;
36 | };
37 |
38 | export function PageSEO({ ...props }: PageSEOType) {
39 | const title = getTitle(props.title);
40 | const description = props.description ?? siteConfig.description;
41 | const url = getRelativeUrl(props.url);
42 | const image = getImageUrl(props.image);
43 |
44 | return (
45 |
56 | );
57 | }
58 |
59 | /**
60 | * use in Article Page SEO
61 | */
62 | export function BlogSEO({
63 | summary,
64 | tags,
65 | image,
66 | ...props
67 | }: {
68 | title: string;
69 | summary: string;
70 | date: string;
71 | url: string;
72 | tags: string[];
73 | image?: string;
74 | }) {
75 | const title = getTitle(props.title);
76 | const url = getRelativeUrl(props.url);
77 | const dateTime = new Date(props.date).toISOString();
78 | const coverImage = image ?? DEFAULT_IMAGE;
79 |
80 | return (
81 | <>
82 |
100 |
111 | >
112 | );
113 | }
114 |
--------------------------------------------------------------------------------
/src/components/ThemeSwitch.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | import useDarkMode from '@/libs/useDarkMode';
4 |
5 | import IconButton from './common/IconButton';
6 |
7 | export default function ThemeSwitch(props: React.ComponentProps<'button'>) {
8 | const [mounted, setMounted] = useState(false);
9 | const { isThemeDark, toggleTheme } = useDarkMode();
10 |
11 | useEffect(() => setMounted(true), []);
12 |
13 | return (
14 |
15 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/common/AnimatedContainer.tsx:
--------------------------------------------------------------------------------
1 | import { AnimationProps, motion, MotionProps } from 'framer-motion';
2 | import { ReactNode } from 'react';
3 |
4 | interface AnimatedContainerProps extends AnimationProps, MotionProps {
5 | as?: keyof JSX.IntrinsicElements;
6 | className?: string;
7 | useTransition?: boolean;
8 | children: ReactNode;
9 | }
10 |
11 | const defaultMotionProps = {
12 | initial: 'initial',
13 | animate: 'animate',
14 | exit: 'exit',
15 | };
16 |
17 | export default function AnimatedContainer({
18 | as = 'div',
19 | className,
20 | useTransition,
21 | children,
22 | ...props
23 | }: AnimatedContainerProps) {
24 | const Container = motion[as as keyof typeof motion];
25 |
26 | return (
27 |
32 | {children}
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/common/CopyLinkButton.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { toast } from 'react-hot-toast';
3 |
4 | import CheckIcon from '@/components/icons/CheckIcon';
5 | import LinkIcon from '@/components/icons/LinkIcon';
6 | import useWatchTimeout from '@/libs/useWatchTimeout';
7 |
8 | import IconButton from './IconButton';
9 |
10 | export default function CopyLinkButton(props: React.ComponentProps<'button'>) {
11 | const [copied, setCopied] = useState(false);
12 |
13 | useWatchTimeout(copied, 1500, () => {
14 | setCopied(false);
15 | });
16 |
17 | const handleCopy = async () => {
18 | const url = window.document.location.href;
19 |
20 | try {
21 | await navigator.clipboard.writeText(url);
22 | setCopied(true);
23 | toast('링크를 복사했습니다.', { icon: '🔗' });
24 | } catch (e) {
25 | console.error(e);
26 | toast.error('링크 복사에 실패했습니다.');
27 | }
28 | };
29 |
30 | return (
31 |
32 | {copied ? : }
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/common/Fonts.tsx:
--------------------------------------------------------------------------------
1 | import { fontMono, fontSans } from '@/constants/fonts';
2 |
3 | export default function Fonts() {
4 | return (
5 | <>
6 |
12 | >
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/common/HeaderNavigation.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { useRouter } from 'next/router';
3 | import { useEffect, useState } from 'react';
4 |
5 | import { LogoIcon, NavItem, SearchInput } from '@/components/common';
6 | import ThemeSwitch from '@/components/ThemeSwitch';
7 | import { siteConfig } from '@/constants/config';
8 | import { $ } from '@/libs/core';
9 |
10 | export default function HeaderNavigation() {
11 | const router = useRouter();
12 | const [isMenuOpen, setIsMenuOpen] = useState(false);
13 | const currentPath = '/' + router.asPath.split('/')[1].split('?')[0] ?? '';
14 |
15 | const currentSiteMenu = siteConfig.menus.filter(
16 | (item) => item.path === currentPath,
17 | );
18 |
19 | const isActiveNav = (navPath: string) => {
20 | if (navPath === '/') return router.asPath === navPath;
21 |
22 | return router.asPath.startsWith(navPath);
23 | };
24 |
25 | const toggleMenu = () => {
26 | if (isMenuOpen) {
27 | setIsMenuOpen(false);
28 | document.body.classList.remove('overflow-hidden');
29 | } else {
30 | setIsMenuOpen(true);
31 | document.body.classList.add('overflow-hidden');
32 | }
33 | };
34 |
35 | useEffect(() => {
36 | return function cleanup() {
37 | document.body.classList.remove('overflow-hidden');
38 | };
39 | }, []);
40 |
41 | return (
42 |
94 | );
95 | }
96 |
--------------------------------------------------------------------------------
/src/components/common/HoverCard.tsx:
--------------------------------------------------------------------------------
1 | import { gsap } from 'gsap';
2 | import React, { useCallback, useState } from 'react';
3 |
4 | export default function HoverCard({
5 | children,
6 | }: {
7 | children?: React.ReactNode;
8 | }) {
9 | const [cardElement, setCardElement] = useState(null);
10 |
11 | const setTransformVariable: React.MouseEventHandler =
12 | useCallback(
13 | (e) => {
14 | if (cardElement != null) {
15 | const bounds = cardElement.getBoundingClientRect();
16 |
17 | const mouseX = e.clientX;
18 | const mouseY = e.clientY;
19 | const leftX = mouseX - bounds.x;
20 | const topY = mouseY - bounds.y;
21 |
22 | const cardX = leftX - bounds.width / 2;
23 | const cardY = topY - bounds.height / 2;
24 |
25 | const cardRotateX = cardY / 100;
26 | const cardRotateY = (-1 * cardX) / 100;
27 |
28 | gsap.to(cardElement, {
29 | scale: 1.07,
30 | rotateX: cardRotateX * 5,
31 | rotateY: cardRotateY * 5,
32 | });
33 | }
34 | },
35 | [cardElement],
36 | );
37 |
38 | const clearTransformVariable = useCallback(() => {
39 | if (cardElement != null) {
40 | gsap.to(cardElement, {
41 | scale: 1,
42 | rotateX: 0,
43 | rotateY: 0,
44 | });
45 | }
46 | }, [cardElement]);
47 |
48 | return (
49 |
50 |
55 |
56 | {children}
57 |
58 |
59 |
60 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/src/components/common/IconButton.tsx:
--------------------------------------------------------------------------------
1 | import { $ } from '@/libs/core';
2 |
3 | export default function IconButton({
4 | className,
5 | type = 'button',
6 | ...props
7 | }: React.ComponentProps<'button'>) {
8 | return (
9 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/common/IconText.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { $ } from '@/libs/core';
4 |
5 | export interface IconTextProps extends React.ComponentProps<'div'> {
6 | Icon: (props: React.ComponentProps<'svg'>) => JSX.Element;
7 | iconSize?: number;
8 | text?: React.ReactNode;
9 | className?: string;
10 | }
11 |
12 | export default function IconText({
13 | Icon,
14 | iconSize = 14,
15 | text,
16 | className,
17 | ...props
18 | }: IconTextProps) {
19 | return (
20 |
24 |
25 | {text}
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/common/LinkArrow.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | import { $ } from '@/libs/core';
4 |
5 | export default function LinkArrow({
6 | ref: _,
7 | className,
8 | href,
9 | children,
10 | ...props
11 | }: React.ComponentProps<'a'>) {
12 | return (
13 |
22 | {children}
23 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/common/LinkExternal.tsx:
--------------------------------------------------------------------------------
1 | import { $ } from '@/libs/core';
2 |
3 | export default function LinkExternal({
4 | children,
5 | className,
6 | ...props
7 | }: React.ComponentProps<'a'>) {
8 | return (
9 |
15 | {children}
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/common/LinkHover.tsx:
--------------------------------------------------------------------------------
1 | import { $ } from '@/libs/core';
2 |
3 | export default function LinkHover({
4 | ref: _,
5 | className,
6 | href,
7 | children,
8 | ...props
9 | }: React.ComponentProps<'a'>) {
10 | return (
11 |
19 | {children}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/common/LogoIcon.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 |
3 | import { $ } from '@/libs/core';
4 |
5 | export default function LogoIcon({ className }: React.ComponentProps<'img'>) {
6 | return (
7 |
8 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/common/NavItem.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 |
3 | import { $ } from '@/libs/core';
4 |
5 | import LinkHover from './LinkHover';
6 |
7 | export default function NavItem({
8 | href,
9 | children,
10 | className,
11 | ...props
12 | }: React.ComponentProps<'a'>) {
13 | const router = useRouter();
14 | const currentPath = '/' + router.asPath.split('/')[1].split('?')[0] ?? '';
15 | const isActive = href === currentPath;
16 |
17 | return (
18 |
26 | {children}
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/common/Pill.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { $ } from '@/libs/core';
4 |
5 | export interface PillProps extends React.ComponentProps<'div'> {
6 | selected?: boolean;
7 | }
8 |
9 | export default function Pill({ className, selected, ...props }: PillProps) {
10 | return (
11 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/common/PlainText.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { $ } from '@/libs/core';
4 |
5 | export default function PlainText({
6 | children,
7 | className,
8 | }: {
9 | children?: React.ReactNode;
10 | className?: string;
11 | }) {
12 | return (
13 |
14 | {children}
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/common/PressedEffect.tsx:
--------------------------------------------------------------------------------
1 | import { hasTouch } from 'detect-touch';
2 | import { motion } from 'framer-motion';
3 | import { MouseEvent, TouchEvent, useRef, useState } from 'react';
4 |
5 | interface PressedEffectProps {
6 | children: React.ReactNode;
7 | className?: string;
8 | disable?: boolean;
9 | action?: () => void;
10 | }
11 |
12 | export default function PressedEffect({
13 | children,
14 | className,
15 | disable = false,
16 | action,
17 | }: PressedEffectProps) {
18 | const [buttonDown, setButtonDown] = useState(false);
19 | const [touchStartX, setTouchStartX] = useState(0);
20 | const [touchStartY, setTouchStartY] = useState(0);
21 |
22 | const pressedRef = useRef(null);
23 |
24 | const handleMouseDown = (e: MouseEvent) => {
25 | if (!hasTouch) {
26 | e.preventDefault();
27 | e.stopPropagation();
28 | setButtonDown(true);
29 | }
30 | };
31 | const handleClick = (e: MouseEvent) => {
32 | if (!hasTouch) {
33 | if (!disable && action) {
34 | e.preventDefault();
35 | e.stopPropagation();
36 | action();
37 | }
38 | }
39 | };
40 |
41 | const handleTouchStart = (e: TouchEvent) => {
42 | setTouchStartY(e.changedTouches[0].clientY);
43 | setTouchStartX(e.changedTouches[0].clientX);
44 | if (!disable) {
45 | if (e.cancelable) e.preventDefault();
46 | e.stopPropagation();
47 | setButtonDown(true);
48 | }
49 | };
50 |
51 | const handleTouchMove = (e: TouchEvent) => {
52 | if (
53 | Math.abs(e.changedTouches[0].clientY - touchStartY) > 0 ||
54 | Math.abs(e.changedTouches[0].clientX - touchStartX) > 0
55 | ) {
56 | setButtonDown(false);
57 | }
58 | };
59 |
60 | const handleTouchEnd = (e: TouchEvent) => {
61 | setButtonDown(false);
62 |
63 | if (
64 | !(
65 | Math.abs(e.changedTouches[0].clientY - touchStartY) > 5 ||
66 | Math.abs(e.changedTouches[0].clientX - touchStartX) > 5
67 | )
68 | ) {
69 | if (!disable && action) {
70 | if (e.cancelable) e.preventDefault();
71 | e.stopPropagation();
72 | action();
73 | }
74 | }
75 | };
76 |
77 | return (
78 |
89 | {children}
90 |
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/common/SearchInput.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import { ChangeEvent, FormEvent, useEffect, useState } from 'react';
3 |
4 | import { $ } from '@/libs/core';
5 | import useDebounce from '@/libs/useDebounce';
6 |
7 | type SearchInputProps = {
8 | className?: string;
9 | placeholder: string;
10 | };
11 |
12 | export default function SearchInput({
13 | className,
14 | placeholder,
15 | }: SearchInputProps) {
16 | const router = useRouter();
17 | const isSearchPage = router.pathname === '/search';
18 |
19 | const [inputText, setInputText] = useState('');
20 | const debouncedText = useDebounce(inputText);
21 |
22 | const handleSearchQuery = () => {
23 | router.push({
24 | pathname: '/search',
25 | query: { q: debouncedText },
26 | });
27 | };
28 |
29 | const handleChange = (e: ChangeEvent) => {
30 | if (isSearchPage) {
31 | const value = e.target.value;
32 | setInputText(value);
33 | }
34 | };
35 |
36 | const handleClick = () => {
37 | if (!isSearchPage) {
38 | handleSearchQuery();
39 | }
40 | };
41 |
42 | const handleSubmit = (e: FormEvent) => {
43 | e.preventDefault();
44 | };
45 |
46 | useEffect(() => {
47 | if (isSearchPage) {
48 | handleSearchQuery();
49 | }
50 | }, [debouncedText]);
51 |
52 | return (
53 |
95 | );
96 | }
97 |
--------------------------------------------------------------------------------
/src/components/common/SectionBorder.tsx:
--------------------------------------------------------------------------------
1 | import { $ } from '@/libs/core';
2 |
3 | export default function SectionBorder({
4 | className,
5 | ...props
6 | }: React.ComponentProps<'hr'>) {
7 | return (
8 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/common/SiteFooter.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 |
3 | import { siteConfig } from '@/constants/config';
4 |
5 | import AuthorContacts from '../AuthorContacts';
6 | import LinkExternal from './LinkExternal';
7 |
8 | export default function SiteFooter() {
9 | const since = useMemo(() => {
10 | const year = new Date().getFullYear();
11 | if (siteConfig.since === year) {
12 | return year;
13 | }
14 | return `${siteConfig.since}-${year}`;
15 | }, []);
16 |
17 | return (
18 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/common/SubTitle.tsx:
--------------------------------------------------------------------------------
1 | import { $ } from '@/libs/core';
2 |
3 | export default function SubTitle({
4 | className,
5 | ...props
6 | }: React.ComponentProps<'h2'>) {
7 | return (
8 |
12 | {props.children}
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/common/Tag.tsx:
--------------------------------------------------------------------------------
1 | import title from 'title';
2 |
3 | import Pill from './Pill';
4 |
5 | export default function Tag({ tag }: { tag: string }) {
6 | return {title(tag)};
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/common/Title.tsx:
--------------------------------------------------------------------------------
1 | import { $ } from '@/libs/core';
2 |
3 | export default function Title({
4 | className,
5 | ...props
6 | }: React.ComponentProps<'h1'>) {
7 | return (
8 |
15 | {props.children}
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/common/index.ts:
--------------------------------------------------------------------------------
1 | export { default as AnimatedContainer } from './AnimatedContainer';
2 | export { default as CopyLinkButton } from './CopyLinkButton';
3 | export { default as Fonts } from './Fonts';
4 | export { default as HeaderNavigation } from './HeaderNavigation';
5 | export { default as HoverCard } from './HoverCard';
6 | export { default as IconButton } from './IconButton';
7 | export { default as IconText } from './IconText';
8 | export { default as LinkArrow } from './LinkArrow';
9 | export { default as LinkExternal } from './LinkExternal';
10 | export { default as LinkHover } from './LinkHover';
11 | export { default as LogoIcon } from './LogoIcon';
12 | export { default as NavItem } from './NavItem';
13 | export { default as Pill } from './Pill';
14 | export { default as PlainText } from './PlainText';
15 | export { default as PressedEffect } from './PressedEffect';
16 | export { default as SearchInput } from './SearchInput';
17 | export { default as SectionBorder } from './SectionBorder';
18 | export { default as SiteFooter } from './SiteFooter';
19 | export { default as SubTitle } from './SubTitle';
20 | export { default as Tag } from './Tag';
21 | export { default as Title } from './Title';
22 |
--------------------------------------------------------------------------------
/src/components/contents/ContentsTable.tsx:
--------------------------------------------------------------------------------
1 | import { SectionBorder } from '@/components/common';
2 | import { TableOfContents } from '@/types/post';
3 |
4 | export default function ContentsTable({
5 | tableOfContents,
6 | className,
7 | onlyTitle = false,
8 | }: {
9 | tableOfContents: TableOfContents;
10 | className?: string;
11 | onlyTitle?: boolean;
12 | }) {
13 | if (tableOfContents.length === 0) {
14 | return <>>;
15 | }
16 |
17 | return (
18 |
19 |
20 | Table of contents
21 |
22 |
23 | {tableOfContents.map((section) => (
24 | -
25 | {section.text}
26 | {!onlyTitle && (
27 |
28 | {section.subSections.map((subSection) => (
29 | -
30 | {subSection.text}
31 |
32 | ))}
33 |
34 | )}
35 |
36 | ))}
37 |
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/contents/Giscus.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import { useTheme } from 'next-themes';
3 | import { useEffect, useRef, useState } from 'react';
4 |
5 | export default function Giscus() {
6 | const ref = useRef(null);
7 | const { resolvedTheme } = useTheme();
8 | const [mounted, setMounted] = useState(false);
9 | const router = useRouter();
10 |
11 | // https://github.com/giscus/giscus/tree/main/styles/themes
12 | const theme = resolvedTheme === 'dark' ? 'dark' : 'light';
13 |
14 | useEffect(() => {
15 | if (!ref.current || ref.current.hasChildNodes()) return;
16 |
17 | const scriptElem = document.createElement('script');
18 | scriptElem.src = 'https://giscus.app/client.js';
19 | scriptElem.async = true;
20 | scriptElem.crossOrigin = 'anonymous';
21 |
22 | scriptElem.setAttribute('data-repo', 'pysoo/Next-blog');
23 | scriptElem.setAttribute('data-repo-id', 'R_kgDOJ0NjBQ');
24 | scriptElem.setAttribute('data-category', 'General');
25 | scriptElem.setAttribute('data-category-id', 'DIC_kwDOJ0NjBc4CXzzE');
26 | scriptElem.setAttribute('data-mapping', 'pathname');
27 | scriptElem.setAttribute('data-strict', '0');
28 | scriptElem.setAttribute('data-reactions-enabled', '1');
29 | scriptElem.setAttribute('data-emit-metadata', '0');
30 | scriptElem.setAttribute('data-input-position', 'bottom');
31 | scriptElem.setAttribute('data-theme', theme);
32 | scriptElem.setAttribute('data-lang', 'en');
33 |
34 | ref.current.appendChild(scriptElem);
35 | setMounted(true);
36 | }, []);
37 |
38 | // https://github.com/giscus/giscus/blob/main/ADVANCED-USAGE.md#isetconfigmessage
39 | useEffect(() => {
40 | if (!mounted) return;
41 |
42 | const iframe = document.querySelector(
43 | 'iframe.giscus-frame',
44 | );
45 | iframe?.contentWindow?.postMessage(
46 | { giscus: { setConfig: { theme } } },
47 | 'https://giscus.app',
48 | );
49 | }, [theme, mounted]);
50 |
51 | useEffect(() => {
52 | const iframe = document.querySelector(
53 | 'iframe.giscus-frame',
54 | );
55 | iframe?.contentWindow?.postMessage(
56 | { giscus: { setConfig: { term: router.asPath } } },
57 | 'https://giscus.app',
58 | );
59 | }, [router.asPath]);
60 |
61 | return ;
62 | }
63 |
--------------------------------------------------------------------------------
/src/components/contents/ReadingProgressBar.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export default function ReadingProgressBar() {
4 | const [width, setWidth] = useState(0);
5 |
6 | const scrollHeight = () => {
7 | const element = document.documentElement;
8 | const ScrollTop = element.scrollTop || document.body.scrollTop;
9 | const ScrollHeight = element.scrollHeight || document.body.scrollHeight;
10 | const percent = (ScrollTop / (ScrollHeight - element.clientHeight)) * 100;
11 |
12 | setWidth(percent);
13 | };
14 |
15 | useEffect(() => {
16 | window.addEventListener('scroll', scrollHeight);
17 | return () => window.removeEventListener('scroll', scrollHeight);
18 | }, []);
19 |
20 | return (
21 |
22 |
33 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/contents/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ContentsBanner } from './ContentsBanner';
2 | export { default as ContentsTable } from './ContentsTable';
3 | export { default as Giscus } from './Giscus';
4 | export { default as ReadingProgressBar } from './ReadingProgressBar';
5 |
--------------------------------------------------------------------------------
/src/components/home/FeaturedPosts.tsx:
--------------------------------------------------------------------------------
1 | import { fadeIn, staggerHalf } from '@/constants/animations';
2 | import { allFeaturedPosts } from '@/libs/dataset';
3 |
4 | import { AnimatedContainer, LinkArrow, SubTitle } from '../common';
5 | import { PostPressedCard } from '../post';
6 |
7 | export default function FeaturedPosts() {
8 | return (
9 |
10 |
14 | Featured Posts
15 | 모든 글 보기
16 |
17 |
21 | {allFeaturedPosts.map((post) => (
22 |
29 | ))}
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/home/IntroduceDescription.tsx:
--------------------------------------------------------------------------------
1 | export default function IntroduceDescription() {
2 | return (
3 |
4 |
5 | 반갑습니다 🤗
6 | 어려운 것을 쉽게 설명하고 싶은 프론트엔드 개발자 박수현입니다.
7 |
8 |
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/home/index.ts:
--------------------------------------------------------------------------------
1 | export { default as FeaturedPosts } from './FeaturedPosts';
2 | export { default as IntroduceDescription } from './IntroduceDescription';
3 |
--------------------------------------------------------------------------------
/src/components/icons/CalendarIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function CalendarIcon({
2 | className,
3 | ...props
4 | }: React.ComponentProps<'svg'>) {
5 | return (
6 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/icons/ChatIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function ChatIcon({
2 | className,
3 | ...props
4 | }: React.ComponentProps<'svg'>) {
5 | return (
6 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/icons/CheckIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function CheckIcon({
2 | className,
3 | ...props
4 | }: React.ComponentProps<'svg'>) {
5 | return (
6 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/icons/ClockIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function ClockIcon({
2 | className,
3 | ...props
4 | }: React.ComponentProps<'svg'>) {
5 | return (
6 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/icons/ContactsIcon.tsx:
--------------------------------------------------------------------------------
1 | import GithubIcon from './GithubIcon';
2 | import MailIcon from './MailIcon';
3 | import TagIcon from './TagIcon';
4 | import VelogIcon from './VelogIcon';
5 |
6 | const icons: { [key: string]: React.FunctionComponent } = {
7 | email: MailIcon,
8 | github: GithubIcon,
9 | velog: VelogIcon,
10 | };
11 |
12 | export default function ContactsIcon({
13 | contact,
14 | ...props
15 | }: React.ComponentProps<'svg'> & { contact: string }) {
16 | const Component = icons[contact] ?? TagIcon;
17 |
18 | return ;
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/icons/GithubIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function GithubIcon({
2 | className,
3 | width = 16,
4 | height = 16,
5 | ...props
6 | }: React.ComponentProps<'svg'>) {
7 | return (
8 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/icons/LinkIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function LinkIcon({
2 | className,
3 | ...props
4 | }: React.ComponentProps<'svg'>) {
5 | return (
6 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/icons/ListIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function ListIcon({
2 | className,
3 | ...props
4 | }: React.ComponentProps<'svg'>) {
5 | return (
6 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/icons/MailIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function MailIcon({
2 | className,
3 | width = 16,
4 | height = 16,
5 | ...props
6 | }: React.ComponentProps<'svg'>) {
7 | return (
8 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/icons/TagIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function TagIcon({
2 | className,
3 | width = 16,
4 | height = 16,
5 | ...props
6 | }: React.ComponentProps<'svg'>) {
7 | return (
8 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/icons/UpIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function UpIcon({
2 | className,
3 | ...props
4 | }: React.ComponentProps<'svg'>) {
5 | return (
6 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/icons/VelogIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function VelogIcon({
2 | className,
3 | width = 16,
4 | height = 16,
5 | ...props
6 | }: React.ComponentProps<'svg'>) {
7 | return (
8 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/icons/index.ts:
--------------------------------------------------------------------------------
1 | export { default as CalendarIcon } from './CalendarIcon';
2 | export { default as ChatIcon } from './ChatIcon';
3 | export { default as CheckIcon } from './CheckIcon';
4 | export { default as ClockIcon } from './ClockIcon';
5 | export { default as ContactsIcon } from './ContactsIcon';
6 | export { default as GithubIcon } from './GithubIcon';
7 | export { default as LinkIcon } from './LinkIcon';
8 | export { default as ListIcon } from './ListIcon';
9 | export { default as MailIcon } from './MailIcon';
10 | export { default as TagIcon } from './TagIcon';
11 | export { default as UpIcon } from './UpIcon';
12 | export { default as VelogIcon } from './VelogIcon';
13 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as AuthorContacts } from './AuthorContacts';
2 | export * as SEO from './SEO';
3 | export { default as ThemeSwitch } from './ThemeSwitch';
4 |
--------------------------------------------------------------------------------
/src/components/mdx/CodeBlock.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from 'react';
2 | import { toast } from 'react-hot-toast';
3 |
4 | import { $ } from '@/libs/core';
5 | import useWatchTimeout from '@/libs/useWatchTimeout';
6 |
7 | import CheckIcon from '../icons/CheckIcon';
8 |
9 | export default function CodeBlock({
10 | children,
11 | title,
12 | }: React.ComponentProps<'pre'>) {
13 | const ref = useRef(null);
14 | const [copied, setCopied] = useState(false);
15 |
16 | useWatchTimeout(copied, 1500, () => {
17 | setCopied(false);
18 | });
19 |
20 | const handleCopy = async () => {
21 | const text = ref.current?.querySelector('code')?.innerText;
22 | if (!text) return;
23 |
24 | try {
25 | await navigator.clipboard.writeText(text);
26 | setCopied(true);
27 | toast('코드를 복사했습니다.', { icon: '⌨️' });
28 | } catch (e) {
29 | console.error(e);
30 | toast.error('코드 복사에 실패했습니다.');
31 | }
32 | };
33 |
34 | return (
35 |
36 | {title && (
37 |
38 |
39 | {title}
40 |
41 |
42 |
43 | )}
44 |
45 | {children}
46 |
47 |
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/mdx/ZoomImage.tsx:
--------------------------------------------------------------------------------
1 | import mediumZoom, { Zoom } from 'medium-zoom';
2 | import { useTheme } from 'next-themes';
3 | import { useEffect, useRef, useState } from 'react';
4 |
5 | export default function ZoomImage({
6 | src,
7 | alt,
8 | ...props
9 | }: React.ComponentProps<'img'>) {
10 | const ref = useRef(null);
11 | const [zoom, setZoom] = useState();
12 |
13 | const { resolvedTheme } = useTheme();
14 | const background = resolvedTheme === 'dark' ? '#171717' : '#FAFAFA';
15 |
16 | useEffect(() => {
17 | if (!ref.current || ref.current.classList.contains('medium-zoom-image'))
18 | return;
19 |
20 | setZoom(mediumZoom(ref.current, { background }));
21 | }, []);
22 |
23 | useEffect(() => {
24 | zoom?.update({ background });
25 | }, [background]);
26 |
27 | return
;
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/mdx/index.ts:
--------------------------------------------------------------------------------
1 | export { default as CodeBlock } from './CodeBlock';
2 | export { default as ZoomImage } from './ZoomImage';
3 |
--------------------------------------------------------------------------------
/src/components/post/PostContent.tsx:
--------------------------------------------------------------------------------
1 | import { useMDXComponent } from 'next-contentlayer/hooks';
2 |
3 | import { fadeInHalf } from '@/constants/animations';
4 | import { MAX_TABLE_CONTENTS_LENGTH } from '@/constants/contents';
5 | import { Post, TableOfContents } from '@/types/post';
6 |
7 | import { AnimatedContainer } from '../common';
8 | import { ContentsBanner, ContentsTable } from '../contents';
9 | import { CodeBlock, ZoomImage } from '../mdx';
10 |
11 | type PostContentProps = {
12 | post: Post;
13 | tableOfContents: TableOfContents;
14 | };
15 |
16 | const mdxComponents = {
17 | img: ZoomImage,
18 | pre: CodeBlock,
19 | };
20 |
21 | export default function PostContent({
22 | post,
23 | tableOfContents,
24 | }: PostContentProps) {
25 | const MDXContent = useMDXComponent(post.body?.code ?? '');
26 |
27 | return (
28 |
29 |
30 | MAX_TABLE_CONTENTS_LENGTH}
34 | />
35 |
36 |
37 |
38 |
39 | MAX_TABLE_CONTENTS_LENGTH}
42 | />
43 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/post/PostFooter.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AnimatedContainer,
3 | LinkExternal,
4 | SectionBorder,
5 | Tag,
6 | } from '@/components/common';
7 | import { fadeInHalf } from '@/constants/animations';
8 | import { siteConfig } from '@/constants/config';
9 | import { Post } from '@/types/post';
10 |
11 | import AuthorContacts from '../AuthorContacts';
12 | import { Giscus } from '../contents';
13 | import PostNavigation, { PostNavigationProps } from './PostNavigation';
14 |
15 | type PostFooterType = {
16 | post: Post;
17 | postNavigation: PostNavigationProps;
18 | };
19 |
20 | export default function PostFooter({ post, postNavigation }: PostFooterType) {
21 | return (
22 |
23 |
24 | {post.tags.map((tag) => (
25 |
26 | ))}
27 |
28 |
29 |
30 |
31 |
34 |
40 |
41 |
42 |
{siteConfig.author.name}
43 |
{siteConfig.author.bio}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/post/PostHeader.tsx:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs';
2 | import Link from 'next/link';
3 |
4 | import { fadeInHalf } from '@/constants/animations';
5 | import { Post, Series } from '@/types/post';
6 |
7 | import { AnimatedContainer, IconText, SectionBorder, Title } from '../common';
8 | import { CalendarIcon, ChatIcon, ClockIcon } from '../icons';
9 |
10 | type PostHeaderProps = {
11 | post: Post;
12 | series?: Series | null;
13 | };
14 |
15 | export default function PostHeader({ post, series }: PostHeaderProps) {
16 | const headerTagTitle = series?.title ?? post.snippetName;
17 | const headerTagSlug =
18 | series?.slug ?? `/snippets?key=${post.snippetName ?? 'all'}`;
19 |
20 | return (
21 |
22 | {post.title}
23 | {headerTagTitle && (
24 |
25 | {post.snippetName && snippet: }
26 |
27 |
28 | {headerTagTitle}
29 |
30 |
31 |
32 | )}
33 |
34 |
35 |
39 |
40 | document.querySelector('.giscus')?.scrollIntoView()}
44 | text="댓글"
45 | />
46 |
47 |
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/post/PostItem.tsx:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs';
2 | import Image from 'next/image';
3 | import Link from 'next/link';
4 | import React from 'react';
5 |
6 | import { defaultCoverImage } from '@/constants/image';
7 | import { $ } from '@/libs/core';
8 | import { ReducedPost } from '@/types/post';
9 |
10 | import { IconText, Tag } from '../common';
11 | import { CalendarIcon, ClockIcon } from '../icons';
12 |
13 | export default function PostItem({ post }: { post: ReducedPost }) {
14 | const href = !!post.snippetName ? `/snippets/[...slug]` : `/blog/[...slug]`;
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
29 |
30 |
31 | {post.title}
32 |
33 |
34 |
{post.description}
35 |
36 |
37 |
38 | {post.tags.map((tag, i) => (
39 |
40 | ))}
41 |
42 |
43 |
44 |
48 |
49 |
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/post/PostList.tsx:
--------------------------------------------------------------------------------
1 | import { fadeIn, fadeInUp } from '@/constants/animations';
2 | import { Post } from '@/types/post';
3 |
4 | import { AnimatedContainer } from '../common';
5 | import PostItem from './PostItem';
6 |
7 | export default function PostList({ postList }: { postList: Post[] }) {
8 | return (
9 | <>
10 | {postList.map((post) => (
11 |
12 |
19 |
20 |
21 |
22 | ))}
23 | >
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/post/PostNavigation.tsx:
--------------------------------------------------------------------------------
1 | import { Post } from '@/types/post';
2 |
3 | import LinkHover from '../common/LinkHover';
4 |
5 | export type PostNavigationProps = {
6 | prevPost: Pick | null;
7 | nextPost: Pick | null;
8 | };
9 |
10 | export default function PostNavigation({
11 | prevPost,
12 | nextPost,
13 | }: PostNavigationProps) {
14 | return (
15 |
16 | {prevPost && (
17 |
21 |
34 | {prevPost.title}
35 |
36 | )}
37 | {nextPost && (
38 |
42 | {nextPost.title}
43 |
56 |
57 | )}
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/post/PostPressedCard.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion';
2 | import Image from 'next/image';
3 |
4 | import { fadeIn, fadeInUp } from '@/constants/animations';
5 | import { PostPressedCardType } from '@/types/post';
6 |
7 | import { AnimatedContainer, IconText, PressedEffect } from '../common';
8 | import { CalendarIcon } from '../icons';
9 |
10 | export default function PostPressedCard({
11 | href,
12 | imgUrl,
13 | title,
14 | date,
15 | }: PostPressedCardType) {
16 | return (
17 |
18 |
19 |
20 |
28 |
29 |
38 |
39 |
40 |
41 |
42 |
43 | {title}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/post/index.ts:
--------------------------------------------------------------------------------
1 | export { default as PostContent } from './PostContent';
2 | export { default as PostFooter } from './PostFooter';
3 | export { default as PostHeader } from './PostHeader';
4 | export { default as PostItem } from './PostItem';
5 | export { default as PostNavigation } from './PostNavigation';
6 | export { default as PostPressedCard } from './PostPressedCard';
7 |
--------------------------------------------------------------------------------
/src/components/series/SeriesCard.tsx:
--------------------------------------------------------------------------------
1 | import { Post } from 'contentlayer/generated';
2 | import Link from 'next/link';
3 | import { useState } from 'react';
4 |
5 | import { $ } from '@/libs/core';
6 | import { Series } from '@/types/post';
7 |
8 | import { IconText } from '../common';
9 | import { ListIcon } from '../icons';
10 |
11 | export default function SeriesCard({
12 | currentPost,
13 | series,
14 | }: {
15 | currentPost: Post;
16 | series: Series;
17 | }) {
18 | const [open, setOpen] = useState(true);
19 |
20 | const currentPostIndex = series.posts.findIndex(
21 | (post) => post.slug === currentPost.slug,
22 | );
23 |
24 | const onClickCard = () => {
25 | !open && setOpen(!open);
26 | };
27 |
28 | return (
29 |
36 |
37 |
{series.title}
38 |
42 |
43 |
44 |
{series.description}
45 |
46 | {open && (
47 |
48 | {series.posts.map((post, i) => (
49 |
59 | {i + 1}. {post.title}
60 |
61 | ))}
62 | setOpen(false)}
65 | >
66 | 간략히
67 |
68 |
69 | )}
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/snippet/SnippetItem.tsx:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs';
2 | import Link from 'next/link';
3 |
4 | import { $ } from '@/libs/core';
5 | import { ReducedPost } from '@/types/post';
6 |
7 | import { IconText } from '../common';
8 | import { CalendarIcon, ClockIcon } from '../icons';
9 |
10 | export default function SnippetItem({ post }: { post: ReducedPost }) {
11 | return (
12 |
13 |
19 |
20 | {post.title}
21 |
22 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/snippet/SnippetList.tsx:
--------------------------------------------------------------------------------
1 | import { AnimatePresence } from 'framer-motion';
2 |
3 | import { fadeInHalf, staggerImmediate } from '@/constants/animations';
4 | import { Snippet } from '@/types/post';
5 |
6 | import { AnimatedContainer } from '../common';
7 | import SnippetItem from './SnippetItem';
8 |
9 | type SnippetListProps = {
10 | snippetList: Snippet[];
11 | };
12 |
13 | export default function SnippetList({ snippetList }: SnippetListProps) {
14 | return (
15 |
16 | {snippetList.map(({ key, postList }) => {
17 | return (
18 |
24 |
25 | {postList.map((post) => (
26 |
31 |
32 |
33 | ))}
34 |
35 |
36 | );
37 | })}
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/snippet/index.ts:
--------------------------------------------------------------------------------
1 | export { default as SnippetItem } from './SnippetItem';
2 | export { default as SnippetList } from './SnippetList';
3 |
--------------------------------------------------------------------------------
/src/constants/config.ts:
--------------------------------------------------------------------------------
1 | import { DefaultSeoProps } from 'next-seo';
2 | import path from 'path';
3 |
4 | export const BASE_PATH = '/posts';
5 | export const POSTS_PATH = path.join(process.cwd(), BASE_PATH);
6 |
7 | export const siteConfig = {
8 | url: 'https://enjoydev.life',
9 | title: '기억은 기록을 이기지 못한다 ✍️',
10 | description:
11 | '어려운 것을 쉽게 설명하고 싶은 프론트엔드 개발자입니다. 웹 개발 지식과 JavaScript 지식을 쉽게 풀어 쓰는 활동을 하고 있습니다.',
12 | copyright: '물개박수 © All rights reserved.',
13 | since: 2023,
14 | googleAnalyticsId: '',
15 | author: {
16 | name: 'Suhyeon Park',
17 | photo: 'https://avatars.githubusercontent.com/u/55135881?s=288&v=4',
18 | bio: 'Frontend Engineer',
19 | contacts: {
20 | email: 'soopy368@gmail.com',
21 | github: 'pySoo',
22 | velog: '@soopy368',
23 | },
24 | },
25 | menus: [
26 | {
27 | label: 'Home',
28 | path: '/',
29 | },
30 | {
31 | label: 'Blog',
32 | path: '/blog',
33 | },
34 | {
35 | label: 'Snippets',
36 | path: '/snippets',
37 | },
38 | ],
39 | };
40 |
41 | export const seoConfig: DefaultSeoProps = {
42 | title: siteConfig.title,
43 | description: siteConfig.description,
44 | canonical: siteConfig.url,
45 | openGraph: {
46 | type: 'website',
47 | locale: 'ko-KR',
48 | url: siteConfig.url,
49 | siteName: siteConfig.title,
50 | },
51 | additionalMetaTags: [
52 | {
53 | name: 'author',
54 | content: siteConfig.author.name,
55 | },
56 | ],
57 | };
58 |
--------------------------------------------------------------------------------
/src/constants/contents.ts:
--------------------------------------------------------------------------------
1 | export const MAX_TABLE_CONTENTS_LENGTH = 3;
2 |
--------------------------------------------------------------------------------
/src/constants/fonts.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Inter as FontSans,
3 | JetBrains_Mono as FontMono,
4 | } from '@next/font/google';
5 |
6 | export const fontSans = FontSans({
7 | subsets: ['latin'],
8 | variable: '--font-sans',
9 | display: 'swap',
10 | });
11 |
12 | export const fontMono = FontMono({
13 | subsets: ['latin'],
14 | variable: '--font-mono',
15 | display: 'swap',
16 | });
17 |
--------------------------------------------------------------------------------
/src/constants/image.ts:
--------------------------------------------------------------------------------
1 | export const unsplashImageList = [
2 | '/images/samples/unsplash-1.jpg',
3 | '/images/samples/unsplash-2.jpg',
4 | '/images/samples/unsplash-3.jpg',
5 | '/images/samples/unsplash-4.jpg',
6 | '/images/samples/unsplash-5.jpg',
7 | '/images/samples/unsplash-6.jpg',
8 | '/images/samples/unsplash-7.jpg',
9 | '/images/samples/unsplash-8.jpg',
10 | '/images/samples/unsplash-9.jpg',
11 | '/images/samples/unsplash-10.jpg',
12 | '/images/samples/unsplash-11.jpg',
13 | '/images/samples/unsplash-12.jpg',
14 | '/images/samples/unsplash-13.jpg',
15 | '/images/samples/unsplash-14.jpg',
16 | '/images/samples/unsplash-15.jpg',
17 | '/images/samples/unsplash-16.jpg',
18 | '/images/samples/unsplash-17.jpg',
19 | '/images/samples/unsplash-18.jpg',
20 | '/images/samples/unsplash-19.jpg',
21 | '/images/samples/unsplash-20.jpg',
22 | ];
23 |
24 | export const getRandomUnsplashImage = () =>
25 | [...unsplashImageList].sort(() => Math.random() - 0.5)[0];
26 |
27 | export const defaultCoverImage = '/images/samples/unsplash-16.jpg';
28 |
--------------------------------------------------------------------------------
/src/layouts/Layout.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import { AnimatedContainer, PlainText, Title } from '@/components/common';
4 | import HeaderNavigation from '@/components/common/HeaderNavigation';
5 | import SiteFooter from '@/components/common/SiteFooter';
6 | import { fadeInHalf, staggerHalf } from '@/constants/animations';
7 |
8 | type LayoutProps = {
9 | title?: string;
10 | description?: string | ReactNode;
11 | children: ReactNode;
12 | };
13 |
14 | export default function Layout({ title, description, children }: LayoutProps) {
15 | return (
16 |
17 |
18 |
19 | {title && {title}}
20 |
21 | {description && (
22 |
23 | {typeof description === 'string' ? (
24 | {description}
25 | ) : (
26 | description
27 | )}
28 |
29 | )}
30 | {children}
31 |
32 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/layouts/PostLayout.tsx:
--------------------------------------------------------------------------------
1 | import { ReadingProgressBar } from '@/components/contents';
2 | import { PostContent, PostFooter, PostHeader } from '@/components/post';
3 | import { PostNavigationProps } from '@/components/post/PostNavigation';
4 | import { BlogSEO } from '@/components/SEO';
5 | import SeriesCard from '@/components/series/SeriesCard';
6 | import { Post, Series, TableOfContents } from '@/types/post';
7 |
8 | import Layout from './Layout';
9 |
10 | export type PostLayoutProps = {
11 | post: Post;
12 | series?: Series | null;
13 | postNavigation: PostNavigationProps;
14 | tableOfContents: TableOfContents;
15 | };
16 |
17 | export default function PostLayout({
18 | post,
19 | series,
20 | postNavigation,
21 | tableOfContents,
22 | }: PostLayoutProps) {
23 | return (
24 |
25 |
26 |
27 |
28 |
29 | {series && }
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/layouts/SearchLayout.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AnimatedContainer,
3 | PlainText,
4 | SubTitle,
5 | Title,
6 | } from '@/components/common';
7 | import PostList from '@/components/post/PostList';
8 | import { PageSEO } from '@/components/SEO';
9 | import { fadeInHalf, staggerHalf } from '@/constants/animations';
10 | import Layout from '@/layouts/Layout';
11 | import { Post } from '@/types/post';
12 |
13 | export default function SearchLayout({
14 | postList,
15 | searchQuery,
16 | }: {
17 | postList: Post[];
18 | searchQuery: string;
19 | }) {
20 | return (
21 |
22 |
27 | Search
28 |
29 |
33 | {searchQuery ? 'Filtered Posts' : 'All Posts'}
34 | ({postList.length})
35 |
36 |
42 | {postList.length == 0 && (
43 |
46 | )}
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/libs/core.ts:
--------------------------------------------------------------------------------
1 | import type { ClassValue } from 'clsx';
2 | import { clsx } from 'clsx';
3 | import { twMerge } from 'tailwind-merge';
4 |
5 | export const $ = (...inputs: ClassValue[]) => {
6 | return twMerge(clsx(inputs));
7 | };
8 |
9 | export const isDev = process.env.NODE_ENV === 'development';
10 |
--------------------------------------------------------------------------------
/src/libs/gtag.ts:
--------------------------------------------------------------------------------
1 | import { isDev } from './core';
2 | import useRouteChange from './useRouteChange';
3 |
4 | export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_ANALYTICS_ID;
5 |
6 | export const pageview = (url: URL) => {
7 | if (GA_TRACKING_ID) {
8 | window.gtag('config', GA_TRACKING_ID, {
9 | page_path: url,
10 | });
11 | }
12 | };
13 |
14 | export const event = (
15 | action: Gtag.EventNames,
16 | { event_category, event_label, value }: Gtag.EventParams,
17 | ) => {
18 | window.gtag('event', action, {
19 | event_category,
20 | event_label,
21 | value,
22 | });
23 | };
24 |
25 | export const useGtag = () => {
26 | useRouteChange((url) => {
27 | if (isDev) return;
28 |
29 | pageview(url);
30 | });
31 | };
32 |
--------------------------------------------------------------------------------
/src/libs/mdx.ts:
--------------------------------------------------------------------------------
1 | import { TableOfContents } from '@/types/post';
2 |
3 | export const parseContents = (source: string) => {
4 | return source
5 | .split('\n')
6 | .filter((line) => line.match(/(^#{1,3})\s/))
7 | .reduce((ac, rawHeading) => {
8 | const nac = [...ac];
9 | const removeMdx = rawHeading
10 | .replace(/^##*\s/, '')
11 | .replace(/[\*,\~]{2,}/g, '')
12 | .replace(/(?<=\])\((.*?)\)/g, '')
13 | .replace(/(? post;
9 |
--------------------------------------------------------------------------------
/src/libs/rehypeCodeWrap.js:
--------------------------------------------------------------------------------
1 | import { visit } from 'unist-util-visit';
2 |
3 | export default function rehypeCodeWrap() {
4 | return (tree) => {
5 | visit(tree, { tagName: 'pre' }, (node, index) => {
6 | if (!node.children || !node.children.length) return;
7 |
8 | // code tag
9 | const { properties } = node.children[0];
10 | if (!properties.className || !properties.className.length) return;
11 |
12 | // parse code title
13 | const [lang, filename] = properties.className[0]
14 | .split(':')
15 | .map((e) => e.trim());
16 | if (filename) {
17 | properties.className = lang;
18 | node.properties.title = filename;
19 | }
20 |
21 | tree.children[index] = node;
22 | });
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/src/libs/useDarkMode.ts:
--------------------------------------------------------------------------------
1 | import { useTheme } from 'next-themes';
2 |
3 | export default function useDarkMode() {
4 | const { resolvedTheme, setTheme } = useTheme();
5 |
6 | const isThemeDark = resolvedTheme === 'dark';
7 | const setThemeDark = () => setTheme('dark');
8 | const setThemeLight = () => setTheme('light');
9 |
10 | return {
11 | theme: resolvedTheme,
12 | isThemeDark,
13 | setThemeDark,
14 | setThemeLight,
15 | toggleTheme: () => {
16 | if (isThemeDark) {
17 | setThemeLight();
18 | } else {
19 | setThemeDark();
20 | }
21 | },
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/src/libs/useDebounce.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | const useDebounce = (value: T, delay = 500) => {
4 | const [debouncedValue, setDebouncedValue] = useState(value);
5 |
6 | useEffect(() => {
7 | const timerId = setTimeout(() => {
8 | setDebouncedValue(value);
9 | }, delay);
10 |
11 | return () => {
12 | clearTimeout(timerId);
13 | };
14 | }, [value, delay]);
15 |
16 | return debouncedValue;
17 | };
18 |
19 | export default useDebounce;
20 |
--------------------------------------------------------------------------------
/src/libs/useRouteChange.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import { DependencyList, useEffect } from 'react';
3 |
4 | export default function useRouteChange(
5 | onChange: (url: URL) => void,
6 | deps?: DependencyList,
7 | ) {
8 | const router = useRouter();
9 |
10 | useEffect(() => {
11 | const handleRouteChange = (url: URL) => {
12 | onChange(url);
13 | };
14 |
15 | router.events.on('routeChangeComplete', handleRouteChange);
16 | router.events.on('hashChangeComplete', handleRouteChange);
17 | return () => {
18 | router.events.off('routeChangeComplete', handleRouteChange);
19 | router.events.off('hashChangeComplete', handleRouteChange);
20 | };
21 | }, [router.events, deps]);
22 | }
23 |
--------------------------------------------------------------------------------
/src/libs/useWatchTimeout.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | export default function useWatchTimeout(
4 | watch: unknown,
5 | ms: number,
6 | callback: () => void,
7 | ) {
8 | useEffect(() => {
9 | let timeOut: NodeJS.Timeout;
10 |
11 | if (watch) {
12 | timeOut = setTimeout(callback, ms);
13 | }
14 |
15 | return () => {
16 | timeOut && clearInterval(timeOut);
17 | };
18 | }, [watch]);
19 | }
20 |
--------------------------------------------------------------------------------
/src/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import { NavItem, Title } from '@/components/common';
2 | import { PageSEO } from '@/components/SEO';
3 | import Layout from '@/layouts/Layout';
4 |
5 | export default function NotFound() {
6 | return (
7 |
8 |
9 |
10 |
404
11 |
찾을 수 없는 페이지입니다. 🥲
12 |
13 |
17 | 블로그로 돌아가기
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import '@/styles/globals.css';
2 |
3 | import { AppProps } from 'next/app';
4 | import Head from 'next/head';
5 | import Script from 'next/script';
6 | import { DefaultSeo } from 'next-seo';
7 | import { ThemeProvider } from 'next-themes';
8 |
9 | import Fonts from '@/components/common/Fonts';
10 | import { seoConfig } from '@/constants/config';
11 | import { $, isDev } from '@/libs/core';
12 | import * as gtag from '@/libs/gtag';
13 |
14 | export default function App({ Component, pageProps }: AppProps) {
15 | gtag.useGtag();
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {!isDev && (
28 | <>
29 | {/* Global Site Tag (gtag.js) - Google Analytics */}
30 |
34 |
48 | >
49 | )}
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Head, Html, Main, NextScript } from 'next/document';
2 |
3 | import { isDev } from '@/libs/core';
4 |
5 | export default function Document() {
6 | return (
7 |
8 |
9 |
10 |
15 |
16 |
22 |
28 |
33 |
38 |
44 |
45 |
46 | {!isDev && (
47 |
56 | )}
57 |
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/src/pages/blog/[...slugs].tsx:
--------------------------------------------------------------------------------
1 | import { GetStaticPaths, GetStaticProps } from 'next';
2 |
3 | import { PostNavigationProps } from '@/components/post/PostNavigation';
4 | import PostLayout, { PostLayoutProps } from '@/layouts/PostLayout';
5 | import { allBlogPosts, allSeries } from '@/libs/dataset';
6 | import { parseContents } from '@/libs/mdx';
7 | import { Series } from '@/types/post';
8 |
9 | export const getStaticPaths: GetStaticPaths = async () => {
10 | return {
11 | paths: allBlogPosts.map((post) => post.slug),
12 | fallback: 'blocking',
13 | };
14 | };
15 |
16 | export const getStaticProps: GetStaticProps = async ({ params }) => {
17 | const { slugs } = params as { slugs: string[] };
18 | const slug = `/blog/${[...slugs].join('/')}`;
19 |
20 | const post = allBlogPosts.find((v) => v.slug === slug);
21 | const postIndex = allBlogPosts.findIndex((v) => v.slug === slug);
22 |
23 | if (!post || postIndex < 0) {
24 | return {
25 | notFound: true,
26 | };
27 | }
28 |
29 | const postNavigation: PostNavigationProps = {
30 | prevPost: allBlogPosts.at(postIndex + 1) ?? null,
31 | nextPost: postIndex === 0 ? null : allBlogPosts.at(postIndex - 1) ?? null,
32 | };
33 |
34 | let series: Series | null = null;
35 |
36 | if (post.seriesName) {
37 | series =
38 | allSeries.find((series) =>
39 | series.slug.startsWith(`/blog/${post.seriesName}`),
40 | ) ?? null;
41 | }
42 |
43 | const props: PostLayoutProps = {
44 | post,
45 | series,
46 | postNavigation,
47 | tableOfContents: parseContents(post.body.raw),
48 | };
49 |
50 | return {
51 | props,
52 | };
53 | };
54 |
55 | export default function PostPage(props: PostLayoutProps) {
56 | return ;
57 | }
58 |
--------------------------------------------------------------------------------
/src/pages/blog/[slug].tsx:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs';
2 | import { GetStaticPaths, GetStaticProps } from 'next';
3 |
4 | import { AnimatedContainer, HoverCard, IconText } from '@/components/common';
5 | import { CalendarIcon, ListIcon } from '@/components/icons';
6 | import PostItem from '@/components/post/PostItem';
7 | import { PageSEO } from '@/components/SEO';
8 | import {
9 | fadeIn,
10 | fadeInSlideToLeft,
11 | fadeInUp,
12 | staggerOne,
13 | staggerTwo,
14 | } from '@/constants/animations';
15 | import Layout from '@/layouts/Layout';
16 | import { allSeries, allSeriesName } from '@/libs/dataset';
17 | import { Series } from '@/types/post';
18 |
19 | export const getStaticPaths: GetStaticPaths = () => {
20 | return {
21 | paths: allSeriesName.map((seriesName) => `/blog/${seriesName}`),
22 | fallback: 'blocking',
23 | };
24 | };
25 |
26 | export const getStaticProps: GetStaticProps = ({ params }) => {
27 | const { slug } = params as { slug: string };
28 |
29 | const series = allSeries.find((v) => v.seriesName === slug);
30 |
31 | if (!series) {
32 | return {
33 | notFound: true,
34 | };
35 | }
36 |
37 | return {
38 | props: {
39 | series,
40 | },
41 | };
42 | };
43 |
44 | export default function PostPage({ series }: { series: Series }) {
45 | return (
46 |
47 |
52 |
53 |
54 |
55 |
56 |
60 |
61 |
62 |
63 |
64 | {series.title}
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
76 | {series.description}
77 |
78 |
82 |
83 |
84 |
85 |
86 |
91 | {series.posts.map((post, i) => (
92 |
93 |
97 |
98 | ))}
99 |
100 |
101 |
102 |
103 |
104 | );
105 | }
106 |
--------------------------------------------------------------------------------
/src/pages/blog/index.tsx:
--------------------------------------------------------------------------------
1 | import { AnimatePresence } from 'framer-motion';
2 | import Link from 'next/link';
3 |
4 | import { AnimatedContainer, HoverCard, SubTitle } from '@/components/common';
5 | import PostList from '@/components/post/PostList';
6 | import { PageSEO } from '@/components/SEO';
7 | import {
8 | fadeInHalf,
9 | fadeInSlideToLeft,
10 | staggerOne,
11 | } from '@/constants/animations';
12 | import Layout from '@/layouts/Layout';
13 | import { allBlogPosts, allSeries } from '@/libs/dataset';
14 | import { Post, Series } from '@/types/post';
15 |
16 | export const getStaticProps = () => {
17 | return {
18 | props: {
19 | postList: allBlogPosts,
20 | seriesList: allSeries,
21 | },
22 | };
23 | };
24 |
25 | export default function BlogPage({
26 | postList,
27 | seriesList,
28 | }: {
29 | postList: Post[];
30 | seriesList: Series[];
31 | }) {
32 | return (
33 |
37 |
38 |
39 |
40 | {'All Series'}
41 | ({seriesList.length})
42 |
43 |
47 |
48 | {seriesList.map((series) => (
49 |
50 |
51 |
52 |
53 |
54 |
55 | {series.title}
56 |
57 |
58 |
59 |
60 |
61 | ))}
62 |
63 |
64 |
65 |
69 | {'All Posts'}
70 | ({postList.length})
71 |
72 |
76 |
77 |
78 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { FeaturedPosts, IntroduceDescription } from '@/components/home';
2 | import { PageSEO } from '@/components/SEO';
3 | import { siteConfig } from '@/constants/config';
4 | import Layout from '@/layouts/Layout';
5 |
6 | export default function Home() {
7 | return (
8 | }>
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/pages/search/index.tsx:
--------------------------------------------------------------------------------
1 | import { GetStaticProps } from 'next';
2 | import { useRouter } from 'next/router';
3 | import { useEffect, useState } from 'react';
4 |
5 | import SearchLayout from '@/layouts/SearchLayout';
6 | import { allBlogPosts } from '@/libs/dataset';
7 | import { Post } from '@/types/post';
8 |
9 | export const getStaticProps: GetStaticProps = () => {
10 | return {
11 | props: {
12 | postList: allBlogPosts,
13 | },
14 | };
15 | };
16 |
17 | export default function SearchPage({ postList }: { postList: Post[] }) {
18 | const router = useRouter();
19 | const searchQuery = (router.query?.q as string) || '';
20 | const [filteredPosts, setFilteredPosts] = useState([]);
21 |
22 | const isDiffObjectList = (list1: Post[], list2: Post[]) => {
23 | if (list1.length !== list2.length) return true;
24 |
25 | const sameList = list1.filter((item1) =>
26 | list2.filter((item2) => item2.slug === item1.slug),
27 | );
28 |
29 | if (sameList.length === list1.length) return false;
30 | return true;
31 | };
32 |
33 | useEffect(() => {
34 | const filteredList = postList.filter((post) =>
35 | post.title.toLowerCase().includes(searchQuery.toLowerCase()),
36 | );
37 | if (isDiffObjectList(filteredPosts, filteredList)) {
38 | setFilteredPosts(filteredList);
39 | }
40 | }, [searchQuery]);
41 |
42 | return ;
43 | }
44 |
--------------------------------------------------------------------------------
/src/pages/snippets/[...slugs].tsx:
--------------------------------------------------------------------------------
1 | import { GetStaticPaths, GetStaticProps } from 'next';
2 |
3 | import { PostNavigationProps } from '@/components/post/PostNavigation';
4 | import PostLayout, { PostLayoutProps } from '@/layouts/PostLayout';
5 | import { allSnippets } from '@/libs/dataset';
6 | import { parseContents } from '@/libs/mdx';
7 |
8 | export const getStaticPaths: GetStaticPaths = () => {
9 | return {
10 | paths: allSnippets
11 | .filter((post) => post.snippetName)
12 | .map((post) => post.slug),
13 | fallback: 'blocking',
14 | };
15 | };
16 |
17 | export const getStaticProps: GetStaticProps = async ({ params }) => {
18 | const { slugs } = params as { slugs: string[] };
19 | const slug = `/snippets/${[...slugs].join('/')}`;
20 |
21 | const post = allSnippets.find((v) => v.slug === slug);
22 | const postIndex = allSnippets.findIndex((v) => v.slug === slug);
23 |
24 | if (!post || postIndex === -1) {
25 | return {
26 | notFound: true,
27 | };
28 | }
29 |
30 | const postNavigation: PostNavigationProps = {
31 | prevPost: postIndex > 0 ? allSnippets.at(postIndex - 1) ?? null : null,
32 | nextPost: allSnippets.at(postIndex + 1) ?? null,
33 | };
34 |
35 | const tableOfContents = parseContents(post.body.raw);
36 |
37 | const props: PostLayoutProps = {
38 | post,
39 | postNavigation,
40 | tableOfContents,
41 | };
42 |
43 | return { props };
44 | };
45 |
46 | export default function PostPage({ ...postLayoutProps }: PostLayoutProps) {
47 | return ;
48 | }
49 |
--------------------------------------------------------------------------------
/src/pages/snippets/index.tsx:
--------------------------------------------------------------------------------
1 | import { GetStaticProps } from 'next';
2 | import Link from 'next/link';
3 | import { useRouter } from 'next/router';
4 | import title from 'title';
5 |
6 | import { AnimatedContainer, Pill } from '@/components/common';
7 | import { PageSEO } from '@/components/SEO';
8 | import { SnippetList } from '@/components/snippet';
9 | import {
10 | fadeInHalf,
11 | staggerHalf,
12 | staggerImmediate,
13 | } from '@/constants/animations';
14 | import Layout from '@/layouts/Layout';
15 | import { reducedAllSnippets } from '@/libs/dataset';
16 | import { ReducedPost, Snippet } from '@/types/post';
17 |
18 | export const getStaticProps: GetStaticProps = () => {
19 | const tagSnippets = reducedAllSnippets.reduce<{
20 | [key: string]: ReducedPost[];
21 | }>((ac, snippet) => {
22 | if (!snippet.snippetName) {
23 | return ac;
24 | }
25 |
26 | if (!ac[snippet.snippetName]) {
27 | ac[snippet.snippetName] = [];
28 | }
29 |
30 | ac[snippet.snippetName].push(snippet);
31 | return ac;
32 | }, {});
33 |
34 | const snippetList = Object.keys(tagSnippets)
35 | .map((key) => ({
36 | key,
37 | postList: tagSnippets[key],
38 | }))
39 | .sort((a, b) => b.postList.length - a.postList.length);
40 |
41 | return {
42 | props: { snippetList },
43 | };
44 | };
45 |
46 | export default function SnippetPage({
47 | snippetList,
48 | }: {
49 | snippetList: Snippet[];
50 | }) {
51 | const router = useRouter();
52 | const selectedKey = router.query?.key;
53 |
54 | const isAll = !selectedKey || selectedKey === 'all';
55 | const allSnippetsCnt = snippetList.reduce(
56 | (ac, snippet) => ac + snippet.postList.length,
57 | 0,
58 | );
59 |
60 | const filteredSnippetList = snippetList.filter((snippet) => {
61 | if (isAll) return true;
62 | return snippet.key === selectedKey;
63 | });
64 |
65 | return (
66 |
70 |
75 |
76 |
80 |
81 |
82 |
86 | All {allSnippetsCnt}
87 |
88 |
89 |
90 | {snippetList.map(({ key, postList }) => (
91 |
92 |
93 |
97 | {title(key)}
98 | {postList.length}
99 |
100 |
101 |
102 | ))}
103 |
104 |
105 |
106 |
107 | );
108 | }
109 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @import 'intellij-prism.css';
6 | @import 'prose.css';
7 |
8 | @layer base {
9 | html {
10 | @apply scroll-smooth;
11 | }
12 | }
13 |
14 | html,
15 | body,
16 | #__next {
17 | min-width: 100%;
18 | min-height: 100%;
19 | }
20 |
21 | * {
22 | /* MW touch highlight 제거 */
23 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
24 | }
25 |
26 | ::selection {
27 | @apply bg-neutral-400/30;
28 | }
29 |
30 | .dark ::selection {
31 | @apply bg-neutral-500/30;
32 | }
33 |
--------------------------------------------------------------------------------
/src/styles/prose.css:
--------------------------------------------------------------------------------
1 | /* common markdown */
2 | .prose {
3 | @apply text-secondary;
4 | }
5 | .prose strong,
6 | .prose h1,
7 | .prose h2,
8 | .prose h3,
9 | .prose h4,
10 | .prose thead th {
11 | @apply text-primary break-words;
12 | }
13 |
14 | .prose del {
15 | @apply text-mute;
16 | }
17 |
18 | .prose p {
19 | @apply leading-8;
20 | }
21 |
22 | .prose img {
23 | @apply my-0 mx-auto;
24 | }
25 | .prose img + span {
26 | display: block;
27 | margin-top: 0.5rem;
28 | }
29 |
30 | .prose a {
31 | @apply text-primary break-words transition-all;
32 | @apply decoration-neutral-450 underline-offset-2 hover:decoration-neutral-350;
33 | }
34 | .prose a:has(strong) {
35 | @apply decoration-yellow-300 decoration-wavy underline-offset-1 hover:decoration-yellow-400;
36 | @apply dark:decoration-yellow-300/70 dark:hover:decoration-yellow-300/90;
37 | }
38 |
39 | .prose code {
40 | font-family: var(--font-mono), Consolas, Monaco, 'Andale Mono', monospace;
41 | }
42 |
43 | .prose code:not(:where(pre *)) {
44 | @apply text-primary rounded-lg px-2 py-0.5 content-none;
45 | @apply bg-neutral-200 dark:bg-neutral-750;
46 | }
47 |
48 | .prose pre::-webkit-scrollbar {
49 | display: none;
50 | }
51 | .prose pre {
52 | -ms-overflow-style: none; /* IE and Edge */
53 | scrollbar-width: none; /* Firefox */
54 | }
55 |
56 | /* prose first child, not */
57 | .prose > :first-child {
58 | margin-top: 3rem !important;
59 | }
60 | .prose > .lg\:hidden + * {
61 | @apply lg:mt-12;
62 | }
63 |
64 | /* rehype-autolink-headings */
65 | .prose .anchor {
66 | @apply absolute -ml-10 mt-1 flex h-6 w-6 items-center justify-center opacity-0 transition-all;
67 | @apply rounded-md border-none no-underline ring-1 ring-neutral-700/10;
68 | @apply hover:shadow hover:ring-neutral-700/30 dark:bg-neutral-700 dark:text-neutral-400;
69 | }
70 |
71 | .prose .anchor:after {
72 | content: ' ';
73 | mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' aria-hidden='true'%3E%3Cpath d='M3.75 1v10M8.25 1v10M1 3.75h10M1 8.25h10' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' /%3E%3C/svg%3E");
74 | mask-repeat: no-repeat;
75 | mask-position: center;
76 |
77 | @apply h-4 w-4 bg-neutral-700 dark:bg-neutral-400;
78 | }
79 |
80 | .prose .anchor:hover,
81 | .prose *:hover > .anchor {
82 | @apply md:opacity-100;
83 | }
84 |
85 | .prose ul,
86 | .prose ol {
87 | padding-left: 1.2rem;
88 | }
89 |
90 | .prose :where(blockquote):not(:where([class~='not-prose'] *)) {
91 | font-style: normal !important;
92 | border-left-color: var(--prism-highlight) !important;
93 | margin: 1.4rem 0 !important;
94 | @apply p-4 bg-neutral-100 dark:bg-neutral-800;
95 | }
96 |
97 | .prose
98 | :where(p, ul, ol):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
99 | margin: 1.1rem 0 !important;
100 | }
101 |
102 | .prose blockquote > :first-child {
103 | margin: 0;
104 | }
105 |
--------------------------------------------------------------------------------
/src/types/post.ts:
--------------------------------------------------------------------------------
1 | import { Post as TPost } from 'contentlayer/generated';
2 |
3 | export type Optional = Omit &
4 | Partial>;
5 |
6 | export type Post = TPost & {
7 | seriesName?: string | null;
8 | snippetName?: string | null;
9 | };
10 | export type ReducedPost = Omit, '_raw'>, '_id'>;
11 |
12 | export type Series = Post & {
13 | posts: ReducedPost[];
14 | };
15 |
16 | export type TableOfContents = Section[];
17 | export type SubSection = {
18 | slug: string;
19 | text: string;
20 | };
21 | export type Section = SubSection & {
22 | subSections: SubSection[];
23 | };
24 |
25 | export type PostPressedCardType = {
26 | href: string;
27 | imgUrl: string;
28 | title: string;
29 | date: string;
30 | isDraft?: boolean;
31 | };
32 |
33 | export type Snippet = {
34 | key: string;
35 | postList: ReducedPost[];
36 | };
37 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const { fontFamily } = require('tailwindcss/defaultTheme');
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | content: ['./src/**/*.tsx'],
6 | darkMode: 'class',
7 | theme: {
8 | extend: {
9 | colors: {
10 | neutral: {
11 | 50: '#fafafa',
12 | 100: '#f5f5f5',
13 | 150: '#ededed',
14 | 200: '#e5e5e5',
15 | 250: '#dedede',
16 | 300: '#d4d4d4',
17 | 350: '#b5b5b5',
18 | 400: '#a3a3a3',
19 | 450: '#8a8a8a',
20 | 470: '#808080',
21 | 500: '#737373',
22 | 600: '#525252',
23 | 700: '#404040',
24 | 750: '#363636',
25 | 800: '#262626',
26 | 900: '#171717',
27 | },
28 | highlight: {
29 | blue: '#0091ea',
30 | },
31 | },
32 | dropShadow: {
33 | base: '0px 0px 4px rgba(240, 186, 31, 0.75)',
34 | },
35 | fontFamily: {
36 | sans: ['var(--font-sans)', ...fontFamily.sans],
37 | spoqa: ['var(--font-spoqa)', ...fontFamily.sans],
38 | mono: ['var(--font-mono)', ...fontFamily.mono],
39 | },
40 | typography: (theme) => ({
41 | DEFAULT: {
42 | css: {
43 | 'h2,h3,h4': {
44 | 'scroll-margin-top': 'var(--scroll-mt)',
45 | },
46 | 'hr, thead, tbody tr': { borderColor: theme('colors.neutral.300') },
47 | 'blockquote p:first-of-type::before': false,
48 | 'blockquote p:last-of-type::after': false,
49 | 'code::before': false,
50 | 'code::after': false,
51 | },
52 | },
53 | dark: {
54 | css: {
55 | blockquote: {
56 | borderLeftColor: theme('colors.neutral.700'),
57 | color: theme('colors.neutral.300'),
58 | },
59 | 'hr, thead, tbody tr': { borderColor: theme('colors.neutral.700') },
60 | 'ol li::marker, ul li::marker': {
61 | color: theme('colors.neutral.500'),
62 | },
63 | },
64 | },
65 | }),
66 | },
67 | },
68 | variants: {
69 | typography: ['dark'],
70 | },
71 | plugins: [
72 | require('@tailwindcss/typography'),
73 | ({ addComponents, addUtilities }) => {
74 | addComponents({
75 | '.text-primary': {
76 | '@apply text-neutral-900 dark:text-neutral-200': '',
77 | },
78 | '.text-secondary': {
79 | '@apply text-neutral-700 dark:text-neutral-350': '',
80 | },
81 | '.text-tertiary': {
82 | '@apply text-neutral-600 dark:text-neutral-400': '',
83 | },
84 | '.text-mute': {
85 | '@apply text-neutral-500 dark:text-neutral-470': '',
86 | },
87 | '.text-highlight': {
88 | '@apply text-highlight-blue': '',
89 | },
90 | '.bg-primary': {
91 | '@apply bg-neutral-50 dark:bg-neutral-900': '',
92 | },
93 | '.bg-secondary': {
94 | '@apply bg-neutral-150 dark:bg-neutral-800': '',
95 | },
96 | '.bg-tertiary': {
97 | '@apply bg-neutral-200 dark:bg-neutral-750': '',
98 | },
99 | '.bg-mute': {
100 | '@apply bg-neutral-250 dark:bg-neutral-800': '',
101 | },
102 | });
103 | addUtilities(
104 | {
105 | '.no-scrollbar': {
106 | /* IE and Edge */
107 | '-ms-overflow-style': 'none',
108 | /* Firefox */
109 | 'scrollbar-width': 'none',
110 | /* Safari and Chrome */
111 | '&::-webkit-scrollbar': {
112 | display: 'none',
113 | },
114 | },
115 | },
116 | ['responsive'],
117 | );
118 | },
119 | ],
120 | };
121 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "baseUrl": "./",
23 | "paths": {
24 | "@/*": ["src/*"],
25 | "contentlayer/generated": ["./.contentlayer/generated"]
26 | }
27 | },
28 | "include": [
29 | "next-env.d.ts",
30 | "**/*.ts",
31 | "**/*.tsx",
32 | ".next/types/**/*.ts",
33 | ".contentlayer/generated"
34 | ],
35 | "exclude": ["node_modules"]
36 | }
37 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------