24.09.29

강연주·2024년 9월 29일

📚 TIL

목록 보기
47/186

징그럽고 예쁘다.

한 게 없는데 내일이 월요일이어서 초조한 건에 관하여

  • 한 게 전혀 없는 것은 아니다.
  • 적어도 뭐를 모르는지는 아는 상태로 발전했다.
  • 일상의 소일거리들을 미루지 말자. 기분이 더 나빠진다.
  • 체크리스트를 하나씩 지워나가면 된다.
  • 누워서 영상 편집을 조금이라도 하면 굉장히 뿌듯할 것이다.
  • 재활운동으로 내일 웨이트 컨디션을 만들자.

의미없는 화장과 렌즈로 피부 트러블과 안구 피로와 우울감 누적 이슈

  • 그 사람(들)이 그럴 가치가 있나?
  • 가치가 있다 한들 내 인생 최우선순위에 놓을 것인가?
  • 나 혼자 아무리 곱씹고 생각해도 정답은 나오지 않고
    뇌를 갉아먹는 기생충만 냠냠 마시쪙~! 시간은 사르륵 녹는다.
  • 실질적으로 나를 성장시키는 것보다 모든 면에서 더 좋은 결과를 가져올 방안이 있나? 없다.

스트레스는?

  • 살 뺀다는 사람이 몸이 아파서 운동을 제대로 못하고 있다.
  • 먹고 싶은 게 많고도 명확한데 참고 있쥬?
  • 할일의 회피 + 짜증 콜라보
  • 왜 그런고 하니 오르막길을 오르고 있어서 입니다.
  • 다이어트, 재활, 공부, 취준을 동시에 하는데 쉬울 리가 있겠느냐.
  • 기술면접 준비와 수영이라는 새로운 태스크가 추가되기도 했다.

🐣 트러블슈팅 2

BadgeCards.tsx (1)

🖥️ 수정 전 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;
  • badgeConditions 배열 : 각 배지 조건을 객체로 정의하여 쉽게 확장 가능. 이 배열에 새로운 조건을 추가하면 기존 코드 수정 없이 기능 확장.
  • useMemo : 배지 상태를 계산하는 로직을 useMemo로 감싸, diaryCount나 membershipDays가 변경될 때만 재계산되도록 함.
  • 조건 평가 : badgeConditions 배열에서 badge.id에 해당하는 조건을 찾고, 그 조건에 따라 isObtained 값을 설정. 새로운 조건이 추가되면 badgeConditions 배열에 새로운 객체를 추가하면 된다.

⭐ useState 대신 useMemo를 사용한 이유

🖥️ 수정 후 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]);
  • 불필요한 상태 관리 회피 : useState는 상태(state)를 관리하는 데 사용됨. 상태가 변경되면 컴포넌트가 다시 렌더링되며, 상태가 자주 변경될 경우 불필요한 렌더링이 발생할 수 있다. 현재 코드의 badgesState는 외부로부터 변경되지 않는 값으로, diaryCount와 membershipDays가 변경될 때만 새로 계산되어야 하고, 이럴 때 useMemo를 사용해 불필요한 상태 관리 및 렌더링 감소.
  • 의존성 기반 재계산 : useMemo는 메모이제이션된 값을 반환. 즉, 특정 의존성(diaryCount, membershipDays)이 변경될 때만 재계산을 수행하고, 그렇지 않으면 이전 계산된 값을 재사용한다. badgesState는 diaryCount와 membershipDays에 의존해 계산되므로, 이 값들이 변경될 때만 재계산이 필요하고, useMemo를 사용하면 이러한 의존성을 명확하게 관리할 수 있다!
  • 퍼포먼스 최적화: 불필요한 재계산을 방지하여 성능 최적화 가능.가령, 부모 컴포넌트가 리렌더링되어도diaryCount와 membershipDays가 변경되지 않았다면badgesState는 재계산되지 않는 것.반면, useState를 사용하면 상태가 유지되더라도컴포넌트가 다시 렌더링될 때마다 불필요한 연산 수행.

🖥️ 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 문이나 객체 매핑을 사용하여 확장 가능한 구조로 변경할 수 있습니다.

  • 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와 연동시켜 값이 쏙쏙 들어가게, 커스텀훅으로 만들고 싶어졌는데
너무 버겁습니다!!!!!!!!

profile
아무튼, 개발자

0개의 댓글