Type오류, 단언, 오버라이딩&오버로드에 대하여

정소현·2024년 10월 29일
0

팀프로젝트

목록 보기
30/50
post-thumbnail

🌟 TypeScript & useinfinitequery(tanstackquery) & 오버로드, 오버라이딩

TypescriptJavascript의 단점을 보완하기 위해 만들어진 정적 타입 언어이다.

☘️ TypeScript 사용의미

  • 개발 단계에서 발생할 수 있는 에러들을 사전에 방지한다.
  • 개발도구에 (컴파일러, IDE) 개발자가 의도한 변수나 함수 등의 목적을 명확하게 전달한다.
  • 전달한 정보를 기반으로 코드 자동완성을 지원하고 잘못된 변수/함수 사용의 에러알림을 받으며 피드백 받을 수 있다.

☘️ Tanstack-Query

  • React-query라는 이름으로 운영하던 패키지가 다른 프레임워크들을 지원하면서 Tanstack-Query로 이름이 변경되었다.
  • Tanstack-Query는 서버 상태 가져오기, 캐싱, 동기화 및 업데이트를 쉽게 도와주는 라이브러리이다.
  • onSuccess, invalidatequeries 등을 통해 데이터를 받은 후 바로 적용을 시킬 수 있다.

Typescript로 이번 청첩장 템플릿 만들기를 진행하면서 오류로 많은 시간을 보내게 되었다.
무한스크롤을 구현하며 TanstackqueryuseInfinitequery를 사용했는데 이때 겪었던 오류들을 중심으로 이야기하려고 한다.

1. 첫번째 오류

  • 후기 데이터를 불러오고 그 데이터를 스크롤의 일정 높이에 닿을 때마다 무한으로 스크롤을 불러오기 위해 useInfiniteQuery()를 사용하여 데이터를 불러오는 로직을 작성하였다.

☝️ 처음 작성한 로직

 const {
    data: reviewsData,
    isLoading,
    error,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery<Review[]>({
    queryKey: ['reviews'],
    queryFn: async ({ pageParam }) => await getReview({ pageParam: pageParam as number, row }),
    getNextPageParam: (lastPage, allPages) => {
      return lastPage.length === row ? allPages.length * row : undefined;
    },
  });

💥 발생한 오류
: initialPageParam 관련 오류가 발생하였고, initialPageParam을 필수로 설정해야 한다는 메시지가 출력되었다.

  • 먼저 tanstackquery는 2023년 10월경 v5로 업데이트가 되었다.
    나는 Tanstack-query docs를 보고 처음 useinfinityquery를 사용하게 되었는데 이 부분에서 버전이 v4가 체크가 되어있었고 v4로 작업을 한 결과 예상 하지 못한 여러가지 오류들이 계속 발생했다. 이 오류들을 해결하여 google에 검색했을 때에도 대부분 참고한 글들이 v4를 사용하고 있어 이번 오류를 해결하는데 상당히 많은 시간을 소요하였다.

(당연한 이야기겠지만 개발에서 개발언어와 프레임워크, 라이브러리들은 변화가 생긴다. 개발자들은 이 변화들을 빠르게 익히고 적용해야 하며 적응해야한다.
어제까지 자주 쓰이던 것들이 오늘부터 새롭게 변화해야할 수도 있다...!
이 점을 머리로는 익히고 있었지만 이번에 몸소 느끼게 된 것 같다.)

✨ 오류를 해결한 방법

  • 타입이 어떠한 오류를 발생시키고 있는지 메세지를 분석해본다.
    : 지금 같은 경우에는 속성이 추가되지 않았다는 문구를 발생시키고 있었고 initialPageParam에 관한 이 호출과 일치하는 오버로드가 없다는 문구가 나왔다. => 어떤 것이 추가되지 않았음을 의미

해결로직

  const {
    data: reviewsData,
    isLoading,
    error,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery<Review[]>({
    queryKey: ['reviews'],
    queryFn: async ({ pageParam }) => await getReview({ pageParam: pageParam as number, row }),
    initialPageParam: 0,
    getNextPageParam: (lastPage, allPages) => {
      return lastPage.length === row ? allPages.length * row : undefined;
    },
  });

TanstackQuery Docs 에서도 알 수 있듯이 v4에서는 initialPageParam: 0, 이 존재하지 않았지만 v5에서는 필수로 사용해야하는 로직으로 추가되었다.

2. 두번째 오류

💥 발생한 오류

  • supabase에 저장되어 있는 후기 정보들을 가져오는 fetch로직에서 props의 type과 반환받는 데이터의 타입을 지정해주었음에도 any[] | null 형식은 'Review[] 형식에 할당할 수 없다는 오류가 계속 발생하였다. 이 오류는 실제 supabase에서 반환받는 값과 내가 커스텀해준 Review[] type의 형태가 달라서 나타나는 오류이다.
    찾아보니, 백엔드(supabase)에서 받아오는 데이터들의 형태는 내가 예상한 형태와 다를 수 있었고, supabase authtype라이브러리를 설치하거나 커스텀타입 설정을 변경하는 방법이 있었다.

✨ 오류를 해결한 방법
: 이 오류를 해결하기 위해 return되는 데이터를 type 단언을 통하여 해결해주었다.

export const getReview = async ({ pageParam = 0, row }: ReviewProps): Promise<Review[]> => {
  const { data, error } = await browserClient
    .from('reviews')
    .select('*')
    .range(pageParam, pageParam + row - 1);

  if (error) {
    console.error(error);
  }

  return data as unknown as Review[];
};

☝️ 타입단언이란? (as ~~)

: 타입 단언을 통해 컴파일러에게 특정 타입 정보의 사용을 강제할 수 있다.
타입스크립트 컴파일러는 타입 표기, 타입 추론 등을 이용해 type을 판단한다.
타입 단언을 하게 되면 타입 체크를 할 수 없게 된다.
따라서 타입스크립트 컴파일러에게

이 값은 무조건 이런 타입이야! 그러니까 에러를 그만띄워!

하는 것이다. 타입에 대한 확신이 있을 때 사용하는 것이 타입 단언 이기 때문에
따라서. 평소 사용할 때는 지양하는 것이 좋다.

  • 고려해야할 점들
  1. 타입의 안정성
  2. 타입 추론
  3. 유지 관리 & 보수
  4. 복잡한 type casting (중첩된 구조)

🌼 하지만 지금처럼 type단언으로 문제를 해결해야 할 때가있다.

  • stringify JSON값을 parsing해서 사용하는 경우이다.

나는 supabase에 저장되어있는 후기를 가져왔고 browserClient와 supabase에서 반환되는 데이터가 기본적으로 any타입이거나 예상되는 타입을 보장하지 않기 때문이다.

type단언이 이렇게 보면 모든 타입오류들을 해결해주고 원할하게 작동을 시켜줄 것 같지만 여전히 type단언은 조심해서 써야한다.
type단언은 런타임 오류를 낼 수 있기 때문이다.


🔥 TS에 대해서 더 알아보기

비슷해보이지만 다른 [Unknown, Never, Any]

참고 : 인프런 타입스크립트는 왜 그럴까?

▶︎ Unknown 타입 (전체 집합)

🤫 언제 사용되나?

  • 현재 정확한 타입을 알기 어려울 때 사용
  • 타입 좁히기와 함께 값을 유연하게 사용 가능

타입스크립트에서 unknown 타입은 매우 중요한 역할을 한다. unknown 타입은 어떤 값이든 올 수 있지만, 그 값에 대해 명시적으로 타입을 확인하고 처리하기 전까지는 그 값을 사용할 수 없도록 제한하는 안전한 타입이다. 타입 안전성을 강화할 수 있으며, 특정 타입으로 변환할 때 타입 단언을 사용해야 한다는 점에서 unknown 타입은 any 타입보다 더 안전한 방식으로 사용된다.

unknown 타입은 JavaScriptany 타입과 비슷한 성질을 가지고 있지만, any는 타입 검사를 완전히 비활성화하는 반면, unknown은 타입 체크를 요구하여 더 안전하게 사용할 수 있다. 예를 들어, unknown 타입의 변수는 사용하기 전에 타입을 검사하거나 단언해야한다.

사용예시

function processData(data: unknown) {
  if (typeof data === 'string') {
    console.log(data.toUpperCase());  // string으로 타입이 단언되어 있음
  } else if (typeof data === 'number') {
    console.log(data.toFixed(2));  // number로 타입이 단언되어 있음
  } else {
    console.error('알 수 없는 타입입니다.');
  }
}

processData('Hello');  // 'HELLO'
processData(42);       // '42.00'
processData({});       // 알 수 없는 타입

● unknown 타입에서 다운 캐스팅 (as unknown as ~~ 이렇게 단언해준 이유)

다운 캐스팅(Downcasting)은 자식 타입을 부모 타입으로 변환하는 것이다. unknown 타입을 다른 특정 타입으로 변환하기 위해서는 타입 단언(as)을 사용한다. 이때 타입 안전성을 확보하려면, 가능한 unknown 타입을 신중하게 처리하고, 적절한 타입 체크 후에 단언하는 것이 중요하다.

예시

function processData(data: unknown) {
  if (typeof data === 'object' && data !== null && 'name' in data) {
    const person = data as { name: string };  // 다운 캐스팅 (타입 단언)
    console.log(person.name);  // 정상적으로 사용 가능
  } else {
    console.error('유효한 데이터가 아닙니다.');
  }
}

processData({ name: 'Alice' });  // 'Alice'
processData({});  // 유효한 데이터가 아닙니다.

-----
  

▶︎ Unknown과 반대되는 Never 타입 (공집합)

  • 모든 타입의 변수에 저장 가능
  • 도달할 수 없는 함수, 호출하지 않아야 하는 함수
  • 주로 무한 루프나 예외를 던지는 함수에서 사용
  • switch문의 완정성을 보장할 때 활용가능
  • TypeScript에서 never는 null을 포함하여 모든 값과 호환되지 않습니다.
// 호출시 오류가 발생하는 함수
function error(v:never){
  throw new Error();
  
error(); // 아무것도 안 넣어서 문제
error(1); 
error(null); 
error(undefined);
// 완전성이 보장되지 않은 switch 문 (blue type빠져있음)
function error(v:never){
  throw new Error();
}

type Color = "RED" | "GREEN" | "BLUE"

function getColorName(c:Color){
  switch(c){
    case "RED":
      	return "rgb(255,0,0)";
    case "GREEN":
      return "rgb(0,255,0)";
    default:{
      return error(c);
    }
  }

▶︎ 치트키 Any Type

  • 모든 타입을 통과시킴
  • 단, 예외적으로 Never 타입의 변수에는 저장 불가
  • 타입을 재정의할 수 있음
    : any 타입은 명시적으로 다른 타입으로 변환할 수 있기 때문에, 타입을 재정의할 때 유용하게 사용될 수 있습니다. 하지만 너무 자주 사용하면 타입 안전성을 잃게 되므로 신중히 사용해야 한다.

🌟 Typescript에서의 오버로드 & 오버라이딩

☼ TypeScript의 오버로딩

: TypeScript는 런타임 오버로딩을 지원하지 않는다. 즉, 함수의 구현이 실제로 하나만 존재해야 하며, 타입 시그니처만 여러 개 정의할 수 있다. TypeScript는 컴파일 타임에 각 호출이 어느 시그니처와 맞는지 검사하여 적합한 시그니처를 선택할 뿐, 함수를 여러 개로 구분해서 오버로딩을 구현하지는 않는다.

☘️ 오버라이딩? (Overriding)

오버라이딩은 부모 클래스에 정의된 메서드를 자식 클래스에서 새로 구현하는 것이다.
내 코드에서는 클래스 대신 React 컴포넌트의 특성을 사용하여 오버라이딩과 유사한 역할을 하는 구조를 구현하고 있다. 후기 작성 페이지에서 리뷰페이지 안의 ReviewCard.tsx 컴포넌트에서 reviews데이터를 받아와 내부에서 어떻게 표시할 지 직접 정의하고 있다.

  • 예시 1 : reviews 배열을 받아 각 리뷰 카드를 개별적으로 렌더링하는데, 리뷰의 내용이 100자가 넘어갈 경우 toggleContent 함수를 사용해 본문을 확장하거나 접을 수 있다. 부모클래스에서 가져온 props를 자식 클래스에서 표시 방식을 변경하는 것.toggleContent 함수로 제어되어 컴포넌트 내부에서 특정 요소의 표시 방식을 변경함으로써 오버라이딩과 비슷한 기능을 수행하게 된다.

☘️ 오버로딩? (Overloading)

오버로딩은 함수나 메서드가 같은 이름을 가지지만 다른 매개변수를 사용하는 것이다.

💥 오버라이딩과 오버로딩의 차이점

오버로딩과 달리, 오버라이딩(Overriding)은 상속 관계에서 주로 발생하며, 부모 클래스의 메서드를 자식 클래스가 재정의하는 것을 의미한다. 오버로딩은 같은 이름의 함수가 여러 버전으로 존재하는 것이고, 오버라이딩은 상속된 함수의 내용을 자식 클래스에서 새롭게 정의하는 것이라는 점에서 차이가 있다.


🌼 이번 경험을 통해

TypeScript의 타입 단언, 오버로딩 및 오버라이딩의 개념을 깊이 이해하게 되었고, 라이브러리의 버전 업데이트에 따른 차이점을 빠르게 습득하는 것이 얼마나 중요한지 깨달았다. 타입 단언은 때때로 유용하지만, 안정성을 해칠 수 있기 때문에 신중하게 사용해야 하며, 오버로딩과 오버라이딩은 코드의 유연성을 높이고 가독성을 개선하는 데 중요한 역할을 한다. 이 경험을 바탕으로 앞으로의 프로젝트에서 TypeScript의 강점을 더욱 효과적으로 활용할 것 같다.

0개의 댓글