10강: 컨텍스트 패러다임: 컨텍스트 타입의 통일
한 줄 요약: 컨텍스트 패러다임(Context Paradigm)은 「내가 어디에 있는가(현재 상태), 내가 무엇을 바꿨는가(역함수), 내가 무엇을 필요로 하는가(의존성 테이블)」이라는 세 가지를 모두 하나의 재귀 가능한 ctx 엔티티에 담습니다. 이를 통해 컴포넌트의 「플러그인·플러그아웃」은 단순한 비유에서 실제로 구현할 수 있는 구조로 바뀌고, 실행 취소와 재연결의 정확성은 더 이상 개발자의 자율이 아니라 구조 자체에 의해 보장됩니다.
1단계: 하나의 ctx에 세 가지를 담는다——「내가 어디에 있는가, 무엇을 바꿨는가, 무엇을 필요로 하는가」
먼저 앞 레슨들의 두 가지 주요 흐름을 떠올려 봅시다.
- 이펙트(effect): 컴포넌트가 작업할 때 남기는 부수 효과(파일 변경, 프로세스 시작……). 실행 취소가 가능해야 합니다.
- 코이펙트(coeffect): 컴포넌트가 작업할 때 필요로 하는 것들(어떤 서비스, 어떤 설정……). 자동으로 연결되어야 합니다.
이전에는 이 둘을 각각 다른 두 가지 메커니즘으로 처리했습니다. 이번 절의 논문은 과감한 통일을 시도합니다. 이펙트, 코이펙트, 그리고 「현재 상태」를 모두 같은 엔티티에 넣는 것입니다. 바로 재귀적인 컨텍스트 타입입니다.
정의 24(컨텍스트 타입): Γ∞ ≔ μΓ. Γ × (Γ → Γ) × Σ
이 수식을 읽지 못해도 괜찮습니다. 뜯어 보면 한 문장입니다. 하나의 ctx 안에 세 가지가 동시에 담겨 있다는 것입니다.
| 구성 요소 | 담고 있는 것 | 쉬운 말로 | 한 줄 비유 |
|---|---|---|---|
| Γ | 현재 컨텍스트 상태(재귀적) | 내가 지금 어디에 있는가, 세상이 지금 어떻게 보이는가 | 현재 위치 |
| Γ → Γ | 누적 역함수 | 내가 그동안 무엇을 바꿨는가, 어떻게 한 단계씩 되돌아갈 것인가 | 실행 취소 로그 |
| Σ | 의존성 테이블 | 내가 지금 필요로 하는 것들 | 장보기 목록 |
세 가지 구성 요소를 합치면, 컴포넌트가 환경과 상호작용하기 전에 매번 답해야 하는 세 가지 질문이 됩니다. 나는 어디에 있는가? 내가 무엇을 바꿨는가? 나에게 아직 무엇이 부족한가? 모든 상호작용은 이 하나의 ctx 엔티티만을 거칩니다.
一个 ctx 同时记住「我在哪、我改了什么、我需要什么」——效应与余效应合体
왜 「재귀」라고 부를까요? 첫 번째 구성 요소 Γ 안에 담긴 것도 여전히 같은 Γ이기 때문입니다. 러시아 인형(마트료시카)처럼 모든 층이 같은 종류의 것이므로 원하는 만큼 깊이 중첩할 수 있습니다. 이처럼 「자기가 자기를 품는」 구조에는 전용 명칭이 있습니다. 바로 자기 유사성입니다. 논문 앞부분에서는 이펙트를 점점 높아지는 「𝜕 타워」로 추상화했는데, 여기서는 재귀를 사용해 타워 전체를 하나의 타입으로 평탄화했습니다.
한 단계 더 깊이 들여다보면, 이 설계는 두 가지 추가 이점도 가져옵니다.
- 이펙트가 ctx 위의 자기 준동형(엔도모피즘)이 된다: 한 번의 이펙트란 「ctx 하나를 가지고 들어가서, 새 ctx와 역함수 하나를 가지고 나오는 것」입니다. 들어가고 나오는 것이 같은 타입이므로 어떤 이펙트든 자유롭게 이어 붙일 수 있습니다.
- Σ는 모든 공유 상태를 담을 수 있다: Σ가 기반으로 하는 타입에 제한이 없기 때문에, 컴포넌트 간에 공유하고 싶은 어떤 전역 상태든 Σ 안의 타입이 있는 의존성 항목으로 인코딩할 수 있습니다. 다시 말해 Σ가 다루는 범위는 「컴포넌트 간 의존성」뿐 아니라 모든 공유 가변 상태입니다.
2단계: 계층적 합성——플러그인·플러그아웃은 정말 「꽂기」와 「뽑기」뿐이다
지난 레슨(컴포넌트 생명주기)에서 「컴포넌트는 플러그처럼 꽂고 뽑을 수 있다」고 말했습니다. 이제 Γ∞가 생겼으니, 이 비유는 마침내 수사에서 구조로 바뀝니다.
ctx는 재귀적이므로, 자식 컴포넌트의 ctx는 자연스럽게 부모 컴포넌트의 ctx 안에 중첩됩니다. 부모 컨텍스트가 여러 자식 계층의 이펙트를 집계하여 트리 형태의 제어 구조를 형성합니다.
┌───────────────┐
│ 부모 컨텍스트 │ ← 모든 자식 컴포넌트의 이펙트를 집계·관리
└───────┬───────┘
┌───────────┼───────────┐
▼ ▼ ▼
[컴포넌트 A] [컴포넌트 B] [컴포넌트 C] ← 각각 독립적으로 꽂고 뽑으며, 서로 영향을 주지 않음
「플러그인·플러그아웃」은 두 가지 연산으로 직접 구현됩니다.
| 연산 | 하는 일 | 쉬운 말로 |
|---|---|---|
| 컴포넌트 마운트 | 그 이펙트를 실행한다(삽입) | 플러그를 꽂으면 효과가 적용된다 |
| 컴포넌트 언마운트 | 그 이펙트를 복원한다(뽑기) | 플러그를 뽑으면 효과가 취소된다 |
이 설계에는 세 가지 핵심 보장이 있습니다.
- 뽑아도 다른 것에 영향을 주지 않는다: 컴포넌트를 언마운트하면 그 컴포넌트 자신의 이펙트만 복원되며, 실행 중인 다른 컴포넌트에는 전혀 영향을 주지 않습니다.
- 계층끼리 서로 간섭하지 않는다: 트리의 서로 다른 계층에 있는 컴포넌트는 각각 독립적으로 마운트·언마운트할 수 있으며, 전역적으로 통일된 순서가 필요하지 않습니다.
- 임의의 깊이로 중첩 가능: 부모 컨텍스트는 모든 자식 컴포넌트의 이펙트를 집계·관리하고, 자식 컴포넌트는 다시 자식 컴포넌트를 품을 수 있습니다. 몇 겹이든 원하는 대로 중첩할 수 있습니다.
🎁 비유하자면: 부모 컨텍스트는 「여러 콘센트가 달린 멀티탭」과 같습니다. 기기 하나를 뽑아도 다른 기기에는 그대로 전원이 공급되고, 멀티탭 자체를 또 다른 멀티탭에 꽂아 무한히 연장할 수도 있습니다.
3단계: 같은 이펙트, 두 가지 구현——인플레이스냐 파생이냐
논문은 여기서 매우 중요한 개념의 구분을 합니다. 지시(denotation)와 구현(implementation)의 분리입니다.
어떤 연산을 Γ∞ 위의 이펙트로 타입을 매기면, 고정되는 것은 그 지시입니다. 즉 「후속 상태 하나 + 대응하는 역함수 하나」입니다. 하지만 구현은 고정되지 않습니다. 그 역함수를 실제로 어떻게 실행할지는 구현 쪽에서 결정합니다.
정의 25(이펙트 함수의 두 가지 구현): 이펙트 함수 f는 두 가지 구현 방식을 허용합니다.
| 인플레이스 구현(in-place) | 파생 구현(derived) | |
|---|---|---|
| 컨텍스트 | 원래 컨텍스트를 수정하고, 후속 상태는 입력의 별칭이 된다 | 입력을 변경하지 않고, 재귀 구조 안에서 새 컨텍스트를 반환한다 |
| 역함수 | 비자명한 역함수를 반환한다(변경 내용을 실제로 기록함) | 항등 함수를 반환한다(아무것도 바꾸지 않았으므로 취소할 것이 없음) |
| 복원 방식 | 역함수를 실행하여 방금 한 수정을 되돌린다 | 파생된 새 컨텍스트를 폐기한다 |
| 비유 | 계약서 원본에 직접 수정하고, 매 변경을 실행 취소 목록에 기록하며, 되돌릴 때는 목록을 역순으로 따라가며 고친다 | 사본을 한 부 뜨고 사본에만 수정하며 원본은 건드리지 않는다. 필요 없어지면 사본을 파쇄기에 넣는다 |
「지시」와 「구현」의 구분에 주의하세요. 같은 이펙트라도 지시는 고정(후속 상태 + 역함수)이지만, 두 구현 중 하나를 자유롭게 선택할 수 있습니다. 어느 쪽을 고를지는 호스트 환경에 따라 달라집니다.
- 순수 함수형 환경에서는 「인플레이스 수정」이라는 것 자체가 없으므로, 두 구현이 서로 일치합니다(둘 다 파생만 가능합니다).
- 명령형 호스트에서는 개발자가 연산 단위로 자유롭게 선택할 수 있습니다. 빠르게 바꾸고 싶으면 인플레이스를, 원본 데이터를 건드리기 싫으면 파생을 사용합니다.
💡 논문은 4.1.2절에서 두 구현의 대표적인 작성법을 제공한다고 말합니다. 다음 레슨에서 그것들이 어떤 모습인지 보게 될 것입니다.
4단계: 왜 이것이 하나의 「패러다임」일까?——두 가지 낡은 사고방식과의 비교
논문의 야심은 「타입 하나를 제시하는 것」에 그치지 않고, 이 컨텍스트 타입 자체가 하나의 프로그래밍 패러다임을 이룬다고 주장하는 데 있습니다. 이를 이해하려면 먼저 두 가지 낡은 패러다임이 부수 효과를 어떻게 처리해 왔는지 살펴야 합니다. 둘은 같은 스펙트럼의 양극에 서 있습니다.
극좌: 명시적 상태 전달(함수형)
순수 함수형 언어는 참조 투명성을 유지하기 위해 부수 효과를 상태에 대한 명시적 변환으로 모델링합니다. 고전적인 예는 상태 모나드 S → (A, S)로, 환경을 모든 계산에 관통시킵니다.
- ✅ 장점: 이펙트가 타입에 드러나므로 등식 추론이 가능하고 추적 가능성이 매우 뛰어나다.
- ❌ 대가: 호출 체인의 모든 함수가 상태 매개변수를 받고 반환해야 합니다. 상태를 그대로 아래로 넘기기만 하더라도 마찬가지입니다. 이펙트 차원이 늘어나면(로깅, 설정, I/O) 모나드 스택이나 이펙트 핸들러의 보일러플레이트가 급격히 팽창합니다.
🎁 비유하자면: 회사가 「모든 문서는 모든 직원의 손을 거쳐 전원의 서명을 받아야 한다」고 규정한 것과 같습니다. 그저 전달만 하더라도 마찬가지입니다. 추적은 되지만, 진이 빠집니다.
극우: 암묵적 변경(명령형 / OOP)
주류 명령형 언어는 컴포넌트가 공유 상태를 직접 수정하고 의존성을 직접 가져올 수 있게 하며, 호출 지점에서는 아무것도 선언할 필요가 없습니다.
- ✅ 장점: 작성하기 편하고 인체공학성이 매우 뛰어나다.
- ❌ 대가: 추적 불가능. 논문은 두 가지 생생한 예를 듭니다.
- 이펙트 측: React의
useEffect훅——컴포넌트 내부의 파이버 위에 지속적인 부수 효과를 등록하지만, 이펙트 대상과 등록 메커니즘 모두 명시적 매개변수가 아니며, 그 정체성은 런타임 상태에 숨겨진 호출 순서상의 위치로 결정됩니다. - 코이펙트 측: Java의 서비스 로케이터(예: Spring의
getBean(...))——전역 레지스트리에서 의존성을 가져오고, 모든 호출 지점에서 null 체크와 타입 캐스팅을 해야 하며, 의존 관계는 코드베이스 전체에 암묵적으로 흩어져 있습니다.
- 이펙트 측: React의
더 나쁜 것은, f()가 시스템의 무엇을 바꾸고 무엇에 의존하는지 이해하려면 호출 관계를 따라 그 구현을 재귀적으로 읽어야 한다는 점입니다. 리팩터링도 그만큼 취약해집니다. 어떤 호출을 옮기거나 삭제하는 것만으로도 멀리 떨어진 불변 조건을 소리 없이 망가뜨릴 수 있습니다.
🎁 비유하자면: 공용 칠판으로, 누구나 쓸 수 있지만 아무도 이름을 남기지 않는 것과 같습니다. 편리하지만, 사고가 나면 누가 바꿨는지 찾을 수 없습니다.
중간: 컨텍스트 패러다임——두 가지 장점을 모두 취한다
컨텍스트 패러다임은 이 양극을 꿰매어 잇습니다. 이펙트와 코이펙트 모두 명시적인 컨텍스트 매개변수를 통해 매개됩니다. 따라서:
- 모든 연산은 그것을 호출한 ctx에 귀속되고, 나아가 그 ctx를 소유한 컴포넌트에 귀속됩니다. 함수형처럼 추적 가능합니다.
- 개발자는 호출 체인에 상태 매개변수를 잔뜩 손으로 적을 필요가 없습니다. 명령형처럼 간결합니다.
그리고 이것은 단순한 「절충」이 아니라, 정확성 보장까지 한 단계 업그레이드합니다.
| 상황 | 기존 방식(자율에 의존) | 컨텍스트 패러다임(구조에 의존) |
|---|---|---|
| 롤백 가능한 이펙트 | 모든 복합 연산마다 개발자가 직접 실행 취소 로직을 작성해야 함 | 각 원자 연산에 역함수만 제공하면, 복합 연산의 역함수는 합성에 의해 자동으로 얻어진다——마운트와 해체가 구조적으로 가역적이 된다 |
| 반응형 코이펙트 | 의존성이 제대로 연결되었는지는 전적으로 주의력에 달림 | 컴포넌트는 필요한 의존성만 선언하면 런타임이 자동으로 해석하고 재연결한다——프로바이더가 추가·제거·교체되어도 연결은 항상 올바르게 유지된다 |
두 방향을 합치면 이 절의 핵심 결론이 됩니다. 원래 개발자의 자율로 보장해야 했던 정확성이, 이 패러다임의 구조적 속성이 된 것입니다.
핵심 정리
정보량이 적지 않았지만, 이 다섯 문장만 기억하면 됩니다.
- 통일 컨텍스트: Γ∞ ≔ μΓ. Γ × (Γ → Γ) × Σ——하나의 ctx가 「현재 상태 Γ」「누적 역함수 Γ → Γ」「의존성 테이블 Σ」를 동시에 담는다. 즉 「내가 어디에 있는가, 무엇을 바꿨는가, 무엇을 필요로 하는가」.
- 재귀적 자기 유사성: Γ 안에 든 것도 Γ로, 러시아 인형처럼 원하는 만큼 깊이 중첩할 수 있다. 이펙트는 ctx 위의 자기 준동형이 되고(ctx가 들어가고, ctx와 역함수가 나옴), Σ는 모든 공유 가변 상태를 인코딩할 수 있다.
- 계층적 합성: 부모 컨텍스트는 자식 이펙트를 집계해 트리 구조를 형성한다. 마운트 = 이펙트 삽입, 언마운트 = 뽑기와 복원. 서로 영향을 주지 않고 임의 깊이의 중첩을 지원한다.
- 지시와 구현의 분리: 이펙트는 「지시」(후속 상태 + 역함수)를 고정하고 구현은 고정하지 않는다——인플레이스 구현(ctx 수정 + 비자명한 역) 또는 파생 구현(새 ctx 반환 + 항등 역, 복원 = 폐기).
- 패러다임으로서의 위치: 함수형의 명시적 상태 전달은 추적 가능하지만 보일러플레이트가 많고, 명령형의 암묵적 변경은 사용하기 편하지만 추적이 불가능하다. 컨텍스트 패러다임은 명시적 ctx로 두 가지를 모두 얻어, 정확성을 「개발자의 자율」에서 「구조적 보장」으로 바꾼다.
🚀 탁상공론은 여기까지입니다. 다음 레슨은 논문 4장 「구현과 사례 연구」로 들어갑니다. Cordis 코어 라이브러리에서 Γ∞가 어떻게 실제 코드가 되는지, 인플레이스 / 파생 두 구현이 실제로 어떻게 작성되는지 살펴야겠습니다.
셀프 체크 · 컨텍스트 패러다임
답을 모두 선택한 후 「답안 제출」을 클릭하면 정답 여부와 해설을 확인할 수 있습니다.
