Typescript

[Typescript] 이펙티브 타입스크립트 2장 아이템17-18

sian han 2023. 3. 7. 21:40

※ 아이템17 변경 관련된 오류 방지를 위해 readonly 사용하기

매개변수로 받은 배열의 합을 구하는 함수 arraySum

function arraySum(arr: number[]){
    let sum = 0, num;
    while((num = arr.pop()) !== undefined){
        sum += num;
    }
    return sum;
}

 

삼각수를 출력하는 함수 printTriangles

0 1 3 6 10 이 출력되어야하는데, 오류가 발생하고있다

arraySum 이 nums 를 변경하지 않는다고 간주해서 문제가 발생했다. 실제로 arraySum 은 pop() 을 사용해 매개변수 arr 을 변경한다. 

 

계산이 끝나면 배열 arr 은 비게 되고, 다음과 반복문을 돌면서 각 변수들은 다음과 같은 값들을 갖게된다.

1회차:
i = 0
nums = [0]
sum 0
nums = [] //pop 이후 nums 는 다시 빈배열이 됨

2회차:
i = 1
nums = [1]
sum 1
nums = []

3회차:
i = 2
nums = [2]
sum 2
nums = []

 

 

오류를 좁히기 위해 readonly 를 사용해 arraySum 함수가 배열을 변경하지 않는다고 아래와 같이 선언한다.

오류메세지 : Property 'pop' does not exist on type 'readonly number[]'

 

매개변수로 받은 readonly 배열을 변경하지 않으면 오류를 없앨 수 있다

 - 배열에서 pop 을 사용하지 않고 순회해서 더한다

function arraySum(arr: readonly number[]) {
  let sum = 0
  for (const num of arr) {
    sum += num
  }
  return sum
}

function printTriangles(n: number) {
  const nums = []
  for (let i = 0; i < n; i++) {
    nums.push(i)
    console.log(arraySum(nums))
  }
}

printTriangles(5) //0 1 3 6 10

함수가 매개변수를 변경하지 않을 때 readonly 로 선언한다면

  • 더 넓은 타입으로 호출할 수 있고
  • 의도치 않은 변경을 방지할 수 있다

 

연속된 행을 가져와서 빈 줄을 기준으로 구분되는 단락으로 나누는 기능을 하는 함수 parseTageedText

function parseTaggedText(lines: string[]): string[][] {
  const paragraphs: string[][] = []
  const currPara: string[] = []

  const addParagraph = () => {
    if (currPara.length) {
      paragraphs.push(currPara)
      currPara.length = 0 // Clear the lines
    }
  }

  for (const line of lines) {
    if (!line) {
      addParagraph()
    } else {
      currPara.push(line)
    }
  }
  addParagraph()
  return paragraphs
}
const str = `Lorem ipsum dolor sit amet,consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 

Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 
Excepteur sint occaecat cupidatat non proident, 

sunt in culpa qui officia deserunt mollit anim id est laborum.`

console.log(parseTaggedText(str.split('\n'))) //[ [], [], [] ]

addParagraph() 함수 내부의

paragraphs.push(currPara)
currPara.length = 0

에서 currPara 는 같은 객체이다. currPara의 길이를 0으로 만들어줌으로서 paragraphs배열에 추가된 currPara 의 길이도 0이 된다.

따라서 빈 배열이 출력되었고, 문제는 currPara.length 를 수정하고 currPara.push 를 호출하면 둘 다 currPara 배열을 변경한다는 점이다. 

 

코드를 다음과 같이 수정해볼 수 있다.

paragraphs.push([...currPara])
currPara.length = 0

 

 

1. currPara 를 readonly 로 선언한다

const currPara: readonly string[] = [];

2. currPara 를 let 으로 선언한다

let currPara: radonly string[] = [];
...
currPara = []; //Clear the lines

3. 변환이 없는 메서드를 사용한다

  - push 와 달리 concat 은 원본을 수정하지 않고 새 배열을 반환한다

currPara = currpara.concat([line]);

 

 

▶ readonly number[] 타입

  • number[] 는 readonly number[] 의 서브타입이다
  • 배열의 요소를 읽기만 가능하다
  • lenght 를 읽을 수 있지만, 바꿀 수 없다
  • 배열을 변경하는 메서드를 호출할 수 없다 (pop, push, slice ..)
  • readonly 배열에 변경가능한 배열(a)을 할당할 수는 있지만, 변경가능한 배열에 readonly 배열을 할당하는 것은 불가하다.
const a: number[] =[1,2,3];
const b: readonly number[] = a; //가능
const c: number[] = b; //불가

 

매개변수를 readonly로 선언했을 때

  • 타입스크립트는 매개변수가 함수 내에서 변경이 일어나는지 체크한다
  • 호출하는 쪽에서는 함수가 매개변수를 변경하지 않는다는 보장을 받게된다
  • 호출하는쪽에서 함수에 readonly 배열을 매개변수로 넣을 수도 있다

 

 

요약

  • 만약 함수가 매개변수를 수정하지 않는다면 readonly 로 선언하는 것이 좋다. reaonly 매개변수는 인터페이스를 명확하게 하며, 매개변수가 변경되는 것을 방지한다.
  • readonly 를 사용하면 변경하면서 발생하는 오류를 방지할 수 있고 변경이 발생하는 코드도 쉽게 찾을 수 있다
  • const 와 readonly의 차이를 이해해야 한다
  • readonly 는 얕게 동작한다는 것을 명심해야 한다

 


 

※ 아이템18 매핑된 타입을 사용하여 값을 동기화하기

산점도(Scattter plot) : 좌표상의 점들을 표시함으로써 두 개 변수 간의 관계를 나타내는 그래프 방법

 

산점도를 그리기 위한 UI 컴포넌트를 작성한다고 가정해보자

interface ScatterProps{
	//The data
    xs: number[];
    ys: number[];
    
    //Display
    xRange: [number, number];
    yRange: [number, number];
    color: string;
	
    //Event
    onClick: (x: number, y: number, index: number) => void;
}

데이터나 디스플레이 속성이 변경되는 것 처럼 필요할 때에만 차트를 다시 그리고, 이벤트 핸들러가 변경되면 다시 그릴 필요가 없다.

이 코드를 아래와 같이 최적화 할 수 있다

 

보수적 접근법

function shouldUpdate(
    oldProps: ScatterProps,
    newProps: ScatterProps
){
    let k: keyof ScatterProps;
    for(k in oldProps){
        if(oldProps[k] !== newProps[k]){
            if(k !== 'onClick') return true;
        }
    }
    return false;
}

props 얕은 비교를 진행해서 onClick 이 바뀐 것이 아니면 true 를 리턴한다. (onClick 은 익명함수로 매번 새로 선언되기 때문에 예외처리를 해준 것)

보수적 접근법을 이용하면 차트가 정확하지만 너무 자주 그려질 가능성이 있다. 

 

 

실패에 열린 접근법

function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps){
    return(
        oldProps.xs !== newProps.xs ||
        oldProps.ys !== newProps.ys ||
        oldProps.xRange !== newProps.xRange ||
        oldProps.yRange !== newProps.yRange ||
        oldProps.color !== newProps.color
        //(no check for onClick)
    )
}

하나씩 일일이 비교하고, onclick 에 대해서는 비교하지 않는다

실패에 열린 접근법은 차트를 불필요하게 다시 그리는 단점을 해결했지만 실제로 차트를 다시 그려야 할 경우에 누락되는 일이 생길 수도 있다. ScatterProps 객체에 새로운 속성이 추가되면 그 속성은 변경을 감지할 수 없기 때문이다. 

 

보수적 접근법과 실패에 열린 접근법 모두 이상적이지 않다.

아래는 타입 체커가 동작하도록 개선한 코드이다. 핵심은 매핑된 타입과 객체를 사용하는 것이다. 

const REQUIRES_UPDATE: { [k in keyof ScatterProps]: boolean } = {
  xs: true,
  ys: true,
  xRange: true,
  yRange: true,
  color: true,
  onClick: false,
}

[k in keyof ScatterProps] 는 REQUIRES_UPDATE 객체는 ScatterProps 와 동일한 속성을 가져야 한다는 정보를 제공한다. 만약 ScatterProps 속성이 변경된다면 이를 타입체커가 감지해서 오류를 발생시켜준다. 이런 방식은 오류를 정확히 잡아낸다. 

 

function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps) {
  let k: keyof ScatterProps
  for (k in oldProps) {
    if (oldProps[k] !== newProps[k] && REQUIRES_UPDATE[k]) {
      return true
    }
  }
  return false
}

oldProps 와 newProps 값이 다르고 REQUIRES_UPDATE 객체에서 k 속성의 값이 true 인 경우에만 업데이트를 한다

 

 

요약

  • 매핑된 타입을 사용해서 관련된 값과 타입을 동기화하도록 한다.
  • 인터페이스에 새로운 속성을 추가할 때, 선택을 강제하도록 매핑된 타입을 고려해야 한다