├── .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 |
23 | 24 | | Area | Tech Stack | 25 | | :----------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | 26 | | **Frontend** | | 27 | 28 |
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 | ![230724-143333](/posts/2-browser-rendering/230724-143333.png) 50 | 51 | 파싱 중 HTML에 CSS가 포함되어 있다면 파싱을 통해 CSSOM(CSS Object Model) Tree 구성 작업도 함께 진행합니다. 52 | ![230724-143341](/posts/2-browser-rendering/230724-143341.png) 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 | ![230724-143409](/posts/2-browser-rendering/230724-143409.png) 67 | 예를 들어서, `visibility: hidden` 속성은 요소가 공간을 차지하고 보이지만 않기 때문에 렌더 트리에 포함이 됩니다. 68 | 69 | 하지만 `display: none` 의 경우는 **공간을 차지하지 않기 때문에 렌더 트리에서 제외**됩니다. 70 | 71 | ## Layout 72 | 73 | ![230724-145656](/posts/2-browser-rendering/230724-145656.png) 74 | 75 | Layout 단계에서는 렌더 트리를 화면에 배치하기 위해 정확한 위치와 크기를 계산합니다. 76 | 루트부터 노드를 순회하면서 `노드의 위치와 크기를 계산`하고 렌더 트리에 반영합니다. 77 | 78 | ## Paint 79 | 80 | ![230724-145700](/posts/2-browser-rendering/230724-145700.png) 81 | 82 | Paint 단계에서는 Layout 단계에서 계산된 값을 이용하여 각 노드들을 화면 상의 실제 픽셀로 변환합니다. 83 | 이때 픽셀로 변환된 결과는 하나의 레이어가 아니라 여러 개의 레이어로 관리됩니다. 84 | 85 | 브라우저 화면에 `픽셀을 렌더링` 하는 과정이라고 이해하면 좋습니다. 86 | 87 | ## Composite 88 | 89 | Composite 단계에서는 Paint 단계에서 생성된 레이어를 합성하여 `실제 화면에 나타냅니다.` 90 | 이제 우리는 화면에서 웹 페이지를 볼 수 있습니다 ✨ 91 | 92 | --- 93 | 94 | > ## Okay! 95 | 96 | 그래프를 보면서 과정을 정리해봅시다. 97 | 98 | ![230724-145856](/posts/2-browser-rendering/230724-145856.png) 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 | ![230706-180256](/posts/alias/230706-180256.png) 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 | ![230706-180514](/posts/alias/230706-180514.png) 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 | ![230706-182532](/posts/commit/230706-182532.png) 55 | 56 | ### 4. git commit을 입력 57 | 58 | 터미널에서 `git commit`을 입력하면 vscode에서 `.gitmessage.txt` 파일이 열립니다. 59 | ![230706-182303](/posts/commit/230706-182303.png) 60 | 61 | ### 5. 커밋하기 62 | 63 | 필요한 명령어 부분의 `주석을 지우고 내용을 입력`한뒤 txt 파일을 닫으면 (X 버튼 클릭) 커밋이 완료됩니다! 👏 64 | ![230706-182223](/posts/commit/230706-182223.png) 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 | ![230706-183626](/posts/issue-template/230706-183626.png) 25 | 26 | 3. Custom Template을 클릭하고 아래와 같이 내용을 채워줍니다. 27 | 28 | ![230706-183658](/posts/issue-template/230706-183658.png) 29 | ![230706-183643](/posts/issue-template/230706-183643.png) 30 | 31 | 4. Propose changes를 누르고 커밋을 하면 .github 폴더 안에 아래와 같은 파일이 생성됩니다. 32 | 33 | ![230706-183812](/posts/issue-template/230706-183812.png) 34 | 35 | 5. 이슈를 생성해보겠습니다. 방금 만들었던 템플릿을 확인할 수 있습니다. 36 | 37 | ![230706-183823](/posts/issue-template/230706-183823.png) 38 | 39 | 6. 시작 버튼을 누르면 템플릿에 맞춰 내용을 작성할 수 있습니다. 40 | 41 | ![230706-184024](/posts/issue-template/230706-184024.png) 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 | ![230706-183836](/posts/issue-template/230706-183836.png) 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 | ![230706-185053](/posts/husky/230706-185053.png) 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 | ![230706-185355](/posts/husky/230706-185355.png) 116 | 117 | ### 2. push: push할 파일들이 eslint 규칙에 어긋나지 않는지 검사합니다. 118 | 119 | - 강제성이 있기 때문에 규칙에 어긋난다면 push를 할 수 없습니다. 120 | 121 | ```bash 122 | git push origin main 123 | ``` 124 | 125 | ![230706-185406](/posts/husky/230706-185406.png) 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 | 21 | {!mounted ? ( 22 | <> 23 | ) : isThemeDark ? ( 24 | 29 | ) : ( 30 | 31 | )} 32 | 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 | 94 | 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 |
19 |
20 | 21 |

22 | 23 | © {since} {siteConfig.title} 24 | 25 | Powered by 26 | Next.js 27 | , 28 | Vercel 29 |

30 |
31 |
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 | 15 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/icons/ChatIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function ChatIcon({ 2 | className, 3 | ...props 4 | }: React.ComponentProps<'svg'>) { 5 | return ( 6 | 15 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/icons/CheckIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function CheckIcon({ 2 | className, 3 | ...props 4 | }: React.ComponentProps<'svg'>) { 5 | return ( 6 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/icons/ClockIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function ClockIcon({ 2 | className, 3 | ...props 4 | }: React.ComponentProps<'svg'>) { 5 | return ( 6 | 15 | 20 | 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 | 18 | GitHub 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/icons/LinkIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function LinkIcon({ 2 | className, 3 | ...props 4 | }: React.ComponentProps<'svg'>) { 5 | return ( 6 | 15 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/icons/ListIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function ListIcon({ 2 | className, 3 | ...props 4 | }: React.ComponentProps<'svg'>) { 5 | return ( 6 | 13 | 18 | 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 | 18 | Gmail 19 | 20 | 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 | 19 | 24 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/icons/UpIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function UpIcon({ 2 | className, 3 | ...props 4 | }: React.ComponentProps<'svg'>) { 5 | return ( 6 | 15 | 20 | 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 | 18 | Velog 19 | 20 | 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 {alt}; 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 | 25 | 33 | 34 | {prevPost.title} 35 | 36 | )} 37 | {nextPost && ( 38 | 42 | {nextPost.title} 43 | 47 | 55 | 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 | {title} 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 |
23 |
24 | 28 | 29 |
30 |
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}</PlainText> 25 | ) : ( 26 | description 27 | )} 28 | </AnimatedContainer> 29 | )} 30 | {children} 31 | </AnimatedContainer> 32 | </main> 33 | <SiteFooter /> 34 | </div> 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 | <Layout> 25 | <BlogSEO {...post} url={post.slug} summary={post.description} /> 26 | <ReadingProgressBar /> 27 | <PostHeader post={post} series={series} /> 28 | <PostContent post={post} tableOfContents={tableOfContents} /> 29 | {series && <SeriesCard currentPost={post} series={series} />} 30 | <PostFooter post={post} postNavigation={postNavigation} /> 31 | </Layout> 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 | <Layout> 22 | <PageSEO 23 | title="search" 24 | description="포스트 검색 결과입니다." 25 | url="/search" 26 | /> 27 | <Title>Search</Title> 28 | <AnimatedContainer variants={staggerHalf} useTransition> 29 | <AnimatedContainer 30 | variants={fadeInHalf} 31 | className="mt-8 mb-4 flex items-end gap-2" 32 | > 33 | <SubTitle>{searchQuery ? 'Filtered Posts' : 'All Posts'}</SubTitle> 34 | <span className="font-bold">({postList.length})</span> 35 | </AnimatedContainer> 36 | <AnimatedContainer 37 | variants={staggerHalf} 38 | className={`grid w-full gap-8 lg:gap-12 ${ 39 | postList.length !== 0 && 'lg:grid-cols-2' 40 | }`} 41 | > 42 | {postList.length == 0 && ( 43 | <div className="min-h-[300px] flex items-center mx-auto mt-4"> 44 | <PlainText>검색된 내용이 없습니다.</PlainText> 45 | </div> 46 | )} 47 | <PostList postList={postList} /> 48 | </AnimatedContainer> 49 | </AnimatedContainer> 50 | </Layout> 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<TableOfContents>((ac, rawHeading) => { 8 | const nac = [...ac]; 9 | const removeMdx = rawHeading 10 | .replace(/^##*\s/, '') 11 | .replace(/[\*,\~]{2,}/g, '') 12 | .replace(/(?<=\])\((.*?)\)/g, '') 13 | .replace(/(?<!\S)((http)(s?):\/\/|www\.).+?(?=\s)/g, ''); 14 | 15 | const section = { 16 | slug: removeMdx 17 | .trim() 18 | .toLowerCase() 19 | .replace(/[^a-z0-9ㄱ-ㅎ|ㅏ-ㅣ|가-힣 -]/g, '') 20 | .replace(/\s/g, '-'), 21 | text: removeMdx, 22 | }; 23 | 24 | const isSubTitle = rawHeading.split('#').length - 1 === 3; 25 | 26 | if (ac.length && isSubTitle) { 27 | nac.at(-1)?.subSections.push(section); 28 | } else { 29 | nac.push({ ...section, subSections: [] }); 30 | } 31 | 32 | return nac; 33 | }, []); 34 | }; 35 | -------------------------------------------------------------------------------- /src/libs/post.ts: -------------------------------------------------------------------------------- 1 | import { Post, ReducedPost } from '@/types/post'; 2 | 3 | export const reducePost = ({ 4 | body: _, 5 | _raw, 6 | _id, 7 | ...post 8 | }: Post): ReducedPost => 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 = <T>(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 | <Layout> 8 | <PageSEO title="404" /> 9 | <div className="mb-16"> 10 | <Title>404</Title> 11 | <p className="text-secondary mb-8">찾을 수 없는 페이지입니다. 🥲</p> 12 | 13 | <NavItem 14 | href="/blog" 15 | className="rounded-md px-4 py-2 ring-1 ring-neutral-400/70" 16 | > 17 | 블로그로 돌아가기 18 | </NavItem> 19 | </div> 20 | </Layout> 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 | <ThemeProvider attribute="class"> 19 | <Head> 20 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 21 | </Head> 22 | <DefaultSeo {...seoConfig} /> 23 | <Fonts /> 24 | <div className={$('font-sans break-keep')}> 25 | <Component {...pageProps} /> 26 | </div> 27 | {!isDev && ( 28 | <> 29 | {/* Global Site Tag (gtag.js) - Google Analytics */} 30 | <Script 31 | strategy="afterInteractive" 32 | src={`https://www.googletagmanager.com/gtag/js?id=${gtag.GA_TRACKING_ID}`} 33 | /> 34 | <Script 35 | id="gtag-init" 36 | strategy="afterInteractive" 37 | dangerouslySetInnerHTML={{ 38 | __html: ` 39 | window.dataLayer = window.dataLayer || []; 40 | function gtag(){dataLayer.push(arguments);} 41 | gtag('js', new Date()); 42 | gtag('config', '${gtag.GA_TRACKING_ID}', { 43 | page_path: window.location.pathname, 44 | }); 45 | `, 46 | }} 47 | /> 48 | </> 49 | )} 50 | </ThemeProvider> 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 | <Html lang="en"> 8 | <Head /> 9 | <link rel="icon" href="/favicon.ico" /> 10 | <link 11 | rel="apple-touch-icon" 12 | sizes="180x180" 13 | href="/images/favicon/apple-icon-180x180.png" 14 | /> 15 | <link rel="manifest" href="/images/favicon/manifest.json" /> 16 | <link 17 | rel="icon" 18 | type="image/png" 19 | sizes="32x32" 20 | href="/images/favicon/favicon-32x32.png" 21 | /> 22 | <link 23 | rel="icon" 24 | type="image/png" 25 | sizes="16x16" 26 | href="/images/favicon/favicon-16x16.png" 27 | /> 28 | <meta 29 | name="theme-color" 30 | content="#fafafa" 31 | media="(prefers-color-scheme: light)" 32 | /> 33 | <meta 34 | name="theme-color" 35 | content="#131313" 36 | media="(prefers-color-scheme: dark)" 37 | /> 38 | <body 39 | className="text-primary bg-primary" 40 | style={{ 41 | WebkitTouchCallout: 'none', 42 | }} 43 | > 44 | <Main /> 45 | <NextScript /> 46 | {!isDev && ( 47 | <script 48 | dangerouslySetInnerHTML={{ 49 | __html: ` 50 | window.dataLayer = window.dataLayer || []; 51 | function gtag(){dataLayer.push(arguments);} 52 | gtag('js', new Date()); 53 | `, 54 | }} 55 | /> 56 | )} 57 | </body> 58 | </Html> 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 <PostLayout {...props} />; 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 | <Layout> 47 | <PageSEO 48 | title={series.title} 49 | description={series.description} 50 | url={series.slug} 51 | /> 52 | 53 | <AnimatedContainer variants={staggerTwo} useTransition> 54 | <div className="grid gap-8 sm:grid-cols-3 sm:gap-32"> 55 | <div className="sm:sticky sm:top-8 sm:self-start"> 56 | <AnimatedContainer 57 | variants={fadeInSlideToLeft} 58 | className="sm:col-span-1" 59 | > 60 | <HoverCard> 61 | <div className="relative mx-auto h-[336px] w-[240px] select-none rounded-lg bg-neutral-200 px-11 pb-16 pt-12 dark:bg-neutral-800"> 62 | <div className="absolute inset-y-0 left-4 w-[1px] bg-neutral-50 dark:bg-neutral-700" /> 63 | <div className="text-primary flex h-full break-keep bg-neutral-50 px-3 py-4 text-xl font-semibold dark:bg-neutral-700"> 64 | {series.title} 65 | </div> 66 | </div> 67 | </HoverCard> 68 | </AnimatedContainer> 69 | </div> 70 | 71 | <div className="sm:col-span-2"> 72 | <AnimatedContainer 73 | variants={fadeIn} 74 | className="bg-secondary rounded-lg px-5 py-4" 75 | > 76 | <p className="text-primary font-medium">{series.description}</p> 77 | <div className="text-secondary mt-1 flex gap-2"> 78 | <IconText 79 | Icon={CalendarIcon} 80 | text={dayjs(series.date).format('YY.MM.DD')} 81 | /> 82 | <IconText Icon={ListIcon} text={`${series.posts.length}편`} /> 83 | </div> 84 | </AnimatedContainer> 85 | 86 | <AnimatedContainer 87 | variants={staggerOne} 88 | className="mt-16 space-y-4" 89 | useTransition 90 | > 91 | {series.posts.map((post, i) => ( 92 | <AnimatedContainer key={post.slug} variants={fadeInUp}> 93 | <div className="flex space-x-6"> 94 | <div className="pt-4 font-bold">{i + 1}.</div> 95 | <PostItem post={post} /> 96 | </div> 97 | </AnimatedContainer> 98 | ))} 99 | </AnimatedContainer> 100 | </div> 101 | </div> 102 | </AnimatedContainer> 103 | </Layout> 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 | <Layout 34 | title="Blog" 35 | description={`개발에 필요한 지식들을 소소하게 기록하는 공간입니다. \n 시리즈로 연재된 글은 아래의 시리즈 북을 통해 열람할 수 있습니다. 🙌`} 36 | > 37 | <PageSEO title="Blog" description="블로그 설명입니다." url="/blog" /> 38 | <AnimatedContainer variants={fadeInHalf} useTransition> 39 | <div className="mt-10 mb-4 flex items-end gap-2"> 40 | <SubTitle>{'All Series'}</SubTitle> 41 | <span className="font-bold">({seriesList.length})</span> 42 | </div> 43 | <AnimatedContainer 44 | variants={staggerOne} 45 | className="-my-12 -ml-8 -mr-5 flex items-center space-x-8 overflow-scroll py-12 px-8 no-scrollbar" 46 | > 47 | <AnimatePresence mode="wait"> 48 | {seriesList.map((series) => ( 49 | <AnimatedContainer key={series.slug} variants={fadeInSlideToLeft}> 50 | <Link as={series.slug} href={`/blog/[slug]`}> 51 | <HoverCard> 52 | <div className="relative h-56 w-40 select-none rounded-lg bg-neutral-200 px-8 pt-8 pb-12 shadow-lg transition-all hover:scale-[1.01] hover:shadow-xl dark:bg-neutral-800"> 53 | <div className="absolute inset-y-0 left-2.5 w-[1px] bg-neutral-100 dark:bg-neutral-700" /> 54 | <div className="flex h-full break-keep bg-white px-2 py-3 text-sm font-medium dark:bg-neutral-700 dark:text-white"> 55 | {series.title} 56 | </div> 57 | </div> 58 | </HoverCard> 59 | </Link> 60 | </AnimatedContainer> 61 | ))} 62 | </AnimatePresence> 63 | </AnimatedContainer> 64 | </AnimatedContainer> 65 | <AnimatedContainer 66 | variants={fadeInHalf} 67 | className="pt-8 mt-8 mb-4 flex items-end gap-2" 68 | > 69 | <SubTitle>{'All Posts'}</SubTitle> 70 | <span className="font-bold">({postList.length})</span> 71 | </AnimatedContainer> 72 | <AnimatedContainer 73 | variants={fadeInHalf} 74 | className="grid w-full gap-8 lg:grid-cols-2 lg:gap-12" 75 | > 76 | <PostList postList={postList} /> 77 | </AnimatedContainer> 78 | </Layout> 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 | <Layout title={siteConfig.title} description={<IntroduceDescription />}> 9 | <PageSEO url="/" /> 10 | <FeaturedPosts /> 11 | </Layout> 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<Post[]>([]); 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 <SearchLayout postList={filteredPosts} searchQuery={searchQuery} />; 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 <PostLayout {...postLayoutProps} />; 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<Snippet>((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 | <Layout 67 | title="Snippets" 68 | description="개발하면서 실제 유용하게 사용했던 코드 조각 모음입니다." 69 | > 70 | <PageSEO 71 | title="Snippets" 72 | description="Snippets 설명입니다." 73 | url="/snippets" 74 | /> 75 | <AnimatedContainer variants={staggerHalf} useTransition> 76 | <AnimatedContainer 77 | variants={staggerImmediate} 78 | className="bg-primary sticky top-0 z-10 -mx-2 flex items-center gap-2 overflow-scroll bg-opacity-70 px-2 py-4 backdrop-blur transition-all no-scrollbar dark:bg-opacity-70" 79 | > 80 | <AnimatedContainer variants={fadeInHalf}> 81 | <Link href="?key=all"> 82 | <Pill 83 | selected={isAll} 84 | className="cursor-pointer whitespace-nowrap" 85 | > 86 | All <span className="text-xs">{allSnippetsCnt}</span> 87 | </Pill> 88 | </Link> 89 | </AnimatedContainer> 90 | {snippetList.map(({ key, postList }) => ( 91 | <AnimatedContainer key={key} variants={fadeInHalf}> 92 | <Link href={`?key=${key}`}> 93 | <Pill 94 | className="cursor-pointer whitespace-nowrap" 95 | selected={key === selectedKey} 96 | > 97 | {title(key)} 98 | <span className="text-xs ml-1">{postList.length}</span> 99 | </Pill> 100 | </Link> 101 | </AnimatedContainer> 102 | ))} 103 | </AnimatedContainer> 104 | <SnippetList snippetList={filteredSnippetList} /> 105 | </AnimatedContainer> 106 | </Layout> 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 <TocTop /> */ 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<Type, Key extends keyof Type> = Omit<Type, Key> & 4 | Partial<Pick<Type, Key>>; 5 | 6 | export type Post = TPost & { 7 | seriesName?: string | null; 8 | snippetName?: string | null; 9 | }; 10 | export type ReducedPost = Omit<Omit<Omit<Post, 'body'>, '_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 | --------------------------------------------------------------------------------