24.10.01

강연주·2024년 10월 1일

📚 TIL

목록 보기
48/186

🖥️ BagdeCards.tsx

사실상 다 갈아엎는 수준의 리팩토링 진행 중,
totalBadges를 순회하며 획득 여부에 따라
다른 BadgeCards를 렌더링하는 코드 일부에 타입 오류가 떴다.

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ diaryCount: number; membershipDays: number | null; }'.
No index signature with a parameter of type 'string' was found on type '{ diaryCount: number; membershipDays: number | null; }'.ts(7053)
⚠ Error (TS7053)

import React, { useMemo } from 'react';
import useUserStore from '@/stores/user.store';
import { totalBadges } from '../atoms/TotalBadges';
import Image from 'next/image';

const BadgeCardsExtendable3ByMapping = () => {
  const { diaryCount, membershipDays } = useUserStore((state) => state);

  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 valueMap = {
    diaryCount: diaryCount,
    membershipDays: membershipDays
  };

  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, totalBadges, BadgeConditions]);

  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 BadgeCardsExtendable3ByMapping;

💠 badgesState는 useMemo로 아래와 같이 작동한다.

  • totalBadges라는 배열을 순회하면서,
  • 아이디(idPart)와 일치하는 충족 조건 타입(conditionType)을 찾고 matchingCondition에 할당,
  • valueMap 객체에서 matchingCondition의 충족 조건 타입으로 요소에 접근
  • 요소의 값을 valueToCheck에 할당, 획득 여부를 결정한다.
const valueToCheck = matchingCondition ? valueMap[matchingCondition.conditionType] : null; 

그런데 위 코드에서 에러 발생.
valueMap의 요소에 접근할 때 타입 더 명확하게 하라는 거겠죠?

💡두 가지 해결 방식이 있는데 결론부터 말하자면,
이 경우에 as 타입 단언이 간단하면서도 확장성이 좋다!

1. 명시적으로 타입 지정

  // valueMap 타입을 명시적으로 지정
  const valueMap: { [key in 'diaryCount' | 'membershipDays']: number | null } = {
    diaryCount: diaryCount,
    membershipDays: membershipDays
  };

conditionType이 diaryCount나 membershipDays만 포함하는
문자열임을 명시적으로 지정한다.
새 타입이 추가될 때마다 직접적으로 새 타입을 작성해야 함!


2. as로 타입 단언

  // valueMap 객체 정의 (타입 단언을 사용할 예정)
  const valueMap = {
    diaryCount: diaryCount,
    membershipDays: membershipDays,
    // 새로운 conditionType이 생기면 여기에 추가하고
  };

  const badgesState = useMemo(() => {
    return totalBadges.map((badgeGroup) => {
      const badge = { ...badgeGroup[1] }; // badge 객체 복사
      const matchingCondition = BadgeConditions.find((cond) => badge.id.includes(cond.idPart));

      // ⭐ valueMap에서 conditionType에 해당하는 값을 가져옴, 타입 단언을 사용하여 오류 방지
      const valueToCheck = matchingCondition ? valueMap[matchingCondition.conditionType as keyof typeof valueMap] : null;
      
      // isObtained 결정
      const isObtained = matchingCondition ? matchingCondition.condition(valueToCheck) : false;

      // badge의 isObtained 및 content 업데이트
      badge.isObtained = isObtained;
      badge.content = isObtained ? badge.content.replace('false', 'true') : badge.content.replace('true', 'false');

      return badge; // 변경된 badge 반환
    });
  }, [diaryCount, membershipDays, totalBadges, BadgeConditions]);

타입 단언을 통해 matchingCondition.conditionType이
valueMap의 유효한 키(keyof typeof valueMap)임을 TypeScript에 알린다.

이 방법의 장점은,

  • 만약 valueMap에 새로운 타입이 추가되어도 이 코드를 수정할 필요가 없어, 더 나은 간결성과 가독성

  • valueMap에 어떤 키가 있는지를 코드 상에서 알 필요가 없다는 점.
    즉, conditionType이 어떤 타입의 키가 되어도
    valueMap에서 참조할 수만 있다면 문제가 되지 않는다.
    새로운 conditionType이 추가되더라도 valueMap 객체만 올바르게 확장해주면 타입 단언 부분을 수정할 필요가 없습니다.


✨ typeof

JS의 typeof (값의 타입을 알려줌) 연산자와 달리,
TypeScript의 typeof는 주로 타입 추론을 위해
변수나 객체의 타입을 참조할 때 사용!

  • 런타임 값의 타입을 추론해, 해당 타입을 다른 곳에서 재사용
  • 타입스크립트 타입 정의 시, 특정 변수의 타입을 가져오기
// 변수 선언
const user = {
  name: 'John Doe',
  age: 30,
};

// `typeof`를 사용하여 변수의 타입을 추론
type UserType = typeof user;

// `UserType` 타입은 다음과 같이 자동으로 추론됨
// type UserType = {
//   name: string;
//   age: number;
// }

const newUser: UserType = {
  name: 'Jane Doe',
  age: 25,
};

➡️ user 객체의 구조가 변경되더라도, UserType의 별도 수정 없이
기존 값을 기준으로 자동으로 타입 정의해 유지보수성 향상


🖥️ typeof 일반적 사용 예시

const myString = 'Hello World!';
type StringType = typeof myString; // StringType은 string 타입으로 추론됨

let anotherString: StringType = 'New String'; // `anotherString`의 타입은 string

🖥️ 함수의 typeof 사용 예시

function add(a: number, b: number) {
  return a + b;
}

// `typeof`를 사용하여 함수 타입을 가져올 수 있음
type AddFunctionType = typeof add;

// `AddFunctionType` 타입은 다음과 같음:
// type AddFunctionType = (a: number, b: number) => number

const sum: AddFunctionType = (x, y) => x + y; // 같은 함수 시그니처를 가진 함수를 사용 가능

근데 같은 함수 시그니처를 가진 함수 사용의 장점과
함수 시그니처가 뭔지 모르겠다!


✨ keyof

객체 타입의 키(프로퍼티 이름)들을 추출하여 유니언 타입으로 변환.
➡️ 객체 타입의 모든 키를 하나의 타입으로 만들 수 있으므로
타입 정의와 유효성 검사에 유용하다.

type User = {
  id: number;
  name: string;
  age: number;
};

// `keyof`를 사용하여 `User` 타입의 키를 추출
type UserKeys = keyof User; // "id" | "name" | "age"

// `UserKeys`는 유니언 타입: "id" | "name" | "age"가 됩니다.

let key1: UserKeys = 'id';    // 유효
let key2: UserKeys = 'name';  // 유효
let key3: UserKeys = 'age';   // 유효
let key4: UserKeys = 'address'; // 오류: 'address'는 'UserKeys'에 포함되지 않음

type UserKeys = keyof User;

  • User 타입의 모든 키를 추출하여 유니언 타입 "id" | "name" | "age"로 변환.
  • 결과적으로, UserKeys 타입은 id, name, age 중 하나의 값을 가지는 문자열 타입.
    이 방식은 객체의 키 이름을 변수로 사용하거나 유효성 검사를 해야 할 때 매우 유용하다.
    예를 들어, 함수에서 전달된 키가 해당 객체의 키 중 하나인지 확인할 때 사용 가능.

🖥️ keyof 일반적인 사용 예시

type Car = {
  brand: string;
  model: string;
  year: number;
};

// `keyof`를 사용하여 `Car` 타입의 키를 유니언 타입으로 변환
type CarKeys = keyof Car; // "brand" | "model" | "year"

let carKey1: CarKeys = 'brand'; // 유효
let carKey2: CarKeys = 'model'; // 유효
let carKey3: CarKeys = 'year';  // 유효
let carKey4: CarKeys = 'price'; // 오류: "price"는 'CarKeys' 타입에 존재하지 않음

🖥️ keyof와 typeof 결합 사용 예시

const person = {
  name: 'John',
  age: 30,
  address: '123 Main St',
};

// `typeof`로 `person`의 타입을 추출
type PersonType = typeof person;

// `keyof`를 사용하여 `PersonType`의 키를 추출
type PersonKeys = keyof PersonType; // "name" | "age" | "address"

let key: PersonKeys = 'name'; // 유효
key = 'age';                  // 유효
key = 'address';              // 유효
key = 'phone';                // 오류: 'phone'은 'PersonKeys'에 포함되지 않음

keyof와 typeof의 결합 사용 설명
type PersonType = typeof person;
typeof person을 사용하여 person 객체의 타입을 추출합니다.
PersonType은 { name: string; age: number; address: string } 타입이 됩니다.
type PersonKeys = keyof PersonType;
keyof PersonType을 사용하여 객체의 모든 키를 유니언 타입으로 변환합니다.
PersonKeys는 "name" | "age" | "address" 타입이 됩니다.
결론
typeof는 값의 타입을 참조하기 위해 사용됩니다.
값의 타입을 추출하여 다른 타입 정의나 변수에 사용하고자 할 때 유용합니다.
keyof는 타입의 모든 키를 유니언 타입으로 변환합니다.
주로 객체의 키를 검사하거나 특정 키들만 허용하는 형태의 유효성 검사를 위해 사용됩니다.

profile
아무튼, 개발자

0개의 댓글