Home Effective Typescript 스터디 4주차 (Item 16 ~ 20)
Post
Cancel

Effective Typescript 스터디 4주차 (Item 16 ~ 20)

DesktopView

이 게시물은 SW 마에스트로 연수생 간 진행한 Effective Typescript 스터디의 개인 정리 내용입니다

[ Item 16 ] number 인덱스 시그니처보다는 Array, 튜플, ArrayLike 사용하기


number 인덱스 시그니처보다는 Array, 튜플, ArrayLike 사용하기

앞선 Item 15에서 <동적> 데이터에만 인덱스 시그니처를 사용하기를 권장했습니다.

하지만 알다시피 인덱스 시그니처를 사용한다면 object의 키값은 string, number, symbol 모두 가능합니다. ( 세가지’만’ 가능합니다. boolean타입은 안되더라구요..?)

또한 알다시피 런타임까지 객체의 타입을 알 수 없을때만 인덱스 시그니처 사용을 제안했었습니다.

JS 는 과할정도로 느슨한 언어다

1
2
3
4
{1:2,3:4} => {"1":2,"3":4}
let x = {}
x[[1,2,3]]= 2
Object.keys(x) = ["1,2,3"]

위 예제에서 알 수 있듯, 일반적으로 JS 에서 Key 에 숫자값을 넣으면 String으로 자동 변환됩니다.

하지만, 앞서 인덱스 시그니처에서 Key값에 string, number, symbol 타입을 지정할 수 있다고 했던 것 기억나시나요?

TS에서 Key값에 number를 지정하는것은 완전 순수 TS 문법에 속합니다.

1
2
3
4
5
6
7
8
9
10
11
type alpha = {
    [n:number|string]:string
}

const test:alpha ={
    1:'a',
    2:'b',
    3:'c',
    "1":'d'
    // 여기서 에러는 Type 에러가 아닌 Same name 에러가 발생한다! ㅋㅋ
}

또한, 실제로 JS 로 컴파일 되며 모든 타입들이 사라질 땐 number로 지정된 key 값들 또한 일반적인 JS 에서 그러하듯 string 값으로 변하게 된다!

위 예제를 보면 알 수 있듯이 마지막 key “1” 에서 1과는 타입이 다르니 TS 에서는 완전히 다른 객체로 이해하고 에러가 발생하지 않아야 하는데, 정작 “1” 에서 SAME NAME 에러가 발생한다. 재밌는 부분이다.

결론

사실 인덱스 시그니처 자체도 사용을 지양하라 해놓고서 number 인덱스 시그니처까지 지양하라고 하면 사실상 쓰지 말라는 것에 가깝지 않나 싶다.

대신 Array, Tuple, ArrayLike 를 애용해달라는데 ArrayLike 이번에 있는 줄 처음알았고, 사실 이정도로 복잡한 Type 을 사용할 일이 얼마나 있을까 싶기도 하다.

[ Item 17 ] 변경 오류방지를 위해 readonly 사용하기


변경 오류방지를 위해 readonly 사용하기

코드를 작성하며 어떤 값 A 에 대해서 참조만 필요로 하거나, 변경되어선 안되는 경우가 자주 있습니다.

Typescript 에서 그런 타입에 사용할 수 있는 기능이 바로 readonly 입니다.

C# 개발자가 만든 언어 아니랄까봐 C# 에서도 사용되는 개념으로 알고 있는데, 이 Item 에서는 주로 JS 를 많이 접해왔을 우리에게 어떤 상황에서 readonly 를 사용하면 좋을 지 간략하게 설명해줍니다.

🙄 사실 이번 챕터에선 바뀌면 안되는 내용이 바뀌는 경우에 대해서 구구절절 예시를 들어주며 설명한다.
바뀌면 안되는 내용이 바뀌면 당연히 곤란하지… 이걸 왜 이렇게 구구절절 예시까지 들어주는지는 좀 의아하다.

const 와 readonly 의 차이 ?

Item에선 다루지 않으나 요약 부분에서 나온 키워드인데 생각해보니 재밌는 부분이어서 찾아봤습니다.

우선 두 키워드의 공통점은 “초기 할당값을 변경할 수 없다” 라는 점입니다.

하지만 그 차이점이 무엇일까요?

  • const
    • 변수 참조를 위한 것이다. ( 변수를 위한 것 )
    • 변수에 다른 값을 할당/대입 할 수 없다.
  • readonly
    • 속성을 위한 것이다.
    • 속성의 변경을 방지하나, 불가능한 것은 아니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
      let foo: {
      readonly bar: number;
        } = {
          bar: 123,
        };
    
      function iMutateFoo(foo: { bar: number }) {
          foo.bar = 456;
      }
    
      iMutateFoo(foo);
      console.log(foo.bar); // 456!
    
    1
    2
    3
    4
    5
    6
    7
    
      interface Foo {readonly bar: number;}
      let foo: Foo = {bar: 123};
      function iTakeFoo(foo: Foo) {
          foo.bar = 456; 
      	// error ! : bar 는 readonly
      }
      iTakeFoo(foo);
    

readonly 는 [ 내가 ] 속성을 변경하지 못함은 보장하지만, 객체를 다른 사람에게 넘길 경우는 보장되지 않는 듯 합니다. ( iMutateFoo() )

당연한 얘기지만, 가장 큰 차이점은 변수에 사용되느냐, 속성에 사용되느냐에 대한 차이점이 아닐까 싶습니다. 이에 더해서 readonly 가 변경을 방지하긴 하지만, 불가능한 것은 아니라는 점까지 !

결론

변경되면 안되는 값에는 readonly를 써보세요!

++ 특히 함수에 들어가는 매개변수가 수정되지 않는다면 더욱 !

[ Item 18 ] 매핑된 타입을 사용하여 값을 동기화하기


매핑된 타입을 사용하여 값을 동기화하기

타입시스템이 강점을 보이는 부분은 타입을 이미 정해두고 그 타입을 벗어나는 값이 들어가는 것과 비슷한 정적인 경우입니다.

반대로 약하고 이 책에서도 조심히 다루는 부분인 바뀌는 값이 동적인 경우. 즉, 인덱스 시그니처를 사용해야하는 경우가 아닐까 싶습니다.

결론 . . . ?

이 Item 에서 거의 대부분의 내용이 예제를 속성이 동적을 바뀌는 경우에 대해서 설명하고 있습니다.

Component Prop 에서 속성이 추가될 때 마다 Interface 의 값을 바꿔줘야 하는 것은 너무 비효율적이다.

라는 내용인데 결론은 그 경우엔 인덱스 시그니처에 타입 연산(keyof)을 섞어서 매번 추가하고 체크하는 그 비효율을 줄이라고 가이드 해줍니다.

그 의견에 저도 동의하고 참 좋은 방법인 것 같다는 생각이지만……… 이게 이 챕터 내용의 끝입니다. ( 최소한 제가 얻어간 )

이전에는 계속 런타임까지 속성 모를때만 인덱스 시그니처 써라!! 진짜 어쩔 수 없을때만 써라!!! 이런 느낌이라 지양하려 했는데 이번엔 또 이럴 땐 쓰세요~ 하니 어느 장단에 춤을 춰야하는지 모르겠습니다.

[ Item 19 ] 추론 가능한 타입을 사용해 장황한 코드 방지하기


추론 가능한 타입을 사용해 장황한 코드 방지하기

Typescript 의 타입 추론 기능은 정말정말 강력합니다. 실제로 사용해보니까 더더욱 그걸 느낍니다.

실제로 세어보면 몇몇 내가 구현한 객체들 빼고 일반적으로 사용하는 함수나 라이브러리들은 타입을 굳이 설정하지 않아도 어디서 알아왔는지 모를 요상한 타입들이 이미 추론 되어 있는 것을 볼 수 있습니다.

‘추론을 안시켜도 하도 잘해줘서 딱히 js 쓸 때랑 엄청나게 많이 까다로운 것 같진 않은데, 내가 잘 못 쓰고있는건가?’

최근 사용하며 많이 드는 생각이었는데 마침 잘 만난 Item이라는 생각이 들었습니다.

실제로 정말 대부분의 경우 추론 기능은 강력하게 작동하고, 작성한 코드의 맥락에 맞춰서 적절하게 타입을 설정해줍니다.

다만 책에서 그럼에도 불구하고 타입 명시를 사용하기를 추천하는 경우가 두 가지 있다.

타입 추론이 있지만 명시가 필요한 경우

  • 객체 리터럴 정의
  • 함수의 반환에 정의

객체 리터럴 정의

앞서

Effective TypeScript Study Week3 - Item11 - 잉여 속성 체크의 한계 인지하기

에서 잉여 속성 체크 기능을 소개한 적 있었습니다.

만약 잉여 속성 체크 기능이 작동하지 않고 에러가 발생한다면, 디버깅을 위해 수정해야 할 객체를 선언한 곳이 아니라 객체가 사용되는 곳에서 에러가 발생하게 됩니다.

그렇기에 타입을 제대로 명시한다면, 실수한 부분에서 오류를 받아볼 수 있고 더욱 의도에 맞게 사용할 수 있을 것입니다.

함수의 반환에 정의

이 또한 앞선 객체 리터럴과 마찬가지로 오류의 위치를 정확히 받아보는 것 이점이 있지만,

이에 더해 두 가지 이점이 더 있습니다.

  • 반환 타입 명시로 함수에 대해 더욱 명확하게 알 수 있습니다.
  • 명명된 타입을 사용하기 위해서

라고 하는데, 개인적으론 이 두 가지 보다는

‘함수를 구현하기 전에 테스트를 먼저 작성하는 TDD 와 비슷하다’

라는 문장이 가장 납득이 가는 이유였습니다.

결론

Item의 앞 부분은 많이 생략했으나, 타입 추론은 정말정말 강력합니다. 타입 추론이 정상적으로 이뤄지고 있다면 굳이 타입구문을 작성해서 코드를 더 복잡하게 만들지 않는 게 더 좋습니다.

굳이 예외를 두자면 객체 리터럴의 경우에만 ‘잉여 속성 체크’ 를 사용하기 위해서 명시하는게 좋겠습니다!

[ Item 20 ] 다른 타입에는 다른 변수 사용하기


다른 타입에는 다른 변수 사용하기

다만 해당 아이템은 스터디날엔 정말 별 것 없는 내용이라고 하고 넘어갔지만, 지금 우연히 이 아이템이 담긴 챕터 명이 “타입 추론”임을 인지하고 다시 보니 아주 별 볼일 없는 아이템은 아니지 않나? 라는 생각이 듭니다.

example case 1.

1
2
3
4
5
6
let id : string:number = "12-34-56";
fetchProduct(id)
// 아마 string 을 처리하는 함수로 추정됨
id = 123456;
fetchProductBySerialNumber(id)
// 함수 이름부터 SerialNumber가 들어가니 number를 처리하겠지.

example case 2.

1
2
3
4
5
const id = "12-34-56"
fetchProduct(id)

const serial = 123456;
fetchProductBySerialNumber(serial)

id 를 재사용하던 case 1 에서 변수를 두개로 나눈 case 2로의 변화에서 무엇을 확인했나요?

당연히 변수명이 바뀌었지만, 그 전에 타입 구문이 사라졌습니다.

너무 간단한 변수이기때문에 타입추론에 의존해도 괜찮다는 뜻이기도 하겠죠. 반대로 얘기하자면 간단한 변수는 타입을 명시하는게 오히려 생산성을 떨어뜨리는 원인이 된다는 것이기도 합니다.

결론

앞선 Item 19에서 타입추론을 믿고 타입구문 남발을 지양하라는 메세지를 확인할 수 있었습니다.

case 1 의 코드를 보고있자면 id 라는 변수에 2개의 타입이 들어올 수 있으니 타입을 두개 설정하고 각각의 케이스를 나누고있는 제가 보입니다.

case 1은 극단적인 예시이기 때문에 뭐 저런걸 예시로 드나 싶겠지만, 우리는 정말로 저런 불필요한 타입구문을 사용하고있진 않은지 되돌아 볼 필요가 있을 듯 싶습니다.

This post is licensed under CC BY 4.0 by the author.

Effective Typescript 스터디 3주차 (Item 11 ~ 15)

BOJ_9077 지뢰제거 - Python