paint-brush
웹 개발에 Swift를 사용하는 방법~에 의해@imike
17,068 판독값
17,068 판독값

웹 개발에 Swift를 사용하는 방법

~에 의해 Mikhail Isaev33m2023/03/20
Read on Terminal Reader
Read this story w/o Javascript

너무 오래; 읽다

SwifWeb은 SwiftUI를 사용하여 웹사이트를 작성할 수 있는 기능을 제공하는 프레임워크입니다. 이는 전체 HTML 및 CSS 표준은 물론 모든 웹 API를 래핑합니다. 이 기사에서는 SwifWeb 프레임워크를 사용하여 웹사이트 구축을 시작하는 방법을 보여 드리겠습니다.

People Mentioned

Mention Thumbnail
featured image - 웹 개발에 Swift를 사용하는 방법
Mikhail Isaev HackerNoon profile picture
0-item
1-item

웹 개발 세계는 광활하며, 매일 새롭게 등장하는 끊임없는 새로운 기술의 흐름 속에서 길을 잃기 쉽습니다. 이러한 새로운 기술의 대부분은 JavaScript 또는 TypeScript를 사용하여 구축되었습니다. 하지만 이 기사에서는 브라우저 내에서 직접 네이티브 Swift를 사용하여 웹 개발을 하는 방법을 소개할 것이며 이것이 여러분에게 깊은 인상을 줄 것이라고 확신합니다.


그게 어떻게 가능해?

Swift가 웹 페이지에서 기본적으로 작동하려면 먼저 WebAssembly 바이트 코드로 컴파일되어야 하며 그런 다음 JavaScript가 해당 코드를 페이지에 로드할 수 있습니다. 특수 도구 체인을 사용하고 도우미 파일을 빌드해야 하기 때문에 전체 컴파일 프로세스가 약간 까다롭습니다. 이것이 바로 Carton 및 Webber와 같은 도우미 CLI 도구를 사용할 수 있는 이유입니다.

타사 툴체인을 사용해도 되나요?

SwiftWasm 커뮤니티는 원래 Swift 툴체인을 패치하여 Swift를 WebAssembly로 컴파일할 수 있도록 엄청난 양의 작업을 수행했습니다. 매일 원래 도구 체인에서 변경 사항을 자동으로 가져오고 테스트가 실패하면 포크를 수정하여 도구 체인을 업데이트합니다. 그들의 목표는 공식 툴체인의 일부가 되는 것이며 가까운 시일 내에 그렇게 되기를 희망합니다.

카톤 또는 웨버?

Carton은 SwiftWasm 커뮤니티에서 제작되었으며 SwiftUI를 사용하여 웹 사이트를 작성할 수 있는 기능을 제공하는 프레임워크인 Tokamak 프로젝트에 사용할 수 있습니다.


Webber는 SwifWeb 프로젝트용으로 만들어졌습니다. SwifWeb은 전체 HTML 및 CSS 표준은 물론 모든 웹 API를 포함한다는 점에서 다릅니다.


코드 일관성을 위해 SwiftUI를 사용하여 웹 앱을 작성하는 것을 선호할 수도 있지만 웹 개발은 본질적으로 다르며 SwiftUI와 동일한 방식으로 접근할 수 없기 때문에 이것이 잘못된 접근 방식이라고 생각합니다.


이것이 바로 제가 자동 완성 및 문서화 기능이 포함된 아름다운 구문을 사용하여 Swift에서 직접 HTML, CSS 및 웹 API의 모든 기능을 사용할 수 있는 기능을 제공하는 SwifWeb을 만든 이유입니다. 그리고 Webber 도구를 만든 이유는 Carton이 SwifWeb 앱을 위해 만들어지지 않았기 때문에 SwifWeb 앱을 올바른 방식으로 컴파일, 디버깅 및 배포할 수 없기 때문입니다.


제 이름은 Mikhail Isaev이고 SwifWeb의 저자입니다. 이 기사에서는 SwifWeb을 사용하여 웹사이트 구축을 시작하는 방법을 보여 드리겠습니다.

필수 도구


빠른

Swift를 설치해야 합니다. 가장 쉬운 방법은 다음과 같습니다.

  • macOS에서는 Xcode를 설치하는 것입니다
  • Linux 또는 Windows(WSL2)에서는 Swiftlang.xyz 의 스크립트를 사용하는 것입니다.


다른 경우에는 공식 웹사이트의 설치 지침을 살펴보세요.

웨버 CLI

저는 여러분의 앱 구축, 디버그, 배포를 돕기 위해 Webber를 만들었습니다.


macOS에서는 HomeBrew를 사용하여 쉽게 설치할 수 있습니다(홈브루 웹사이트 에서 설치).

 brew install swifweb/tap/webber

나중에 최신 버전으로 업데이트하려면 다음을 실행하세요.

 brew upgrade webber


Ubuntu 또는 Windows(WSL2의 Ubuntu)에서는 Webber를 수동으로 복제하고 컴파일합니다.

 sudo apt-get install binaryen curl https://get.wasmer.io -sSfL | sh apt-get install npm cd /opt sudo git clone https://github.com/swifweb/webber cd webber sudo swift build -c release sudo ln -s /opt/webber/.build/release/Webber /usr/local/bin/webber

나중에 마지막 버전으로 업데이트하려면 실행하세요.

 cd /opt/webber sudo git pull sudo swift build -c release

메인 브랜치에는 항상 안정적인 코드가 포함되어 있으므로 자유롭게 업데이트를 가져오세요.

새 프로젝트 만들기

터미널을 열고 실행

 webber new

대화형 메뉴에서 pwa 또는 spa 선택하고 프로젝트 이름을 입력합니다.


새로 생성된 프로젝트로 디렉터리를 변경하고 webber serve 실행합니다.

이 명령은 프로젝트를 WebAssembly로 컴파일하고, 필요한 모든 파일을 특수 .webber 폴더에 패키지하고, 기본적으로 포트 8888 사용하여 모든 인터페이스에서 프로젝트 제공을 시작합니다.


webber serve 에 대한 추가 인수

  • 앱 유형

    프로그레시브 웹 앱의 경우 -t pwa

    단일 웹 앱용 -t spa

  • 서비스 작업자 대상의 이름(일반적으로 PWA 프로젝트에서는 Service 라고 함)

    -s Service

  • 앱 대상 이름(기본적으로 App )

    -a App

  • 콘솔에서 추가 정보 인쇄

    -v

  • Webber 서버용 포트(기본값은 8888 )

    -p 8080

실제 SSL처럼 테스트하려면 -p 443 사용하세요(자체 서명된 SSL 설정이 허용됨).

  • 자동으로 시작할 대상 브라우저 이름

    --browser safari 또는 --browser chrome

  • 서비스 워커 디버깅을 위해 자체 서명된 SSL 설정이 허용된 브라우저의 추가 인스턴스

    --browser-self-signed

  • 시크릿 모드의 추가 브라우저 인스턴스

    --browser-incognito

애플리케이션

앱은 Sources/App/App.swift 에서 시작됩니다.

 import Web @main class App: WebApp { @AppBuilder override var app: Configuration { Lifecycle.didFinishLaunching { app in app.registerServiceWorker("service") } Routes { Page { IndexPage() } Page("login") { LoginPage() } Page("article/:id") { ArticlePage() } Page("**") { NotFoundPage() } } MainStyle() } }

수명주기

iOS와 유사한 방식으로 작동합니다.

didFinishLaunching 앱이 막 시작되었을 때

willTerminate 앱이 종료될 때

창이 비활성화될 때 willResignActive

창이 활성화되었을 때 didBecomeActive

창이 배경으로 들어갈 때 didEnterBackground

willEnterForeground 창이 전경으로 들어갈 때


여기서 가장 유용한 방법은 didFinishLaunching 입니다. 왜냐하면 앱을 구성하기에 좋은 장소이기 때문입니다. 정말 iOS 앱과 같은 느낌이군요! 😀


여기 app 에는 유용한 편의 방법이 포함되어 있습니다.

PWA 서비스 워커를 등록하기 위한 registerServiceWorker(“serviceName“) 호출

상대 또는 외부 스크립트를 추가하기 위한 addScript(“path/to/script.js“) 호출

상대 또는 외부 스타일을 추가하기 위한 addStylesheet(“path/to/style.css“) 호출

addFont(“path/to/font.woff”, type:) 상대 또는 외부 글꼴을 추가하기 위한 호출, 선택적으로 유형 설정

addIcon(“path/to/icon“, type:color:) 아이콘을 추가하기 위한 호출, 선택적으로 유형 및 색상 설정


또한 Autolayout , Bootstrap , Materialize 등과 같은 추가 라이브러리를 구성하는 곳입니다.

노선

현재 URL을 기반으로 적절한 페이지를 표시하려면 라우팅이 필요합니다.


라우팅을 사용하는 방법을 이해하려면 URL이 무엇인지 이해해야 합니다.

https://website.com/hello/world - 여기서는 /hello/world경로 입니다.

보시다시피 처음에는 App 클래스에서 모든 최상위 경로를 선언해야 합니다.

최상위 수준은 이러한 경로에 선언된 페이지가 창의 전체 공간을 차지한다는 의미입니다.


좋습니다. 예를 들어 루트 경로는 세 가지 방법으로 설정할 수 있습니다.

 Page("/") { IndexPage() } Page("") { IndexPage() } Page { IndexPage() }

마지막이 제일 예쁜 것 같아요 🙂


로그인 또는 등록 경로는 다음과 같이 설정할 수 있습니다.

 Page("login") { LoginPage() } Page("registration") { RegistrationPage() }


매개변수 관련 경로

 Page("article/:id") { ArticlePage() }

위 예의 :id는 경로의 동적 부분입니다. ArticlePage 클래스에서 이 식별자를 검색하여 이와 관련된 기사를 표시할 수 있습니다.

 class ArticlePage: PageController { override func didLoad(with req: PageRequest) { if let articleId = req.parameters.get("id") { // Retrieve article here } } }

경로에 둘 이상의 매개변수가 있을 수 있습니다. 같은 방법으로 모두 검색하세요.

쿼리

경로에서 다음으로 흥미로운 점은 사용하기 매우 쉬운 쿼리 입니다. 예를 들어, 검색 textage 쿼리 매개변수가 있을 것으로 예상되는 /search 경로를 고려해 보겠습니다.

https://website.com/search**?text=Alex&age=19** - 마지막 부분은 쿼리 입니다.


간단히 검색 경로를 선언하세요.

 Page("search") { SearchPage() }

그리고 다음과 같이 SearchPage 클래스에서 쿼리 데이터를 검색합니다.

 class SearchPage: PageController { struct Query: Decodable { let text: String? let age: Int? } override func didLoad(with req: PageRequest) { do { let query = try req.query.decode(Query.self) // use optional query.text and query.age // to query search results } catch { print("Can't decode query: \(error)") } } }

아무것

* 사용하여 다음과 같이 특정 경로 부분의 모든 항목을 허용하는 경로를 선언할 수도 있습니다.

 Page("foo", "*", "bar") { SearchPage() }

위의 경로는 foo와 bar 사이의 모든 항목을 허용합니다(예: /foo/aaa/bar, /foo/bbb/bar 등).

포괄

** 기호를 사용하면 특정 경로의 다른 경로와 일치하지 않는 모든 항목을 처리하는 특수 포괄 경로를 설정할 수 있습니다.


글로벌 404 경로를 만드는 데 사용합니다.

 Page("**") { NotFoundPage() }

또는 특정 경로(예: 사용자를 찾을 수 없는 경우)

 Page("user", "**") { UserNotFoundPage() }


위에서 선언한 경로의 상황을 명확히 합시다.

/user/1 - /user/:id에 대한 경로가 있으면 UserPage 를 반환합니다. 그렇지 않으면…


UserNotFound페이지

/user/1/hello - /user/:id/hello에 대한 경로가 있으면 UserNotFoundPage 에 속하게 됩니다.

/something - /something에 대한 경로가 없으면 NotFoundPage 에 속하게 됩니다.

중첩된 라우팅

다음 경로를 위해 페이지의 전체 콘텐츠를 교체하는 것이 아니라 특정 블록만 교체하고 싶을 수도 있습니다. FragmentRouter 가 유용한 곳입니다!


/user 페이지에 탭이 있다고 가정해 보겠습니다. 각 탭은 하위 경로이며 FragmentRouter를 사용하여 하위 경로의 변경 사항에 반응하려고 합니다.


App 클래스에서 최상위 경로를 선언합니다.

 Page("user") { UserPage() }

UserPage 클래스에서 FragmentRouter를 선언합니다.

 class UserPage: PageController { @DOM override var body: DOM.Content { // NavBar is from Materialize library :) Navbar() .item("Profile") { self.changePath(to: "/user/profile") } .item("Friends") { self.changePath(to: "/user/friends") } FragmentRouter(self) .routes { Page("profile") { UserProfilePage() } Page("friends") { UserFriendsPage() } } } }


위의 예에서 FragmentRouter는 /user/profile/user/friends 하위 경로를 처리하고 Navbar 아래에 렌더링하므로 페이지는 전체 콘텐츠를 다시 로드하지 않고 특정 조각만 다시 로드합니다.


동일하거나 다른 하위 경로를 가진 두 개 이상의 조각이 선언될 수도 있으며 모두 마술처럼 함께 작동합니다!


Btw FragmentRouterDiv 이며 호출하여 구성할 수 있습니다.

 FragmentRouter(self) .configure { div in // do anything you want with the div }

스타일시트

기존 CSS 파일을 사용할 수 있지만 Swift로 작성된 스타일시트를 사용할 수 있는 새롭고 마법 같은 기능도 있습니다!

기초

Swift를 사용하여 CSS 규칙을 선언하기 위해 Rule 객체가 있습니다.


메소드를 호출하여 선언적으로 구축할 수 있습니다.

 Rule(...selector...) .alignContent(.baseline) .color(.red) // or rgba/hex color .margin(v: 0, h: .auto)

또는 @resultBuilder를 사용하는 SwiftUI와 유사한 방식

 Rule(...selector...) { AlignContent(.baseline) Color(.red) Margin(v: 0, h: .auto) }


두 가지 방법 모두 동일하지만 을 입력한 직후의 자동 완성 때문에 첫 번째 방법을 선호합니다 . 😀

MDN에 설명된 모든 CSS 메서드를 사용할 수 있습니다.

그 이상으로 브라우저 접두어를 자동으로 처리합니다!

그러나 특정 경우에는 이런 방식으로 사용자 정의 속성을 설정할 수 있습니다.

 Rule(...selector...) .custom("customKey", "customValue")

선택자

규칙이 영향을 미치는 요소를 설정하려면 선택기를 설정해야 합니다. 선택기는 데이터베이스의 쿼리로 표시되지만 해당 선택기 쿼리의 일부는 포인터라고 합니다.


포인터를 만드는 가장 쉬운 방법은 원시 문자열을 사용하여 포인터를 초기화하는 것입니다.

 Pointer("a")


하지만 올바른 빠른 방법은 다음과 같이 필요한 HTML 태그에서 .pointer 호출하여 빌드하는 것입니다.

 H1.pointer // h1 A.pointer // a Pointer.any // * Class("myClass").pointer // .myClass Id("myId").pointer // #myId

기본 포인터에 관한 것이지만 :hover :first :first-child 등과 같은 수정자도 있습니다.

 H1.pointer.first // h1:first H1.pointer.firstChild // h1:first-child H1.pointer.hover // h1:hover

기존 수정자를 선언할 수 있으며 모두 사용할 수 있습니다.

뭔가 빠진 것이 있으면 주저하지 말고 확장 프로그램을 만들어 추가하세요!

그리고 모든 사람에게 추가하려면 github에 풀 요청을 보내는 것을 잊지 마세요.

포인터를 연결할 수도 있습니다

 H1.class(.myClass) // h1.myClass H1.id(.myId) // h1#myId H1.id(.myId).disabled // h1#myId:disabled Div.pointer.inside(P.pointer) // div p Div.pointer.parent(P.pointer) // div > p Div.pointer.immediatedlyAfter(P.pointer) // Div + p P.pointer.precededBy(Ul.pointer) // p ~ ul


규칙 에서 선택기를 사용하는 방법

 Rule(Pointer("a")) // or Rule(A.pointer)

규칙 에서 둘 이상의 선택기를 사용하는 방법

 Rule(A.pointer, H1.id(.myId), Div.pointer.parent(P.pointer))

다음 CSS 코드를 생성합니다.

 a, h1#myId, div > p { }

반동

앱에 대해 어두운 스타일과 밝은 스타일을 선언해 보겠습니다. 그러면 나중에 두 스타일 사이를 쉽게 전환할 수 있습니다.

 import Web @main class App: WebApp { enum Theme { case light, dark } @State var theme: Theme = .light @AppBuilder override var app: Configuration { // ... Lifecycle, Routes ... LightStyle().disabled($theme.map { $0 != .happy }) DarkStyle().disabled($theme.map { $0 != .sad }) } }


LightStyleDarkStyle은 별도의 파일이나 예를 들어 App.swift에서 선언될 수 있습니다.


 class LightStyle: Stylesheet { @Rules override var rules: Rules.Content { Rule(Body.pointer).backgroundColor(.white) Rule(H1.pointer).color(.black) } } class DarkStyle: Stylesheet { @Rules override var rules: Rules.Content { Rule(Body.pointer).backgroundColor(.black) Rule(H1.pointer).color(.white) } }


그런 다음 일부 페이지의 UI 어딘가에서 호출하면 됩니다.

 App.current.theme = .light // to switch to light theme // or App.current.theme = .dark // to switch to dark theme

그리고 관련 스타일시트를 활성화하거나 비활성화합니다! 멋지지 않나요? 😍


하지만 CSS 대신 Swift로 스타일을 설명하는 것이 더 어렵다고 말할 수도 있습니다. 그렇다면 요점은 무엇입니까?


핵심은 반응성! CSS 속성과 함께 @State를 사용하고 즉시 값을 변경할 수 있습니다!


살펴보세요. 일부 반응성 속성이 포함된 클래스를 생성하고 런타임 중에 언제든지 변경할 수 있으므로 해당 클래스를 사용하는 화면의 모든 요소가 업데이트됩니다! 많은 요소에 대해 클래스를 전환하는 것보다 훨씬 더 효과적입니다!


 import Web @main class App: WebApp { @State var reactiveColor = Color.cyan @AppBuilder override var app: Configuration { // ... Lifecycle, Routes ... MainStyle() } } extension Class { static var somethingCool: Class { "somethingCool" } } class MainStyle: Stylesheet { @Rules override var rules: Rules.Content { // for all elements with `somethingCool` class Rule(Class.hello.pointer) .color(App.current.$reactiveColor) // for H1 and H2 elements with `somethingCool` class Rule(H1.class(.hello), H2.class(.hello)) .color(App.current.$reactiveColor) } }


나중에 코드의 어느 위치에서나 호출하면 됩니다.

 App.current.reactiveColor = .yellow // or any color you want

그러면 스타일시트와 이를 사용하는 모든 요소의 색상이 업데이트됩니다 😜


또한 스타일시트에 원시 CSS를 추가하는 것도 가능합니다.

 class MainStyle: Stylesheet { @Rules override var rules: Rules.Content { // for all elements with `somethingCool` class Rule(Class.hello.pointer) .color(App.current.$reactiveColor) // for H1 and H2 elements with `somethingCool` class Rule(H1.class(.hello), H2.class(.hello)) .color(App.current.$reactiveColor) """ /* Raw CSS goes here */ body { margin: 0; padding: 0; } """ } }

필요한 만큼 원시 CSS 문자열을 혼합할 수 있습니다.

페이지

라우터는 각 경로에서 페이지를 렌더링하고 있습니다. Page는 PageController 에서 상속된 클래스입니다.


PageController 에는 willLoad didLoad willUnload didUnload , UI 메서드 buildUIbody , HTML 요소에 대한 속성 래퍼 변수와 같은 수명 주기 메서드가 있습니다.

기술적으로 PageController 는 단지 Div이며 buildUI 메소드에서 해당 속성을 설정할 수 있습니다.


 class IndexPage: PageController { // MARK: - Lifecycle override func willLoad(with req: PageRequest) { super.willLoad(with: req) } override func didLoad(with req: PageRequest) { super.didLoad(with: req) // set page title and metaDescription self.title = "My Index Page" self.metaDescription = "..." // also parse query and hash here } override func willUnload() { super.willUnload() } override func didUnload() { super.didUnload() } // MARK: - UI override func buildUI() { super.buildUI() // access any properties of the page's div here // eg self.backgroundcolor(.lightGrey) // optionally call body method here to add child HTML elements body { P("Hello world") } // or alternatively self.appendChild(P("Hello world")) } // the best place to declare any child HTML elements @DOM override var body: DOM.Content { H1("Hello world") P("Text under title") Button("Click me") { self.alert("Click!") print("button clicked") } } }


페이지가 작다면 이렇게 짧은 방법으로도 선언할 수 있습니다.

 PageController { page in H1("Hello world") P("Text under title") Button("Click me") { page.alert("Click!") print("button clicked") } } .backgroundcolor(.lightGrey) .onWillLoad { page in } .onDidLoad { page in } .onWillUnload { page in } .onDidUnload { page in }

아름답고 간결하지 않나요? 🥲


보너스 편의 방법

alert(message: String) - 직접 JS alert 방법

changePath(to: String) - URL 경로 전환

HTML 요소

마지막으로 HTML 요소를 만들고 사용하는 방법(!)을 알려드리겠습니다!


해당 속성이 포함된 모든 HTML 요소는 Swift에서 사용할 수 있으며 전체 목록은 예를 들어 MDN 에 있습니다.


HTML 요소의 간단한 목록 예:

SwifWeb 코드

HTML 코드

Div()

<div></div>

H1(“text“)

<h1>text</h1>

A(“Click me“).href(““).target(.blank)

<a href=”” target=”_blank”>Click me</a>

Button(“Click“).onClick { print(“click“) }

<button onclick=”…”>Click</button>

InputText($text).placeholder("Title")

<input type=”text” placeholder=”title”>

InputCheckbox($checked)

<input type=”checkbox”>


보시다시피, 입력을 제외하고는 모두 동일한 이름으로 표시되므로 Swift의 모든 HTML 태그에 액세스하는 것은 매우 쉽습니다. 입력 유형마다 방법이 다르기 때문에 혼합하고 싶지 않았습니다.


간단한 Div

 Div()

다음과 같이 모든 속성과 스타일 속성에 액세스할 수 있습니다.

 Div().class(.myDivs) // <div class="myDivs"> .id(.myDiv) // <div id="myDiv"> .backgroundColor(.green) // <div style="background-color: green;"> .onClick { // adds `onClick` listener directly to the DOM element print("Clicked on div") } .attribute("key", "value") // <div key="value"> .attribute("boolKey", true, .trueFalse) // <div boolKey="true"> .attribute("boolKey", true, .yesNo) // <div boolKey="yes"> .attribute("checked", true, .keyAsValue) // <div checked="checked"> .attribute("muted", true, .keyWithoutValue) // <div muted> .custom("border", "2px solid red") // <div style="border: 2px solid red;">

서브클래싱

HTML 요소를 하위 클래스화하여 스타일을 미리 정의하거나, 미리 정의된 많은 하위 요소와 외부에서 사용할 수 있는 몇 가지 편리한 메서드를 사용하여 복합 요소를 만들거나, didAddToDOMdidRemoveFromDOM 과 같은 수명 주기 이벤트를 달성합니다.

Div 이지만 미리 정의된 .divider 클래스를 사용하여 Divider 요소를 만들어 보겠습니다.

 public class Divider: Div { // it is very important to override the name // because otherwise it will be <divider> in HTML open class override var name: String { "\(Div.self)".lowercased() } required public init() { super.init() } // this method executes immediately after any init method public override func postInit() { super.postInit() // here we are adding `divider` class self.class(.divider) } }

서브클래싱할 때 슈퍼 메소드를 호출하는 것은 매우 중요합니다.

그렇지 않으면 예상치 못한 동작이 발생할 수 있습니다.

DOM에 추가

요소는 즉시 또는 나중에 PageController 의 DOM 또는 HTML 요소 에 추가될 수 있습니다.


 Div { H1("Title") P("Subtitle") Div { Ul { Li("One") Li("Two") } } }


또는 나중에 lazy var 사용하여

 lazy var myDiv1 = Div() lazy var myDiv2 = Div() Div { myDiv1 myDiv2 }

따라서 HTML 요소를 미리 선언하고 나중에 언제든지 DOM에 추가할 수 있습니다!

DOM에서 제거

 lazy var myDiv = Div() Div { myDiv } // somewhere later myDiv.remove()

상위 요소에 액세스

모든 HTML 요소에는 DOM에 추가된 경우 상위 요소에 대한 액세스를 제공하는 선택적 superview 속성이 있습니다.

 Div().superview?.backgroundColor(.red)

if/else 조건

특정 조건에서만 요소를 표시해야 하는 경우가 많으므로 이를 위해 if/else 사용하겠습니다.

 lazy var myDiv1 = Div() lazy var myDiv2 = Div() lazy var myDiv3 = Div() var myDiv4: Div? var showDiv2 = true Div { myDiv1 if showDiv2 { myDiv2 } else { myDiv3 } if let myDiv4 = myDiv4 { myDiv4 } else { P("Div 4 was nil") } }

하지만 반응성이 없습니다. showDiv2 false 로 설정하려고 하면 아무 일도 일어나지 않습니다.


반응적 예

 lazy var myDiv1 = Div() lazy var myDiv2 = Div() lazy var myDiv3 = Div() @State var showDiv2 = true Div { myDiv1 myDiv2.hidden($showDiv2.map { !$0 }) // shows myDiv2 if showDiv2 == true myDiv3.hidden($showDiv2.map { $0 }) // shows myDiv3 if showDiv2 == false }


$showDiv2.map {…} 사용해야 합니까 ?

정렬: SwiftUI가 아니기 때문입니다. 조금도.


아래에서 @State 에 대해 자세히 알아보세요 .


원시 HTML

페이지나 HTML 요소에 원시 HTML을 추가해야 할 수도 있으며 이는 쉽게 가능합니다.

 Div { """ <a href="https://google.com">Go to Google</a> """ }


각각

정적 예

 let names = ["Bob", "John", "Annie"] Div { ForEach(names) { name in Div(name) } // or ForEach(names) { index, name in Div("\(index). \(name)") } // or with range ForEach(1...20) { index in Div() } // and even like this 20.times { Div().class(.shootingStar) } }

동적 예

 @State var names = ["Bob", "John", "Annie"] Div { ForEach($names) { name in Div(name) } // or with index ForEach($names) { index, name in Div("\(index). \(name)") } } Button("Change 1").onClick { // this will append new Div with name automatically self.names.append("George") } Button("Change 2").onClick { // this will replace and update Divs with names automatically self.names = ["Bob", "Peppa", "George"] }

CSS

위의 예와 동일하지만 BuilderFunction 도 사용할 수 있습니다.

 Stylesheet { ForEach(1...20) { index in CSSRule(Div.pointer.nthChild("\(index)")) // set rule properties depending on index } 20.times { index in CSSRule(Div.pointer.nthChild("\(index)")) // set rule properties depending on index } }


다음 예제의 delay 값처럼 ForEach 루프에서 BuilderFunction 사용하여 일부 값을 한 번만 계산할 수 있습니다.

 ForEach(1...20) { index in BuilderFunction(9999.asRandomMax()) { delay in CSSRule(Pointer(".shooting_star").nthChild("\(index)")) .custom("top", "calc(50% - (\(400.asRandomMax() - 200)px))") .custom("left", "calc(50% - (\(300.asRandomMax() + 300)px))") .animationDelay(delay.ms) CSSRule(Pointer(".shooting_star").nthChild("\(index)").before) .animationDelay(delay.ms) CSSRule(Pointer(".shooting_star").nthChild("\(index)").after) .animationDelay(delay.ms) } }


인수로 기능할 수도 있습니다.

 BuilderFunction(calculate) { calculatedValue in // CSS rule or DOM element } func calculate() -> Int { return 1 + 1 }

BuilderFunction 은 HTML 요소에도 사용할 수 있습니다 :)

@State와의 반응성

@State 는 오늘날 선언적 프로그래밍 에 가장 바람직한 것입니다.


위에서 말했듯이 SwiftUI가 아니므로 모든 것을 추적하고 다시 그리는 전역 상태 머신이 없습니다. HTML 요소는 임시 구조체가 아니라 클래스이므로 실제 개체이며 직접 액세스할 수 있습니다. 훨씬 더 좋고 유연하며 모든 제어가 가능합니다.

후드 아래에는 무엇이 있나요?

모든 구독자에게 변경 사항을 알리는 속성 래퍼입니다.

변경 사항을 구독하는 방법은 무엇입니까?

 enum Countries { case usa, australia, mexico } @State var selectedCounty: Countries = .usa $selectedCounty.listen { print("country changed") } $selectedCounty.listen { newValue in print("country changed to \(newValue)") } $selectedCounty.listen { oldValue, newValue in print("country changed from \(oldValue) to \(newValue)") }

HTML 요소가 변경 사항에 어떻게 반응할 수 있나요?

간단한 텍스트 예

 @State var text = "Hello world!" H1($text) // whenever text changes it updates inner-text in H1 InputText($text) // while user is typing text it updates $text which updates H1

간단한 숫자 예

 @State var height = 20.px Div().height($height) // whenever height var changes it updates height of the Div

간단한 부울 예제

 @State var hidden = false Div().hidden($hidden) // whenever hidden changes it updates visibility of the Div

매핑 예

 @State var isItCold = true H1($isItCold.map { $0 ? "It is cold 🥶" : "It is not cold 😌" })

두 상태 매핑

 @State var one = true @State var two = true Div().display($one.and($two).map { one, two in // returns .block if both one and two are true one && two ? .block : .none })

두 개 이상의 상태 매핑

 @State var one = true @State var two = true @State var three = 15 Div().display($one.and($two).map { one, two in // returns true if both one and two are true one && two }.and($three).map { oneTwo, three in // here oneTwo is a result of the previous mapping // returns .block if oneTwo is true and three is 15 oneTwo && three == 15 ? .block : .none })

모든 HTML 및 CSS 속성은 @State 값을 처리할 수 있습니다.

확장

HTML 요소 확장

Div와 같은 구체적인 요소에 몇 가지 편리한 방법을 추가할 수 있습니다.

 extension Div { func makeItBeautiful() {} }

또는 상위 class 알고 있는 경우 요소 그룹입니다.


부모 클래스가 거의 없습니다.

BaseActiveStringElement - a , h1 등과 같이 문자열로 초기화할 수 있는 요소용입니다.

BaseContentElement - div , ul 등과 같이 내부에 콘텐츠를 포함할 수 있는 모든 요소에 사용됩니다.

BaseElement - 모든 요소에 사용됩니다.


따라서 모든 요소에 대한 확장은 이런 식으로 작성할 수 있습니다.

 extension BaseElement { func doSomething() {} }

색상 선언

Color 클래스는 색상을 담당합니다. HTML 색상이 미리 정의되어 있지만 자신만의 색상을 가질 수 있습니다.

 extension Color { var myColor1: Color { .hex(0xf1f1f1) } // which is 0xF1F1F1 var myColor2: Color { .hsl(60, 60, 60) } // which is hsl(60, 60, 60) var myColor3: Color { .hsla(60, 60, 60, 0.8) } // which is hsla(60, 60, 60, 0.8) var myColor4: Color { .rgb(60, 60, 60) } // which is rgb(60, 60, 60) var myColor5: Color { .rgba(60, 60, 60, 0.8) } // which is rgba(60, 60, 60, 0.8) }

그런 다음 H1(“Text“).color(.myColor1) 처럼 사용하십시오.

클래스 선언

 extension Class { var my: Class { "my" } }

그런 다음 Div().class(.my) 처럼 사용하십시오.

ID 선언

 extension Id { var myId: Id { "my" } }

그런 다음 Div().id(.my) 처럼 사용하십시오.

웹 API

창문

window 객체는 App.current.window 변수를 통해 완전히 래핑되고 액세스 가능합니다.

전체 참조는 MDN 에서 확인할 수 있습니다.


아래에서 간략한 개요를 살펴보겠습니다.

전경 플래그

App.swiftLifecycle 에서 또는 이 방법으로 직접 들을 수 있습니다.

 App.current.window.$isInForeground.listen { isInForeground in // foreground flag changed }

아니면 언제 어디서나 읽으세요.

 if App.current.window.isInForeground { // do somethign }

또는 HTML 요소로 반응

 Div().backgroundColor(App.current.window.$isInForeground.map { $0 ? .grey : .white })

활성 플래그

Foreground 플래그와 동일하지만 App.current.window.isActive 통해 액세스할 수 있습니다.

사용자가 여전히 창 내에서 상호 작용하고 있는지 감지합니다.

온라인 상태

Foreground 플래그와 동일하지만 App.current.window.isOnline 통해 액세스할 수 있습니다.

사용자가 여전히 인터넷에 액세스할 수 있는지 감지합니다.

다크모드 상태

Foreground 플래그와 동일하지만 App.current.window.isDark 통해 액세스할 수 있습니다.

사용자의 브라우저나 운영 체제가 어두운 모드에 있는지 감지합니다.

내부 크기

스크롤 막대를 포함한 창의 콘텐츠 영역(뷰포트) 크기

App.current.window.innerSize 는 내부의 widthheight 값 내의 Size 개체입니다.

@State 변수로도 사용 가능합니다.

외부 크기

도구 모음/스크롤 막대를 포함한 브라우저 창의 크기입니다.

App.current.window.outerSize 는 내부의 widthheight 값 내의 Size 개체입니다.

@State 변수로도 사용 가능합니다.

화면

현재 창이 렌더링되는 화면의 속성을 검사하기 위한 특수 개체입니다. App.current.window.screen 통해 사용 가능합니다.

가장 흥미로운 속성은 일반적으로 pixelRatio 입니다.

역사

사용자가 브라우저 창 내에서 방문한 URL을 포함합니다.

App.current.window.history 또는 History.shared 통해 사용할 수 있습니다.

@State 변수로 액세스할 수 있으므로 필요한 경우 변경 사항을 수신할 수 있습니다.

 App.current.window.$history.listen { history in // read history properties }

단순 변수로도 접근 가능

 History.shared.length // size of the history stack History.shared.back() // to go back in history stack History.shared.forward() // to go forward in history stack History.shared.go(offset:) // going to specific index in history stack

자세한 내용은 MDN 에서 확인할 수 있습니다.

위치

현재 URL에 대한 정보를 포함합니다.

App.current.window.location 또는 Location.shared 통해 사용할 수 있습니다.

@State 변수로 액세스할 수 있으므로 필요한 경우 변경 사항을 수신할 수 있습니다.

예를 들어 라우터가 작동하는 방식입니다.

 App.current.window.$location.listen { location in // read location properties }


간단한 변수로도 접근 가능

 Location.shared.href // also $href Location.shared.host // also $host Location.shared.port // also $port Location.shared.pathname // also $pathname Location.shared.search // also $search Location.shared.hash // also $hash

자세한 내용은 MDN 에서 확인할 수 있습니다.

항해자

브라우저에 대한 정보가 포함되어 있습니다.

App.current.window.navigator 또는 Navigator.shared 통해 사용 가능

가장 흥미로운 속성은 일반적으로 userAgent platform language cookieEnabled .

로컬스토리지

웹 브라우저에 키/값 쌍을 저장할 수 있습니다. 만료 날짜 없이 데이터를 저장합니다.

App.current.window.localStorage 또는 LocalStorage.shared 로 사용할 수 있습니다.

 // You can save any value that can be represented in JavaScript LocalStorage.shared.set("key", "value") // saves String LocalStorage.shared.set("key", 123) // saves Int LocalStorage.shared.set("key", 0.8) // saves Double LocalStorage.shared.set("key", ["key":"value"]) // saves Dictionary LocalStorage.shared.set("key", ["v1", "v2"]) // saves Array // Getting values back LocalStorage.shared.string(forKey: "key") // returns String? LocalStorage.shared.integer(forKey: "key") // returns Int? LocalStorage.shared.string(forKey: "key") // returns String? LocalStorage.shared.value(forKey: "key") // returns JSValue? // Removing item LocalStorage.shared.removeItem(forKey: "key") // Removing all items LocalStorage.shared.clear()

변경 사항 추적

 LocalStorage.onChange { key, oldValue, newValue in print("LocalStorage: key \(key) has been updated") }

모든 항목 제거 추적

 LocalStorage.onClear { print("LocalStorage: all items has been removed") }

세션스토리지

웹 브라우저에 키/값 쌍을 저장할 수 있습니다. 한 세션에 대한 데이터만 저장합니다.

App.current.window.sessionStorage 또는 SessionStorage.shared 로 사용할 수 있습니다.

API는 위에서 설명한 LocalStorage 와 완전히 동일합니다.

문서

브라우저에 로드된 모든 웹 페이지를 나타내며 웹 페이지 콘텐츠에 대한 진입점 역할을 합니다.

App.current.window.document 통해 사용 가능합니다.

 App.current.window.document.title // also $title App.current.window.document.metaDescription // also $metaDescription App.current.window.document.head // <head> element App.current.window.document.body // <body> element App.current.window.documentquerySelector("#my") // returns BaseElement? App.current.window.document.querySelectorAll(".my") // returns [BaseElement]

현지화

정적 현지화

클래식 현지화는 자동으로 이루어지며 사용자 시스템 언어에 따라 달라집니다.

사용하는 방법

 H1(String( .en("Hello"), .fr("Bonjour"), .ru("Привет"), .es("Hola"), .zh_Hans("你好"), .ja("こんにちは")))

동적 현지화

페이지를 다시 로드하지 않고 화면에서 현지화된 문자열을 즉시 변경하려는 경우

전화를 걸어 현재 언어를 변경할 수 있습니다.

 Localization.current = .es

사용자의 언어를 쿠키나 로컬 저장소에 저장한 경우 앱 실행 시 설정해야 합니다.

 Lifecycle.didFinishLaunching { Localization.current = .es }

사용하는 방법

 H1(LString( .en("Hello"), .fr("Bonjour"), .ru("Привет"), .es("Hola"), .zh_Hans("你好"), .ja("こんにちは")))

고급 예

 H1(Localization.currentState.map { "Curent language: \($0.rawValue)" }) H2(LString(.en("English string"), .es("Hilo Español"))) Button("change lang").onClick { Localization.current = Localization.current.rawValue.contains("en") ? .es : .en }

술책

 import FetchAPI Fetch("https://jsonplaceholder.typicode.com/todos/1") { switch $0 { case .failure: break case .success(let response): print("response.code: \(response.status)") print("response.statusText: \(response.statusText)") print("response.ok: \(response.ok)") print("response.redirected: \(response.redirected)") print("response.headers: \(response.headers.dictionary)") struct Todo: Decodable { let id, userId: Int let title: String let completed: Bool } response.json(as: Todo.self) { switch $0 { case .failure(let error): break case .success(let todo): print("decoded todo: \(todo)") } } } }

XMLHttp요청

 import XMLHttpRequest XMLHttpRequest() .open(method: "GET", url: "https://jsonplaceholder.typicode.com/todos/1") .onAbort { print("XHR onAbort") }.onLoad { print("XHR onLoad") }.onError { print("XHR onError") }.onTimeout { print("XHR onTimeout") }.onProgress{ progress in print("XHR onProgress") }.onLoadEnd { print("XHR onLoadEnd") }.onLoadStart { print("XHR onLoadStart") }.onReadyStateChange { readyState in print("XHR onReadyStateChange") } .send()

웹소켓

 import WebSocket let webSocket = WebSocket("wss://echo.websocket.org").onOpen { print("ws connected") }.onClose { (closeEvent: CloseEvent) in print("ws disconnected code: \(closeEvent.code) reason: \(closeEvent.reason)") }.onError { print("ws error") }.onMessage { message in print("ws message: \(message)") switch message.data { case .arrayBuffer(let arrayBuffer): break case .blob(let blob): break case .text(let text): break case .unknown(let jsValue): break } } Dispatch.asyncAfter(2) { // send as simple string webSocket.send("Hello from SwifWeb") // send as Blob webSocket.send(Blob("Hello from SwifWeb")) }

콘솔

간단한 print(“Hello world“) JavaScript의 console.log('Hello world') 와 동일합니다.


콘솔 메소드도 사랑으로 포장됩니다 ❤️

 Console.dir(...) Console.error(...) Console.warning(...) Console.clear()

실시간 미리보기

실시간 미리보기 작업을 수행하려면 원하는 각 파일에서 WebPreview 클래스를 선언하세요.

 class IndexPage: PageController {} class Welcome_Preview: WebPreview { @Preview override class var content: Preview.Content { Language.en Title("Initial page") Size(640, 480) // add here as many elements as needed IndexPage() } }


Xcode

저장소 페이지 의 지침을 읽어보세요. 까다롭지만 완벽하게 작동하는 솔루션입니다 🙂


VSCode

VSCode 내의 Extensions 로 이동하여 Webber를 검색하세요.

설치가 완료되면 Cmd+Shift+P (또는 Linux/Windows에서는 Ctrl+Shift+P )를 누르세요.

Webber Live Preview 찾아 실행합니다.

오른쪽에는 실시간 미리보기 창이 표시되며 WebPreview 클래스가 포함된 파일을 저장할 때마다 새로 고쳐집니다.

자바스크립트에 대한 액세스

SwifWeb 의 기초인 JavaScriptKit을 통해 사용할 수 있습니다.

공식 저장소 에서 방법을 읽어보세요.

자원

프로젝트 내부에 css , js , png , jpg 및 기타 정적 리소스를 추가할 수 있습니다.

하지만 디버그 도중 이나 최종 릴리스 파일에서 사용할 수 있게 하려면 Package.swift 에서 다음과 같이 모두 선언해야 합니다.

 .executableTarget(name: "App", dependencies: [ .product(name: "Web", package: "web") ], resources: [ .copy("css/*.css"), .copy("css"), .copy("images/*.jpg"), .copy("images/*.png"), .copy("images/*.svg"), .copy("images"), .copy("fonts/*.woff2"), .copy("fonts") ]),

나중에 Img().src(“/images/logo.png“) 와 같이 액세스할 수 있습니다.

디버깅

다음과 같은 방법으로 Webber를 시작하십시오.

webber serve 빠르게 실행하기 위한 것입니다.

webber serve -t pwa -s Service

추가 매개변수

-v 또는 --verbose 디버깅 목적으로 콘솔에 추가 정보 표시

-p 443 또는 --port 443 기본 8888 대신 443 포트에서 Webber 서버를 시작합니다.

--browser chrome/safari 원하는 브라우저를 자동으로 엽니다. 기본적으로 어떤 브라우저도 열리지 않습니다.

--browser-self-signed 서비스 워커를 로컬에서 디버그하는 데 필요합니다. 그렇지 않으면 작동하지 않습니다.

--browser-incognito 시크릿 모드에서 브라우저의 추가 인스턴스를 열려면 Chrome에서만 작동합니다.


따라서 디버그 모드에서 앱을 빌드하려면 Chrome에서 자동으로 앱을 열고 파일을 변경할 때마다 브라우저를 자동으로 새로고침하여 이 방법으로 실행하세요.

SPA용

webber serve --browser chrome

실제 PWA 테스트를 위해

webber serve -t pwa -s Service -p 443 --browser chrome --browser-self-signed --browser-incognito


초기 앱 로딩

초기 로딩 프로세스를 개선하고 싶을 수도 있습니다.

이를 위해서는 프로젝트 내부의 .webber/entrypoint/dev 폴더를 열고 index.html 파일을 편집하세요.

여기에는 매우 유용한 리스너가 포함된 초기 HTML 코드( WASMLoadingStarted WASMLoadingStartedWithoutProgress WASMLoadingProgress WASMLoadingError )가 포함되어 있습니다.

사용자 정의 스타일을 구현하고 싶은 대로 해당 코드를 자유롭게 편집할 수 있습니다 🔥

새로운 구현을 마치면 동일한 내용을 .webber/entrypoint/release 폴더에 저장하는 것을 잊지 마세요.

건물 출시

webber release 또는 webber release -t pwa -s Service for PWA를 실행하기만 하면 됩니다.

그런 다음 .webber/release 폴더에서 컴파일된 파일을 가져와 서버에 업로드하세요.

배포 방법

모든 정적 호스팅에 파일을 업로드할 수 있습니다.

호스팅은 wasm 파일에 대한 올바른 콘텐츠 유형을 제공해야 합니다!

예, wasm 파일에 대한 올바른 헤더 Content-Type: application/wasm 갖는 것이 매우 중요합니다. 그렇지 않으면 불행하게도 브라우저가 WebAssembly 애플리케이션을 로드할 수 없습니다.

예를 들어 GithubPages는 wasm 파일에 대한 올바른 Content-Type을 제공하지 않으므로 불행히도 WebAssembly 사이트를 호스팅하는 것은 불가능합니다.

엔진스

nginx와 함께 자체 서버를 사용하는 경우 /etc/nginx/mime.types 열고 application/wasm wasm; 기록. 그렇다면 가셔도 좋습니다!

결론

오늘 제가 여러분을 놀라게 하길 바랍니다. 적어도 SwifWeb을 사용해 보고 다음 대규모 웹 프로젝트에 SwifWeb을 최대로 사용하기 시작할 것입니다!


SwifWeb 라이브러리 에 자유롭게 기여하고 ⭐️모두 별표 표시해 주세요!


저는 Discord에 엄청난 지원을 받고, 작은 튜토리얼을 읽고, 다가오는 업데이트에 대해 먼저 알림을 받을 수 있는 훌륭한 SwiftStream 커뮤니티를 가지고 있습니다! 우리와 함께 만나면 반가울 것 같아요!


이는 단지 시작일 뿐이므로 SwifWeb에 대한 더 많은 기사를 계속 지켜봐 주시기 바랍니다!


친구들에게 알려주세요!