📎아이템 17 변경 관련된 오류 방지를 위해 readonly 사용하기
삼각수(1, 1+2, 1+2+3)를 출력하는 코드이다.
function printTriangles(n: number) {
  const nums = [];
  for (let i = 0; i < n; i++) {
    nums.push(i);
    console.log(arraySum(nums));
  }
}실행결과
> printTriangles(5)
0
1
2
3
4arraySum이 nums을 변경하지 않는다고 간주해서 문제가 발생했다. 다음과 같이 해결한다.
function arraySum(arr: number[]) {
  let sum = 0, num;
  while ((num = arr.pop()) !== undefined) {
    sum += num;
  }
  return sum;
}이 함수는 배열 안의 숫자들을 모두 합친다. 계산이 끝나면 원래 배열이 전부 비게 된다. 자바스크립트 배열은 내용을 변경할 수 있기 때문에, 타입스크립트에서도 오류 없이 통과하게 된다.
📍 readonly
오류의 범위를 좁히기 위해 arraySum이 배열을 변경하지 않는다는 선언을 해보자. readonly 접근 제어자를 사용하면 된다.
function arraySum(arr: readonly number[]) {
  let sum = 0, num;
  while ((num = arr.pop()) !== undefined) {
      // 'readonly number[]' 형식에 'pop'속성이 없다.
    sum += num;
  }
  return sum;
}⭐️ readonly number[]는 타입이고, number[]와 구분되는 몇 가지 특징이 있다.
- 배열의 요소를 읽을 수 있지만, 쓸 수는 없다. 
- length를 읽을 수 있지만, 바꿀 수는 없다. (배열 변경)
- 배열을 변경하는 - pop을 비롯한 다른 메서드를 호출할 수 없다.
number[]는 readonly number[]보다 기능이 많기 때문에, readonly number[]의 서브타입이 된다. 따라서 변경 가능한 배열을 readonly 배열에 할당할 수 있다. 하지만 반대는 불가능하다.
const a: number[] = [1, 2, 3];
const b: readonly number[] = a;
const c: number[] = b;
//    ~ Type 'readonly number[]' is 'readonly' and cannot be
//      assigned to the mutable type 'number[]'
// readonly number[] 타입은 readonly이므로 변경 가능한 number[] 타입에 할당될 수 없다.타입 단언문 없이 readonly 접근제어자를 제거할 수 있다면 readonly는 쓸모 없으므로 오류가 발생하는게 맞다.
⭐️ 매개변수를 readonly로 선언하면 다음과 같은 일이 생긴다.
- 타입스크립트는 매개변수가 함수 내에서 변경이 일어나는지 체크한다. 
- 호출하는 쪽에서는 함수가 매개변수를 변경하지 않는다는 보장을 받게 된다. 
- 호출하는 쪽에서 함수에 readonly 배열을 매개변수로 넣을 수도 있다. 
자바스크립트에서는 명시적으로 언급이 없지만, 함수가 매개변수를 변경하지 않는다고 가정한다.
→ 타입 체크에 문제를 일으킬 수 있다. (아이템30, 31) 명시적인 방법을 사용하는 것이 컴파일러와 사람에게 모두 좋다.
앞 예제를 고쳐보자. 배열을 변경하지 않으면 된다.
function arraySum(arr: readonly number[]) {
  let sum = 0;
  for (const num of arr) {
    sum += num;
  }
  return sum;
}⭐️ 만약 함수가 매개변수를 변경하지 않는다면 readonly로 선언해야한다.
더 넓은 타입으로 호출할 수 있고, 의도치 않은 변경이 방지된다.
readonly의 단점
매개변수가 readonly로 선언되지 않은 함수를 호출해야 할 경우
만약 함수가 매개변수를 변경하지 않고도 제어가 가능하다면 readonly로 선언하면 된다. 하지만, 어던 함수를 readonly로 만들면, 그 함수를 호출하는 다른 함수들도 readonly로 만들어야 한다. 
→ 인터페이스를 명확히 하고 타입 안정성을 높일 수 있다.
→ 다른 라이브러리에 있는 함수를 호출하는 경우, 타입 선언을 바꿀 수 없으므로 타입 단언문(as number[])을 사용해야 한다.
⭐️ readonly를 사용하면 지역 변수와 관련된 모든 종류의 변경 오류를 방지할 수 있다.
연속된 행을 가져와서 빈줄을 기준으로 구분되는 단락으로 나누는 기능을 하는 프로그램이다.
function parseTaggedText(lines: string[]) : string[][] {
    const para : string[][] = [];
    const currPara : string[] = [];
    const addPara = () => {
        if (currPara.length) {
            para.push(currPara)  // 오류발생지점 1 (배열의 참조가 삽입)
            currPara.length = 0; // 오류발생지점 2 (배열을 비움)
        }
    }
    for (const line of lines) {
        if (!line) {
            addPara();
        } else {
            currPara.push(line);
        }
    }
    addPara();
    return para;
}
// output : [[],[],[]]이 코드의 문제점은 별칭과 변경을 동시에 사용하여 발생했다.
currPara의 내용이 삽입되지 않고 배열의 참조가 삽입되었다. 따라서 currPara 의 길이가 0이 되면 para로 들어간 currPara의 길이 또한 0이 되어 빈 배열로 반환되었다. 따라서 이 코드는 새 단락을 para에 집어넣고 바로 지워 버린다.
currPara를 readonly로 선언하여 오류를 해결한다.
function parseTaggedText(lines: string[]) : string[][] {
    const para : string[][] = [];
    const currPara : readonly string[] = [];
    const addPara = () => {
        if (currPara.length) {
            para.push(currPara)
          	// 1. readonly string[]속성의 인수는 string[]에 할당할 수 없습니다.
            currPara.length = 0;
          	// 2. 읽기 전용 속성으로 'length'에 할당할 수 없습니다.
        }
    }
    for (const line of lines) {
        if (!line) {
            addPara();
        } else {
            currPara.push(line);
          	// 3. readonly string[] 형식에 push 속성이 없습니다.
        }
    }
    addPara();
    return para;
}currPara를 let으로 선언하고 변환이 없는 메서드를 사용함으로써 두 개의 오류를 고칠 수 있다.
let currPara: readonly string[] = [];
// ...
currPara = [];    // 배열을 비움
// ...
currPara = currPara.concat((line));push와 달리 concat은 원본을 수정하지 않고(3번 오류 해결), 새 배열을 반환한다. 선언부를 const에서 let으로 변경 후 (2번 오류 해결)readonly를 추가하여 한쪽의 변경 가능성을 또 다른 쪽으로 옮긴 것 이다.
currPara 변수는 가리키는 배열을 자유롭게 변경할 수 있지만, 그 배열 자체는 변경하지 못하게 된다.
⭐️ 1번 오류를 해결하는 3가지 방법
- currPara의 복사본을 만든다. 
paragraphs.push([...currPara]);currPara는 readonly로 유지되지만, 복사본은 원하는대로 변경이 가능하므로 오류가 사라진다.
- paragraphs를- readonly string[]의 배열로 변경한다.
const paragraphs: (readonly string[])[] = [];- 배열의 - readonly속성을 제거하기 위해 단언문을 사용한다.
paragraphs.push(currPara as string[]);⭐️ readonly는 얕게 동작한다는 것에 유의해야 한다.
readonly string[][] 에서 만약 객체의 readonly 배열이 있다면, 그 객체 자체는 readonly가 아니다.
const dates: readonly Date[] = [new Date()];
dates.push(new Date()); // 'readonly Date[]' 형식에 'push' 속성이 없습니다.
dates[0].setFullyear(2000); // OKReadonly 제너릭에도 해당된다.
interface Outer {
    inner: {
        x: number
    }
}
const o: Readonly<Outer> = {inner: {x:0}}
o.inner = {x:1}; // 읽기 전용 속성이기 때문에 'inner'에 할당할 수 없습니다.
o.inner.x = 1;
// 타입 별칭을 만들면 어떻게 동작했는 지 알 수 있다.
type T = Readonly<Outer>
// Type T = {
//     readonly inner: {
//         x:number;
//     }
// }중요한 점은 readonly 접근제어자는 inner에 적용되는 것이지 x는 아니라는 것이다. 제너릭을 만들면 깊은 readonly 타입을 사용할 수 있다. 그러나 제너릭은 만들기 까다로워 라이브러리를 사용하는 것이 낫다. (ts-essentials에 있는 DeepReadonly를 사용)
인덱스 시그니처에도 readonly를 사용할 수 있다. 읽기는 허용하되 쓰기를 방지하는 효과가 있다.
📍 요약
- 만약 함수가 매개변수를 수정하지 않는다면, readonly로 선언하는 것이 좋다. readonly 매개변수는 인터페이스를 명확하게 하며, 매개변수가 변경되는 것을 방지한다. 
- readonly를 사용하면 변경하면서 발생하는 오류를 방지할 수 있고, 변경이 발생하는 코드도 쉽게 찾을 수 있다. 
- const와 readonly의 차이를 이해해야 한다. 
- readonly는 얕게 동작한다. 
Last updated