nsgo은(는) 무엇을 하나요?
Go UI 엔진: 앱 로직, 반응형 그래프, 스타일, 레이아웃 및 텍스트는 Go에서 실행되며, 각 플랫폼은 자체 네이티브 뷰로 그립니다. 앱은 Go(또는 goja로 컴파일된 TSX)로 작성됩니다. 디자인, 이름 및 동작은 NSH(Rust)에서 포팅되었습니다.
철칙: API를 읽고, 절대 추측하지 마세요
NSGo는 사전 출시 버전이며, 안정적인 API가 없고 웹상에 공개 문서도 거의 없으므로 기억에 의존한 식별자는 거의 항상 틀립니다. 필드 이름 하나만 틀려도 Go 컴파일이 실패합니다. 앱을 작성하기 전에 모듈 자체에서 진실을 얻으세요:
go doc github.com/oneo-me/nsgo/core # 또는: go doc -all ./core
go doc github.com/oneo-me/nsgo/base
저장소를 체크아웃한 경우 ARCHITECTURE.md, docs/architecture/*.md (25개 주제 문서) 및 examples/가 권위 있는 출처입니다. examples/gallery는 모든 요소, 컴포넌트 및 서비스를 테스트합니다. 모듈이나 저장소가 모두 없으면 그렇게 말하세요 — 다른 프레임워크(Fyne, Gio, Wails)를 사용자의 실제 요청을 대신해서 제공하지 마세요.
설정
Go 1.26.0+ 필요 (go.mod에 go 1.26.0 명시).
| OS | 네이티브 대상 | 참고 |
|---|---|---|
| macOS | ✅ | 전체 도구 체인: Go + Xcode로 모든 대상 빌드/서명 |
| Windows | ✅ | 네이티브 + 웹 빌드 및 실행 |
| Linux | ✅ | GTK 4.14+, Pango, Cairo 필요; 웹 뷰/비디오를 위해 WebKitGTK 6.0 및 GStreamer 필요 |
앱에 모듈과 도구를 한 번 추가하면 go tool nsgo로 설치 없이 실행 가능:
go get -tool github.com/oneo-me/nsgo/cmd/nsgo
NSGo는 태그된 버전을 게시하지 않으므로 모듈은 유사 버전(v0.0.0-2026…-46c7d9c)으로 확인됩니다. 일부 지역 미러는 태그되지 않은 모듈에서 404 오류가 발생할 수 있습니다. go get이 모듈을 찾을 수 없으면 프록시를 고정하세요:
GOPROXY=https://proxy.golang.org,direct GOSUMDB=off go get -tool github.com/oneo-me/nsgo/cmd/nsgo
GOSUMDB=off는 sum.golang.org에 연결할 수 없는 경우에만 필요합니다. 대신 로컬 체크아웃에서 작업하려면 다음 명령어로 가리키고 정리하세요:
go mod edit -replace github.com/oneo-me/nsgo=/path/to/nsgo && go mod tidy
프로젝트 생성
mkdir counter && cd counter
go mod init counter
go get -tool github.com/oneo-me/nsgo/cmd/nsgo # 라이브러리 및 도구
go tool nsgo init -name counter . # nsgo.yaml, app.go, app_test.go 생성
go mod tidy
go tool nsgo dev . # 실행; 저장 시마다 다시 빌드
go tool nsgo test . # 헤드리스: 2 passed
init에는 이름이 필요합니다. 빈 디렉토리에서 go tool nsgo init을 실행하면 "." is not an app name 오류가 발생하는데, 기본 이름이 디렉토리에서 가져오기 때문입니다(그리고 .에서는 .입니다). -name을 전달하거나 생성할 디렉토리를 전달하세요:
| 명령어 | 효과 |
|---|---|
go tool nsgo init -name Counter . | 현재 디렉토리에 앱 이름 Counter로 작성 |
go tool nsgo init Counter | ./Counter/ 디렉토리를 생성하여 앱 보관 |
go tool nsgo init -template tsx Counter | ./Counter/를 TSX 앱으로 생성 |
init은 다음 단계를 출력합니다(next: go tool nsgo dev Counter) — 기억보다는 출력을 신뢰하세요.
-template은 go(기본값), tsx, mixed 중 하나입니다. 앱 이름은 문자, 숫자, 공백, - 및 _를 사용할 수 있습니다. 번들 ID는 dev.nsgo.<name>이 됩니다. -module을 전달하면 go.mod도 함께 작성됩니다.
| 템플릿 | 파일 |
|---|---|
go | nsgo.yaml, app.go, app_test.go |
tsx | nsgo.yaml, main.go, App.tsx, app.test.tsx, tsconfig.json, .gitignore |
mixed | 동일하며, App.tsx가 자체 Go를 가져오고 native.go가 포함됨 |
nsgo.yaml은 앱과 대상(targets)의 이름을 지정합니다:
name: Hello
id: dev.oneo.nsgo.hello
version: 0.1.0
dev:
target: native # native | web | ios | ios_simulator | android
hot_reload: true
fps: false
build:
output: dist
targets: [macos_arm64, windows_x64, ios_simulator, android_arm64, web]
유효한 다른 필드: build_number, appearance, theme, i18n, icon.{apple,png}, dev.icon, android.permissions, sign.{profile,keystore}, web.host. 그 외에 resources/는 제공된 파일(asset:으로 로드됨), assets/는 프로젝트 전용 파일(아이콘), locales/는 ICU MessageFormat의 en.yaml, de.yaml…을 보관합니다.
생성되는 앱 (-template go)
import (
"fmt"
"github.com/oneo-me/nsgo"
"github.com/oneo-me/nsgo/base"
"github.com/oneo-me/nsgo/core"
)
func Root(cx *core.Scope) core.Node {
count := core.NewSignal(cx, 0)
return core.View{Style: core.Style{FlexGrow: 1, Padding: core.All(24), Gap: 16}, Children: []core.Node{
core.Text{ContentFunc: func() string { return fmt.Sprintf("Pressed %d times", count.Get()) }},
base.Button{
Label: "Press me",
OnPress: func(core.PressEvent) { count.Update(func(n int) int { return n + 1 }) },
Style: core.Style{Background: core.RGB(0x0969da), CornerRadius: 8, Padding: core.Symmetric(10, 16)},
Children: []core.Node{core.Text{Content: "Press me", Style: core.Style{Color: core.RGB(0xffffff)}}},
},
}}
}
func main() { nsgo.Run(Root) }
nsgo.Run은 루트 뷰(func(cx *core.Scope) core.Node) 또는 앱 함수(func(app *core.Scope))를 인자로 받으며, 앱 함수는 desktop을 통해 자체 창을 엽니다. 옵션: WindowSize(w, h), Appearance(...), HideDock(), FPS(bool), WindowMaterial(...), Fonts(paths...), Component(name, factory).
개념 모델
- 신호(Signals).
core.NewSignal[T comparable](cx, v)→.Get(),.Set(v),.Update(fn).NewMemo,NewEffect. 그래프는 엔진 고루틴에 속하며 절대 잠가지 않습니다. 다른 고루틴은Scope.Post를 통해 진입합니다. - 요소는 구조체이며, 제로 값이 기본값입니다 (
http.Server처럼). 차이를 두고 싶은 것만 설정하세요. - 모든 반응형 필드에는 쌍이 있습니다:
Content+ContentFunc func() string,Style+StyleFunc,Class+ClassFunc.XFunc가X보다 우선합니다. 둘 다 설정하면 마운트 시 패닉이 발생합니다. 정적 형태가 없는 필드는 함수만 사용합니다(Disabled func() bool,Selected,Expanded). - 컴포넌트는
Render(cx *core.Scope) core.Node메서드를 가진 모든 타입이며, 필드는 props입니다.core.Func는 일반 함수를 어댑팅합니다. - 영역(Regions) —
Show,Each,Dynamic,Catch— 자체 범위만 다시 빌드합니다.Each는 메서드가 아닌 제네릭 함수입니다. - 스타일링: 타입이 지정된
core.Style값 또는Class의 Tailwind 클래스("flex-row gap-4 p-4 bg-white dark:bg-zinc-900").base의 컴포넌트는 의도적으로 스타일이 지정되지 않았습니다 — 동작, 상태 및 접근성만 제공합니다.
모션(Motion)
두 가지 클록. 커밋(Commit) = Go가 스팬을 다시 렌더링합니다(신호, Show/Each/Dynamic). 프레임(Frame) = 플랫폼의 애니메이터가 AnimatedStyle 필드와 셰이더 유니폼을 단계별로 실행하며, 커밋은 발생하지 않습니다. 모든 모션은 프레임 클록에 속해야 합니다. 렌더링 중에 값을 읽으면 하위 트리가 다시 렌더링되고 그 안의 모든 애니메이션이 다시 시작됩니다.
| 필요 상황 | 사용 |
|---|---|
| 상태 변경(호버, 누름, 선택, 크기 조정) | Style.Transition — 지속 시간, core.EaseOut, Properties 비트마스크; 그래프가 전혀 없으며, reduce-motion 설정 시 건너뜀 |
| 표준 진입/퇴장/주의/루프 | core.Motion + core.MotionPreset (38개, 모두 reduce-motion을 존중함) |
| 사용자 정의 안무, 겹침, 유지, 교체 | AnimatedStyle + AnimatedNode 그래프 (Timing, Spring, Interpolate, Sequence, Stagger, Loop) |
| 픽셀 단위 / 무한 필드 / 3D | core.Shader 유니폼을 노드로 사용 — 커밋 없음 |
| 절차적 드로잉 | core.Canvas Draw (변경 시마다 다시 기록) 또는 d.Bind (플랫폼이 재생, 재기록 없음) |
spec := core.FadeInUp.Spec() // 또는 자체 core.MotionSpec
m := core.NewMotion(cx, &spec) // nil = 정지 상태에서 빌드, 나중에 Play
return core.View{Style: core.Style{Animated: m.Style()}, Children: []core.Node{card()}}
API보다 더 중요한 다섯 가지 규칙: (1) 렌더링에서 신호나 애니메이션 값을 절대 읽지 마세요 — 클로저나 AnimatedStyle만 사용하세요; (2) 다시 빌드 대신 스타일을 변경하세요; (3) 유한 애니메이션은 마지막 값을 유지하므로, 끝의 프레임이 읽을 수 있는 값이어야 합니다(시작으로 돌아가는 루프는 정착되면 비어 보입니다); (4) 애니메이션 요소에는 측정된 부모가 필요합니다 — 모두 절대 위치인 View는 0×0으로 측정되어 모든 것을 자릅니다; (5) AnimatedStyle 필드는 AnimatedNode이며 숫자가 아닙니다(core.Constant(1), 1 아님). AnimatedStyle에는 Width/Height/Perspective가 없습니다.
애니메이션을 작성하기 전에 references/animation.md를 읽으세요: 프리셋 표, 작동했던 이징/지속 시간 숫자, 안무 레시피, 셰이더 및 캔버스 모션, 3D(레이 마칭), 테스트, 그리고 모션 버그에 대한 증상→원인 표.
개발 루프, 빌드, 테스트
| 작업 | 명령어 |
|---|---|
| 실행, 저장 시 다시 빌드 | go tool nsgo dev <dir> |
| 브라우저에서 실행 | go tool nsgo dev -target web <dir> |
| 시뮬레이터/기기에서 실행 | go tool nsgo dev -target ios_simulator|android|ios <dir> |
| 모든 커밋 추적 | ... -trace |
| 릴리스 패키지 | go tool nsgo build <dir> (-target all, -output DIR, -run, -device D) |
| 호스트 앱에 임베드 | ... build -embed → XCFramework (iOS) / AAR (Android) / nsgo.js (웹); 데스크탑은 nsgo.NewView 사용 |
| 테스트 | go tool nsgo test <dir> (-run REGEXP, -junit FILE, -target macos|windows|linux|web|ios_simulator|android, -fps) |
| TSX 타입 체크 | go tool nsgo check <dir> |
| 메시지 카탈로그 재생성 | go tool nsgo i18n <dir> |
플래그는 해당 이름을 가진 nsgo.yaml 값을 재정의합니다. build.targets 및 -target은 all 또는 windows_x64, macos_arm64, linux_x64, ios, web과 같은 이름을 받습니다. build는 -output(기본값 dist) 아래 각 대상에 하나의 디렉토리를 <name>-<version>-<target>/ 형식으로 작성합니다. 데스크탑 테스트 대상은 해당 OS에서만 실행됩니다(Windows에서는 -target windows, Mac에서는 -target macos). web, ios_simulator 및 android는 Mac에서 실행됩니다.
테스트는 사용자처럼 작동합니다. nsgotest는 창 없이 헤드리스로 실행합니다. -target은 실제 창에서 동일한 테스트를 네이티브 입력 및 스크린샷(app.Screenshot("name"))과 함께 실행합니다.
func TestMain(m *testing.M) { nsgotest.Main(m) } // 플랫폼에 메인 스레드를 제공
func TestPress(t *testing.T) {
app := nsgotest.Start(t, Root)
app.Press("text:Press me")
app.Find("text:Pressed 1 times")
}
선택자는 Playwright와 동일합니다: id:, label:, text: (부분 문자열; text:"Save" 또는 순수 문자열 = 전체 텍스트), role:<role> <name>, view:<n>, /regexp/flags, 그리고 하위로 이동하는 >>. 단계: Press, PressDown/PressUp, LongPress, Key, KeyPress, Type, Focus, Hover, Scroll, ScrollTo, Drag, Touch, Back, Resize, Settle, Advance, WaitFor, WaitForGone, Exists, Query, Texts, Dump. 전체 목록: go doc github.com/oneo-me/nsgo/nsgotest.
TSX 앱도 동일한 형태입니다: App.tsx는 "nsgo"에서 mount, signal, Text, View를("nsgo/base"에서 컴포넌트) 가져오고 mount(App)으로 끝납니다. main.go는 소스를 임베드하고 nsgo.Run(compiler.App(src, "App.tsx"))을 호출합니다. *.test.tsx 파일은 Go 테스트로 실행됩니다. references/api.md를 참조하세요.
일반적인 실수
| 실수 | 현실 |
|---|---|
| 필드 이름 추측하기 | NSGo는 사전 출시 버전이며 공개 문서가 거의 없으므로 기억에 의존한 식별자는 틀립니다. 먼저 go doc을 확인하세요 — 이름 하나 틀리면 컴파일 오류입니다 |
Content와 ContentFunc 둘 다 설정 | 마운트 시 패닉. 하나만 선택하세요 |
NewSignal(cx, []string{...}) | T는 comparable이어야 합니다. 자체 equal 함수와 함께 NewSignalFunc을 사용하세요 |
스코프 외부에서 NewSignal | 신호는 cx가 필요합니다. 그래프는 고루틴 로컬이므로 cx가 유일한 핸들입니다. 다른 고루틴에서는 Scope.Post를 사용하세요 |
Button.Label이 렌더링될 것이라고 기대 | Label은 접근 가능한 이름입니다(role:/label: 선택자가 일치하는 것). 보이는 텍스트는 Children에서 옵니다 |
internal/... 가져오기 | 앱은 nsgo, core, base, mobile, desktop, i18n, icons, tsx, nsgotest만 가져올 수 있습니다 |
Windows나 Linux에서 go build ./... | 실패: internal/platform/apple/edge.go에 //go:build darwin 태그가 없어 darwin 전용 purego/objc를 가져옵니다. 자체 패키지를 빌드하거나(go build .), 체크아웃에서 태그를 추가하세요 |
JustifyContent: core.AlignCenter | 두 개의 열거형: AlignItems는 core.AlignStart/Center/End/Stretch/Baseline을 받고 JustifyContent는 core.JustifyStart/Center/End/SpaceBetween/...을 받습니다. 혼합하면 컴파일 오류입니다 |
core.Shader{OnMouseMove: ...} | 셰이더에는 OnPress, OnLayout, Style이 있습니다 — 포인터 이동 핸들러는 없습니다. OnMouseMove func(Point) / OnHoverOut func()을 주변의 core.View에 배치하세요 |
MustCompileShader만 신뢰 | Kage는 런타임에 컴파일되므로 잘못된 셰이더는 빌드 시가 아닌 시작 시 패닉을 발생시킵니다. 각 소스를 core.CompileShader로 테스트하고 오류를 보고하세요 |
스크롤되지 않고 막대도 표시되지 않는 Scroll | Scroll의 기본값은 FlexShrink: 1이지만 View 부모는 그렇지 않습니다: FlexGrow: 1만 있는 열은 내용에 맞게 커지고 스크롤 뷰는 그 높이를 상속받아 오버플로가 발생하지 않습니다. 열에 FlexGrow: 1, FlexShrink: 1을 지정하세요(React Native의 flex: 1). 확인: 스크롤 프레임은 창보다 짧아야 합니다 |
| 영구적인 스크롤바 찾기 | 표시기는 오버레이입니다: 네이티브가 스크롤 중에 나타났다가 700ms 유지 후 300ms 동안 사라지는 둥근 썸을 그립니다. HideIndicator가 제거합니다. 웹에서는 브라우저 자체 막대입니다(overflow-y: auto, scrollbar-width: auto) |
Left만으로 고정된 오버레이 | 하나의 수평 가장자리로 절대 위치에 배치된 뷰는 내용 너비가 아닌 포함 블록의 나머지 너비를 차지합니다: 26pt 둥근 앵커 내부에서 Left: 34인 툴팁은 아무것도 남지 않아 한 줄에 한 문자로 줄 바꿈됩니다. 명시적인 Width를 지정하세요 |
| 보간된 색상 내부의 알파 | core.Interpolate로 Colors: {RGBA(0xffffff, 0.12), ...}와 함께 제공된 AnimatedStyle.Background는 12%가 아닌 거의 흰색으로 칠해집니다. 보간된 채우기에는 불투명한 색상을 사용하고 알파는 Style.Background용으로 유지하세요 |
MotionPreset.Name()을 레이블로 사용 | camelCase를 반환합니다(fadeIn, zoomIn, breathe). Go 상수의 이름이 아닙니다 |
| 애니메이션이 "시작으로 다시 점프" | 렌더링 중에 읽힌 신호나 애니메이션 값, 또는 Dynamic의 렌더 내부에서 빌드된 NewAnimatedValue: 스팬이 다시 빌드되고 값이 다시 시작됩니다. 내부에서 읽기 |