티스토리 뷰

나는 영화 정보를 확인할 수 있는 간단한 프로젝트를 했었다.

이 프로젝트는 자바스크립트 기반의 React 프로젝트로 구성되었다. 강의나 실습으로 배운 타입스크립트 지식을 활용해 자바스크립트로 구성된 프로젝트를 타입스크립트로 전환하기로 했다.

 

설치

npm install --save typescript @types/react @types/react-dom

 

`tsconfig.json` 파일 생성을 진행한다.

npx tsc --init

 

 

기본 설정

// tsconfig.json
{
  "compilerOptions": {
    "target": "es2016", // 자바스크립트 코드 버전 변환 설정
    "lib": ["DOM", "DOM.Iterable", "ESNext"], // 프로젝트에서 사용할 라이브러리 설정
    "jsx": "react-jsx", // JSX 코드 변환 방식 설정
    "module": "CommonJS", // 모듈 설정
    "moduleResolution": "Node", // 모듈 해석 방식 설정
    "allowJs": true, // 자바스크립트 파일을 타입스크립트 프로젝트에 허용할 지 설정
    "noFallthroughCasesInSwitch": true, // Switch 문에 case가 누락되었는지 확인하는 옵션
    "resolveJsonModule": true, // JSON 모듈을 Import 할 수 있도록 설정
    "noEmit": true, // 파일을 출력하지 않도록 설정
    "isolatedModules": true, // 각 파일을 독립적으로 트랜스파일
    "allowSyntheticDefaultImports": true, // 기본값이 없는 모듈에서도 기본값을 가져올 수 있도록 설정
    "esModuleInterop": true, // ES 모듈과 CommonJS 모듈 간 상호 운용성 지원 설정
    "forceConsistentCasingInFileNames": true, // 파일명 대소문자 일관성 강제 설정
    "strict": true, // 모든 엄격 모드 옵션 활성화(타입 검사를 엄격하게)
    "skipLibCheck": true // 라이브러리 파일의 타입 검사 건너뛰기
  },
  "include": ["./src/**/*"], // 포함할 파일 및 디렉토리 지정
  "exclude": ["node_modules", "build", "dist"] // 제외할 파일 및 디렉토리 지정
}

 

`tsconfig.json`의 기본 설정은 구글 검색과 ChatGPT를 활용해서 작성했다.

 

자바스크립트 코드를 타입스크립트 코드로 바꾸기

설치와 설정을 마치고 나면, 이제 자바스크립트로 작성되어 있는 코드를 타입스크립트로 전환해야 한다.

기존 파일 확장자를 `.js`, `.jsx`에서 `.ts`, `.tsx`로 바꾸고 나면 에러가 발생할 것이다. 왜냐하면 아직 타입이 주어지지 않았으니까.

 

간단한 프로젝트였기 때문에 타입 설정이 간단할 줄 알았다. 그러나, 생각했던 것보다 시간이 많이 소요됐다. 연결되어 있는 파라미터의 인자도 같이 맞춰야 했고, 라이브러리에 맞게 타입을 설정해줘야 하는 등 생각보다 고려해야 할 것들이 많았다.

 

예를 들면, 영화 리스트를 가져오는 API Fetch 함수의 경우

const QueryMovie = async (apiUrl) => {
  try {
    const response = await axios.get(apiUrl);
    return response.data.results;
  } catch (error) {
    console.error(`에러 발생: ${error}`);
    if (error.response) {
      const status = error.response.status;
      if (status === 404 || status === 500) {
        throw {
          status,
          message: error.response.statusText,
          errorMessage: error.response.data?.message || 'No Error Message'
        };
      }
    }

    return [];
  }
}

자바스크립트로 잓어할 경우, 위 처럼 작성해 사용했었다. `try/catch`문으로 데이터를 가져오고, 에러가 발생할 경우 해당 에러의 상태에 따라 상태, 메시지, 에러 메시지를 전달하도록 했다.

 

const QueryMovie = async (
  apiUrl: string
): Promise<MovieCarouselItemProps[]> => {
  try {
    const response = await axios.get(apiUrl);
    return response.data.results;
  } catch (error) {
    console.error(`에러 발생: ${error}`);
    if (axios.isAxiosError(error)) {
      const status = error.response?.status;
      if (status === 404 || status === 500) {
        throw {
          status,
          message: error.response?.statusText || "No Error Status Message",
          errorMessage: error.response?.data?.message || "No Error Message",
        };
      }
    }

    return [];
  }
};

이를, 타입스크립트로 전환하고 나서 코드를 위 처럼 수정하였다. 무엇이 달라졌냐 하면, 파라미터의 타입과 `try/catch`문에서 데이터를 가져올 때, 이 데이터들을 어떤 타입으로 반환할 것이냐가 추가되었다.

 

반환할 데이터가 하나가 아닌 리스트로 여러 개이기 때문에 `MovieCarouselItemProps` 타입을 배열로 설정하였다.

그리고 위의 데이터를 가져오는 함수를 `useQuery`를 활용해 사용했는데, `useQuery` 함수를 타입스크립트로 코드를 바꾸는 과정에서 많은 시간이 소요됐다.

 

export const useMovieQuery = (key, queryFn, enabled = true) => {
    return useQuery({
        queryKey: key,
        queryFn: queryFn,
        enabled: enabled,
        staleTime: 10 * 60 * 1000,
        cacheTime: 30 * 60 * 1000,
    })
}


export const useCarouselQuery = (url) => {
    return useMovieQuery(
        ['carouselResults', url],
        () => QueryMovie(url),
        !!url
    );
};

기존에는 `useQuery`를 반환하는 함수와 영화 리스트 데이터를 가져오도록 하는 `useCarouselQuery` 함수를 생성했다. 자바스크립트를 사용하는 경우에는 특별하게 어렵거나 문제가 되는 부분이 없었다.

 

type UseMovieQueryOptions<T> = {
  key: unknown[];
  queryFn: QueryFunction<T>;
  enabled?: boolean;
};

export const useMovieQuery = <T>({
  key,
  queryFn,
  enabled = true,
}: UseMovieQueryOptions<T>) => {
  return useQuery<T>({
    queryKey: key,
    queryFn: queryFn,
    enabled: enabled,
  });
};

타입스크립트로 전환할 때, 생각보다 오류가 여러 번 발생했다. 예를 들어, `staleTime`과 `cacheTime`을 찾을 수 없다는 에러가 있었다. 나중에 조금씩 수정하면서 `staleTime`을 사용할 수 있었지만, 여전히 `cacheTime`은 주어진 타입에서는 찾을 수 없어 사용할 수 없다는 에러가 떴다. 그래서 ChatGPT의 도움을 받아 코드를 조금 수정했다.

 

type UseMovieQueryOptions<T> = {
  key: unknown[];
  queryFn: QueryFunction<T>;
  enabled?: boolean;
  staleTime?: number;
  cacheTime?: number;
};

export const useMovieQuery = <T>({
  key,
  queryFn,
  enabled = true,
  staleTime = 1000 * 60 * 10,
  cacheTime = 1000 * 60 * 30,
}: UseMovieQueryOptions<T>) => {
  const options = {
    queryKey: key,
    queryFn: queryFn,
    enabled: enabled,
    staleTime: staleTime,
    cacheTime: cacheTime,
  };

  return useQuery<T>(options);
};

`staleTime`과 `cacheTime`에 타입을 주고, 바로 `useQuery`에 조건을 넣어 반환했던 것을 조건을 변수로 선언한 뒤, 이를 담아서 반환하니 제대로 동작하였다.

 

그렇다면, `staleTime`과 `cacheTime`의 타입을 선언해주지 않았기 때문에 이전 코드에서는 에러가 발생했던 걸까?

이러한 생각으로 타입은 그대로 두고, 이전처럼 `useQuery`에 조건을 바로 넣어 그대로 반환하도록 했는데 이 때는 또 에러가 발생했다. `cacheTime`을 찾을 수 없다는 에러가 발생해 다시 조건을 변수로 두고 반환하였다. (이유를 찾고 공부할 것이다)

 

그리고, 위 처럼 작성하기는 했지만 `key`의 타입이 `unkown[]`으로 설정하는 게 맞을까? 이 부분에 대해서도 공부가 필요하다고 느꼈다.

 

그리고 하나 더!

타입스크립트로 코드를 전환한 뒤에 `useCarouselQuery`를 사용하니 로딩 중일 경우에 출력하는 부분에서 수정이 필요했다.

const { status, data, error, isFetching } = useCarouselQuery(RECOMMENDATION_URL);

if (status === 'loading' || isFetching) {
	return <MainContentSkeleton />;
}

if (error) {
	return <ErrorHandling error={error} viewName="main" />
}

기존에는 `status === 'loading'`으로 처리했었는데, 타입을 설정하고 나니 `loading`을 찾을 수 없다는 에러가 발생했다. 그리고 `loading` 대신 `pending`이 있으니 사용하라는 메시지를 볼 수 있었고, `loading`을 `pending`으로 수정하여 처리했다.

 

그리고 이 프로젝트에서 스타일을 설정할 때 `Styled-Components`를 사용했는데, 공통적으로 사용되는 스타일에는 `Props`를 전달해 크기를 다르게 설정하도록 했다. 나는 스타일의 `Props`에도 타입을 지정해줘야 하는지 몰랐지만, 이번에 타입스크립트로 전환하면서 `Props`에도 타입을 설정해주어야 하는 것을 알게 됐다.

 

interface QueryError {
  status?: number;
  message?: string;
  errorMessage?: string;
}

type ErrorProps = {
  error: QueryError | null;
  viewName: string | null;
};

export const ErrorHandling = ({ error, viewName }: ErrorProps) => {
  if (!error) return null;

  console.log(error.errorMessage);

  switch (error.status) {
    case 404:
      return <Error404 status={viewName} />;
    case 500:
      return <Error500 status={viewName} />;
    default:
      return viewName === "main" ? (
        <MainContentSkeleton />
      ) : viewName === "carousel" ? (
        <CarouselSkeleton />
      ) : (
        <Loading />
      );
  }
};

// ERROR404
export default function Error404({ status }: ErrorProps) {
  return (
    <ErrorContainer stat={status}>
      <svg
        xmlns="http://www.w3.org/2000/svg"
        fill="none"
        viewBox="0 0 24 24"
        strokeWidth={1.5}
        stroke="currentColor"
        className="size-6"
      >
        <path
          strokeLinecap="round"
          strokeLinejoin="round"
          d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
        />
      </svg>
      <ErrorTitle>404 Error</ErrorTitle>
      <ErrorMessage>페이지를 찾을 수 없습니다.</ErrorMessage>
    </ErrorContainer>
  );
}

 

에러핸들링 역할을 하는 컴포넌트에서 각 뷰포트의 이름에 따라 크기를 다르게 주려고 했다. `mainContent`는 크게, `Carousel`은 작게 말이다. 그래서, `Styled-Components`의 각 에러 스타일에 뷰포트 이름을 `Prop`으로 전달받아 크기를 다르게 주고자 했다.

 

interface LoadingProps {
  stat?: "main" | "carousel" | "error" | string | null;
}

export const ErrorContainer = styled.div<LoadingProps>`
  width: 100%;
  height: ${(props) =>
    props.stat === "main"
      ? "clamp(300px, 20vw, 450px)"
      : props.stat === "carousel"
      ? "clamp(20vh, 10vw, 40vh)"
      : "clamp(350px, 10vw, 500px)"};
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  border: 1px solid rgba(0, 0, 0, 0.05);

  svg {
    width: clamp(50px, 10vw, 150px);
    height: clamp(50px, 10vw, 150px);
    color: #ff0f0f;
  }
`;

위 처럼, `Props`로 전달받는 값 또는 타입을 설정하였다. 그리고 그 값에 따라 높이값을 다르게 주려고 했다.

삼항연산자를 위 처럼 연속으로 사용하는 것은 가독성 문제 등으로 좋지 않다고 배웠는데, 아직 더 나은 방법을 찾지 못했다.

 

어이없는 실수

자바스크립트를 타입스크립트로 전환하면서 어이없는 일로 시간이 조금 많이 소요된 적이 있었다.

위의 `ErrorHandling` 컴포넌트를 타입스크립트로 전환하는 과정에서 타입을 설정을 해주었는데도 에러가 발생했다.

 

'Error404' refers to a value, but is being used as a type here. Did you mean 'typeof Error404'?

 

도대체 이 에러가 무엇일까? `typeof`냐고 왜 물어볼까?

ChatGPT에 물어봐도 제대로 된 답을 주지 못해서 에러 메시지를 여러 번 읽고 해석하고 타입을 바꿔보고 해볼 수 있는 건 다 해봤다. 그래도 위의 에러가 사라지지 않았다.

 

그래서 마지막으로 구글 검색을 해봤더니 정말 단순한 문제였다. 바로 확장자 문제!

`ErrorHandling` 컴포넌트를 `.ts`으로 변경한 것이 문제였다. `.tsx`로 변경해야 하는 것을 `.ts`로 변경해서 위와 같은 에러가 발생한 것이었다. 그래서 `ErrorHandling` 컴포넌트를 제대로 읽지 못한 것이었다.

 

`Switch` 문으로 반환 처리하면서 반환하는 것이 값을 내보내는 것으로 착각하여 이러한 문제가 만들게 됐다.

이번 경험으로 같은 실수가 반복되지 않도록 더 집중해야 겠다고 느꼈다.