decide(tween): add Animate CanAnimate field, document Luau syntax facts
CanAnimate: State<boolean>|boolean|nil (nil defaults true) lets Animate
bypass wrapping in Tween{} entirely, covering the reduceMotion use case
natively. Also documents that Luau's if-then-else expression and const
bindings are official syntax (not hallucinated), so agents don't revert
them; const adoption deferred pending tooling support.
This commit is contained in:
parent
8fdb9f1bb2
commit
a1f8601cc7
4 changed files with 133 additions and 18 deletions
|
|
@ -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에 정식 도입된
|
||||
표현식 문법(공식 릴리스 노트: <https://luau.org/news/2021-10-31-luau-recap-october-2021/#if-then-else-expression>)
|
||||
— `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 공식 문법**(<https://luau.org/syntax/#const-bindings>)
|
||||
이지만 **지금은 채택하지 않음** — 타입 추출/narrowing 등 주변 툴링이
|
||||
아직 `const`를 폭넓게 지원하지 못해서, 지금 전면 도입하면 나중에 그
|
||||
간극을 메꾸는 비용이 더 클 수 있음. **원칙**: 새로 짜는 코드는 일단
|
||||
`local`로 — 나중 리팩터 시점에 특정 바인딩을 `const`로 바꾸는 비용이
|
||||
싸 보이면 그때 바꿔도 되고, 비싸 보이면 굳이 지금 손대지 않아도 됨.
|
||||
지금 `const`가 없다고 "이 프로젝트가 구식 Luau를 쓴다"고 오해하지
|
||||
말 것 — 툴링 성숙도 문제일 뿐 문법 자체를 모르거나 기각한 게 아님.
|
||||
|
||||
## 테스트 전략: quad-base용 최소 mock (2026-08-04)
|
||||
|
||||
**결정**: quad-base 테스트는 Vide 선례(`initreq/vide/test/mock.luau`, 약
|
||||
|
|
|
|||
|
|
@ -187,12 +187,13 @@ State<T | Tween<T>>`가 나옴 — Modifier/State/Source/StoreBind 코드엔
|
|||
얇은 sugar — 다시 보니 매우 단순해서(사용자 표현: "생각해보니 엄청
|
||||
간단하다") 다음 세션으로 미룰 이유가 없어 이번 세션에 바로 확정.
|
||||
|
||||
**모양**: `Tween`의 옵션(`Value` 제외 전부)을 그대로 받되, **각 필드가
|
||||
`T | State<T>`를 받을 수 있음** — `Tween{...}` 자신의 필드는 plain만
|
||||
받는 것과 대조적(위 "`Tween{...}`의 모든 필드는 plain 값만 받음" 절).
|
||||
모순이 아님: `Animate`가 반환하는 함수 안에서 각 필드를 **`:Get()`으로
|
||||
한 번 풀어 plain 값으로 만든 뒤에만** `Tween{...}`에 넘기므로, `Tween`
|
||||
쪽 불변식(plain-only)은 그대로 유지됨.
|
||||
**모양**: `Tween`의 옵션(`Value` 제외 전부) + `Animate` 전용 필드
|
||||
`CanAnimate`(아래 절) 하나를 더해서 받되, **각 필드가 `T | State<T>`를
|
||||
받을 수 있음** — `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> | boolean | nil`** — 애니메이션 자체를 켜고
|
||||
끄는 필드, **생략(`nil`)이면 기본 `true`**(항상 애니메이션). `false`로
|
||||
resolve되면 `Tween{...}`으로 안 감싸고 `self:Get()`을 그대로 반환 —
|
||||
reduceMotion류 접근성 우회가 이 필드 하나로 바로 표현됨:
|
||||
|
||||
```lua
|
||||
-- reduceMotion: State<boolean>
|
||||
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<boolean>
|
||||
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)
|
||||
|
|
|
|||
49
.claude/session/2026-08-12-03-cananimate-luau-syntax-note.md
Normal file
49
.claude/session/2026-08-12-03-cananimate-luau-syntax-note.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<!-- quad-v2 세션 로그 원문 — CLAUDE.md에서 이전됨(2026-08-11 정리 세션에서 확립된 관례를 따름). -->
|
||||
<!-- 이 파일은 quadnomicon 개발로그 소재용 원자료로, 당시 시행착오(정정 전 서술 포함)를 그대로 보존함. -->
|
||||
<!-- 현재 유효한 설계는 이 파일이 아니라 base//research//archive/가 최종 소스 — 이 파일 안의 판단이 이후 세션에서 뒤집혔을 수 있음. -->
|
||||
|
||||
## 2026-08-12 세 번째 세션 — `Animate`에 `CanAnimate` 필드 추가, Luau 문법(`if-then-else`/`const`) 공식성 문서화
|
||||
|
||||
**`Animate(info)`에 `CanAnimate: State<boolean> | 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에 정식 도입된 공식 표현식
|
||||
문법(사용자가 릴리스 노트 링크 직접 제공:
|
||||
<https://luau.org/news/2021-10-31-luau-recap-october-2021/#if-then-else-expression>).
|
||||
`architecture.md`의 "코드 스타일 — 네이밍 케이싱" 절 바로 뒤에 "코드
|
||||
스타일 — Luau 문법 관례" 절을 신설해 이 사실+이 프로젝트의 기본 삼항
|
||||
표현 방식이 `if-then-else`라는 것(`and`/`or`는 가운데 값이 항상-truthy로
|
||||
보장될 때만 예외 허용)을 명문화. 같이 언급된 `const` 바인딩
|
||||
(<https://luau.org/syntax/#const-bindings>)도 공식 문법이지만 **지금은
|
||||
채택 보류** — 타입 추출/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/`
|
||||
결과 확인 우선).
|
||||
|
|
@ -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>|boolean|nil` 필드
|
||||
추가(`nil`=기본 `true`, `false`면 `Tween`로 안 감싸고 plain 값 그대로 —
|
||||
reduceMotion류 우회가 이걸로 표현됨). `base/architecture.md`에 "코드
|
||||
스타일 — Luau 문법 관례" 절 신설 — `if-then-else`(2021년 정식 도입)와
|
||||
`const` 바인딩 둘 다 공식 Luau 문법임을 명문화(에이전트가 모르고
|
||||
`and`/`or`로 되돌리는 회귀 방지), `const`는 툴링 미성숙으로 지금은 보류.
|
||||
|
|
|
|||
Loading…
Reference in a new issue