6. 타입 관련 배운 것

Circlewee·2022년 7월 27일
0

WYLSBingsu

목록 보기
7/8
post-thumbnail

1. ?와 undefined의 차이점

TS으로 개발을 하다보면 반드시 interface혹은 type으로 타입을 명시해줘야하는 일이 생긴다.
하지만 API로 데이터를 받아오거나 자신이 정의한 타입의 데이터 변수를 사용할 때 무조건 초기값이 원하는 타입대로 들어가지 않는 경우가 있다.(비어있거나 가공되기 전이라면, 혹은 있을 수도 없을 수도 있는 경우) 이럴때 무조건 이 타입이다라고 명시해 놓는다면 TS에서 오류를 출력해버린다.

이럴때 사용할 수 있는 방식이 undefined일 수 있음을 직접 명시하거나 ?을 적어줘서 해결할 수 있는 데 프로젝트를 하면서 두 방식의 차이를 알 수 있었다.

function addString(a: string, b: string) {
  return a + b;
}

const a = 'hello';
const b = 'world!' as string | undefined;

console.log(addString(a, b));

다음과 같은 함수가 있다고 하자. 인자 a, b는 반드시 string 타입이 와야 하는 상황이고 일부러 b에 as를 사용해 undefined일 수도 타입 표명해주었다.

그러면 다음과 같은 오류를 확인할 수 있다.

'string | undefined' 형식의 인수는 'string' 형식의 매개 변수에 할당될 수 없습니다.
'undefined' 형식은 'string' 형식에 할당할 수 없습니다.

물론 js파일로 함수에 변수 b를 undefined로 전달하면 'hellowundefined'라고 출력되지만 이렇게 되지 않으려고 TS를 사용하는 것이기 때문에 두 방식으로 해결해보자.

1.1 ?

function helloWorld(a: string, b?: string) {
  return a + b;
}

const a = 'hello';
const b = 'world!' as string | undefined;

console.log(helloWorld(a, b)); // helloworld!
console.log(helloWorld(a)); // helloundefined

?를 사용한다는 것은 해당 인자가 존재하지 않을 수도 있음을 함수에 전달한다.
그리고 두번째 출력을 보면 알 수 있듯이 인수로 a만 넘겨도 정상 동작한다.

1.2 undefined

function helloWorld(a: string, b: string | undefined) {
  return a + b;
}

const a = 'hello';
const b = 'world!' as string | undefined;

console.log(helloWorld(a, b)); // helloworld!
console.log(helloWorld(a)); // TS Error

이번엔 인자 b의 타입이 string 또는 undefined라고 명시해 주었다. 위의 코드와 마찬가지로 첫 번째 출력은 오류없이 원하는 대로 출력되는 것을 확인할 수 있다.
다만 두번째 출력은 오류가 발생하는 데, 2개의 인수가 필요한데 1개를 가져왔습니다.라는 오류를 확인할 수 있을 것이다.

1.3 왜?

?_parameter
undefined_parameter

두 코드의 에디터화면 캡쳐를 보면 인자 b의 type은 동일한 것을 확인할 수 있다.
다만 ?는 b자체가 존재하면 string이고 그렇지 않다면 undefined임을 의미하는 것이고,
string, undefined의 union 타입을 가진 b는 반드시 인수로 주어져야함을 의미하는 것 임을 알수 있다.

따라서 상황에 따라 반드시 인자로 데이터가 전해져야하는 상황이라면 union 타입을 활용하고 없어도 괜찮은 상황이라면 ?를 사용하면 된다.

1.4 반드시 특정 타입의 데이터가 전달되어야하는 경우

다시 맨 처음 코드로 가서 a, b가 반드시 문자열로 주어져야한다고 하자. 하지만 인수로 제공하는 변수의 타입이 그렇지 못한 경우 어떻게 해결해야할까?

function addString(a: string, b: string) {
  return a + b;
}

const a = 'hello';
const b = 'world!' as string | undefined;

console.log(addString(a, b)); // TS Error

✨ 제일 쉬운 방법으로는 조건문을 통과할때만 함수를 실행하게 하는 것이다.

...
if (b) {
  console.log(helloWorld(a, b));
}

조건문을 통과하면 b가 undefined가 아닌 것이 확실해지는 것을 이용한 방법이다.
이 밖에도 여러 방법이 있는 데 이 글을 참고해보면 좋을 것 같다.

2. 함수 컴포넌트의 props type

// TS Error
const ArticleForm = ({ foo, children }) => {
  ...

함수 컴포넌트를 작성하면서 props를 사용할 때 이것들의 type을 지정해주지 않으면 반드시 any타입이라는 오류가 생긴다.
이럴 때 해결할 수 있는 방법이 여러가지 있는 데 예시를 통해 알아보자.

2.1 React에서 제공하는 interface

2.1.1 두 가지 방법

// @types/react/index.d.ts
type PropsWithChildren<P = unknown> = P & { children?: ReactNode | undefined };

type FC<P = {}> = FunctionComponent<P>;
interface FunctionComponent<P = {}> {
    (props: P, context?: any): ReactElement<any, any> | null;
    propTypes?: WeakValidationMap<P> | undefined;
    contextTypes?: ValidationMap<any> | undefined;
    defaultProps?: Partial<P> | undefined;
    displayName?: string | undefined;
}
const ArticleForm = ({ foo, children }: PropsWithChildren<ArticleProps>) => {
  ...
}
const ArticleForm: React.FC<ArticleProps> = ({ foo, children }) => {
  ...
}

제일 대표적인 방법으론 React에서 제공하는 interface를 이용하는 것이다.
코드에는 PropsWithChildren과 FC를 사용하는 방법이며 둘다 props의 type과 children을 포함하고 있는 interface이다.
사실 이 방법을 사용한다면 props와 children을 모두 처리할 수 있고 IDE의 자동완성을 사용할 수 있는 장점도 존재하지만 다음과 같은 단점도 존재한다.

  1. 반드시 children이 존재한다.
    컴포넌트를 제작할 때 children의 유무를 보장해줘야하는 경우가 있는 데 위 interface를 사용하면 children이 무조건 포함되기 때문에 보장되지 않는다.
  2. React코드의 변경
    함수 컴포넌트에 hook이라는 개념이 등장하기 전에는 SFC(Stateless Function Component)라는 것도 존재했지만 시간이 지나 상태가 존재하지 않는 개념이 사라지면서 deprecated되었다.
    고로 FC도 추후에 다른 무언가로 변경된다면 React.FC<T>로 작성된 모든 코드들을 수정해야한다는 얘기다.
  3. defaultProps의 오류.
const Test:React.FC<{ message: string }> = ({ message }) => {
  return <div>{message}</div>;
};

Test.defaultProps = {
  message: 'defaultProps',
};

// AnotherComponent.tsx
  <Test /> // 'message' 속성이 '{}' 형식에 없지만 '{ message: string; }' 형식에서 필수입니다.

위 컴포넌트 코드에선 오류가 전혀 없다. 다만 defaultProps를 썼음에도 사용하는 쪽에서 props를 명시하지 않았다는 TS오류가 생긴다. (물론 앱에 치명적인 오류는 아니다.)

const Test: React.FC<{ message?: string }> = ({ message = 'default' }) => {
  ...
}

결국 코드를 이렇게 바꾸면 오류가 사라진다.

2.1.2 권장하는 방법

interface TestProps {
  foo: string;
  children: React.ReactNode;
  func: () => void;
  // func(): void;
}

const Test = ({ foo, children, func }: TestProps) => {
  ...
}

위 소제목에서도 말하는 것처럼 이것은 반드시 이렇게 해야하는 것이 아닌 권장하는 방법이다.
위에서 부정적인 면을 위주로 작성한 것처럼 권장하는 방법 역시 interface를 사용하는 것이 아닌 직접 타입을 명시해주는 것이다.

3. 마무리

프로젝트에서 TS를 처음 써보면서 마주하는 오류들은 개발 속도를 내야하는 경우에는 굉장한 스트레스로 다가왔다. 오류를 해결해야 다음 단계로 넘어갈 수 있었고 해결하기 위해 원인 파악부터 구글링에 쏟는 시간도 아까웠었다.

하지만 뒤돌아서 생각해보면 프로그래밍 단계에서부터 이런 오류를 잡아내는 것이 결국에는 나중에 발생하는 문제를 미리 해결하는 것이 아닐까 싶다. 가령 map을 통해 배열로 컴포넌트를 만든다고 했을 때 주어지는 값이 배열이 아닌 undefined라면? 또 object의 key값이 존재하지 않는다면?

결국 제대로 된 테스트를 해보거나 콘솔창에 찍힌 빨간 에러 메시지들을 확인해야 발견할 수 있었을 것이다.
TS에 대한 숙련도가 늘었음에 기쁨을 느끼면서 다음 글에서는 배포 시 마주쳤던 문제들에 이야기 해보려 한다.

profile
공부할 게 너무 많아요

0개의 댓글