기여하기

이 문서 편집

문서를 번역하거나 컴퓨터에서 전체 프로젝트를 여세요.

작업을 제출하기 전까지는 아무것도 게시되지 않습니다.

QR 코드 서비스

QR 코드 서비스를 만들어 Wails 서비스 알아보기

Wails에서 서비스는 프런트엔드에서 사용할 비즈니스 로직을 포함하는 Go 구조체입니다. 서비스는 관련 기능을 하나로 묶어 코드를 체계적으로 관리할 수 있게 해 줍니다.

서비스는 JavaScript 코드에서 호출할 수 있는 메서드의 모음이라고 생각하면 됩니다. 바인딩을 생성하면 서비스의 각 공개 메서드를 프런트엔드에서 호출할 수 있습니다.

이 튜토리얼에서는 이러한 개념을 설명하기 위해 QR 코드 생성기 서비스를 만들겠습니다. 이 과정을 마치면 서비스를 만들고 종속성을 관리하며 Go 코드를 프런트엔드에 연결하는 방법을 이해하게 됩니다.


1

QR 서비스 파일 만들기

애플리케이션 디렉터리에 qrservice.go이라는 새 파일을 만드세요:

qrservice.go
go
package main

import (
    "github.com/skip2/go-qrcode"
)

// QRService handles QR code generation
type QRService struct {
    // We can add state here if needed
}

// NewQRService creates a new QR service
func NewQRService() *QRService {
    return &QRService{}
}

// Generate creates a QR code from the given text
func (s *QRService) Generate(text string, size int) ([]byte, error) {
    // Generate the QR code
    qr, err := qrcode.New(text, qrcode.Medium)
    if err != nil {
        return nil, err
    }

    // Convert to PNG
    png, err := qr.PNG(size)
    if err != nil {
        return nil, err
    }

    return png, nil
}

이 코드의 동작:

  • QRService은 QR 코드 생성 메서드를 담을 빈 구조체입니다.
  • NewQRService()은 서비스의 새 인스턴스를 만드는 생성자 함수입니다.
  • Generate()은 텍스트와 크기를 받아 QR 코드를 PNG 바이트 배열로 반환하는 메서드입니다.
  • 이 메서드는 오류를 마지막 값으로 반환하는 Go 규칙에 따라 ([]byte, error)을 반환합니다.
  • 실제 QR 코드 생성은 github.com/skip2/go-qrcode 패키지를 사용해 처리합니다.

2

서비스 등록하기

서비스를 만드는 것만으로는 충분하지 않습니다. Wails 애플리케이션이 서비스의 존재를 인식하고 바인딩을 생성할 수 있도록 서비스를 애플리케이션에 등록해야 합니다.

애플리케이션을 만들 때 main.go에서 등록합니다. 서비스 인스턴스를 Services 옵션에 전달하세요:

main.go
go
 func main() {

     app := application.New(application.Options{
         Name:        "myproject",
         Description: "A demo of using raw HTML & CSS",
         LogLevel:    slog.LevelDebug,
         Services: []application.Service{
             application.NewService(NewQRService()),
         },
         Assets: application.AssetOptions{
             Handler: application.AssetFileServerFS(assets),
         },
         Mac: application.MacOptions{
             ApplicationShouldTerminateAfterLastWindowClosed: true,
         },
     })

     app.Window.NewWithOptions(application.WebviewWindowOptions{
         Title:  "myproject",
         Width:  600,
         Height: 400,
     })

     // Run the application. This blocks until the application has been exited.
     err := app.Run()

     // If an error occurred while running the application, log it and exit.
     if err != nil {
         log.Fatal(err)
     }
 }

이 코드의 동작:

  • application.NewService()은 Wails에서 서비스를 관리할 수 있도록 서비스를 래핑합니다.
  • NewQRService()을 호출해 서비스 인스턴스를 만듭니다.
  • 서비스가 애플리케이션 옵션의 Services 슬라이스에 추가됩니다.
  • 이제 Wails는 이 서비스에서 공개 메서드를 검색해 프런트엔드에서 사용할 수 있게 합니다.

3

종속성 설치하기

코드에서 github.com/skip2/go-qrcode 패키지를 참조했지만 아직 실제로 다운로드하지는 않았습니다. Go가 이 종속성을 인식하고 프로젝트에 다운로드하도록 해야 합니다.

프로젝트 디렉터리에서 터미널을 열고 다음 명령을 실행하세요:

bash
go mod tidy

이 명령의 동작:

  • go mod tidy은 Go 파일에서 import 문을 검색합니다.
  • 누락된 패키지(예: go-qrcode)를 다운로드하고 go.mod에 추가합니다.
  • 더 이상 사용하지 않는 종속성도 제거합니다.
  • 이렇게 하면 프로젝트를 성공적으로 컴파일하는 데 필요한 코드가 모두 갖춰집니다.

QR 코드 패키지가 다운로드되어 프로젝트에 추가되었다는 출력이 표시되어야 합니다.


4

바인딩 생성하기

프런트엔드에서 이 메서드들을 호출하려면 바인딩을 생성해야 합니다. 프로젝트 루트 디렉터리에서 wails generate bindings을 실행하면 됩니다.

실행을 마치면 터미널에 다음과 비슷한 내용이 표시되어야 합니다:

bash
 % wails3 generate bindings
 INFO  Processed: 337 Packages, 1 Service, 1 Method, 0 Enums, 0 Models in 740.196125ms.
 INFO  Output directory: /Users/leaanthony/myproject/frontend/bindings

프런트엔드 디렉터리에 bindings이라는 새 디렉터리가 생긴 것을 확인할 수 있습니다:

bash
frontend/
└── bindings
    └── changeme
        ├── index.js
        └── qrservice.js

5

바인딩 이해하기

bindings/changeme/qrservice.js에 생성된 바인딩을 살펴보겠습니다:

bindings/changeme/qrservice.js
js
 // @ts-check
 // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
 // This file is automatically generated. DO NOT EDIT

 /**
  * QRService handles QR code generation
  * @module
  */

 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
 // @ts-ignore: Unused imports
 import {Call as $Call, Create as $Create} from "@wailsio/runtime";

 /**
  * Generate creates a QR code from the given text
  * @param {string} text
  * @param {number} size
  * @returns {Promise<string> & { cancel(): void }}
  */
 export function Generate(text, size) {
     let $resultPromise = /** @type {any} */($Call.ByID(3576998831, text, size));
     let $typingPromise = /** @type {any} */($resultPromise.then(($result) => {
         return $Create.ByteSlice($result);
     }));
     $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
     return $typingPromise;
 }

Generate 메서드의 바인딩이 생성된 것을 확인할 수 있습니다. 매개변수 이름과 주석도 그대로 유지되었습니다. 또한 IDE에 형식 정보를 제공하도록 메서드의 JSDoc도 생성되었습니다.

바인딩은 다음을 제공합니다:

  • Go 메서드에 대응하는 함수
  • Go 형식과 JavaScript 형식 간 자동 변환
  • Promise 기반 비동기 작업
  • JSDoc 주석으로 제공되는 형식 정보

생성된 서비스는 index.js 파일에서 다시 내보내집니다:

bindings/changeme/index.js
js
 // @ts-check
 // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
 // This file is automatically generated. DO NOT EDIT

 import * as QRService from "./qrservice.js";
 export {
     QRService
 };

그러면 파일 이름을 지정하지 않고 Go 패키지 경로만으로 구성된 간소화된 가져오기 경로 ./bindings/changeme을 통해 서비스에 접근할 수 있습니다.


6

프런트엔드에서 바인딩 사용하기

이제 JavaScript에서 Go 서비스를 호출할 수 있습니다! 생성된 바인딩을 사용하면 쉽고 형식 안전하게 호출할 수 있습니다.

새 바인딩을 사용하도록 frontend/src/main.js을 업데이트하세요:

frontend/src/main.js
js
 import { QRService } from './bindings/changeme';

 async function generateQR() {
     const text = document.getElementById('text').value;
     if (!text) {
         alert('Please enter some text');
         return;
     }

     try {
         // Generate QR code as base64
         const qrCodeBase64 = await QRService.Generate(text, 256);

         // Display the QR code
         const qrDiv = document.getElementById('qrcode');
         qrDiv.src = `data:image/png;base64,${qrCodeBase64}`;

     } catch (err) {
         console.error('Failed to generate QR code:', err);
         alert('Failed to generate QR code: ' + err);
     }
 }

 export function initializeQRGenerator() {
     const button = document.getElementById('generateButton');
     button.addEventListener('click', generateQR);
 }

이 코드의 동작:

  • 생성된 바인딩에서 QRService을 가져옵니다.
  • QRService.Generate()은 Go 메서드를 호출합니다. 이 호출은 Promise를 반환하므로 await을 사용합니다.
  • Go 메서드는 []byte을 반환하며, Wails는 이를 JavaScript용 base64 문자열로 자동 변환합니다.
  • PNG 이미지를 표시하기 위해 base64 문자열로 데이터 URL을 만듭니다.
  • try/catch 블록은 잘못된 입력과 같이 Go 측에서 발생한 오류를 처리합니다.
  • Go 코드가 오류를 반환하면 Promise가 거부되고 여기에서 해당 오류를 포착합니다.

이제 initializeQRGenerator 함수에서 새 바인딩을 사용하도록 index.html을 업데이트하세요:

frontend/src/index.html
html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>QR Code Generator</title>
            <style>
                body {
                font-family: Arial, sans-serif;
                display: flex;
                flex-direction: column;
                align-items: center;
                justify-content: center;
                height: 100vh;
                margin: 0;
            }
                #qrcode {
                margin-bottom: 20px;
                width: 256px;
                height: 256px;
                display: flex;
                align-items: center;
                justify-content: center;
            }
                #controls {
                display: flex;
                gap: 10px;
            }
                #text {
                padding: 5px;
            }
                #generateButton {
                padding: 5px 10px;
                cursor: pointer;
            }
            </style>
</head>
<body>
<img id="qrcode"/>
<div id="controls">
    <input type="text" id="text" placeholder="Enter text">
        <button id="generateButton">Generate QR Code</button>
</div>

<script type="module">
    import { initializeQRGenerator } from './main.js';
    document.addEventListener('DOMContentLoaded', initializeQRGenerator);
</script>
</body>
</html>

wails3 dev을 실행해 개발 서버를 시작하세요. 몇 초 후 애플리케이션이 열려야 합니다.

텍스트를 입력하고 “QR 코드 생성” 버튼을 클릭하세요. 페이지 중앙에 QR 코드가 표시되어야 합니다:

QR 코드



7

대안: HTTP 핸들러

지금까지 다음 내용을 살펴보았습니다:

  • 새 서비스 만들기
  • 바인딩 생성하기
  • 프런트엔드 코드에서 바인딩 사용하기

HTTP 핸들러를 사용하는 이유는 무엇인가요?

메서드 바인딩은 데이터 작업에 매우 적합하지만, 파일이나 이미지 또는 기타 미디어를 제공할 때는 다른 방식을 사용할 수도 있습니다. 모든 데이터를 base64로 변환하여 바인딩을 통해 전송하는 대신, 서비스를 소형 웹 서버처럼 작동하게 만들 수 있습니다.

이 방식은 다음과 같은 경우에 유용합니다.

  • 이미지, 동영상 또는 대용량 파일을 제공하는 경우
  • src 속성이 있는 표준 HTML <img> 또는 <video> 태그를 사용하려는 경우
  • 리소스에 URL로 직접 접근해야 하는 경우

서비스가 Go의 표준 ServeHTTP(w http.ResponseWriter, r *http.Request) 메서드를 구현하면 Wails에서 이를 HTTP 엔드포인트로 접근할 수 있게 만들 수 있습니다. QR 코드 서비스가 이 기능을 지원하도록 확장해 보겠습니다.

qrservice.go
go
package main

import (
    "net/http"
    "strconv"

    "github.com/skip2/go-qrcode"
)

// QRService handles QR code generation
type QRService struct {
    // We can add state here if needed
}

// NewQRService creates a new QR service
func NewQRService() *QRService {
    return &QRService{}
}

// Generate creates a QR code from the given text
func (s *QRService) Generate(text string, size int) ([]byte, error) {
    // Generate the QR code
    qr, err := qrcode.New(text, qrcode.Medium)
    if err != nil {
        return nil, err
    }

    // Convert to PNG
    png, err := qr.PNG(size)
    if err != nil {
        return nil, err
    }

    return png, nil
}

func (s *QRService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Extract the text parameter from the request
    text := r.URL.Query().Get("text")
    if text == "" {
        http.Error(w, "Missing 'text' parameter", http.StatusBadRequest)
        return
    }
    // Extract Size parameter from the request
    sizeText := r.URL.Query().Get("size")
    if sizeText == "" {
        sizeText = "256"
    }
    size, err := strconv.Atoi(sizeText)
    if err != nil {
        http.Error(w, "Invalid 'size' parameter", http.StatusBadRequest)
        return
    }

    // Generate the QR code
    qrCodeData, err := s.Generate(text, size)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // Write the QR code data to the response
    w.Header().Set("Content-Type", "image/png")
    w.Write(qrCodeData)
}

여기에서 수행되는 작업:

  • ServeHTTP은 HTTP 요청을 처리하기 위한 Go의 표준 인터페이스입니다.
  • URL에서 쿼리 매개변수를 파싱합니다(?text=hello&size=256).
  • 기존 Generate() 메서드를 호출하여 QR 코드를 생성합니다.
  • 브라우저가 이미지임을 알 수 있도록 콘텐츠 유형을 image/png으로 설정합니다.
  • 원시 PNG 바이트를 응답에 직접 씁니다. base64가 필요하지 않습니다!

이제 QR 코드 서비스에 접근할 경로를 지정하도록 main.go을 업데이트합니다.

main.go
go
 func main() {

     app := application.New(application.Options{
         Name:        "myproject",
         Description: "A demo of using raw HTML & CSS",
         LogLevel:    slog.LevelDebug,
         Services: []application.Service{
             application.NewServiceWithOptions(NewQRService(), application.ServiceOptions{
                 Route: "/qrservice",
             }),
         },
         Assets: application.AssetOptions{
             Handler: application.AssetFileServerFS(assets),
         },
         Mac: application.MacOptions{
             ApplicationShouldTerminateAfterLastWindowClosed: true,
         },
     })

     app.Window.NewWithOptions(application.WebviewWindowOptions{
         Title:  "myproject",
         Width:  600,
         Height: 400,
     })

     // Run the application. This blocks until the application has been exited.
     err := app.Run()

     // If an error occurred while running the application, log it and exit.
     if err != nil {
         log.Fatal(err)
     }
 }

여기에서 수행되는 작업:

  • 서비스를 노출하는 방식을 구성하기 위해 application.ServiceOptions을 추가합니다.
  • Route: "/qrservice"을 사용하면 /qrservice에서 HTTP 핸들러에 접근할 수 있습니다.
  • 이제 /qrservice?text=hello에 대한 모든 요청이 ServeHTTP 메서드를 호출합니다.
  • Route을 설정하지 않으면 HTTP 핸들러 기능이 비활성화됩니다.

마지막으로 base64 인코딩 대신 간단한 이미지 src을 사용하도록 main.js을 업데이트합니다.

frontend/src/main.js
js
async function generateQR() {
    const text = document.getElementById('text').value;
    if (!text) {
        alert('Please enter some text');
        return;
    }

    const img = document.getElementById('qrcode');
    // Make the image source the path to the QR code service, passing the text
    img.src = `/qrservice?text=${encodeURIComponent(text)}`
}

export function initializeQRGenerator() {
    const button = document.getElementById('generateButton');
    if (button) {
        button.addEventListener('click', generateQR);
    } else {
        console.error('Generate button not found');
    }
}

여기에서 수행되는 작업:

  • import와 await QRService.Generate() 호출을 제거했습니다.
  • 대신 img.src이 HTTP 엔드포인트를 가리키도록 설정하기만 하면 됩니다.
  • encodeURIComponent()은 URL의 특수 문자를 안전하게 이스케이프합니다.
  • src을 설정하면 브라우저가 자동으로 HTTP GET 요청을 보냅니다.
  • 이미지에는 이 방식이 더 간단하고 효율적이며 base64 변환도 필요하지 않습니다!

애플리케이션을 다시 실행하면 이전과 동일한 QR 코드가 표시됩니다.

QR 코드



8

동적 구성 지원

하드코딩된 경로의 문제점:

위 예제에서는 JavaScript 코드에 /qrservice 경로를 하드코딩했습니다. 이로 인해 Go 구성과 프런트엔드 코드가 강하게 결합됩니다.

main.go을 편집하여 Route 옵션을 변경하면서 main.js을 업데이트하지 않으면, 애플리케이션이 작동하지 않습니다.

main.go
go
        // ...
            application.NewServiceWithOptions(NewQRService(), application.ServiceOptions{
                Route: "/services/qr",
            }),
        // ...

하드코딩된 경로는 간단한 애플리케이션에서는 사용할 수 있지만, 코드가 변경에 취약해지고 유지 관리하기도 어려워집니다.

해결 방법: 동적 구성

메서드 바인딩과 HTTP 핸들러는 함께 사용할 수 있습니다! 바인딩을 통해 프런트엔드에 사용할 경로를 알려 주면 구성을 동적으로 만들고 하드코딩된 경로를 없앨 수 있습니다.

작동 방식은 다음과 같습니다.

  1. 앱이 시작될 때 ServiceStartup 수명 주기 메서드가 실행됩니다.
  2. 옵션에 구성된 경로를 저장합니다.
  3. 프런트엔드에서 호출하여 올바른 경로를 가져올 수 있는 URL() 메서드를 추가합니다.
  4. 이제 프런트엔드는 경로를 추측하는 대신 Go 서비스에 경로를 요청합니다.

먼저 ServiceStartup 인터페이스를 구현하고 새로운 URL 메서드를 추가합니다.

qrservice.go
go
package main

import (
    "context"
    "net/http"
    "net/url"
    "strconv"

    "github.com/skip2/go-qrcode"
    "github.com/wailsapp/wails/v3/pkg/application"
)

// QRService handles QR code generation
type QRService struct {
    route string
}

// NewQRService creates a new QR service
func NewQRService() *QRService {
    return &QRService{}
}

// ServiceStartup runs at application startup.
func (s *QRService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
    s.route = options.Route
    return nil
}

// Generate creates a QR code from the given text
func (s *QRService) Generate(text string, size int) ([]byte, error) {
    // Generate the QR code
    qr, err := qrcode.New(text, qrcode.Medium)
    if err != nil {
        return nil, err
    }

    // Convert to PNG
    png, err := qr.PNG(size)
    if err != nil {
        return nil, err
    }

    return png, nil
}

// URL returns an URL that may be used to fetch
// a QR code with the given text and size.
// It returns an error if the HTTP handler is not available.
func (s *QRService) URL(text string, size int) (string, error) {
    if s.route == "" {
        return "", errors.New("http handler unavailable")
    }

    return fmt.Sprintf("%s?text=%s&size=%d", s.route, url.QueryEscape(text), size), nil
}

func (s *QRService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Extract the text parameter from the request
    text := r.URL.Query().Get("text")
    if text == "" {
        http.Error(w, "Missing 'text' parameter", http.StatusBadRequest)
        return
    }
    // Extract Size parameter from the request
    sizeText := r.URL.Query().Get("size")
    if sizeText == "" {
        sizeText = "256"
    }
    size, err := strconv.Atoi(sizeText)
    if err != nil {
        http.Error(w, "Invalid 'size' parameter", http.StatusBadRequest)
        return
    }

    // Generate the QR code
    qrCodeData, err := s.Generate(text, size)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // Write the QR code data to the response
    w.Header().Set("Content-Type", "image/png")
    w.Write(qrCodeData)
}

여기에서 수행되는 작업:

  • ServiceStartup에 구성된 경로를 저장하기 위해 route 필드를 추가했습니다.
  • 앱이 시작될 때 ServiceStartup(ctx, options)이 호출되며, 여기에서 경로를 저장합니다.
  • URL() 메서드는 쿼리 매개변수가 포함된 전체 URL을 생성합니다.
  • 구성된 경로가 없으면(경로가 비어 있으면) 오류를 반환합니다.
  • url.QueryEscape()은 URL에서 사용할 수 있도록 텍스트를 안전하게 인코딩합니다.
  • 이 메서드는 바인딩을 통해 프런트엔드에서 사용할 수 있게 됩니다.

이제 하드코딩된 경로 대신 URL 메서드를 사용하도록 main.js을 업데이트합니다.

frontend/src/main.js
js
import { QRService } from "./bindings/changeme";

async function generateQR() {
    const text = document.getElementById('text').value;
    if (!text) {
        alert('Please enter some text');
        return;
    }

    const img = document.getElementById('qrcode');
    // Invoke the URL method to obtain an URL for the given text.
    img.src = await QRService.URL(text, 256);
}

export function initializeQRGenerator() {
    const button = document.getElementById('generateButton');
    if (button) {
        button.addEventListener('click', generateQR);
    } else {
        console.error('Generate button not found');
    }
}

여기에서 수행되는 작업:

  • 바인딩을 다시 사용하기 위해 QRService을 가져옵니다.
  • /qrservice을 하드코딩하는 대신 await QRService.URL(text, 256)을 호출합니다.
  • Go 서비스가 올바른 경로와 매개변수를 사용하여 URL을 생성합니다.
  • 이제 main.go에서 경로를 변경하면 프런트엔드가 새 경로를 자동으로 사용합니다.
  • 이제 Go 구성과 프런트엔드 코드를 수동으로 동기화할 필요가 없습니다!

이전 예제와 똑같이 작동하지만, 이제 main.go에서 서비스 경로를 변경해도 프런트엔드가 작동하지 않는 문제가 발생하지 않습니다.


페이지 편집

마지막 업데이트: