
징그럽고 예쁘다.
한 게 없는데 내일이 월요일이어서 초조한 건에 관하여
의미없는 화장과 렌즈로 피부 트러블과 안구 피로와 우울감 누적 이슈
스트레스는?
🖥️ 수정 전 BadgeCards.tsx
'use client';
import React, { useEffect, useState } from 'react';
import Image from 'next/image';
import { totalBadges } from '../atoms/TotalBadges';
import useUserStore from '@/stores/user.store';
const BadgeCards: React.FC = () => {
const { diaryCount, membershipDays } = useUserStore((state) => state);
const [badgesState, setBadgesState] = useState(
totalBadges.map((badgeGroup) => ({
...badgeGroup[1],
content: badgeGroup[1].content.replace('true', 'false')
}))
);
useEffect(() => {
const updateBadges = () => {
setBadgesState((prevBadges) =>
prevBadges.map((badge) => {
let isObtained = false;
if (badge.id.includes('다이어리수집가') && diaryCount !== null && diaryCount >= 3) {
isObtained = true;
} else if (badge.id.includes('문구점사장님') && diaryCount !== null && diaryCount >= 15) {
isObtained = true;
} else if (badge.id.includes('안녕하세요') && membershipDays !== null && membershipDays >= 1) {
isObtained = true;
} else if (badge.id.includes('빨리친해지길바라') && membershipDays !== null && membershipDays >= 7) {
isObtained = true;
} else if (badge.id.includes('찐친') && membershipDays !== null && membershipDays >= 30) {
isObtained = true;
}
return {
...badge,
isObtained,
content: isObtained ? badge.content.replace('false', 'true') : badge.content.replace('true', 'false')
};
})
);
};
updateBadges();
}, [diaryCount, membershipDays]);
return (
<div className="grid grid-cols-4 sm:grid-cols-2">
{badgesState.map((badge, index) => (
<div
key={index}
className="relative w-[20.8rem] h-[32.4rem] m-[0.8rem] sm:m-[0.4rem] sm:w-[10rem] sm:h-[17.2rem] sm:mx-[0.8rem]">
<Image
src={badge.content}
alt={badge.isObtained ? 'Obtained Badge' : 'Unobtained Badge'}
fill // 이미지가 부모 요소를 가득 채우도록 함
style={{ objectFit: 'contain' }} // 왜곡 없이 이미지를 표시
className="rounded-[1.6rem]"
/>
</div>
))}
</div>
);
};
export default BadgeCards;
🖥️ 수정 후 BadgeCards.tsx
: 조건을 평가하는 로직을 외부로 분리하고,
이 로직을 쉽게 확장할 수 있도록 만들어야 한다.
badgeConditions 배열을 활용하여 개방폐쇄원칙 적용.
'use client';
import React, { useEffect, useMemo, useState } from 'react';
import Image from 'next/image';
import { totalBadges } from '../atoms/TotalBadges';
import useUserStore from '@/stores/user.store';
const badgeConditions = [
{
idPart: '다이어리수집가',
condition: (diaryCount: number | null) => diaryCount !== null && diaryCount >= 3,
},
{
idPart: '문구점사장님',
condition: (diaryCount: number | null) => diaryCount !== null && diaryCount >= 15,
},
{
idPart: '안녕하세요',
condition: (membershipDays: number | null) => membershipDays !== null && membershipDays >= 1,
},
{
idPart: '빨리친해지길바라',
condition: (membershipDays: number | null) => membershipDays !== null && membershipDays >= 7,
},
{
idPart: '찐친',
condition: (membershipDays: number | null) => membershipDays !== null && membershipDays >= 30,
},
];
const BadgeCards: React.FC = () => {
const { diaryCount, membershipDays } = useUserStore((state) => state);
const badgesState = useMemo(() => {
return totalBadges.map((badgeGroup) => {
const badge = { ...badgeGroup[1] };
const condition = badgeConditions.find((cond) => badge.id.includes(cond.idPart));
const isObtained = condition ? condition.condition(diaryCount, membershipDays) : false;
badge.isObtained = isObtained;
badge.content = isObtained ? badge.content.replace('false', 'true') : badge.content.replace('true', 'false');
return badge;
});
}, [diaryCount, membershipDays]);
return (
<div className="grid grid-cols-4 sm:grid-cols-2">
{badgesState.map((badge, index) => (
<div
key={index}
className="relative w-[20.8rem] h-[32.4rem] m-[0.8rem] sm:m-[0.4rem] sm:w-[10rem] sm:h-[17.2rem] sm:mx-[0.8rem]"><Image
src={badge.content}
alt={badge.isObtained ? 'Obtained Badge' : 'Unobtained Badge'}
fill
style={{ objectFit: 'contain' }}
className="rounded-[1.6rem]"/></div>
))}
</div>
);
};
export default BadgeCards;
🖥️ 수정 후 BadgeCards.tsx
const badgesState = useMemo(() => {
return totalBadges.map((badgeGroup) => {
const badge = { ...badgeGroup[1] };
const matchingCondition = badgeConditions.find((cond) => badge.id.includes(cond.idPart));
const isObtained = matchingCondition ? matchingCondition.condition(
badge.id.includes(matchingCondition.idPart)
? diaryCount
: membershipDays
) : false;
badge.isObtained = isObtained;
badge.content = isObtained ? badge.content.replace('false', 'true') : badge.content.replace('true', 'false');
return badge;
});
}, [diaryCount, membershipDays]);
🖥️ BadgeCardsAfter.tsx
{
idPart: '다이어리수집가',
condition: (diaryCount: number | null) => diaryCount !== null && diaryCount >= 3
}
A : 여기서 condition은 함수야?Q : 예.이 함수는 특정 조건을 평가하여 true 또는 false 값을 반환.각각의 배지가 획득되었는지를 평가하기 위한 로직을 담고 있다.(diaryCount를 인자로 받아 diaryCount가 null이 아니고 3 이상인지 평가 후,그 결과에 따라 true 또는 false를 반환. )
매개변수
: diaryCount라는 매개변수를 받고 이 매개변수는 number 또는 null 타입을 가질 수 있다.
반환값
: 조건(diaryCount !== null && diaryCount >= 3)이 참이면 true를, 그렇지 않으면 false를 반환.
역할
: 각 condition 함수는 배지 조건을 평가.
이 방식은 코드가 매우 유연하게 확장될 수 있ㄷ록 한다.
다른 배지 조건을 추가할 때 새로운 함수를 정의하여 쉽게 추가 가능.
➡️ condition으로 정의된 것은 조건을 평가하는 함수이며,badge.id와 함께 사용하여 각 배지가 특정 조건을 만족하는지를 판단.
그런데 코드를 읽던 저는 또 다른 의문이 생겼습니다.
🖥️ BadgeCardsAfter.tsx
const isObtained = matchingCondition
? matchingCondition.condition(badge.id.includes(matchingCondition.idPart) ? diaryCount : membershipDays)
: false;
Q : 이거 왜 matchingCondition의 id가 idPart를 include할 때 diaryCount를 대입해? 왜 반대의 경우에 membershipDay를 assign하고?includes하는 id의 내용이 membershipDays를 조건으로 갖는 애면 어쩔건데?(한 줄 요약 : 왜 id에서 쓰이는 조건값을 두 개 최대로 한정하고, 그걸 또 임의의 순서로 지정했어?)
A : 말씀대로, 현재 코드에서 matchingCondition.idPart가 포함된 badge.id가
diaryCount를 인자로 받고, 반대 경우 membershipDays를 받는 로직은
특정한 경우에만 동작합니다.
하지만 배지가 diaryCount를 사용하는지,
membershipDays를 사용하는지를 더 명확하게 구분해야 합니다.
➡️ 따라서, 각 condition 함수가필요한 인자를 명확히 전달하는 방식으로 코드를 개선할 수 있습니다.각 badgeCondition에 사용할 값을 idPart나 다른 정보로 구분하는 대신,조건 자체에 맞는 값을 직접 전달해야 합니다.
🖥️ 수정 후 (1) BadgeCardsAfter.tsx
const badgesState = useMemo(() => {
return totalBadges.map((badgeGroup) => {
const badge = { ...badgeGroup[1] };
const matchingCondition = badgeConditions.find((cond) => badge.id.includes(cond.idPart));
const isObtained = matchingCondition
? matchingCondition.condition(
matchingCondition.idPart.includes('다이어리수집가') || matchingCondition.idPart.includes('문구점사장님')
? diaryCount
: membershipDays
)
: false;
badge.isObtained = isObtained;
badge.content = isObtained ? badge.content.replace('false', 'true') : badge.content.replace('true', 'false');
return badge;
});
}, [diaryCount, membershipDays]);
동작 방식
매칭된 조건 찾기
: matchingCondition은 badgeConditions 배열에서
badge.id가 idPart를 포함하는 조건을 찾는다.
조건 평가
: isObtained는 matchingCondition이 존재하면,
condition 함수를 호출하여 결과를 평가.
matchingCondition.idPart가 '다이어리수집가'나 '문구점사장님'을
(즉, diaryCount를 조건으로 사용하는 배지의 id를) 포함하면 diaryCount를 전달하고, 그렇지 않으면 membershipDays를 전달.
➡️하지만 이 로직은 여전히 특정 문자열에 의존하고 있으므로,
더 나은 방법은 각 condition 함수가 어떤 값을 사용해야 하는지 명확히 알 수 있도록 만드는 것입니다.
🖥️ 수정 후 (2) BadgeCardsAfter.tsx
const badgeConditions = [
{ idPart: '다이어리수집가', conditionType: 'diaryCount', condition: (diaryCount: number | null) => diaryCount !== null && diaryCount >= 3 },
{ idPart: '문구점사장님', conditionType: 'diaryCount', condition: (diaryCount: number | null) => diaryCount !== null && diaryCount >= 15 },
{ idPart: '안녕하세요', conditionType: 'membershipDays', condition: (membershipDays: number | null) => membershipDays !== null && membershipDays >= 1 },
{ idPart: '빨리친해지길바라', conditionType: 'membershipDays', condition: (membershipDays: number | null) => membershipDays !== null && membershipDays >= 7 },
{ idPart: '찐친', conditionType: 'membershipDays', condition: (membershipDays: number | null) => membershipDays !== null && membershipDays >= 30 },
];
const badgesState = useMemo(() => {
return totalBadges.map((badgeGroup) => {
const badge = { ...badgeGroup[1] };
const matchingCondition = badgeConditions.find((cond) => badge.id.includes(cond.idPart));
const isObtained = matchingCondition
? matchingCondition.condition(
matchingCondition.conditionType === 'diaryCount' ? diaryCount : membershipDays
)
: false;
badge.isObtained = isObtained;
badge.content = isObtained ? badge.content.replace('false', 'true') : badge.content.replace('true', 'false');
return badge;
});
}, [diaryCount, membershipDays]);
conditionType 속성 추가
: 각 badgeCondition에 conditionType 속성을 추가하여
어떤 값을 사용할지를 명확히 한다.
조건 평가:
: conditionType이 'diaryCount'인 경우 diaryCount를 전달하고, 'membershipDays'인 경우 membershipDays를 전달합니다.
어제 더 나은 방법이라고 제시된 코드이긴 한데,
여전히 condition은 두 종류밖에 없다는 전제 하에 동작하고 있어
확장성이 0에 수렴한다.
실제로 향후 프로젝트의 발전 방향을 놓고 볼 때,
배지 추가와 그에 따른 확장성 증대는 필수불가결이라고 생각하므로
더 고민하기로 한다.
const badgesState = useMemo(() => {
return totalBadges.map((badgeGroup) => {
const badge = { ...badgeGroup[1] };
const matchingCondition = badgeConditions.find((cond) => badge.id.includes(cond.idPart));
const isObtained = matchingCondition
? matchingCondition.condition(
matchingCondition.idPart.includes('다이어리수집가') || matchingCondition.idPart.includes('문구점사장님')
? diaryCount
: membershipDays
)
: false;
badge.isObtained = isObtained;
badge.content = isObtained ? badge.content.replace('false', 'true') : badge.content.replace('true', 'false');
return badge;
});
}, [diaryCount, membershipDays]);
Q : 그럼 이 코드에서 condition 타입의 종류가 2개를 초과할 때는 어떻게 해? 마치 타입 종류가 2개만 있는 것처럼 삼항연산자를 사용하고 있잖아. 확장성이 떨어지지 않아?
A : 맞습니다. 이를 해결하기 위해서는 conditionType에 따라 처리할 값을 선택하는 더 유연한 방법이 필요합니다. 예를 들어, switch 문이나 객체 매핑을 사용하여 확장 가능한 구조로 변경할 수 있습니다.
const badgesState = useMemo(() => {
return totalBadges.map((badgeGroup) => {
const badge = { ...badgeGroup[1] };
const matchingCondition = badgeConditions.find((cond) => badge.id.includes(cond.idPart));
let valueToCheck;
switch (matchingCondition?.conditionType) {
case 'diaryCount':
valueToCheck = diaryCount;
break;
case 'membershipDays':
valueToCheck = membershipDays;
break;
// 추가적인 conditionType에 대해 다른 값을 사용할 수 있습니다.
default:
valueToCheck = null; // 만약 conditionType이 정의되지 않았을 경우
}
const isObtained = matchingCondition ? matchingCondition.condition(valueToCheck) : false;
badge.isObtained = isObtained;
badge.content = isObtained ? badge.content.replace('false', 'true') : badge.content.replace('true', 'false');
return badge;
});
}, [diaryCount, membershipDays]);
const valueMap = {
diaryCount: diaryCount,
membershipDays: membershipDays,
// 다른 conditionType에 대한 값을 추가할 수 있습니다.
};
const badgesState = useMemo(() => {
return totalBadges.map((badgeGroup) => {
const badge = { ...badgeGroup[1] };
const matchingCondition = badgeConditions.find((cond) => badge.id.includes(cond.idPart));
const valueToCheck = matchingCondition ? valueMap[matchingCondition.conditionType] : null;
const isObtained = matchingCondition ? matchingCondition.condition(valueToCheck) : false;
badge.isObtained = isObtained;
badge.content = isObtained ? badge.content.replace('false', 'true') : badge.content.replace('true', 'false');
return badge;
});
}, [diaryCount, membershipDays]);
객체를 사용하여 conditionType과 관련된 값을 매핑.
이렇게 하면 새로운 conditionType이 생겨도,
객체에 추가만 하면 되므로 확장성을 개선할 수 있다.
그런데 말입니다.
1. TanstackQuery로 userData 페칭을 싹 뜯어고치게 됐습니다.
2. TotalBadges.tsx가 고정 내용을 가진 컴포넌트인데, 이 내용이 유동적으로 들어오게 바꿔야 하나 의문이 듭니다.
(로직 자체가 따로 분리되어 있으니 필수는 아니라는 판단)
3. BadgeConditions라는 배열 속 값들을 일일이 하드코딩 하지 않고
DB와 연동시켜 값이 쏙쏙 들어가게, 커스텀훅으로 만들고 싶어졌는데
너무 버겁습니다!!!!!!!!