타입스크립트 때려잡는 꿀팁 모음

우빈·2023년 8월 12일
107
post-thumbnail

계기

내가 타입스크립트를 막 시작했을 때
자주 했던 실수들을 겪는 사람들을 위해 정리해본 글이다.

그렇기에 "타입스크립트"라는 대주제 하나를 다룰 뿐, 기능의 종류나 상황은
다양하게 묘사될 수 있는 점 양해바란다.

또한, 리액트 환경에서 타입스크립트를 사용했을 때의 기준으로 글을 서술할 것이다.
바닐라 타입스크립트나 뷰, 앵귤러에 대해서는 더 공부하고 새로 글을 써보겠다.

제너릭 타입이란 무엇인가

일단 타입스크립트 끝판왕이라고도 불리는 제너릭에 대해 알아보자.
진짜 짧고 쉽게 설명해주겠다. 어려울 것 없다. 간단하게 생각해라.

만약 문자열 두 개를 받아서 출력하는 함수와, 숫자 두 개를 받아서 출력하는 함수 둘이 있다고 해보자.

const printString = (str1: string, str2: string) => {
	console.log(str1, str2);
}

const printNumber = (num1: number, num2: number) => {
 	console.log(num1, num2); 
}

printString("Hello", "World");
printNumber(10, 20); 

진짜 간단하게 생각해라. 한 함수로 저 두 함수의 기능을 모두 처리하는 법이 뭘까?
파라미터의 타입을 체크하고 이런 여러가지 방법이 있겠지만, 제너릭 타입을 사용하면 편하다.

제너릭 타입은 함수의 파라미터 앞에 꺾쇠를 열고 선언해준 다음, 그 타입을 사용하면 된다.
자 뭔소린지 모르겠지?? 코드를 보면서 알아보자.

// 꺾쇠를 열면 이 놈은 제너릭 타입을 사용하는 함수입니다~~ 라고 알려주는거다.
const printAll = <T>(prop1: T, prop2: T) => {
  console.log(prop1, prop2);
}

// 난 친절하니까 function 키워드 쓰는 분들에게도 알려주겠음

function printAny<T>(prop1: T, prop2: T) {
  console.log(prop1, prop2);
}

이해가 가는가? 함수의 파라미터 앞에다가 <T>를 적어주면,
"요놈은 제너릭 타입 쓰는 놈입니다~~~"라고 트랜스파일러한테 알려주는 것이다.
꺾쇠 안에 들어가는 네이밍은 자유롭게 해도 된다. 이렇게 꺾쇠로 선언을 하면,
파라미터를 포함한 그 함수 내에서는 T라는 타입을 사용할 수 있는거다.

여기서 T는 무엇이든 될 수 있다. number도 올 수 있고, string, boolean도 올 수 있다.

printAll(3, 5);
printAll("Hello", "World");
printAll(true, false);

꽤나 유쾌한 기능이죠? 그런데 이렇게 되면 한 가지 의문이 들 수 있다.
"이렇게 되면 함수의 파라미터에 어떤 변수가 들어오는지 명시할 수 없어!"

그 문제를 위해 함수를 사용을 할 때 "요 타입으로 씁니다~~"라고 말해줄 수 있다.

방법은 선언때와 비슷하다. 파라미터 앞에 쓰고 싶은 타입을 적어주면 된다.

printAll<number>(3, 5);
printAll<string>("Hello", "World");
printAll<string>(true, false); // 에러가 발생한다. 

이거 알면 제너릭 타입 끝이다. 이제 나 타입스크립트 아는 놈이다 하고 자랑하자.

이벤트 타입 지정하기

내가 제일 처음으로 any를 사용했던 타입이다. 몇 시간 동안
찾아보다가 너무 어지러워서 결국엔 울며 겨자먹기로 any를 썼던..

먼저 자주 사용되는 onClick 이벤트의 타입부터 알아보자.
이벤트 타입의 공통점은 제너릭 타입으로 이벤트를 받는 HTML 태그의 타입을
받는다는 것이다.

코드로 설명하는게 낫다. onClick 이벤트를 타입 지정해보자.

import React from "react";

const Component = () => {
  	const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
     	 
    }
  
 	return (
    	<button onClick={handleClick}>하트 클릭해주세용 ㅎㅎ</button>  
    )
}

이해가 되는가? 이런 식으로 사용하면 된다. onClick 이벤트는 MouseEvent이기
때문에 MouseEvent를, 그리고 제너릭 타입으로 onClick을 받는 태그의
타입을 넣어주면 된다. 어려울 것 없음.

div 태그라면 HTMLDivElement,
h1 태그라면 HTMLHeadingElement,
img 태그라면 HTMLImageElement,
a 태그라면 HTMLAnchorElement를 제너릭으로 넣어주면 된다.

참고로 React.MouseEvent와 같이 써준 것은 MouseEvent라는 기본 타입이
있기 때문에 헷갈릴까봐 저렇게 사용한 것이다. 취향 차이니까 그냥 써도 상관 없다!

onChange도 같이 알아보자. 이 친구는 보통 input창에 무언가를 입력했을 때
useState로 많이 관리한다. 그럼 마우스 이벤트가 아니라 다른걸 써줘야한다.

import React from "react";

const Component = () => {
  	const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     	 
    }
  
 	return (
		<input type="text" onChange={handleChange} />
    )
}

댜른건 MDN Docs를 참고하거나 GPT 검색하면 나옴.
이 외에도 onScrollReact.UIEvent이고 이런 다른 것들이 있다.
구글링 하는 법 알려주겠다.

내가 뭐 onMouseOver 타입을 쓰고있다 가정해보자. 그렇다면
onMouseOver event type 이렇게 검색하면 스택 오버 플로우가 잘 알려준다.

이제 더 이상 any 박지마라.. 떳떳하게 살자고 우리

useRef 타입 선언

useRef 선언하고 사용할 때도 오류나서 any 사용하는 사람들이 있을 수 있다.
이건 진짜 간단하다. 아까 배운 제너릭 타입 있제? 그거 선언할 때 넣어주면 끝이다.

import React from "react";

const Component = () => {
    // 참고로 Null 꼭 넣어줘야 편하다
	const divRef = React.useRef<HTMLDivElement>(null); 	 
  
  	const handlePrintDifRef = () => {
      	// ref에 접근할 때는 current를 붙여줘야 한다
		if(divRef.current) {
          // divRef에 선언할 때 null을 넣어뒀으니 if문으로 체킹을 해주고 써줘야 한다
          console.log(divRef);
        }
    }
  
  	return (
    	<div ref={divRef}>
      		Hello World!
      	</div>  
    )
}

쉽다 그죠? any 쓰지 말고 떳떳하게 살자.

HTML 태그 타입 그대로 사용하기

내가 컴포넌트를 만들었는데 div에 있는 타입을 그대로 사용하고 싶을 때가 있다.

const Component = () => {
 	return <div />
}

뭐 이런 컴포넌트가 있는데,

import Component from "./Component"

const App = () => {
  	const handleClick = () => {
     	console.log("Hello World!"); 
    }
  
 	return (
    	<div>
      		<Component onClick={handleClick} style={{ color: "red"; }} />
      	</div>  
    )
}

이렇게 쓰고 싶다고 가정해보자. 그럼 당연히 에러가 난다.
왜? Component는 props를 안 받는데, props에 넣어주니까 에러가 나지.
그럼 이제 직접 props에다가 onClick, style 이런 속성 선언하고 props로 받아서
div에 직접 넣어주는 그런 방법이 있다. 하지만, 더 혁신적인 방법이 있다.

스프레드 연산자를 야무지게 사용하면 코드를 대폭 줄일 수 있음.
자 이 코드를 보아라.

const Component = ({ ...props }: React.HTMLProps<HTMLDivElement>) => {
	return <div {...props} />
}

이런 식으로 구조분해할당 + 스프레드 연산자 써서 HTMLProps를 사용하고
제너릭 타입에 넣어주고 싶은 HTML 태그의 타입을 지정해주면 된다.

그러고나서 그 태그에다가 중괄호를 열고 그대로 스프레드 연산자로 props를 넣어주면 끗.
이렇게 되면 오류도 안나고, 자연스럽게 props에 넣어줄 때 자동완성도 된다.

InputElement나 AnchorElement같은 경우는 조금 다르게도 사용 가능하다.

const Component = ({ ...props }: React.InputHTMLAttributes<HTMLInputElement>) => {
 	return <input {...props} /> 
}

이런 식으로 사용 가능함. SVG는 사용법이 조금 다르다.

const Component = ({ ...props }: React.SVGAttributes<HTMLOrSVGElement>) => {
	return <svg {...props} />
}

나는 이렇게 사용했는데, 더 짧게 사용하는 방법이 있다.

const Component = (props: React.SVGProps<SVGSVGElement>) {
  	return <svg {...props} />
}

솔직히 처음에 보고 SVGSVGElement가 뭐여 말장난하나 이렇게 생각했는데 진짜 있다.
나도 친구 통해서 들은건데 좀 충격적이다.

진짜 있다. 당근 마켓 개발자도 이렇게 쓴다고 함^^.. 이거 쓰도록 하자...
나한테 이거 알려준 개발자한테 샤라웃하고감
@dev_seokjin

함수 props 타입 지정 에러

이것도 독학할 때 좀 시달렸던 에러이다.
내가 string 타입의 props를 컴포넌트 props로 받고 싶다고 가정해보자.

const Component = (props: string) => {
	return (
    	<div>{props}</div>  
    )
}

이렇게 쓰고 왜 안되냐고 하는 경우 있음.
일단 우리가 Component에 props를 줄 때 어떻게 주는지부터 알아보자.

const str = "Hello World!"

return <Component props={str} />

이렇게 쓴다. 일단 name={value} 이런 식으로 주기 때문에 객체로 전달받는 것이라 생각하면 편함.

그럼 객체로 받는다면 무엇을 해야 하는가? 바로 타입을 객체로 바꿔주어야 한다.

const Component = (parameter: { props: string }) => {
 	return (
    	<div>{parameter.props}</div>  
    )
}

사용할 때도 객체처럼 사용해야 한다. 이러면 에러 해결 끗!!
인터페이스나 타입으로 떼줄 수도 있음.

interface IComponentProps {
 	props: string 
}

const Component = (parameter: IComponentProps) => {
	return (
    	<div>{parameter.props}</div>  
    )
}

여기서 props를 객체 말고 바로 써주고 싶다? 구조분해할당 쓰면됨.

interface IComponentProps {
 	props: string 
}

const Component = ({ props }: IComponentProps) => {
 	return (
    	<div>{props}</div>  
    )
}

끗~ 다들 엄한데서 오류나서 힘들어하지말고 이렇게 사용하시오!

never에 값을 할당할 수 없습니다

useState에서 많이 나는 에러이다. 일단 이 에러는 state의 초깃값이
비어있거나 빈 배열이거나 하면 나는 에러이다.

해결법은 빈 값을 채워주면됨. 간단히 컴포넌트 내부라 생각하고 바로 코드 써보겠다.

const [number, setNumber] = useState(0);
const [str, setStr] = useState("");

// 배열이면 제너릭 타입을 활용해라
const [strArr, setStrArr] = useState<string[]>([]);

근데 이제 유저 값을 담은 객체 등 바로 초깃값을 넣어주면 코드가 유쾌하지
않은 경우의 state들이 있을 것이다.

근데 딱히 옵셔널체이닝으로 물음표살인마 빙의 말고는 방법이 없다.
그러니까 그냥 넣어라. 넣되, 유쾌한 코드로 바꿔라. 빈 state를 모듈화해라.

export interface IUserType {
 	name: string;
  	id: number;
    isLogined: boolean;
  	hobby: string[]
}

export const emptyUser = {
 	name: "",
  	id: -1,
  	isLogined: false,
  	hobby: [],
}

const [user, setUser] = useState(emptyUser);

이렇게 하면 안전하면서도 유쾌한 코드를 짤 수 있다~ 로그아웃하고
emptyUser라는 모듈을 한번 더 이용해서 state를 다시 바꿔주면 좋겠죠?

리터럴 타입

가끔씩 문자열 중에서도 특정 문자열을, 숫자 중에서도 특정 숫자만 들어오는 타입을
쓰고 싶을 때가 있을 것이다. 그럼 그냥 그렇게 써주면 됨. 보자.

interface IUser {
  name: string;
  gender: "male" | "female"
}

이런 식으로 선언해주면 gender에는 male이나 female만 올 수 있다.
다른 string이 올 경우 오류가 발생한다. string 타입만이 예외는 아니다.

interface INotLoginUser {
  isLogined: false; // false만 올 수 있다
}

interface ILoginedUser {
  serverId: 1 | 2 | 3 // 1, 2, 3만 올 수 있다
  isLogined: true; // true만 올 수 있다 
}

이를 타입으로 모듈화해서 사용해줄 수도 있다.

export type ServerIdType = 1 | 2 | 3

interface ILoginedUser {
  serverId: ServerIdType;
  isLogined: true;
}

유용하죠? 보통 로컬스토리지 키나 axios로 API 통신할 때 사용하곤 한다.
토큰 관련해서 access_token | refresh_token만 와야할 때 자주 쓰는 듯.

Array<string> vs string[]

여담이긴 한데 특정 타입의 배열을 두 가지 방법으로 사용할 수 있다.
제너릭 타입을 사용해서 쓰거나, 타입 뒤에 [ ]를 붙여 사용할 수 있음.

const B: Array<number> = [5, 3]
const A: number[] = [3, 5]

나는 Array라는걸 문자적으로 명시해주고, 제너릭 타입을 개인적으로 좋아하기도 해서
전자가 더 낫다고 생각하는데, 성능 차이에서 문제가 있는지 궁금하다.
개인적인 취향으로는 전자가 좋다고 생각한다. 잘 아시는 분 피드백 환영합니다

스타일드 컴포넌트 타입

타입스크립트 환경에서 스타일드 컴포넌트를 사용하는데, props를 받아야하는
경우가 있다. 이럴 때 지정 안해주면 props를 넣을 때 오류가 발생하곤 함.

import문은 생략한다. 다음과 같이 넣어주자.

const A = () => {
	return <StyledDiv color="blue" />
}

const StyledDiv = styled.div<{ color: string }>`
	background-color: ${({ color }) => color};
`

이렇게 써주면 된다~ 유쾌하지 않으면 이것도 제너릭 타입이기 때문에
인터페이스로 떼주어도 상관없다~~

interface IStyledDivProps {
 	color: string; 
}

const StyledDiv = styled.div<IStyledDivProps>``

as

보통 타입 주석은 초기화할 때 외에는 사용하기 어려운데, 이를 위해
as 키워드로 타입을 "이 친구는 요놈입니다~~"하고 지정해줄 수가 있다.

const A = (str: string) => `Hello, ${str}!`;

A("Ubin" as string);

사용될 일이 잘 없긴한데, 값이 undefined일 수도 있는 경우나 뭐 그런
예외가 있을 때 as를 사용하곤 한다. 보통은 파이프라인 두 개로 undefined 체킹 연산자
를 사용하는게 더 안전하다고 생각함. 이벤트 타입에서 오류 나는 경우 등의
특별한 경우가 아니면 as는 많이 사용하진 말자.

as unknown as type

거의 any 아닌 척 하는 any라고 할 수 있다.
타입스크립트 서버를 상대로 가스라이팅하는거라고 생각하면 된다.

const A = (str1: string) => str1

A(3 as unknown as string)

이거 쓰면 number타입을 넣어도 string으로 인식되어 에러가 안난다.
솔직히 any랑 다를 바가 없으니까 알아두기만 하는 편법이면 좋겠다.

마무리

정리를 해보니 후딱 끝내자 생각했는데 글 쓰는데만 한 시간이 걸렸다...
다음부터는 글 쓰기 실력과 속도를 늘리는 데에 집중을 해야할 것 같다.

타입스크립트를 처음 겪으며 "도대체 이거 왜쓰지"라고 생각하며 머리를
쥐어뜯고 계신 분들이 혹여나 있을까봐 과거의 내가 생각나서 써본 글이다.

모두 즐거운 코딩~~ 타입스크립트 알고 나면 정말 좋다~~!!

profile
프론트엔드 공부중

8개의 댓글

comment-user-thumbnail
2023년 8월 20일

오우 사랑합니다

1개의 답글
comment-user-thumbnail
2023년 8월 21일

사랑합니다22

1개의 답글
comment-user-thumbnail
2023년 8월 21일

사랑합니다33

1개의 답글
comment-user-thumbnail
2023년 8월 21일

https://developer.mozilla.org/en-US/docs/Web/SVG/Element

Svg element에는 circle, path, text 등등 여러가지고 있고, 그중에 svg 도 있는데요~
그래서 이 전체가 SVGElement 이구여~, svg가 SVGSVGElement로 있는것으로 보입니다. SVGCircleElement, SVGClipPathElement 등도 따로 있구여~

HTMLElement에도 HTMLImageElement, HTMLButtonElement 등이 따로 있듯이요ㅎㅎ

1개의 답글