Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8938a66
과제시작
milmilkim Nov 29, 2025
1ac51e6
컴포넌트 분리
milmilkim Nov 29, 2025
a2a3eaf
state props로 전달
milmilkim Nov 29, 2025
ad9350b
constants 분리
milmilkim Nov 29, 2025
b7de744
useDebounce
milmilkim Nov 29, 2025
8e9e7d8
useLocalStorage
milmilkim Nov 29, 2025
5371426
커스텀 hooks 분리
milmilkim Nov 30, 2025
9c4e5c4
SearchBar 컴포넌트 분리
milmilkim Nov 30, 2025
46342e7
cartModels, useCart
milmilkim Nov 30, 2025
d73a69d
useCart 바인딩
milmilkim Nov 30, 2025
ec36f13
Cart 핸들러 함수 이름 변경
milmilkim Nov 30, 2025
e2fb80b
벌크 할인 추가 및 사용하지 않는 코드 삭제
milmilkim Nov 30, 2025
3e17da1
검색어 디바운스 위치 수정
milmilkim Nov 30, 2025
72d2d08
useNotifications, Notification
milmilkim Nov 30, 2025
a018397
Cart, Admin 위치 변경
milmilkim Nov 30, 2025
479841e
deboune 위치 변경
milmilkim Nov 30, 2025
3477367
utils 추가
milmilkim Nov 30, 2025
9fab33c
useProducts
milmilkim Nov 30, 2025
0b4ebbb
useCoupons
milmilkim Nov 30, 2025
923ce71
어드민 탭 분리
milmilkim Nov 30, 2025
3de8ef8
컴포넌트 분리
milmilkim Dec 3, 2025
bc5ef19
어드민 컴포넌트 분리
milmilkim Dec 3, 2025
07782e3
유틸 함수 적용
milmilkim Dec 3, 2025
01e1389
productModel 추가
milmilkim Dec 3, 2025
c3323c0
advanced
milmilkim Dec 3, 2025
6cceea4
context 리팩토링
milmilkim Dec 4, 2025
793d921
context 리팩토링
milmilkim Dec 4, 2025
1b7e700
deploy
milmilkim Dec 4, 2025
382f5ec
deploy
milmilkim Dec 4, 2025
a29536a
Update README.md
milmilkim Dec 8, 2025
5931061
Update README.md
milmilkim Dec 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Deploy to GitHub Pages

on:
push:
branches:
- main

workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: 'pages'
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build
run: pnpm run build:deploy

- name: Set advanced as default
run: cp dist/index.advanced.html dist/index.html

- name: Copy 404.html
run: cp dist/index.html dist/404.html

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: dist

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
6 changes: 6 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
dist
build
*.min.js
pnpm-lock.yaml

9 changes: 9 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80,
"arrowParens": "always"
}

128 changes: 20 additions & 108 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,119 +1,31 @@
# Chapter3-2. 디자인 패턴과 함수형 프로그래밍 그리고 상태 관리 설계

## 기본과제: 거대 단일 컴포넌트 리팩토링
함수형 프로그래밍에서 말하는 액션, 계산, 데이터의 개념을 정리하고 그 원칙에 따라 리팩토링 했다.
그리고 그냥 내 맘대로, 감으로 익힌 느낌으로 개발을 할 때는 다른 작업자에게 어떤 방향으로 개발해야 하는지 명확한 방법과 이유를 설명하지 못했었는데, 언어를 알게 되어서 도움이 되었다.

이번 과제는 단일책임원칙을 위반한 거대한 컴포넌트를 리팩토링 하는 것입니다. React의 컴포넌트는 단일 책임 원칙(Single Responsibility Principle, SRP)을 따르는 것이 좋습니다. 즉, 각 컴포넌트는 하나의 책임만을 가져야 합니다. 하지만 실제로는 여러 가지 기능을 가진 거대한 컴포넌트를 작성하는 경우가 많습니다.
이 과정에선 약 1000줄에 임박한 단일 컴포넌트인 `App.tsx`를 전면 개편했다.
기존과 동일하게 작동하면서 오류가 생기지 않게 작업하기 위해 우선적으로 페이지 단위의 큰 것부터 컴포넌트를 분리하고 모든 데이터의 전달을 props로 처리했다.
그리고 cart, coupon...등의 엔티티 단위를 나누어 커스텀 훅으로 만들었다.

[목표]
## 1. 취지
- React의 추구미(!)를 이해해보아요!
- 단일 책임 원칙(SRP)을 위반한 거대한 컴포넌트가 얼마나 안 좋은지 경험해보아요!
- 단일 책임이라는 개념을 이해하기 상태, 순수함수, 컴포넌트, 훅 등 다양한 계층을 이해해합니다.
- 엔티티와 UI를 구분하고 데이터, 상태, 비즈니스 로직 등의 특징이 다르다는 것을 이해해보세요.
- 이를 통해 적절한 Custom Hook과 유틸리티 함수를 분리하고, 컴포넌트 계층 구조를 정리하는 능력을 갖춥니다!
basic은 상태 관리 라이브러리나 Context API를 사용하지 않고 리팩토링 한 것이고, advanced는 Context API를 사용하여 Props drilling을 해소한 버전이다.

basic을 마치고 advanced로 넘어가며, 이 지긋지긋한 props drilling을 감내하였으니 이제 context로 전환하는 작업만 더해서, 최종적으로 코드를 보기 좋게 정리하려 했다.

## 2. 목표
하나는 상태 관리에 대한 것이었는데 나는 Context API를 그저 props drilling을 없애기 위한 목적으로만 사용할 생각이었다.
그런데 'Context는 Props drilling을 해결하기 위한 것이 아니다'라는 말을 보게 되고, 일단 그 말도 헷갈렸지만 그래서 상태관리는 무엇인가 하는 근원적인 의문에 빠져들었다.
(원래같았더라면 API를 사용하여 조회하는 부분을 클라이언트에서 다루어야 했기에 이런 구조가 되었다. 실제 프로젝트였다면 서버 상태는 Tasntack Query를 사용하는 선으로 정리됐을 것이다.)

모든 소프트웨어에는 적절한 책임과 계층이 존재합니다. 하나의 계층(Component)만으로 소프트웨어를 구성하게 되면 나중에는 정리정돈이 되지 않은 코드를 만나게 됩니다. 예전에는 이러한 BestPractice에 대해서 혼돈의 시대였지만 FE가 진화를 거듭하는 과정에서 적절한 계측에 대한 합의가 이루어지고 있는 상태입니다.
사실 일반적인 웹 프로젝트라면 상태 관리를 꼭 써야 할 일이 많지는 않은 듯 하다.
현실 세계와는 좀 다른 결과물이겠으나,
서버에서 데이터를 받아오는 것 자체가 액션이고, 그걸 화면에 보여주기 위해 가공하는 건 계산이며, 가공된 결과물은 데이터다-이런식으로 분리하는 원칙을 챙겨서 리액트의 추구미를 느껴보았다.

React의 주요 책임 계층은 Component, hook, function 등이 있습니다. 그리고 주요 분류 기준은 **엔티티**가 되어 줍니다.
`useEffect`훅을 거의 생명주기를 대체하여 사용하고 있으나 사실 생명주기와 동일하거나, 그것을 대체하기 위해 만들어진 훅이 아니라는 말은 여기저기서 많이 보았다.
함수형 프로그래밍 관점에서 보면 useEffect는 액션을 다루는 곳이다. API 호출, 구독 설정, 타이머, DOM 직접 조작 같은 부수효과들을 컴포넌트 렌더링 로직(계산)과 분리해서 관리하는 것.
렌더링 자체는 순수해야 하고, 부수효과는 useEffect 안에 격리시키는 것이다.

- 엔티티를 다루는 상태와 그렇지 않은 상태 - cart, isCartFull vs isShowPopup
- 엔티티를 다루는 컴포넌트와 훅 - CartItemView, useCart(), useProduct()
- 엔티티를 다루지 않는 컴포넌트와 훅 - Button, useRoute, useEvent 등
- 엔티티를 다루는 함수와 그렇지 않은 함수 - calculateCartTotal(cart) vs capaitalize(str)
나는 이미 함수형 컴포넌트가 탄생되어 보급된 후의 세상에서 개발을 시작했기 때문에 순수한 렌더링만 하던 시절의 함수형 컴포넌트와 그 역사를 체감하지 못했으나 함수형 프로그래밍에 대해 알아보면서 이런 식으로 발전한 이유를 따라갈 수 있었다.

이번 과제의 목표는 이러한 계층을 이해하고 분리하여 정리정돈을 하는 기준이나 방법등을 습득하는데 있습니다.
다른 하나는 폴더 구조에 대한 것인데 사실 폴더 구조라는 것은 다른 많은 개발자들도 많이 고민하는 부분이라 내가 결론을 내리기도 어려운 부분이다.
다른 분들과 대화를 나누거나 발제를 들으며 생각했지만 역시 폴더 구조가 불편해지는 것은 2depth부터 규칙이 없기 떄문인 듯 하다.

제시된 코드는 각각의 컴포넌트에 모든 비즈니스 로직이 작성되어 있습니다. 여기에서 custom hook과 util 함수를 적절하게 분리하고, **테스트 코드를 통과할 수 있도록 해주세요.**

> basic의 경우 상태관리를 쓰지 않고 작업을 해주세요.

### (1) 요구사항

#### 1) 장바구니 페이지 요구사항

- 상품 목록
- 상품명, 가격, 재고 수량 등을 표시
- 각 상품의 할인 정보 표시
- 재고가 없는 경우 품절 표시가 되며 장바구니 추가가 불가능
- 장바구니
- 장바구니 내 상품 수량 조절 가능
- 각 상품의 이름, 가격, 수량과 적용된 할인율을 표시
- 적용된 할인율 표시 (예: "10% 할인 적용")
- 장바구니 내 모든 상품의 총액을 계산해야
- 쿠폰 할인
- 할인 쿠폰을 선택하면 적용하면 최종 결제 금액에 할인정보가 반영
- 주문요약
- 할인 전 총 금액
- 총 할인 금액
- 최종 결제 금액

#### 2) 관리자 페이지 요구사항

- 상품 관리
- 상품 정보 (상품명, 가격, 재고, 할인율) 수정 가능
- 새로운 상품 추가 가능
- 상품 제거 가능
- 할인 관리
- 상품별 할인 정보 추가/수정/삭제 가능
- 할인 조건 설정 (구매 수량에 따른 할인율)
- 쿠폰 관리
- 전체 상품에 적용 가능한 쿠폰 생성
- 쿠폰 정보 입력 (이름, 코드, 할인 유형, 할인 값)
- 할인 유형은 금액 또는 비율로 설정 가능

### (2) 코드 개선 요구사항

#### 1) cart, product에 대한 계산 함수 분리

- calculateItemTotal
- getMaxApplicableDiscount
- calculateCartTotal
- updateCartItemQuantity

#### 2) 상태를 다루는 hook, 유틸리티 hook 분리

- useCart
- useCoupon
- useProduct
- useLocalStorage

#### 3) 엔티티 컴포넌트와 UI 컴포넌트 분리하여 계층구조 만들기

- ProductCard
- Cart
- …

### (3) 테스트 코드 통과하기



## 심화과제: Props drilling
> **이번 심화과제는 Props drilling을 없애기 입니다.**

# 2. 목표

- basic에서 열심히 컴포넌트를 분리해주었겠죠?
- 아마 그 과정에서 container - presenter 패턴으로 만들어졌기에 props drilling이 상당히 불편했을거에요.
- 그래서 심화과제에서는 props drilling을 제거하는 작업을 할거에요.
- 전역상태관리가 아직 낯설다. - jotai를 선택해주세요 (참고자료 참고)
- 나는 React만으로 해보고 싶다. - context를 선택해서 상태관리를 해보세요.
- 나는 지금 대세인 Zustand를 할거에요. - zustand를 선택해주세요.


### (1) 요구사항

- 불필요한 props를 제거하고, 필요한 props만을 전달하도록 개선합니다.
- Context나 Jotai 혹은 Zustand를 사용하여 상태를 관리합니다.
- 테스트 코드를 통과합니다.

### (2) 힌트

- UI 컴포넌트와 엔티티 컴포넌트는 각각 props를 다르게 받는게 좋습니다.
- UI 컴포넌트는 재사용과 독립성을 위해 상태를 최소화하고,
- 엔티티 컴포넌트는 가급적 엔티티를 중심으로 전달받는 것이 좋습니다.
- 특히 콜백의 경우,
- UI 컴포넌트는 이벤트 핸들러를 props로 받아서 처리하도록 해서 재사용성을 높이지만,
- 엔티티 컴포넌트는 props가 아닌 컴포넌트 내부에서 상태를 관리하는 것이 좋습니다.
FSD 아키텍처가 이런 부분을 해결할 수 있는 방법이라고 하는데 일 하면서 써본 적은 없다. 그래서 조금씩 알아보는 중.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"test:advanced": "vitest src/advanced",
"test:ui": "vitest --ui",
"build": "tsc -b && vite build",
"build:advanced": "tsc -b && vite build --config vite.config.ts",
"build:deploy": "tsc -b && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
Expand All @@ -33,6 +35,7 @@
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"jsdom": "^26.1.0",
"prettier": "^3.7.2",
"typescript": "^5.9.2",
"vite": "^7.0.6",
"vitest": "^3.2.4"
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading