"읽기 전용은 값을 얼리는 주문이 아니라, 이 참조를 통해 바꾸지 않겠다는 계약이야."
속성의 쓰기를 막아
readonly id: number라고 선언하면 객체가 만들어진 뒤 그 속성에 다시 대입할 수 없어. 함수가 받은 설정이나 생성 뒤 바뀌면 안 되는 식별자처럼, 변경 권한을 줄 이유가 없는 값에 잘 맞아. 컴파일러가 우발적인 대입을 바로 막아 주지.
하지만 실행 중 객체를 얼리지는 않아. TypeScript 타입은 지워지므로 다른 느슨한 참조나 JavaScript 코드가 값을 바꿀 수도 있어. 실제 런타임 불변성이 필요하면 Object.freeze, 캡슐화, 불변 자료 구조 같은 별도 장치가 필요해.
얕은 불변성
readonly user: User는 user 속성 자체를 다른 User로 갈아 끼우지 못하게 할 뿐, User 안의 name까지 자동으로 읽기 전용으로 만들지 않아. 배열의 readonly User[]도 배열에 push하거나 인덱스를 바꾸는 것은 막지만 각 User 객체의 내부 변경까지 막지는 않아.
깊은 불변성이 필요하면 모든 층에 readonly를 적용하는 재귀 타입이나 실제 불변 객체 설계를 써야 해. 무조건 깊게 얼리면 업데이트 코드가 복잡해질 수 있으니 도메인이 요구하는 깊이를 먼저 정해.
함수 매개변수에서 특히 값져
배열을 읽기만 하는 함수는 readonly T[]를 매개변수로 받으면 좋아. 함수 몸에서 실수로 정렬하거나 원소를 추가하는 일을 막고, 호출자에게도 원본을 바꾸지 않겠다는 뜻을 전해. 변경 가능한 배열은 읽기 전용 매개변수에 넘길 수 있으므로 사용 범위도 넓어져.
Readonly 유틸리티와 조합
Readonly<T>는 T의 모든 최상위 속성에 readonly를 붙여. 설정 스냅샷이나 함수 입력을 파생할 때 유용해. 원본 타입을 다시 적지 않고 관계를 보존하므로 속성이 추가되어도 읽기 전용 버전이 함께 따라와.
readonly는 속성, 배열, 튜플, 매개변수 프로퍼티, 매핑 타입 등 여러 자리에 둘 수 있어. const config = { port: 5173 } as const는 값과 중첩 속성을 리터럴 읽기 전용으로 좁혀. 런타임 보호에는 Object.freeze나 Proxy가 필요하고, 깊은 불변 갱신에는 Immer 같은 도구를 선택할 수 있어.