다음 달이면 취업 한지 1년, 프론트엔드 개발자로서 1년이 채워진다. 입사 전, 회사에서 프로젝트에 투입되면 마친 후 회고 글을 주기적으로 작성하자고 나 자신과 약속을 했다. 하지만 1년이 다 되어서야 첫 회고 글을 작성하게 되었다.
프로젝트에 투입 되지 않았던게 아니다. 그렇다고 프로젝트를 완성했는데 회고 글을 작성하지 않은 것도 아니다.
올해 초에 새롭게 추가될 신규 서비스를 만드는데 참여했고 3~4개월이 지난 시점에 완성하게 되었다. 하지만 두 번의 지연으로 아직까지 배포와 운영이 이루어 지지 않고 있다. 지연되고 있는 이유에는 내가 납득할 수 있는 이유와 내가 납득할 수 없는 이유가 있었다. 이 시기에 나와 팀원들 모두 힘든 시간을 보냈다.
열심히 만든 프로젝트가 배포, 운영 되지 않고 중단된 것에 낙심했지만 뭐라도 하지 않으면 아무것도 되지 않을 것 같아 미뤄왔던 타입스크립트 공부와 기존에 운영되고 있는 서비스에 도입 하는 것을 제안했고 동료 프론트엔드 개발자 한분과 같이 해보기로 했다.
서론이 길었지만 1년차 회고 글을 미리 작성한 셈 치겠다.
사실 이 부분에 대해선 관련 키워드로 검색을 하면 도입해야 하는 이유가 도입해야 하지 말아야 할 이유보다 몇 배는 더 많이 나오기에, 도입에 대한 필요성 정도만 강조하고자 조사 후 간단하게 정리해 문서화 시켜 프로젝트를 진행해야 하는 이유를 뒷받침 해줄 수 있는 자료로 사용했다.
타입스크립트는 마이크로소프트에서 구현한 자바스크립트(Javascript)를 기반으로 정적 타입 문법을 추가한 프로그래밍 언어이다.
| Javascript | Typescript |
|---|---|
| 동적타입 언어 | 정적타입 언어 |
| 인터프리터 언어 | 컴파일 언어 |
| 독립적으로 사용가능 | 자바스크립트에 의존적(자바스크립트로 컴파일된 후 실행 |
| 타입에 제한을 받지 않아 유연함 | 더 나은 구조와 간결함, 일관성, 재사용성 |
현재 서비스에선 운영 단계에서 발생 할 수 있는 오류를 캐치하기 위해 Sentry를 사용 중이다. 실제로 개발이나 테스트 단계에서 인지하지 못한 오류들이 운영 단계에서 여럿 발생 되는데, 발생한 오류들 중 useRef의 current 속성이 null 혹은 undefined인 경우 오류가 발생했다고 알람이 오는 비율이 가장 높았다.
이 경우엔 사전에 optional chaining 연산자를 이용하는 등 값을 미리 체크를 해주면 발생하지 않는데, 타입스크립트를 사용하게 되면 값을 미리 체크하지 않으면 오류가 발생할 수 있다고 코드 작성 단계에서 알려주더라. 이 부분에서 타입스크립트의 장점과 필요성을 실감했다.
이번 프로젝트는 타입스크립트를 공부 목적이 아닌 실제 프로젝트에 적용하는 첫 번째 경험이었다. 그리고 보통 프로젝트를 진행할 때 팀장님이 작업을 나누어서 각자 할당을 해주셨는데, 이번엔 동료분과 둘이 처음부터 끝까지 진행해야 했기에 작업의 범위와 양을 할당하는 작업도 스스로 생각하고 실행해야 했다.
관련된 내용으로 검색 해 본 결과, 서버 통신하는 코드를 제일 먼저 작업하는 것을 추천하는 글을 발견했다. 서버에서 데이터를 받아오거나 보내는 작업은 백엔드에 스키마 혹은 api 명세서가 존재하기에 시작하는 것이 조금 수월할 것이라는 내용이었다.
그래서 우리가 결정하게 된 작업 순서는 크게 다음과 같다. 참고로 상태 관리 툴은 RTK를 사용 중이다.
상태 관리와 서버 통신을 위한 파일 -> 공용 함수와 상수 파일 -> layout -> component
프로젝트를 진행하기 전 가장 우선순위에 둔 것은 '되도록 로직은 수정하지 말자'였다. 이미 자바스크립트로 잘 만들어져 운영되고 있는 서비스에 타입스크립트를 도입 함으로 불필요하게 로직을 수정하고 이전에 정상적으로 작동하던 기능이 작동하지 않는다면 사용자 경험이 떨어질 것이라고 생각했다. 코드 퀄리티 향상, 사전 오류 체크, 더 나은 개발자 경험 등등 모두 좋은 취지이지만, 사용자 입장에서 잘 사용하고 있던 기능이 업데이트 이후에 작동하지 않는다면 불쾌할 것이고 업데이트 한 의미가 퇴색되는게 아닐까라는 생각이 들었다.
100% 수정하지 않은 것은 아니지만, 불필요한 로직 수정은 피하고, 기존 코드에서 타입 체크를 올바르게 해 오류를 사전 검증하는데 초점을 맞췄다.
우리 팀에서 관리하는 프로젝트들은 대부분 자바스크립트로 작성되어 있다. 이번에 마이그레이션을 하면서 처음으로 타입스크립트로 작성된 프로젝트가 생기는 것이고 아마 이후에 새로 생길 프로젝트나 기존에 자바스크립트 코드로 작성되어 있는 프로젝트 모두 타입스크립트로 전환하지 않을까 싶다.
그렇기에 컨벤션을 고민하고 정해놓고 잘 지켜야 코딩 스타일이 일관되게 적용 될 것이고 이후 프로젝트를 진행하는데 있어 생산성이 향상 될 것이다. ESLint, Prettier, Husky를 사용해서 강제화 하는 컨벤션 이외에 정해야 할 것들을 동료분과 같이 고민해보고 결정하고 문서화 시키고 공유했다.
이 과정에서 고민한 내용과 결과를 공유해 보고자 한다.
// image.d.ts
declare module '*.png';
declare module '*.jpeg';
declare module '*.jpg';
declare module '*.gif';
declare module '*.svg';
type STATUS = 'ACTIVE' | 'STOP' | 'DORMANCY' | 'WAIT';
type Card = {
cardId: number;
...
};
type STATUS = 'ACTIVE' | 'STOP' | 'DORMANCY' | 'WAIT';
interface UserState {
memberId: number;
status: STATUS;
...
}
type Roles = 'admin' | 'manager';
const rolesImages: Record<Roles, string> = {
admin: 'image1.png',
manager: 'image2.png',
};
// cardId는 서버 요청의 응답으로 오는 값이다
type AssigneesState = {
// value: Record<number, number[]>; ❌
value?: { [cardId: number]: number[] | undefined }; ⭕
};
export enum Paths {
ROOT = '/',
CARD_CREATE = '/card/create',
CARD_UPDATE = '/card/:cardId/update',
...
}
이 부분에서 고민했던 내용은 enum, const enum, const assertions의 장단점을 비교 해 결정 했다. 세가지 기능간의 차이점을 검색해 보았을때 enum은 트리 쉐이킹이 지원되지 않고 js로 변환 했을 때 용량이 크고 다른 두 선택지는 그렇지 않기 때문에 enum 사용을 지양해야 한다는 글이 많았다. 실제로 이 글에선 enum을 사용할 경우 파일에서 import한 뒤 사용하지 않아도 rollup 같은 번들러에선 빌드 파일에 포함되어 생성된다고 한다.
정말 enum은 트리 쉐이킹을 지원하지 않는지 직접 확인하고 싶었다.
현재 프로젝트에선 webpack 5.7 버전을 사용 중이였고, 기존 환경에서 상수 값을 enum으로 선언 후 파일에서 import한 뒤 사용하지 않아도 빌드 파일에 포함 되는지 테스트 해보았다.
import { Paths } from './constants';
function test() {
const element = document.createElement('div');
// elment.innerHTML = Paths.ADMIN_MANUAL;
element.innerHTML = 'hello';
return element;
}
document.body.appendChild(test());
// 빌드 결과
"use strict";
(self.webpackChunktask_front=self.webpackChunktask_front||[]).push([[179],{193:()=> {
var e;
document.body.appendChild(((e=document.createElement("div")).innerHTML="hello",e))}},e=>{e
(e.s=193)}]);
하지만 예상과는 다르게 사용하지 않은 enum 값들은 빌드 파일에 포함되지 않았다?????
상수 값을 import해 사용하는 경우엔 결과 값이 조금 달랐는데 enum과 const assertions를 사용해 상수 값을 선언하고 string을 꺼내 사용하면 빌드 파일에서 즉시실행함수(IIFE)가 생성되어 상수 값 객체 전체가 빌드 파일에 포함된 것을 볼 수 있었다.
그런데 const enum을 사용하게 되면 사용한 string만 빌드 파일에 포함되어 enum이나 const assertions를 사용했을 때 보다 번들 파일의 용량이 줄어드는 것을 확인했다.
결론적으로 enum을 사용하는 것으로 결정한 것은 상수 값의 파일 용량이 그렇게 방대하지 않았고 enum의 장점, 특히 값을 enum으로 선언함으로 이 값이 어떤 역할을 하는지 분명하게 할 수 있어 합의하에 결정하게 되었다.
interface AssigneeProps {
path: LayerType;
assignee: Member | Profile;
selectedIds: number[];
handleSelect: (e: React.ChangeEvent) => void;
}
const Assignee: React.FC<AssigneeProps> = ({ path, assignee, handleSelect, selectedIds }) => {}
const Checklist = forwardRef<HTMLInputElement, ChecklistProps>(({ checklist, handleModalResize
}, ref) => {
...
}
Checklist.displayName = 'Checklist';
const { cardId } = useParams<{ cardId?: string }>();
이외에 다른 컨벤션들도 정리해놓았지만, 모두 기록하기엔 글이 너무 길어질 것 같아 여기까지 기록하겠다.
타입스크립트를 도입함으로 기존에 크게 신경쓰지 않았던 사용되는 데이터들의 타입에 대해 깊게 고민해 보는 시간을 갖게 되었다. 자바스크립트에서는 유연하게 넘어갔을 타입간의 형변환이나 undefined 등 고민을 크게 하지 않았던 점들을 타입스크립트가 되짚어 주면서 다시 되돌아보게 되었고, 이러한 과정들로 타입스크립트가 사전 오류 검출 확률을 높여준다는 말이 정말 와닿았다. 기존의 프로젝트와 앞으로 진행하게 될 프로젝트는 타입스크립트에 대한 공부를 좀 더 하고 사용하는 언어를 타입스크립트로 전환하거나 시작해야겠다.
프로젝트 기간을 시작하기 전에 정해놓고 시작했는데 작업 마감일이 다가오자 조급함을 느껴 프로젝트 초기 단계만큼 깊게 고민하는 시간을 갖지 못하고 !나 as 등 그 순간 타입 오류를 제거하고 넘기기위해 코드를 작성했던 것은 아쉬웠다. 그리고 대부분의 컴포넌트들을 함수형 컴포넌트로 작성 했고 타입에 React.FC를 사용하는 것 같아 따라서 사용 했는데 나중에 조사해본 결과 여러 단점 때문에 사용을 권장하지 않는 곳이 많다는걸 알게 되었다. 그리고 데이터의 값이 아닌 타입만을 import해서 사용하는 경우 import type {...} from ***처럼 type import를 사용하게 되면 번들링 파일이 가벼워지는 등 여러 장점이 있다고 하는데 사전에 미리 고려 못했다.
추후에 꼭 리팩토링 작업을 진행하고, 다음 프로젝트에서 이와 같은 놓친 부분까지 고려해서 코드를 작성해야겠다.