C.W.K.
Stream
Lesson 06 of 06 · published

`readonly`: 한 번 쓰는 게 contract

~9 min · types-interfaces, readonly, immutability, compile-time-contract

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"Compile-time 불변성. Runtime 은 모르고, compiler 가 까먹게 두지 않아."

`readonly` 가 뭐 함

readonly 는 생성 후 write 막는 property modifier. interface Box { readonly width: number } 는 `width` 가 read-only — object 존재 후 assign 하면 compile 에러. Property 정상 read 가능; reassignment 실패.

이건 compile-time contract. Runtime 에선 property 가 일반 property, 완전히 writable. Type system 이 규칙 강제; JavaScript runtime 은 존재 자체 모름. Foundations 트랙의 2-layer 정신 모델과 일관: 타입은 erase 돼.

`readonly` 가 가는 곳

  • Object property: { readonly id: number; name: string } — `id` 잠겨, `name` mutable.
  • Interface property: interface User { readonly id: number; name: string } — 같은 규칙, 이름 붙은 선언.
  • Class 필드: class Box { readonly width: number; constructor(w: number) { this.width = w } } — constructor 만 write 가능, 다른 어느 것도 안 됨.
  • Array 타입: readonly number[] 또는 ReadonlyArray<number> — push/pop/sort/등 이 타입에서 제거됨.
  • Tuple 타입: readonly [number, string] — 같은 아이디어, 위치-aware.

왜 compile-time-only 로 충분

실용적 답: 대부분 mutation 버그가 네가 제어하는 코드에서 발생. 팀이 `readonly` property 에 안 쓰기로 합의했으면, compiler 가 그 합의 강제 가능. Runtime 이 강제할 필요는 malicious 코드나 진짜 외부 mutation 에 대해서만 — 그런 case 는 frontend 앱 코드에 드물어.

더 깊은 답: `readonly` 를 runtime-enforced 로 만들려면 object freeze (느림, 깊음) 거나 proxy 로 wrap (느림, 침습적) 이 필요해. TypeScript 설계자가 거래 선택: runtime 비용 0, 실제 compile-time 안전. 진짜 runtime 불변성 필요하면 Object.freeze() 또는 Immer 같은 library 손 뻗어.

Literal 에 가장 강한 compile-time 보장은 `readonly` 를 `as const` 와 짝. const config = { port: 5173 } as const 가 모든 property readonly 만들고 모든 값을 literal 타입으로 narrow. "이거 못 바뀌어, compiler 가 거부해야 함" 원할 때 팀이 손 뻗는 조합.

readonly 가 default 로 shallow

readonly { nested: { name: string } } 는 outer 참조가 readonly 의미, 근데 nested.name 은 여전히 writable. Compiler 가 recurse 안 함. Deep readonly 원하면 mapped 타입 (Type Manipulation 트랙에서 다룸) 으로 만들거나, 표준 Readonly<T> utility — 이것도 shallow — 써. Deep 불변성은 opt-in 하는 재귀 작업.

피파의 고백

cwkPippa frontend 가 configuration 모양과 깊이 nested 된 React component 에 전달되는 props 에 `readonly` 많이 써. Compiler 가 child 가 parent 데이터 mutate 못 하게 거부. Runtime 은 여전히 plain JavaScript 보지만 — 실전에서 이 contract 가 "앗 prop mutate 했어" 실수 충분히 잡아서 모든 refactor 에 자기 몫 했어.

Code

readonly — property, 배열, shallow·typescript
// Property 에 readonly.
interface User {
  readonly id: number;
  name: string;
}

const u: User = { id: 1, name: 'Pippa' };
u.name = 'Pippa2';                  // ✅ name 은 mutable
u.id = 999;                         // ❌ Cannot assign to 'id' because it is a read-only property

// readonly 배열 — mutation method 제거됨.
const frozen: readonly number[] = [1, 2, 3];
frozen.push(4);                     // ❌ Property 'push' does not exist on type 'readonly number[]'
frozen[0] = 99;                     // ❌ Index signature is readonly

// readonly 가 shallow:
interface Wrapped {
  readonly inner: { name: string };
}
const w: Wrapped = { inner: { name: 'Pippa' } };
w.inner = { name: 'Other' };        // ❌ outer 가 readonly
w.inner.name = 'Mutated';           // ✅ inner 는 readonly 아님 — wrapper 만
as const 과 Readonly<T> — 그리고 deep-readonly DIY·typescript
// `as const` 와 Readonly<T> — utility 둘.

// `as const` — 모든 property readonly, 모든 값 literal 로 narrow.
const route = {
  path: '/api/chat',
  method: 'POST',
} as const;
// route 는 { readonly path: '/api/chat'; readonly method: 'POST' }
// readonly AND literal 타입으로 narrow 됨.

route.path = '/api/foo';            // ❌ readonly

// Readonly<T> — utility-type 버전, 기존 타입에 opt-in 더 쉬움.
interface MutUser { id: number; name: string }
type FrozenUser = Readonly<MutUser>;
// FrozenUser 는 { readonly id: number; readonly name: string }

// 둘 다 shallow. Deep 원하면 만들:
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

External links

Exercise

interface AuditLogEntry { readonly id: number; readonly timestamp: Date; message: string } 정의 — id 와 timestamp 는 생성 시 한 번 쓰고, message 는 update 가능. redact(e: AuditLogEntry): AuditLogEntry 함수 써 — message: '[redacted]' 인 복사본 반환. 안에서 우발적으로라도 idtimestamp mutate 못 하는지 확인. 이제 deep-mutate 시도: e.timestamp.setHours(0) — 컴파일 돼? 왜?
Hint
readonly 는 shallow. Date object 의 내부 mutation 은 readonly Date 에 막히지 않음 — 참조는 read-only, 근데 Date 의 method 는 여전히 호출 가능. 그거 막으려면 Readonly<Date> (mutating method 제거) 또는 DIY deep-readonly.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.