🔍 현재 상황 분석
1단계에서 탄탄한 데이터 구조를 구축한 후, "거대한 컴포넌트를 어떻게 작은 단위로 나눌 것인가?" 그리고 "사용자가 원하는 인터랙티브한 UI를 어떻게 구현할 것인가?" 라는 두 가지 핵심 문제를 해결해야 합니다.
1. 동일한 코드가 반복되는 문제
// Tier4.js - 기존 코드
function Tier4(props) {
return (
<div>
<Grid container spacing={{ xs: 2, md: 3 }}>
{raidList.map((raidName, index) => (
<Grid key={index} size={{ xs: 2, sm: 4, md: 4 }}>
<div className="efficiency-raid-card">
<img src={...} className="efficiency-raid-image" />
<div className="efficiency-raid-title">{raidName}</div>
<div className="efficiency-raid-info">
{/* 복잡한 배열 처리 로직 */}
{arr[index]?.map((a, i) => (
<div key={i}>{a}</div>
))}
</div>
</div>
</Grid>
))}
</Grid>
</div>
);
}
// Tier3.js - 거의 동일한 코드 반복
- Tier4.js와 Tier3.js에서 90% 이상 동일한 코드
- 새로운 기능 추가 시 두 파일 모두 수정 필요
- 버그 수정도 두 번씩 해야
2. 복잡한 데이터 처리 로직
- UI 컴포넌트에 데이터 처리 로직이 섞임
- 특히 Tier3.js의 중첩 배열 처리가 매우 복잡
- 가독성과 유지보수성 저하
3. 관심사 분리 부족
- 레이아웃, 데이터 처리, UI 렌더링이 한 곳에 섞임
- 테스트하기 어려운 구조
- 재사용성 제로
🎯해결 방안: 컴포넌트 아키텍처 설계
설계 원칙 수립
- 단일 책임 원칙: 각 컴포넌트는 하나의 역할만
- 재사용성: 4티어, 3티어에서 동일한 컴포넌트 사용
- 확장성: 새로운 기능 추가 시 최소한의 수정
- 관심사 분리: 데이터 처리와 UI 로직 완전 분리
새로운 컴포넌트 구조
src/components/
├── RaidCard/
│ ├── RaidCard.js # 메인 카드 컴포넌트
│ └── RaidCard.css # 카드 전용 스타일
└── RaidGrid/
├── RaidGrid.js # 그리드 레이아웃 컴포넌트
└── RaidGrid.css # 그리드 전용 스타일
💡 RaidCard 컴포넌트 구현
핵심 아이디어: 하이브리드 데이터 처리
기존 코드와의 호환성을 유지하면서 새로운 구조도 지원
/**
* 메인 RaidCard 컴포넌트
*/
function RaidCard({
raidName,
raidData,
isNewStructure = false, // 구조 타입 구분
onDetailClick = null
}) {
// 데이터 처리 (구조에 따라 다른 처리)
const processedData = isNewStructure
? processNewData(raidData, selectedDifficulty)
: processLegacyData(raidData);
const { summary, details, availableDifficulties } = processedData;
// ... UI 렌더링
}
레거시 데이터 처리 함수
기존 문자열 배열 형태의 데이터를 처리하는 함수:
function processLegacyData(legacyArray) {
if (!Array.isArray(legacyArray)) {
return { summary: '데이터 없음', details: [] };
}
// 기본 정보 추출 (첫 번째 항목에서)
const firstItem = legacyArray[0] || '';
const goldMatch = firstItem.match(/더보기 골드: (\d+)G/);
const rewardMatch = firstItem.match(/더보기 보상 골드: ([\d.]+)/);
const totalGold = goldMatch ? parseInt(goldMatch[1]) : 0;
const totalReward = rewardMatch ? parseFloat(rewardMatch[1]) : 0;
const efficiency = Math.round(totalReward - totalGold);
return {
summary: {
totalGold,
totalReward,
efficiency: efficiency,
gateCount: legacyArray.length
},
details: legacyArray
};
}
새로운 구조 데이터 처리 함수
1단계에서 만든 구조화된 데이터를 처리:
function processNewData(structuredData, selectedDifficulty = null) {
if (!structuredData || !structuredData.difficulties) {
return { summary: '데이터 없음', details: [] };
}
// 선택된 난이도가 있으면 사용, 없으면 기본 난이도 사용
const targetDifficulty = selectedDifficulty || structuredData.defaultDifficulty;
const difficultyData = structuredData.difficulties[targetDifficulty];
// 요약 정보 계산
const totalGold = difficultyData.gates.reduce((sum, gate) => sum + gate.goldCost, 0);
const totalReward = difficultyData.gates.reduce((sum, gate) => sum + gate.totalMaterialPrice, 0);
return {
summary: {
totalGold,
totalReward,
efficiency: difficultyData.overallEfficiency,
gateCount: difficultyData.gates.length,
difficulty: targetDifficulty,
itemLevel: difficultyData.itemLevel
},
details: difficultyData.gates,
availableDifficulties: structuredData.availableDifficulties
};
}
UI 컴포넌트 구조화
카드 레이아웃 설계
return (
<div className="raid-card">
{/* 카드 헤더 */}
<div className="raid-card__header">
<img src={...} className="raid-card__image" />
<div className="raid-card__title">{raidName}</div>
{/* 난이도 배지 */}
</div>
{/* 카드 내용 */}
<div className="raid-card__content">
{/* 요약 정보 */}
<div className="raid-card__summary">
<div className="summary-item">
<span className="summary-label">총 골드</span>
<span className="summary-value">{totalGold}G</span>
</div>
{/* ... 다른 요약 정보들 */}
</div>
{/* 상세 정보 */}
<div className="raid-card__details">
{/* 관문별 상세 정보 */}
</div>
</div>
{/* 카드 액션 */}
<div className="raid-card__actions">
{/* 버튼들 */}
</div>
</div>
);
RaidGrid 컴포넌트 구현
그리드 레이아웃
return (
<div className="raid-grid">
{/* 그리드 헤더 : 업데이트된 시간 표시 */}
<div className="raid-grid__header">
<div className="stat-item">
<span className="stat-label">데이터 업데이트</span>
<span className="stat-value">
{new Date().toLocaleTimeString('ko-KR', {
hour: '2-digit',
minute: '2-digit'
})}
</span>
</div>
</div>
{/* 메인 그리드 */}
<Grid
container
spacing={{ xs: 2, md: 3 }}
columns={{ xs: 4, sm: 8, md: 12 }}
className="raid-grid__container"
>
{raidList.map((raidName, index) => (
<Grid
key={`${raidName}-${index}`}
size={{ xs: 4, sm: 4, md: 4 }}
className="raid-grid__item"
>
<RaidCard
raidName={raidName}
raidData={raidDataArray ? raidDataArray[index] : null}
isNewStructure={isNewStructure}
onDetailClick={onRaidDetailClick}
/>
</Grid>
))}
</Grid>
</div>
);
}
난이도별 탭 시스템 구현
사용자 요구사항 분석
"하드 난이도만 보고 싶어요"
"노말이랑 하드 효율성을 쉽게 비교하고 싶어요"
"클릭 한 번으로 난이도를 바꿀 수 있으면 좋겠어요"
난이도 변경 기능 구현
1. 상태 관리 추가
function RaidCard({ raidName, raidData, isNewStructure = false }) {
// 난이도 변경을 위한 상태 추가
const [selectedDifficulty, setSelectedDifficulty] = useState(null);
// 난이도 변경 핸들러
const handleDifficultyChange = (difficulty) => {
setSelectedDifficulty(difficulty);
console.log(`${raidName} 난이도 변경: ${summary.difficulty} → ${difficulty}`);
//변경된 데이터 미리보기
const newData = processNewData(raidData, difficulty);
console.log(`순이익 변화: ${summary.efficiency}G → ${newData.summary.efficiency}G`);
};
}
2. 클릭 가능한 난이도 탭
{/* 클릭 가능한 난이도 탭 표시 */}
{isNewStructure && availableDifficulties && availableDifficulties.length > 1 && (
<div className="raid-card__difficulties">
<span className="difficulties-label">난이도 선택:</span>
<div className="difficulties-list">
{availableDifficulties.map(diff => (
<button
key={diff}
className={`difficulty-tag difficulty-tag--clickable ${
diff === summary.difficulty ? 'difficulty-tag--active' : ''
}`}
onClick={() => handleDifficultyChange(diff)}
title={`${diff} 난이도로 변경`}
>
{diff.toUpperCase()}
</button>
))}
</div>
</div>
)}
3. 시각적 피드백 강화
.difficulty-tag--clickable {
cursor: pointer;
padding: 6px 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
transition: all 0.2s ease;
}
.difficulty-tag--clickable:hover {
background: var(--bg-tertiary);
border-color: var(--primary-gold);
color: var(--primary-gold);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(212, 175, 55, 0.2);
}
.difficulty-tag--active {
background: linear-gradient(135deg, var(--primary-gold), var(--primary-gold-light));
color: var(--bg-primary);
border-color: var(--primary-gold);
box-shadow: 0 2px 8px rgba(212, 175, 55, 0.3);
}
🔄 기존 컴포넌트 리팩토링
Tier4.js 완전 개선
Before (97줄의 복잡한 코드)
function Tier4(props) {
const raidList = ['카제로스(종막)', '아르모체(4막)', ...];
let arr = calcEfficiency(raidList, props.itemData);
if (!props.itemData) {
return <div>로딩 중...</div>;
}
return (
<div>
<Grid container spacing={{ xs: 2, md: 3 }}>
{raidList.map((raidName, index) => (
<Grid key={index} size={{ xs: 2, sm: 4, md: 4 }}>
<div className="efficiency-raid-card">
{/* 복잡한 UI 코드 30줄... */}
<div className="efficiency-raid-info">
{arr[index]?.map((a, i) => (
<div key={i}>{a}</div>
))}
</div>
</div>
</Grid>
))}
</Grid>
</div>
);
}
After (35줄의 깔끔한 코드)
function Tier4(props) {
const raidList = ['카제로스(종막)', '아르모체(4막)', ...];
// 새로운 데이터 구조 사용
const useNewStructure = process.env.NODE_ENV === 'development' && true;
let raidDataArray = useNewStructure
? hybridCalcEfficiency(raidList, props.itemData, true)
: calcEfficiency(raidList, props.itemData);
// 상세 정보 클릭 핸들러
const handleRaidDetailClick = (raidName, raidData) => {
console.log(`${raidName} 상세 정보:`, raidData);
};
return (
<div className="tier4-container">
{/* 페이지 헤더 */}
<div className="tier-header">
<h2 className="tier-title">4티어 레이드 효율성</h2>
<p className="tier-description">
4티어 레이드의 더보기 보상 효율성을 확인하세요
</p>
</div>
{/* 새로운 RaidGrid 컴포넌트 사용 */}
<RaidGrid
raidList={raidList}
raidDataArray={raidDataArray}
isNewStructure={useNewStructure}
onRaidDetailClick={handleRaidDetailClick}
loading={!props.itemData}
error={props.error || null}
/>
</div>
);
}
사용자 피드백 반영: 추가 개선사항
1. 효율성 계산 방식 개선
사용자 피드백: "백분율보다는 실제 골드를 보여주는게 더 알아보기 편할 것 같다."
Before (백분율 방식)
efficiency: (totalReward / totalGold * 100).toFixed(1) + "%"
// 결과: "150.0%"
After (실제 골드 차이)
efficiency: Math.round(totalReward - totalGold)
// 결과: "+500G" 또는 "-200G"
개선 효과:
- 사용자가 "아, 500골드 이익이구나!" 바로 이해
- 손실인 경우 "-200G"로 명확하게 표시
- 레이드 선택 시 더 직관적인 판단 가능
2. 상세 정보 기본 표시
사용자 피드백: "매번 '상세히 보기'를 클릭하는게 번거로워요"
Before (토글 방식)
1. 카드 확인
2. "상세히 보기" 버튼 클릭
3. 관문별 정보 확인
4. 다른 카드로 이동
5. 다시 "상세히 보기" 클릭 (반복)
After (기본 표시)
1. 카드 확인
2. 관문별 정보 바로 확인
3. 난이도 변경으로 즉시 비교
4. 다른 카드로 이동
5. 바로 정보 확인 가능
구현:
// 상세 정보 (항상 표시)
<div className="raid-card__details">
{isNewStructure ? (
details.map((gate, index) => (
<div key={index} className="gate-detail">
<div className="gate-detail__header">
<span className="gate-number">{gate.gate}관문</span>
<span className="gate-efficiency">
{gate.efficiency >= 0 ? '+' : ''}{gate.efficiency.toLocaleString()}G
</span>
</div>
<div className="gate-detail__content">
<span>골드: {gate.goldCost.toLocaleString()}G</span>
<span>보상: {gate.totalMaterialPrice.toLocaleString()}G</span>
</div>
</div>
))
) : (
// 기존 구조 처리
)}
</div>
🎓 핵심 학습 포인트
1. 컴포넌트 설계 원칙
처음부터 완벽할 필요는 없지만, 확장 가능한 구조를 고민해야 합니다.
단일 책임 원칙 적용
// ❌ 잘못된 예: 하나의 컴포넌트가 모든 일을 함
function Tier4() {
// 데이터 처리 + UI 렌더링 + 레이아웃 + 상태 관리
}
// ✅ 올바른 예: 각 컴포넌트가 하나의 역할만 담당
function Tier4() {
return <RaidGrid />; // 레이아웃만 담당
}
function RaidGrid() {
return <RaidCard />; // 그리드 배치만 담당
}
function RaidCard() {
// 카드 UI만 담당
}
2. 사용자 중심 설계
코드를 작성하기 전에 사용자가 어떻게 사용할지 충분히 고민
실제 사용 패턴 분석
- 사용자들은 여러 레이드를 빠르게 비교하고 싶어함
- 백분율보다 실제 골드 차이를 선호
- 클릭 수를 최소화하고 싶어함
피드백 반영 프로세스
- 문제 발견: "백분율이 이해하기 어려워요"
- 해결책 설계: 실제 골드 차이로 변경
- 구현: 계산 로직 수정
- 검증: 사용자 테스트 및 피드백 수집
'LostBoard 프로젝트' 카테고리의 다른 글
| [LostBoard 프로젝트] 더보기 효율 화면 수정 - 1. 복잡한 데이터 구조 수정 (0) | 2025.09.29 |
|---|---|
| [LostBoard 프로젝트] 현황판 화면 디자인 수정 (0) | 2025.09.21 |
| [LostBoard 프로젝트] 초기화면 디자인 수정 (0) | 2025.09.16 |
| [LostBoard 프로젝트] 초기 완성본 (0) | 2025.02.12 |
| [LostBoard 프로젝트] API를 활용하여 데이터 받아오기 (0) | 2025.01.08 |