장기효

네이버 프런트엔드 개발자
캡틴판교의 프런트엔드 개발 상담소 유튜브 채널 운영
대학생, 실무자를 위한 인프런 멘토링 프로그램 운영
CaptainPangyo
Cracking Vue.js | 타입스크립트 핸드북
1 / 40
1

장기효

네이버 프런트엔드 개발자
캡틴판교의 프런트엔드 개발 상담소 유튜브 채널 운영
대학생, 실무자를 위한 인프런 멘토링 프로그램 운영
CaptainPangyo
Cracking Vue.js | 타입스크립트 핸드북
2

목차

  • 도입을 결정하기까지
  • 전략 세우기
  • 컴포넌트 기반 FE 아키텍처 설계
  • 적용 결과
  • 회고
3

시작하며

  • 이 발표는 JS로 제작된 아래 사이트를 모두 TS로 변환한 과정과 경험을 공유하는 발표입니다.
  • Vue.js와 TypeScript의 기본 개념과 문법은 설명하지 않습니다.
  • React, Vue 등 컴포넌트 기반 프런트엔드 설계와 타입스크립트 도입에 관심 있으신 분들께 적합한 발표입니다.
4

도입을 결정하기까지

5

대상 서비스

  • 동영상 웹 편집기. TTS(Text to Speech) 저작 도구. 비디오, 오디오 등의 미디어 제어와 타임라인 편집
6

문제 상황 - 왜 타입스크립트를 도입했나?

적은 인원으로 빠르게 서비스를 오픈하면서 기술 부채가 심하게 쌓였던 상황.

컴포넌트 설계

컴포넌트 1개가 4천 줄이 넘고 1, 2천 줄이 넘는 컴포넌트가 여러 개 존재

서비스 특징

위치 ↔ 시간 단위 변환 코드가 많음

데이터 흐름

컴포넌트 ↔ API 함수까지 7단계의 레이어. 특정 위치의 코드가 어떤 값을 들고 있는지 알기 어려움

API 문서

REST API 문서가 최신화 되어 있지 않아 오히려 FE 코드가 최신 문서 역할

개발 환경

ESLint, TS 등 빌드 과정에서의 사전 에러 검출 과정 부재

기타

그외 n개로 분산된 액시오스 인터셉터 설정, 권한 관리 체계 부재 등…

7

문제 상황 - 왜 타입스크립트를 도입했나?

적은 인원으로 빠르게 서비스를 오픈하면서 기술 부채가 심하게 쌓였던 상황.

컴포넌트 설계

컴포넌트 1개가 4천 줄이 넘고 1, 2천 줄이 넘는 컴포넌트가 여러 개 존재

서비스 특징 적합

위치 ↔ 시간 단위 변환 코드가 많음

데이터 흐름

컴포넌트 ↔ API 함수까지 7단계의 레이어. 특정 위치의 코드가 어떤 값을 들고 있는지 알기 어려움

API 문서 적합

REST API 문서가 최신화 되어 있지 않아 오히려 FE 코드가 최신 문서 역할

개발 환경 적합

ESLint, TS 등 빌드 과정에서의 사전 에러 검출 과정 부재

기타

그외 n개로 분산된 액시오스 인터셉터 설정, 권한 관리 체계 부재 등…

8

하지만 현실은…

안정성
  • 잘 돌아가는 서비스는 건드리지 않는게 좋다
  • 오랜 기간을 거쳐 안정성을 확보한 서비스와 기능을 건드리는 건 위험하다
학습 비용
  • TS를 아는 사람은 나 혼자
  • 팀 전체의 학습 비용이 큰 상황
테스팅
  • 기능을 보장할 수 있는 테스트 코드는 없었다
9

코드 생산 비용

“Is High Quality Software Worth the Cost?” - 마틴 파울러
10

"코드베이스의 수명이 다할 때까지 직면하는 변화가
몰고 오는 모든 변경을 안전하게 처리할 수 있다면
그 코드 베이스는 지속 가능하다."

구글 엔지니어는 이렇게 일한다. 톰 맨쉬렉 저

11

"서비스의 변경 사항을 안전하게 처리할 수 있다면
그 코드는 지속 가능하다."

구글 엔지니어는 이렇게 일한다. 톰 맨쉬렉 저

12

결론

현재의 상황과 이후의 유지보수 비용을 고려하면

TS 기반 코드 재작성이 필요하다고 판단




13

전략 세우기

14

프로젝트 진행 절차

  1. TS 기반 별도의 신규 Vue.js 프로젝트 환경 구성
    • 기존 소스 코드의 ESLint 에러가 수천개 ➡ 기존 프로젝트에서 점진적 TS 적용 불가능
  2. 페이지, UI 영역별 코드 재작성
    • 기존 JS 코드의 버그 수정을 위해 신규로 작성된 TS 코드를 역으로 JS 코드에 병합 ➡ 병합시 타입만 제거
  3. 코드 재작성 중 추가되는 신규 비즈니스 기능은 JS 소스 그대로 TS 프로젝트에 포함
    • allowJS 옵션을 이용하여 컴포넌트, 모듈, 함수 등을 그대로 임포트
15

  • JSDoc 작성
  • 컴포넌트 부분 부분 갈아끼우기
  • 가성비 안나오면 그냥 allowJS



16

첫 번째 전략 - JSDoc 적용

  • JSDoc이란 일정한 형식으로 코드에 설명을 추가하는 주석
// app.js

/**
 * @typedef {Object} User
 * @property {string} name 사용자 이름. ex) capt
 * @property {number} age 사용자 나이. ex) 100
 *
 * @param {number} id 사용자 아이디
 * @return {Promise<User>} 사용자 정보
 **/
function fetchUserById(id) {
  const url = `https://infcon.day/users/${id}`;
  return fetch(url).then(res => res.json());
    // { name: 'capt', age: 100 }
}
// app.js

/**
 * @typedef {Object} User
 * @property {string} name 사용자 이름. ex) capt
 * @property {number} age 사용자 나이. ex) 100
 *
 * @param {number} id 사용자 아이디
 * @return {Promise<User>} 사용자 정보
 **/
function fetchUserById(id) {
  const url = `https://infcon.day/users/${id}`;
  return fetch(url).then(res => res.json());
    // { name: 'capt', age: 100 }
}

TIP! API 함수와 주요 비즈니스 로직이 담긴 함수에 먼저 적용

17

두 번째 전략 - 부분 부분 갈아끼우기

  • 컴포넌트 간 결합도가 낮은 단위 컴포넌트 위주로 먼저 포팅 작업
18

두 번째 전략 - 부분 부분 갈아끼우기

  • 컴포넌트 간 결합도가 낮은 단위 컴포넌트 위주로 먼저 포팅 작업
19

세 번째 전략 - allowJs로 포용하기

  • TS로 변환했을 때 타입 에러가 많다면? allowJs 옵션을 이용해 그냥 JS로 돌리자.

app.js

app.ts

TIP! 중요도가 떨어지고 손만 많이 가는 컴포넌트가 있다면 과감히 TS 적용 포기

20

컴포넌트 기반 FE 아키텍처 설계

21

설계 원칙 5가지

  • 컴포넌트 ↔ API 레이어의 데이터 흐름 단순화
  • Reactivity는 필요한 곳에만
  • 덩어리가 큰 모듈은 컴포넌트보다 클래스로
  • 불필요한 전역 상태는 줄이고 state는 화면과 가장 가깝게 배치
  • 제약이 없는 믹스인은 죄악이다
22

설계 원칙 #1 - 데이터 흐름 단순화

  • 데이터를 받아와 화면에 뿌리기까지의 레이어 단순화
23

설계 원칙 #2 - Reactivity는 필요한 곳에만

  • <template/>에 표시되어야 하는 속성인가?
<template>
  <input type="file" @change="uploadImage" ref="selectedFile">이미지 업로드</input>
  <p v-if="uploading">업로드 중입니다</p>
</template>

<script>
const allowedFileTypes = ['jpg','png','webp'];

export default {
  data() {
    return { uploading: false, allowedFileTypes: [...] }
  },
  methods: {
    uploadImage() {
      this.uploading = true;
      const file = this.$refs.selectedFile.files[0];
      allowedFileTypes.some(allowedType => file.type.includes(allowedType));
    }
  }
}
</script>
<template>
  <input type="file" @change="uploadImage" ref="selectedFile">이미지 업로드</input>
  <p v-if="uploading">업로드 중입니다</p>
</template>

<script>
const allowedFileTypes = ['jpg','png','webp'];

export default {
  data() {
    return { uploading: false, allowedFileTypes: [...] }
  },
  methods: {
    uploadImage() {
      this.uploading = true;
      const file = this.$refs.selectedFile.files[0];
      allowedFileTypes.some(allowedType => file.type.includes(allowedType));
    }
  }
}
</script>
24

설계 원칙 #3 - 덩어리가 큰 모듈은 컴포넌트보다 클래스로

  • 화면단 코드와 직접적인 연관이 있는 코드인가?
// VideoInput.vue
export default {
  methods: {
    validateCodec() {
      // ..
    },
    validateVideoResolution() {
      // ..
    },
    validateFileSize() {
      // ..
    }
  }
}
// VideoInput.vue
export default {
  methods: {
    validateCodec() {
      // ..
    },
    validateVideoResolution() {
      // ..
    },
    validateFileSize() {
      // ..
    }
  }
}
class VideoFileValidator {
  // ...
}

// VideoInput.vue
export default {
  methods: {
    validateVideo() {
      const validator = new VideoFileValidator(file);
      this.isVideoValid = validator.check();
    }
  }
}
class VideoFileValidator {
  // ...
}

// VideoInput.vue
export default {
  methods: {
    validateVideo() {
      const validator = new VideoFileValidator(file);
      this.isVideoValid = validator.check();
    }
  }
}
25

설계 원칙 #4 - state는 줄이고 data는 화면과 가깝게 배치

  • 스토어에 꼭 들어가야 하는 상태인가?
26

설계 원칙 #4 - state는 줄이고 data는 화면과 가깝게 배치

  • data는 항상 사용하는 곳과 가장 가깝게 배치
27

설계 원칙 #4 - state는 줄이고 data는 화면과 가깝게 배치

  • data는 항상 사용하는 곳과 가장 가깝게 배치
28

설계 원칙 #5 - 제약이 없는 믹스인은 죄악이다

  • 제한된 규칙으로 사용해야 효과가 증대되는 믹스인(mixins)

믹스인을 잘못 사용한 사례

// mixins/audioAdd.js
export default {
  mixins: [getAudioIndex],
  data() {
    return { audioAddCheck: {valid: false} }
  },
  methods: {
    isAudioBetween() {
      this.audioIndex = this.getAudioIndex();
    }
  }
}
// mixins/audioAdd.js
export default {
  mixins: [getAudioIndex],
  data() {
    return { audioAddCheck: {valid: false} }
  },
  methods: {
    isAudioBetween() {
      this.audioIndex = this.getAudioIndex();
    }
  }
}

믹스인 중첩은 지양해야함

// mixins/getAudioIndex.js
export default {
  data() {
    return { audioIndex: -1 }
  },
  methods: {
    getAudioIndex() {
      // ...
    }
  }
}
// mixins/getAudioIndex.js
export default {
  data() {
    return { audioIndex: -1 }
  },
  methods: {
    getAudioIndex() {
      // ...
    }
  }
}

주의! 믹스인은 컴포넌트에 병합되어 들어가는 순서나 로직, 값 덮어쓰기 등 주의할 점이 많음

29

설계 원칙 #5 - 제약이 없는 믹스인은 죄악이다

  • 믹스인 대신 컴포지션 API(리액트 훅)을 사용
// composables/useAudio.js
export function useAudio() {
  const valid = ref(false);

  const getAudioIndex = () => { ... }
  const isAudioBetween = () => { ... }

  return { valid, getAudioIndex, isAudioBetween };
}
// composables/useAudio.js
export function useAudio() {
  const valid = ref(false);

  const getAudioIndex = () => { ... }
  const isAudioBetween = () => { ... }

  return { valid, getAudioIndex, isAudioBetween };
}
// AudioList.vue
export default {
  setup() {
    const { 
      valid, getAudioIndex, isAudioBetween 
    } = useAudio();

    return { valid, getAudioIndex, isAudioBetween };
  },
  data() { ... }
  methods: { ... }
}
// AudioList.vue
export default {
  setup() {
    const { 
      valid, getAudioIndex, isAudioBetween 
    } = useAudio();

    return { valid, getAudioIndex, isAudioBetween };
  },
  data() { ... }
  methods: { ... }
}

TIP! data 속성과 강하게 얽혀 있는 재사용 대상 로직을 모두 컴포지션으로 옮겨도 정상 동작함

30

적용 결과

31

적용 결과 #1 - 코드 가독성 향상

  • 코드의 역할을 더 구체적으로 파악할 수 있음 ➡ 신규 입사자 온보딩과 디버깅에 도움

api/audio.js

// 오디오 변경 API 함수
function editAudio({ projectMediaId, time }) {
  return axios.put(`${projectMediaId}`, { time });
}
// 오디오 변경 API 함수
function editAudio({ projectMediaId, time }) {
  return axios.put(`${projectMediaId}`, { time });
}

api/audio.ts

interface Params {
  projectMediaId: number;
  time: ms;
}

interface ApiResponse {
  id: string;
  url: string;
}

// 오디오 변경 API 함수
function editAudio({
  projectMediaId,
  time,
}: Params): AxiosPromise<ApiResponse> {
  return axios.put(`${projectMediaId}`, { time });
}
interface Params {
  projectMediaId: number;
  time: ms;
}

interface ApiResponse {
  id: string;
  url: string;
}

// 오디오 변경 API 함수
function editAudio({
  projectMediaId,
  time,
}: Params): AxiosPromise<ApiResponse> {
  return axios.put(`${projectMediaId}`, { time });
}
32

적용 결과 #2 - 에러의 사전 검출

  • 로컬 개발 환경에서의 린트 + TS 컴파일로 사전 에러 검출 가능
import Vuex from 'vuex'; 
import { i18n } from '../plugins/i18n';
import { encodeBase64 } from '../mixins/encodeBase64';
import IndexPage from './components/IndexPage.vue';
import { sum } from './utils/sum';

const store = new Vuex.Store({
  mutations: {
    // ...
    doSomething() {
      router.push('/home').catch(() => logging(Error));
    }
  }
})
import Vuex from 'vuex'; 
import { i18n } from '../plugins/i18n';
import { encodeBase64 } from '../mixins/encodeBase64';
import IndexPage from './components/IndexPage.vue';
import { sum } from './utils/sum';

const store = new Vuex.Store({
  mutations: {
    // ...
    doSomething() {
      router.push('/home').catch(() => logging(Error));
    }
  }
})
import Vuex from 'vuex'; 
import { i18n } from '../plugins/i18n';
import { encodeBase64 } from '../mixins/encodeBase64';
import IndexPage from './components/IndexPage.vue';
import { sum } from './utils/sum';

const store = new Vuex.Store({
  mutations: {
    // ...
    doSomething() {
      router.push('/home').catch(() => logging(Error));
    }
  }
})
import Vuex from 'vuex'; 
import { i18n } from '../plugins/i18n';
import { encodeBase64 } from '../mixins/encodeBase64';
import IndexPage from './components/IndexPage.vue';
import { sum } from './utils/sum';

const store = new Vuex.Store({
  mutations: {
    // ...
    doSomething() {
      router.push('/home').catch(() => logging(Error));
    }
  }
})
33

적용 결과 #3 - 기능 향상

  • 재생선 동작 개선 및 사용자 제어 정확도 향상

요 다음에 빠른 문제 파악 및 디버깅 & 수정 속도 향상 이라는 슬라이드 넣으면 좋을 듯

34

회고

35

첫 번째 아쉬웠던 점

  • 서비스 전체 구조를 개편하는데 팀 전체가 참여하지 않고 홀로 진행 -> 새로운 구조 & 언어에 대한 팀 학습 비용 증가
https://www.meme-arsenal.com/en/create/meme/7393515
36

두 번째 아쉬웠던 점

  • 코드 베이스에 대한 이해가 높았다면 주요 사용자 액션 기준으로 e2e 테스트 코드라도 작성하고 시작했을 듯
https://semaphoreci.com/blog/testing-pyramid
37

제 경험이 TS 도입을 고민하고 계신 분들께

좋은 참고 자료가 되었으면 좋겠습니다




38

https://joshua1988.github.io/ts/etc/convert-js-to-ts.html
39

감사합니다.

https://joshua1988.github.io
https://bit.ly/3mgTeWZ
github.com/joshua1988
40