├── .cursor └── rules │ ├── techincal-writing-overall.mdc │ ├── technical-writing-architecture.mdc │ └── technical-writing-editing.mdc ├── .github ├── DISCUSSION_TEMPLATE │ ├── code-smell.yml │ └── suggest-new-strategy.yml └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .nvmrc ├── .pnp.cjs ├── .pnp.loader.mjs ├── .prettierrc ├── .tossrc.yml ├── .vscode ├── extensions.json └── settings.json ├── .yarn ├── releases │ └── yarn-4.6.0.cjs └── sdks │ ├── integrations.yml │ ├── prettier │ ├── bin │ │ └── prettier.cjs │ ├── index.cjs │ └── package.json │ └── typescript │ ├── bin │ ├── tsc │ └── tsserver │ ├── lib │ ├── tsc.js │ ├── tsserver.js │ ├── tsserverlibrary.js │ └── typescript.js │ └── package.json ├── .yarnrc.yml ├── LICENSE.md ├── README.md ├── api └── github.js ├── fundamentals ├── a11y │ ├── .vitepress │ │ ├── config.mts │ │ ├── shared.mts │ │ └── theme │ │ │ ├── Layout.vue │ │ │ ├── components │ │ │ └── Comments.vue │ │ │ ├── custom.css │ │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useGiscusTheme.tsx │ │ │ └── useLocale.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ └── index.ts │ ├── .vscode │ │ ├── extensions.json │ │ └── settings.json │ ├── get-started.md │ ├── index.md │ └── package.json ├── bundling │ ├── .vitepress │ │ ├── config.mts │ │ ├── config.mts.timestamp-1747125474385-32b0e63a4dd11.mjs │ │ ├── shared.mts │ │ └── theme │ │ │ ├── Layout.vue │ │ │ ├── components │ │ │ └── Comments.vue │ │ │ ├── custom.css │ │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useGiscusTheme.tsx │ │ │ └── useLocale.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ └── index.ts │ ├── .vscode │ │ ├── extensions.json │ │ └── settings.json │ ├── bundler.md │ ├── deep-dive │ │ ├── bundling-process │ │ │ ├── entry.md │ │ │ ├── loader.md │ │ │ ├── output.md │ │ │ ├── overview.md │ │ │ ├── plugin.md │ │ │ └── resolution.md │ │ ├── dev │ │ │ ├── dev-server.md │ │ │ ├── env-variable.md │ │ │ ├── hmr.md │ │ │ ├── overview.md │ │ │ └── source-map.md │ │ ├── optimization │ │ │ ├── bundle-analyzer.md │ │ │ ├── code-splitting.md │ │ │ └── tree-shaking.md │ │ └── overview.md │ ├── files │ │ └── example.zip │ ├── get-started.md │ ├── index.md │ ├── library.md │ ├── overview.md │ ├── package.json │ ├── public │ │ └── images │ │ │ ├── bf-symbol.webp │ │ │ ├── browser-thinking.png │ │ │ ├── build-result.png │ │ │ ├── bundle-analyzer.png │ │ │ ├── bundle-dev-server.png │ │ │ ├── bundle-img.png │ │ │ ├── bundler.png │ │ │ ├── bundling │ │ │ ├── array-entry.png │ │ │ ├── depend-on-before.png │ │ │ ├── depend-on-example.png │ │ │ ├── depend-on.png │ │ │ ├── dependency-graph.png │ │ │ ├── module-resolution.png │ │ │ ├── multiple-entry.png │ │ │ └── single-entry.png │ │ │ ├── code-insert.png │ │ │ ├── emoji-of-the-day.png │ │ │ ├── entry_object-dependon-shared-after.png │ │ │ ├── entry_object-dependon-shared-before.png │ │ │ ├── entry_object-network.png │ │ │ ├── entry_single-network.png │ │ │ ├── esm-dev-server.png │ │ │ ├── favicon.ico │ │ │ ├── hmr-1.png │ │ │ ├── hmr-2.png │ │ │ ├── mode-compare1.png │ │ │ ├── mode-compare2.png │ │ │ ├── network-minified.png │ │ │ ├── project-reset.png │ │ │ ├── react-app.png │ │ │ ├── resolve_compile-error.png │ │ │ ├── source-map-1.png │ │ │ ├── source-map-2.png │ │ │ ├── source-map-3.png │ │ │ └── style-less.png │ ├── rollup-tutorial │ │ └── intro.md │ ├── setting-template.md │ ├── tsconfig.json │ ├── tutorial │ │ ├── basic.md │ │ ├── css.md │ │ ├── dev-server.md │ │ ├── image-and-font.md │ │ ├── optimization.md │ │ ├── plugin.md │ │ ├── typescript.md │ │ └── with-react.md │ ├── vercel.json │ └── webpack-tutorial │ │ ├── assets.md │ │ ├── dev-server.md │ │ ├── environment.md │ │ ├── example-project │ │ ├── assets │ │ │ ├── Inter-Regular.woff2 │ │ │ └── logo.svg │ │ ├── emoji.js │ │ ├── index.html │ │ ├── main.js │ │ └── style.css │ │ ├── intro.md │ │ ├── make-first-bundle.md │ │ ├── module-system.md │ │ ├── plugin.md │ │ ├── react.md │ │ ├── style.md │ │ └── typescript.md ├── code-quality │ ├── .vitepress │ │ ├── config.mts │ │ ├── en.mts │ │ ├── ja.mts │ │ ├── ko.mts │ │ ├── shared.mts │ │ ├── theme │ │ │ ├── Layout.vue │ │ │ ├── components │ │ │ │ ├── Comments.vue │ │ │ │ ├── CustomBanner.vue │ │ │ │ ├── GithubDiscussions.vue │ │ │ │ └── GithubDiscussionsDetail.vue │ │ │ ├── composables │ │ │ │ ├── index.ts │ │ │ │ ├── useBanner.ts │ │ │ │ ├── useDiscussionFilter.ts │ │ │ │ ├── useGithubApi.ts │ │ │ │ ├── useGithubDiscussions.ts │ │ │ │ ├── useLocale.ts │ │ │ │ ├── usePagination.ts │ │ │ │ └── useSortableData.ts │ │ │ ├── config │ │ │ │ └── OneNavigationItems.ts │ │ │ ├── custom.css │ │ │ ├── data │ │ │ │ └── bannerData.ts │ │ │ ├── hooks │ │ │ │ └── useGiscusTheme.tsx │ │ │ ├── index.ts │ │ │ ├── types │ │ │ │ └── github.ts │ │ │ └── utils │ │ │ │ ├── emoji.ts │ │ │ │ ├── index.ts │ │ │ │ └── markdown.ts │ │ └── zhHans.mts │ ├── .vscode │ │ ├── extensions.json │ │ └── settings.json │ ├── code │ │ ├── coming-soon.md │ │ ├── community.md │ │ ├── community │ │ │ └── good-discussions.md │ │ ├── detail │ │ │ └── index.md │ │ ├── dicussions │ │ │ └── index.md │ │ ├── examples │ │ │ ├── code-directory.md │ │ │ ├── condition-name.md │ │ │ ├── error-boundary.md │ │ │ ├── form-fields.md │ │ │ ├── hidden-logic.md │ │ │ ├── http.md │ │ │ ├── item-edit-modal.md │ │ │ ├── login-start-page.md │ │ │ ├── magic-number-cohesion.md │ │ │ ├── magic-number-readability.md │ │ │ ├── submit-button.md │ │ │ ├── submit1.md │ │ │ ├── ternary-operator.md │ │ │ ├── use-bottom-sheet.md │ │ │ ├── use-page-state-coupling.md │ │ │ ├── use-page-state-readability.md │ │ │ ├── use-user.md │ │ │ └── user-policy.md │ │ ├── index.md │ │ └── start.md │ ├── en │ │ ├── code │ │ │ ├── coming-soon.md │ │ │ ├── community.md │ │ │ ├── examples │ │ │ │ ├── code-directory.md │ │ │ │ ├── condition-name.md │ │ │ │ ├── form-fields.md │ │ │ │ ├── hidden-logic.md │ │ │ │ ├── http.md │ │ │ │ ├── item-edit-modal.md │ │ │ │ ├── login-start-page.md │ │ │ │ ├── magic-number-cohesion.md │ │ │ │ ├── magic-number-readability.md │ │ │ │ ├── submit-button.md │ │ │ │ ├── ternary-operator.md │ │ │ │ ├── use-bottom-sheet.md │ │ │ │ ├── use-page-state-coupling.md │ │ │ │ ├── use-page-state-readability.md │ │ │ │ ├── use-user.md │ │ │ │ └── user-policy.md │ │ │ ├── index.md │ │ │ └── start.md │ │ └── index.md │ ├── images │ │ └── examples │ │ │ ├── submit-button-dark.png │ │ │ └── submit-button.png │ ├── index.md │ ├── ja │ │ ├── code │ │ │ ├── coming-soon.md │ │ │ ├── community.md │ │ │ ├── examples │ │ │ │ ├── code-directory.md │ │ │ │ ├── condition-name.md │ │ │ │ ├── error-boundary.md │ │ │ │ ├── form-fields.md │ │ │ │ ├── hidden-logic.md │ │ │ │ ├── http.md │ │ │ │ ├── item-edit-modal.md │ │ │ │ ├── login-start-page.md │ │ │ │ ├── magic-number-cohesion.md │ │ │ │ ├── magic-number-readability.md │ │ │ │ ├── submit-button.md │ │ │ │ ├── ternary-operator.md │ │ │ │ ├── use-bottom-sheet.md │ │ │ │ ├── use-page-state-coupling.md │ │ │ │ ├── use-page-state-readability.md │ │ │ │ ├── use-user.md │ │ │ │ └── user-policy.md │ │ │ ├── index.md │ │ │ └── start.md │ │ └── index.md │ ├── package.json │ ├── public │ │ └── images │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-96x96.png │ │ │ ├── favicon.ico │ │ │ ├── ff-meta.png │ │ │ ├── ff-symbol-gradient-webp-80.webp │ │ │ ├── ff-symbol-gradient.png │ │ │ ├── ff-symbol.svg │ │ │ ├── site.webmanifest │ │ │ ├── web-app-manifest-192x192.png │ │ │ └── web-app-manifest-512x512.png │ ├── tsconfig.json │ ├── vercel.json │ └── zh-hans │ │ ├── code │ │ ├── coming-soon.md │ │ ├── community.md │ │ ├── examples │ │ │ ├── code-directory.md │ │ │ ├── condition-name.md │ │ │ ├── error-boundary.md │ │ │ ├── form-fields.md │ │ │ ├── hidden-logic.md │ │ │ ├── http.md │ │ │ ├── item-edit-modal.md │ │ │ ├── login-start-page.md │ │ │ ├── magic-number-cohesion.md │ │ │ ├── magic-number-readability.md │ │ │ ├── submit-button.md │ │ │ ├── ternary-operator.md │ │ │ ├── use-bottom-sheet.md │ │ │ ├── use-page-state-coupling.md │ │ │ ├── use-page-state-readability.md │ │ │ ├── use-user.md │ │ │ └── user-policy.md │ │ ├── index.md │ │ └── start.md │ │ └── index.md ├── debug │ ├── .vitepress │ │ ├── config.mts │ │ ├── shared.mts │ │ └── theme │ │ │ ├── Layout.vue │ │ │ ├── components │ │ │ └── Comments.vue │ │ │ ├── custom.css │ │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useGiscusTheme.tsx │ │ │ └── useLocale.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ └── index.ts │ ├── .vscode │ │ ├── extensions.json │ │ └── settings.json │ ├── get-started.md │ ├── index.md │ └── package.json └── shared │ ├── components │ ├── OneNavigation.vue │ └── index.ts │ ├── composables │ └── useLocale.ts │ └── config │ └── OneNavigationItems.ts ├── images ├── apple-touch-icon.png ├── bf-meta.png ├── bf-symbol.webp ├── favicon-96x96.png ├── favicon.ico ├── ff-meta.png ├── ff-symbol-gradient-webp-80.webp ├── ff-symbol-gradient.png ├── ff-symbol.svg ├── site.webmanifest ├── web-app-manifest-192x192.png └── web-app-manifest-512x512.png ├── package.json ├── public └── files │ ├── bundling-example-project.zip │ └── bundling-example.zip ├── tsconfig.json ├── vercel.json └── yarn.lock /.cursor/rules/techincal-writing-overall.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # 역할 7 | 8 | 이 봇의 역할은 기술 문서를 작성하기 전에, 문서 유형과 각 유형에 맞는 작성법을 안내하는 것입니다. 9 | 아래 정보를 참고하여, 내 상황에 가장 적합한 문서 유형과 그 문서 유형에 맞는 작성 가이드를 추천해 주고. 필요하다면 복수의 문서 유형을 제안해도 괜찮지만, 최대한 하나로 정해주세요. 10 | 11 | 아래 정보를 바탕으로, 가장 적합한 문서 유형(학습 중심 / 문제 해결 / 참조 / 설명)과 작성 시 유의해야 할 점을 제안해 주세요. 12 | 이모지는 사용하지 마세요. 13 | 14 | - 문서 목표 (예: “React의 Hook 개념을 자세히 알리고 싶다” / “Webpack 설정을 잡아주고 싶다” / “에러 발생 시 해결 방법을 제공하고 싶다” 등) 15 | - 독자 수준 (예: “React를 처음 접하는 초급 개발자” / “이미 Webpack 사용 경험이 있는 중급 개발자” / “비개발자 포함” 등) 16 | - 프로젝트 상황 (예: “새로운 기술을 도입해 보고 싶다” / “기존 프로젝트를 개선 중이라 빠른 해결이 필요하다” / “참조 문서가 너무 길어 핵심만 요약해야 한다” 등) 17 | - 추가 고려 사항 (예: “짧은 시간 안에 완성해야 한다” / “시각 자료를 많이 활용하고 싶다” / “다양한 OS 환경을 고려해야 한다” 등) 18 | 19 | 위 정보를 종합해서, 내가 어떤 문서 유형을 쓰면 좋을지, 그리고 그 유형에 맞춰 작성할 때 주의해야 할 사항을 알려 주세요. 필요하다면 복수의 문서 유형을 제안해도 괜찮습니다. 20 | 21 | ## 학습을 위한 문서를 작성할 때 주의해야 할 사항 22 | 23 | 문서에 포함해야 할 사항: 24 | 1. 명확한 학습 목표 및 완료 후 얻게 될 능력 25 | 2. 사전 준비 사항 및 환경 설정 방법 26 | 3. 단계별 안내와 설명(단계마다 무엇을 하는지, 왜 하는지 설명) 27 | 4. 실행할 수 있는 코드 예제(간단한 것부터 점진적으로 난이도 상승) 28 | 5. 문서 마지막에 FAQ 섹션 또는 자주 발생하는 문제와 해결책 29 | 30 | 독자가 막힘없이 따라 할 수 있도록 구성하고, 모든 예제 코드는 실제로 실행할 수 있어야 합니다. 31 | 32 | ## 깊은 이해를 위한 문서를 작성할 때 주의해야 할 사항 33 | 34 | 문서에 포함해야 할 사항: 35 | 1. 이 기술/개념이 등장한 배경과 해결하려는 문제 36 | 2. 기본 원리와 동작 방식에 대한 상세 설명 37 | 3. 다른 접근 방식과의 비교 및 장단점 38 | 4. 시각적 요소(다이어그램, 흐름도 등)를 활용한 개념 설명 39 | 5. 실제 사용 사례 및 응용 방법 40 | 41 | 문서는 독자가 단순한 사용법을 넘어 기술의 원리와 철학을 이해할 수 있도록 작성해 주세요. 42 | 43 | ## 문제 해결 문서를 작성할 때 주의해야 할 사항 44 | 45 | 문서에 포함해야 할 사항: 46 | 1. 명확한 문제 상황 또는 작업 목표 정의 47 | 2. 문제의 원인 또는 작업 수행에 필요한 배경지식 48 | 3. 단계별 해결 방법 또는 수행 절차 49 | 4. 실행할 수 있는 코드 예제나 명령어 50 | 5. 환경별 차이점(OS, 라이브러리 버전 등에 따른 주의 사항) 51 | 6. 해결책이 어떤 원리로 문제를 해결하는지에 대한 설명 52 | 53 | 문서는 독자가 바로 적용할 수 있는 실용적인 해결책을 제공해야 합니다. 54 | 55 | ## 참조 문서 작성 프롬프트를 작성할 때 주의해야 할 사항 56 | 57 | 문서에 포함해야 할 사항: 58 | 1. 간결한 개요 및 주요 기능 설명 59 | 2. 구문 및 파라미터 설명(타입, 기본값, 필수 여부 포함) 60 | 3. 반환 값 및 타입 설명 61 | 4. 사용 예제 코드(기본 사용법부터 다양한 활용 사례까지) 62 | 5. 관련 API/함수/컴포넌트와의 연계 방법 63 | 6. 주의 사항 및 제한사항 64 | 65 | 문서는 일관된 구조로 정확하고 완전한 정보를 제공하며, 독자가 필요한 정보를 빠르게 찾을 수 있도록 구성해 주세요. 66 | 67 | 출처: https://technical-writing.dev/tutorial/review-prompt.html -------------------------------------------------------------------------------- /.cursor/rules/technical-writing-architecture.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # 역할 7 | 8 | 이 봇의 역할은 기술 문서의 구조를 분석하고, 아래의 원칙들을 반영하여 문서를 개선할 수 있는 피드백과 개선안을 제안하는 것입니다. 9 | 아래 정보를 참고하여, 내가 작성한 문서 초안 혹은 문서 구조에 대해 피드백과 구체적인 개선안을 추천해 주세요. 10 | 여러 개선 옵션을 모두 반영한 하나의 좋은 안을 만들어 주세요. 11 | 12 | ## 참고할 원칙 및 체크리스트 13 | 14 | 1. 한 페이지에서는 하나의 목표만 다루기 15 | 16 | - 핵심 원칙: 한 페이지에서 하나의 주제나 목표에 집중해야 독자가 핵심 내용을 빠르게 파악할 수 있습니다. 17 | - 체크리스트: 18 | - 제목 깊이가 #### (H4) 이상이면 문서를 분리할 필요가 있음 19 | - 개요를 통해 핵심 목표를 명확하게 전달하고 있는지 확인 20 | - 너무 많은 개념이 혼합되어 있지는 않은지 점검 21 | 22 | 2. 개요 빠트리지 않기 23 | 24 | - 핵심 원칙: 문서의 핵심 내용을 요약하는 개요를 반드시 포함하여 독자가 전체 흐름을 미리 파악할 수 있도록 해야 합니다. 25 | - 체크리스트: 26 | - 문서 시작 부분에 명확한 개요가 배치되어 있는지 27 | - 독자가 “이 문서를 읽으면 무엇을 얻을 수 있는가?”를 바로 이해할 수 있는지 28 | 29 | 3. 예측 가능한 문서 구조 30 | 31 | - 핵심 원칙: 문서의 제목, 형식, 정보 배치가 일관되고 논리적인 순서를 유지하여 독자가 정보를 쉽게 탐색할 수 있어야 합니다. 32 | - 체크리스트: 33 | - 동일한 수준의 제목과 소제목이 일관된 패턴을 따르는지 34 | - 기본 개념부터 점진적으로 상세 내용이 배치되어 있는지 35 | - 용어가 일관되게 사용되는지 36 | 37 | 4. 가치를 먼저 제공하기 38 | 39 | - 핵심 원칙: 기능이나 세부 설정보다, 독자가 문서를 통해 얻을 수 있는 구체적인 가치나 문제 해결 효과를 먼저 전달해야 합니다. 40 | - 체크리스트: 41 | - 문서 도입부에서 독자가 얻을 이점이 명확하게 제시되어 있는지 42 | - 부수적인 세부 정보는 후순위로 배치되어 있는지 43 | 44 | 5. 효과적인 제목 쓰기 45 | 46 | - 핵심 원칙: 제목은 문서의 핵심을 간결하고 명확하게 전달해야 하며, 검색과 탐색에 용이하도록 구성되어야 합니다. 47 | - 체크리스트: 48 | - 제목에 핵심 키워드가 포함되어 있는지 49 | - 제목의 길이가 적절하고(예: 30자 이내), 일관된 스타일(동사형 또는 명사형)로 작성되었는지 50 | - 평서문 형태로 작성되어 있는지 51 | 52 | ## 제공할 정보 53 | 54 | - 문서 초안 혹은 구조: (예: “React 컴포넌트 생성” 문서의 현재 구조 혹은 목차) 55 | - (optional) 문서 목표 및 독자: (예: “독자가 React 컴포넌트 생성의 기본 원리를 이해하고 직접 코드를 작성할 수 있도록 돕는 것”) 56 | - (optional)현재 겪고 있는 문제점: (예: “한 페이지에 너무 많은 내용이 혼합되어 있어 독자가 원하는 정보를 찾기 어렵다” 또는 “개요가 없어서 문서 전체 흐름이 파악되지 않는다”) 57 | 58 | 위 정보를 종합하여, 문서 구조를 개선할 수 있는 구체적인 피드백과 개선안을 제안해 주세요. -------------------------------------------------------------------------------- /.cursor/rules/technical-writing-editing.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # 역할 7 | 8 | 이 봇의 역할은 기술 문서의 문장을 효과적이고 간결하게 개선하는 것입니다. 아래 정보를 참고하여, 입력된 문장을 더 명확하고 이해하기 쉬운 문장으로 수정할 수 있도록 피드백과 개선안을 제안해 주세요. 9 | 여러 개선 옵션을 모두 반영한 하나의 좋은 안을 만들어 주세요. 10 | 11 | ## 참고할 원칙 및 체크리스트 12 | 13 | 1. 필요한 정보만 남기기 14 | 15 | - 핵심 원칙: 문장은 짧고 간결해야 하며, 한 문장에 하나의 생각만 담아야 합니다. 16 | - 체크리스트: 17 | - 문장이 불필요하게 길거나 복잡한가? 18 | - 한 문장에 여러 개의 아이디어가 혼재되어 있지는 않은가? 19 | 20 | 2. 메타 담화를 최소화하기 21 | 22 | - 핵심 원칙: 핵심 메시지를 전달하는 데 방해가 되는 ‘말에 대한 말’을 제거합니다. 23 | - 체크리스트: 24 | - 문장 내 불필요한 서술(예: "앞서 설명했듯이", "여러분도 아실 것입니다")는 없는가? 25 | 26 | 3. 구체적으로 쓰기 27 | 28 | - 핵심 원칙: 모호한 표현 대신 구체적이고 직접적인 언어를 사용하여 독자가 바로 이해할 수 있도록 합니다. 29 | - 체크리스트: 30 | - 명확하지 않은 표현이나 불필요한 추상적 용어가 사용되지는 않았는가? 31 | - 동사를 사용하여 명확한 행동 지시를 제공하고 있는가? 32 | 33 | 4. 일관되게 쓰기 34 | 35 | - 핵심 원칙: 용어와 표현을 일관되게 사용하여 독자가 혼란 없이 정보를 받아들일 수 있도록 합니다. 36 | - 체크리스트: 37 | - 동일한 개념이 다양한 표현으로 나타나지는 않는가? 38 | - 약어나 외래어 표기는 처음 등장할 때 풀어서 표기하고 있는가? 39 | 40 | 5. 문장의 주체를 분명하게 하기 41 | 42 | - 핵심 원칙: 문장의 주어가 명확해야 독자가 어떤 행동을 해야 하는지 쉽게 파악할 수 있습니다. 43 | - 체크리스트: 44 | - 수동형 문장은 능동형으로 개선할 수 있는가? 45 | - 도구나 기술 자체가 주체가 되지 않고, 독자(개발자)가 주체가 되는지 확인 46 | 47 | ## 제공할 정보 48 | 49 | - 입력 문장: (예: “이 API를 호출할 때 요청 헤더와 인증 정보를 포함해야 정상적으로 응답을 받을 수 있습니다.”) 50 | 51 | 위 정보를 종합하여, 주어진 문장을 효과적이고 간결하게 개선할 수 있는 피드백과 수정안을 제안해 주세요. -------------------------------------------------------------------------------- /.github/DISCUSSION_TEMPLATE/code-smell.yml: -------------------------------------------------------------------------------- 1 | labels: ['Discussion'] 2 | body: 3 | - type: markdown 4 | attributes: 5 | value: | 6 | ## ✏️ 고민 7 | 배경, 개선 과정, 목표와 같은 내용을 포함해 작성해주시면, 더 유용한 답변을 받을 수 있어요. 8 | 9 | - type: textarea 10 | id: summary 11 | attributes: 12 | label: 고민 13 | validations: 14 | required: true 15 | 16 | - type: markdown 17 | attributes: 18 | value: | 19 | ## 💻 코드 20 | 맥락을 이해할 수 있도록, 해당 코드와 연관된 부분도 함께 작성해주세요. 21 | 22 | - type: textarea 23 | id: code 24 | attributes: 25 | label: 코드 26 | value: | 27 | ```tsx 28 | ``` 29 | validations: 30 | required: true 31 | 32 | - type: markdown 33 | attributes: 34 | value: | 35 | ## 🖼️ 참고 이미지(또는 자료) 36 | 문제를 더 명확히 전달할 수 있는 스크린샷, 다이어그램, 또는 링크를 추가해주세요. 37 | 38 | - type: textarea 39 | id: resources 40 | attributes: 41 | label: 참고 자료 42 | value: | 43 | - 스크린샷: 44 | - 다이어그램: 45 | - 링크 (ex. 피그마, 노션 등): 46 | validations: 47 | required: false 48 | -------------------------------------------------------------------------------- /.github/DISCUSSION_TEMPLATE/suggest-new-strategy.yml: -------------------------------------------------------------------------------- 1 | labels: ["Suggest new strategy"] 2 | body: 3 | - type: markdown 4 | attributes: 5 | value: | 6 | ## ✏️ Frontend Fundamentals 문서에 기여하기 7 | Frontend Fundamental 문서를 통해 알리고 싶은 좋은 코드의 기준과 패턴들을 제안해주세요. 8 | 제안된 내용은 디스커션에서 충분히 논의된 후, 메인테이너의 최종 검토를 거쳐 문서에 반영돼요. 9 | 10 | 만약 제안이 채택된다면, PR 작성을 부탁드릴 예정이에요. 11 | 부담 없이 자유롭게 의견을 공유해 주시고, 함께 좋은 아이디어를 발전시켜 나가길 기대하고 있어요. 🚀 12 | 13 | - type: markdown 14 | attributes: 15 | value: | 16 | ## 패턴 제목 17 | - 제안하고자 하는 기준(또는 패턴)의 제목을 그대로 작성해 주세요. ex: 같이 실행되지 않는 코드 분리하기 18 | - "~하지 않기" 보다는 "~하기"로 제목을 작성해주세요. 19 | 20 | - type: textarea 21 | id: title 22 | attributes: 23 | label: 패턴 제목 24 | validations: 25 | required: true 26 | 27 | - type: markdown 28 | attributes: 29 | value: | 30 | ## 문서 위계 31 | 예시) 1. 가독성 > 맥락 줄이기 > D. 같이 실행되지 않는 코드 분리하기 32 | 33 | - type: textarea 34 | id: folder 35 | attributes: 36 | label: 문서 위계 37 | validations: 38 | required: true 39 | 40 | - type: markdown 41 | attributes: 42 | value: | 43 | ## 개요 44 | 해당 개선 아이디어를 제안하게 된 이유를 간단히 설명해 주세요. 45 | 46 | - type: textarea 47 | id: summary 48 | attributes: 49 | label: 개요 50 | validations: 51 | required: true 52 | 53 | - type: markdown 54 | attributes: 55 | value: | 56 | ## 내용 57 | 기존 문서 포맷에 맞춰서 작성해주세요. 58 | 59 | - type: textarea 60 | id: contents 61 | attributes: 62 | label: 내용 63 | value: | 64 | 65 | {한줄설명} 66 | 67 | ## 📝 코드 예시 68 | 69 | {예시코드 설명} 70 | 71 | ```tsx 72 | console.log("Bad code"); 73 | ``` 74 | 75 | ## 👃 코드 냄새 맡아보기 76 | 77 | 78 | 79 | {냄새나는 이유} 80 | 81 | ## ✏️ 개선해보기 82 | 83 | {개선 방법} 84 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 📝 Key Changes 2 | 3 | 4 | ## 🖼️ Before and After Comparison 5 | 6 | 7 | | | | 8 | |:—:|:—:| 9 | |**Before**|**After**| 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .next 4 | out 5 | *.log 6 | dist 7 | .idea/ 8 | *.map 9 | .env 10 | esm 11 | .rpt2_cache 12 | umd 13 | .toss-es5-cache.json 14 | code-owners.json 15 | .linaria-cache 16 | .pnp.* 17 | .yarn/* 18 | # !.yarn/cache 19 | !.yarn/releases 20 | !.yarn/plugins 21 | !.yarn/sdks 22 | !.yarn/versions 23 | !.yarn/patches 24 | **/*/nft-analysis.json 25 | *.tsbuildinfo 26 | .eslintcache 27 | .i18n 28 | 29 | # asset-manifest.json 는 빌드 결과물이라 제외 30 | asset-manifest.json 31 | assets-preload.html 32 | _assetManifest.json 33 | __assetManifest.json 34 | 35 | # local 개발용 36 | .next-local 37 | .toss 38 | 39 | #rn 40 | .swc 41 | 42 | .tosscore 43 | **/.vitepress/cache 44 | **/.vitepress/dist 45 | 46 | config.mts.*.js -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "trailingComma": "none", 4 | "tabWidth": 2, 5 | "semi": true 6 | } 7 | -------------------------------------------------------------------------------- /.tossrc.yml: -------------------------------------------------------------------------------- 1 | version: '0.14' 2 | service: 3 | image: 4 | source: ./fundamentals/code-quality/.vitepress/dist 5 | ci: 6 | - branch: dev/* 7 | tasks: 8 | - service build 9 | - service pack -o image.tar.br 10 | - service publish image.tar.br s3:internal.alpha/docs-code-quality -m s3:internal.alpha/docs-code-quality/alpha 11 | - service deploy image.tar.br s3:core.alpha/docs-code-quality/alpha 12 | - branch: main 13 | tasks: 14 | - service build 15 | - service pack -o image.tar.br 16 | - service publish image.tar.br s3:internal.alpha/docs-code-quality -m s3:internal.alpha/docs-code-quality/alpha 17 | - service publish image.tar.br s3:internal.common/docs-code-quality -m s3:internal.common/docs-code-quality/live 18 | - service deploy image.tar.br s3:core.alpha/docs-code-quality/alpha -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs", 4 | "esbenp.prettier-vscode" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/.yarn": true, 4 | "**/.pnp.*": true 5 | }, 6 | "typescript.tsdk": ".yarn/sdks/typescript/lib", 7 | "typescript.enablePromptUseWorkspaceTsdk": true, 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll": "explicit" 10 | }, 11 | "editor.formatOnSave": true, 12 | "prettier.prettierPath": ".yarn/sdks/prettier/index.cjs" 13 | } 14 | -------------------------------------------------------------------------------- /.yarn/sdks/integrations.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by @yarnpkg/sdks. 2 | # Manual changes might be lost! 3 | 4 | integrations: 5 | - vscode 6 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/bin/prettier.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); 12 | const absRequire = createRequire(absPnpApiPath); 13 | 14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 16 | 17 | if (existsSync(absPnpApiPath)) { 18 | if (!process.versions.pnp) { 19 | // Setup the environment to be able to require prettier/bin/prettier.cjs 20 | require(absPnpApiPath).setup(); 21 | if (isPnpLoaderEnabled && register) { 22 | register(pathToFileURL(absPnpLoaderPath)); 23 | } 24 | } 25 | } 26 | 27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath) 28 | ? exports => absRequire(absUserWrapperPath)(exports) 29 | : exports => exports; 30 | 31 | // Defer to the real prettier/bin/prettier.cjs your application uses 32 | module.exports = wrapWithUserWrapper(absRequire(`prettier/bin/prettier.cjs`)); 33 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/index.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); 12 | const absRequire = createRequire(absPnpApiPath); 13 | 14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 16 | 17 | if (existsSync(absPnpApiPath)) { 18 | if (!process.versions.pnp) { 19 | // Setup the environment to be able to require prettier 20 | require(absPnpApiPath).setup(); 21 | if (isPnpLoaderEnabled && register) { 22 | register(pathToFileURL(absPnpLoaderPath)); 23 | } 24 | } 25 | } 26 | 27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath) 28 | ? exports => absRequire(absUserWrapperPath)(exports) 29 | : exports => exports; 30 | 31 | // Defer to the real prettier your application uses 32 | module.exports = wrapWithUserWrapper(absRequire(`prettier`)); 33 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prettier", 3 | "version": "3.5.3-sdk", 4 | "main": "./index.cjs", 5 | "type": "commonjs", 6 | "bin": "./bin/prettier.cjs" 7 | } 8 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); 12 | const absRequire = createRequire(absPnpApiPath); 13 | 14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 16 | 17 | if (existsSync(absPnpApiPath)) { 18 | if (!process.versions.pnp) { 19 | // Setup the environment to be able to require typescript/bin/tsc 20 | require(absPnpApiPath).setup(); 21 | if (isPnpLoaderEnabled && register) { 22 | register(pathToFileURL(absPnpLoaderPath)); 23 | } 24 | } 25 | } 26 | 27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath) 28 | ? exports => absRequire(absUserWrapperPath)(exports) 29 | : exports => exports; 30 | 31 | // Defer to the real typescript/bin/tsc your application uses 32 | module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsc`)); 33 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsserver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); 12 | const absRequire = createRequire(absPnpApiPath); 13 | 14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 16 | 17 | if (existsSync(absPnpApiPath)) { 18 | if (!process.versions.pnp) { 19 | // Setup the environment to be able to require typescript/bin/tsserver 20 | require(absPnpApiPath).setup(); 21 | if (isPnpLoaderEnabled && register) { 22 | register(pathToFileURL(absPnpLoaderPath)); 23 | } 24 | } 25 | } 26 | 27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath) 28 | ? exports => absRequire(absUserWrapperPath)(exports) 29 | : exports => exports; 30 | 31 | // Defer to the real typescript/bin/tsserver your application uses 32 | module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsserver`)); 33 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsc.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); 12 | const absRequire = createRequire(absPnpApiPath); 13 | 14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 16 | 17 | if (existsSync(absPnpApiPath)) { 18 | if (!process.versions.pnp) { 19 | // Setup the environment to be able to require typescript/lib/tsc.js 20 | require(absPnpApiPath).setup(); 21 | if (isPnpLoaderEnabled && register) { 22 | register(pathToFileURL(absPnpLoaderPath)); 23 | } 24 | } 25 | } 26 | 27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath) 28 | ? exports => absRequire(absUserWrapperPath)(exports) 29 | : exports => exports; 30 | 31 | // Defer to the real typescript/lib/tsc.js your application uses 32 | module.exports = wrapWithUserWrapper(absRequire(`typescript/lib/tsc.js`)); 33 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/typescript.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`); 12 | const absRequire = createRequire(absPnpApiPath); 13 | 14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 16 | 17 | if (existsSync(absPnpApiPath)) { 18 | if (!process.versions.pnp) { 19 | // Setup the environment to be able to require typescript 20 | require(absPnpApiPath).setup(); 21 | if (isPnpLoaderEnabled && register) { 22 | register(pathToFileURL(absPnpLoaderPath)); 23 | } 24 | } 25 | } 26 | 27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath) 28 | ? exports => absRequire(absUserWrapperPath)(exports) 29 | : exports => exports; 30 | 31 | // Defer to the real typescript your application uses 32 | module.exports = wrapWithUserWrapper(absRequire(`typescript`)); 33 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "5.7.3-sdk", 4 | "main": "./lib/typescript.js", 5 | "type": "commonjs", 6 | "bin": { 7 | "tsc": "./bin/tsc", 8 | "tsserver": "./bin/tsserver" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | enableGlobalCache: false 3 | nodeLinker: pnp 4 | yarnPath: .yarn/releases/yarn-4.6.0.cjs 5 | 6 | supportedArchitectures: 7 | cpu: 8 | - current 9 | - x64 10 | - arm64 11 | libc: 12 | - current 13 | - glibc 14 | - musl 15 | os: 16 | - current 17 | - darwin 18 | - linux 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Viva Republica, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Frontend Fundamentals](./images/ff-meta.png) 2 | 3 | # Frontend Fundamentals 4 | 5 | > Essential principles for frontend development 6 | 7 | **Frontend Fundamentals (FF)** is a growing collection of essential principles for building reliable, maintainable, and accessible frontend applications. It helps you answer common but tricky questions like how to judge code quality, why bundling behaves differently in dev vs. production, how screen readers interpret your UI, and how to debug efficiently. 8 | 9 | https://frontend-fundamentals.com/en/ 10 | 11 | ## 🧭 When to Use 12 | 13 | - 🦨 When you're unsure about your code but find it hard to explain the issues logically 14 | - 🧯 When you're trying to debug a persistent frontend bug and want to double-check your fundamentals 15 | - 👥 When you're reviewing UI code for accessibility and want concrete do's and don'ts 16 | - 🛠️ When your team is setting shared standards for clean, robust frontend code 17 | 18 | ## 📚 Collections 19 | 20 | - [Code Quality Fundamentals](https://frontend-fundamentals.com/code-quality/) 21 | - [Bundling Fundamentals](https://frontend-fundamentals.com/bundling/) 22 | - A11y Fundamentals (coming soon!) 23 | 24 | ## Contributing 25 | 26 | **Frontend Fundamentals (FF)** is a community-driven project to establish standards for writing good code. 27 | 28 | If you have a piece of code you're unsure about, post it on the GitHub Discussions page. 29 | The community can provide diverse reviews of your code, helping you and others think critically about what makes good code. 30 | Highly supported cases may even make it into the Frontend Fundamentals documentation. Contribution guidelines will be announced soon. 31 | 32 | - [Post on GitHub Discussions](https://github.com/toss/frontend-fundamentals/discussions) 33 | 34 | ## License 35 | 36 | MIT © Viva Republica, Inc. See [LICENSE](./LICENSE.md) for details. 37 | 38 | 39 | 40 | 41 | Toss 42 | 43 | 44 | -------------------------------------------------------------------------------- /api/github.js: -------------------------------------------------------------------------------- 1 | export default async function handler(req, res) { 2 | const { query } = req.body; 3 | 4 | const token = process.env.READ_GITHUB_DISCUSSION_ACCESS_TOKEN; 5 | 6 | if (!token) { 7 | console.error("[Server] GitHub 토큰 누락"); 8 | return res.status(500).json({ error: "GitHub token is not configured" }); 9 | } 10 | 11 | const response = await fetch("https://api.github.com/graphql", { 12 | method: "POST", 13 | headers: { 14 | Authorization: `Bearer ${token}`, 15 | "Content-Type": "application/json" 16 | }, 17 | body: JSON.stringify({ query }) 18 | }); 19 | 20 | const data = await response.json(); 21 | 22 | return res.status(200).json(data); 23 | } 24 | -------------------------------------------------------------------------------- /fundamentals/a11y/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | import footnote from "markdown-it-footnote"; 3 | import path from "node:path"; 4 | import { createRequire } from "node:module"; 5 | import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs' 6 | import { sharedConfig } from './shared.mjs'; 7 | 8 | const require = createRequire(import.meta.url); 9 | 10 | // https://vitepress.dev/reference/site-config 11 | export default defineConfig({ 12 | ...sharedConfig, 13 | title: "A11y Fundamentals", 14 | description: "프론트엔드 접근성의 모든 것", 15 | ignoreDeadLinks: false, 16 | base: "/a11y/", 17 | themeConfig: { 18 | // https://vitepress.dev/reference/default-theme-config 19 | ...sharedConfig.themeConfig, 20 | nav: [ 21 | { text: "홈", link: "/" }, 22 | ], 23 | sidebar: [ 24 | { 25 | text: "소개", 26 | items: [ 27 | { 28 | text: "시작하기", 29 | link: "/get-started", 30 | }, 31 | ] 32 | }, 33 | { 34 | text: "튜토리얼", 35 | }, 36 | { 37 | text: "심화 학습", 38 | }, 39 | ], 40 | }, 41 | markdown: { 42 | config: (md) => { 43 | md.use(footnote); 44 | md.use(tabsMarkdownPlugin); 45 | }, 46 | }, 47 | head: [ 48 | [ 49 | "link", 50 | { rel: "icon", type: "image/x-icon", href: "/bundling/images/favicon.ico" } 51 | ], 52 | [ 53 | "meta", 54 | { 55 | property: "og:image", 56 | content: "https://static.toss.im/illusts/bf-meta.png" 57 | } 58 | ], 59 | [ 60 | "meta", 61 | { 62 | name: "twitter:image", 63 | content: "https://static.toss.im/illusts/bf-meta.png" 64 | } 65 | ], 66 | [ 67 | "meta", 68 | { 69 | name: "twitter:card", 70 | content: "summary" 71 | } 72 | ], 73 | ], 74 | vite: { 75 | resolve: { 76 | alias: [ 77 | { 78 | find: /^vue$/, 79 | replacement: path.dirname( 80 | require.resolve("vue/package.json", { 81 | paths: [require.resolve("vitepress")] 82 | }) 83 | ) 84 | }, 85 | { 86 | find: /^vue\/server-renderer$/g, 87 | replacement: path.dirname( 88 | require.resolve("vue/server-renderer", { 89 | paths: [require.resolve("vitepress")] 90 | }) 91 | ) 92 | }, 93 | { 94 | find: /^@shared/, 95 | replacement: path.resolve(__dirname, '../../shared'), 96 | } 97 | ] 98 | } 99 | }, 100 | }) 101 | 102 | 103 | -------------------------------------------------------------------------------- /fundamentals/a11y/.vitepress/shared.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig, HeadConfig } from 'vitepress' 2 | import { DefaultTheme } from "vitepress"; 3 | 4 | const search: DefaultTheme.LocalSearchOptions["locales"] = { 5 | root: { 6 | translations: { 7 | button: { 8 | buttonText: "검색", 9 | buttonAriaLabel: "검색" 10 | }, 11 | modal: { 12 | backButtonTitle: "뒤로가기", 13 | displayDetails: "더보기", 14 | footer: { 15 | closeKeyAriaLabel: "닫기", 16 | closeText: "닫기", 17 | navigateDownKeyAriaLabel: "아래로", 18 | navigateText: "이동", 19 | navigateUpKeyAriaLabel: "위로", 20 | selectKeyAriaLabel: "선택", 21 | selectText: "선택" 22 | }, 23 | noResultsText: "검색 결과를 찾지 못했어요.", 24 | resetButtonTitle: "모두 지우기" 25 | } 26 | } 27 | } 28 | }; 29 | 30 | 31 | export const sharedConfig = defineConfig({ 32 | lastUpdated: true, 33 | head: [ 34 | [ 35 | "link", 36 | { rel: "icon", type: "image/x-icon", href: "/images/favicon.ico" } 37 | ], 38 | [ 39 | "meta", 40 | { 41 | property: "og:image", 42 | content: "https://static.toss.im/illusts/bf-meta.png" 43 | } 44 | ], 45 | [ 46 | "meta", 47 | { 48 | name: "twitter:image", 49 | content: "https://static.toss.im/illusts/bf-meta.png" 50 | } 51 | ], 52 | [ 53 | "meta", 54 | { 55 | name: "twitter:card", 56 | content: "summary" 57 | } 58 | ], 59 | ], 60 | transformHead: ({ pageData }) => { 61 | const head: HeadConfig[] = []; 62 | const title = 63 | pageData.frontmatter.title || pageData.title || "Bundling Fundamentals"; 64 | const description = 65 | pageData.frontmatter.description || 66 | pageData.description || 67 | "Practical Guide to Efficient Frontend Bundling"; 68 | 69 | head.push(["meta", { property: "og:title", content: title }]); 70 | head.push(["meta", { property: "og:description", content: description }]); 71 | 72 | return head; 73 | }, 74 | 75 | themeConfig: { 76 | socialLinks: [ 77 | { icon: 'github', link: 'https://github.com/toss/frontend-fundamentals' } 78 | ], 79 | search: { 80 | provider: "local", 81 | options: { 82 | locales: { 83 | ...search, 84 | } 85 | } 86 | }, 87 | } 88 | }) 89 | -------------------------------------------------------------------------------- /fundamentals/a11y/.vitepress/theme/Layout.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | 22 | 43 | -------------------------------------------------------------------------------- /fundamentals/a11y/.vitepress/theme/components/Comments.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 37 | -------------------------------------------------------------------------------- /fundamentals/a11y/.vitepress/theme/custom.css: -------------------------------------------------------------------------------- 1 | html.dark .light-only { 2 | display: none !important; 3 | } 4 | 5 | html:not(.dark) .dark-only { 6 | display: none !important; 7 | } 8 | 9 | :root { 10 | --vp-font-family-base: "Toss Product Sans", ui-sans-serif, system-ui, 11 | sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", 12 | "Noto Color Emoji"; 13 | } 14 | 15 | :root[lang="ko"] { 16 | word-break: keep-all; 17 | } 18 | -------------------------------------------------------------------------------- /fundamentals/a11y/.vitepress/theme/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useLocale"; 2 | export * from "./useGiscusTheme"; 3 | -------------------------------------------------------------------------------- /fundamentals/a11y/.vitepress/theme/hooks/useGiscusTheme.tsx: -------------------------------------------------------------------------------- 1 | import { ref, watch, computed, onMounted } from "vue"; 2 | import { useData } from "vitepress"; 3 | import { GISCUS_ORIGIN, GISCUS_THEME, sendGiscusMessage } from "../utils"; 4 | 5 | export function useGiscusTheme() { 6 | const { isDark } = useData(); 7 | const isIframeLoaded = ref(false); 8 | 9 | const syncTheme = () => { 10 | sendGiscusMessage({ 11 | setConfig: { 12 | theme: isDark.value ? GISCUS_THEME.dark : GISCUS_THEME.light 13 | } 14 | }); 15 | }; 16 | 17 | const setupThemeHandler = () => { 18 | window.addEventListener("message", (event) => { 19 | if (event.origin === GISCUS_ORIGIN && event.data?.giscus != null) { 20 | isIframeLoaded.value = true; 21 | syncTheme(); 22 | } 23 | }); 24 | }; 25 | 26 | onMounted(() => { 27 | setupThemeHandler(); 28 | }); 29 | 30 | watch(isDark, () => { 31 | if (isIframeLoaded.value) { 32 | syncTheme(); 33 | } 34 | }); 35 | 36 | return { 37 | theme: computed(() => 38 | isDark.value ? GISCUS_THEME.dark : GISCUS_THEME.light 39 | ) 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /fundamentals/a11y/.vitepress/theme/hooks/useLocale.ts: -------------------------------------------------------------------------------- 1 | import { useData } from "vitepress"; 2 | import { computed } from "vue"; 3 | 4 | export function useLocale() { 5 | const { lang } = useData(); 6 | 7 | const isKorean = computed(() => lang.value === "ko" || lang.value === "root"); 8 | 9 | return { 10 | isKorean 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /fundamentals/a11y/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from "vitepress/theme"; 2 | import Layout from "./Layout.vue"; 3 | import * as amplitude from "@amplitude/analytics-browser"; 4 | import "./custom.css"; 5 | import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client'; 6 | 7 | export default { 8 | extends: DefaultTheme, 9 | Layout, 10 | async enhanceApp({ app }) { 11 | if (typeof window !== "undefined") { 12 | const amplitudeApiKey = (import.meta as any).env.VITE_AMPLITUDE_API_KEY; 13 | amplitude.init(amplitudeApiKey, { autocapture: true }); 14 | enhanceAppWithTabs(app) 15 | } 16 | } 17 | }; -------------------------------------------------------------------------------- /fundamentals/a11y/.vitepress/theme/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const GISCUS_ORIGIN = "https://giscus.app" as const; 2 | 3 | export const GISCUS_LANG_MAP = { 4 | ko: "ko", 5 | en: "en", 6 | ja: "ja", 7 | "zh-hans": "zh-CN" 8 | } as const; 9 | 10 | export const GISCUS_THEME = { 11 | light: "light_tritanopia", 12 | dark: "dark_tritanopia" 13 | }; 14 | 15 | export function getGiscusLang(lang) { 16 | return GISCUS_LANG_MAP[lang] || "en"; 17 | } 18 | 19 | export function sendGiscusMessage(message: T) { 20 | const iframe = document.querySelector( 21 | "iframe.giscus-frame" 22 | ); 23 | if (!iframe) return; 24 | 25 | iframe.contentWindow?.postMessage({ giscus: message }, GISCUS_ORIGIN); 26 | } 27 | -------------------------------------------------------------------------------- /fundamentals/a11y/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /fundamentals/a11y/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": "explicit" 4 | }, 5 | "editor.formatOnSave": true, 6 | "eslint.nodePath": "../../.yarn/sdks", 7 | "prettier.prettierPath": "../../.yarn/sdks/prettier/index.cjs", 8 | "typescript.tsdk": "../../.yarn/sdks/typescript/lib", 9 | "typescript.enablePromptUseWorkspaceTsdk": true, 10 | "search.exclude": { 11 | "**/.yarn": true, 12 | "**/.pnp.*": true, 13 | "**/.next": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /fundamentals/a11y/get-started.md: -------------------------------------------------------------------------------- 1 | # 시작하기 2 | 3 | ## 이런 분들에게 추천해요 4 | 5 | ## 저작자 -------------------------------------------------------------------------------- /fundamentals/a11y/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "A11y Fundamentals" 7 | tagline: "프론트엔드 접근성의 모든 것" 8 | image: 9 | loading: eager 10 | fetchpriority: high 11 | decoding: async 12 | src: /images/bf-symbol.webp 13 | alt: Frontend Fundamentals symbol 14 | actions: 15 | - text: 시작하기 16 | link: /get-started 17 | - theme: alt 18 | text: 접근성이란? 19 | link: /overview 20 | 21 | features: 22 | - icon: 📦 23 | title: 1 24 | details: 1-설명 25 | link: /overview 26 | - icon: 🚀 27 | title: 2 28 | details: 2-설명 29 | link: /webpack-tutorial/intro 30 | - icon: 🔍 31 | title: 3 32 | details: 3-설명 33 | link: /deep-dive/bundling-process/overview 34 | --- 35 | -------------------------------------------------------------------------------- /fundamentals/a11y/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@frontend-fundamentals/a11y", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "docs:dev": "vitepress dev", 8 | "docs:build": "vitepress build", 9 | "docs:preview": "vitepress preview" 10 | }, 11 | "dependencies": { 12 | "@amplitude/analytics-browser": "^2.11.11", 13 | "markdown-it-footnote": "^4.0.0", 14 | "typescript": "^5.6.3", 15 | "vitepress": "^1.4.1", 16 | "vitepress-plugin-tabs": "^0.7.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /fundamentals/bundling/.vitepress/shared.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig, HeadConfig } from 'vitepress' 2 | import { DefaultTheme } from "vitepress"; 3 | 4 | const search: DefaultTheme.LocalSearchOptions["locales"] = { 5 | root: { 6 | translations: { 7 | button: { 8 | buttonText: "검색", 9 | buttonAriaLabel: "검색" 10 | }, 11 | modal: { 12 | backButtonTitle: "뒤로가기", 13 | displayDetails: "더보기", 14 | footer: { 15 | closeKeyAriaLabel: "닫기", 16 | closeText: "닫기", 17 | navigateDownKeyAriaLabel: "아래로", 18 | navigateText: "이동", 19 | navigateUpKeyAriaLabel: "위로", 20 | selectKeyAriaLabel: "선택", 21 | selectText: "선택" 22 | }, 23 | noResultsText: "검색 결과를 찾지 못했어요.", 24 | resetButtonTitle: "모두 지우기" 25 | } 26 | } 27 | } 28 | }; 29 | 30 | 31 | export const sharedConfig = defineConfig({ 32 | lastUpdated: true, 33 | head: [ 34 | [ 35 | "link", 36 | { rel: "icon", type: "image/x-icon", href: "/images/favicon.ico" } 37 | ], 38 | [ 39 | "meta", 40 | { 41 | property: "og:image", 42 | content: "https://static.toss.im/illusts/bf-meta.png" 43 | } 44 | ], 45 | [ 46 | "meta", 47 | { 48 | name: "twitter:image", 49 | content: "https://static.toss.im/illusts/bf-meta.png" 50 | } 51 | ], 52 | [ 53 | "meta", 54 | { 55 | name: "twitter:card", 56 | content: "summary" 57 | } 58 | ], 59 | ], 60 | transformHead: ({ pageData }) => { 61 | const head: HeadConfig[] = []; 62 | const title = 63 | pageData.frontmatter.title || pageData.title || "Bundling Fundamentals"; 64 | const description = 65 | pageData.frontmatter.description || 66 | pageData.description || 67 | "Practical Guide to Efficient Frontend Bundling"; 68 | 69 | head.push(["meta", { property: "og:title", content: title }]); 70 | head.push(["meta", { property: "og:description", content: description }]); 71 | 72 | return head; 73 | }, 74 | 75 | themeConfig: { 76 | socialLinks: [ 77 | { icon: 'github', link: 'https://github.com/toss/frontend-fundamentals' } 78 | ], 79 | search: { 80 | provider: "local", 81 | options: { 82 | locales: { 83 | ...search, 84 | } 85 | } 86 | }, 87 | } 88 | }) 89 | -------------------------------------------------------------------------------- /fundamentals/bundling/.vitepress/theme/Layout.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | 22 | 43 | -------------------------------------------------------------------------------- /fundamentals/bundling/.vitepress/theme/components/Comments.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 37 | -------------------------------------------------------------------------------- /fundamentals/bundling/.vitepress/theme/custom.css: -------------------------------------------------------------------------------- 1 | html.dark .light-only { 2 | display: none !important; 3 | } 4 | 5 | html:not(.dark) .dark-only { 6 | display: none !important; 7 | } 8 | 9 | :root { 10 | --vp-font-family-base: "Toss Product Sans", ui-sans-serif, system-ui, 11 | sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", 12 | "Noto Color Emoji"; 13 | } 14 | 15 | :root[lang="ko"] { 16 | word-break: keep-all; 17 | } 18 | -------------------------------------------------------------------------------- /fundamentals/bundling/.vitepress/theme/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useLocale"; 2 | export * from "./useGiscusTheme"; 3 | -------------------------------------------------------------------------------- /fundamentals/bundling/.vitepress/theme/hooks/useGiscusTheme.tsx: -------------------------------------------------------------------------------- 1 | import { ref, watch, computed, onMounted } from "vue"; 2 | import { useData } from "vitepress"; 3 | import { GISCUS_ORIGIN, GISCUS_THEME, sendGiscusMessage } from "../utils"; 4 | 5 | export function useGiscusTheme() { 6 | const { isDark } = useData(); 7 | const isIframeLoaded = ref(false); 8 | 9 | const syncTheme = () => { 10 | sendGiscusMessage({ 11 | setConfig: { 12 | theme: isDark.value ? GISCUS_THEME.dark : GISCUS_THEME.light 13 | } 14 | }); 15 | }; 16 | 17 | const setupThemeHandler = () => { 18 | window.addEventListener("message", (event) => { 19 | if (event.origin === GISCUS_ORIGIN && event.data?.giscus != null) { 20 | isIframeLoaded.value = true; 21 | syncTheme(); 22 | } 23 | }); 24 | }; 25 | 26 | onMounted(() => { 27 | setupThemeHandler(); 28 | }); 29 | 30 | watch(isDark, () => { 31 | if (isIframeLoaded.value) { 32 | syncTheme(); 33 | } 34 | }); 35 | 36 | return { 37 | theme: computed(() => 38 | isDark.value ? GISCUS_THEME.dark : GISCUS_THEME.light 39 | ) 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /fundamentals/bundling/.vitepress/theme/hooks/useLocale.ts: -------------------------------------------------------------------------------- 1 | import { useData } from "vitepress"; 2 | import { computed } from "vue"; 3 | 4 | export function useLocale() { 5 | const { lang } = useData(); 6 | 7 | const isKorean = computed(() => lang.value === "ko" || lang.value === "root"); 8 | 9 | return { 10 | isKorean 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /fundamentals/bundling/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from "vitepress/theme"; 2 | import Layout from "./Layout.vue"; 3 | import * as amplitude from "@amplitude/analytics-browser"; 4 | import "./custom.css"; 5 | import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client'; 6 | 7 | export default { 8 | extends: DefaultTheme, 9 | Layout, 10 | async enhanceApp({ app }) { 11 | if (typeof window !== "undefined") { 12 | const amplitudeApiKey = (import.meta as any).env.VITE_AMPLITUDE_API_KEY; 13 | amplitude.init(amplitudeApiKey, { autocapture: true }); 14 | enhanceAppWithTabs(app) 15 | } 16 | } 17 | }; -------------------------------------------------------------------------------- /fundamentals/bundling/.vitepress/theme/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const GISCUS_ORIGIN = "https://giscus.app" as const; 2 | 3 | export const GISCUS_LANG_MAP = { 4 | ko: "ko", 5 | en: "en", 6 | ja: "ja", 7 | "zh-hans": "zh-CN" 8 | } as const; 9 | 10 | export const GISCUS_THEME = { 11 | light: "light_tritanopia", 12 | dark: "dark_tritanopia" 13 | }; 14 | 15 | export function getGiscusLang(lang) { 16 | return GISCUS_LANG_MAP[lang] || "en"; 17 | } 18 | 19 | export function sendGiscusMessage(message: T) { 20 | const iframe = document.querySelector( 21 | "iframe.giscus-frame" 22 | ); 23 | if (!iframe) return; 24 | 25 | iframe.contentWindow?.postMessage({ giscus: message }, GISCUS_ORIGIN); 26 | } 27 | -------------------------------------------------------------------------------- /fundamentals/bundling/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /fundamentals/bundling/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": "explicit" 4 | }, 5 | "editor.formatOnSave": true, 6 | "eslint.nodePath": "../../.yarn/sdks", 7 | "prettier.prettierPath": "../../.yarn/sdks/prettier/index.cjs", 8 | "typescript.tsdk": "../../.yarn/sdks/typescript/lib", 9 | "typescript.enablePromptUseWorkspaceTsdk": true, 10 | "search.exclude": { 11 | "**/.yarn": true, 12 | "**/.pnp.*": true, 13 | "**/.next": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /fundamentals/bundling/deep-dive/bundling-process/overview.md: -------------------------------------------------------------------------------- 1 | # 번들링, 꼭 필요할까요? 2 | 3 | 프로젝트 규모가 커지면 모듈 구조가 복잡해져요. 동일한 모듈을 여러 곳에서 참조하거나, 순환 참조가 발생해 실행 순서를 예측하기 어려워질 수 있어요. 만약 브라우저에서 각 모듈을 개별로 불러온다면, 모든 의존성을 직접 관리해야 하고, 이는 개발과 유지보수를 어렵게 만들어요. 4 | 5 | 번들링은 이런 복잡성을 해결하기 위한 방법이에요. 6 | 모듈 간 연결을 정리하고, 필요한 코드만 묶어 실행 흐름을 최적화해요. 덕분에 브라우저는 효율적으로 코드를 불러오고 안정적으로 실행할 수 있어요. 7 | 8 | ## 번들링 과정 9 | 10 | 번들링은 프로젝트에 흩어져 있는 모듈을 분석해, 실행에 필요한 순서와 구조를 정리하는 작업이에요. 11 | 번들러는 코드의 연결 관계를 파악하고, 이를 하나의 파일로 묶어 브라우저가 효율적으로 코드를 실행할 수 있도록 도와줘요. 12 | 13 | 이 과정은 다음 세 단계로 이루어져요. 14 | 15 | ### 1. 모듈 탐색 (Module Resolution) 16 | 17 | 번들러는 진입점(Entry Point)부터 시작해, import나 require 구문을 따라가며 연결된 모든 모듈을 탐색해요. 이 과정에서 애플리케이션 전체의 참조 구조를 파악하죠. 18 | 19 | 예를 들어, index.js에서 math.js 모듈을 가져와 사용하는 경우를 살펴볼게요. 20 | 21 | ```javascript 22 | // index.js 23 | import { add } from './math.js'; 24 | console.log(add(2, 3)); 25 | 26 | // math.js 27 | export function add(a, b) { 28 | return a + b; 29 | } 30 | ``` 31 | 32 | 번들러는 진입점(Entry Point)인 index.js에서 시작해, import나 require를 따라가며 연결된 모듈들을 하나씩 찾아가요. 33 | 34 | 코드 속 참조를 따라 이동하며 전체 애플리케이션이 어떤 식으로 구성되어 있는지 탐색해요. 35 | 36 | ### 2. 의존성 구조 정리 37 | 38 | 탐색한 정보를 바탕으로, 번들러는 의존성 그래프(Dependency Graph)를 만들어요. 39 | 이 그래프는 각 모듈이 어떤 다른 모듈에 의존하는지를 보여주고, 번들 파일 생성의 설계도가 돼요. 40 | 41 | 탐색 결과를 아래와 같은 형태로 정리할 수 있어요. 42 | ```json 43 | { 44 | "index.js": { 45 | "dependencies": ["math.js"], 46 | "code": "console.log(add(2, 3));" 47 | }, 48 | "math.js": { 49 | "dependencies": [], 50 | "code": "function add(a, b) { return a + b; }" 51 | } 52 | } 53 | ``` 54 | 정리된 객체를 보면, index.js는 math.js에 의존하고, math.js는 다른 의존성이 없음을 확인할 수 있어요. 55 | 56 | ### 3. 번들 파일 생성 57 | 58 | 마지막으로 번들러는 의존성 그래프를 바탕으로 필요한 모든 코드를 하나로 합쳐요. 59 | 이를 통해 브라우저가 여러 파일을 따로 요청하지 않고도 한 번에 코드를 불러올 수 있게 돼요. 60 | 61 | ```javascript 62 | (function(modules) { 63 | function require(file) { 64 | const exports = {}; 65 | modules[file](exports, require); 66 | return exports; 67 | } 68 | 69 | require('index.js'); 70 | })({ 71 | 'index.js': function(exports, require) { 72 | const { add } = require('math.js'); 73 | console.log(add(2, 3)); 74 | }, 75 | 'math.js': function(exports) { 76 | exports.add = function(a, b) { 77 | return a + b; 78 | }; 79 | } 80 | }); 81 | ``` 82 | 83 | 번들 파일은 각각의 모듈을 객체로 관리하고, require 함수를 통해 모듈 간 의존성을 연결해요. 84 | 덕분에 브라우저는 별도의 파일 요청 없이 모든 코드를 순서대로 실행할 수 있어요. 85 | 86 | :::details Q. 번들 파일에서는 왜 즉시 실행 함수(IIFE)를 사용할까요? 87 | 1. **스코프를 분리할 수 있어요** 88 | 89 | IIFE는 자체 스코프를 가지기 때문에, 내부 변수나 함수가 전역 스코프를 오염시키지 않아요. 90 | 91 | 2. **모듈 시스템을 안전하게 구성할 수 있어요** 92 | 93 | `require` 함수나 `modules` 객체 같은 모듈 로딩 로직을 IIFE 내부에서 정의하면 외부와 격리된 안전한 실행 환경을 만들 수 있어요. 94 | ::: -------------------------------------------------------------------------------- /fundamentals/bundling/deep-dive/dev/overview.md: -------------------------------------------------------------------------------- 1 | # 웹팩으로 효율적인 개발환경 구성하기 2 | 3 | 현대 프론트엔드 개발 환경에서는 소스 코드를 수정했을 때 브라우저에 즉시 반영되거나, 에러가 발생했을 때 원본 소스 코드를 쉽게 확인할 수 있어요. 또, 빌드 환경에 따라 환경 변수를 사용해 빌드 결과물을 다르게 만들 수도 있어요. 4 | 5 | 웹팩은 단순한 코드 번들러를 넘어, 이렇게 개발 편의성을 높이는 다양한 기능을 제공해요. 예를 들어 HMR(Hot Module Replacement)로 코드 변경 사항을 브라우저에 즉시 반영하거나, 소스 맵(Source Map)을 사용해 빌드된 코드에서도 원본 소스를 쉽게 추적할 수 있어요. 6 | 7 | ## 실시간으로 코드를 반영하는 개발 서버 8 | 9 | 브라우저에서는 기본적으로 HTTP 요청을 통해서만 에셋(이미지, JavaScript 파일 등)을 가져올 수 있어요. 그래서 로컬에서 작업 중인 JavaScript 코드도 반드시 `localhost`로 요청을 보내야 사용할 수 있죠. 이를 위해 작업 중인 파일을 제공할 웹 서버가 필요해요. 10 | 11 | 웹팩은 기본적으로 에셋을 제공(서빙)하는 개발 서버를 제공해요. 리액트 프로젝트를 `Create React App(CRA)`나 `Vite`로 생성하면, `start` 스크립트를 실행해 바로 개발 환경을 구성할 수 있는 것도 이 개발 서버 덕분이에요. 이 역할을 담당하는 것이 웹팩의 개발 서버(`webpack-dev-server`)예요. 12 | 13 | `webpack-dev-server`는 다음과 같은 기능을 제공해요. 14 | 15 | - **에셋 서빙**: 웹팩으로 번들링된 파일을 브라우저에 제공해요. 16 | - **HMR(Hot Module Replacement)**: 소스 코드가 수정되면 브라우저를 새로고침하지 않아도 변경 사항을 실시간으로 반영해요. 17 | - **프록시 설정**: 특정 API 요청을 다른 서버로 전달하거나 응답을 조작할 수 있어요. 18 | 19 | ## 디버깅을 쉽게 만드는 소스맵 20 | 21 | 소스맵은 다음과 같은 기능을 제공해요. 22 | 23 | - **디버깅 편의성**: 브라우저 개발자 도구에서 원본 코드를 기준으로 중단점(Breakpoint)을 설정할 수 있어요. 24 | - **에러 위치 확인**: 에러가 발생했을 때 난독화된 번들 코드가 아니라, 원본 코드의 에러 위치를 바로 확인할 수 있어요. 25 | 26 | 소스맵이 있으면 브라우저에서 디버깅할 때도 원본 소스를 기준으로 중단점을 설정할 수 있어요. 소스맵이 없다면 난독화된 코드를 보면서 디버깅해야 하므로 개발이 매우 어려워져요. 27 | 28 | ## 환경별로 동작을 설정하는 환경 변수 29 | 30 | 환경 변수는 애플리케이션이 실행될 환경(개발, 스테이징, 운영)에 따라 동작을 달리하기 위해 사용돼요. 31 | 32 | 환경 변수는 다음과 같은 기능을 제공해요. 33 | 34 | - **환경 맞춤 설정**: 개발 환경에서는 로컬 서버를, 운영 환경에서는 배포된 서버를 사용하도록 쉽게 전환할 수 있어요. 35 | - **성능 최적화**: 런타임 중 환경 변수 로딩 없이 미리 치환된 값을 사용하므로 코드 크기를 줄이고 실행 성능을 높일 수 있어요. 36 | -------------------------------------------------------------------------------- /fundamentals/bundling/deep-dive/overview.md: -------------------------------------------------------------------------------- 1 | # 소개 2 | 3 | 튜토리얼을 통해 번들러의 기본 사용법을 익히셨다면, 이제는 한 단계 더 깊이 들어가볼 차례예요. 4 | 5 | 6 | 심화 학습에서는 단순히 도구를 사용하는 것을 넘어서, 번들러가 내부적으로 어떻게 작동하는지, 구성 방식과 설정이 실제 개발 효율과 성능에 어떤 영향을 주는지 함께 살펴볼게요. 7 | 8 | ## 번들러 동작 이해하기 9 | 10 | 번들러가 어떤 과정을 거쳐 작동하는지 단계별로 살펴보며, 각 단계가 어떤 문제를 해결하는지 함께 살펴봐요. 11 | 12 | 1. [진입점](../deep-dive/bundling-process/entry): 번들링이 시작되는 파일이에요. 여기서부터 모든 의존성을 따라가며 필요한 자원을 수집해요. 13 | 14 | 2. [경로 탐색](../deep-dive/bundling-process/resolution): `import`나 `require` 문을 보고, 실제 어떤 파일을 불러올지 결정하는 과정이에요. 이 흐름을 이해하면 경로 오류를 줄이고 빌드 속도도 높일 수 있어요. 15 | 16 | 3. [로더](../deep-dive/bundling-process/loader): 다양한 파일 형식(JS, CSS, 이미지 등)을 번들러가 이해할 수 있는 코드로 바꿔줘요. 17 | 18 | 4. [플러그인](../deep-dive/bundling-process/plugin): 빌드 과정을 확장하거나 제어하고 싶을 때 사용하는 도구예요. 예를 들어, HTML 파일을 자동으로 생성하거나, 환경 변수 주입처럼 전체 빌드 흐름을 바꿔줄 수 있어요. 19 | 20 | 5. [출력](../deep-dive/bundling-process/output): 최종 번들이 어떤 이름과 경로로 저장될지 결정하는 부분이에요. 여러 개의 파일로 나뉘거나, 해시가 붙는 등의 설정도 여기서 할 수 있어요. 21 | 22 | ## 개발 환경 구성하기 23 | 24 | 번들러는 개발 속도와 편의성을 높이기 위해 다양한 기능들을 제공해요.
25 | 각각의 설정을 활용하면 디버깅도 쉬워지고, 더 빠르고 안정적인 개발을 할 수 있어요. 26 | 27 | 1. [개발 서버](../deep-dive/dev/dev-server): 코드 변경 사항을 실시간으로 반영해서 개발 속도를 높일 수 있어요. 개발 중 페이지를 새로고침하지 않아도 최신 상태를 유지할 수 있어요. 28 | 29 | 2. [HMR(Hot Module Replacement)](../deep-dive/dev/hmr): 전체 페이지를 새로고침하지 않고, 수정된 모듈만 빠르게 업데이트해서 상태가 초기화되지 않고 작업을 지속할 수 있어요. 30 | 31 | 3. [소스맵(Source Map)](../deep-dive/dev/source-map): 코드와 빌드된 파일 간의 매핑 정보를 제공해서, 브라우저 디버거에서 원본 코드를 보며 디버깅할 수 있어요. 32 | 33 | ## 번들 최적화 전략 34 | 35 | 애플리케이션 성능은 사용자 경험에 직접적인 영향을 미쳐요.
36 | 최적화 기능을 잘 활용하면 더 빠르고 효율적인 애플리케이션을 만들 수 있어요. 37 | 38 | 1. [코드 스플리팅](../deep-dive/optimization/code-splitting): 하나의 큰 번들을 여러 개의 작은 번들로 나누는 기법이에요. 이를 통해 초기 로딩 속도를 줄이고 필요에 따라 추가 번들을 불러올 수 있어요. 39 | 40 | 2. [트리셰이킹](../deep-dive/optimization/tree-shaking): 실제로 사용하지 않는 코드를 분석하고 제거하여 번들 크기를 줄여요. 41 | 42 | 3. [번들 분석](../deep-dive/optimization/code-splitting): 번들 결과물을 시각적으로 분석하여 어떤 모듈이 용량을 많이 차지하는지 파악하고 개선 방향을 찾아요. 43 | 44 | -------------------------------------------------------------------------------- /fundamentals/bundling/files/example.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/files/example.zip -------------------------------------------------------------------------------- /fundamentals/bundling/get-started.md: -------------------------------------------------------------------------------- 1 | # 시작하기 2 | 3 | 프론트엔드 서비스를 만들 때, 개발 서버를 띄우고 프로덕션 빌드를 준비하는 모든 과정에서 번들링(Bundling) 은 빠질 수 없어요. 4 | 작은 프로젝트부터 대형 서비스까지, 파일을 효율적으로 묶고 최적화하는 과정은 웹 성능과 직결되기 때문이에요. 5 | 6 | 어느 정도 프론트엔드 개발 경험이 쌓였다면 번들링이 “어디서 어떻게” 일어나는지 이해하는 것이 프로젝트 완성도를 높이는 데 큰 도움이 돼요. 7 | 8 | **Bundling Fundamentals**는 복잡하고 어렵게 느껴지는 번들링을 쉽게 이해할 수 있게 설명해요. 번들링이란 무엇인지부터, 라이브러리 번들링 설정 튜토리얼, 그리고 대표적인 도구인 웹팩(Webpack)의 동작 원리와 최적화 방법까지, 하나씩 탄탄하게 이해할 수 있도록 도와줄 거예요. 9 | 10 | ## 이런 분들에게 추천해요 11 | 12 | - 🤔 번들링이 왜 필요한지 막연하게만 알고 있는 개발자 13 | - 🔍 번들링 도구(특히 Webpack)의 동작 원리를 정확히 이해하고 싶은 개발자 14 | - 🧰 번들 크기나 성능 이슈가 생겼을 때 뭘 고쳐야 할지 막막했던 경험이 있는 개발자 15 | - 🧱 라이브러리나 컴포넌트를 직접 만들며 번들 설정을 손봐야 했던 개발자 16 | - 🧠 "빌드만 잘 되면 됐지"에서 벗어나 번들링을 진짜 내 지식으로 만들고 싶은 개발자 17 | 18 | ## 저작자 19 | 20 | - [mycolki](https://github.com/mycolki) 21 | - [helloworld-hellohyeon](https://github.com/helloworld-hellohyeon) 22 | - [raon0211](https://github.com/raon0211) 23 | - [donghyeon](https://github.com/Kimbangg) 24 | - [milooy](https://github.com/milooy) 25 | - [jennybehan](https://github.com/jennybehan) -------------------------------------------------------------------------------- /fundamentals/bundling/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Bundling Fundamentals" 7 | tagline: "프론트엔드 번들링의 모든 것" 8 | image: 9 | loading: eager 10 | fetchpriority: high 11 | decoding: async 12 | src: /images/bf-symbol.webp 13 | alt: Frontend Fundamentals symbol 14 | actions: 15 | - text: 시작하기 16 | link: /get-started 17 | - theme: alt 18 | text: 번들링이란 19 | link: /overview 20 | 21 | features: 22 | - icon: 📦 23 | title: 번들링, 왜 필요할까요? 24 | details: 번들링이 무엇이고 왜 중요한지 쉽게 알려드려요. 25 | link: /overview 26 | - icon: 🚀 27 | title: 웹팩으로 실습해봐요 28 | details: 웹팩을 직접 따라 하며 번들링 과정을 익혀봐요. 29 | link: /webpack-tutorial/intro 30 | - icon: 🔍 31 | title: 더 깊이 배우고 싶다면 32 | details: 웹팩의 핵심 개념과 고급 기법까지 단계별로 배워봐요. 33 | link: /deep-dive/bundling-process/overview 34 | --- 35 | -------------------------------------------------------------------------------- /fundamentals/bundling/overview.md: -------------------------------------------------------------------------------- 1 | # 번들링이란 2 | 3 | 번들링(Bundling)은 여러 개의 파일(특히 JavaScript, CSS, 이미지 등 웹 개발에 필요한 리소스 파일들)을 하나 또는 몇 개의 파일로 묶는 작업이에요. 이렇게 묶인 파일을 번들(Bundle)이라고 불러요. 4 | 5 | ![](/images/bundler.png) 6 | 7 | 8 | 웹 애플리케이션을 만들다 보면 JavaScript 파일이 수십, 수백 개로 쪼개지게 돼요. 각각의 파일은 기능 단위로 잘게 나눠지기 때문에 개발하기는 편하지만, 브라우저가 서버로부터 파일의 수만큼 요청을 보내게 되어 네트워크 비용이 커지고 로딩 속도가 느려질 수 있어요. 9 | 10 | 번들링은 이 문제를 해결하기 위한 방법이에요. 11 | 12 | ## 번들링의 목적 13 | 14 | 1. **요청 수 감소**: 수십, 수백개 파일을 하나로 묶어서 브라우저가 요청해야 할 파일 수를 줄여 로딩 속도를 향상시켜요. 15 | 2. **캐싱 최적화**: 묶인 파일 하나만 캐시하면 되어 효율적이에요. 16 | 3. **유지보수성과 배포 효율성**: 개발할 때는 모듈화를 유지하면서, 배포할 때는 성능 최적화를 할 수 있어요. 17 | 18 | 19 | ## 번들링 과정 한눈에 보기 20 | 21 | 간단한 예시로 번들링의 흐름을 이해해볼게요. 22 | 23 | ### 1. 여러 개의 JavaScript 파일이 있어요 24 | 25 | 개발할 때는 기능별로 파일을 잘게 나눠서 관리해요. 26 | 27 | ``` 28 | ├── index.js 29 | ├── utils.js 30 | ├── auth.js 31 | └── dashboard.js 32 | ``` 33 | 34 | ### 2. 파일들이 서로 의존하고 있어요 35 | 36 | 예를 들면, `index.js`가 `auth.js`와 `dashboard.js`를 불러오고, `auth.js`는 `utils.js`를 사용하고 있을 수 있어요. 37 | 즉, 파일 간에 의존성(dependency) 관계가 생겨요. 38 | 39 | ### 3. 번들러가 파일 관계를 분석해요 40 | 41 | 번들러는 프로젝트 안의 파일들을 스캔하면서 누가 누구를 쓰는지 분석해요. 42 | 시작 지점(예: `index.js`)부터 출발해서 모든 필요한 파일을 찾고, 의존성 그래프를 그려요. 43 | 44 | ### 4. 하나의 파일로 묶어요 (Bundling) 45 | 46 | 필요한 파일들을 의존성 순서에 맞춰서 하나의 파일로 합쳐요. 47 | 48 | ``` 49 | └── bundle.js 50 | ``` 51 | 52 | ### 5. 번들러가 추가 최적화 작업도 해줘요 53 | - 사용되지 않는 코드는 제거해요. ([트리 셰이킹](/deep-dive/optimization/tree-shaking.md)) 54 | - 필요한 경우, 여러 개의 작은 번들로 나누기도 해요. ([코드 스플리팅](/deep-dive/optimization/code-splitting.md)) 55 | - 코드에서 공백, 주석을 없애서 크기를 줄여요. (Minification) 56 | 57 | ### 6. 최종 결과물을 배포해요 58 | 59 | 최적화된 `bundle.js` 파일을 서버에 올리고, 사용자는 브라우저로 빠르게 접근할 수 있어요. 60 | 61 | ## 다음 단계 62 | 63 | 위의 예시처럼, 현대 번들링 도구들은 단순히 여러 파일을 하나로 합치는 것에 그치지 않고, 코드를 더 빠르게 동작하도록 압축하거나, 사용하지 않는 코드를 제거하는 등 다양한 최적화도 자동으로 해줘요. 64 | 65 | 번들러의 다양한 기능이 궁금하다면 [번들러란](/bundler) 문서를 확인해 보세요. 66 | -------------------------------------------------------------------------------- /fundamentals/bundling/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@frontend-fundamentals/bundling", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "docs:dev": "vitepress dev", 8 | "docs:build": "vitepress build", 9 | "docs:preview": "vitepress preview" 10 | }, 11 | "dependencies": { 12 | "@amplitude/analytics-browser": "^2.11.11", 13 | "markdown-it-footnote": "^4.0.0", 14 | "typescript": "^5.6.3", 15 | "vitepress": "^1.4.1", 16 | "vitepress-plugin-tabs": "^0.7.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/bf-symbol.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/bf-symbol.webp -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/browser-thinking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/browser-thinking.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/build-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/build-result.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/bundle-analyzer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/bundle-analyzer.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/bundle-dev-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/bundle-dev-server.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/bundle-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/bundle-img.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/bundler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/bundler.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/bundling/array-entry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/bundling/array-entry.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/bundling/depend-on-before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/bundling/depend-on-before.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/bundling/depend-on-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/bundling/depend-on-example.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/bundling/depend-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/bundling/depend-on.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/bundling/dependency-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/bundling/dependency-graph.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/bundling/module-resolution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/bundling/module-resolution.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/bundling/multiple-entry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/bundling/multiple-entry.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/bundling/single-entry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/bundling/single-entry.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/code-insert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/code-insert.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/emoji-of-the-day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/emoji-of-the-day.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/entry_object-dependon-shared-after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/entry_object-dependon-shared-after.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/entry_object-dependon-shared-before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/entry_object-dependon-shared-before.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/entry_object-network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/entry_object-network.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/entry_single-network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/entry_single-network.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/esm-dev-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/esm-dev-server.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/favicon.ico -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/hmr-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/hmr-1.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/hmr-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/hmr-2.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/mode-compare1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/mode-compare1.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/mode-compare2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/mode-compare2.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/network-minified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/network-minified.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/project-reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/project-reset.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/react-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/react-app.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/resolve_compile-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/resolve_compile-error.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/source-map-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/source-map-1.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/source-map-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/source-map-2.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/source-map-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/source-map-3.png -------------------------------------------------------------------------------- /fundamentals/bundling/public/images/style-less.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/bundling/public/images/style-less.png -------------------------------------------------------------------------------- /fundamentals/bundling/rollup-tutorial/intro.md: -------------------------------------------------------------------------------- 1 | # 튜토리얼 소개 2 | 3 | 롤업(rollup)은 라이브러리 제작에 최적화된 번들러예요. 4 | 이 튜토리얼에서는 간단한 예제 라이브러리를 만들고 npm으로 배포하는 과정을 살펴볼거예요. 5 | 6 | 곧 출시됩니다. 기다려주세요! -------------------------------------------------------------------------------- /fundamentals/bundling/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist", 6 | "composite": true 7 | }, 8 | "include": [ 9 | ".vitepress/**/*", 10 | "**/*.ts", 11 | "**/*.tsx", 12 | "**/*.vue" 13 | ] 14 | } -------------------------------------------------------------------------------- /fundamentals/bundling/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/index.html" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /fundamentals/bundling/webpack-tutorial/dev-server.md: -------------------------------------------------------------------------------- 1 | # 개발 서버로 생산성 높이기 2 | 3 | 지금까지는 코드를 수정할 때마다 매번 `npm run build`를 돌리고, 브라우저 새로고침을 직접 눌러야 했죠. 4 | 5 | 이제는 그럴 필요 없어요. **웹팩 개발 서버**를 도입하면 코드를 저장하는 순간 자동으로 브라우저에 반영돼요. 6 | 게다가 화면이 리로드되지 않아도 컴포넌트가 실시간으로 바뀌는 `Hot Module Replacement` 기능까지 쓸 수 있어요. 7 | 8 | ## 웹팩 개발 서버(`webpack-dev-server`) 9 | 10 | 웹팩 개발 서저는 우리가 작성한 코드를 메모리에 번들링해서, 브라우저에 빠르게 반영해주는 가상의 개발용 서버로, 다음과 같은 기능을 제공해요. 11 | 12 | - 코드 저장시 자동 새로고침 13 | - 스타일이나 컴포넌트 수정 시 상태를 유지하면서 실시간 반영(Hot Module Replacement) 14 | - 브라우저에서 에러 메시지를 오버레이로 띄워주기 15 | - API 서버와 연동 시 프록시 설정 가능 16 | 17 | ## 1. 개발 서버 설치하기 18 | 19 | 먼저 다음 명령어로 개발 서버 패키지를 설치할게요. 20 | 21 | ```bash 22 | npm install --save-dev webpack-dev-server 23 | ``` 24 | 25 | ## 2. 웹팩 설정에 devServer 추가하기 26 | 27 | 다음으로 `webpack.config.js` 파일에 개발 서버 설정을 추가해 주세요. 28 | 29 | ```js{5-16} 30 | const path = require("path"); 31 | 32 | module.exports = { 33 | // ... 기존 설정 34 | devServer: { 35 | static: { 36 | directory: path.join(__dirname, "dist") // 빌드된 파일을 이 경로에서 서빙해요 37 | }, 38 | port: 3000, // localhost:3000에서 실행 39 | open: true, // 서버 실행 시 브라우저 자동 열기 40 | hot: true, // HMR 사용 41 | historyApiFallback: true, // SPA 라우팅 지원 42 | client: { 43 | overlay: true // 에러 발생 시 브라우저에 띄워줘요 44 | } 45 | } 46 | }; 47 | ``` 48 | 49 | ## 3. package.json에 실행 스크립트 추가하기 50 | 51 | `npm start`로 바로 실행할 수 있도록 `package.json`에 스크립트를 추가해요. 52 | 53 | ```json 54 | { 55 | "scripts": { 56 | "start": "webpack serve --mode development", 57 | "build": "webpack --mode production" 58 | } 59 | } 60 | ``` 61 | 62 | 이제 아래 명령어로 개발 서버를 실행해볼 수 있어요. 63 | 64 | ```bash 65 | npm start 66 | ``` 67 | 68 | 브라우저가 자동으로 열리고, 코드를 수정하면 바로바로 반영되는 걸 확인할 수 있어요. 69 | 70 | ## 정리 71 | 72 | 지금까지는 코드를 수정할 때마다 `npm run build`를 직접 실행하고, 브라우저를 새로고침해가며 웹팩의 동작을 직접 경험해봤죠. 73 | 74 | 개발 서버를 마지막에 소개한 건 의도적인 선택이었어요. 이 과정을 거쳤기 때문에, 이제 웹팩 개발 서버의 편리함이 훨씬 더 와닿을 거예요. 마치 수동 변속기로 운전을 배운 뒤 자동 변속기의 고마움을 느끼는 것처럼요. 😊 75 | 76 | --- 77 | 78 | 지금까지 웹팩의 핵심 개념부터 실전 적용까지 한 걸음씩 밟아왔어요. 단계마다 실제 '오늘의 이모지' 프로젝트를 개선하면서 웹팩이 어떤 역할을 하고, 어떤 문제를 해결해주는지 몸으로 익힐 수 있었을 거예요. 79 | 80 | 이제 여러분은 웹팩을 단순한 설정 도구가 아니라 프로젝트를 더 잘 구조화하고 유지보수하기 위한 도구로 활용할 수 있을 거예요. 81 | 82 | 앞으로 새로운 프로젝트를 시작할 때 자신만의 웹팩 설정을 만들어보세요. 이 튜토리얼이 그 출발점이 되어주길 바래요! -------------------------------------------------------------------------------- /fundamentals/bundling/webpack-tutorial/example-project/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 😊 4 | -------------------------------------------------------------------------------- /fundamentals/bundling/webpack-tutorial/example-project/emoji.js: -------------------------------------------------------------------------------- 1 | const emojis = [ 2 | { icon: '😊', name: 'Smiling Face' }, 3 | { icon: '🚀', name: 'Rocket' }, 4 | { icon: '🍕', name: 'Pizza' }, 5 | { icon: '🐱', name: 'Cat' }, 6 | { icon: '🌈', name: 'Rainbow' }, 7 | { icon: '🎸', name: 'Guitar' } 8 | ]; -------------------------------------------------------------------------------- /fundamentals/bundling/webpack-tutorial/example-project/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Emoji of the Day 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |

Emoji of the Day

17 |
18 |
19 |
😊
20 |
Today's Emoji
21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /fundamentals/bundling/webpack-tutorial/example-project/main.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | const today = new Date(); 3 | const formattedDate = dateFns.format(today, 'MMMM d, yyyy'); 4 | document.getElementById('dateDisplay').textContent = formattedDate; 5 | 6 | showRandomEmoji(); 7 | }); 8 | 9 | function showRandomEmoji() { 10 | const randomIndex = Math.floor(Math.random() * emojis.length); 11 | const selectedEmoji = emojis[randomIndex]; 12 | 13 | document.getElementById('emojiDisplay').textContent = selectedEmoji.icon; 14 | document.getElementById('emojiName').textContent = selectedEmoji.name; 15 | } -------------------------------------------------------------------------------- /fundamentals/bundling/webpack-tutorial/example-project/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Inter', sans-serif; 3 | margin: 0; 4 | padding: 0; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | min-height: 100vh; 9 | background-color: #f5f5f5; 10 | } 11 | 12 | .container { 13 | background-color: white; 14 | border-radius: 10px; 15 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); 16 | padding: 2rem; 17 | text-align: center; 18 | max-width: 500px; 19 | width: 100%; 20 | } 21 | 22 | .logo { 23 | max-width: 100px; 24 | margin-bottom: 1rem; 25 | } 26 | 27 | h1 { 28 | color: #333; 29 | margin-bottom: 1rem; 30 | } 31 | 32 | .date-display { 33 | color: #666; 34 | margin-bottom: 2rem; 35 | } 36 | 37 | .emoji-container { 38 | margin: 2rem 0; 39 | } 40 | 41 | .emoji { 42 | font-size: 4rem; 43 | margin-bottom: 0.5rem; 44 | } 45 | 46 | .emoji-name { 47 | font-size: 1.2rem; 48 | color: #555; 49 | } -------------------------------------------------------------------------------- /fundamentals/bundling/webpack-tutorial/intro.md: -------------------------------------------------------------------------------- 1 | # 소개 2 | 3 | 이 튜토리얼에서는 외부 라이브러리, 이미지, 폰트, CSS 같은 다양한 자원을 웹팩(Webpack)으로 한데 묶어보면서, 웹 개발에서 번들링이 왜 중요한지 직접 체험해볼 수 있어요. 4 | 간단한 예제 프로젝트인 **오늘의 이모지**를 만들어가며, 웹팩의 기본 개념부터 실전 사용법까지 차근차근 익혀볼 거예요. 5 | 6 | 7 | ## 준비사항 8 | - 코드 에디터를 준비해요. 9 | - HTML과 JavaScript의 기본 개념만 알고 있으면 충분해요. 10 | 11 | ## 이런 순서로 진행돼요 12 | 13 | 1. **웹 프로젝트의 시작을 준비해요** 14 | - Node.js와 npm으로 프로젝트를 시작해요. 15 | - 웹팩을 설치하고 첫 번째 번들을 만들어요. 16 | 17 | 2. **모듈 시스템과 번들링을 이해해요** 18 | - npm으로 라이브러리를 설치하고 모듈로 불러오는 방법을 배워요. 19 | - 웹팩으로 모듈을 하나로 합치는 과정을 이해해요. 20 | 21 | 3. **현대 프론트엔드 개발 방식에 익숙해져요** 22 | - TypeScript로 타입 안전성을 높여요. 23 | - React로 컴포넌트 기반 개발을 해요. 24 | - CSS와 이미지, 폰트를 모듈처럼 다뤄요. 25 | 26 | 4. **개발 환경을 효율적으로 구성해요** 27 | - 플러그인으로 기능을 확장해요. 28 | - 개발 서버로 생산성을 높여요. 29 | 30 | ## 예제 프로젝트 세팅하기 31 | 1. [오늘의 이모지 예제 프로젝트 ZIP 파일](https://github.com/toss/frontend-fundamentals/blob/main/public/files/bundling-example-project.zip)를 다운로드하고 압축을 풀어주세요. 32 | 2. 압축을 풀면 이런 폴더 구조가 보여요. 33 | ``` 34 | example-project/ 35 | ├── index.html 36 | ├── style.css 37 | ├── main.js 38 | ├── emoji.js 39 | └── assets/ 40 | ├── logo.svg 41 | └── Inter-Regular.woff2 42 | ``` 43 | 3. `index.html` 파일을 브라우저에서 열어 예제 프로젝트를 확인해보세요. 44 | 45 | ![](/images/emoji-of-the-day.png) 46 | 47 | :::details HTML 파일을 어떻게 여나요? 48 | 파일 탐색기에서 `index.html` 파일을 더블클릭하거나 브라우저에 파일을 드래그 앤 드롭하면 열 수 있어요. 이 방식은 파일을 로컬에서 직접 불러오는 방식으로, 개발 초기 단계에서 간단히 결과를 확인하기 좋아요. 49 | 50 | 나중에 웹팩 개발 서버를 사용하게 되면 자동 새로고침, 모듈 핫 리로딩 등 개발 생산성을 높이는 기능들을 활용할 수 있게 되니, 기대해주세요. 51 | ::: 52 | 53 | ## 다음 단계 54 | 이제 프로젝트 환경을 설정하고 첫 번째 번들을 만들어 볼게요. 차근차근 따라오시면 어느새 웹 개발의 핵심 도구인 웹팩을 자유롭게 활용할 수 있게 될 거예요. 55 | -------------------------------------------------------------------------------- /fundamentals/bundling/webpack-tutorial/style.md: -------------------------------------------------------------------------------- 1 | # 스타일 관리하기 2 | 3 | 이번 단계에서는 '오늘의 이모지' 프로젝트의 스타일을 웹팩으로 관리하는 방법을 배워볼게요. 웹팩의 로더를 사용하면 CSS 파일도 JavaScript 모듈처럼 import해서 사용할 수 있어요. 4 | 5 | ## CSS를 모듈처럼 사용하기 6 | 7 | 기존에는 HTML 파일에서 `` 태그로 CSS를 불러왔어요. 하지만 웹팩을 사용하면 JavaScript에서 CSS를 import할 수 있어요. 이렇게 하면 CSS도 모듈처럼 관리할 수 있고, 필요한 스타일만 번들에 포함시킬 수 있어요. 8 | 9 | ```javascript 10 | // CSS를 import하면 웹팩이 알아서 처리해요 11 | import './style.css'; 12 | ``` 13 | 14 | ## 1. CSS 로더 설치하기 15 | 16 | 웹팩이 CSS를 처리할 수 있도록 필요한 로더들을 설치해볼게요. 17 | 18 | ```bash 19 | npm install --save-dev style-loader css-loader 20 | ``` 21 | 22 | - `css-loader`: CSS 파일을 JavaScript에서 import할 수 있는 형태로 바꿔줘요. 23 | - `style-loader`: 변환된 CSS를 브라우저 실행 시 ` 47 | -------------------------------------------------------------------------------- /fundamentals/code-quality/.vitepress/theme/components/Comments.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 37 | -------------------------------------------------------------------------------- /fundamentals/code-quality/.vitepress/theme/composables/index.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed } from "vue"; 2 | import { useData } from "vitepress"; 3 | 4 | export function useLocale() { 5 | const { lang } = useData(); 6 | 7 | // 한국어 여부 확인 8 | const isKorean = computed(() => { 9 | return lang.value === "ko-KR" || lang.value === "ko"; 10 | }); 11 | 12 | return { 13 | isKorean 14 | }; 15 | } 16 | 17 | export * from "./useBanner"; 18 | -------------------------------------------------------------------------------- /fundamentals/code-quality/.vitepress/theme/composables/useBanner.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed, onMounted } from "vue"; 2 | import { BANNER_DATA, type Banner } from "../data/bannerData"; 3 | 4 | export function useBanner() { 5 | const banners = ref(BANNER_DATA); 6 | 7 | const currentBannerIndex = ref(0); 8 | 9 | const currentBanner = computed(() => { 10 | return banners.value[currentBannerIndex.value]; 11 | }); 12 | 13 | const rotationInterval = 30000; 14 | 15 | const rotateBanner = () => { 16 | currentBannerIndex.value = 17 | (currentBannerIndex.value + 1) % banners.value.length; 18 | }; 19 | 20 | const getRandomBannerIndex = () => { 21 | return Math.floor(Math.random() * banners.value.length); 22 | }; 23 | 24 | onMounted(() => { 25 | // 초기에 랜덤한 배너 표시 26 | currentBannerIndex.value = getRandomBannerIndex(); 27 | 28 | // 일정 시간 간격으로 배너 로테이션 29 | const intervalId = setInterval(rotateBanner, rotationInterval); 30 | 31 | return () => { 32 | clearInterval(intervalId); 33 | }; 34 | }); 35 | 36 | const setBannerIndex = (index: number) => { 37 | if (index >= 0 && index < banners.value.length) { 38 | currentBannerIndex.value = index; 39 | } 40 | }; 41 | 42 | const trackBannerClick = (banner: Banner) => { 43 | console.log("Banner clicked:", banner.title); 44 | }; 45 | 46 | return { 47 | banners, 48 | currentBanner, 49 | currentBannerIndex, 50 | rotateBanner, 51 | setBannerIndex, 52 | trackBannerClick 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /fundamentals/code-quality/.vitepress/theme/composables/useDiscussionFilter.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed } from "vue"; 2 | import type { GithubDiscussion } from "../types/github"; 3 | import type { Ref } from "vue"; 4 | 5 | export function useDiscussionFilter(discussions: Ref) { 6 | const selectedCategory = ref(null); 7 | const selectedStatus = ref<"all" | "open" | "closed" | "popular">("popular"); 8 | 9 | const isPopular = (discussion: GithubDiscussion) => { 10 | return discussion.comments.totalCount >= 5 || discussion.upvotes >= 10; 11 | }; 12 | 13 | const categories = computed(() => { 14 | const categorySet = new Set(); 15 | discussions.value.forEach((discussion) => { 16 | categorySet.add(discussion.category.name); 17 | }); 18 | return Array.from(categorySet); 19 | }); 20 | 21 | const filteredDiscussions = computed(() => { 22 | let filtered = discussions.value; 23 | 24 | if (selectedCategory.value) { 25 | filtered = filtered.filter( 26 | (discussion) => discussion.category.name === selectedCategory.value 27 | ); 28 | } 29 | 30 | if (selectedStatus.value === "popular") { 31 | filtered = filtered.filter(isPopular); 32 | } 33 | 34 | if (selectedStatus.value !== "all") { 35 | filtered = filtered.filter( 36 | (discussion) => 37 | (selectedStatus.value === "closed") === discussion.closed 38 | ); 39 | } 40 | 41 | return filtered; 42 | }); 43 | 44 | const setCategory = (category: string | null) => { 45 | selectedCategory.value = category; 46 | }; 47 | 48 | const setStatus = (status: "all" | "open" | "closed" | "popular") => { 49 | selectedStatus.value = status; 50 | }; 51 | 52 | return { 53 | filteredDiscussions, 54 | categories, 55 | selectedCategory, 56 | selectedStatus, 57 | setCategory, 58 | setStatus 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /fundamentals/code-quality/.vitepress/theme/composables/useGithubDiscussions.ts: -------------------------------------------------------------------------------- 1 | import { ref, onMounted, computed } from "vue"; 2 | import type { GithubDiscussion } from "../types/github"; 3 | import { useGithubApi } from "./useGithubApi"; 4 | import { useDiscussionFilter } from "./useDiscussionFilter"; 5 | import { usePagination } from "./usePagination"; 6 | import { useSortableData } from "./useSortableData"; 7 | 8 | interface UseGithubDiscussionsOptions { 9 | perPage?: number; 10 | } 11 | 12 | export function useGithubDiscussions( 13 | owner: string, 14 | repo: string, 15 | options: UseGithubDiscussionsOptions = {} 16 | ) { 17 | const allDiscussions = ref([]); 18 | 19 | const { loading, error, fetchDiscussions } = useGithubApi({ 20 | owner, 21 | repo 22 | }); 23 | 24 | const { 25 | filteredDiscussions, 26 | categories, 27 | selectedCategory, 28 | selectedStatus, 29 | setCategory, 30 | setStatus 31 | } = useDiscussionFilter(allDiscussions); 32 | 33 | const { 34 | sortField, 35 | sortDirection, 36 | sortedData: sortedDiscussions, 37 | handleSort 38 | } = useSortableData( 39 | computed(() => filteredDiscussions.value), 40 | { 41 | defaultField: null, 42 | defaultDirection: "desc", 43 | sortFunctions: { 44 | upvotes: (a, b) => a.upvotes - b.upvotes 45 | } 46 | } 47 | ); 48 | 49 | const { 50 | currentPage, 51 | perPage, 52 | paginatedData: paginatedDiscussions, 53 | totalCount, 54 | setPage 55 | } = usePagination( 56 | computed(() => sortedDiscussions.value), 57 | options?.perPage || 20 58 | ); 59 | 60 | const fetchData = async () => { 61 | try { 62 | allDiscussions.value = await fetchDiscussions(); 63 | } catch (e) {} 64 | }; 65 | 66 | onMounted(() => { 67 | fetchData(); 68 | }); 69 | 70 | return { 71 | discussions: paginatedDiscussions, 72 | loading, 73 | error, 74 | totalCount, 75 | currentPage, 76 | categories, 77 | selectedCategory, 78 | selectedStatus, 79 | setPage, 80 | setCategory, 81 | setStatus, 82 | sortField, 83 | sortDirection, 84 | handleSort 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /fundamentals/code-quality/.vitepress/theme/composables/useLocale.ts: -------------------------------------------------------------------------------- 1 | import { useData } from "vitepress"; 2 | import { computed } from "vue"; 3 | 4 | export function useLocale() { 5 | const { lang } = useData(); 6 | 7 | const isKorean = computed(() => lang.value === "ko" || lang.value === "root"); 8 | 9 | return { 10 | isKorean 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /fundamentals/code-quality/.vitepress/theme/composables/usePagination.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed } from "vue"; 2 | import type { Ref } from "vue"; 3 | 4 | export function usePagination(data: Ref, itemsPerPage: number) { 5 | const currentPage = ref(1); 6 | const perPage = ref(itemsPerPage); 7 | 8 | const paginatedData = computed(() => { 9 | const start = (currentPage.value - 1) * perPage.value; 10 | const end = start + perPage.value; 11 | return data.value.slice(start, end); 12 | }); 13 | 14 | const totalCount = computed(() => data.value.length); 15 | 16 | const setPage = (page: number) => { 17 | currentPage.value = page; 18 | }; 19 | 20 | return { 21 | currentPage, 22 | perPage, 23 | paginatedData, 24 | totalCount, 25 | setPage 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /fundamentals/code-quality/.vitepress/theme/composables/useSortableData.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed, type ComputedRef } from "vue"; 2 | 3 | export type SortDirection = "asc" | "desc"; 4 | 5 | export interface SortableOptions { 6 | defaultField?: string | null; 7 | defaultDirection?: SortDirection; 8 | sortFunctions?: { 9 | [K in keyof T]?: (a: T, b: T) => number; 10 | }; 11 | } 12 | 13 | export function useSortableData( 14 | data: ComputedRef, 15 | options: SortableOptions = {} 16 | ) { 17 | const sortField = ref(options.defaultField ?? null); 18 | const sortDirection = ref(options.defaultDirection ?? "desc"); 19 | 20 | const sortedData = computed(() => { 21 | if (!data.value || !sortField.value) return data.value; 22 | 23 | return [...data.value].sort((a, b) => { 24 | const multiplier = sortDirection.value === "asc" ? 1 : -1; 25 | 26 | const sortFn = options.sortFunctions?.[sortField.value as keyof T]; 27 | if (sortFn) { 28 | return sortFn(a, b) * multiplier; 29 | } 30 | 31 | const aValue = a[sortField.value as keyof T]; 32 | const bValue = b[sortField.value as keyof T]; 33 | 34 | if (typeof aValue === "number" && typeof bValue === "number") { 35 | return (aValue - bValue) * multiplier; 36 | } 37 | 38 | return 0; 39 | }); 40 | }); 41 | 42 | const handleSort = (field: string) => { 43 | if (sortField.value === field) { 44 | sortDirection.value = sortDirection.value === "asc" ? "desc" : "asc"; 45 | } else { 46 | sortField.value = field; 47 | sortDirection.value = "desc"; 48 | } 49 | }; 50 | 51 | return { 52 | sortField, 53 | sortDirection, 54 | sortedData, 55 | handleSort 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /fundamentals/code-quality/.vitepress/theme/custom.css: -------------------------------------------------------------------------------- 1 | html.dark .light-only { 2 | display: none !important; 3 | } 4 | 5 | html:not(.dark) .dark-only { 6 | display: none !important; 7 | } 8 | 9 | :root { 10 | --vp-font-family-base: "Toss Product Sans", ui-sans-serif, system-ui, 11 | sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", 12 | "Noto Color Emoji"; 13 | --nav-width: 50px; 14 | --nav-height-mobile: 56px; 15 | } 16 | 17 | :root[lang="ko"] { 18 | word-break: keep-all; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /fundamentals/code-quality/.vitepress/theme/data/bannerData.ts: -------------------------------------------------------------------------------- 1 | export interface Banner { 2 | title: string; 3 | description: string; 4 | link: string; 5 | } 6 | 7 | export const BANNER_DATA: Banner[] = [ 8 | { 9 | title: "🎙️ 조건부 렌더링, 어떻게 처리하시나요?", 10 | description: 11 | "논리 연산자(&&)와 삼항 연산자(?:)를 활용하는 전통적인 방식, 혹은 같은 선언적인 컴포넌트를 사용하는 방식. 어떤 접근법이 효과적일까요?", 12 | link: "https://github.com/toss/frontend-fundamentals/discussions/4" 13 | }, 14 | { 15 | title: 16 | "[🏟️ 3월 콜로세움] 처음 커리어 시작은 대기업에서 vs 스타트업에서 어디가 좋을까?", 17 | description: 18 | "UpVote을 많이 받은 코멘트 작성자에게는 Frontend Fundamentals 굿즈를 보내드려요.", 19 | link: "https://github.com/toss/frontend-fundamentals/discussions/172" 20 | }, 21 | { 22 | title: "토스에서 Desktop Frontend Engineer를 모시고 있어요.", 23 | description: 24 | "수많은 기능을 어떻게 우아한 코드로 구현할 수 있을까요? 함께 개발해요!", 25 | link: "https://toss.im/career/job-detail?job_id=4664498003" 26 | }, 27 | { 28 | title: "🎙️ if문의 return이 간단한 한 줄이라면 어떻게 사용하시나요?", 29 | description: 30 | "간단한 조건문에서 return을 한 줄로 작성할지, 중괄호 {}를 사용할지 고민되시나요? 가독성, 코드 일관성, 유지보수성, Diff 최소화 등의 측면에서 다양한 의견이 오갔습니다.", 31 | link: "https://github.com/toss/frontend-fundamentals/discussions/41" 32 | } 33 | ]; 34 | -------------------------------------------------------------------------------- /fundamentals/code-quality/.vitepress/theme/hooks/useGiscusTheme.tsx: -------------------------------------------------------------------------------- 1 | import { ref, watch, computed, onMounted } from "vue"; 2 | import { useData } from "vitepress"; 3 | import { GISCUS_ORIGIN, GISCUS_THEME, sendGiscusMessage } from "../utils"; 4 | 5 | export function useGiscusTheme() { 6 | const { isDark } = useData(); 7 | const isIframeLoaded = ref(false); 8 | 9 | const syncTheme = () => { 10 | sendGiscusMessage({ 11 | setConfig: { 12 | theme: isDark.value ? GISCUS_THEME.dark : GISCUS_THEME.light 13 | } 14 | }); 15 | }; 16 | 17 | const setupThemeHandler = () => { 18 | window.addEventListener("message", (event) => { 19 | if (event.origin === GISCUS_ORIGIN && event.data?.giscus != null) { 20 | isIframeLoaded.value = true; 21 | syncTheme(); 22 | } 23 | }); 24 | }; 25 | 26 | onMounted(() => { 27 | setupThemeHandler(); 28 | }); 29 | 30 | watch(isDark, () => { 31 | if (isIframeLoaded.value) { 32 | syncTheme(); 33 | } 34 | }); 35 | 36 | return { 37 | theme: computed(() => 38 | isDark.value ? GISCUS_THEME.dark : GISCUS_THEME.light 39 | ) 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /fundamentals/code-quality/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from "vitepress/theme"; 2 | import Layout from "./Layout.vue"; 3 | import * as amplitude from "@amplitude/analytics-browser"; 4 | import "./custom.css"; 5 | import GithubDiscussions from "./components/GithubDiscussions.vue"; 6 | import GithubDiscussionsDetail from "./components/GithubDiscussionsDetail.vue"; 7 | 8 | export default { 9 | extends: DefaultTheme, 10 | Layout, 11 | async enhanceApp({ app }) { 12 | if (typeof window !== "undefined") { 13 | const amplitudeApiKey = (import.meta as any).env.VITE_AMPLITUDE_API_KEY; 14 | amplitude.init(amplitudeApiKey, { autocapture: true }); 15 | } 16 | app.component("GithubDiscussions", GithubDiscussions); 17 | app.component("GithubDiscussionsDetail", GithubDiscussionsDetail); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /fundamentals/code-quality/.vitepress/theme/types/github.ts: -------------------------------------------------------------------------------- 1 | export interface GithubDiscussion { 2 | id: string; 3 | title: string; 4 | url: string; 5 | upvotes: number; 6 | author: { 7 | login: string; 8 | url: string; 9 | }; 10 | createdAt: string; 11 | category: { 12 | name: string; 13 | emoji: string; 14 | }; 15 | comments: { 16 | totalCount: number; 17 | }; 18 | closed: boolean; 19 | closedAt: string | null; 20 | } 21 | 22 | export interface GithubDiscussionsResponse { 23 | repository: { 24 | discussions: { 25 | nodes: GithubDiscussion[]; 26 | }; 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /fundamentals/code-quality/.vitepress/theme/utils/emoji.ts: -------------------------------------------------------------------------------- 1 | const emojiMap: Record = { 2 | ":earth_americas:": "🌎", 3 | ":speech_balloon:": "💬", 4 | ":nose:": "👃", 5 | ":thinking:": "🤔", 6 | ":announce:": "📣" 7 | }; 8 | 9 | export function convertGithubEmoji(text: string): string { 10 | if (!text) return ""; 11 | 12 | return text.replace(/:([\w_+-]+):/g, (match) => { 13 | return emojiMap[match] || match; 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /fundamentals/code-quality/.vitepress/theme/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const GISCUS_ORIGIN = "https://giscus.app" as const; 2 | 3 | export const GISCUS_LANG_MAP = { 4 | ko: "ko", 5 | en: "en", 6 | ja: "ja", 7 | "zh-hans": "zh-CN" 8 | } as const; 9 | 10 | export const GISCUS_THEME = { 11 | light: "light_tritanopia", 12 | dark: "dark_tritanopia" 13 | }; 14 | 15 | export function getGiscusLang(lang) { 16 | return GISCUS_LANG_MAP[lang] || "en"; 17 | } 18 | 19 | export function sendGiscusMessage(message: T) { 20 | const iframe = document.querySelector( 21 | "iframe.giscus-frame" 22 | ); 23 | if (!iframe) return; 24 | 25 | iframe.contentWindow?.postMessage({ giscus: message }, GISCUS_ORIGIN); 26 | } 27 | -------------------------------------------------------------------------------- /fundamentals/code-quality/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /fundamentals/code-quality/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": "explicit" 4 | }, 5 | "editor.formatOnSave": true, 6 | "eslint.nodePath": "../../.yarn/sdks", 7 | "prettier.prettierPath": "../../.yarn/sdks/prettier/index.cjs", 8 | "typescript.tsdk": "../../.yarn/sdks/typescript/lib", 9 | "typescript.enablePromptUseWorkspaceTsdk": true, 10 | "search.exclude": { 11 | "**/.yarn": true, 12 | "**/.pnp.*": true, 13 | "**/.next": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /fundamentals/code-quality/code/coming-soon.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: doc 3 | title: Bundling 4 | description: Frontend Bundling Guide (Coming Soon) 5 | --- 6 | 7 |
8 |

✨ 번들링, 접근성 문서가 곧 출시되어요

9 |

조금만 기다려주세요! 여러분을 위한 멋진 콘텐츠를 준비 중입니다!

10 |
11 | 12 | -------------------------------------------------------------------------------- /fundamentals/code-quality/code/community.md: -------------------------------------------------------------------------------- 1 | --- 2 | comments: false 3 | --- 4 | 5 | # 함께 만들기 6 | 7 | `Frontend Fundamentals`(FF)는 커뮤니티와 함께 좋은 코드의 기준을 만들어 가고 있어요. 8 | 9 | 지금은 토스 프론트엔드 챕터가 운영하고 있어요. 10 | 11 | 12 | 13 | ## 🙋 고민되는 코드에 대해 논의하기 14 | 15 | 고민되는 코드가 있다면 깃허브 디스커션에 글을 올려 보세요. 16 | 내 코드에 대해서 커뮤니티에서 다각도로 리뷰를 받을 수 있고, 좋은 코드의 기준에 대해 커뮤니티와 함께 고민할 수 있어요. 17 | 18 | 많은 공감을 받은 사례는 직접 Frontend Fundamentals 문서에 올릴 수 있어요. 기여 방법은 추후 공개될 예정이에요. 19 | 20 | - [깃허브 디스커션에 글 올리기](https://github.com/toss/frontend-fundamentals/discussions) 21 | 22 | ## 🗣️ 좋은 코드의 기준에 의견 더하기 23 | 24 | 좋은 코드의 기준에 대해 의견이 있거나, 새로운 의견을 더하고 싶다면 더 좋은 코드가 어떤 코드인지 투표하고, 의견을 남겨 보세요. 25 | 커뮤니티와 소통하며 더욱 풍부하고 깊이 있는 기준을 만들어 나가요. 26 | 27 | 이 코드가 좋을까? 저 코드가 좋을까? 에 대해서 나만의 기준을 확립하는 계기가 될 수 있어요. 28 | 29 | - [A vs B에 올라온 코드 보기](https://github.com/toss/frontend-fundamentals/discussions/categories/a-vs-b) 30 | 31 | ## 🏆 명예의 전당 32 | 33 | 커뮤니티에서 있었던 좋은 토론을 살펴보세요. Frontend Fundamentals 문서에 나오는 내용을 넘어, 좋은 코드에 대한 생각을 넓힐 수 있어요. 34 | 35 | - [명예의 전당](/code/community/good-discussions) -------------------------------------------------------------------------------- /fundamentals/code-quality/code/detail/index.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fundamentals/code-quality/code/dicussions/index.md: -------------------------------------------------------------------------------- 1 | # 토론하기 2 | 3 | 4 | -------------------------------------------------------------------------------- /fundamentals/code-quality/code/examples/code-directory.md: -------------------------------------------------------------------------------- 1 | # 함께 수정되는 파일을 같은 디렉토리에 두기 2 | 3 |
4 | 5 |
6 | 7 | 프로젝트에서 코드를 작성하다 보면 Hook, 컴포넌트, 유틸리티 함수 등을 여러 파일로 나누어서 관리하게 돼요. 이런 파일들을 쉽게 만들고, 찾고, 삭제할 수 있도록 올바른 디렉토리 구조를 갖추는 것이 중요해요. 8 | 9 | 함께 수정되는 소스 파일을 하나의 디렉토리에 배치하면 코드의 의존 관계를 명확하게 드러낼 수 있어요. 그래서 참조하면 안 되는 파일을 함부로 참조하는 것을 막고, 연관된 파일들을 한 번에 삭제할 수 있어요. 10 | 11 | ## 📝 코드 예시 12 | 13 | 다음 코드는 프로젝트의 모든 파일을 모듈의 종류(Presentational 컴포넌트, Container 컴포넌트, Hook, 상수 등)에 따라 분류한 디렉토리 구조예요. 14 | 15 | ```text 16 | └─ src 17 | ├─ components 18 | ├─ constants 19 | ├─ containers 20 | ├─ contexts 21 | ├─ remotes 22 | ├─ hooks 23 | ├─ utils 24 | └─ ... 25 | ``` 26 | 27 | ## 👃 코드 냄새 맡아보기 28 | 29 | ### 응집도 30 | 31 | 파일을 이렇게 종류별로 나누면 어떤 코드가 어떤 코드를 참조하는지 쉽게 확인할 수 없어요. 코드 파일 사이의 의존 관계는 개발자가 스스로 코드를 분석하면서 챙겨야 해요. 32 | 또한 더 이상 특정 컴포넌트나 Hook, 유틸리티 함수가 사용되지 않아서 삭제된다고 했을 때, 연관된 코드가 함께 삭제되지 못해서 사용되지 않는 코드가 남아있게 될 수도 있어요. 33 | 34 | 프로젝트의 크기는 점점 커지기 마련인데, 프로젝트의 크기가 2배, 10배, 100배 커짐에 따라서 코드 사이의 의존관계도 크게 복잡해질 수 있어요. 디렉토리 하나가 100개가 넘는 파일을 담고 있게 될 수도 있어요. 35 | 36 | ## ✏️ 개선해보기 37 | 38 | 다음은 함께 수정되는 코드 파일끼리 하나의 디렉토리를 이루도록 구조를 개선한 예시예요. 39 | 40 | ```text 41 | └─ src 42 | │ // 전체 프로젝트에서 사용되는 코드 43 | ├─ components 44 | ├─ containers 45 | ├─ hooks 46 | ├─ utils 47 | ├─ ... 48 | │ 49 | └─ domains 50 | │ // Domain1에서만 사용되는 코드 51 | ├─ Domain1 52 | │ ├─ components 53 | │ ├─ containers 54 | │ ├─ hooks 55 | │ ├─ utils 56 | │ └─ ... 57 | │ 58 | │ // Domain2에서만 사용되는 코드 59 | └─ Domain2 60 | ├─ components 61 | ├─ containers 62 | ├─ hooks 63 | ├─ utils 64 | └─ ... 65 | ``` 66 | 67 | 함께 수정되는 코드 파일을 하나의 디렉토리 아래에 둔다면, 코드 사이의 의존 관계를 파악하기 쉬워요. 68 | 69 | 예를 들어, 다음과 같이 한 도메인(`Domain1`)의 하위 코드에서 다른 도메인(`Domain2`)의 소스 코드를 참조한다고 생각해 볼게요. 70 | 71 | ```typescript 72 | import { useFoo } from "../../../Domain2/hooks/useFoo"; 73 | ``` 74 | 75 | 이런 import 문을 만난다면 잘못된 파일을 참조하고 있다는 것을 쉽게 인지할 수 있게 돼요. 76 | 77 | 또한, 특정 기능과 관련된 코드를 삭제할 때 한 디렉토리 전체를 삭제하면 깔끔하게 모든 코드가 삭제되므로, 프로젝트 내부에 더 이상 사용되지 않는 코드가 없도록 할 수 있어요. 78 | -------------------------------------------------------------------------------- /fundamentals/code-quality/code/examples/condition-name.md: -------------------------------------------------------------------------------- 1 | # 복잡한 조건에 이름 붙이기 2 | 3 |
4 | 5 |
6 | 7 | 복잡한 조건식이 특별한 이름 없이 사용되면, 조건이 뜻하는 바를 한눈에 파악하기 어려워요. 8 | 9 | ## 📝 코드 예시 10 | 11 | 다음 코드는 상품 중에서 카테고리와 가격 범위가 일치하는 상품만 필터링하는 로직이에요. 12 | 13 | ```typescript 14 | const result = products.filter((product) => 15 | product.categories.some( 16 | (category) => 17 | category.id === targetCategory.id && 18 | product.prices.some( 19 | (price) => price >= minPrice && price <= maxPrice 20 | ) 21 | ) 22 | ); 23 | ``` 24 | 25 | ## 👃 코드 냄새 맡아보기 26 | 27 | ### 가독성 28 | 29 | 이 코드에서는 익명 함수와 조건이 복잡하게 얽혀 있어요. `filter`와 `some`, `&&` 같은 로직이 여러 단계로 중첩되어 있어서 정확한 조건을 파악하기 어려워졌어요. 30 | 31 | 코드를 읽는 사람이 한 번에 고려해야 하는 맥락이 많아서, 가독성이 떨어져요. [^1] 32 | 33 | [^1]: [프로그래머의 뇌](https://www.yes24.com/product/goods/105911017)에 따르면, 사람의 뇌가 한 번에 저장할 수 있는 정보의 숫자는 6개라고 해요. 34 | 35 | ## ✏️ 개선해보기 36 | 37 | 다음 코드와 같이 조건에 명시적인 이름을 붙이면, 코드를 읽는 사람이 한 번에 고려해야 할 맥락을 줄일 수 있어요. 38 | 39 | ```typescript 40 | const matchedProducts = products.filter((product) => { 41 | return product.categories.some((category) => { 42 | const isSameCategory = category.id === targetCategory.id; 43 | const isPriceInRange = product.prices.some( 44 | (price) => price >= minPrice && price <= maxPrice 45 | ); 46 | 47 | return isSameCategory && isPriceInRange; 48 | }); 49 | }); 50 | ``` 51 | 52 | 명시적으로 같은 카테고리 안에 속해 있고, 가격 범위가 맞는 제품들로 필터링한다고 작성함으로써, 복잡한 조건식을 따라가지 않고도 코드의 의도를 명확히 드러낼 수 있어요. 53 | 54 | ## 🔍 더 알아보기: 조건식에 이름을 붙이는 기준 55 | 56 | 언제 조건식이나 함수에 이름을 붙이고 분리하는 것이 좋을까요? 57 | 58 | ### 조건에 이름을 붙이는 것이 좋을 때 59 | 60 | - **복잡한 로직을 다룰 때**: 조건문이나 함수에서 복잡한 로직이 여러 줄에 걸쳐 처리되면, 이름을 붙여 함수의 역할을 명확히 드러내는 것이 좋아요. 이렇게 하면 코드 가독성이 높아지고, 유지보수나 코드 리뷰가 더 쉬워져요. 61 | 62 | - **재사용성이 필요할 때**: 동일한 로직을 여러 곳에서 반복적으로 사용할 가능성이 있으면, 변수나 함수를 선언해 재사용할 수 있어요. 이를 통해 코드 중복을 줄이고 유지보수가 더 쉬워져요. 63 | 64 | - **단위 테스트가 필요할 때**: 함수를 분리하면 독립적으로 단위 테스트를 작성할 수 있어요. 단위 테스트는 함수가 올바르게 동작하는지 쉽게 확인할 수 있어, 복잡한 로직을 테스트할 때 특히 유용해요. 65 | 66 | ### 조건에 이름을 붙이지 않아도 괜찮을 때 67 | 68 | - **로직이 간단할 때**: 로직이 매우 간단하면, 굳이 이름을 붙이지 않아도 돼요. 예를 들어, 배열의 요소를 단순히 두 배로 만드는 `arr.map(x => x * 2)`와 같은 코드는 이름을 붙이지 않아도 직관적이에요. 69 | 70 | - **한 번만 사용될 때**: 특정 로직이 코드 내에서 한 번만 사용되며, 그 로직이 복잡하지 않으면 익명 함수에서 직접 로직을 처리하는 것이 더 직관적일 수 있어요. 71 | -------------------------------------------------------------------------------- /fundamentals/code-quality/code/examples/error-boundary.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/code-quality/code/examples/error-boundary.md -------------------------------------------------------------------------------- /fundamentals/code-quality/code/examples/hidden-logic.md: -------------------------------------------------------------------------------- 1 | # 숨은 로직 드러내기 2 | 3 |
4 | 5 |
6 | 7 | 함수나 컴포넌트의 이름, 파라미터, 반환 값에 드러나지 않는 숨은 로직이 있다면, 함께 협업하는 동료들이 동작을 예측하는 데에 어려움을 겪을 수 있어요. 8 | 9 | ## 📝 코드 예시 10 | 11 | 다음 코드는 사용자의 계좌 잔액을 조회할 때 사용할 수 있는 `fetchBalance` 함수예요. 함수를 호출할 때마다 암시적으로 `balance_fetched`라는 로깅이 이루어지고 있어요. 12 | 13 | ```typescript 4 14 | async function fetchBalance(): Promise { 15 | const balance = await http.get("..."); 16 | 17 | logging.log("balance_fetched"); 18 | 19 | return balance; 20 | } 21 | ``` 22 | 23 | ## 👃 코드 냄새 맡아보기 24 | 25 | ### 예측 가능성 26 | 27 | `fetchBalance` 함수의 이름과 반환 타입만을 가지고는 `balance_fetched` 라는 로깅이 이루어지는지 알 수 없어요. 그래서 로깅을 원하지 않는 곳에서도 로깅이 이루어질 수 있어요. 28 | 29 | 또, 로깅 로직에 오류가 발생했을 때 갑자기 계좌 잔액을 가져오는 로직이 망가질 수도 있죠. 30 | 31 | ## ✏️ 개선해보기 32 | 33 | 함수의 이름과 파라미터, 반환 타입으로 예측할 수 있는 로직만 구현 부분에 남기세요. 34 | 35 | ```typescript 36 | async function fetchBalance(): Promise { 37 | const balance = await http.get("..."); 38 | 39 | return balance; 40 | } 41 | ``` 42 | 43 | 로깅을 하는 코드는 별도로 분리하세요. 44 | 45 | ```tsx 46 | 56 | ``` 57 | -------------------------------------------------------------------------------- /fundamentals/code-quality/code/examples/http.md: -------------------------------------------------------------------------------- 1 | # 이름 겹치지 않게 관리하기 2 | 3 |
4 | 5 |
6 | 7 | 같은 이름을 가지는 함수나 변수는 동일한 동작을 해야 해요. 작은 동작 차이가 코드의 예측 가능성을 낮추고, 코드를 읽는 사람에게 혼란을 줄 수 있어요. 8 | 9 | ## 📝 코드 예시 10 | 11 | 어떤 프론트엔드 서비스에서 원래 사용하던 HTTP 라이브러리를 감싸서 새로운 형태로 HTTP 요청을 보내는 모듈을 만들었어요. 12 | 공교롭게 원래 HTTP 라이브러리와 새로 만든 HTTP 모듈의 이름은 `http`로 같아요. 13 | 14 | ::: code-group 15 | 16 | ```typescript [http.ts] 17 | // 이 서비스는 `http`라는 라이브러리를 쓰고 있어요 18 | import { http as httpLibrary } from "@some-library/http"; 19 | 20 | export const http = { 21 | async get(url: string) { 22 | const token = await fetchToken(); 23 | 24 | return httpLibrary.get(url, { 25 | headers: { Authorization: `Bearer ${token}` } 26 | }); 27 | } 28 | }; 29 | ``` 30 | 31 | ```typescript [fetchUser.ts] 32 | // http.ts에서 정의한 http를 가져오는 코드 33 | import { http } from "./http"; 34 | 35 | export async function fetchUser() { 36 | return http.get("..."); 37 | } 38 | ``` 39 | 40 | ::: 41 | 42 | ## 👃 코드 냄새 맡아보기 43 | 44 | ### 예측 가능성 45 | 46 | 이 코드는 기능적으로 문제가 없지만, 읽는 사람에게 혼란을 줄 수 있어요. `http.get`을 호출하는 개발자는 이 함수가 원래의 HTTP 라이브러리가 하는 것처럼 단순한 GET 요청을 보내는 것으로 예상하지만, 실제로는 토큰을 가져오는 추가 작업이 수행돼요. 47 | 48 | 오해로 인해서 기대 동작과 실제 동작의 차이가 생기고, 버그가 발생하거나, 디버깅 과정을 복잡하고 혼란스럽게 만들 수 있어요. 49 | 50 | ## ✏️ 개선해보기 51 | 52 | 서비스에서 만든 함수에는 라이브러리의 함수명과 구분되는 명확한 이름을 사용해서 함수의 동작을 예측 가능하게 만들 수 있어요. 53 | 54 | ::: code-group 55 | 56 | ```typescript [httpService.ts] 57 | // 이 서비스는 `http`라는 라이브러리를 쓰고 있어요 58 | import { http as httpLibrary } from "@some-library/http"; 59 | 60 | // 라이브러리 함수명과 구분되도록 명칭을 변경했어요. 61 | export const httpService = { 62 | async getWithAuth(url: string) { 63 | const token = await fetchToken(); 64 | 65 | // 토큰을 헤더에 추가하는 등 인증 로직을 추가해요. 66 | return httpLibrary.get(url, { 67 | headers: { Authorization: `Bearer ${token}` } 68 | }); 69 | } 70 | }; 71 | ``` 72 | 73 | ```typescript [fetchUser.ts] 74 | // httpService.ts에서 정의한 httpService를 가져오는 코드 75 | import { httpService } from "./httpService"; 76 | 77 | export async function fetchUser() { 78 | // 함수명을 통해 이 함수가 인증된 요청을 보내는 것을 알 수 있어요. 79 | return await httpService.getWithAuth("..."); 80 | } 81 | ``` 82 | 83 | ::: 84 | 85 | 이렇게 해서 함수의 이름을 봤을 때 동작을 오해할 수 있는 가능성을 줄일 수 있어요. 86 | 다른 개발자가 이 함수를 사용할 때, 서비스에서 정의한 함수라는 것을 인지하고 올바르게 사용할 수 있어요. 87 | 88 | 또한, `getWithAuth`라는 이름으로 이 함수가 인증된 요청을 보낸다는 것을 명확하게 전달할 수 있어요. 89 | -------------------------------------------------------------------------------- /fundamentals/code-quality/code/examples/magic-number-cohesion.md: -------------------------------------------------------------------------------- 1 | # 매직 넘버 없애기 2 | 3 |
4 | 5 |
6 | 7 | **매직 넘버**(Magic Number)란 정확한 뜻을 밝히지 않고 소스 코드 안에 직접 숫자 값을 넣는 것을 말해요. 8 | 9 | 예를 들어, 찾을 수 없음(Not Found)을 나타내는 HTTP 상태 코드로 `404` 값을 바로 사용하는 것이나, 10 | 하루를 나타내는 `86400`초를 그대로 사용하는 것이 있어요. 11 | 12 | ## 📝 코드 예시 13 | 14 | 다음 코드는 좋아요 버튼을 눌렀을 때 좋아요 개수를 새로 내려받는 함수예요. 15 | 16 | ```typescript 3 17 | async function onLikeClick() { 18 | await postLike(url); 19 | await delay(300); 20 | await refetchPostLike(); 21 | } 22 | ``` 23 | 24 | ## 👃 코드 냄새 맡아보기 25 | 26 | ### 응집도 27 | 28 | `300`이라고 하는 숫자를 애니메이션 완료를 기다리려고 사용했다면, 재생하는 애니메이션을 바꿨을 때 조용히 서비스가 깨질 수 있는 위험성이 있어요. 29 | 충분한 시간동안 애니메이션을 기다리지 않고 바로 다음 로직이 시작될 수도 있죠. 30 | 31 | 같이 수정되어야 할 코드 중 한쪽만 수정된다는 점에서, 응집도가 낮은 코드라고도 할 수 있어요. 32 | 33 | ::: info 34 | 35 | 이 Hook은 [가독성](./magic-number-readability.md) 관점으로도 볼 수 있어요. 36 | 37 | ::: 38 | 39 | ## ✏️ 개선해보기 40 | 41 | 숫자 `300`의 맥락을 정확하게 표시하기 위해서 상수 `ANIMATION_DELAY_MS`로 선언할 수 있어요. 42 | 43 | ```typescript 1,5 44 | const ANIMATION_DELAY_MS = 300; 45 | 46 | async function onLikeClick() { 47 | await postLike(url); 48 | await delay(ANIMATION_DELAY_MS); 49 | await refetchPostLike(); 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /fundamentals/code-quality/code/examples/magic-number-readability.md: -------------------------------------------------------------------------------- 1 | # 매직 넘버에 이름 붙이기 2 | 3 |
4 | 5 |
6 | 7 | **매직 넘버**(Magic Number)란 정확한 뜻을 밝히지 않고 소스 코드 안에 직접 숫자 값을 넣는 것을 말해요. 8 | 9 | 예를 들어, 찾을 수 없음(Not Found)을 나타내는 HTTP 상태 코드로 `404` 값을 바로 사용하는 것이나, 10 | 하루를 나타내는 `86400`초를 그대로 사용하는 것이 있어요. 11 | 12 | ## 📝 코드 예시 13 | 14 | 다음 코드는 좋아요 버튼을 눌렀을 때 좋아요 개수를 새로 내려받는 함수예요. 15 | 16 | ```typescript 3 17 | async function onLikeClick() { 18 | await postLike(url); 19 | await delay(300); 20 | await refetchPostLike(); 21 | } 22 | ``` 23 | 24 | ## 👃 코드 냄새 맡아보기 25 | 26 | ### 가독성 27 | 28 | 이 코드는 `delay` 함수에 전달된 `300`이라고 하는 값이 어떤 맥락으로 쓰였는지 알 수 없어요. 29 | 원래 코드를 작성한 개발자가 아니라면, 어떤 목적으로 300ms동안 기다리는지 알 수 없죠. 30 | 31 | - 애니메이션이 완료될 때까지 기다리는 걸까? 32 | - 좋아요 반영에 시간이 걸려서 기다리는 걸까? 33 | - 테스트 코드였는데, 깜빡하고 안 지운 걸까? 34 | 35 | 하나의 코드를 여러 명의 개발자가 함께 수정하다 보면 의도를 정확히 알 수 없어서 코드가 원하지 않는 방향으로 수정될 수도 있어요. 36 | 37 | ::: info 38 | 39 | 이 Hook은 [응집도](./magic-number-cohesion.md) 관점으로도 볼 수 있어요. 40 | 41 | ::: 42 | 43 | ## ✏️ 개선해보기 44 | 45 | 숫자 `300`의 맥락을 정확하게 표시하기 위해서 상수 `ANIMATION_DELAY_MS`로 선언할 수 있어요. 46 | 47 | ```typescript 1,5 48 | const ANIMATION_DELAY_MS = 300; 49 | 50 | async function onLikeClick() { 51 | await postLike(url); 52 | await delay(ANIMATION_DELAY_MS); 53 | await refetchPostLike(); 54 | } 55 | ``` 56 | 57 | ## 🔍 더 알아보기 58 | 59 | 매직 넘버는 응집도 관점에서도 살펴볼 수 있어요. [매직 넘버 없애서 응집도 높이기](./magic-number-cohesion.md) 문서도 참고해 보세요. 60 | -------------------------------------------------------------------------------- /fundamentals/code-quality/code/examples/submit-button.md: -------------------------------------------------------------------------------- 1 | # 같이 실행되지 않는 코드 분리하기 2 | 3 |
4 | 5 |
6 | 7 | 동시에 실행되지 않는 코드가 하나의 함수 또는 컴포넌트에 있으면, 동작을 한눈에 파악하기 어려워요. 8 | 구현 부분에 많은 숫자의 분기가 들어가서, 어떤 역할을 하는지 이해하기 어렵기도 해요. 9 | 10 | ## 📝 코드 예시 11 | 12 | 다음 `` 컴포넌트는 사용자의 권한에 따라서 다르게 동작해요. 13 | 14 | - 사용자의 권한이 보기 전용(`"viewer"`)이면, 초대 버튼은 비활성화되어 있고, 애니메이션도 재생하지 않아요. 15 | - 사용자가 일반 사용자이면, 초대 버튼을 사용할 수 있고, 애니메이션도 재생해요. 16 | 17 | ```tsx 18 | function SubmitButton() { 19 | const isViewer = useRole() === "viewer"; 20 | 21 | useEffect(() => { 22 | if (isViewer) { 23 | return; 24 | } 25 | showButtonAnimation(); 26 | }, [isViewer]); 27 | 28 | return isViewer ? ( 29 | Submit 30 | ) : ( 31 | 32 | ); 33 | } 34 | ``` 35 | 36 | ## 👃 코드 냄새 맡아보기 37 | 38 | ### 가독성 39 | 40 | `` 컴포넌트에서는 사용자가 가질 수 있는 2가지의 권한 상태를 하나의 컴포넌트 안에서 한 번에 처리하고 있어요. 41 | 그래서 코드를 읽는 사람이 한 번에 고려해야 하는 맥락이 많아요. 42 | 43 | 예를 들어, 다음 코드에서 파란색은 사용자가 보기 전용 권한(`'viewer'`)을 가지고 있을 때, 빨간색은 일반 사용자일 때 실행되는 코드예요. 44 | 동시에 실행되지 않는 코드가 교차되어서 나타나서 코드를 이해할 때 부담을 줘요. 45 | 46 | ![](../../images/examples/submit-button.png){.light-only} 47 | ![](../../images/examples/submit-button-dark.png){.dark-only} 48 | 49 | ## ✏️ 개선해보기 50 | 51 | 다음 코드는 사용자가 보기 전용 권한을 가질 때와 일반 사용자일 때를 완전히 나누어서 관리하도록 하는 코드예요. 52 | 53 | ```tsx 54 | function SubmitButton() { 55 | const isViewer = useRole() === "viewer"; 56 | 57 | return isViewer ? : ; 58 | } 59 | 60 | function ViewerSubmitButton() { 61 | return Submit; 62 | } 63 | 64 | function AdminSubmitButton() { 65 | useEffect(() => { 66 | showAnimation(); 67 | }, []); 68 | 69 | return ; 70 | } 71 | ``` 72 | 73 | - `` 코드 곳곳에 있던 분기가 단 하나로 합쳐지면서, 분기가 줄어들었어요. 74 | - ``과 `` 에서는 하나의 분기만 관리하기 때문에, 코드를 읽는 사람이 한 번에 고려해야 할 맥락이 적어요. 75 | -------------------------------------------------------------------------------- /fundamentals/code-quality/code/examples/submit1.md: -------------------------------------------------------------------------------- 1 | # 같이 실행되지 않는 코드 분리하기 2 | 3 |
4 | 5 |
6 | 7 | 동시에 실행되지 않는 코드가 하나의 함수 또는 컴포넌트에 있으면, 동작을 한눈에 파악하기 어려워요. 8 | 구현 부분에 많은 숫자의 분기가 들어가서, 어떤 역할을 하는지 이해하기 어렵기도 해요. 9 | 10 | ## 📝 코드 예시 11 | 12 | 다음 `` 컴포넌트는 사용자의 권한에 따라서 다르게 동작해요. 13 | 14 | - 사용자의 권한이 보기 전용(`"viewer"`)이면, 초대 버튼은 비활성화되어 있고, 애니메이션도 재생하지 않아요. 15 | - 사용자가 일반 사용자이면, 초대 버튼을 사용할 수 있고, 애니메이션도 재생해요. 16 | 17 | ```tsx 18 | function SubmitButton() { 19 | const isViewer = useRole() === "viewer"; 20 | 21 | useEffect(() => { 22 | if (isViewer) { 23 | return; 24 | } 25 | showButtonAnimation(); 26 | }, [isViewer]); 27 | 28 | return isViewer ? ( 29 | Submit 30 | ) : ( 31 | 32 | ); 33 | } 34 | ``` 35 | 36 | ## 👃 코드 냄새 맡아보기 37 | 38 | ### 가독성 39 | 40 | `` 컴포넌트에서는 사용자가 가질 수 있는 2가지의 권한 상태를 하나의 컴포넌트 안에서 한 번에 처리하고 있어요. 41 | 그래서 코드를 읽는 사람이 한 번에 고려해야 하는 맥락이 많아요. 42 | 43 | 예를 들어, 다음 코드에서 파란색은 사용자가 보기 전용 권한(`'viewer'`)을 가지고 있을 때, 빨간색은 일반 사용자일 때 실행되는 코드예요. 44 | 동시에 실행되지 않는 코드가 교차되어서 나타나서 코드를 이해할 때 부담을 줘요. 45 | 46 | ![](../../images/examples/submit-button.png){.light-only} 47 | ![](../../images/examples/submit-button-dark.png){.dark-only} 48 | 49 | ## ✏️ 개선해보기 50 | 51 | 다음 코드는 사용자가 보기 전용 권한을 가질 때와 일반 사용자일 때를 완전히 나누어서 관리하도록 하는 코드예요. 52 | 53 | ```tsx 54 | function SubmitButton() { 55 | const isViewer = useRole() === "viewer"; 56 | 57 | return isViewer ? : ; 58 | } 59 | 60 | function ViewerSubmitButton() { 61 | return Submit; 62 | } 63 | 64 | function AdminSubmitButton() { 65 | useEffect(() => { 66 | showAnimation(); 67 | }, []); 68 | 69 | return ; 70 | } 71 | ``` 72 | 73 | - `` 코드 곳곳에 있던 분기가 단 하나로 합쳐지면서, 분기가 줄어들었어요. 74 | - ``과 `` 에서는 하나의 분기만 관리하기 때문에, 코드를 읽는 사람이 한 번에 고려해야 할 맥락이 적어요. 75 | -------------------------------------------------------------------------------- /fundamentals/code-quality/code/examples/ternary-operator.md: -------------------------------------------------------------------------------- 1 | # 삼항 연산자 단순하게 하기 2 | 3 |
4 | 5 |
6 | 7 | 삼항 연산자를 복잡하게 사용하면 조건의 구조가 명확하게 보이지 않아서 코드를 읽기 어려울 수 있어요. 8 | 9 | ## 📝 코드 예시 10 | 11 | 다음 코드는 `A조건`과 `B조건`에 따라서 `"BOTH"`, `"A"`, `"B"` 또는 `"NONE"` 중 하나를 `status`에 지정하는 코드예요. 12 | 13 | ```typescript 14 | const status = 15 | (A조건 && B조건) ? "BOTH" : (A조건 || B조건) ? (A조건 ? "A" : "B") : "NONE"; 16 | ``` 17 | 18 | ## 👃 코드 냄새 맡아보기 19 | 20 | ### 가독성 21 | 22 | 이 코드는 여러 삼항 연산자가 중첩되어 사용되어서, 정확하게 어떤 조건으로 값이 계산되는지 한눈에 파악하기 어려워요. 23 | 24 | ## ✏️ 개선해보기 25 | 26 | 다음과 같이 조건을 `if` 문으로 풀어서 사용하면 보다 명확하고 간단하게 조건을 드러낼 수 있어요. 27 | 28 | ```typescript 29 | const status = (() => { 30 | if (A조건 && B조건) return "BOTH"; 31 | if (A조건) return "A"; 32 | if (B조건) return "B"; 33 | return "NONE"; 34 | })(); 35 | ``` 36 | -------------------------------------------------------------------------------- /fundamentals/code-quality/code/examples/use-bottom-sheet.md: -------------------------------------------------------------------------------- 1 | # 중복 코드 허용하기 2 | 3 |
4 | 5 |
6 | 7 | 개발자로서 여러 페이지나 컴포넌트에 걸친 중복 코드를 하나의 Hook이나 컴포넌트로 공통화하는 경우가 많아요. 8 | 중복 코드를 하나의 컴포넌트나 Hook으로 공통화하면, 좋은 코드의 특징 중 하나인 응집도를 챙겨서, 함께 수정되어야 할 코드들을 한꺼번에 수정할 수 있어요. 9 | 10 | 그렇지만, 불필요한 결합도가 생겨서, 공통 컴포넌트나 Hook을 수정함에 따라 영향을 받는 코드의 범위가 넓어져서, 오히려 수정이 어려워질 수도 있어요. 11 | 12 | 처음에는 비슷하게 동작한다고 생각해서 공통화한 코드가, 이후 페이지마다 다른 특이한 요구사항이 생겨서, 점점 복잡해질 수 있어요. 13 | 동시에 공통 코드를 수정할 때마다, 그 코드에 의존하는 코드들을 일일이 제대로 테스트해야 해서, 오히려 코드 수정이 어려워지기도 하죠. 14 | 15 | ## 📝 코드 예시 16 | 17 | 아래와 같이 점검 정보를 인자로 받아서, 점검 중이라면 점검 바텀시트를 열고, 사용자가 알림 받기에 동의하면 이를 로깅하고, 현재 화면을 닫는 Hook을 살펴볼게요. 18 | 19 | ```typescript 20 | export const useOpenMaintenanceBottomSheet = () => { 21 | const maintenanceBottomSheet = useMaintenanceBottomSheet(); 22 | const logger = useLogger(); 23 | 24 | return async (maintainingInfo: TelecomMaintenanceInfo) => { 25 | logger.log("점검 바텀시트 열림"); 26 | const result = await maintenanceBottomSheet.open(maintainingInfo); 27 | if (result) { 28 | logger.log("점검 바텀시트 알림받기 클릭"); 29 | } 30 | closeView(); 31 | }; 32 | }; 33 | ``` 34 | 35 | 이 코드는 여러 페이지에서 반복적으로 사용되었기에 공통 Hook으로 분리되었어요. 36 | 37 | ## 👃 코드 냄새 맡아보기 38 | 39 | ### 결합도 40 | 41 | 이 Hook은 여러 페이지에서 반복적으로 보이는 로직이기에 공통화되었어요. 그렇지만 앞으로 생길 수 있는 다양한 코드 변경의 가능성을 생각해볼 수 있어요. 42 | 43 | - 만약에 페이지마다 로깅하는 값이 달라진다면? 44 | - 만약에 어떤 페이지에서는 점검 바텀시트를 닫더라도 화면을 닫을 필요가 없다면? 45 | - 바텀시트에서 보여지는 텍스트나 이미지를 다르게 해야 한다면? 46 | 47 | 위 Hook은 이런 코드 변경사항에 유연하게 대응하기 위해서 복잡하게 인자를 받아야 할 거예요. 48 | 이 Hook의 구현을 수정할 때마다, 이 Hook을 쓰는 모든 페이지들이 잘 작동하는지 테스트도 해야 할 것이고요. 49 | 50 | ## ✏️ 개선해보기 51 | 52 | 다소 반복되어 보이는 코드일지 몰라도, 중복 코드를 허용하는 것이 좋은 방향일 수 있어요. 53 | 54 | 함께 일하는 동료들과 적극적으로 소통하며 점검 바텀시트의 동작을 정확하게 이해해야 해요. 55 | 페이지에서 로깅하는 값이 같고, 점검 바텀시트의 동작이 동일하고, 바텀시트의 모양이 동일하다면, 그리고 앞으로도 그럴 예정이라면, 공통화를 통해 코드의 응집도를 높일 수 있어요. 56 | 57 | 그렇지만 페이지마다 동작이 달라질 여지가 있다면, 공통화 없이 중복 코드를 허용하는 것이 더 좋은 선택이에요. 58 | -------------------------------------------------------------------------------- /fundamentals/code-quality/code/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | comments: false 3 | --- 4 | 5 | # 좋은 코드를 위한 4가지 기준 6 | 7 | 좋은 프론트엔드 코드는 **변경하기 쉬운** 코드예요. 8 | 새로운 요구사항을 구현하고자 할 때, 기존 코드를 수정하고 배포하기 수월한 코드가 좋은 코드죠. 9 | 코드가 변경하기 쉬운지는 4가지 기준으로 판단할 수 있어요. 10 | 11 | ## 1. 가독성 12 | 13 | **가독성**(Readability)은 코드가 읽기 쉬운 정도를 말해요. 14 | 코드가 변경하기 쉬우려면 먼저 코드가 어떤 동작을 하는지 이해할 수 있어야 해요. 15 | 16 | 읽기 좋은 코드는 읽는 사람이 한 번에 머릿속에서 고려하는 맥락이 적고, 위에서 아래로 자연스럽게 이어져요. 17 | 18 | ### 가독성을 높이는 전략 19 | 20 | - **맥락 줄이기** 21 | - [같이 실행되지 않는 코드 분리하기](./examples/submit-button.md) 22 | - [구현 상세 추상화하기](./examples/login-start-page.md) 23 | - [로직 종류에 따라 합쳐진 함수 쪼개기](./examples/use-page-state-readability.md) 24 | - **이름 붙이기** 25 | - [복잡한 조건에 이름 붙이기](./examples/condition-name.md) 26 | - [매직 넘버에 이름 붙이기](./examples/magic-number-readability.md) 27 | - **위에서 아래로 읽히게 하기** 28 | - [시점 이동 줄이기](./examples/user-policy.md) 29 | - [삼항 연산자 단순하게 하기](./examples/ternary-operator.md) 30 | 31 | ## 2. 예측 가능성 32 | 33 | **예측 가능성**(Predictability)이란, 함께 협업하는 동료들이 함수나 컴포넌트의 동작을 얼마나 예측할 수 있는지를 말해요. 34 | 예측 가능성이 높은 코드는 일관적인 규칙을 따르고, 함수나 컴포넌트의 이름과 파라미터, 반환 값만 보고도 어떤 동작을 하는지 알 수 있어요. 35 | 36 | ### 예측 가능성을 높이는 전략 37 | 38 | - [이름 겹치지 않게 관리하기](./examples/http.md) 39 | - [같은 종류의 함수는 반환 타입 통일하기](./examples/use-user.md) 40 | - [숨은 로직 드러내기](./examples/hidden-logic.md) 41 | 42 | ## 3. 응집도 43 | 44 | **응집도**(Cohesion)란, 수정되어야 할 코드가 항상 같이 수정되는지를 말해요. 45 | 응집도가 높은 코드는 코드의 한 부분을 수정해도 의도치 않게 다른 부분에서 오류가 발생하지 않아요. 46 | 함께 수정되어야 할 부분이 반드시 함께 수정되도록 구조적으로 뒷받침되기 때문이죠. 47 | 48 | ::: info 가독성과 응집도는 서로 상충할 수 있어요 49 | 50 | 일반적으로 응집도를 높이기 위해서는 변수나 함수를 추상화하는 등 가독성을 떨어뜨리는 결정을 해야 해요. 51 | 함께 수정되지 않으면 오류가 발생할 수 있는 경우에는, 응집도를 우선해서 코드를 공통화, 추상화하세요. 52 | 위험성이 높지 않은 경우에는, 가독성을 우선하여 코드 중복을 허용하세요. 53 | 54 | ::: 55 | 56 | ### 응집도를 높이는 전략 57 | 58 | - [함께 수정되는 파일을 같은 디렉토리에 두기](./examples/code-directory.md) 59 | - [매직 넘버 없애기](./examples/magic-number-cohesion.md) 60 | - [폼의 응집도 생각하기](./examples/form-fields.md) 61 | 62 | ## 4. 결합도 63 | 64 | **결합도**(Coupling)란, 코드를 수정했을 때의 영향범위를 말해요. 65 | 코드를 수정했을 때 영향범위가 적어서, 변경에 따른 범위를 예측할 수 있는 코드가 수정하기 쉬운 코드예요. 66 | 67 | ### 결합도를 낮추는 전략 68 | 69 | - [책임을 하나씩 관리하기](./examples/use-page-state-coupling.md) 70 | - [중복 코드 허용하기](./examples/use-bottom-sheet.md) 71 | - [Props Drilling 지우기](./examples/item-edit-modal.md) 72 | 73 | ## 코드 품질 여러 각도로 보기 74 | 75 | 아쉽게도 이 4가지 기준을 모두 한꺼번에 충족하기는 어려워요. 76 | 77 | 예를 들어서, 함수나 변수가 항상 같이 수정되기 위해서 공통화 및 추상화하면, 응집도가 높아지죠. 그렇지만 코드가 한 차례 추상화되기 때문에 가독성이 떨어져요. 78 | 79 | 중복 코드를 허용하면, 코드의 영향범위를 줄일 수 있어서, 결합도를 낮출 수 있어요. 그렇지만 한쪽을 수정했을 때 다른 한쪽을 실수로 수정하지 못할 수 있어서, 응집도가 떨어지죠. 80 | 81 | 프론트엔드 개발자는 현재 직면한 상황을 바탕으로, 깊이 있게 고민하면서, 장기적으로 코드가 수정하기 쉽게 하기 위해서 어떤 가치를 우선해야 하는지 고민해야 해요. 82 | -------------------------------------------------------------------------------- /fundamentals/code-quality/code/start.md: -------------------------------------------------------------------------------- 1 | --- 2 | comments: false 3 | --- 4 | 5 | # 시작하기 6 | 7 | `Frontend Fundamentals`(FF)는 좋은 프론트엔드 코드의 기준을 제공해요. 8 | 프론트엔드 개발자로서 코드 품질을 높이고자 할 때 방향을 찾는 나침반처럼 활용해 보세요. 9 | 10 | 좋은 코드에 대한 [4개 원칙](./index.md)과 함께, 구체적인 예시 및 해결 방안을 제시해요. 11 | 12 | ## 이런 분들에게 추천해요 13 | 14 | - 🦨 코드에 대해서 고민되는데 **논리적으로 설명하기 어려운 개발자** 15 | - 👀 **나쁜 코드를 빠르게 감지**하고 개선하는 방법을 공부하고 싶은 개발자 16 | - 🤓 코드 리뷰 등에서 누가 전달해준 링크를 타고 들어와 "내 코드가 이랬구나"를 **객관적 시각으로 인지**하게 될 개발자 17 | - 👥 **팀과 함께** 공통의 코딩 스타일과 코드 품질의 기준을 세워보고 싶은 개발자 18 | 19 | ## 저작자 20 | 21 | - [milooy](https://github.com/milooy) 22 | - [donghyeon](https://github.com/kimbangg) 23 | - [chkim116](https://github.com/chkim116) 24 | - [inseong.you](https://github.com/inseong.you) 25 | - [raon0211](https://github.com/raon0211) 26 | - [bigsaigon333](https://github.com/bigsaigon333) 27 | - [jho2301](https://github.com/jho2301) 28 | - [KimChunsick](https://github.com/KimChunsick) 29 | - [jennybehan](https://github.com/jennybehan) 30 | 31 | ## 문서 기여자 32 | 33 | - [andy0414](https://github.com/andy0414) 34 | - [pumpkiinbell](https://github.com/pumpkiinbell) 35 | -------------------------------------------------------------------------------- /fundamentals/code-quality/en/code/coming-soon.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: doc 3 | title: Bundling 4 | description: Frontend Bundling Guide (Coming Soon) 5 | --- 6 | 7 |
8 |

✨ Coming Soon

9 |

Stay tuned! We're working on something awesome for you.

10 |
11 | 12 | -------------------------------------------------------------------------------- /fundamentals/code-quality/en/code/community.md: -------------------------------------------------------------------------------- 1 | --- 2 | comments: false 3 | --- 4 | 5 | # Contributing Together 6 | 7 | `Frontend Fundamentals` (FF) is developing standards for good code together with the community. 8 | 9 | It's currently maintained by the Toss Frontend chapter. 10 | 11 | ## Featured Discussions 12 | 13 | Explore some of the best discussions in the community. Expand your thinking about good code beyond what's in the Frontend Fundamentals documentation. 14 | 15 | - [Featured Discussions](https://github.com/toss/frontend-fundamentals/discussions?discussions_q=is%3Aopen+label%3A%22%EC%84%B1%EC%A7%80+%E2%9B%B2%22) 16 | 17 | ## Discussing Code Concerns 18 | 19 | If you have code that you're concerned about, post it on the GitHub discussions. 20 | You can receive reviews from the community from various perspectives on your code and discuss the standards for good code with the community. 21 | 22 | Cases that receive a lot of support can be directly added to the Frontend Fundamentals documentation. The contribution method will be announced later. 23 | 24 | - [Post on GitHub Discussions](https://github.com/toss/frontend-fundamentals/discussions) 25 | 26 | ## Adding Opinions on Good Code Standards 27 | 28 | If you have opinions on the standards for good code or want to add new opinions, vote on what makes better code and leave your thoughts. 29 | Communicate with the community to create richer and deeper standards. 30 | 31 | This can be an opportunity to establish your own criteria on whether this code is good or that code is good. 32 | 33 | - [View Code on A vs B](https://github.com/toss/frontend-fundamentals/discussions/categories/a-vs-b) 34 | -------------------------------------------------------------------------------- /fundamentals/code-quality/en/code/examples/hidden-logic.md: -------------------------------------------------------------------------------- 1 | # Revealing Hidden Logic 2 | 3 |
4 | 5 |
6 | If there is hidden logic that is not revealed in the name, parameters, or return value of a function or component, it can be difficult for collaborating colleagues to predict its behavior. 7 | 8 | ## 📝 Code Example 9 | 10 | The following code is a `fetchBalance` function that can be used to check a user's account balance. Each time the function is called, it implicitly logs `balance_fetched`. 11 | 12 | ```typescript 4 13 | async function fetchBalance(): Promise { 14 | const balance = await http.get("..."); 15 | 16 | logging.log("balance_fetched"); 17 | 18 | return balance; 19 | } 20 | ``` 21 | 22 | ## 👃 Smell the Code 23 | 24 | ### Predictability 25 | 26 | From the name and return type of the `fetchBalance` function, it is not clear that logging of `balance_fetched` is taking place. Therefore, logging might occur even in places where it is not desired. 27 | 28 | Additionally, if an error occurs in the logging logic, the logic for fetching the account balance might suddenly break. 29 | 30 | ## ✏️ Work on Improving 31 | 32 | Leave only the logic that can be predicted by the function's name, parameters, and return type in the implementation. 33 | 34 | ```typescript 35 | async function fetchBalance(): Promise { 36 | const balance = await http.get("..."); 37 | 38 | return balance; 39 | } 40 | ``` 41 | 42 | Separate the logging code. 43 | 44 | ```tsx 45 | 55 | ``` 56 | -------------------------------------------------------------------------------- /fundamentals/code-quality/en/code/examples/magic-number-cohesion.md: -------------------------------------------------------------------------------- 1 | # Eliminating Magic Numbers 2 | 3 |
4 | 5 |
6 | 7 | **Magic Number** refers to directly inserting numerical values into the source code without explicitly stating their meaning. 8 | 9 | For example, using the value `404` directly as the HTTP status code for Not Found, or using `86400` seconds directly to represent a day. 10 | 11 | ## 📝 Code Example 12 | 13 | The following code is a function that retrieves the new like count when the like button is clicked. 14 | 15 | ```typescript 3 16 | async function onLikeClick() { 17 | await postLike(url); 18 | await delay(300); 19 | await refetchPostLike(); 20 | } 21 | ``` 22 | 23 | ## 👃 Smell the Code 24 | 25 | ### Cohesion 26 | 27 | If you used the number `300` to wait for the animation to complete, there is a risk that the service may quietly break when the animation being played is changed. Additionally, the next logic may start immediately without waiting for a sufficient amount of time for the animation. 28 | 29 | From the perspective that only one side of the code that needs to be modified together is modified, it can also be said to be code with low cohesion. 30 | 31 | ::: info 32 | 33 | This hook can also be viewed from the perspective of [readability](./magic-number-readability.md). 34 | 35 | ::: 36 | 37 | ## ✏️ Work on Improving 38 | 39 | To accurately represent the context of the number `300`, you can declare it as a constant `ANIMATION_DELAY_MS`. 40 | 41 | ```typescript 1,5 42 | const ANIMATION_DELAY_MS = 300; 43 | 44 | async function onLikeClick() { 45 | await postLike(url); 46 | await delay(ANIMATION_DELAY_MS); 47 | await refetchPostLike(); 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /fundamentals/code-quality/en/code/examples/magic-number-readability.md: -------------------------------------------------------------------------------- 1 | # Naming Magic Numbers 2 | 3 |
4 | 5 |
6 | 7 | **Magic Number** refers to directly inserting numerical values into the source code without explicitly stating their meaning. 8 | 9 | For example, using the value `404` directly as the HTTP status code for Not Found, or using `86400` seconds directly to represent a day. 10 | 11 | ## 📝 Code Example 12 | 13 | The following code is a function that retrieves the new like count when the like button is clicked. 14 | 15 | ```typescript 3 16 | async function onLikeClick() { 17 | await postLike(url); 18 | await delay(300); 19 | await refetchPostLike(); 20 | } 21 | ``` 22 | 23 | ## 👃 Smell the Code 24 | 25 | ### Readability 26 | 27 | The value `300` passed to the `delay` function is used in an unclear context. 28 | If you are not the original developer of the code, you may not know why it waits for 300ms. 29 | 30 | - Is it waiting for the animation to complete? 31 | - Is it waiting for the like to be reflected? 32 | - Was it a test code that was forgotten to be removed? 33 | 34 | When multiple developers work on the same code, the intention may not be accurately understood, leading to unintended modifications. 35 | 36 | ::: info 37 | 38 | This Hook can also be viewed from the perspective of [cohesion](./magic-number-cohesion.md). 39 | 40 | ::: 41 | 42 | ## ✏️ Work on Improving 43 | 44 | To accurately represent the context of the number `300`, you can declare it as a constant `ANIMATION_DELAY_MS`. 45 | 46 | ```typescript 1,5 47 | const ANIMATION_DELAY_MS = 300; 48 | 49 | async function onLikeClick() { 50 | await postLike(url); 51 | await delay(ANIMATION_DELAY_MS); 52 | await refetchPostLike(); 53 | } 54 | ``` 55 | 56 | ## 🔍 Learn More 57 | 58 | Magic numbers can also be viewed from the perspective of cohesion. Please also refer to the document [Eliminating Magic Numbers to Increase Cohesion](./magic-number-cohesion.md). 59 | -------------------------------------------------------------------------------- /fundamentals/code-quality/en/code/examples/ternary-operator.md: -------------------------------------------------------------------------------- 1 | # Simplifying Ternary Operators 2 | 3 |
4 | 5 |
6 | 7 | Using complex ternary operators can obscure the structure of the conditions, making the code harder to read. 8 | 9 | ## 📝 Code Example 10 | 11 | The following code assigns `"BOTH"`, `"A"`, `"B"`, or `"NONE"` to `status` based on `ACondition` and `BCondition`. 12 | 13 | ```typescript 14 | const status = 15 | (ACondition && BCondition) 16 | ? "BOTH" 17 | : (ACondition || BCondition) 18 | ? (ACondition 19 | ? "A" 20 | : "B") 21 | : "NONE"; 22 | ``` 23 | 24 | ## 👃 Smell the Code 25 | 26 | ### Readability 27 | 28 | This code uses multiple nested ternary operators, making it difficult to quickly understand the exact conditions under which values are calculated. 29 | 30 | ## ✏️ Work on Improving 31 | 32 | You can rewrite the conditions using `if` statements, as shown below, to make the logic clearer and easier to follow. 33 | 34 | ```typescript 35 | const status = (() => { 36 | if (ACondition && BCondition) return "BOTH"; 37 | if (ACondition) return "A"; 38 | if (BCondition) return "B"; 39 | return "NONE"; 40 | })(); 41 | ``` 42 | -------------------------------------------------------------------------------- /fundamentals/code-quality/en/code/start.md: -------------------------------------------------------------------------------- 1 | --- 2 | comments: false 3 | --- 4 | 5 | # Getting Started 6 | 7 | `Frontend Fundamentals` (FF) provides standards for good frontend code. 8 | Use it as a compass to find direction when you want to improve code quality as a frontend developer. 9 | 10 | It presents [four principles](./index.md) of good code, along with specific examples and solutions. 11 | 12 | ## Who Is This For? 13 | 14 | - 🦨 Developers who are concerned about code but find it **difficult to explain logically** 15 | - 👀 Developers who want to learn how to **quickly detect and improve bad code** 16 | - 🤓 Developers who, during code reviews, follow a link provided by someone and come to **objectively recognize** "Oh, this is how my code was" 17 | - 👥 Developers who want to establish a common coding style and code quality standards **with their team** 18 | 19 | ## Authors 20 | 21 | - [milooy](https://github.com/milooy) 22 | - [donghyeon](https://github.com/kimbangg) 23 | - [chkim116](https://github.com/chkim116) 24 | - [inseong.you](https://github.com/inseong.you) 25 | - [raon0211](https://github.com/raon0211) 26 | - [bigsaigon333](https://github.com/bigsaigon333) 27 | - [jho2301](https://github.com/jho2301) 28 | - [KimChunsick](https://github.com/KimChunsick) 29 | - [jennybehan](https://github.com/jennybehan) 30 | 31 | ## Document Contributors 32 | 33 | - [andy0414](https://github.com/andy0414) 34 | - [pumpkiinbell](https://github.com/pumpkiinbell) 35 | -------------------------------------------------------------------------------- /fundamentals/code-quality/en/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Frontend Fundamentals" 7 | tagline: "Guidelines for easily modifiable frontend code" 8 | image: 9 | src: /images/ff-symbol-gradient-webp-80.webp 10 | alt: Frontend Fundamentals symbol 11 | actions: 12 | - text: Learn about good code 13 | link: /en/code/ 14 | - theme: alt 15 | text: Communicate 16 | link: /en/code/community 17 | 18 | features: 19 | - icon: 🤓 20 | title: Improve your code review skills 21 | details: Explore principles to determine if the code is easily changeable. 22 | - icon: 🤝 23 | title: Conduct better code reviews 24 | details: Actively explore various code improvement cases. 25 | - icon: 📝 26 | title: Concerned about your code? 27 | details: Communicate with other developers in the GitHub discussions. 28 | --- 29 | -------------------------------------------------------------------------------- /fundamentals/code-quality/images/examples/submit-button-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/code-quality/images/examples/submit-button-dark.png -------------------------------------------------------------------------------- /fundamentals/code-quality/images/examples/submit-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/code-quality/images/examples/submit-button.png -------------------------------------------------------------------------------- /fundamentals/code-quality/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Frontend Fundamentals" 7 | tagline: "변경하기 쉬운 프론트엔드 코드를 위한 지침서" 8 | image: 9 | loading: eager 10 | fetchpriority: high 11 | decoding: async 12 | src: /images/ff-symbol-gradient-webp-80.webp 13 | alt: Frontend Fundamentals symbol 14 | actions: 15 | - text: 좋은 코드의 기준 알아보기 16 | link: /code/ 17 | - theme: alt 18 | text: 소통하기 19 | link: /code/community 20 | 21 | features: 22 | - icon: 🤓 23 | title: 코드를 보는 눈을 키우고 싶다면 24 | details: 변경하기 쉬운 코드인지 판단하기 위한 원칙을 살펴보세요. 25 | - icon: 🤝 26 | title: 코드 리뷰를 잘하고 싶다면 27 | details: 다양한 코드 개선 사례를 능동적으로 탐색해 보세요. 28 | - icon: 📝 29 | title: 내 코드가 고민된다면 30 | details: 깃허브 디스커션에서 다른 개발자들과 소통해 보세요. 31 | --- 32 | -------------------------------------------------------------------------------- /fundamentals/code-quality/ja/code/coming-soon.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: doc 3 | title: Bundling 4 | description: Frontend Bundling Guide (Coming Soon) 5 | --- 6 | 7 |
8 |

✨ Coming Soon

9 |

Stay tuned! We're working on something awesome for you.

10 |
11 | 12 | -------------------------------------------------------------------------------- /fundamentals/code-quality/ja/code/community.md: -------------------------------------------------------------------------------- 1 | --- 2 | comments: false 3 | --- 4 | 5 | # みんなで作る 6 | 7 | `Frontend Fundamentals`(FF)は、コミュニティと共に良いコードの基準を作り上げています。 8 | 9 | 現在はTossのフロントエンドチャプターが運営をしています。 10 | 11 | ## 良い議論をまとめて見る 12 | 13 | コミュニティで行われた良い議論を見てみましょう。Frontend Fundamentalsのドキュメントに掲載されている内容を超えて、良いコードについての考えを広げることができます。 14 | 15 | - [良い議論をまとめて見る](https://github.com/toss/frontend-fundamentals/discussions?discussions_q=is%3Aopen+label%3A%22성지+⛲%22) 16 | 17 | ## 悩んでいるコードについて議論する 18 | 19 | 悩んでいるコードがある場合は、GitHubディスカッションに投稿してみましょう。自分のコードについてコミュニティから様々なレビューを受けることができ、良いコードの基準についてコミュニティと共に考えることができます。また、多くの共感を得た事例は、直接Frontend Fundamentalsの文書に投稿することができます。貢献方法は後日公開される予定です。 20 | 21 | - [Githubディスカッションに投稿する](https://github.com/toss/frontend-fundamentals/discussions) 22 | 23 | ## 良いコードの基準に意見を追加する 24 | 25 | 良いコードの基準について意見がある場合や、新しい意見を追加したい場合は、どのコードがより良いか投票し、意見を残してください。コミュニティと交流しながら、より豊かで深い基準を作り上げていきましょう。 26 | 27 | このコードは良いのか?あのコードは良いのか?ということについて、自分自身の基準を確立するきっかけになるかもしれません。 28 | 29 | - [A vs Bに投稿されたコードを見る](https://github.com/toss/frontend-fundamentals/discussions/categories/a-vs-b) 30 | -------------------------------------------------------------------------------- /fundamentals/code-quality/ja/code/examples/code-directory.md: -------------------------------------------------------------------------------- 1 | # 一緒に修正されるファイルは同じディレクトリに置く 2 | 3 |
4 | 5 |
6 | 7 | プロジェクトでコードを作成していると、Hook、コンポーネント、ユーティリティ関数などを複数のファイルに分けて管理することになります。こうしたファイルを簡単に作成、検索、削除できるように、適切なディレクトリ構造を確立することが重要です。 8 | 9 | 関連するソースファイルを同じディレクトリに配置することで、コードの依存関係を明確に示すことができます。これにより、参照してはいけないファイルを誤って参照するのを防ぎ、関連するファイルを一度に削除することも可能になります。 10 | 11 | ## 📝 コード例 12 | 13 | 次のコードは、プロジェクトのすべてのファイルをモジュールの種類(Presentational コンポーネント、Container コンポーネント、Hook、定数など)に応じて分類したディレクトリ構造です。 14 | 15 | ```text 16 | └─ src 17 | ├─ components 18 | ├─ constants 19 | ├─ containers 20 | ├─ contexts 21 | ├─ remotes 22 | ├─ hooks 23 | ├─ utils 24 | └─ ... 25 | ``` 26 | 27 | ## 👃 コードの不吉な臭いを嗅いでみる 28 | 29 | ### 凝集度 30 | 31 | ファイルをこのように種類別に分けると、どのコードがどのコードを参照しているのかを簡単に確認できなくなります。コードファイル間の依存関係を開発者が自らコードを分析しながら把握する必要があります。 32 | また、特定のコンポーネントや Hook、ユーティリティ関数が不要になり削除する場合、関連するコードが一緒に削除されずに残ってしまうこともあります。 33 | 34 | プロジェクトの規模は時間とともに大きくなることが一般的で、プロジェクトのサイズが 2 倍、10 倍、100 倍と大きくなるにつれて、コード間の依存関係も同時に複雑になる可能性があります。1 つのディレクトリに 100 を超えるファイルが含まれることもあるでしょう。 35 | 36 | ## ✏️ リファクタリングしてみる 37 | 38 | 次のコードは、関連して修正されるコードファイルを一つのディレクトリにまとめるように構造を改善した例です。 39 | 40 | ```text 41 | └─ src 42 | │ // プロジェクト全体で使われるコード 43 | ├─ components 44 | ├─ containers 45 | ├─ hooks 46 | ├─ utils 47 | ├─ ... 48 | │ 49 | └─ domains 50 | │ // Domain1でのみ使われるコード 51 | ├─ Domain1 52 | │ ├─ components 53 | │ ├─ containers 54 | │ ├─ hooks 55 | │ ├─ utils 56 | │ └─ ... 57 | │ 58 | │ // Domain2でのみ使われるコード 59 | └─ Domain2 60 | ├─ components 61 | ├─ containers 62 | ├─ hooks 63 | ├─ utils 64 | └─ ... 65 | ``` 66 | 67 | 一緒に修正されるコードファイルを一つのディレクトリに配置すれば、コード間の依存関係を把握しやすくなります。 68 | 69 | 例えば、次のようにあるドメイン(`Domain1`)の下にあるコードで別のドメイン(`Domain2`)のソースコードを参照していると考えてみましょう。 70 | 71 | ```typescript 72 | import { useFoo } from '../../../Domain2/hooks/useFoo' 73 | ``` 74 | 75 | このような import 文に遭遇した場合、誤ったファイルを参照していることを容易に認識できるようになります。 76 | 77 | また、特定の機能に関連するコードを削除する際に、1 つのディレクトリ全体を削除すれば、すべてのコードがきれいに削除されるため、プロジェクト内に不要なコードが残らないようにすることができます。 78 | -------------------------------------------------------------------------------- /fundamentals/code-quality/ja/code/examples/condition-name.md: -------------------------------------------------------------------------------- 1 | # 複雑な条件に名前を付ける 2 | 3 |
4 | 5 |
6 | 7 | 複雑な条件式が何の名前もなく使用されると、その条件が意味することを一目で把握するのが難しくなります。 8 | 9 | ## 📝 コード例 10 | 11 | 次のコードは、商品の中でカテゴリーと価格範囲が一致する商品だけをフィルタリングするロジックです。 12 | 13 | ```typescript 14 | const result = products.filter((product) => 15 | product.categories.some( 16 | (category) => 17 | category.id === targetCategory.id && 18 | product.prices.some((price) => price >= minPrice && price <= maxPrice) 19 | ) 20 | ); 21 | ``` 22 | 23 | ## 👃 コードの不吉な臭いを嗅いでみる 24 | 25 | ### 可読性 26 | 27 | このコードでは、匿名関数と条件が複雑に絡み合っています。`filter`、`some`、`&&`などのロジックが何重にもなっているため、正確な条件を把握するのが難しくなっています。 28 | 29 | コードを読む人が一度に考慮しなければならないコンテキストが多いため、可読性が低下しています。[^1] 30 | 31 | [^1]: [プログラマー脳](https://www.amazon.co.jp/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9E%E3%83%BC%E8%84%B3-%EF%BD%9E%E5%84%AA%E3%82%8C%E3%81%9F%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9E%E3%83%BC%E3%81%AB%E3%81%AA%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E8%AA%8D%E7%9F%A5%E7%A7%91%E5%AD%A6%E3%81%AB%E5%9F%BA%E3%81%A5%E3%81%8F%E3%82%A2%E3%83%97%E3%83%AD%E3%83%BC%E3%83%81-Felienne-Hermans/dp/4798068535)によると、人間の脳が一度に保存できる情報の数は 6 個だそうです。 32 | 33 | ## ✏️ リファクタリングしてみる 34 | 35 | 次のコードのように条件に明示的な名前を付けると、コードを読む人が一度に考慮しなければならないコンテキストを減らすことができます。 36 | 37 | ```typescript 38 | const matchedProducts = products.filter((product) => { 39 | return product.categories.some((category) => { 40 | const isSameCategory = category.id === targetCategory.id; 41 | const isPriceInRange = product.prices.some( 42 | (price) => price >= minPrice && price <= maxPrice 43 | ); 44 | 45 | return isSameCategory && isPriceInRange; 46 | }); 47 | }); 48 | ``` 49 | 50 | 明示的に同じカテゴリーに属し、価格範囲が一致する商品でフィルタリングするように書くことで、複雑な条件式を追わなくてもコードの意図を明確に示すことができます。 51 | 52 | ## 🔍 さらに詳しく: 条件式に名前を付ける基準 53 | 54 | いつ条件式や関数に名前を付けて分離するのが良いのでしょうか? 55 | 56 | ### 条件に名前を付けるべき時 57 | 58 | - **複雑なロジックを扱う時**: 条件文や関数で複雑なロジックが複数行にわたって処理される場合、名前を付けて関数の役割を明確にしたほうが良いです。こうすることによってコードの可読性が向上し、保守やコードレビューが容易になります。 59 | 60 | - **再利用性が必要な時**: 同じロジックを複数の場所で繰り返し使用する可能性がある場合、変数や関数を宣言して再利用できます。これによりコードの重複を減らし、保守が容易になります。 61 | 62 | - **単体テストが必要な時**: 関数を分離すると、独立して単体テストを作成できます。単体テストは関数が正しく動作しているかを簡単に確認でき、特に複雑なロジックをテストする際に有効です。 63 | 64 | ### 条件に名前を付けなくても良い時 65 | 66 | - **ロジックが簡単な時**: ロジックが非常に簡単な場合は、わざわざ名前を付ける必要はありません。例えば、配列の要素を単に 2 倍にする `arr.map(x => x * 2)`のようなコードは、名前を付けなくても直観的です。 67 | 68 | - **一度だけしか使用しない時**: 特定のロジックがコード内で一度だけ使用され、そのロジックが複雑でない場合、匿名関数で直接ロジックを処理する方が直観的かもしれません。 69 | -------------------------------------------------------------------------------- /fundamentals/code-quality/ja/code/examples/error-boundary.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/code-quality/ja/code/examples/error-boundary.md -------------------------------------------------------------------------------- /fundamentals/code-quality/ja/code/examples/hidden-logic.md: -------------------------------------------------------------------------------- 1 | # 隠れたロジックを露呈させる 2 | 3 |
4 | 5 |
6 | 7 | 関数やコンポーネントの名前、パラメータ、返り値に明示されていない隠れたロジックがある場合、一緒に開発をしているチームメンバーがその挙動を予測するのが困難になる可能性があります。 8 | 9 | ## 📝 コード例 10 | 11 | 次のコードは、ユーザーの口座残高を照会する際に使用できる`fetchBalance`関数です。この関数を呼び出すたびに、暗黙的に `balance_fetched`というロギングが行われています。 12 | 13 | ```typescript 4 14 | async function fetchBalance(): Promise { 15 | const balance = await http.get("..."); 16 | 17 | logging.log("balance_fetched"); 18 | 19 | return balance; 20 | } 21 | ``` 22 | 23 | ## 👃 コードの不吉な臭いを嗅いでみる 24 | 25 | ### 予測可能性 26 | 27 | `fetchBalance`関数の名前と返り値の型だけでは、`balance_fetched`というロギングが行われるかどうか分かりません。そのため、ロギングを望まない場所でもロギングが行われてしまう可能性があります。 28 | 29 | また、ロギングのロジックにエラーが発生した場合、突然口座残高を取得するロジックが壊れてしまう可能性もあるでしょう。 30 | 31 | ## ✏️ リファクタリングしてみる 32 | 33 | 関数の名前、パラメータ、返り値の型から予測できるロジックだけを実装部分に残してください。 34 | 35 | ```typescript 36 | async function fetchBalance(): Promise { 37 | const balance = await http.get("..."); 38 | 39 | return balance; 40 | } 41 | ``` 42 | 43 | そして、ロギングを行うコードは別に分離してください。 44 | 45 | ```tsx 46 | 56 | ``` 57 | -------------------------------------------------------------------------------- /fundamentals/code-quality/ja/code/examples/http.md: -------------------------------------------------------------------------------- 1 | # 名前が被らないように管理する 2 | 3 |
4 | 5 |
6 | 7 | 同じ名前を持つ関数や変数は、同じ挙動をするべきです。小さな挙動の違いはコードの予測可能性を低下させ、コードを読む人に誤解を招く可能性があります。 8 | 9 | ## 📝  コード例 10 | 11 | とあるフロントエンドサービスで、元々使用していたHTTPライブラリをラップして新しい形でHTTPリクエストを送るモジュールを作成しました。 12 | 偶然にも、元のHTTPライブラリと新しく作成したHTTPモジュールの名前は`http`という同じ名前です。 13 | 14 | ::: code-group 15 | 16 | ```typescript [http.ts] 17 | // このサービスは`http`というライブラリを使っています 18 | import { http as httpLibrary } from "@some-library/http"; 19 | 20 | export const http = { 21 | async get(url: string) { 22 | const token = await fetchToken(); 23 | 24 | return httpLibrary.get(url, { 25 | headers: { Authorization: `Bearer ${token}` } 26 | }); 27 | } 28 | }; 29 | ``` 30 | 31 | ```typescript [fetchUser.ts] 32 | // http.tsで定義したhttpを持ってくるコード 33 | import { http } from "./http"; 34 | 35 | export async function fetchUser() { 36 | return http.get("..."); 37 | } 38 | ``` 39 | 40 | ::: 41 | 42 | ## 👃 コードの不吉な臭いを嗅いでみる 43 | 44 | ### 予測可能性 45 | 46 | このコードは機能的には問題ありませんが、読む人に誤解を与える可能性があります。`http.get`を呼び出す開発者は、この関数が元のHTTPライブラリが行うような単純なGETリクエストを送信することを期待すると思いますが、実際にはトークンを取得する追加の処理が行われます。 47 | 48 | 誤解によって期待される挙動と実際の挙動の間にギャップが生じ、バグが発生したり、デバッグプロセスが複雑で混乱を招く可能性があります。 49 | 50 | ## ✏️ リファクタリングしてみる 51 | 52 | サービスで作成した関数には、ライブラリの関数名と区別できる明確な名前を使用することで、関数の挙動を予測可能にすることができます。 53 | 54 | ::: code-group 55 | 56 | ```typescript [httpService.ts] 57 | // このサービスは`http`というライブラリを使っています 58 | import { http as httpLibrary } from "@some-library/http"; 59 | 60 | // ライブラリの関数名と区別されるようにネーミングを変えました 61 | export const httpService = { 62 | async getWithAuth(url: string) { 63 | const token = await fetchToken(); 64 | 65 | // トークンをヘッダーに追加するなど認証のロジックを追加します 66 | return httpLibrary.get(url, { 67 | headers: { Authorization: `Bearer ${token}` } 68 | }); 69 | } 70 | }; 71 | ``` 72 | 73 | ```typescript [fetchUser.ts] 74 | // httpService.tsで定義したhttpServiceを持ってくるコード 75 | import { httpService } from "./httpService"; 76 | 77 | export async function fetchUser() { 78 | // 関数名から、この関数が認証済みのリクエストを送ることが分かります。 79 | return await httpService.getWithAuth("..."); 80 | } 81 | ``` 82 | 83 | ::: 84 | 85 | こうすることで、関数の名前を見たときに挙動を誤解する可能性を減らすことができます。 86 | 他の開発者がこの関数を使用する際に、サービスで定義された関数であることを認識し、正しく使用できるようになります。 87 | 88 | また、`getWithAuth`という名前にすることで、この関数が認証済みのリクエストを送信することを明確に伝えることができます。 89 | -------------------------------------------------------------------------------- /fundamentals/code-quality/ja/code/examples/magic-number-cohesion.md: -------------------------------------------------------------------------------- 1 | # マジックナンバーを排除する 2 | 3 |
4 | 5 |
6 | 7 | **マジックナンバー**(Magic Number)とは、正確な意味を説明せずにソースコードの中に直接数字の値を入れることを指します。 8 | 9 | 例えば、見つからないことを示すHTTPステータスコードとして`404`の値をそのまま使用することや、 10 | 1日を表す`86400`秒をそのまま使用することがこれに該当します。 11 | 12 | ## 📝 コード例 13 | 14 | 次のコードは、いいねボタンを押した時にいいねの数を新しく取得する関数です。 15 | 16 | ```typescript 3 17 | async function onLikeClick() { 18 | await postLike(url); 19 | await delay(300); 20 | await refetchPostLike(); 21 | } 22 | ``` 23 | 24 | ## 👃 コードの不吉な臭いを嗅いでみる 25 | 26 | ### 凝集度 27 | 28 | `300`という数字がアニメーションの完了を待つために使われている場合、再生するアニメーションを変更した際に、サービスが予期せず壊れるリスクがあります。十分な時間を置かずに、アニメーションが終了する前に次のロジックが始まってしまう可能性もあります。 29 | 30 | また、修正が必要なコードの片方だけが変更されることから、凝集度が低いコードとも言えます。 31 | 32 | ::: info 33 | 34 | この Hook は[可読性](./magic-number-readability.md)の観点からも考えることができます。 35 | 36 | ::: 37 | 38 | ## ✏️ リファクタリングしてみる 39 | 40 | 数字の`300`というコンテキストを正確に示すために、定数の`ANIMATION_DELAY_MS`として宣言することができます。 41 | 42 | ```typescript 1,5 43 | const ANIMATION_DELAY_MS = 300; 44 | 45 | async function onLikeClick() { 46 | await postLike(url); 47 | await delay(ANIMATION_DELAY_MS); 48 | await refetchPostLike(); 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /fundamentals/code-quality/ja/code/examples/magic-number-readability.md: -------------------------------------------------------------------------------- 1 | # マジックナンバーに名前を付ける 2 | 3 |
4 | 5 |
6 | 7 | **マジックナンバー**(Magic Number)とは、正確な意味を説明せずにソースコードの中に直接数字の値を入れることを指します。 8 | 9 | 例えば、見つからないことを示すHTTPステータスコードとして`404`の値をそのまま使用することや、 10 | 1日を表す`86400`秒をそのまま使用することがこれに該当します。 11 | 12 | ## 📝 コード例 13 | 14 | 次のコードは、いいねボタンを押した時にいいねの数を新しく取得する関数です。 15 | 16 | ```typescript 3 17 | async function onLikeClick() { 18 | await postLike(url); 19 | await delay(300); 20 | await refetchPostLike(); 21 | } 22 | ``` 23 | 24 | ## 👃 コードの不吉な臭いを嗅いでみる 25 | 26 | ### 可読性 27 | 28 | このコードは、`delay`関数に渡された`300`という値がどのようなコンテキストで使われているのかが不明です。 29 | 元のコードを書いた開発者でなければ、300msの待機がどのような目的で行われているのかは分かりません。 30 | 31 | - アニメーションが完了するまで待機しているのか? 32 | - いいねを反映させるのに時間がかかって待機しているのか? 33 | - テストコードをうっかり削除し忘れただけなのか? 34 | 35 | 複数の開発者が1つのコードを共同で修正していく中で、その意図が正確に理解できないと、コードが意図しない方向に修正される可能性があります。 36 | 37 | ::: info 38 | 39 | この Hook は[凝集度](./magic-number-cohesion.md)の観点からも考えることができます。 40 | 41 | ::: 42 | 43 | ### ✏️ リファクタリングしてみる 44 | 45 | 数字の`300`というコンテキストを正確に示すために、定数の`ANIMATION_DELAY_MS`として宣言することができます。 46 | 47 | ```typescript 1,5 48 | const ANIMATION_DELAY_MS = 300; 49 | 50 | async function onLikeClick() { 51 | await postLike(url); 52 | await delay(ANIMATION_DELAY_MS); 53 | await refetchPostLike(); 54 | } 55 | ``` 56 | 57 | ## 🔍 もっと調べてみる 58 | 59 | マジックナンバーは凝集度の観点からも考えることができます。[マジックナンバーを排除する](./magic-number-cohesion.md)のドキュメントも参考にしてみてください。 60 | -------------------------------------------------------------------------------- /fundamentals/code-quality/ja/code/examples/submit-button.md: -------------------------------------------------------------------------------- 1 | # 一緒に実行されないコードを分離する 2 | 3 |
4 | 5 |
6 | 7 | 同時に実行されないコードが1つの関数やコンポーネントにあると、挙動を一目で把握するのが難しくなります。 8 | 実装部分に多くの分岐が含まれていると、どのような役割を果たしているのか理解しづらくなります。 9 | 10 | ## 📝 コード例 11 | 12 | 次の``コンポーネントは、ユーザーの権限に応じて異なる挙動をします。 13 | 14 | - ユーザーの権限が閲覧専用(`"viewer"`)の場合、招待ボタンは無効化されており、アニメーションも再生されません。 15 | - ユーザーが一般ユーザーの場合、招待ボタンを使用でき、アニメーションも再生されます。 16 | 17 | ```tsx 18 | function SubmitButton() { 19 | const isViewer = useRole() === "viewer"; 20 | 21 | useEffect(() => { 22 | if (isViewer) { 23 | return; 24 | } 25 | showButtonAnimation(); 26 | }, [isViewer]); 27 | 28 | return isViewer ? ( 29 | Submit 30 | ) : ( 31 | 32 | ); 33 | } 34 | ``` 35 | 36 | ## 👃 コードの不吉な臭いを嗅いでみる 37 | 38 | ### 可読性 39 | 40 | ``コンポーネントでは、ユーザーが持つ2つの権限状態を1つのコンポーネント内で同時に処理しています。そのため、コードを読む人が一度に考慮しなければならないコンテキストが多くなります。 41 | 42 | 例えば、次のコードで青色の部分はユーザーが閲覧専用権限(`'viewer'`)を持っているときに実行されるコードで、赤色の部分は一般ユーザーの場合に実行されるコードです。同時に実行されないコードが交差して現れるため、コードを理解するのが難しくなってしまっています。 43 | 44 | ![](../../../images/examples/submit-button.png){.light-only} 45 | ![](../../../images/examples/submit-button-dark.png){.dark-only} 46 | 47 | ## ✏️ リファクタリングしてみる 48 | 49 | 次のコードは、ユーザーが閲覧専用権限を持つ場合と一般ユーザーの場合を完全に分けて管理するようにしたものです。 50 | 51 | ```tsx 52 | function SubmitButton() { 53 | const isViewer = useRole() === "viewer"; 54 | 55 | return isViewer ? : ; 56 | } 57 | 58 | function ViewerSubmitButton() { 59 | return Submit; 60 | } 61 | 62 | function AdminSubmitButton() { 63 | useEffect(() => { 64 | showAnimation(); 65 | }, []); 66 | 67 | return ; 68 | } 69 | ``` 70 | 71 | - ``のコードの至る所にあった分岐が1つにまとめられ、分岐が減りました。 72 | - ``と``では1つの分岐のみを管理しているため、コードを読む人が一度に考慮しなければならないコンテキストが少なくなります。 73 | -------------------------------------------------------------------------------- /fundamentals/code-quality/ja/code/examples/ternary-operator.md: -------------------------------------------------------------------------------- 1 | # 三項演算子をシンプルにする 2 | 3 |
4 | 5 |
6 | 7 | 三項演算子を複雑に使用すると、条件の構造が明確に見えなくなり、コードが読みづらくなることがあります。 8 | 9 | ## 📝 コード例 10 | 11 | 次のコードは、`A条件`と`B条件`に応じて、`BOTH`、`A`、または`NONE`のいずれかを`status`に指定するコードです。 12 | 13 | ```typescript 14 | const status = 15 | A条件 && B条件 ? "BOTH" : どちらも違う場合 ? "NONE" : A条件 ? "A" : undefined; 16 | ``` 17 | 18 | ## 👃 コードの不吉な臭いを嗅いでみる 19 | 20 | ### 可読性 21 | 22 | このコードは複数の三項演算子がネストされて使用されているため、正確にどの条件で値が計算されているのかを一目で把握するのが難しいです。 23 | 24 | ## ✏️ リファクタリングしてみる 25 | 26 | 次のように条件を`if文`で展開すると、より明確で簡潔に条件を示すことができます。 27 | 28 | ```typescript 29 | const status = (() => { 30 | if (A条件 && B条件) { 31 | return "BOTH"; 32 | } 33 | 34 | if (A条件) { 35 | return "A条件"; 36 | } 37 | 38 | return "NONE"; 39 | })(); 40 | ``` 41 | -------------------------------------------------------------------------------- /fundamentals/code-quality/ja/code/examples/use-bottom-sheet.md: -------------------------------------------------------------------------------- 1 | # 重複コードを許容する 2 | 3 |
4 | 5 |
6 | 7 | 開発者として、複数のページやコンポーネントにわたる重複コードを1つのHookやコンポーネントに共通化することがよくあります。 8 | 重複コードを1つのコンポーネントやHookに共通化することで、良いコードの特徴の1つである凝集度を高め、共に修正が必要なコードを一度に修正することができます。 9 | 10 | しかし、不要な結合度が生じることで、共通コンポーネントやHookを修正する際に影響を受けるコードの範囲が広がり、逆に修正が難しくなることもあります。 11 | 12 | 最初は似たような動作すると考えて共通化したコードが、後にページごとに異なる特異な仕様が生じて、次第に複雑化する可能性があります。 13 | 同時に、共通コードを修正するたびに、そのコードに依存するコードを一つ一つ適切にテストしなければならず、逆にコード修正が難しくなることもあります。 14 | 15 | ## 📝 コード例 16 | 17 | 次のように、点検情報を引数として受け取り、点検中であれば点検のボトムシートを開き、ユーザーが通知を受け取ることに同意した場合はそれをログに記録し、現在の画面を閉じるHookを見てみましょう。 18 | 19 | ```typescript 20 | export const useOpenMaintenanceBottomSheet = () => { 21 | const maintenanceBottomSheet = useMaintenanceBottomSheet(); 22 | const logger = useLogger(); 23 | 24 | return async (maintainingInfo: TelecomMaintenanceInfo) => { 25 | logger.log("点検のボトムシートを開く"); 26 | const result = await maintenanceBottomSheet.open(maintainingInfo); 27 | if (result) { 28 | logger.log("点検のボトムシートの通知を受け取るをクリック"); 29 | } 30 | closeView(); 31 | }; 32 | }; 33 | ``` 34 | 35 | このコードは複数のページで繰り返し使用されたため、共通のHookとして分離されました。 36 | 37 | ## 👃 コードの不吉な臭いを嗅いでみる 38 | 39 | ### 結合度 40 | 41 | このHookは複数のページで繰り返し見られるロジックであるため、共通化されました。しかし、将来生じる可能性のあるさまざまなコード変更を考慮する必要があります。 42 | 43 | - もしページごとにロギングする値が異なる場合は? 44 | - もしあるページでは点検ボトムシートを閉じても画面を閉じる必要がない場合は? 45 | - ボトムシートで表示されるテキストや画像を異なるものにする必要がある場合は? 46 | 47 | このHookはこうしたコード変更に柔軟に対応するために、複雑に引数を受け取る必要があるでしょう。 48 | また、このHookの実装を修正するたびに、このHookを使用しているすべてのページが正常に動作するかどうかをテストしなければなりません。 49 | 50 | ## ✏️ リファクタリングしてみる 51 | 52 | 一見すると繰り返しのように見えるコードでも、重複コードを許可することが良い方向性である場合があります。 53 | 54 | チームメンバーと積極的にコミュニケーションを取りながら、点検ボトムシートの動作を正確に理解する必要があります。 55 | もしページでロギングする値が同じで、点検ボトムシートの動作や見た目が同じであり、今後もそうであるならば、共通化することでコードの凝集度を高めることができます。 56 | 57 | しかし、ページごとに動作が異なる可能性がある場合は、共通化せずに重複コードを許可する方が良い選択肢となるでしょう。 58 | -------------------------------------------------------------------------------- /fundamentals/code-quality/ja/code/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | comments: false 3 | --- 4 | 5 | # 良いコードの4つの原則 6 | 7 | 良いフロントエンドコードは **変更しやすい** コードです。 8 | 新しい仕様を実装する際に、既存のコードを修正してリリースしやすいコードが良いコードだと言えます。 9 | コードが変更しやすいかどうかは4つの基準で判断できます。 10 | 11 | ## 1. 可読性 12 | 13 | **可読性**(Readability)は、コードがどれだけ読みやすいかを指します。コードを変更しやすくするためには、まずそのコードがどのように動くのかを理解する必要があります。読みやすいコードは、読む人が考えることを最小限に抑え、上から下へと自然に流れるように実装されています。 14 | 15 | ### 可読性を向上させる戦略 16 | 17 | - **コンテキストを減らす** 18 | - [一緒に実行されないコードを分離する](./examples/submit-button.md) 19 | - [実装の詳細を抽象化する](./examples/login-start-page.md) 20 | - [ロジックの種類に応じて一体化している関数を分ける](./examples/use-page-state-readability.md) 21 | - **名前付け** 22 | - [複雑な条件に名前を付ける](./examples/condition-name.md) 23 | - [マジックナンバーに名前を付ける](./examples/magic-number-readability.md) 24 | - **上から下へ読めるようにする** 25 | - [視点の移動を減らす](./examples/user-policy.md) 26 | - [三項演算子をシンプルにする](./examples/ternary-operator.md) 27 | 28 | ## 2. 予測可能性 29 | 30 | **予測可能性**(Predictability)とは、一緒に働くチームメンバーたちが関数やコンポーネントの挙動をどれだけ予測できるかを指します。 31 | 予測可能性が高いコードは、一貫したルールに従い、関数やコンポーネントの名前、パラメータ、返り値だけを見てもどのような挙動をするのかが分かります。 32 | 33 | ### 予測可能性を高める戦略 34 | 35 | - [名前が被らないように管理する](./examples/http.md) 36 | - [同じ種類の関数は返り値の型を統一する](./examples/use-user.md) 37 | - [隠れたロジックを露呈させる](./examples/hidden-logic.md) 38 | 39 | ## 3. 凝集度 40 | 41 | **凝集度**(Cohesion)とは、修正されるべきコードが常に一緒に修正されるかどうかを指します。凝集度が高いコードは、コードの一部を修正しても意図せず他の部分でエラーが発生しません。これは、一緒に修正されるべき部分が必ず一緒に修正されるようにコードが実装されているためです。 42 | 43 | ::: info 可読性と凝集度は相反することがあります 44 | 45 | 一般的に、凝集度を高めるためには変数や関数を抽象化するなど、可読性を低下させる決断をする必要があります。 46 | 一緒に修正されないとエラーが発生する可能性がある場合には、凝集度を優先してコードを共通化・抽象化してください。 47 | リスクが高くない場合には、可読性を優先してコードの重複を許可してください。 48 | 49 | ::: 50 | 51 | ### 凝集度を高める戦略 52 | 53 | - [一緒に修正されるファイルは同じディレクトリに置く](./examples/code-directory.md) 54 | - [マジックナンバーを排除する](./examples/magic-number-cohesion.md) 55 | - [フォームの凝集度について考える](./examples/form-fields.md) 56 | 57 | ## 4. 結合度 58 | 59 | **結合度**(Coupling)とは、コードを修正したときの影響範囲を指します。コードを修正した際に影響範囲が小さく、変更に伴う影響を予測できるコードが、修正しやすいコードと言えます。 60 | 61 | ### 結合度を下げる戦略 62 | 63 | - [責任を一つずつ管理する](./examples/use-page-state-coupling.md) 64 | - [重複コードを許容する](./examples/use-bottom-sheet.md) 65 | - [Props Drilling を解消する](./examples/item-edit-modal.md) 66 | 67 | ## コード品質を多角的に見る 68 | 69 | 残念ながら、この 4つの基準をすべて同時に満たすことは難しいです。 70 | 71 | 例えば、関数や変数を常に一緒に修正できるように共通化や抽象化を行うと、凝集度が高まります。しかし、その結果としてコードが一度抽象化されるため、可読性が低下します。 72 | 73 | 重複コードを許容することで、コードの影響範囲を減らし、結合度を下げることができますが、片方を修正した時にもう片方を誤って修正し忘れてしまう可能性があるため、凝集度が低下します。 74 | 75 | フロントエンド開発者は、現在直面している状況を考慮し、よく考えた上で、長期的にコードの修正が容易になるにはどれを優先すべきかを検討する必要があります。 76 | -------------------------------------------------------------------------------- /fundamentals/code-quality/ja/code/start.md: -------------------------------------------------------------------------------- 1 | --- 2 | comments: false 3 | --- 4 | 5 | # はじめる 6 | 7 | `Frontend Fundamentals`は、フロントエンドコードのベストプラクティスを提供します。フロントエンド開発者としてコードの品質を向上させたい時に、方向を見つけるためのコンパスのように活用してみてください。 8 | 9 | 良いコードに関する[4 つの原則](./index.md)とともに、具体的な例と解決策を提示します。 10 | 11 | ## こんな時に活用してみてください 12 | 13 | - 🦨 コードについて悩んでいるけれど **論理的に説明できない開発者** 14 | - 👀 **悪いコードを素早く見つけて**改善する方法を学びたい開発者 15 | - 🤓 コードレビューなどで他の誰かに指摘してもらうことで、自分のコードがどうなっているのかを**客観的に認識**したい開発者。 16 | - 👥 **チームメンバーと一緒に** 共通のコーディングスタイルとコード品質の基準を決めたい開発者 17 | 18 | ## 制作者 19 | 20 | - [milooy](https://github.com/milooy) 21 | - [donghyeon](https://github.com/kimbangg) 22 | - [chkim116](https://github.com/chkim116) 23 | - [inseong.you](https://github.com/inseong.you) 24 | - [raon0211](https://github.com/raon0211) 25 | - [bigsaigon333](https://github.com/bigsaigon333) 26 | - [jho2301](https://github.com/jho2301) 27 | - [KimChunsick](https://github.com/KimChunsick) 28 | - [jennybehan](https://github.com/jennybehan) 29 | 30 | ## ドキュメント貢献者 31 | 32 | - [andy0414](https://github.com/andy0414) 33 | - [pumpkiinbell](https://github.com/pumpkiinbell) 34 | - [jennybehan](https://github.com/jennybehan) 35 | -------------------------------------------------------------------------------- /fundamentals/code-quality/ja/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Frontend Fundamentals" 7 | tagline: "簡単に変更できるフロントエンドコードのガイドライン" 8 | image: 9 | src: /images/ff-symbol-gradient-webp-80.webp 10 | alt: Frontend Fundamentals symbol 11 | actions: 12 | - text: 良いコードについて学ぶ 13 | link: /ja/code/ 14 | - theme: alt 15 | text: コミュニケーション 16 | link: /ja/code/community 17 | 18 | features: 19 | - icon: 🤓 20 | title: コードレビューのスキルを向上させる 21 | details: コードが簡単に変更できるかどうかを判断するための原則を探ります。 22 | - icon: 🤝 23 | title: より良いコードレビューを行う 24 | details: 様々なコード改善のケースを積極的に探ります。 25 | - icon: 📝 26 | title: 自分のコードが気になりますか? 27 | details: GitHubのディスカッションで他の開発者とコミュニケーションを取りましょう。 28 | --- 29 | -------------------------------------------------------------------------------- /fundamentals/code-quality/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@frontend-fundamentals/code-quality", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "docs:dev": "vitepress dev .", 8 | "docs:build": "vitepress build .", 9 | "docs:preview": "vitepress preview ." 10 | }, 11 | "dependencies": { 12 | "@amplitude/analytics-browser": "^2.11.11", 13 | "markdown-it-footnote": "^4.0.0", 14 | "typescript": "^5.6.3", 15 | "vitepress": "^1.4.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /fundamentals/code-quality/public/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/code-quality/public/images/apple-touch-icon.png -------------------------------------------------------------------------------- /fundamentals/code-quality/public/images/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/code-quality/public/images/favicon-96x96.png -------------------------------------------------------------------------------- /fundamentals/code-quality/public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/code-quality/public/images/favicon.ico -------------------------------------------------------------------------------- /fundamentals/code-quality/public/images/ff-meta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/code-quality/public/images/ff-meta.png -------------------------------------------------------------------------------- /fundamentals/code-quality/public/images/ff-symbol-gradient-webp-80.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/code-quality/public/images/ff-symbol-gradient-webp-80.webp -------------------------------------------------------------------------------- /fundamentals/code-quality/public/images/ff-symbol-gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/code-quality/public/images/ff-symbol-gradient.png -------------------------------------------------------------------------------- /fundamentals/code-quality/public/images/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Frontend Fundamentals", 3 | "short_name": "FF", 4 | "icons": [ 5 | { 6 | "src": "/images/web-app-manifest-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/images/web-app-manifest-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } 22 | -------------------------------------------------------------------------------- /fundamentals/code-quality/public/images/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/code-quality/public/images/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /fundamentals/code-quality/public/images/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/code-quality/public/images/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /fundamentals/code-quality/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist", 6 | "composite": true 7 | }, 8 | "include": [ 9 | ".vitepress/**/*", 10 | "**/*.ts", 11 | "**/*.tsx", 12 | "**/*.vue" 13 | ] 14 | } -------------------------------------------------------------------------------- /fundamentals/code-quality/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/index.html" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /fundamentals/code-quality/zh-hans/code/coming-soon.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: doc 3 | title: Bundling 4 | description: Frontend Bundling Guide (Coming Soon) 5 | --- 6 | 7 |
8 |

✨ Coming Soon

9 |

Stay tuned! We're working on something awesome for you.

10 |
11 | 12 | -------------------------------------------------------------------------------- /fundamentals/code-quality/zh-hans/code/community.md: -------------------------------------------------------------------------------- 1 | --- 2 | comments: false 3 | --- 4 | 5 | # 一起构建 6 | 7 | `Frontend Fundamentals`(FF)与社区一起制定好代码的标准。 8 | 9 | 目前由 Toss 前端分部维护。 10 | 11 | ## 专题讨论 12 | 13 | 查看社区中的精彩讨论。超出《Frontend Fundamentals》 文档中的内容,拓宽你对好代码的思考。 14 | 15 | - [专题讨论](https://github.com/toss/frontend-fundamentals/discussions?discussions_q=is%3Aopen+label%3A%22%EC%84%B1%EC%A7%80+%E2%9B%B2%22) 16 | 17 | ## 探讨代码疑虑 18 | 19 | 如果你有令人疑惑的代码,可以将其发布到 GitHub 论坛上。 20 | 可以在社区中多角度审查你的代码,并与社区一起探讨好代码的标准。 21 | 22 | 受到广泛共鸣的案例可直接上传到 Frontend Fundamentals 文档中。贡献方法将会稍后公布。 23 | 24 | - [在 GitHub 论坛上发帖](https://github.com/toss/frontend-fundamentals/discussions) 25 | 26 | ## 为好代码标准添加意见 27 | 28 | 如果对好代码的标准有意见,或者想提出新的观点,可以参与投票选出你认为更好的代码,并留下自己的意见。与社区沟通,共同构建更加丰富而深入的标准。 29 | 30 | 这可以成为一个契机,帮助你确立判断两段代码之间哪一段更好的标准。 31 | 32 | - [查看 A vs B 上的代码](https://github.com/toss/frontend-fundamentals/discussions/categories/a-vs-b) 33 | -------------------------------------------------------------------------------- /fundamentals/code-quality/zh-hans/code/examples/code-directory.md: -------------------------------------------------------------------------------- 1 | # 需同时修改的文件位于同一目录下 2 | 3 |
4 | 5 |
6 | 7 | 在项目中编写代码时,通常会将 Hook、组件和工具函数等拆分到多个文件进行管理。为了更轻松地创建、查找和删除这些文件,设计一个合适的目录结构至关重要。 8 | 9 | 将需要一起修改的源文件放在同一目录下,可以更直观地展现代码的依赖关系。这不仅可以防止随意引用不应被引用的文件,还能一次性删除相关文件。 10 | 11 | ## 📝 代码示例 12 | 13 | 以下代码是按照模块类型(如 Presentational 组件、Container 组件、Hook、常量等)分类的目录结构。 14 | 15 | ```text 16 | └─ src 17 | ├─ components 18 | ├─ constants 19 | ├─ containers 20 | ├─ contexts 21 | ├─ remotes 22 | ├─ hooks 23 | ├─ utils 24 | └─ ... 25 | ``` 26 | 27 | ## 👃 闻代码 28 | 29 | ### 内聚性 30 | 31 | 如果按照这种方式按类划分文件,就很难确定代码之间的引用关系。代码文件之间的依赖关系需要开发者在分析代码时自行掌握和处理。 32 | 此外,如果某个组件、Hook 或工具函数不再使用而被删除,相关代码可能未被同时删除,从而留下未使用代码。 33 | 34 | 项目的规模往往会逐步扩大,随着项目规模的增加(比如 2 倍、10 倍甚至 100 倍),代码之间的依赖关系也将变得更加复杂。一个目录可能包含 100 多个文件。 35 | 36 | ## ✏️ 尝试改善 37 | 38 | 以下是一个实例,展示了如何通过将需要一起修改的代码文件放在同一目录下,来优化项目结构。 39 | 40 | ```text 41 | └─ src 42 | │ // 整个项目中使用的代码 43 | ├─ components 44 | ├─ containers 45 | ├─ hooks 46 | ├─ utils 47 | ├─ ... 48 | │ 49 | └─ domains 50 | │ // 只在 Domain1 中使用的代码 51 | ├─ Domain1 52 | │ ├─ components 53 | │ ├─ containers 54 | │ ├─ hooks 55 | │ ├─ utils 56 | │ └─ ... 57 | │ 58 | │ // 只在 Domain2 中使用的代码 59 | └─ Domain2 60 | ├─ components 61 | ├─ containers 62 | ├─ hooks 63 | ├─ utils 64 | └─ ... 65 | ``` 66 | 67 | 将一起修改的代码文件放在同一目录下,很容易理解代码之间的依赖关系。 68 | 69 | 例如,假设一个域(`Domain1`)的子代码中引用另一个域(`Domain2`)的源代码,如下所示: 70 | 71 | ```typescript 72 | import { useFoo } from '../../../Domain2/hooks/useFoo' 73 | ``` 74 | 75 | 如果遇到这样的 import 语句,就能很容易地意识到引用了错误的文件。 76 | 77 | 此外,当删除与特定功能相关的代码时,只需删除整个目录即可一并清理所有相关代码,从而确保项目中不会遗留未使用的代码。 78 | -------------------------------------------------------------------------------- /fundamentals/code-quality/zh-hans/code/examples/condition-name.md: -------------------------------------------------------------------------------- 1 | # 为复杂条件命名 2 | 3 |
4 | 5 |
6 | 7 | 如果复杂的条件表达式没有特定的命名,就很难一眼理解其含义。 8 | 9 | ## 📝 代码示例 10 | 11 | 下列代码实现了筛选类别和价格范围相匹配的商品的逻辑。 12 | 13 | ```typescript 14 | const result = products.filter((product) => 15 | product.categories.some( 16 | (category) => 17 | category.id === targetCategory.id && 18 | product.prices.some((price) => price >= minPrice && price <= maxPrice) 19 | ) 20 | ); 21 | ``` 22 | 23 | ## 👃 闻代码 24 | 25 | ### 可读性 26 | 27 | 在这段代码中,匿名函数和条件错综复杂。`filter`、`some`和`&&`等逻辑多层嵌套,导致很难确定正确的条件。 28 | 29 | 代码阅读者需要考虑的上下文过多,导致可读性变差。[^1] 30 | 31 | [^1]: [程序员超强大脑](https://product.dangdang.com/29567786.html)一书中提到,人的大脑一次性能够处理和存储的信息大约是 6 个。 32 | 33 | ## ✏️ 尝试改善 34 | 35 | 以下代码展示了如何给条件加上明确的名称,以减少代码阅读者需要考虑的语境。 36 | 37 | ```typescript 38 | const matchedProducts = products.filter((product) => { 39 | return product.categories.some((category) => { 40 | const isSameCategory = category.id === targetCategory.id; 41 | const isPriceInRange = product.prices.some( 42 | (price) => price >= minPrice && price <= maxPrice 43 | ); 44 | 45 | return isSameCategory && isPriceInRange; 46 | }); 47 | }); 48 | ``` 49 | 50 | 通过明确命名筛选同类且价格范围的商品条件,可以避免追踪复杂的条件表达式,清晰表达代码的意图。 51 | 52 | ## 🔍 深入了解:为条件式命名的标准 53 | 54 | 什么时候适合给条件表达式或函数命名并将其提取? 55 | 56 | ### 适合为条件命名的情况 57 | 58 | - **处理复杂逻辑时**:当条件语句或函数中的复杂逻辑跨越多行时,最好为其命名,明确展示函数的作用。这样可以提高代码可读性,维护和审查变得更加容易。 59 | 60 | - **需要重用时**:如果同一逻辑可能在多个地方反复使用,可以通过声明变量或函数来实现重用。这样可以减少代码重复,便于后续的维护。 61 | 62 | - **需要单元测试时**:分离函数后,可以独立编写单元测试。单元测试可以轻松验证函数是否正常工作,尤其在测试复杂逻辑时非常实用。 63 | 64 | ### 不需要为条件命名的情况 65 | 66 | - **当逻辑简单时**:如果逻辑非常简单,实际上不需要为其命名。例如,将数组中的元素翻倍的代码 `arr.map(x => x * 2)` ,即使不命名,也很直观。 67 | 68 | - **当只使用一次时**:如果某个逻辑在代码中只出现一次,而且逻辑并不复杂,那么在匿名函数中直接处理逻辑可能更加直观。 69 | -------------------------------------------------------------------------------- /fundamentals/code-quality/zh-hans/code/examples/error-boundary.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/fundamentals/code-quality/zh-hans/code/examples/error-boundary.md -------------------------------------------------------------------------------- /fundamentals/code-quality/zh-hans/code/examples/hidden-logic.md: -------------------------------------------------------------------------------- 1 | # 揭示隐藏的逻辑 2 | 3 |
4 | 5 |
6 | 7 | 如果函数或组件的名称、参数、返回值中存在未明确表达的隐藏逻辑,那么与你合作的同事可能会难以预测其行为。 8 | 9 | ## 📝 代码示例 10 | 11 | 下面的代码是一个名为 `fetchBalance` 的函数,用于查询用户的账户余额。每次调用函数时,都会隐式地启动名为 `balance_fetched` 的日志函数。 12 | 13 | ```typescript 4 14 | async function fetchBalance(): Promise { 15 | const balance = await http.get("..."); 16 | 17 | logging.log("balance_fetched"); 18 | 19 | return balance; 20 | } 21 | ``` 22 | 23 | ## 👃 闻代码 24 | 25 | ### 可预测性 26 | 27 | 仅根据 `fetchBalance` 函数的名称和返回类型,无法得知是否会记录名为 `balance_fetched` 的日志。因此,即使在不需要日志记录的地方,也可能会触发日志记录。 28 | 29 | 另外,如果日志记录逻辑出错,获取账户余额的功能也可能突然失效。 30 | 31 | ## ✏️ 尝试改善 32 | 33 | 请仅在实现部分保留可以通过函数名、参数和返回类型来预测的逻辑。 34 | 35 | ```typescript 36 | async function fetchBalance(): Promise { 37 | const balance = await http.get("..."); 38 | 39 | return balance; 40 | } 41 | ``` 42 | 43 | 请将日志记录的代码单独分离出来。 44 | 45 | ```tsx 46 | 56 | ``` 57 | -------------------------------------------------------------------------------- /fundamentals/code-quality/zh-hans/code/examples/http.md: -------------------------------------------------------------------------------- 1 | # 避免命名重复 2 | 3 |
4 | 5 |
6 | 7 | 具有相同名称的函数或变量应该具有相同的行为。微小的行为差异会降低代码的可预测性,并可能使代码阅读者感到困惑。 8 | 9 | ## 📝 代码示例 10 | 11 | 在某个前端服务中,通过封装原本使用的 HTTP 库,创建了一个以新形式发送 HTTP 请求的模块。 12 | 巧合的是,原本的 HTTP 库和新创建的 HTTP 模块名称相同,都叫 `http` 。 13 | 14 | ::: code-group 15 | 16 | ```typescript [http.ts] 17 | // 该服务使用 `http` 库。 18 | import { http as httpLibrary } from "@some-library/http"; 19 | 20 | export const http = { 21 | async get(url: string) { 22 | const token = await fetchToken(); 23 | 24 | return httpLibrary.get(url, { 25 | headers: { Authorization: `Bearer ${token}` } 26 | }); 27 | } 28 | }; 29 | ``` 30 | 31 | ```typescript [fetchUser.ts] 32 | // 从 http.ts 文件中导入 http 的代码 33 | import { http } from "./http"; 34 | 35 | export async function fetchUser() { 36 | return http.get("..."); 37 | } 38 | ``` 39 | 40 | ::: 41 | 42 | ## 👃 闻代码 43 | 44 | ### 可预测性 45 | 46 | 这段代码在功能上没有问题,但可能会让代码阅读者感到困惑。调用 `http.get` 的开发者可能会预期这个函数像原始的 HTTP 库一样只是发送一个简单的 GET 请求,但实际上会执行额外的操作,如获取令牌。 47 | 48 | 由于误解,预期行为与实际行为之间会出现差异,从而引发错误,或者使调试过程变得复杂和混乱。 49 | 50 | ## ✏️ 尝试改善 51 | 52 | 为了提高函数行为的可预测性,在服务中自定义函数时,应该使用与库函数明显区分开来的、具有描述性的名称。 53 | 54 | ::: code-group 55 | 56 | ```typescript [httpService.ts] 57 | // 该服务正在使用 `http` 库。 58 | import { http as httpLibrary } from "@some-library/http"; 59 | 60 | // 将库函数的名称与自定义函数区分开来。 61 | export const httpService = { 62 | async getWithAuth(url: string) { 63 | const token = await fetchToken(); 64 | 65 | // 添加认证逻辑,例如在请求头中添加令牌。 66 | return httpLibrary.get(url, { 67 | headers: { Authorization: `Bearer ${token}` } 68 | }); 69 | } 70 | }; 71 | ``` 72 | 73 | ```typescript [fetchUser.ts] 74 | // 从 httpService.ts 文件中引入定义的 httpService 模块 75 | import { httpService } from "./httpService"; 76 | 77 | export async function fetchUser() { 78 | // 通过函数名,可知该函数发送的是已通过认证的请求。 79 | return await httpService.getWithAuth("..."); 80 | } 81 | ``` 82 | 83 | ::: 84 | 85 | 这种方式可以减少在看到函数名时对其功能产生误解的可能性。 86 | 当其他开发者使用该函数时,他们能够意识到这是服务中定义的函数,并能够正确的使用它。 87 | 88 | 另外,通过 `getWithAuth` 这个名称,可以明确传达该函数是用来发送通过认证的请求。 89 | -------------------------------------------------------------------------------- /fundamentals/code-quality/zh-hans/code/examples/magic-number-cohesion.md: -------------------------------------------------------------------------------- 1 | # 消除魔数 2 | 3 |
4 | 5 |
6 | 7 | **魔数**(Magic Number)指的是缺乏明确说明而直接插入的数值。 8 | 9 | 例如,直接使用 `404` 来表示未找到(Not Found)的 HTTP 状态码,或者直接使用 `86400` 秒来表示一天的时间。 10 | 11 | ## 📝 代码示例 12 | 13 | 下列代码是一个函数,当点击点赞按钮时重新获取点赞数量。 14 | 15 | ```typescript 3 16 | async function onLikeClick() { 17 | await postLike(url); 18 | await delay(300); 19 | await refetchPostLike(); 20 | } 21 | ``` 22 | 23 | ## 👃 闻代码 24 | 25 | ### 内聚性 26 | 27 | 如果使用像 `300` 这样的固定时间值来等待动画完成,那么在动画播放的过程中进行更改时,服务可能会悄无声息地出现故障。因为后续的逻辑可能会在动画还未完成时就开始执行。 28 | 29 | 此外,由于只修改了需要同步更改的代码中的一部分,这段代码的内聚性很低。 30 | 31 | ::: info 32 | 33 | 这个 Hook 也可以从 [可读性](./magic-number-readability.md) 的角度来考虑。 34 | 35 | ::: 36 | 37 | ## ✏️ 尝试改善 38 | 39 | 为了更准确的表达数字 `300` 的含义,可以将其声明为常量 `ANIMATION_DELAY_MS` 。 40 | 41 | ```typescript 1,5 42 | const ANIMATION_DELAY_MS = 300; 43 | 44 | async function onLikeClick() { 45 | await postLike(url); 46 | await delay(ANIMATION_DELAY_MS); 47 | await refetchPostLike(); 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /fundamentals/code-quality/zh-hans/code/examples/magic-number-readability.md: -------------------------------------------------------------------------------- 1 | # 为魔数命名 2 | 3 |
4 | 5 |
6 | 7 | **魔数**(Magic Number)指的是缺乏明确说明而直接插入的数值。 8 | 9 | 例如,直接使用 `404` 来表示未找到(Not Found)的 HTTP 状态码,或者直接使用 `86400` 秒来表示一天的时间。 10 | 11 | ## 📝 代码示例 12 | 13 | 下列代码是一个函数,当点击点赞按钮时重新获取点赞数量。 14 | 15 | ```typescript 3 16 | async function onLikeClick() { 17 | await postLike(url); 18 | await delay(300); 19 | await refetchPostLike(); 20 | } 21 | ``` 22 | 23 | ## 👃 闻代码 24 | 25 | ### 可读性 26 | 27 | 这段代码中的 `delay` 函数传递了一个值 `300` ,但无法从上下文推测该值的具体用途。 28 | 如果不是该代码的编写者,就无法理解 300ms 等待的是什么。 29 | 30 | - 是在等待动画完成? 31 | - 是在等待点赞反映时间? 32 | - 是不是忘了删测试代码? 33 | 34 | 当多名开发者共同修改同一段代码时,可能无法明确原意,从而导致代码被修改成不符合预期的结果。 35 | 36 | ::: info 37 | 38 | 这个 Hook 也可以从 [内聚性](./magic-number-cohesion.md) 的角度来考虑。 39 | 40 | ::: 41 | 42 | ## ✏️ 尝试改善 43 | 44 | 为了更准确的表达数字 `300` 的含义,可以将其声明为常量 `ANIMATION_DELAY_MS` 。 45 | 46 | ```typescript 1,5 47 | const ANIMATION_DELAY_MS = 300; 48 | 49 | async function onLikeClick() { 50 | await postLike(url); 51 | await delay(ANIMATION_DELAY_MS); 52 | await refetchPostLike(); 53 | } 54 | ``` 55 | 56 | ## 🔍 深入了解 57 | 58 | 魔数也可以从内聚性角度来审视。请参考 [消除魔数提高内聚性](./magic-number-cohesion.md) 一文。 59 | -------------------------------------------------------------------------------- /fundamentals/code-quality/zh-hans/code/examples/submit-button.md: -------------------------------------------------------------------------------- 1 | # 分离不一起运行的代码 2 | 3 |
4 | 5 |
6 | 7 | 如果不同时运行的代码被放在同一个函数或组件中,就很难一眼看清他们各自的作用。 8 | 实现过程中内含复杂的分支,很难理解代码各个部分的作用。 9 | 10 | ## 📝 代码示例 11 | 12 | `` 组件会根据用户的权限以不同的方式运行。 13 | 14 | - 如果用户的权限是仅查看(`"viewer"`),邀请按钮会处于非激活状态,不会播放动画。 15 | - 如果用户是普通用户,邀请按钮处于激活状态,并且播放动画。 16 | 17 | ```tsx 18 | function SubmitButton() { 19 | const isViewer = useRole() === "viewer"; 20 | 21 | useEffect(() => { 22 | if (isViewer) { 23 | return; 24 | } 25 | showButtonAnimation(); 26 | }, [isViewer]); 27 | 28 | return isViewer ? ( 29 | Submit 30 | ) : ( 31 | 32 | ); 33 | } 34 | ``` 35 | 36 | ## 👃 闻代码 37 | 38 | ### 可读性 39 | 40 | `` 组件同时处理用户可能具有的两种权限状态,且该两种状态都在同一组件中进行处理。 41 | 所以代码阅读者需要考虑的语境过多。 42 | 43 | 例如,在下面的代码中,蓝色部分表示当用户具有仅查看权限(`'viewer'`)时运行的代码,红色部分表示当用户是普通用户时运行的代码。 44 | 由于不同时运行的代码交织在一起,理解代码时产生负担。 45 | 46 | ![](../../../images/examples/submit-button.png){.light-only} 47 | ![](../../../images/examples/submit-button-dark.png){.dark-only} 48 | 49 | ## ✏️ 尝试改善 50 | 51 | 以下代码是将用户具有仅查看权限时和作为普通用户时的状态完全分开来管理的代码示例。 52 | 53 | ```tsx 54 | function SubmitButton() { 55 | const isViewer = useRole() === "viewer"; 56 | 57 | return isViewer ? : ; 58 | } 59 | 60 | function ViewerSubmitButton() { 61 | return Submit; 62 | } 63 | 64 | function AdminSubmitButton() { 65 | useEffect(() => { 66 | showAnimation(); 67 | }, []); 68 | 69 | return ; 70 | } 71 | ``` 72 | 73 | - 随着原本分散在 `` 代码各处的分支合并为一,分支数量减少。 74 | - `` 和 `` 各自仅管理一个分支,所以代码阅读者需要考虑的语境减少。 75 | -------------------------------------------------------------------------------- /fundamentals/code-quality/zh-hans/code/examples/ternary-operator.md: -------------------------------------------------------------------------------- 1 | # 简化三元运算符 2 | 3 |
4 | 5 |
6 | 7 | 复杂地使用三元运算符可能会掩盖条件的结构,从而使代码难以阅读。 8 | 9 | ## 📝 代码示例 10 | 11 | 以下代码根据 `条件A` 和 `条件B`,将 `status` 设置为 `"BOTH"`、 `"A"` 、 `"B"` 或 `"NONE"` 中的一个。 12 | 13 | ```typescript 14 | const status = 15 | 条件A && 条件B ? "BOTH" : 条件A || 条件B ? (条件A ? "A" : "B") : "NONE"; 16 | ``` 17 | 18 | ## 👃 闻代码 19 | 20 | ### 可读性 21 | 22 | 这段代码使用了多个嵌套的三元运算符,很难一眼看出计算值使用了哪个条件。 23 | 24 | ## ✏️ 尝试改善 25 | 26 | 如下使用 `if` 语句展开条件,可以简单明了地显示条件。 27 | 28 | ```typescript 29 | const status = (() => { 30 | if (条件A && 条件B) return "BOTH"; 31 | if (条件A) return "A"; 32 | if (条件B) return "B"; 33 | return "NONE"; 34 | })(); 35 | ``` 36 | -------------------------------------------------------------------------------- /fundamentals/code-quality/zh-hans/code/examples/use-bottom-sheet.md: -------------------------------------------------------------------------------- 1 | # 允许重复代码 2 | 3 |
4 | 5 |
6 | 7 | 作为开发者,我们经常会把跨多个页面或组件的重复代码整合于一个 Hook 或组件中来实现代码的共用。 8 | 通过将重复代码整合到一个组件或 Hook 中,使得代码具有高内聚性(好代码的特征之一),从而能够同时修改那些需要一起修改的代码。 9 | 10 | 然而,这也可能引发不必要地耦合性,导致在修改公共组件或 Hook 时,受影响的代码范围扩大,修改代码反而变得更加困难。 11 | 12 | 起初因功能相似而合并的公共代码,后来可能会根据各页面的特殊需求而逐渐变得复杂起来。 13 | 而且,每次修改公共代码时,都必须逐一测试对其依赖的代码,这反而会让代码修改更加困难。 14 | 15 | ## 📝 代码示例 16 | 17 | 下面有一个 Hook,它接受维修信息作为参数,如果系统当前处于维修中状态,就会打开底部动作条(bottom sheet)。如果用户同意接收通知,则会进行日志记录,并随后关闭当前屏幕。 18 | 19 | ```typescript 20 | export const useOpenMaintenanceBottomSheet = () => { 21 | const maintenanceBottomSheet = useMaintenanceBottomSheet(); 22 | const logger = useLogger(); 23 | 24 | return async (maintainingInfo: TelecomMaintenanceInfo) => { 25 | logger.log("打开维修底部动作条"); 26 | const result = await maintenanceBottomSheet.open(maintainingInfo); 27 | if (result) { 28 | logger.log("点击维修底部动作条上的同意接收通知"); 29 | } 30 | closeView(); 31 | }; 32 | }; 33 | ``` 34 | 35 | 这段代码因在多个页面中反复使用,被提取为一个公共 Hook。 36 | 37 | ## 👃 闻代码 38 | 39 | ### 耦合性 40 | 41 | 这个 Hook 之所以被通用化,是因为其包含了很多页面共同反复用到的逻辑。不过,在享受其带来的便利时,需要留意未来可能出现的各种代码变更的情况。 42 | 43 | - 每个页面需要日志记录的值不同时? 44 | - 某些页面即使在关闭维修底部动作条时也不需要关闭整个屏幕时? 45 | - 需要在底部动作条中显示不同文本或图像时? 46 | 47 | 上述 Hook 为了灵活对应代码变更需求,可能需要接受一些复杂的参数。 48 | 而且,每次修改这个 Hook 时,都需要测试所有使用该 Hook 的页面,以确保动作的正常运行。 49 | 50 | ## ✏️ 尝试改善 51 | 52 | 即便重复的代码看上去有些冗余,有时候接受这种重复代码也不失为一种好方法。 53 | 54 | 需要积极与同事沟通,准确理解维修底部动作条的动作原理。 55 | 如果页面上日志记录的值相同,维修底部动作条的动作一致,且其外表相同,并且预计将来也会保持现状,那么我们可以通过通用化提高代码的内聚性。 56 | 57 | 但是,如果每个页面的行为都可能有所不同,那么不追求通用化,而是允许代码重复,可能是更好的选择。 58 | -------------------------------------------------------------------------------- /fundamentals/code-quality/zh-hans/code/examples/use-page-state-coupling.md: -------------------------------------------------------------------------------- 1 | # 单独管理责任 2 | 3 |
4 | 5 |
6 | 7 | 不要根据逻辑类型(如查询参数、状态、API 调用等)为基准拆分函数。 由于同时涉及多种上下文类型,代码变得即难以理解又难以维护。 8 | 9 | ## 📝 代码示例 10 | 11 | 以下 `usePageState()` Hook 可以一次性管理整个页面的 URL 查询参数。 12 | 13 | ```typescript 14 | import moment, { Moment } from "moment"; 15 | import { useMemo } from "react"; 16 | import { 17 | ArrayParam, 18 | DateParam, 19 | NumberParam, 20 | useQueryParams 21 | } from "use-query-params"; 22 | 23 | const defaultDateFrom = moment().subtract(3, "month"); 24 | const defaultDateTo = moment(); 25 | 26 | export function usePageState() { 27 | const [query, setQuery] = useQueryParams({ 28 | cardId: NumberParam, 29 | statementId: NumberParam, 30 | dateFrom: DateParam, 31 | dateTo: DateParam, 32 | statusList: ArrayParam 33 | }); 34 | 35 | return useMemo( 36 | () => ({ 37 | values: { 38 | cardId: query.cardId ?? undefined, 39 | statementId: query.statementId ?? undefined, 40 | dateFrom: 41 | query.dateFrom == null ? defaultDateFrom : moment(query.dateFrom), 42 | dateTo: query.dateTo == null ? defaultDateTo : moment(query.dateTo), 43 | statusList: query.statusList as StatementStatusType[] | undefined 44 | }, 45 | controls: { 46 | setCardId: (cardId: number) => setQuery({ cardId }, "replaceIn"), 47 | setStatementId: (statementId: number) => 48 | setQuery({ statementId }, "replaceIn"), 49 | setDateFrom: (date?: Moment) => 50 | setQuery({ dateFrom: date?.toDate() }, "replaceIn"), 51 | setDateTo: (date?: Moment) => 52 | setQuery({ dateTo: date?.toDate() }, "replaceIn"), 53 | setStatusList: (statusList?: StatementStatusType[]) => 54 | setQuery({ statusList }, "replaceIn") 55 | } 56 | }), 57 | [query, setQuery] 58 | ); 59 | } 60 | ``` 61 | 62 | ## 👃 闻代码 63 | 64 | ### 耦合性 65 | 66 | 该 Hook 肩负着“管理此页面所需的所有查询参数”的广泛责任。因此,页面内的组件或其他 Hook 可能会依赖于该 Hook,在修改代码时,影响范围也会急剧扩大。 67 | 68 | 随着时间的推移,这个 Hook 会变得难以维护,最终演变为难以修改的代码。 69 | 70 | ::: info 71 | 72 | 这个 Hook 也可以从 [可读性](./use-page-state-readability.md) 的角度来考虑。 73 | 74 | ::: 75 | 76 | ## ✏️ 尝试改善 77 | 78 | 可以像下列代码一样,将每个查询参数分别编写单独的 Hook。 79 | 80 | ```typescript 81 | import { useQueryParam } from "use-query-params"; 82 | 83 | export function useCardIdQueryParam() { 84 | const [cardId, _setCardId] = useQueryParam("cardId", NumberParam); 85 | 86 | const setCardId = useCallback((cardId: number) => { 87 | _setCardId({ cardId }, "replaceIn"); 88 | }, []); 89 | 90 | return [cardId ?? undefined, setCardId] as const; 91 | } 92 | ``` 93 | 94 | 由于分离了 Hook 所承担的责任,能够减少修改所带来的影响范围。 95 | 因此,在修改 Hook 时, 能够防止产生预料之外的影响。 96 | -------------------------------------------------------------------------------- /fundamentals/code-quality/zh-hans/code/examples/user-policy.md: -------------------------------------------------------------------------------- 1 | # 减少视点转移 2 | 3 |
4 | 5 |
6 | 7 | 在阅读代码时,反复浏览代码的各个部分,或者在多个文件、函数、变量之间翻看阅读的现象被称为**视点转移**。 8 | 随着视点的多次转移,需要理解代码的时间也随之增加,很难把握代码的整体语境。 9 | 10 | 如果将代码编写成从上到下、在一个函数或文件中就能顺序阅读的方式,代码阅读者可以迅速理解其功能。 11 | 12 | ## 📝 代码示例 13 | 14 | 下列代码会根据用户的权限显示不同的按钮。 15 | 16 | - 如果用户权限是管理员(Admin),则显示 `Invite` 和 `View` 按钮。 17 | - 如果用户权限是仅查看用户(Viewer),则非激活 `Invite` 按钮,显示 `View` 按钮。 18 | 19 | ```tsx 20 | function Page() { 21 | const user = useUser(); 22 | const policy = getPolicyByRole(user.role); 23 | 24 | return ( 25 |
26 | 27 | 28 |
29 | ); 30 | } 31 | 32 | function getPolicyByRole(role) { 33 | const policy = POLICY_SET[role]; 34 | 35 | return { 36 | canInvite: policy.includes("invite"), 37 | canView: policy.includes("view") 38 | }; 39 | } 40 | 41 | const POLICY_SET = { 42 | admin: ["invite", "view"], 43 | viewer: ["view"] 44 | }; 45 | ``` 46 | 47 | ## 👃 闻代码 48 | 49 | ### 可读性 50 | 51 | 为了理解这段代码中为何 `Invite` 按钮被非激活,你需要按照 `policy.canInvite` → `getPolicyByRole(user.role)` → `POLICY_SET` 的顺序,在代码中上下翻阅进行阅读。 52 | 在此过程中,发生了三次视点转移,使得代码阅读者很难维持上下文的语境,增加了理解的难度。 53 | 54 | 虽然使用 `POLICY_SET` 等抽象来按权限管理按钮状态在复杂权限体系中很有用,但在当前简单场景下却增加了阅读者的代码理解难度。 55 | 56 | ## ✏️ 尝试改善 57 | 58 | ### A. 展开并明确展示条件 59 | 60 | 直接在代码中明确展示了基于权限的条件。这样一来,代码中很容易看出 `Invite` 按钮何时会被非激活。 61 | 只需阅读代码的上下文,就能一眼看清处理权限的逻辑。 62 | 63 | ```tsx 64 | function Page() { 65 | const user = useUser(); 66 | 67 | switch (user.role) { 68 | case "admin": 69 | return ( 70 |
71 | 72 | 73 |
74 | ); 75 | case "viewer": 76 | return ( 77 |
78 | 79 | 80 |
81 | ); 82 | default: 83 | return null; 84 | } 85 | } 86 | ``` 87 | 88 | ### B. 将条件整理成清晰易懂的对象形式 89 | 90 | 通过以对象的形式在组件内部管理权限逻辑,可以减少不必要的视点转移,一眼看清并修改条件。 91 | 只需查看 `Page` 组件,就能确认 `canInvite` 和 `canView` 的条件。 92 | 93 | ```tsx 94 | function Page() { 95 | const user = useUser(); 96 | const policy = { 97 | admin: { canInvite: true, canView: true }, 98 | viewer: { canInvite: false, canView: true } 99 | }[user.role]; 100 | 101 | return ( 102 |
103 | 104 | 105 |
106 | ); 107 | } 108 | ``` 109 | -------------------------------------------------------------------------------- /fundamentals/code-quality/zh-hans/code/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | comments: false 3 | --- 4 | 5 | # 编写优秀代码的四大原则 6 | 7 | 好的前端代码是**易于修改的**代码。 8 | 在实现新需求时,能够轻松修改和部署的代码被认为好的代码。 9 | 你可以根据四个标准判断代码是否易于修改。 10 | 11 | ## 1. 可读性 12 | 13 | **可读性**(Readability)指的是代码易于阅读和理解的程度。 14 | 要使代码易于修改,首先必须理解代码的作用。 15 | 16 | 易于阅读的代码要求读者考虑的上下文较少,从上到下自然流畅。 17 | 18 | ### 提高可读性的策略 19 | 20 | - **减少语境** 21 | - [分离不一起运行的代码](./examples/submit-button.md) 22 | - [抽象实现细节](./examples/login-start-page.md) 23 | - [根据逻辑类型拆分合并的函数](./examples/use-page-state-readability.md) 24 | - **命名** 25 | - [为复杂条件命名](./examples/condition-name.md) 26 | - [为魔数命名](./examples/magic-number-readability.md) 27 | - **使其从上到下顺利阅读** 28 | - [减少视点转移](./examples/user-policy.md) 29 | - [简化三元运算符](./examples/ternary-operator.md) 30 | 31 | ## 2. 可预测性 32 | 33 | **可预测性**(Predictability)指的是与团队成员协作时,同事能够预测函数或组件行为的难易程度。 34 | 可预测性高的代码遵循一致的规则,仅通过函数或组件的名称、参数、返回值,就能知道其执行的行为。 35 | 36 | ### 提高可预测性的战略 37 | 38 | - [避免命名重复](./examples/http.md) 39 | - [统一同类函数的返回类型](./examples/use-user.md) 40 | - [揭示隐藏的逻辑](./examples/hidden-logic.md) 41 | 42 | ## 3. 内聚性 43 | 44 | **内聚性**(Cohesion)是指需要被修改的代码是否总是一起修改的特性。 45 | 内聚性高的代码,即使修改了某一部分,也不会在其他部分引发障碍。 46 | 这是因为在结构上保证了相关代码能够同步修改。 47 | 48 | ::: info 可读性与内聚性可能存在冲突 49 | 50 | 一般来说,为了提高内聚性,可能需要做出一些降低可读性的决策,例如抽象化变数或函数。 51 | 以内聚性为准,通过代码的通用化和抽象化来避免未同时修改而引发的障碍。 52 | 风险较低时,应优先考虑可读性,允许代码重复。 53 | 54 | ::: 55 | 56 | ### 提高内聚性的策略 57 | 58 | - [需同时修改的文件位于同一目录下](./examples/code-directory.md) 59 | - [消除魔数](./examples/magic-number-cohesion.md) 60 | - [考虑表单的内聚性](./examples/form-fields.md) 61 | 62 | ## 4. 耦合性 63 | 64 | **耦合性**(Coupling)是指修改代码时的影响范围。 65 | 易于修改的代码被修改时影响范围小,因此更容易预测更改的范围。 66 | 67 | ### 降低耦合性的策略 68 | 69 | - [单独管理责任](./examples/use-page-state-coupling.md) 70 | - [允许重复代码](./examples/use-bottom-sheet.md) 71 | - [消除 Props Drilling](./examples/item-edit-modal.md) 72 | 73 | ## 多角度审视代码质量 74 | 75 | 遗憾的是,这四个标准很难同时兼顾。 76 | 77 | 例如,通过通用化和抽象化提高代码的内聚性,可确保函数或变数总是一同修改。然而,代码进一步抽象化后,可读性也会随之降低。 78 | 79 | 允许代码重复,可以减少代码的影响范围,从而降低耦合性。然而,这也可能导致在修改一处代码时,另一处未被及时修改,从而降低内聚性。 80 | 81 | 前端开发者需要结合当前面临的具体情况,深入思考并在不同价值之间权衡取舍,以确保代码在长期内更易于维护和修改。 82 | -------------------------------------------------------------------------------- /fundamentals/code-quality/zh-hans/code/start.md: -------------------------------------------------------------------------------- 1 | --- 2 | comments: false 3 | --- 4 | 5 | # 开始使用 6 | 7 | `Frontend Fundamentals`(FF)为前端代码规范提供标准。作为前端开发者,当你想提高代码质量时,可以将它做为指南针,帮助你找到正确的方向。 8 | 9 | 它介绍了好代码的[四大原则](./index.md),以及具体的实例与解决方案。 10 | 11 | ## 何时使用 12 | 13 | - 🦨 关心代码但**难以从逻辑上解释的开发者**。 14 | - 👀 想学习如何**快速检测并改善坏代码**的开发者。 15 | - 🤓 在代码审查等环节,通过别人分享的链接**将客观地认识**自己是如何编写代码的开发者。 16 | - 👥 **希望与团队**一起制定共同代码风格和质量标准的开发者。 17 | 18 | ## 作者 19 | 20 | - [milooy](https://github.com/milooy) 21 | - [donghyeon](https://github.com/kimbangg) 22 | - [chkim116](https://github.com/chkim116) 23 | - [inseong.you](https://github.com/inseong.you) 24 | - [raon0211](https://github.com/raon0211) 25 | - [bigsaigon333](https://github.com/bigsaigon333) 26 | - [jho2301](https://github.com/jho2301) 27 | - [KimChunsick](https://github.com/KimChunsick) 28 | - [jennybehan](https://github.com/jennybehan) 29 | 30 | ## 文档贡献者 31 | 32 | - [andy0414](https://github.com/andy0414) 33 | - [pumpkiinbell](https://github.com/pumpkiinbell) 34 | -------------------------------------------------------------------------------- /fundamentals/code-quality/zh-hans/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Frontend Fundamentals" 7 | tagline: "易于修改的前端代码指南" 8 | image: 9 | src: /images/ff-symbol-gradient-webp-80.webp 10 | alt: Frontend Fundamentals symbol 11 | actions: 12 | - text: 了解好代码的标准 13 | link: /zh-hans/code/ 14 | - theme: alt 15 | text: 社区 16 | link: /zh-hans/code/community 17 | 18 | features: 19 | - icon: 🤓 20 | title: 提高你的代码阅读能力 21 | details: 查看判断代码是否易于修改的原则。 22 | - icon: 🤝 23 | title: 提高你的代码审查能力 24 | details: 主动探索各种代码改善的实例。 25 | - icon: 📝 26 | title: 如果对自己的代码感到困惑 27 | details: 在 GitHub 论坛与其他开发者进行交流。 28 | --- 29 | -------------------------------------------------------------------------------- /fundamentals/debug/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | import footnote from "markdown-it-footnote"; 3 | import path from "node:path"; 4 | import { createRequire } from "node:module"; 5 | import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs' 6 | import { sharedConfig } from './shared.mjs'; 7 | 8 | const require = createRequire(import.meta.url); 9 | 10 | // https://vitepress.dev/reference/site-config 11 | export default defineConfig({ 12 | ...sharedConfig, 13 | title: "Debug Fundamentals", 14 | description: "프론트엔드 접근성의 모든 것", 15 | ignoreDeadLinks: false, 16 | base: "/debug/", 17 | themeConfig: { 18 | // https://vitepress.dev/reference/default-theme-config 19 | ...sharedConfig.themeConfig, 20 | nav: [ 21 | { text: "홈", link: "/" }, 22 | ], 23 | sidebar: [ 24 | { 25 | text: "소개", 26 | items: [ 27 | { 28 | text: "시작하기", 29 | link: "/get-started", 30 | }, 31 | ] 32 | }, 33 | { 34 | text: "튜토리얼", 35 | }, 36 | { 37 | text: "심화 학습", 38 | }, 39 | ], 40 | }, 41 | markdown: { 42 | config: (md) => { 43 | md.use(footnote); 44 | md.use(tabsMarkdownPlugin); 45 | }, 46 | }, 47 | head: [ 48 | [ 49 | "link", 50 | { rel: "icon", type: "image/x-icon", href: "/bundling/images/favicon.ico" } 51 | ], 52 | [ 53 | "meta", 54 | { 55 | property: "og:image", 56 | content: "https://static.toss.im/illusts/bf-meta.png" 57 | } 58 | ], 59 | [ 60 | "meta", 61 | { 62 | name: "twitter:image", 63 | content: "https://static.toss.im/illusts/bf-meta.png" 64 | } 65 | ], 66 | [ 67 | "meta", 68 | { 69 | name: "twitter:card", 70 | content: "summary" 71 | } 72 | ], 73 | ], 74 | vite: { 75 | resolve: { 76 | alias: [ 77 | { 78 | find: /^vue$/, 79 | replacement: path.dirname( 80 | require.resolve("vue/package.json", { 81 | paths: [require.resolve("vitepress")] 82 | }) 83 | ) 84 | }, 85 | { 86 | find: /^vue\/server-renderer$/g, 87 | replacement: path.dirname( 88 | require.resolve("vue/server-renderer", { 89 | paths: [require.resolve("vitepress")] 90 | }) 91 | ) 92 | }, 93 | { 94 | find: /^@shared/, 95 | replacement: path.resolve(__dirname, '../../shared'), 96 | } 97 | ] 98 | } 99 | }, 100 | }) 101 | 102 | 103 | -------------------------------------------------------------------------------- /fundamentals/debug/.vitepress/shared.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig, HeadConfig } from 'vitepress' 2 | import { DefaultTheme } from "vitepress"; 3 | 4 | const search: DefaultTheme.LocalSearchOptions["locales"] = { 5 | root: { 6 | translations: { 7 | button: { 8 | buttonText: "검색", 9 | buttonAriaLabel: "검색" 10 | }, 11 | modal: { 12 | backButtonTitle: "뒤로가기", 13 | displayDetails: "더보기", 14 | footer: { 15 | closeKeyAriaLabel: "닫기", 16 | closeText: "닫기", 17 | navigateDownKeyAriaLabel: "아래로", 18 | navigateText: "이동", 19 | navigateUpKeyAriaLabel: "위로", 20 | selectKeyAriaLabel: "선택", 21 | selectText: "선택" 22 | }, 23 | noResultsText: "검색 결과를 찾지 못했어요.", 24 | resetButtonTitle: "모두 지우기" 25 | } 26 | } 27 | } 28 | }; 29 | 30 | 31 | export const sharedConfig = defineConfig({ 32 | lastUpdated: true, 33 | head: [ 34 | [ 35 | "link", 36 | { rel: "icon", type: "image/x-icon", href: "/images/favicon.ico" } 37 | ], 38 | [ 39 | "meta", 40 | { 41 | property: "og:image", 42 | content: "https://static.toss.im/illusts/bf-meta.png" 43 | } 44 | ], 45 | [ 46 | "meta", 47 | { 48 | name: "twitter:image", 49 | content: "https://static.toss.im/illusts/bf-meta.png" 50 | } 51 | ], 52 | [ 53 | "meta", 54 | { 55 | name: "twitter:card", 56 | content: "summary" 57 | } 58 | ], 59 | ], 60 | transformHead: ({ pageData }) => { 61 | const head: HeadConfig[] = []; 62 | const title = 63 | pageData.frontmatter.title || pageData.title || "Bundling Fundamentals"; 64 | const description = 65 | pageData.frontmatter.description || 66 | pageData.description || 67 | "Practical Guide to Efficient Frontend Bundling"; 68 | 69 | head.push(["meta", { property: "og:title", content: title }]); 70 | head.push(["meta", { property: "og:description", content: description }]); 71 | 72 | return head; 73 | }, 74 | 75 | themeConfig: { 76 | socialLinks: [ 77 | { icon: 'github', link: 'https://github.com/toss/frontend-fundamentals' } 78 | ], 79 | search: { 80 | provider: "local", 81 | options: { 82 | locales: { 83 | ...search, 84 | } 85 | } 86 | }, 87 | } 88 | }) 89 | -------------------------------------------------------------------------------- /fundamentals/debug/.vitepress/theme/Layout.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | 22 | 43 | -------------------------------------------------------------------------------- /fundamentals/debug/.vitepress/theme/components/Comments.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 37 | -------------------------------------------------------------------------------- /fundamentals/debug/.vitepress/theme/custom.css: -------------------------------------------------------------------------------- 1 | html.dark .light-only { 2 | display: none !important; 3 | } 4 | 5 | html:not(.dark) .dark-only { 6 | display: none !important; 7 | } 8 | 9 | :root { 10 | --vp-font-family-base: "Toss Product Sans", ui-sans-serif, system-ui, 11 | sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", 12 | "Noto Color Emoji"; 13 | } 14 | 15 | :root[lang="ko"] { 16 | word-break: keep-all; 17 | } 18 | -------------------------------------------------------------------------------- /fundamentals/debug/.vitepress/theme/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useLocale"; 2 | export * from "./useGiscusTheme"; 3 | -------------------------------------------------------------------------------- /fundamentals/debug/.vitepress/theme/hooks/useGiscusTheme.tsx: -------------------------------------------------------------------------------- 1 | import { ref, watch, computed, onMounted } from "vue"; 2 | import { useData } from "vitepress"; 3 | import { GISCUS_ORIGIN, GISCUS_THEME, sendGiscusMessage } from "../utils"; 4 | 5 | export function useGiscusTheme() { 6 | const { isDark } = useData(); 7 | const isIframeLoaded = ref(false); 8 | 9 | const syncTheme = () => { 10 | sendGiscusMessage({ 11 | setConfig: { 12 | theme: isDark.value ? GISCUS_THEME.dark : GISCUS_THEME.light 13 | } 14 | }); 15 | }; 16 | 17 | const setupThemeHandler = () => { 18 | window.addEventListener("message", (event) => { 19 | if (event.origin === GISCUS_ORIGIN && event.data?.giscus != null) { 20 | isIframeLoaded.value = true; 21 | syncTheme(); 22 | } 23 | }); 24 | }; 25 | 26 | onMounted(() => { 27 | setupThemeHandler(); 28 | }); 29 | 30 | watch(isDark, () => { 31 | if (isIframeLoaded.value) { 32 | syncTheme(); 33 | } 34 | }); 35 | 36 | return { 37 | theme: computed(() => 38 | isDark.value ? GISCUS_THEME.dark : GISCUS_THEME.light 39 | ) 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /fundamentals/debug/.vitepress/theme/hooks/useLocale.ts: -------------------------------------------------------------------------------- 1 | import { useData } from "vitepress"; 2 | import { computed } from "vue"; 3 | 4 | export function useLocale() { 5 | const { lang } = useData(); 6 | 7 | const isKorean = computed(() => lang.value === "ko" || lang.value === "root"); 8 | 9 | return { 10 | isKorean 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /fundamentals/debug/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from "vitepress/theme"; 2 | import Layout from "./Layout.vue"; 3 | import * as amplitude from "@amplitude/analytics-browser"; 4 | import "./custom.css"; 5 | import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client'; 6 | 7 | export default { 8 | extends: DefaultTheme, 9 | Layout, 10 | async enhanceApp({ app }) { 11 | if (typeof window !== "undefined") { 12 | const amplitudeApiKey = (import.meta as any).env.VITE_AMPLITUDE_API_KEY; 13 | amplitude.init(amplitudeApiKey, { autocapture: true }); 14 | enhanceAppWithTabs(app) 15 | } 16 | } 17 | }; -------------------------------------------------------------------------------- /fundamentals/debug/.vitepress/theme/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const GISCUS_ORIGIN = "https://giscus.app" as const; 2 | 3 | export const GISCUS_LANG_MAP = { 4 | ko: "ko", 5 | en: "en", 6 | ja: "ja", 7 | "zh-hans": "zh-CN" 8 | } as const; 9 | 10 | export const GISCUS_THEME = { 11 | light: "light_tritanopia", 12 | dark: "dark_tritanopia" 13 | }; 14 | 15 | export function getGiscusLang(lang) { 16 | return GISCUS_LANG_MAP[lang] || "en"; 17 | } 18 | 19 | export function sendGiscusMessage(message: T) { 20 | const iframe = document.querySelector( 21 | "iframe.giscus-frame" 22 | ); 23 | if (!iframe) return; 24 | 25 | iframe.contentWindow?.postMessage({ giscus: message }, GISCUS_ORIGIN); 26 | } 27 | -------------------------------------------------------------------------------- /fundamentals/debug/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /fundamentals/debug/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": "explicit" 4 | }, 5 | "editor.formatOnSave": true, 6 | "eslint.nodePath": "../../.yarn/sdks", 7 | "prettier.prettierPath": "../../.yarn/sdks/prettier/index.cjs", 8 | "typescript.tsdk": "../../.yarn/sdks/typescript/lib", 9 | "typescript.enablePromptUseWorkspaceTsdk": true, 10 | "search.exclude": { 11 | "**/.yarn": true, 12 | "**/.pnp.*": true, 13 | "**/.next": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /fundamentals/debug/get-started.md: -------------------------------------------------------------------------------- 1 | # 시작하기 2 | 3 | ## 이런 분들에게 추천해요 4 | 5 | ## 저작자 -------------------------------------------------------------------------------- /fundamentals/debug/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Debug Fundamentals" 7 | tagline: "프론트엔드 디버깅의 모든 것" 8 | image: 9 | loading: eager 10 | fetchpriority: high 11 | decoding: async 12 | src: /images/bf-symbol.webp 13 | alt: Frontend Fundamentals symbol 14 | actions: 15 | - text: 시작하기 16 | link: /get-started 17 | - theme: alt 18 | text: 접근성이란? 19 | link: /overview 20 | 21 | features: 22 | - icon: 📦 23 | title: 1 24 | details: 1-설명 25 | link: /overview 26 | - icon: 🚀 27 | title: 2 28 | details: 2-설명 29 | link: /webpack-tutorial/intro 30 | - icon: 🔍 31 | title: 3 32 | details: 3-설명 33 | link: /deep-dive/bundling-process/overview 34 | --- 35 | -------------------------------------------------------------------------------- /fundamentals/debug/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@frontend-fundamentals/debug", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "docs:dev": "vitepress dev", 8 | "docs:build": "vitepress build", 9 | "docs:preview": "vitepress preview" 10 | }, 11 | "dependencies": { 12 | "@amplitude/analytics-browser": "^2.11.11", 13 | "markdown-it-footnote": "^4.0.0", 14 | "typescript": "^5.6.3", 15 | "vitepress": "^1.4.1", 16 | "vitepress-plugin-tabs": "^0.7.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /fundamentals/shared/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as OneNavigation } from './OneNavigation.vue'; -------------------------------------------------------------------------------- /fundamentals/shared/composables/useLocale.ts: -------------------------------------------------------------------------------- 1 | import { useData } from "vitepress"; 2 | import { computed } from "vue"; 3 | 4 | export function useLocale() { 5 | const { lang } = useData(); 6 | 7 | const isKorean = computed(() => lang.value === "ko" || lang.value === "root"); 8 | 9 | return { 10 | isKorean 11 | }; 12 | } -------------------------------------------------------------------------------- /fundamentals/shared/config/OneNavigationItems.ts: -------------------------------------------------------------------------------- 1 | export interface NavItem { 2 | path: string; 3 | href: string; 4 | tooltip: { 5 | ko: string; 6 | en: string; 7 | }; 8 | icon: string; 9 | } 10 | 11 | export const ONE_NAVIGATION_ITEMS: NavItem[] = [ 12 | { 13 | path: "/code-quality/", 14 | href: "/code-quality/", 15 | tooltip: { 16 | ko: "코드퀄리티", 17 | en: "Code Quality" 18 | }, 19 | icon: `` 20 | }, 21 | { 22 | path: "/bundling", 23 | href: "/bundling/", 24 | tooltip: { 25 | ko: "번들링", 26 | en: "Bundling" 27 | }, 28 | icon: ` 29 | 30 | ` 31 | }, 32 | // { 33 | // path: "/accessibility", 34 | // href: "/code-quality/code/coming-soon", 35 | // tooltip: { 36 | // ko: "접근성", 37 | // en: "Accessibility" 38 | // }, 39 | // icon: `` 40 | // } 41 | ]; -------------------------------------------------------------------------------- /images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/images/apple-touch-icon.png -------------------------------------------------------------------------------- /images/bf-meta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/images/bf-meta.png -------------------------------------------------------------------------------- /images/bf-symbol.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/images/bf-symbol.webp -------------------------------------------------------------------------------- /images/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/images/favicon-96x96.png -------------------------------------------------------------------------------- /images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/images/favicon.ico -------------------------------------------------------------------------------- /images/ff-meta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/images/ff-meta.png -------------------------------------------------------------------------------- /images/ff-symbol-gradient-webp-80.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/images/ff-symbol-gradient-webp-80.webp -------------------------------------------------------------------------------- /images/ff-symbol-gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/images/ff-symbol-gradient.png -------------------------------------------------------------------------------- /images/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Frontend Fundamentals", 3 | "short_name": "FF", 4 | "icons": [ 5 | { 6 | "src": "/images/web-app-manifest-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/images/web-app-manifest-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } 22 | -------------------------------------------------------------------------------- /images/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/images/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /images/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/images/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend-fundamentals", 3 | "private": true, 4 | "packageManager": "yarn@4.6.0", 5 | "workspaces": [ 6 | "fundamentals/*" 7 | ], 8 | "engines": { 9 | "node": ">=22.0.0" 10 | }, 11 | "scripts": { 12 | "docs:dev": "yarn workspace @frontend-fundamentals/code-quality docs:dev", 13 | "docs:build": "yarn workspace @frontend-fundamentals/code-quality docs:build", 14 | "docs:preview": "yarn workspace @frontend-fundamentals/code-quality docs:preview", 15 | "docs:bundle:dev": "yarn workspace @frontend-fundamentals/bundling docs:dev", 16 | "docs:bundle:build": "yarn workspace @frontend-fundamentals/bundling docs:build", 17 | "docs:bundle:preview": "yarn workspace @frontend-fundamentals/bundling docs:preview", 18 | "docs:debug:dev": "yarn workspace @frontend-fundamentals/debug docs:dev", 19 | "docs:debug:build": "yarn workspace @frontend-fundamentals/debug docs:build", 20 | "docs:debug:preview": "yarn workspace @frontend-fundamentals/debug docs:preview", 21 | "docs:a11y:dev": "yarn workspace @frontend-fundamentals/a11y docs:dev", 22 | "docs:a11y:build": "yarn workspace @frontend-fundamentals/a11y docs:build", 23 | "docs:a11y:preview": "yarn workspace @frontend-fundamentals/a11y docs:preview", 24 | "postbuild": "mkdir -p dist/fundamentals/code-quality dist/fundamentals/bundling dist/fundamentals/debug dist/fundamentals/a11y && cp -r fundamentals/code-quality/.vitepress/dist/* dist/fundamentals/code-quality/ && cp -r fundamentals/bundling/.vitepress/dist/* dist/fundamentals/bundling/ && cp -r fundamentals/debug/.vitepress/dist/* dist/fundamentals/debug/ && cp -r fundamentals/a11y/.vitepress/dist/* dist/fundamentals/a11y/", 25 | "build": "yarn docs:build && yarn docs:bundle:build && yarn docs:debug:build && yarn docs:a11y:build && yarn postbuild" 26 | }, 27 | "dependencies": { 28 | "@amplitude/analytics-browser": "^2.11.11", 29 | "markdown-it-footnote": "^4.0.0", 30 | "typescript": "^5.6.3", 31 | "vitepress": "^1.4.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/files/bundling-example-project.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/public/files/bundling-example-project.zip -------------------------------------------------------------------------------- /public/files/bundling-example.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/frontend-fundamentals/f65af82abcd45edc1062b2826bc693e612a579f4/public/files/bundling-example.zip -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "allowJs": true, 7 | "strict": true, 8 | "jsx": "preserve", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "noEmit": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "@frontend-fundamentals/*": ["fundamentals/*/src"] 17 | } 18 | }, 19 | "exclude": ["**/node_modules", "**/dist"] 20 | } -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "redirects": [ 3 | { 4 | "source": "/", 5 | "destination": "/code-quality/", 6 | "permanent": true 7 | } 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "/fundamentals/code-quality/:path*", 12 | "destination": "/dist/fundamentals/code-quality/:path*" 13 | }, 14 | { 15 | "source": "/fundamentals/bundling/:path*", 16 | "destination": "/dist/fundamentals/bundling/:path*" 17 | }, 18 | { 19 | "source": "/code-quality/:path*", 20 | "destination": "/dist/fundamentals/code-quality/:path*" 21 | } 22 | ] 23 | } --------------------------------------------------------------------------------