yuhyeon99 님의 상세페이지[5팀 김유현] Chapter 2-1. 클린코드와 리팩토링

과제 체크포인트

배포 링크

https://yuhyeon99.github.io/front_6th_chapter2-1/

기본과제

  • 코드가 Prettier를 통해 일관된 포맷팅이 적용되어 있는가?
  • 적절한 줄바꿈과 주석을 사용하여 코드의 논리적 단위를 명확히 구분했는가?
  • 변수명과 함수명이 그 역할을 명확히 나타내며, 일관된 네이밍 규칙을 따르는가?
  • 매직 넘버와 문자열을 의미 있는 상수로 추출했는가?
  • 중복 코드를 제거하고 재사용 가능한 형태로 리팩토링했는가?
  • 함수가 단일 책임 원칙을 따르며, 한 가지 작업만 수행하는가?
  • 조건문과 반복문이 간결하고 명확한가? 복잡한 조건을 함수로 추출했는가?
  • 코드의 배치가 의존성과 실행 흐름에 따라 논리적으로 구성되어 있는가?
  • 연관된 코드를 의미 있는 함수나 모듈로 그룹화했는가?
  • ES6+ 문법을 활용하여 코드를 더 간결하고 명확하게 작성했는가?
  • 전역 상태와 부수 효과(side effects)를 최소화했는가?
  • 에러 처리와 예외 상황을 명확히 고려하고 처리했는가?
  • 코드 자체가 자기 문서화되어 있어, 주석 없이도 의도를 파악할 수 있는가?
  • 비즈니스 로직과 UI 로직이 적절히 분리되어 있는가?
  • 코드의 각 부분이 테스트 가능하도록 구조화되어 있는가?
  • 성능 개선을 위해 불필요한 연산이나 렌더링을 제거했는가?
  • 새로운 기능 추가나 변경이 기존 코드에 미치는 영향을 최소화했는가?
  • 코드 리뷰를 통해 다른 개발자들의 피드백을 반영하고 개선했는가?
  • (핵심!) 리팩토링 시 기존 기능을 그대로 유지하면서 점진적으로 개선했는가?

심화과제

  • 변경한 구조와 코드가 기존의 코드보다 가독성이 높고 이해하기 쉬운가?
  • 변경한 구조와 코드가 기존의 코드보다 기능을 수정하거나 확장하기에 용이한가?
  • 변경한 구조와 코드가 기존의 코드보다 테스트를 하기에 더 용이한가?
  • 변경한 구조와 코드가 기존의 모든 기능은 그대로 유지했는가?
  • (핵심!) 변경한 구조와 코드를 새로운 한번에 새로만들지 않고 점진적으로 개선했는가?

과제 셀프회고

이번 과제를 수행하면서 가장 신경 쓴 부분은 GEMINI.md에 명시된 "마이그레이션 절대 원칙"과 "Clean Code Writing Rules"를 준수하는 것이었습니다. 특히 basic폴더의 순수 JavaScript 코드와 advanced 폴더의 React 기반 코드 간 차이에서 기술적인 고민이 많았습니다.

회고 (클릭해서 열기)

1. 과제 목표

  • 클린 코드와 리팩토링
  • JS 기반 코드를 React + TS 기반 코드로 마이그레이션

2. 클린 코드란?

  • 읽기 쉽고 이해하기 쉬운 코드를 작성하는것
  • 코드자체가 가독성이 뛰어나고 유지보수가 쉽도록 작성되어야 한다는 원칙

일상 속에서도 클린 코드의 개념을 살펴볼 수 있다. 예를 들어 아버지가 방에 들어가시는 상황을 설명할 때:

아버지가방에들어가신다 -> 아버지가 방에 들어가신다.

아버지가방에들어가신다 처럼 띄어쓰기를 하지 않은 채로 작성하면 가독성이 떨어져서 무얼 의미하는지 파악하기가 힘들다.

우리가 작성하는 코드 또한 마찬가지다.

발제 자료를 참고해서 더 자세히 설명하자면

  • 시간이 지나고 시스템이 안정이 되면서 코드를 작성하고 버그를 수정하는 데 비용은 줄어든다.
  • 뭐가 버그이고 뭐가 문제인지 기존의 기능을 확인하고 수정 후 안정성을 확인하는 비용은 점점 비싸진다.

대부분의 개발자들은 코드를 쓰는 것 보다 코드를 읽느라 시간을 거의 다 보낸다.

이러한 문제점들을 해결하기 위한 가장 좋은 방법은

언제나 코드를 깨끗하게 유지하는 습관 이다. 그런 습관을 만들기 위해서는 여러 원칙들을 정해서 일관성 있는 코드를 작성하는 자세가 중요하다.

관련해서 정리해본 몇 가지 원칙들은 다음과 같다:

  • DRY (Don't Repeat Yourself): 공통으로 사용되는 로직이나 UI 요소는 함수나 컴포넌트로 분리
  • 단일 책임 원칙 (Single Responsibility Principle): 각 함수나 컴포넌트가 하나의 명확한 책임을 가지도록 설계
  • 명명 규칙 (Naming Requirements): create~(), get~(), is~ , has~와 같은 예측 가능한 명명 패턴을 엄격하도록 준수. 가급적이면 직관적이고 명확한 명칭으로 작성한다.
  • 코드 조직화 (Code Organization): 관심사별로 폴더를 나누고, 관련 요소들을 근접하게 배치하여 코드의 응집도를 높인다.

이제 해당 원칙들을 기반으로 더러운 코드를 깨끗하게 만들어보자


3. Code Smell(원본 코드)

현재 주어진 의도적으로 더럽게 작성된 코드를 각 항목별로 짧게 분석해보자

전체 코드: 깃허브 링크

3-1. 전역 변수 남발 & 네이밍 규칙 없음

var prodList;
var bonusPts = 0;
var stockInfo;
var itemCnt;
var lastSel;
var sel;
var addBtn;
var totalAmt = 0;
var PRODUCT_ONE = 'p1';
var p2 = 'p2';
var product_3 = 'p3';
var p4 = 'p4';
var PRODUCT_5 = `p5`;
var cartDisp;
  • **문제점: **
    • 전역 스코프에 너무 많은 변수를 선언
      • 모듈화와 테스트가 어려움
    • 네이밍 규칙이 제각각 다름(PRODUCT_ONE, p2, product_3)
      • 일관성 부족으로 인한 유지보수 비용 증가
    • UI 요소(sel, addBtn, cartDisp)와 상태(prodList, totalAmt)가 혼합되어 있음
      • 관심사의 분리 실패(응집도 낮음, 결합도 높음)

3-2. 거대한 main() 함수 (SRP 원칙 위반)

function main() {
  var root;
  var header;
  var gridContainer;
  var leftColumn;
  var selectorContainer;
  var rightColumn;
  var manualToggle;
  var manualOverlay;
  var manualColumn;
  var lightningDelay;
  
  totalAmt = 0;
  itemCnt = 0;
  lastSel = null;
  
  prodList = [
    { id: PRODUCT_ONE, name: '버그 없애는 키보드', val: 10000, q: 50 },
    { id: p2, name: '생산성 폭발 마우스', val: 20000, q: 30 },
    // ...
  ];

  // DOM 생성, 이벤트 바인딩, 초기 상태 세팅, 세일 타이머 로직이 전부 들어있음
  // ...
}
  • **문제점: **
    • UI 초기화, 상태 초기화, 비즈니스 로직, 이벤트 바인딩이 모두 하나의 함수에 섞여 있음
    • 함수 길이가 너무 길어 가독성 저하유지보수 어려움
    • 테스트하기 힘들고 재사용 불가능

3-3. 중첩 if-else 로직

if (q >= 10) {
  if (curItem.id === PRODUCT_ONE) {
    disc = 10 / 100;
  } else {
    if (curItem.id === p2) {
      disc = 15 / 100;
    } else {
      if (curItem.id === product_3) {
        disc = 20 / 100;
      } else {
        if (curItem.id === p4) {
          disc = 5 / 100;
        } else {
          if (curItem.id === PRODUCT_5) {
            disc = 25 / 100;
          }
        }
      }
    }
  }
}
  • 문제점:
    • 중첩 if-else가 깊어서 가독성이 매우 떨어짐
    • 할인 정책이 데이터가 아닌 조건문 로직으로 하드코딩
      • 변경 시 코드 전체 수정 필요
    • 새로운 제품 추가 시 if-else 블록이 더 커짐
      • OCP(Open/Closed Principle) 위반
        • 소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다는 원칙
        • 기능 추가 요청이 오면 클래스를 확장을 통해 손쉽게 구현하면서, 확장에 따른 클래스 수정최소화 하도록 프로그램을 작성해야 하는 설계 기법을 말함.

3-4. DOM 직접 조작 + 비즈니스 로직 혼합

opt = document.createElement('option');
opt.value = item.id;
if (item.q === 0) {
  opt.textContent = item.name + ' - ' + item.val + '원 (품절)';
  opt.disabled = true;
} else {
  opt.textContent = item.name + ' - ' + item.val + '원';
}
sel.appendChild(opt);
  • **문제점: **
    • DOM 조작과 데이터 처리 로직이 섞여 있음
    • 재사용성과 테스트 가능성 낮음
    • React, Vue 같은 선언형 UI로 전환이 어려움

3-5. 매직 넘버 & 하드코딩된 문자열

if (itemCnt >= 30) {
  totalAmt = (subTot * 75) / 100;
}
if (new Date().getDay() === 2) {
  totalAmt = (totalAmt * 90) / 100;
}
  • **문제점: **
    • 30, 75, 90, 2와 같은 숫자가 의미 없이 등장
      • 매직 넘버로 인해 코드 가독성이 떨어지고 유지보수가 어려워짐
    • 할인율, 요일 로직이 코드에 직접 하드코딩
      • 정책 변경 시 코드 수정 필요

3-6. 중복 코드

var totalCount = 0;
for (j = 0; j < cartDisp.children.length; j++) {
  totalCount += parseInt(
    cartDisp.children[j].querySelector('.quantity-number').textContent
  );
}

var totalCount = 0;
for (j = 0; j < cartDisp.children.length; j++) {
  totalCount += parseInt(
    cartDisp.children[j].querySelector('.quantity-number').textContent
  );
}

**문제점: **

  • 동일한 로직이 두 번 등장
    • DRY 원칙 위반
  • 수정 시 한 곳만 바꾸면 버그 발생 가능성 ↑

4. Gemini CLI와 협업

최근 AI 툴들이 쏟아지는 와중에 현업에서도 활용 능력을 요구하는 상황이 종종 주어진다. 과제 취지 또한 AI를 적극 활용해서 클린한 코드를 만드는걸 지향하고 있다.

이 과정에서 최근 구글에서 무료로 풀어놔서 최근에 활용중인 Gemini CLI를 사용하기로 했다.

불편했던 점

  • 터미널에서 실행되기 때문에 긴 컨텍스트를 전달하기가 쉽지 않다.
  • 대규모 작업에는 좋지 않은 성능(이건 모든 AI가 동일한가? 비교적 좋지 않은 듯 하다.)

개선 방안

  • GEMINI.md 파일로 지침 설정
    • 해당 지침을 설정한 후 "GEMINI.md 파일을 참고해서 ~ 작업을 진행하는 과정 설명해" 와 같은 긴 컨텍스트가 필요한 질문을 1줄로 축약해서 요청할 수 있음.
  • 가급적 작은 단위와 명확한 요청사항으로 명령
    • 분할과 정복의 개념을 AI 와의 협업에도 적용시키면 좋다.
    • 만약 내가 하는 요청이 큰 작업 내용인지 잘 모르겠다면 "해당 요구사항의 작업 단위가 크다면 작게 분할해서 작업을 진행해" 와 같은 내용으로 요청

이런 식으로 AI 와 함께 클린 코드를 만들어가고 마이그레이션을 진행해보면서 다가올지도 모르는 AI의 시대에 적응할 수 있는 기반을 다지는 좋은 기회가 되었다.

5. JS 환경에서의 리팩토링

기본 과제는 DOM을 직접 조작하는 순수 JavaScript 환경이었기 때문에 리팩토링 과정에서 React와는 다른 기술적 고민이 필요했다.

5.1. 전체적인 구조 설계

src/basic 폴더는 다음과 같은 모듈화된 구조를 가지고 있었다. 각 파일은 명확한 책임을 가지도록 설계되어 코드의 가독성과 유지보수성을 높임.

  • main.basic.js
    • 애플리케이션의 진입점 역할을 하며 전역 상태를 초기화하고 주요 컴포넌트를 DOM에 렌더링
    • 이벤트 리스너를 설정하고 render 함수를 호출하여 UI를 업데이트
  • components/
    • UI를 구성하는 작은 단위의 컴포넌트를 모아둔 폴더
      • (예: CartItem.js, Header.js, HelpModal.js, Layout.js, Selector.js)
    • 각 컴포넌트는 자신의 DOM 요소를 생성하고 반환하는 역할을 담당
  • product/
    • 상품 관련 로직과 상태를 관리
      • (예: productState.js, lightningSale.js)
  • cart/
    • 장바구니 관련 로직과 상태를 관리
      • (예: cartState.js)
  • point/
    • 포인트 관련 로직과 상태를 관리
      • (예: pointState.js)
  • handlers.js
    • 사용자 인터랙션(상품 추가, 수량 변경 등)에 대한 이벤트 핸들러 함수를 정의
  • render.js
    • 애플리케이션의 상태 변화에 따라 UI를 업데이트하는 핵심 렌더링 로직을 담당
  • calculation.js
    • 장바구니 총액, 할인율, 포인트 계산 등 순수 계산 로직을 분리
  • utils/
    • 범용적으로 사용되는 유틸리티 함수들을 모아둠
      • (예: utils.js, errorMessages.js)
  • tests/
    • 각 모듈에 대한 단위 테스트 코드를 포함했다.
      • 단일 책임 원칙과 코드 조직화 원칙을 적용해 각 모듈이 독립적으로 테스트되고 관리될 수 있도록 했다.

5.2. 리팩토링 과정에서 발생한 문제점 및 해결 방안

5.2.1. 문제점 1: 카트 아이템의 동적 할인 가격 표시 및 이벤트 리스너 유실

**문제 상황: **

  • 번개세일·추천할인 등 상품 가격이 변경될 때 cart-items 영역의 개별 상품 가격과 총 가격이 할인 UI로 올바르게 반영되지 않음
  • src/basic/render.js에서 CartItem DOM 요소를 통째로 교체하려 했을 때 기존에 부착된 수량 변경 버튼의 이벤트 리스너가 유실되는 회귀 발생
  • npx vitest run 실행 시 expected '2' to be '1' AssertionError로 문제를 확인함

**기술적 고민: **

  • GEMINI.md의 기능 불변성 유지 원칙에 따라 기존 기능(수량 변경 버튼의 작동)을 유지해야 했음
  • 순수 JS에서 DOM 교체 시 이벤트 리스너가 함께 사라진다는 특성을 고려해야 했음
  • 이벤트를 유지하면서 UI를 동적으로 업데이트하는 방식이 필요했음

**해결 방안: **

  1. render.js 수정
    • CartItem 전체를 교체하지 않고 기존 DOM 요소를 재활용, 내부 특정 요소만 업데이트하도록 변경함
  2. 가격 표시 HTML 직접 갱신
    • p.onSale 또는 p.suggest 플래그를 확인해 원래 가격에는 취소선을, 할인 가격은 빨간색 강조를 적용한 HTML을 생성해 innerHTML로 삽입함

코드 예시 (src/basic/render.js)

state.cartItems.forEach((ci) => {
  alive.add(ci.id);
  let row = cartWrap.querySelector(`#${ci.id}`);
  if (!row) {
    row = CartItem(ci, state);
    cartWrap.appendChild(row);
  }
  row.querySelector('.quantity-number').textContent = ci.qty;
  const product = state.productList.find((p) => p.id === ci.id);

  const individualPriceHtml = product.onSale || product.suggest
    ? `<span class="line-through text-gray-400">₩${product.originalPrice.toLocaleString()}</span> <span class="text-red-500">₩${product.price.toLocaleString()}</span>`
    : `${product.price.toLocaleString()}`;
  row.querySelector('.text-xs.text-black.mb-3').innerHTML = individualPriceHtml;

  const totalPriceHtml = product.onSale || product.suggest
    ? `<span class="line-through text-gray-400">₩${(product.originalPrice * ci.qty).toLocaleString()}</span> <span class="text-red-500">₩${(product.price * ci.qty).toLocaleString()}</span>`
    : `${(product.price * ci.qty).toLocaleString()}`;
  row.querySelector('.text-lg').innerHTML = totalPriceHtml;
});

이 접근 방식으로 기존 이벤트 리스너를 유지하면서 할인 UI를 실시간으로 반영할 수 있었다.


5.2.2. 문제점 2: "이용 안내" 모달 버튼의 UI/UX 불일치

문제 상황:

  • HelpModal.js 추가 시 모달이 열려도 원래의 토글 버튼이 사라지지 않음
  • 버튼 시각 스타일이 main.original.js와 달라 UI 일관성이 깨짐

기술적 고민:

  • GEMINI.md의 동작 일관성 보장 원칙에 따라 동일한 사용자 경험을 제공해야 했음
  • 순수 JS 환경에서 DOM 가시성 제어, 동적으로 추가된 'X' 버튼의 이벤트 리스너 관리, 인라인 onclick 제거가 필요했음

해결 방안:

  • 버튼 시각 스타일 통일 helpBtn에 manualToggle과 동일한 Tailwind CSS 및 SVG 아이콘을 적용함

  • 가시성 제어 로직 추가

    • helpBtn.onclick: 모달 열릴 때 helpBtn 숨김
    • closeHelpModal(): 모달 닫을 때 helpBtn 다시 표시
    • 오버레이 클릭, 'X' 버튼 클릭 시 closeHelpModal 호출
    • 'X' 버튼의 인라인 onclick 제거 후 querySelector로 이벤트 동적 부착

코드 예시 (src/basic/components/HelpModal.js)

const helpBtn = document.createElement('button');
helpBtn.className = 'fixed top-4 right-4 bg-black text-white p-3 rounded-full hover:bg-gray-900 transition-colors z-50';
helpBtn.innerHTML = `
  <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
  </svg>
`;

slide.innerHTML = `
  <button class="absolute top-4 right-4 text-gray-500 hover:text-black help-modal-close-btn">
    <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
    </svg>
  </button>
  <h2 class="text-xl font-bold mb-4">이용 안내</h2>
`;

const closeHelpModal = () => {
  modal.classList.add('hidden');
  slide.classList.add('translate-x-full');
  helpBtn.classList.remove('hidden');
};

helpBtn.onclick = () => {
  modal.classList.remove('hidden');
  slide.classList.remove('translate-x-full');
  helpBtn.classList.add('hidden');
};

modal.onclick = (e) => {
  if (e.target === modal) closeHelpModal();
};

slide.querySelector('.help-modal-close-btn').onclick = () => {
  closeHelpModal();
};

이렇게 리팩토링해 main.original.js와 동일한 UX를 확보했고 인라인 이벤트를 제거하면서 클린 코드 원칙도 지킬 수 있었다.

6. React + TS 환경으로 마이그레이션

src/advanced 폴더는 React와 TypeScript를 기반으로 구축해야 한다.

basic 폴더의 순수 JavaScript 코드와 달리 React의 컴포넌트 기반 아키텍처, Hooks를 활용한 상태 관리, TypeScript의 타입 안정성을 적극적으로 적용하며 개발을 진행했다.

마이그레이션 과정에서는 GEMINI.md의 React 관용적 코드와 TypeScript 우선 원칙을 지키면서 리팩토링을 진행했다.

6.1. 전체적인 구조 설계

src/advanced 폴더는 다음과 같은 계층적이고 모듈화된 구조를 가지고 있었다.

  • App.tsx

    • 애플리케이션의 최상위 컴포넌트
    • 상품 목록과 장바구니 상태 등 주요 상태를 관리하고 하위 컴포넌트에 props로 전달
    • 커스텀 훅을 사용해 비즈니스 로직과 상태 관리를 분리
  • components/

    • 재사용 가능한 UI 컴포넌트를 모아둔 폴더
    • (예: Cart.tsx, CartItem.tsx, Header.tsx, HelpModal.tsx, Layout.tsx, OrderSummary.tsx, Selector.tsx)
    • 각 컴포넌트는 자신의 UI 렌더링 로직을 캡슐화
  • hooks/

    • React Hooks를 이용해 상태 관리 로직과 기능을 추상화한 커스텀 훅 정의
    • (예: useProductState.ts, useCartState.ts, useLightningSale.ts, useCartActions.ts)
  • types/

    • TypeScript 타입 정의 파일을 모아둔 폴더
    • (예: cart.ts, point.ts, product.ts)
    • 데이터 구조의 타입을 명확히 정의해 코드 안정성과 생산성을 높임
  • utils/

    • 범용적으로 사용되는 유틸리티 함수들을 모아둔 폴더
    • (예: calculation.ts, constants.ts, errorMessages.ts)

React의 컴포넌트 기반 아키텍처와 TypeScript 타입 시스템을 최대한 활용해 코드의 응집도를 높이고 결합도를 낮추는 것을 목표로 설계했다.


6.2. 리팩토링 과정에서 발생한 문제점 및 해결 방안

6.2.1. 문제점 1: CartItem.tsx에서 동적 할인 가격 표시 및 JSX 렌더링 문제

문제 상황:

  • 번개세일이 적용된 상품을 장바구니에 추가했을 때 CartItem.tsx에서 할인 가격이 올바르게 표시되지 않음
  • ₩{(product.val * item.quantity).toLocaleString()} 같은 JavaScript 표현식이 문자열 그대로 화면에 출력되는 문제가 발생함
  • basic 폴더와 유사한 문제였지만 React 환경에서는 다른 접근이 필요했음

기술적 고민:

  • useLightningSale 훅이 productList를 업데이트하지만 Cart.tsx에서 CartItem.tsx로 product 객체를 직접 전달하면 상태 변경이 즉시 반영되지 않을 수 있었음
  • React의 렌더링 사이클과 prop 불변성을 고려해야 했음
  • JSX에서 템플릿 리터럴 안의 표현식이 문자열로 평가되는 경우가 있었음

해결 방안:

  1. Cart.tsx에서 productList 전체 전달

    • CartItem에 개별 product 대신 productList 전체를 prop으로 전달하도록 변경

    코드 예시 (src/advanced/components/Cart.tsx)

    cartItems.map((item) => {
      return <CartItem key={item.id} item={item} productList={productList} />;
    })
    
  2. CartItem.tsx에서 product 동적 조회

    • item.id로 productList에서 해당 product를 찾아 항상 최신 상태를 참조하도록 수정

    코드 예시 (src/advanced/components/CartItem.tsx)

    interface CartItemProps {
      item: CartItemType;
      productList: Product[];
    }
    
    const CartItem = ({ item, productList }: CartItemProps) => {
      const product = productList.find((p) => p.id === item.id);
      if (!product) return null;
      // ...
    }
    
  3. JSX 렌더링 방식 수정

    • 템플릿 리터럴 대신 명시적인 <span> 태그를 사용해 React가 JavaScript 표현식을 올바르게 평가하도록 변경

    코드 예시 (src/advanced/components/CartItem.tsx)

    const priceDisplay = () => {
      if (product.onSale || product.suggestSale) {
        return (
          <>
            <span className="line-through text-gray-400">
    {product.originalVal.toLocaleString()}
            </span>
            <span className="text-red-500">
    {product.val.toLocaleString()}
            </span>
          </>
        );
      } else {
        return <span>{product.val.toLocaleString()}</span>;
      }
    };
    

이렇게 수정해 React의 상태 관리와 렌더링 메커니즘을 활용해 할인 가격 표시 문제를 해결했다.

7. 마무리하며.

이번 과제를 통해 단순히 코드를 리팩토링하는 작업이 아니라 코드의 구조와 책임을 다시 설계하는 경험을 했다. 특히 JS 환경에서 DOM을 직접 조작할 때와 React + TS 환경에서 상태와 타입을 기반으로 구조를 잡을 때의 차이를 체감할 수 있었다.

기능은 같아도 구조와 설계 방식에 따라 유지보수성과 확장성이 얼마나 달라질 수 있는지 체험한 과제였다.

앞으로도 단순히 동작하는 코드를 작성하는 데서 멈추지 않고 읽기 쉽고 테스트 가능하며 확장 가능한 코드를 작성하는 습관을 유지해야겠다고 다짐했다. 이번에 적용한 원칙과 구조를 실제 프로젝트에서도 계속 실험하고 다듬어 나가면서 더 나은 클린 코드를 만들어가고 싶다.

과제를 하면서 내가 제일 신경 쓴 부분은 무엇인가요?

1. basic – DOM 조작과 기능 불변성 유지

  • 문제: CartItem 전체를 재렌더링할 경우 이벤트 리스너 유실 → 테스트 실패 (expected '2' to be '1')
  • 해결: render.js에서 <span> 요소의 innerHTML만 업데이트 → 기존 이벤트 리스너 유지 + 할인 가격 동적 반영
  • 기능 불변성 유지 + 기존 DOM 구조 손상 방지 작업을 통해 문제를 해결했습니다.

2. advanced – React 상태 관리와 데이터 흐름

  • 문제: 할인 적용 후 CartItem.tsx가 즉시 업데이트되지 않음
  • 해결: Cart.tsx에서 productList 전체를 전달, CartItem에서 item.id로 최신 product 검색
  • 추가: JSX 리터럴 문제 해결 -> <span> 태그로 감싸 React가 표현식 평가하도록 수정

과제를 다시 해보면 더 잘 할 수 있었겠다 아쉬운 점이 있다면 무엇인가요?

1. basic – DOM 조작 추상화 부족

  • 아쉬운 점:
    • render.jsCartItem의 HTML 구조에 강하게 의존
    • 구조 변경 시 유지보수 비용이 올라간다.
  • 개선 아이디어:
    • CartItem.jsupdatePrice() 메서드 추가 -> 단일 책임 원칙 강화 + 결합도 감소

2. basic – HelpModal 이벤트 리스너

  • 아쉬운 점:
    • 직접 리스너 추가 -> DOM 제거 시 메모리 누수 위험
  • 개선 아이디어:
    • init()/destroy() 패턴으로 라이프사이클 명시적 관리

3. advanced – CartItem 성능

  • 아쉬운 점:
    • productList 전체를 전달, 매번 find 수행 -> 대규모 데이터에서 비효율 가능
  • 개선 아이디어:
    • React.memo + Context API 도입 or memoizedProduct 전달

리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)

1. advancedproductList 전달 vs 성능

  • CartItem에서 매번 find 수행 시 대규모 productList에서의 성능 저하 가능성이 있을 것 같습니다.
  • React.memo 외에 Context API or memoizedProduct 전달이 더 나은 선택일까요?

과제 피드백

안녕하세요 유현님! 4주차 과제 너무 잘 진행해주셨네요 ㅎㅎ 고생하셨습니다!!

지금 기본과제에서 심화과제로 이어지는 부분이 어느정도 매끄럽게 느껴지는데 아쉬운 부분도 있어요.

기본과제의 calcCartTotals과 심화과제의 calcCartTotals을 보면 함수는 똑같은데 내용이 조금 상이합니다.

아예 기본과제에서 만든 코드를 심화과제에서 불러와서 사용하는 방식을 상상해보면 어떤식으로 코드를 분리하면 좋았을지 감이 잡혔을 것 같아요!


CartItem에서 매번 find 수행 시 대규모 productList에서의 성능 저하 가능성이 있을 것 같습니다. React.memo 외에 Context API or memoizedProduct 전달이 더 나은 선택일까요?

지금은 아예 ProductList를 넘기고 있는데요, 딱 필요한 데이터만 가져와서 넘겨주는 방식을 상상해보시면 좋을 것 같아요.


// 어딘가에서 carts와 products를 정규화해서 관리해야 함.
const toMap = (items, targetKey) => items.reduce((acc, item) => ({
	...acc,
	[item[targetKey]]: item
}), {});

const carts = cartList => toMap(cartList, 'id');

const products = productList => toMap(productList, 'id');

const Cart = ({ carts, products , onCartClick }: CartProps) => {
	const cartWithProduct = useMemo(
		() => Object.values(carts).map(cart => ({
			...cart,
			product: products[cart.id]
		})),
		[carts, products]
	)
  return (
    <div
      id="cart-items"
      role="group"
      aria-label="Cart Items"
      onClick={onCartClick}
    >
      {cartItems.length === 0 ? (
        <p className="text-center text-gray-500 py-10">
          장바구니가 비어있습니다.
        </p>
      ) : (
        cartWithProduct.map(({ quantity, product }) => {
          return (
            <CartItem key={item.id} {...product} quantity={quantity} />
          );
        })
      )}
    </div>
  );
};

요로코롬.. 그리고 CartItem을 메모이제이션 해주는거죠 ㅎㅎ 이러면 CartItem의 props 중 객체가 없기 때문에 높은 확률로 메모이제이션이 잘 적용될 수 있어요.

그리고 context를 만약 CartItem에서 불러온다면 결국 최적화가 제대로 안 될 확률이 높답니다. 차라리 zustand나 jotai 등을 이용하여 최적화를 하는게 더 좋은 방법이라고 생각해요!