Communitygithub.com

HycJack/nsgo

NSGo 네이티브 UI 엔진을 사용하여 Go 백엔드 기반 앱을 작성, 실행, 테스트, 패키징 및 디버깅할 때 사용합니다. macOS, iOS, Windows, Linux, Android 및 웹을 대상으로 하는 크로스 플랫폼 UI 엔진입니다.

nsgo란 무엇인가요?

이 스킬은 `HycJack/nsgo` 레포지토리와 연동된 NSGo(GitHub.com/oneo-me/nsgo) 크로스 플랫폼 네이티브 UI 엔진에 대한 전체 가이드를 제공합니다. NSGo는 Go 백엔드를 사용하며 Native Shell(NSH)의 Go 포트로, macOS, iOS, Windows, Linux, Android 및 웹을 대상으로 합니다. 앱 로직, 반응형 그래프, 스타일, 레이아웃 및 텍스트는 Go에서 실행되며 각 플랫폼은 자체 네이티브 뷰로 렌더링됩니다. 앱은 Go로 작성하거나 TSX로 작성하여 goja로 컴파일할 수 있습니다. 이 스킬은 프로젝트 생성(`go tool nsgo init`), 개발 루프(`go tool nsgo dev`), 빌드(`go tool nsgo build`), 테스트(`go tool nsgo test`, `nsgotest`), 그리고 신호(signals), 애니메이션(Motion, AnimatedStyle, 트랜지션 등), Tailwind CSS 클래스 지원, 컴포넌트 기반 아키텍처 등 핵심 API 사용법을 다룹니다. 또한 각 플랫폼별 설정 요구사항, 일반적인 실수와 해결 방법, 그리고 사전 출시 버전이라는 특성상 문서화되지 않은 API를 `go doc`으로 직접 확인해야 한다는 중요한 규칙을 강조합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/HycJack/nsgo-motion-lab/tree/HEAD

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

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도 함께 작성됩니다.

템플릿파일
gonsgo.yaml, app.go, app_test.go
tsxnsgo.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)
픽셀 단위 / 무한 필드 / 3Dcore.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로 테스트하고 오류를 보고하세요
스크롤되지 않고 막대도 표시되지 않는 ScrollScroll의 기본값은 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: 스팬이 다시 빌드되고 값이 다시 시작됩니다. 내부에서 읽기

관련 스킬