실습 - 2. JS를 TS로 변환하는 절차 및 주의사항

CHOYEAH·2023년 11월 5일
0

TypeScript

목록 보기
19/23
post-custom-banner

자바스크립트 코드에 타입스크립트를 적용할 때 주의해야 할 점


  • 타입 변환 외에 기능적인 변경은 절대 하지 않을 것
  • 테스트 커버리지가 낮을 땐(테스트 코드가 없을 경우) 함부로 타입스크립트를 적용하지 않을 것
  • 처음부터 타입을 엄격하게 적용하지 않을 것 (any부터 시작해 점진적으로 strict 레벨을 증가)

자바스크립트 프로젝트에 타입스크립트 적용하는 절차


  1. jsdoc으로 타입 적용하기

    • @ts-check를 사용하면 js 파일에 타입 적용이 가능하다.
    • 프로젝트가 커서 타입으로의 변환이 어려울 경우 일단 바로 타입으로 변경하기 보다 상황과 적절히 타협하여 jsdoc으로 기존 자바스크립트 코드에 타입을 적용시킬 수 있다.
    • 아래와 같이 소스코드에 @ts-check를 주석으로 적용하면 타입스크립트 랭귀지 서버가 동작하여 타입 기능이 활성화된다.
    // @ts-check
    • @ts-check를 사용하면 jsdoc을 사용하여 타입을 정의를 할 수 있다.
          /**
           * @typedef {object} CovidSummary
           * @property {Array<object>} County
           */
          /**
           * @returns {Promise<CovidSummary>}
           */
          function fetchCovidSummary() {
            const url = "https://api.covid19api.com/summary";
            return axios.get(url);
          }

  1. 타입스크립트 환경 설정 및 ts 파일로 변환
  2. any 타입 선언
  3. any 타입을 더 적절한 타입으로 변경

2. 타입스크립트 프로젝트 환경 구성

  • 프로젝트 생성(또는 기존 프로젝트 준비) 후 NPM 초기화 명령어로 package.json 파일을 생성.
  • 프로젝트 폴더에서 npm i typescript -D로 타입스크립트 라이브러리를 설치.
  • 타입스크립트 설정 파일 tsconfig.json을 생성하고 기본 값을 추가.
    {
      "compilerOptions": {
        "allowJs": true, 
        "target": "ES5",
        "outDir": "./dist",
        "moduleResolution": "Node",
        "lib": ["ES2015", "DOM", "DOM.Iterable"]
      },
      "include": ["./src/**/*"], // src 하위에 모든 파일
      "exclude": ["node_modules", "dist"]
    }
  • 서비스 코드가 포함된 자바스크립트 파일을 타입스크립트 파일로 변환.
  • 타입스크립트 컴파일 명령어 tsc로 타입스크립트 파일을 자바스크립트 파일로 컴파일.
    // package.json에 build 스크립트 추가
    
    {
      "name": "project",
      "version": "1.0.0",
      "description": "최종 프로젝트 폴더입니다",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "build": "tsc"
      },
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "typescript": "^4.8.4"
      }
    }

3. 엄격하지 않은 타입 환경(loose type)에서 프로젝트 돌려보기

  • 프로젝트에 테스트 코드가 있다면 테스트 코드가 통과하는지 먼저 확인.
  • 프로젝트의 js 파일을 모두 ts 파일로 변경.
    • 점진적인 변환을 희망할 경우 서비스에 밀접한 관련이 있는 중요한 파일부터 변경
  • 타입스크립트 컴파일 에러가 나는 것 위주로만 먼저 에러가 나지 않게 수정.
    • 여기서, 기능을 사소하게라도 변경하지 않도록 주의.
  • 테스트 코드가 성공하는지 확인.

4. 명시적인 any 선언하기

  • 프로젝트 테스트 코드가 통과하는지 확인.
  • 타입스크립트 설정 파일에 noImplicitAny: true를 추가.
  • 가능한 타입을 적용할 수 있는 모든 곳에 타입을 적용.
    • 라이브러리를 쓰는 경우 DefinitelyTyped에서 @types 관련 라이브러리를 찾아 설치.
    • 만약, 타입을 정하기 어려운 곳이 있으면 명시적으로라도 any를 선언.
    • // utils
      function $(selector: any) {
        return document.querySelector(selector);
      }
      function getUnixTimestamp(date: any) {
        return new Date(date).getTime();
      }
      
      // DOM
      const confirmedTotal = $(".confirmed-total");
      const deathsTotal = $(".deaths");
      const recoveredTotal = $(".recovered");
      const lastUpdatedTime = $(".last-updated-time");
      const rankList = $(".rank-list");
      const deathsList = $(".deaths-list");
      const recoveredList = $(".recovered-list");
      const deathSpinner = createSpinnerElement("deaths-spinner");
      const recoveredSpinner = createSpinnerElement("recovered-spinner");
      
      function createSpinnerElement(id: any) {
        const wrapperDiv = document.createElement("div");
        wrapperDiv.setAttribute("id", id);
        wrapperDiv.setAttribute(
          "class",
          "spinner-wrapper flex justify-center align-center"
        );
        const spinnerDiv = document.createElement("div");
        spinnerDiv.setAttribute("class", "ripple-spinner");
        spinnerDiv.appendChild(document.createElement("div"));
        spinnerDiv.appendChild(document.createElement("div"));
        wrapperDiv.appendChild(spinnerDiv);
        return wrapperDiv;
      }
      
      // state
      let isDeathLoading = false;
      let isRecoveredLoading = false;
      
      // api
      function fetchCovidSummary() {
        const url = "https://api.covid19api.com/summary";
        return axios.get(url);
      }
      
      function fetchCountryInfo(countryCode: any, status: any) {
        // params: confirmed, recovered, deaths
        const url = `https://api.covid19api.com/country/${countryCode}/status/${status}`;
        return axios.get(url);
      }
      
      // methods
      function startApp() {
        setupData();
        initEvents();
      }
      
      // events
      function initEvents() {
        rankList.addEventListener("click", handleListClick);
      }
      
      async function handleListClick(event: any) {
        let selectedId;
        if (
          event.target instanceof HTMLParagraphElement ||
          event.target instanceof HTMLSpanElement
        ) {
          selectedId = event.target.parentElement.id;
        }
        if (event.target instanceof HTMLLIElement) {
          selectedId = event.target.id;
        }
        if (isDeathLoading) {
          return;
        }
        clearDeathList();
        clearRecoveredList();
        startLoadingAnimation();
        isDeathLoading = true;
        const { data: deathResponse } = await fetchCountryInfo(selectedId, "deaths");
        const { data: recoveredResponse } = await fetchCountryInfo(
          selectedId,
          "recovered"
        );
        const { data: confirmedResponse } = await fetchCountryInfo(
          selectedId,
          "confirmed"
        );
        endLoadingAnimation();
        setDeathsList(deathResponse);
        setTotalDeathsByCountry(deathResponse);
        setRecoveredList(recoveredResponse);
        setTotalRecoveredByCountry(recoveredResponse);
        setChartData(confirmedResponse);
        isDeathLoading = false;
      }
      
      function setDeathsList(data: any) {
        const sorted = data.sort(
          (a: any, b:any) => getUnixTimestamp(b.Date) - getUnixTimestamp(a.Date)
        );
        sorted.forEach((value: any) => {
          const li = document.createElement("li");
          li.setAttribute("class", "list-item-b flex align-center");
          const span = document.createElement("span");
          span.textContent = value.Cases;
          span.setAttribute("class", "deaths");
          const p = document.createElement("p");
          p.textContent = new Date(value.Date).toLocaleDateString().slice(0, -1);
          li.appendChild(span);
          li.appendChild(p);
          deathsList.appendChild(li);
        });
      }
      
      function clearDeathList() {
        deathsList.innerHTML = null;
      }
      
      function setTotalDeathsByCountry(data: any) {
        deathsTotal.innerText = data[0].Cases;
      }
      
      function setRecoveredList(data: any) {
        const sorted = data.sort(
          (a: any, b: any) => getUnixTimestamp(b.Date) - getUnixTimestamp(a.Date)
        );
        sorted.forEach((value: any) => {
          const li = document.createElement("li");
          li.setAttribute("class", "list-item-b flex align-center");
          const span = document.createElement("span");
          span.textContent = value.Cases;
          span.setAttribute("class", "recovered");
          const p = document.createElement("p");
          p.textContent = new Date(value.Date).toLocaleDateString().slice(0, -1);
          li.appendChild(span);
          li.appendChild(p);
          recoveredList.appendChild(li);
        });
      }
      
      function clearRecoveredList() {
        recoveredList.innerHTML = null;
      }
      
      function setTotalRecoveredByCountry(data: any) {
        recoveredTotal.innerText = data[0].Cases;
      }
      
      function startLoadingAnimation() {
        deathsList.appendChild(deathSpinner);
        recoveredList.appendChild(recoveredSpinner);
      }
      
      function endLoadingAnimation() {
        deathsList.removeChild(deathSpinner);
        recoveredList.removeChild(recoveredSpinner);
      }
      
      async function setupData() {
        const { data } = await fetchCovidSummary();
        setTotalConfirmedNumber(data);
        setTotalDeathsByWorld(data);
        setTotalRecoveredByWorld(data);
        setCountryRanksByConfirmedCases(data);
        setLastUpdatedTimestamp(data);
      }
      
      function renderChart(data: any, labels: any) {
        var ctx = $("#lineChart").getContext("2d");
        Chart.defaults.color = "#f5eaea";
        Chart.defaults.font.family = "Exo 2";
        new Chart(ctx, {
          type: "line",
          data: {
            labels,
            datasets: [
              {
                label: "Confirmed for the last two weeks",
                backgroundColor: "#feb72b",
                borderColor: "#feb72b",
                data,
              },
            ],
          },
          options: {},
        });
      }
      
      function setChartData(data: any) {
        const chartData = data.slice(-14).map((value: any) => value.Cases);
        const chartLabel = data
          .slice(-14)
          .map((value: any) => new Date(value.Date).toLocaleDateString().slice(5, -1));
        renderChart(chartData, chartLabel);
      }
      
      function setTotalConfirmedNumber(data: any) {
        confirmedTotal.innerText = data.Countries.reduce(
          (total: any, current: any) => (total += current.TotalConfirmed),
          0
        );
      }
      
      function setTotalDeathsByWorld(data: any) {
        deathsTotal.innerText = data.Countries.reduce(
          (total: any, current: any) => (total += current.TotalDeaths),
          0
        );
      }
      
      function setTotalRecoveredByWorld(data: any) {
        recoveredTotal.innerText = data.Countries.reduce(
          (total: any, current: any) => (total += current.TotalRecovered),
          0
        );
      }
      
      function setCountryRanksByConfirmedCases(data: any) {
        const sorted = data.Countries.sort(
          (a: any, b: any) => b.TotalConfirmed - a.TotalConfirmed
        );
        sorted.forEach((value: any) => {
          const li = document.createElement("li");
          li.setAttribute("class", "list-item flex align-center");
          li.setAttribute("id", value.Slug);
          const span = document.createElement("span");
          span.textContent = value.TotalConfirmed;
          span.setAttribute("class", "cases");
          const p = document.createElement("p");
          p.setAttribute("class", "country");
          p.textContent = value.Country;
          li.appendChild(span);
          li.appendChild(p);
          rankList.appendChild(li);
        });
      }
      
      function setLastUpdatedTimestamp(data: any) {
        lastUpdatedTime.innerText = new Date(data.Date).toLocaleString();
      }
      
      startApp();
  • 테스트 코드가 통과하는지 확인.

5strict 모드 설정하기

  • 타입스크립트 설정 파일에 아래 설정을 추가.
{
  "strict": true,
  "strictNullChecks": true,
  "strictFunctionTypes": true,
  "strictBindCallApply": true,
  "strictPropertyInitialization": true,
  "noImplicitThis": true,
  "alwaysStrict": true,
}
  • any로 되어 있는 타입을 최대한 더 적절한 타입으로 변환.
  • as와 같은 타입단언 키워드를 최대한 사용하지 않도록 고민해서 변경.
  • // utils
    function $(selector: string) {
      return document.querySelector(selector);
    }
    function getUnixTimestamp(date: Date) {
      return new Date(date).getTime();
    }
    
    // DOM
    const confirmedTotal = $(".confirmed-total") as HTMLSpanElement;
    const deathsTotal = $(".deaths") as HTMLParagraphElement;
    const recoveredTotal = $(".recovered") as HTMLParagraphElement;
    const lastUpdatedTime = $(".last-updated-time") as HTMLParagraphElement;
    const rankList = $(".rank-list");
    const deathsList = $(".deaths-list") as HTMLOListElement;
    const recoveredList = $(".recovered-list") as HTMLOListElement;
    const deathSpinner = createSpinnerElement("deaths-spinner");
    const recoveredSpinner = createSpinnerElement("recovered-spinner");
    
    function createSpinnerElement(id: string) {
      const wrapperDiv = document.createElement("div");
      wrapperDiv.setAttribute("id", id);
      wrapperDiv.setAttribute(
        "class",
        "spinner-wrapper flex justify-center align-center"
      );
      const spinnerDiv = document.createElement("div");
      spinnerDiv.setAttribute("class", "ripple-spinner");
      spinnerDiv.appendChild(document.createElement("div"));
      spinnerDiv.appendChild(document.createElement("div"));
      wrapperDiv.appendChild(spinnerDiv);
      return wrapperDiv;
    }
    
    // state
    let isDeathLoading = false;
    let isRecoveredLoading = false;
    
    // api
    function fetchCovidSummary() {
      const url = "https://api.covid19api.com/summary";
      return axios.get(url);
    }
    
    enum CovidStatus {
      Confirmed = 'confirmed',
      Recovered = 'recovered',
      Deaths = 'deaths'
    }
    function fetchCountryInfo(countryCode: string, status: CovidStatus) {
      // params: confirmed, recovered, deaths
      const url = `https://api.covid19api.com/country/${countryCode}/status/${status}`;
      return axios.get(url);
    }
    
    // methods
    function startApp() {
      setupData();
      initEvents();
    }
    
    // events
    function initEvents() {
      rankList.addEventListener("click", handleListClick);
    }
    
    async function handleListClick(event: any) {
      let selectedId;
      if (
        event.target instanceof HTMLParagraphElement ||
        event.target instanceof HTMLSpanElement
      ) {
        selectedId = event.target.parentElement.id;
      }
      if (event.target instanceof HTMLLIElement) {
        selectedId = event.target.id;
      }
      if (isDeathLoading) {
        return;
      }
      clearDeathList();
      clearRecoveredList();
      startLoadingAnimation();
      isDeathLoading = true;
      const { data: deathResponse } = await fetchCountryInfo(
        selectedId,
        CovidStatus.Deaths
      );
      const { data: recoveredResponse } = await fetchCountryInfo(
        selectedId,
        CovidStatus.Recovered
      );
      const { data: confirmedResponse } = await fetchCountryInfo(
        selectedId,
        CovidStatus.Confirmed
      );
      endLoadingAnimation();
      setDeathsList(deathResponse);
      setTotalDeathsByCountry(deathResponse);
      setRecoveredList(recoveredResponse);
      setTotalRecoveredByCountry(recoveredResponse);
      setChartData(confirmedResponse);
      isDeathLoading = false;
    }
    
    function setDeathsList(data: any) {
      const sorted = data.sort(
        (a: any, b:any) => getUnixTimestamp(b.Date) - getUnixTimestamp(a.Date)
      );
      sorted.forEach((value: any) => {
        const li = document.createElement("li");
        li.setAttribute("class", "list-item-b flex align-center");
        const span = document.createElement("span");
        span.textContent = value.Cases;
        span.setAttribute("class", "deaths");
        const p = document.createElement("p");
        p.textContent = new Date(value.Date).toLocaleDateString().slice(0, -1);
        li.appendChild(span);
        li.appendChild(p);
        deathsList.appendChild(li);
      });
    }
    
    function clearDeathList() {
      deathsList.innerHTML = null;
    }
    
    function setTotalDeathsByCountry(data: any) {
      deathsTotal.innerText = data[0].Cases;
    }
    
    function setRecoveredList(data: any) {
      const sorted = data.sort(
        (a: any, b: any) => getUnixTimestamp(b.Date) - getUnixTimestamp(a.Date)
      );
      sorted.forEach((value: any) => {
        const li = document.createElement("li");
        li.setAttribute("class", "list-item-b flex align-center");
        const span = document.createElement("span");
        span.textContent = value.Cases;
        span.setAttribute("class", "recovered");
        const p = document.createElement("p");
        p.textContent = new Date(value.Date).toLocaleDateString().slice(0, -1);
        li.appendChild(span);
        li.appendChild(p);
        recoveredList.appendChild(li);
      });
    }
    
    function clearRecoveredList() {
      recoveredList.innerHTML = null;
    }
    
    function setTotalRecoveredByCountry(data: any) {
      recoveredTotal.innerText = data[0].Cases;
    }
    
    function startLoadingAnimation() {
      deathsList.appendChild(deathSpinner);
      recoveredList.appendChild(recoveredSpinner);
    }
    
    function endLoadingAnimation() {
      deathsList.removeChild(deathSpinner);
      recoveredList.removeChild(recoveredSpinner);
    }
    
    async function setupData() {
      const { data } = await fetchCovidSummary();
      setTotalConfirmedNumber(data);
      setTotalDeathsByWorld(data);
      setTotalRecoveredByWorld(data);
      setCountryRanksByConfirmedCases(data);
      setLastUpdatedTimestamp(data);
    }
    
    function renderChart(data: any, labels: any) {
      var ctx = $("#lineChart").getContext("2d");
      Chart.defaults.color = "#f5eaea";
      Chart.defaults.font.family = "Exo 2";
      new Chart(ctx, {
        type: "line",
        data: {
          labels,
          datasets: [
            {
              label: "Confirmed for the last two weeks",
              backgroundColor: "#feb72b",
              borderColor: "#feb72b",
              data,
            },
          ],
        },
        options: {},
      });
    }
    
    function setChartData(data: any) {
      const chartData = data.slice(-14).map((value: any) => value.Cases);
      const chartLabel = data
        .slice(-14)
        .map((value: any) => new Date(value.Date).toLocaleDateString().slice(5, -1));
      renderChart(chartData, chartLabel);
    }
    
    function setTotalConfirmedNumber(data: any) {
      confirmedTotal.innerText = data.Countries.reduce(
        (total: any, current: any) => (total += current.TotalConfirmed),
        0
      );
    }
    
    function setTotalDeathsByWorld(data: any) {
      deathsTotal.innerText = data.Countries.reduce(
        (total: any, current: any) => (total += current.TotalDeaths),
        0
      );
    }
    
    function setTotalRecoveredByWorld(data: any) {
      recoveredTotal.innerText = data.Countries.reduce(
        (total: any, current: any) => (total += current.TotalRecovered),
        0
      );
    }
    
    function setCountryRanksByConfirmedCases(data: any) {
      const sorted = data.Countries.sort(
        (a: any, b: any) => b.TotalConfirmed - a.TotalConfirmed
      );
      sorted.forEach((value: any) => {
        const li = document.createElement("li");
        li.setAttribute("class", "list-item flex align-center");
        li.setAttribute("id", value.Slug);
        const span = document.createElement("span");
        span.textContent = value.TotalConfirmed;
        span.setAttribute("class", "cases");
        const p = document.createElement("p");
        p.setAttribute("class", "country");
        p.textContent = value.Country;
        li.appendChild(span);
        li.appendChild(p);
        rankList.appendChild(li);
      });
    }
    
    function setLastUpdatedTimestamp(data: any) {
      lastUpdatedTime.innerText = new Date(data.Date).toLocaleString();
    }
    
    startApp();
profile
Move fast & break things
post-custom-banner

0개의 댓글