일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 웹워커
- CRA
- MFA
- 이것저것
- 클린코드
- 합성 컴포넌트
- Web
- 에세이
- provider 패턴
- TypeScript
- 오블완
- MicroFrontEnd
- Webworker
- context.api
- react
- 프론트엔드
- sharedworker
- radixui
- virtaullist
- 리액트
- 티스토리챌린지
- 리팩토링
- server component
- vite
- 회고
- CustomHook
- frontend
- 아키텍처
- 자바스크립트
- JavaScript
- Today
- Total
Lighthouse of FE biginner
[클린코드] TypeScript `any` 본문
Overview
오늘은 TypeScript any에 대해서 다뤄볼 예정입니다.
4월 새로운 팀에서 근무를 시작하고 프로젝트를 받아서 코드 파악을 하는 도중 any 타입이 너무 남발되어 있어서 당황한 적이 있습니다.
최근에 회사에 좋지 않은 바람이 불어서 다른 팀 인원이 부족한 상황에 도움을 주러 팀이 이동됐습니다.
또 다시 프로젝트를 클론 받아서 코드를 파악하려는데 이번 프로젝트는 대부분의 타입이 any로 되어있더군요..
사실 이 글은 그 분노로 인해서 탄생된 글입니다.
TypeScript
타입스크립트란 무엇인가요?
자바스크립트에 정적인 타입을 올려놓은 자바스크립트 슈퍼셋 언어입니다. 자바스크립트(JavaScript)는 동적인(Dynamic한) 언어입니다. 컴파일 단계에서 타입이 정해지지 않고 런타임 시점에 타입이 정해집니다.
JavaScript에서는 변수를 선언할 때 타입 없이 선언합니다.
let a = 1;
a = 'string';
a = false;
자바스크립트로 작성된 위 코드를 살펴봅시다.
a 변수는 선언과 동시에 number 타입인 1로 선언이 됐습니다.
코드의 흐름에 따라서 해당 변수는 string 타입으로 재할당 되었고,
a 변수는 boolean 타입으로 다시 재할당 됐습니다.
또 다른 코드를 살펴봅시다.
let a;
a = await fetch('api').then((res) => res.json());
위 코드는 비동기 함수인 fetch를 통해 특정 api의 JSON 데이터를 받아와 파싱 후 변수에 할당하고 있습니다.
a 변수의 타입은 코드 작성 시점과 컴파일 시점에 알수가 없습니다. 런타임에 타입이 결정이 됩니다.
타입이 런타임에 정해진다는 것은 유연하다는 이야기입니다.
하지만 유연하다는 것은 모호하다는 의미가 될 수 있고 불확실하다는 의미가 될 수 있습니다.
그래서 우리는 코드 작성 시점과 컴파일 시점에 타입을 추론할 수 있는 TypeScript를 사용하고 있습니다.
TypeScript를 사용한다면 런타임 시점에 발생할 에러를 방지할 수 있습니다.
JavaScript에 익숙한 개발자라면 타입을 얹는 것에 러닝 커브가 발생할 수 있지만, TypeScript는 협업에 용이하고 런타임 시점에 캐치할 예외 상황을 개발 시점에 캐치할 수 있기 때문에 개발 경험에 아주 좋다고 할 수 있습니다.
물론 모든 런타임 에러를 방지할수는 없습니다.
예측할 수 없는 사용자의 인터렉션이나, 예측할 수 없는 비동기 값 등 변수는 존재합니다.
하지만 이런 예외 상황도 핸들링 할 수 있어야 하는 것이 프론트엔드 개발자의 업무 커버리지 입니다.
ANYYYYYYYYYYYY
위에서 간단하게 TypeScript를 살펴봤습니다. 그럼 any 타입에 대해서 간단하게 알아보겠습니다.
let a;
a = 1;
a = 'string';
a = () => {};
a = 1.3;
a = false;
a = {
age: 1;
};
a 라는 변수를 선언하고 변수에 할당할 수 있는 값을 모조리 재할당 해봅니다.
a 변수의 타입은 어떻게 될까요? 정답은 아래 주석에 있습니다.
let a;
a = 1; // number
a = 'string'; // string
a = () => {}; // function
a = 1.3; // number
a = false; // boolean
a = {
age: 1;
}; // object
TypeScript를 사용해 변수를 선언해보겠습니다.
let 변수에 타입을 바인딩 하지 않는다면 자동으로 any (어떤것이든 들어갈 수 있음) 타입이 바인딩 됩니다.
먼저 a 변수에 number을 바인딩 해봅시다.
string, object 타입을 할당할 수 없습니다.
a 변수는 선언 당시 number타입을 가지고 있기 때문에 타입스크립트 컴파일러가 에러를 뱉고 있습니다.
만약 위 상황에서 a 변수에 object, string 타입 모두 할당하고 싶다면 어떻게 할까요?
간단하게 유니온 연산자를 사용해 여러 타입을 넣을수 있도록 할 수 있습니다.
그럼 위에 작성한 코드로 타입을 매겨보겠습니다.
극단적인 타입 바인딩이라 프로젝트를 할 때 이런 상황이 있을지는 모르겠습니다.
이렇게 타입이 짬뽕으로 들어온다면 특정 타입으로 추론하는게 의미가 없어집니다. (의미가 있을수도 있지만 추론할 타입이 좁혀지지 않는다면 타입 가드 코드를 작성해야 하고 타입 추론의 의미가 없어지지 않을까 하는 개인적인 생각입니다.)
이런 상황, 혹은 어떤 타입이 들어올지 정확히 알 수 없는 경우 any를 사용할 수 있습니다.
기적같이 any라는 3글자의 타입을 사용하니 어떤 타입도 쉽게 바인딩 할 수 있게 됩니다.
이렇게 좋은 any타입 왜 안써??????
any 사용을 지양해야 하는 상황과 이유
API 응답 타입은 가급적 타입을 정확히 추론하기
먼저 TypeScript를 사용한다면 API 응답 값은 가급적 정확히 타입을 추론해야 합니다.
let a: any;
const asyncFync = async () => {
a = await fetch("api").then((res) => res.json());
};
a.age;
비동기 함수를 활용해 API에서 값을 받아오고 있습니다.
지금 새로운 기능을 개발하는 여러분은 급하거나 타입을 추론하기 귀찮다는 이유로 a를 any로 바인딩 했습니다.
그로부터 4개월이 지난 여러분이 개발한 기능에 추가 기능을 개발해야 하거나 버그가 발생해 기능을 코드를 수정하게 됩니다.
a라는 변수에 어떤 타입이 바인딩 되어있는지 파악이 가능하신가요?
더 극단적인 상황을 예를 들어봅시다.
전임 개발자가 퇴사를 하고 프로젝트를 인수인계 받았습니다.
여러분은 위 코드를 보고 a 변수가 어떤 기능을 하는 변수인지 예측과 디버깅이 가능하신가요?
정확히 추론하지 않은 타입은 미래의 나와 후임 개발자에게 분노를 일으키는 계기가 됩니다.
요즘 흰머리 나는것 같아...
React Props는 정확히 추론해주세요.
솔직히 이해가 안가는 부분이 있습니다. 컴포넌트의 Props 타입에 왜 any를 사용하는 것일까요..
const Comp = (props: any) => {
return <div>{props.age}</div>;
};
정말로 간단한 컴포넌트이기에 props가 어떤 프로퍼티가 있을지 쉽게 파악이 됩니다.
만약 300줄이 넘는 컴포넌트라면요?
인수인계 받은 개발자는 컴포넌트를 파악하는데 최소 몇 분의 시간이 소요가 됩니다. 복잡한 컴포넌트라면 훨씬 더 오래 걸리겠죠.
props의 타입을 정확하게 해주는 것 만큼 쉬운 타입 바인딩은 없습니다.
꼭! 제발! 컴포넌트의 Props type은 명시해주세요..
객체타입의 React State라면 타입을 정확히 추론해주세요.
여러분의 스트레스를 유발하기 위해 아래와 같은 코드를 작성해봤습니다.
const Comp = (props: any) => {
const [utils, setUtils] = useState<any>({
...props.utils,
});
useEffect(() => {
if (props.utils.loading) {
setUtils((prev: any) => ({ ...prev, secondLoading: "what the .." }));
}
}, [props.utils]);
return <div>{props.age}</div>;
};
타입 추론이 가능하신가요?
여러분이 인수인계 받은 프로젝트에서 위 컴포넌트를 마주치신다면 전임 개발자는 어떤 분이셨을까 생각하게 될 것 입니다.
state의 타입을 정확히 알수가 없기 때문에 어떤 상황에서 에러 상황을 마주칠 지 예측을 할 수 없습니다.
위 상황을 제외하고도 any를 남발해서는 안되는 이유는 무궁무진 합니다.
any를 남발해서는 안되는 이유도 추론이 가능하시겠지만 유지보수의 관점에서 이유를 나열해보겠습니다.
이유
지금 당장 돌아가는 코드? No!!
먼저 지금 당장 타입을 추론하고 바인딩이 할 시간이 없어서 위와 같은 코드를 작성하고 지금 당장 문제 없는 코드를 작성한다고 합시다.
개발하는 프로젝트의 수명이 오래되고 상당한 시간이 흐른다면 위와 같은 코드는 유지보수 하기에 몇 곱절의 시간이 들어갈 확률이 높습니다.
다시 말해서 지금 당장만을 위한 코드는 프로젝트의 수명을 줄일 수 있습니다.
협업하기 좋지 않아요
당신이 any 타입을 남발하는 개발자라면 함께하는 개발자를 괴롭히고 싶은 심리가 있을 수 있습니다.
위 예시를 보면 알 수 있지만 any 타입은 함께 협업하는 개발자가 코드를 파악하기 힘들어집니다..
any는 전염을 타고~
프로젝트에 any를 사용하다보면 any는 전염병이 되어서 프로젝트에 퍼져나갑니다.
최근에 아래의 비즈니스 로직을 작성한 적이 있습니다.
const filteredChartData = useMemo(() => {
let data: any = {};
const keys = Object.keys(chartData);
const makeData = (item: Record<string, unknown>) => {
const data = { time: item['time'] };
Object.keys(item).forEach((key) => {
if (key !== 'time') {
if (
typeof item[key] !== 'undefined' &&
(isEmpty(selectedGpuCardList) ||
selectedGpuCardList.includes(key))
) {
data[key] = item[key];
}
}
});
return data;
};
keys.forEach((key) => {
if (key === 'allData') {
data[key] = Object.keys(chartData[key]).map((dataKey) =>
chartData[key][dataKey].map((item) => makeData(item)),
);
} else {
data[key] = chartData[key].map((item) => makeData(item));
}
});
return data;
}, [chartData, selectedGpuCardList]);
데이터를 파싱 차트 컴포넌트에 들어갈 데이터를 만드는 것입니다.
기존에는 chartData라는 데이터를 그대로 차트에 props로 내려주고 있었지만, 새로 추가할 기능에서는 데이터를 필터링 해서 props로 내려줬어야 했습니다. 그리고 chartData의 데이터 타입은 any 였습니다.
콘솔로 데이터를 디버깅 한 결과 object 타입의 객체였고, 당연히 똑같은 모습의 객체로 파싱을 했습니다.
필터링한 data(filteredChartData)의 타입은 Record<string, unknown> 이였습니다.
그리고 props로 내리기 위해 파싱한 데이터를 컴포넌트의 props로 넣어주는 순간 컴파일 에러가 발생했습니다.
알고 보니 컴포넌트의 props의 타입은 list 타입이였습니다..
해당 컴포넌트는 공통 컴포넌트였고 이슈의 데드라인까지 얼마 남지 않았기 때문에 영향이 있는 모든 컴포넌트를 리팩토링할 수 없었습니다.
결국 파싱 된 데이터의 타입도 이전 데이터의 타입과 마찬가지로 any로 바인딩이 되었고, 기술 부채로 쌓이고 말았습니다.
위와 같은 케이스처럼 any를 사용하다보면 프로젝트 전역에 any가 전염되고 맙니다.
그럼에도 불구하고 any를 사용해야 하는 경우
추상화된 코드를 작성할 때 정말 타입의 범위를 좁게 추론할 수 없는 경우
추상화된 코드 (공통 hook이나 공통 컴포넌트)를 작성할 때 정말 타입을 좁게 추론할 수 없는 경우가 있습니다.
예를 들면 event 객체 입니다.
click, mouse event 등의 이벤트 핸들러 함수를 props를 받는 경우 event 객체의 타입을 좁게 추론하기 어렵습니다.
이런 경우 event 객체에 한해서 any 타입을 바인딩 할 수 있습니다.
아래 코드는 이전에 작성한 인터페이스 컴포넌트입니다.
Input 컴포넌트를 비제어 컴포넌트로 핸들링 하기 위해 사용하는 인터페이스 컴포넌트였고, 다양한 Input 컴포넌트의 event를 추론하기 어려워 any 타입을 사용하고 런타임 에러를 방지하기 위해 타입 가드를 사용했던 경우가 있었습니다.
type HookFormControlProps<T extends FieldValues, P extends InputProps> = {
component?: React.ElementType;
componentProps?: P;
onCustomChange?: (e: any) => void;
} & HookFormControlType<T>;
function HookFormControl<T extends FieldValues, P extends InputProps>(props: HookFormControlProps<T, P>): ReactElement {
const { component: Component, control, name, rules, componentProps, onCustomChange } = props;
const {
field: { onChange, value },
} = useController({ rules, name, control });
let val: PathValue<T, Path<T>> | null = value;
const handleChange = useCallback(
(e: any) => {
if (onCustomChange instanceof Function) {
onCustomChange(e);
}
if (e?.type === 'file' && onCustomChange instanceof Function) {
const { value, files = [] } = e;
onChange(e);
onCustomChange({ value, files });
} else {
onChange(e);
}
},
[onChange, onCustomChange],
);
... 중략
하지만 가급적 타입을 추론할 수 없는 경우는 unknown 타입을 활용해 개발 당시 타입을 좁게 추론하고, 예상치 못한 런타임 에러를 방지하시기 바랍니다.
마치며
이번 포스팅은 any 타입을 자세하게 설명하는 포스팅은 아니였습니다.
다만 제가 분노에 사무쳐 any를 지양해야 하는 이유를 남긴 포스팅 이였습니다.
만약 any 타입을 정확히 알고 싶으시다면 아래 블로그 글을 참고해주시면 감사하겠습니다.
이펙티브 타입스크립트: any 다루기
프로젝트의 코드를 작성할 때 내가 파악할 수 있는 코드가 아닌 다른 사람이 쉽게 파악할 수 있는 코드를 작성했으면 좋겠습니다.
그것이 클린코드의 지름길 이라고 생각합니다.
읽어봐주셔서 감사합니다.
'클린코드' 카테고리의 다른 글
[클린코드] 함수의 클린코드 (2) | 2024.11.20 |
---|---|
[클린코드] 반복되는 값, 리터럴 (1) | 2024.08.22 |
[클린 코드] 가독성 좋은 코드 - 변수 (2) | 2024.06.17 |