적은 인원으로 빠르게 서비스를 오픈하면서 기술 부채가 심하게 쌓였던 상황.
컴포넌트 1개가 4천 줄이 넘고 1, 2천 줄이 넘는 컴포넌트가 여러 개 존재
위치 ↔ 시간 단위 변환 코드가 많음
컴포넌트 ↔ API 함수까지 7단계의 레이어. 특정 위치의 코드가 어떤 값을 들고 있는지 알기 어려움
REST API 문서가 최신화 되어 있지 않아 오히려 FE 코드가 최신 문서 역할
ESLint, TS 등 빌드 과정에서의 사전 에러 검출 과정 부재
그외 n개로 분산된 액시오스 인터셉터 설정, 권한 관리 체계 부재 등…
적은 인원으로 빠르게 서비스를 오픈하면서 기술 부채가 심하게 쌓였던 상황.
컴포넌트 1개가 4천 줄이 넘고 1, 2천 줄이 넘는 컴포넌트가 여러 개 존재
위치 ↔ 시간 단위 변환 코드가 많음
컴포넌트 ↔ API 함수까지 7단계의 레이어. 특정 위치의 코드가 어떤 값을 들고 있는지 알기 어려움
REST API 문서가 최신화 되어 있지 않아 오히려 FE 코드가 최신 문서 역할
ESLint, TS 등 빌드 과정에서의 사전 에러 검출 과정 부재
그외 n개로 분산된 액시오스 인터셉터 설정, 권한 관리 체계 부재 등…
구글 엔지니어는 이렇게 일한다. 톰 맨쉬렉 저
구글 엔지니어는 이렇게 일한다. 톰 맨쉬렉 저
allowJS
옵션을 이용하여 컴포넌트, 모듈, 함수 등을 그대로 임포트// 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 함수와 주요 비즈니스 로직이 담긴 함수에 먼저 적용
allowJs
옵션을 이용해 그냥 JS로 돌리자.TIP! 중요도가 떨어지고 손만 많이 가는 컴포넌트가 있다면 과감히 TS 적용 포기
<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>
// 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(); } } }
data
는 항상 사용하는 곳과 가장 가깝게 배치data
는 항상 사용하는 곳과 가장 가깝게 배치// 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() { // ... } } }
주의! 믹스인은 컴포넌트에 병합되어 들어가는 순서나 로직, 값 덮어쓰기 등 주의할 점이 많음
// 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 속성과 강하게 얽혀 있는 재사용 대상 로직을 모두 컴포지션으로 옮겨도 정상 동작함
// 오디오 변경 API 함수 function editAudio({ projectMediaId, time }) { return axios.put(`${projectMediaId}`, { time }); }
// 오디오 변경 API 함수 function editAudio({ projectMediaId, time }) { 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 }); }
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 }); }
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)); } } })
요 다음에 빠른 문제 파악 및 디버깅 & 수정 속도 향상 이라는 슬라이드 넣으면 좋을 듯