[8팀 정유열] Chapter 2-1. 클린코드와 리팩토링 - #11
Conversation
- productId - shopPolicy
- getProductDiscount - getBulkBonus
- getQuantityFromElement - formatPrice - createPriceDisplay
- getter/setter/getAll 연결 - products 데이터와 통합
- subTot, totalAmt -> total, originalTotal
- 불필요한 핸들러 분리 - 컴포넌트 이벤트는 각 컴포넌트로 이동 - 카트 핸들러 분리 - 프로덕트 옵션 컴포넌트화
- 간결한 로직이지만 가독성을 위해 분리한 유틸들
|
호호 유열님 이번주도 고생하셨습니다~ 글이 유열님처럼 차분하고 논리적이네요 ㅎㅎ 공감되는 부분도 많네요!! |
|
유열님 고생하셧슴니다~! |
unseoJang
left a comment
There was a problem hiding this comment.
안녕하세요 유열님~
2-1 과제 진행하시느라 수고하셨습니다.
전체적인 코드를 살펴보니 UI분리도 잘해주시고 상수화도 잘 해주셨네요
중간 중간 아쉬운 부분들은 if문이 2,3개 이상 나오는 부분들과 관심사 분리가 안되어있는 부분들이 있겟네요
다음번에 복습을 진행한다면 스터디고 과제이니 최대한 함수들을 잘게 쪼개어 보는 연습을 해보면 역할이 많은 함수가 이렇게 까지 쪼개질수 있구나를 깨달을수 있을 것같아요
그 부분이 잘되어있는게 지수님 코드니 지수님 코드 구경 한번 해보는 걸 추천드립니다.
이번 주 과제도 너무 고생 많으셨고 다음 주차 과제도 화이팅해봅시다.
수고 많으셨어요~
| }; | ||
|
|
||
| // Manual에 setupEventListeners 메서드 추가 | ||
| container.setupEventListeners = function ({ onClose } = {}) { |
There was a problem hiding this comment.
이 부분 돔과 로직을 분리해서 사용하면 재활용성이 커지지 않을까 싶어요
export function createManual() {
const container = document.createElement('div');
container.className = '...';
const column = createManualColumn();
container.appendChild(column);
const closeManual = () => {
container.classList.add('hidden');
container.querySelector('.transform')?.classList.add('translate-x-full');
};
function setupEventListeners({ onClose } = {}) {
container.addEventListener('click', (e) => {
if (e.target === container) {
closeManual();
onClose?.();
}
});
container.querySelector('#manual-close-button')?.addEventListener('click', () => {
closeManual();
onClose?.();
});
}
return {
el: container,
setupEventListeners,
};
}const manual = createManual();
document.body.appendChild(manual.el);
manual.setupEventListeners({ onClose: () => console.log('닫힘') });이런식으로요!
| // 개별 장바구니 항목 표시 | ||
| for (let i = 0; i < cartItems.length; i++) { | ||
| const curItem = getProduct(cartItems[i].id); | ||
| const qtyElem = cartItems[i].querySelector('.quantity-number'); |
There was a problem hiding this comment.
util함수들을 한곳에 잘 모아두셨네요
util성 성격의 함수들이니 전체적으로 좀 더 주석을 달았으면 가독성과 역할이 구분되어 더 좋았을 것같아요
| ) { | ||
|
|
||
| // 화요일 여부를 파라미터로 받거나 현재 날짜로 체크 | ||
| const isTuesdayActual = |
There was a problem hiding this comment.
const isTuesdayActual = isTuesday ?? new Date().getDay() === 2;해당코드 병합연산자로 이렇게 줄일수 있을 것같아요
| import { TIMER_DELAYS, DISCOUNT_RATES } from '../constants/shopPolicy.js'; | ||
| import { setProduct, getAllProducts } from '../managers/product.js'; | ||
|
|
||
| export function startSuggestSale( |
There was a problem hiding this comment.
이부분 로직 분리를 좀 더 해볼수 있을 것 같아요
function trySuggestSale(getSelectedProduct, onUpdate, onPriceUpdate) {
const selectedProductId = getSelectedProduct();
if (!selectedProductId) return;
const allProducts = getAllProducts();
const suggest = allProducts.find(
(p) =>
p.id !== selectedProductId &&
p.quantity > 0 &&
!p.isSuggestSale
);
if (!suggest) return;
alert(`💝 ${suggest.name}은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!`);
const newPrice = Math.round(
suggest.price * (1 - DISCOUNT_RATES.SUGGEST)
);
setProduct(suggest.id, {
price: newPrice,
isSuggestSale: true,
});
onUpdate();
onPriceUpdate();
}function startSuggestSaleTimer(getSelectedProduct, onUpdate, onPriceUpdate) {
setInterval(() => {
trySuggestSale(getSelectedProduct, onUpdate, onPriceUpdate);
}, TIMER_DELAYS.SUGGEST.INTERVAL);
}export function startSuggestSale(getSelectedProduct, onUpdate, onPriceUpdate) {
const randomDelay = Math.random() * TIMER_DELAYS.SUGGEST.DELAY_MAX;
setTimeout(() => {
startSuggestSaleTimer(getSelectedProduct, onUpdate, onPriceUpdate);
}, randomDelay);
}아무래도 setTimeout, setInterval 이 저렇게 연달아 나오면 의도 분리가 안되어 있다고 느껴질수도 있다고 생각이드네요.
그리고 안에 익명함수로 setTimeout, setInterval 이 진행이 되다 보니 가독성이 떨어지고 디버깅도 어렵고, 재사용도 어려워지고, 이부분도 의도 분리가 안되어있는 느낌도 들구요
| let suggest = null; | ||
|
|
||
| const allProducts = getAllProducts(); | ||
| for (let k = 0; k < allProducts.length; k++) { |
There was a problem hiding this comment.
이 부분 조건이 너무 많다보니 디버깅할떄 어려움이 많을 것 같아요
export function startSuggestSale() {
const delay = Math.random() * TIMER_DELAYS.SUGGEST.DELAY_MAX;
setTimeout(() => {
setInterval(handleSuggestSale, TIMER_DELAYS.SUGGEST.INTERVAL);
}, delay);
}
function handleSuggestSale() {
const selectedProductId = getSelectedProduct();
if (!selectedProductId) return;
const suggest = findSuggestedProduct(selectedProductId);
if (!suggest) return;
applySuggestDiscount(suggest);
alert(`💝 ${suggest.name}은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!`);
onUpdateSelectOptions();
handlePriceUpdate();
}
function findSuggestedProduct(selectedId) {
return getAllProducts().find(
(p) =>
p.id !== selectedId &&
p.quantity > 0 &&
!p.isSuggestSale
);
}
function applySuggestDiscount(product) {
const newPrice = Math.round(
product.price * (1 - DISCOUNT_RATES.SUGGEST)
);
setProduct(product.id, {
price: newPrice,
isSuggestSale: true,
});
}이런식으로 나눠주면 가독성도 좋고 디버깅할떄도 편리하지 않을까 싶어요
|
고생하셨습니다 유열님! |
생각없이 전역변수 이동을 나중으로 미룬게 저의 최대 실수였습니다...ㅋㅋ |
무엇보다 회사 업무치고 하느라 시간이 많이 부족했던 주였네요 더 꼼꼼하게 하고싶었는데 아쉬움이 많이 남지만 어쩔수없죠...ㅎㅎ |
과제 체크포인트
배포링크
https://yuyeol.github.io/front_6th_chapter2-1/
기본과제
심화과제
과제 셀프회고
이슈
[React 마이그레이션] 실시간 가격 업데이트 콜백 덮어쓰기 문제 해결
과제를 하면서 내가 제일 신경 쓴 부분은 무엇인가요?
작업순서를 정하고 그에 따라 작업하기
코드 첫인상
솔직히 이번 과제로 코드 리팩토링을 한다고 했을때 처음 들었던 생각은 "재밌겠다. 얼른 해보고싶다."였습니다. 평소에도 코드를 수시로 정리하고, 가독성을 고려한 코딩 습관을 가지고 있었기에 나름대로 자신도 있었구요.
하지만 약 800줄이 되는 이 코드뭉치를 보고 "이건 도대체 어떻게 동작하는 코드인거지?"라는 생각이 들었습니다.
감상을 간단하게 말해보자면 아래와 같습니다.
진정하고 천천히 작업 순서를 정해보았고 다음과 같은 순서로 작업을 진행하였습니다.
(작업을 진행한 이유도 함께 기록했습니다.)
(1) var → let/const 치환
🚨 코드 스멜
전역 변수 선언: 어디서든 접근 가능해서 의도치 않은 변경 위험
var의 재선언이 가능한 특징: 같은 변수를 여러 번 선언하는 패턴이 가능하여 최초 선언인지 아닌지 구분이 어려움
호이스팅: 전역 변수가 이미 window에 등록되어 있어서 선언 전 출력 시에도 에러가 아닌 undefined 반환
진행되어 NaN이나 예상치 못한 결과가 나오는 버그가 발생할 수 있음.
🔧 개선 후 효과
const는 재할당 불가,let은 재할당 가능을 명시적으로 표현(2) 매직넘버 상수화
🚨 코드 스멜
하드코딩된 매직넘버 산재: 할인율, 수량 기준, 타이머 값들이 의미를 알기 어려운 숫자로
흩어져 있음
일일이 수정하다가 다른 의미의 30까지 실수로 변경해버리는 상황
어떤 정책에 대한 값인지 식별이 어려움: 할인율이나 정책이 바뀔 때마다 코드를 뒤져서 숫자를 찾아야 함
🔧 개선 후 효과
있음
(3) 반복 패턴 함수화
🚨 코드 스멜
간단하지만, 동일한 로직이 빈번히 사용되는 경우: 제품을 찾는 for 루프, format 함수 등이 10개 이상의 위치에서 중복됨
중첩된 if-else 구조: 할인 계산 로직이 지나치게 중첩되어 가독성 저하
🔧 개선 후 효과
(4) 컴포넌트 분리
🚨 코드 스멜
거대한 HTML 문자열 덩어리: main 함수 안에서 모든 UI 구조가 하나의 거대한 innerHTML로 처리됨
UI 로직과 비즈니스 로직의 완전한 혼재: DOM 조작 코드 사이사이에 할인 계산이나 재고 관리 로직이 뒤섞여 있음
재사용 불가능한 UI 조각들: 같은 스타일의 버튼이나 입력창이 여러 곳에서 필요한데 매번 처음부터 작성해야 함
🔧 개선 후 효과
규칙적인 컴포넌트 사용방법 설계:
create*()함수로 통일: 모든 컴포넌트는 DOM 요소를 반환하는 함수로 구성{ itemCount }형태로 필요한 데이터만 받아서 처리재사용성 확보: Header, CartTotal 등을 다른 페이지에서도 바로 가져다 쓸 수 있음
테스트 용이성: 각 컴포넌트를 독립적으로 테스트 가능하며, 비즈니스 로직과 분리되어 UI 테스트 간편함
React 마이그레이션 준비: 이미 컴포넌트 기반으로 분리되어 있어서 React 컴포넌트로 변환 시 구조적 변경 최소화
(5) 거대한 함수 분해
🚨 코드 스멜
만능 함수(?): 하나의 함수가 6가지 다른 일을 처리하는 엄청난 상황
handleCalculateCartStuff()의 로직 중 어느 부분에서 문제가 생겼는지 찾지 못하는 하는 상황테스트 불가능한 구조: 할인 계산 로직만 테스트하고 싶은데 DOM 조작까지 함께 실행됨
불필요한 전체 재계산: 상품 하나만 추가해도 전체 장바구니를 처음부터 다시 계산하는 비효율
🔧 개선 후 효과
거대한 함수를 완전히 제거하고 각 이벤트별 전용 핸들러로 분산
doRenderBonusPoints()함수만 확인하면 끝. 문제 발생 지점을 단계별로 빠르게 특정 가능calculateDiscounts()함수에 값만 넣어보면 됨(6) 전역 변수 제거
🚨 코드 스멜
어디서 쓰는지 찾는데만 한세월인 전역변수들: 변수 선언은 맨 위, 초기화는 중간, 사용은 맨 아래에 있어서 흐름 파악 불가능
totalAmt전역 변수를 어느 함수에서 언제 0으로 초기화했는지 찾느라 800줄 코드를 다 뒤져야 하는 상황함수 간 데이터 공유 가능성: 함수가 어떤 전역 변수에 의존하는지 함수 시그니처만 봐서는 전혀 알 수 없음
doRenderBonusPoints()함수가 내부에서totalAmt전역 변수를 사용하는데, 이 함수를 호출하는 다른 개발자는 그걸 모르고totalAmt를 초기화하지 않아서 포인트가 엉뚱하게 계산되는 상황🔧 개선 후 효과
(7) 최종 main.js 정리
🚨 코드 스멜
🔧 개선 후 효과
과제를 다시 해보면 더 잘 할 수 있었겠다 아쉬운 점이 있다면 무엇인가요?
작업 순서에 대한 아쉬움
작업 순서를 잘못 정했다는 아쉬움이 있습니다. 컴포넌트 분리를 너무 일찍 했고, 전역 변수 제거를 뒤로 미룬다면 더 수월한 리팩토링을 진행할 수 있을 것 같습니다.
실제 진행한 순서:
다시 한다면 진행할 순서:
핵심 문제점:
완성도에 대한 아쉬움
주어진 과제 수행 시간동안 더 많은 것을 하지 못했던점이 가장 아쉽습니다. 현재 리팩토링 결과물은 여전히 함수 분리, 변수 네이밍, 구조적 개선이 부족한 상태입니다.
직장 업무와 병행하다 보니 과제 분량이 생각보다 많게 느껴졌고, AI 토큰이 떨어져가면서 혼자 해결해야 하는 부분이 늘어나면서 심리적 부담도 컸던것 같습니다. 시간에 쫓기다 보니 코딩에 대한 의지와 체력이 점점 떨어졌지만, 그럼에도 이 과제는 정말 몰입할 수 있었던 주제였습니다.
기회가 있다면 충분한 시간을 두고 더 꼼꼼하게 리팩토링을 진행해보고 싶습니다.
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)
위 '과제를 다시 해보면 더 잘 할 수 있었겠다 아쉬운 점이 있다면 무엇인가요?' 항목에서 순서에 대한 아쉬움이 있다고 언급했습니다.
실제 진행한 순서:
다시 한다면 진행할 순서:
저의 실제 작업 진행한 순서에 비해 개선한 순서는 얼마나 적절해 보이는지, 코치님이라면 어떤 순서로 작업을 진행하셨을지 궁금합니다.