diff --git a/.claude/base/architecture.md b/.claude/base/architecture.md index 36f2dc2..6d60f19 100644 --- a/.claude/base/architecture.md +++ b/.claude/base/architecture.md @@ -220,6 +220,26 @@ Tween/existing-instance-bind는 여전히 `research/`에 남아있고 이 구조 `base/bind-system-plan.md`의 "Dispatch는 프리미티브가 아니다" 절/ `base/store-semantics.md`의 "세 번째 카테고리 — Handler" 절 참고. +## 코드 스타일 — Luau 문법 관례: `if-then-else`/`const` (2026-08-12 세션 신설) + +**`if cond then a else b` 표현식은 Luau 공식 문법이다 — 환각/오타로 +간주해 `and`/`or`로 "고치지" 말 것.** 2021년 10월 Luau에 정식 도입된 +표현식 문법(공식 릴리스 노트: ) +— `cond and truthyOnly or fallback` 삼항 관용구와 달리 가운데 값이 +falsy(`nil`/`false`)여도 정확하게 동작함(`bind-system-plan.md`의 +`Dispatch.retractUnder` 정정 사례가 실제 버그 예시). **이 프로젝트의 +기본 삼항 표현 방식은 `if-then-else`** — `cond and x or y`는 `x`가 +테이블/숫자처럼 항상-truthy임이 보장될 때만 예외적으로 허용. + +**`const` 바인딩도 Luau 공식 문법**() +이지만 **지금은 채택하지 않음** — 타입 추출/narrowing 등 주변 툴링이 +아직 `const`를 폭넓게 지원하지 못해서, 지금 전면 도입하면 나중에 그 +간극을 메꾸는 비용이 더 클 수 있음. **원칙**: 새로 짜는 코드는 일단 +`local`로 — 나중 리팩터 시점에 특정 바인딩을 `const`로 바꾸는 비용이 +싸 보이면 그때 바꿔도 되고, 비싸 보이면 굳이 지금 손대지 않아도 됨. +지금 `const`가 없다고 "이 프로젝트가 구식 Luau를 쓴다"고 오해하지 +말 것 — 툴링 성숙도 문제일 뿐 문법 자체를 모르거나 기각한 게 아님. + ## 테스트 전략: quad-base용 최소 mock (2026-08-04) **결정**: quad-base 테스트는 Vide 선례(`initreq/vide/test/mock.luau`, 약 diff --git a/.claude/research/tween-plan.md b/.claude/research/tween-plan.md index 766eaec..67c3bde 100644 --- a/.claude/research/tween-plan.md +++ b/.claude/research/tween-plan.md @@ -187,12 +187,13 @@ State>`가 나옴 — Modifier/State/Source/StoreBind 코드엔 얇은 sugar — 다시 보니 매우 단순해서(사용자 표현: "생각해보니 엄청 간단하다") 다음 세션으로 미룰 이유가 없어 이번 세션에 바로 확정. -**모양**: `Tween`의 옵션(`Value` 제외 전부)을 그대로 받되, **각 필드가 -`T | State`를 받을 수 있음** — `Tween{...}` 자신의 필드는 plain만 -받는 것과 대조적(위 "`Tween{...}`의 모든 필드는 plain 값만 받음" 절). -모순이 아님: `Animate`가 반환하는 함수 안에서 각 필드를 **`:Get()`으로 -한 번 풀어 plain 값으로 만든 뒤에만** `Tween{...}`에 넘기므로, `Tween` -쪽 불변식(plain-only)은 그대로 유지됨. +**모양**: `Tween`의 옵션(`Value` 제외 전부) + `Animate` 전용 필드 +`CanAnimate`(아래 절) 하나를 더해서 받되, **각 필드가 `T | State`를 +받을 수 있음** — `Tween{...}` 자신의 필드는 plain만 받는 것과 대조적(위 +"`Tween{...}`의 모든 필드는 plain 값만 받음" 절). 모순이 아님: `Animate`가 +반환하는 함수 안에서 각 필드를 **`:Get()`으로 한 번 풀어 plain 값으로 +만든 뒤에만** `Tween{...}`에 넘기므로, `Tween` 쪽 불변식(plain-only)은 +그대로 유지됨. ```lua local function resolve(v) @@ -205,8 +206,18 @@ end local function Animate(info) return function(self) + local v = self:Get() + + local canAnimate = resolve(info.CanAnimate) + if canAnimate == nil then + canAnimate = true -- CanAnimate 생략 시 기본 애니메이션 활성 + end + if not canAnimate then + return v -- Tween로 안 감쌈 — 그대로 plain 값 반환(애니메이션 우회) + end + return Tween{ - Value = self:Get(), + Value = v, Info = resolve(info.Info), Time = resolve(info.Time), Style = resolve(info.Style), @@ -221,9 +232,34 @@ end ``` `resolve`가 `and`/`or` 삼항 관용구가 아니라 `if-then-else`인 이유는 -`base/bind-system-plan.md`의 2026-08-12 정정 노트와 같음 — `Override` -등 필드가 `false`일 수 있는 값이면 `isState(v) and v:Get() or v` 식은 -`v:Get()`이 falsy일 때 조용히 `v`(State 객체 자신)로 새는 버그가 됨. +`base/architecture.md`의 "코드 스타일 — Luau 문법 관례" 절과 같음 — +`Override`/`CanAnimate` 등 필드가 `false`일 수 있는 값이면 +`isState(v) and v:Get() or v` 식은 `v:Get()`이 falsy일 때 조용히 +`v`(State 객체 자신)로 새는 버그가 됨. + +**`CanAnimate: State | boolean | nil`** — 애니메이션 자체를 켜고 +끄는 필드, **생략(`nil`)이면 기본 `true`**(항상 애니메이션). `false`로 +resolve되면 `Tween{...}`으로 안 감싸고 `self:Get()`을 그대로 반환 — +reduceMotion류 접근성 우회가 이 필드 하나로 바로 표현됨: + +```lua +-- reduceMotion: State +Position = mySource:Compute(Animate{ + Style = Enum.EasingStyle.Bounce, + CanAnimate = reduceMotion:Compute(function(r) return not r end), +}) +``` + +이 필드도 다른 옵션들과 동일하게 값 자체가 State로 바뀌는 것만으로는 +재계산을 트리거하지 않음(`Value`가 실제로 바뀌는 다음 재계산 때 그 +시점의 최신 `CanAnimate`가 자연히 반영) — 위 "`Style`/`Override` 등이 +State여도..." 절과 같은 이유. + +**필드 이름 케이싱 메모**: 대화 중엔 `canAnimate`(소문자 시작)로 +나왔으나, 같은 옵션 테이블의 나머지 필드가 전부 `Value`/`Style`/`Time` +같은 PascalCase(Roblox 프로퍼티/`Tween` opts 관례를 그대로 따름)라 여기선 +`CanAnimate`로 통일 — 이 필드 하나만 다른 케이싱을 쓸 특별한 이유가 +없다고 판단(확정은 아님, 다음 세션에 뒤집혀도 비용 낮음). **왜 `:Compute`에 직접 넘길 수 있는가**: `Animate(info)`가 반환하는 `function(self) ... end`는 `:Compute(fn)`의 콜백 시그니처(`fn(self, @@ -249,17 +285,18 @@ trailing-deps로 선언 안 됨). 그래서 `Style`이 바뀌어도 그 자체 정확히 일치하는 동작이라 별도 트리거 배선이 오히려 불필요한 복잡도였을 것. -**구 `useTween`(reduceMotion 우회) 스케치는 이 설계로 대체됨** — 이전엔 -`Animate(cond, opts)`처럼 조건 인자를 받아 내부에서 plain/Tween 분기하는 -전용 2-인자 시그니처를 검토했으나, 새 `Animate(info)`는 그 조건 분기를 -아예 안 가짐(단일 책임 유지). 우회가 필요하면 `Animate`를 감싸는 평범한 -`:Compute` 클로저로 여전히 표현 가능 — 새 프리미티브 불필요: +**구 `useTween`(reduceMotion 우회) 스케치는 `CanAnimate` 필드로 대체됨** +(위 "`CanAnimate`" 절) — 흔한 단순 토글은 그걸로 충분. 이전에 검토했던 +전용 2-인자 시그니처(`Animate(cond, opts)`)는 안 씀 — `Animate(info)`는 +조건 인자를 별도로 안 가지는 단일 진입점 유지. `CanAnimate`로 못 담는 +더 복잡한 조건(값 자체를 다른 값으로 바꿔치기하는 등)이 생기면, `Animate`를 +감싸는 평범한 `:Compute` 클로저로도 여전히 표현 가능 — 새 프리미티브 +불필요: ```lua --- reduceMotion: State Position = mySource:Compute(function(self) - if reduceMotion:Get() then - return self:Get() + if someComplexCondition() then + return someOtherValue end return Animate{Style = Enum.EasingStyle.Bounce}(self) end) diff --git a/.claude/session/2026-08-12-03-cananimate-luau-syntax-note.md b/.claude/session/2026-08-12-03-cananimate-luau-syntax-note.md new file mode 100644 index 0000000..ae9bd34 --- /dev/null +++ b/.claude/session/2026-08-12-03-cananimate-luau-syntax-note.md @@ -0,0 +1,49 @@ + + + + +## 2026-08-12 세 번째 세션 — `Animate`에 `CanAnimate` 필드 추가, Luau 문법(`if-then-else`/`const`) 공식성 문서화 + +**`Animate(info)`에 `CanAnimate: State | boolean | nil` 필드 +추가** — 앞선 두 세션(`2026-08-12-01`/`02`)에서 `Animate`를 확정한 직후 +사용자가 "아 맞다"며 빠뜨렸던 필드를 짚음. **`nil`이면 기본 `true`**(항상 +애니메이션), `false`로 resolve되면 `Tween{...}`으로 안 감싸고 `self:Get()`을 +그대로 반환 — reduceMotion류 접근성 우회가 이 필드 하나로 표현됨. 앞선 +세션에서 "구 `useTween` 우회는 수동 `:Compute` 클로저로도 여전히 +가능하다"고 남겨뒀던 예시가 사실상 `Animate` 안으로 흡수됨 — 수동 클로저는 +`CanAnimate`로 못 담는 더 복잡한 조건(값 자체를 다른 값으로 바꿔치기 등)의 +탈출구로만 절 문구를 축소. 필드 케이싱은 대화 중 `canAnimate`(소문자)로 +나왔으나 같은 옵션 테이블의 나머지 필드가 전부 PascalCase(`Value`/`Style`/ +`Time`...)라 `CanAnimate`로 정규화 — 이 필드만 다른 케이싱을 쓸 이유가 +없다고 판단, 확정은 아니고 다음 세션에 뒤집혀도 비용 낮음. + +**Luau `if-then-else`/`const` 문법의 공식성을 `base/architecture.md`에 +명문화** — 사용자가 직접 짚은 우려: 앞선 세션에서 `and`/`or` 삼항 +관용구를 `if-then-else`로 고친 게(`bind-system-plan.md`의 +`Dispatch.retractUnder` 정정), 나중에 이 문법을 모르는 에이전트가 "이런 +문법 없음"이라며 `and`/`or`로 되돌리는 회귀를 부를 수 있음 — 실제로 +`if cond then a else b`는 2021년 10월 Luau에 정식 도입된 공식 표현식 +문법(사용자가 릴리스 노트 링크 직접 제공: +). +`architecture.md`의 "코드 스타일 — 네이밍 케이싱" 절 바로 뒤에 "코드 +스타일 — Luau 문법 관례" 절을 신설해 이 사실+이 프로젝트의 기본 삼항 +표현 방식이 `if-then-else`라는 것(`and`/`or`는 가운데 값이 항상-truthy로 +보장될 때만 예외 허용)을 명문화. 같이 언급된 `const` 바인딩 +()도 공식 문법이지만 **지금은 +채택 보류** — 타입 추출/narrowing 등 주변 툴링이 아직 폭넓게 지원 못 함, +지금 전면 도입하면 나중에 그 간극을 메꾸는 비용이 더 클 수 있음. 원칙: +새 코드는 일단 `local`, 나중 리팩터 시점에 특정 바인딩을 `const`로 +바꾸는 비용이 싸 보이면 그때 바꾸고 비싸면 안 바꿔도 됨 — "이 프로젝트가 +구식 Luau를 쓴다"는 오해 방지용으로 같이 문서화(툴링 성숙도 문제일 뿐 +문법을 모르거나 기각한 게 아님). + +**반영된 파일**: `research/tween-plan.md`(`Animate` 절에 `CanAnimate` +필드/예시/케이싱 메모 추가, "구 useTween 스케치" 절 축소), `base/ +architecture.md`(신규 "코드 스타일 — Luau 문법 관례" 절). + +**여전히 열려있는 것**: 안 바뀜 — 자연 완료(Completed) 시 per-instance +북키핑 정리 여부 하나(`research/pre-implementation-audit.md` 2-10번, +M11 착수 시). + +**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, `.claude/luau-test/` +결과 확인 우선). diff --git a/CLAUDE.md b/CLAUDE.md index 5a20f71..4020d9d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -449,3 +449,12 @@ Luau `if-then-else` 표현식을 언급하며 `.claude/base` 전역 and/or 삼 falsy-값 버그(`v`가 `false`일 때 `nil`로 새는 문제) 발견·수정, 나머지 히트는 가운데 값이 테이블/숫자라 안전 확인. `research/tween-plan.md`는 이걸로 자연완료 북키핑 하나만 남기고 사실상 마감. + +**2026-08-12 세 번째 세션 — `Animate`에 `CanAnimate` 필드 추가, Luau 문법 공식성 문서화** +(`session/2026-08-12-03-cananimate-luau-syntax-note.md`) +`Animate(info)`에 빠져있던 `CanAnimate: State|boolean|nil` 필드 +추가(`nil`=기본 `true`, `false`면 `Tween`로 안 감싸고 plain 값 그대로 — +reduceMotion류 우회가 이걸로 표현됨). `base/architecture.md`에 "코드 +스타일 — Luau 문법 관례" 절 신설 — `if-then-else`(2021년 정식 도입)와 +`const` 바인딩 둘 다 공식 Luau 문법임을 명문화(에이전트가 모르고 +`and`/`or`로 되돌리는 회귀 방지), `const`는 툴링 미성숙으로 지금은 보류.