콘텐츠로 이동

HTML 목록과 HTTP 연결

기본 HTML·컴포넌트 문법은 내장 언어, 공유 상태는 Store에서 읽습니다. 여기서는 항목 선택과 실제 HTTP 처리 흐름을 연결합니다. 콘솔 native가 아닌 HTML frontend 생성 대상의 예제입니다.

표현 역할
ub:each="label in this.labels" 반복 항목을 현재 행 범위에 노출
ub:key={label} 항목의 안정된 식별자. 중복되지 않는 값 선택
ub:if={this.selected != ""} bool 식이 참인 경우 표시
on:click={this.choose(label)} 현재 행의 값을 이벤트에 전달

반복 지시자는 ub:for가 아닌 ub:each입니다. 기본 이벤트의 메서드 참조와 행 범위 이벤트의 인수 전달을 구분하세요. 목록을 재할당한 뒤 선택·빈 목록 표시가 맞는지도 확인합니다.

RequestState는 요청 중 상태를 공유하고 중복 요청을 막습니다. 응답 handler가 성공과 실패 모두에서 loading을 해제합니다. 결과는 response.body() 그대로 보여 주므로 JSON을 정상 업무 결과로 해석하려면 별도의 응답 스키마 검사가 필요합니다.

Vue·React·Svelte 탭을 선택하면 이 원본을 서버에서 해당 프론트엔드 프로젝트로 변환한 실제 생성 파일을 보여 줍니다.

import unibase.core.ui.*;
import unibase.core.ui.web.*;
import unibase.core.ui.window.*;

@Store(reactivity = "explicit")
public class RequestState {
    @Reactive public bool loading = false;
    @Reactive public int status = 0;
    @Reactive public string result = "아직 요청하지 않았습니다.";
    @Reactive public string error = "";
    public HtmlHttpCall active;

    public void send(string path) {
        if (loading) { return; }
        var client = HtmlHttpClient(HtmlHttpEnvironment().origin("DOCS_API"), 3000);
        string tags = client.jsonArray(["string", "string"], ["docs", "example"]);
        string body = client.jsonBody(["count", "tags"], ["int", "json"], ["2", tags]);
        loading = true;
        error = "";
        active = client.requestWithHeaders("POST", path, body,
            ["X-Example"], ["manual"], RequestLoaded());
    }
    public void cancel() {
        if (loading) { active.cancel(); }
    }
}

public class RequestLoaded implements HtmlHttpHandler {
    public void complete(HtmlHttpResponse response) {
        var state = RequestState();
        state.loading = false;
        state.status = response.status();
        state.error = response.error();
        state.result = response.body();
    }
}

public class ExamplePage extends HtmlPage {
    private List<string> labels = ["문법", "라이브러리"];
    private string selected = "";
    private final StyleSheet sheet = css {
        main { max-width: 48rem; margin: 0 auto; padding: 32px 20px; font-family: sans-serif; color: #172033; }
        h1 { font-size: 28px; }
        ul { padding: 0; list-style: none; display: flex; flex-wrap: wrap; gap: 12px; }
        button { padding: 10px 16px; border: 1px solid #59718f; border-radius: 8px; background: #f4f7fc; color: #172033; cursor: pointer; }
        button:focus-visible { outline: 3px solid #315bdd; outline-offset: 3px; }
        button:disabled { opacity: 0.6; cursor: wait; }
        .actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 24px; }
        pre { padding: 16px; background: #f4f7fc; white-space: pre-wrap; overflow-wrap: anywhere; }
    };
    public void choose(string label) { this.selected = label; }
    public void load() { RequestState().send("/echo"); }
    public void fail() { RequestState().send("/failure"); }
    public void slow() { RequestState().send("/slow"); }
    public void cancel() { RequestState().cancel(); }
    public Widget build(HtmlRouteContext route) {
        return this.sheet.apply(html {
            <main ub:id="example-main">
                <h1>목록과 서버 연결</h1>
                <p>항목을 선택하고 테스트 API의 결과를 확인하세요.</p>
                <ul ub:each="label in this.labels" ub:key={label}>
                    <li><button on:click={this.choose(label)}>{label}</button></li>
                </ul>
                <p ub:if={this.selected != ""}>선택: {this.selected}</p>
                <div class="actions">
                    <button on:click={this.load} disabled={RequestState().loading}>요청 보내기</button>
                    <button on:click={this.fail} disabled={RequestState().loading}>오류 응답</button>
                    <button on:click={this.slow} disabled={RequestState().loading}>느린 요청</button>
                    <button on:click={this.cancel}>요청 취소</button>
                </div>
                <p role="status">진행 중: {RequestState().loading}</p>
                <p>HTTP 상태: {RequestState().status}</p>
                <p>오류: {RequestState().error}</p>
                <pre>{RequestState().result}</pre>
            </main>
        });
    }
}

public class ExampleRouter extends HtmlRouter {
    public List<HtmlRoute> buildRoutes() {
        return [{ path: "/", page: ExamplePage() }];
    }
}

public class Application {
    public static void main() {
        HtmlApplication application = HtmlApplication(ExampleRouter());
        Window window = Window("목록과 서버 연결", 960.0, 720.0, "ko", "ltr", application.build());
        window.show();
    }
}

언어 탭의 결과는 서버가 생성한 파일입니다. 변환 성공은 생성된 프로그램의 빌드·실행 성공을 의미하지 않습니다.

소스에는 논리 이름 DOCS_API를 사용하고 생성 환경에서 실제 origin을 연결합니다. 다음은 직접 준비한 로컬 시험 API에 대한 환경 예시입니다. 이 주소에 서버가 자동으로 제공되는 것은 아닙니다.

{
"schemaVersion": 1,
"connections": [{ "reference": "DOCS_API", "origin": "http://127.0.0.1:4417" }]
}

전체 소스와 환경은 ERWeb의 해당 HTML 생성 profile 또는 준비된 project host에 전달합니다. export-ui 기본 명령만 실행해 환경 바인딩까지 준비되었다고 가정하지 마세요. 입력 계약과 출력 프로젝트는 코드 생성 대상을 참고하세요.

이번 예제에서 확인한 project host 입력은 다음과 같습니다. content의 자리표시자를 위 WebApp.uni의 전체 UTF-8 텍스트로 바꿉니다. contractVersion: 1의 Vue JavaScript 경로를 검증했으며 이 입력의 버전만 올려 TypeScript 지원으로 해석하지 않습니다.

web-request.json · content는 전체 소스로 교체
{
"protocolVersion": 2,
"contractId": "UB-FE-CONTRACT-001",
"contractVersion": 1,
"requestId": "manual-web",
"operation": "generateFrontend",
"entryKind": "application",
"target": "vue",
"files": [{ "id": "web", "path": "WebApp.uni", "content": "<전체 소스로 교체>" }],
"resources": [],
"environment": {
"schemaVersion": 1,
"connections": [{ "reference": "DOCS_API", "origin": "http://127.0.0.1:4417" }]
}
}

생성 호출 예제의 generate.mjs는 세 번째 인수로 요청 파일을 받습니다. 새 작업 폴더에서 node generate.mjs <unic-project 경로> web-request.json으로 호출하면 예제 스크립트의 generated-project 폴더에 화면 프로젝트가 저장됩니다. 폴더 이름과 무관하게 실제 생성 대상은 요청의 target이며, 출력 README에 따라 의존성을 준비하고 npm run build로 빌드합니다.

시험 API는 POST /echo에서 JSON body를 돌려주고, /failure에서는 오류 상태, /slow에서는 지연 응답을 반환하도록 준비합니다. 브라우저와 API origin이 다르면 서버의 CORS 허용도 필요합니다. 테스트 요청 header는 X-Example: manual이며 실제 인증 토큰은 포함하지 않습니다.

API 용도
HtmlHttpClient(origin, timeoutMillis) 연결 origin과 제한 시간
queryPath(path, names, values) query 값 인코딩
jsonBody(names, types, values) 타입이 있는 JSON 객체 생성
jsonArray(types, values) JSON 배열 생성. json 타입으로 중첩 가능
request(method, path, body, handler) 기본 요청
requestWithHeaders(..., headerNames, headerValues, handler) 이름·값 목록으로 header 전달
HtmlHttpCall.cancel() 진행 중 요청 취소
HtmlHttpResponse.status/body/error HTTP 상태·본문·전송 오류 구분

header 이름/값의 개수, 금지 header와 body의 타입·개수를 검사합니다. 사용자 입력을 직접 JSON 문자열로 이어 붙이지 마세요. 외부 origin·redirect·credentials는 runtime의 허용 정책을 따릅니다. requestKey()는 요청 식별에 사용하며 서버의 멱등 처리를 자동 제공하지 않습니다.

HTTP 503처럼 서버가 응답한 오류는 status()와 본문으로 판단합니다. 이때 error()가 빈 문자열일 수 있습니다. error()는 취소·시간 초과·전송 실패를 확인할 때 사용하므로 빈 문자열만 보고 업무 성공으로 처리하지 마세요.

이 예제의 Vue JavaScript 생성 경로에서는 취소와 시간 초과가 모두 status 0과 브라우저의 abort 메시지로 전달됩니다. 두 경우를 서로 다른 업무 상태로 표시하려면 사용자의 취소 동작·요청 시작 시점도 상태로 관리해야 합니다. 오류 문자열만으로 두 원인을 구별하지 마세요.

화면 문구는 @I18n 클래스의 메시지 메서드로 선언합니다. 소스의 어노테이션이 유일한 번역 원본이며 별도 번역 JSON은 사용하지 않습니다. 컴파일하면 메시지는 일반 생성 메서드가 되고, Vue·React·Svelte 화면은 각 프레임워크의 코드와 상태로 생성합니다.

Messages.uni
import unibase.core.ui.web.*;
@I18n(defaultLocale = "ko", strict = true)
@I18nLocale(tag = "ko", name = "한국어", direction = "ltr", fallback = "", enabled = true)
@I18nLocale(tag = "en", name = "English", direction = "ltr", fallback = "", enabled = true)
public class Messages {
@Message(key = "welcome", id = "message-stable-id", allowEmpty = false)
@Translation(locale = "ko", text = "안녕하세요 {name}")
@Translation(locale = "en", text = "Hello {name}")
public static native string welcome(string locale, string name);
}
  • 첫 매개변수는 string locale이고 나머지는 string·int·long·double·bool 타입의 이름 있는 인수입니다. 모든 번역은 선언한 인수를 빠짐없이 사용해야 하며 선언하지 않은 이름은 쓸 수 없습니다. 메시지 메서드는 오버로드할 수 없습니다.
  • {name}은 인수를 문자열로 넣고, {count, plural, one {…} other {# items}}와 {gender, select, … other {…}}로 분기합니다. other는 필수이며 중첩·offset·정확한 값 선택자·서수는 거절됩니다.
  • locale은 완전한 BCP 47 태그와 방향을 명시합니다. 중복 태그, 없는 fallback 대상, 순환 fallback은 오류입니다. strict = true 카탈로그는 활성 언어의 번역이 모두 있어야 생성되고, strict가 아니면 선언한 fallback만 따릅니다. 빈 문구는 allowEmpty가 있어야 허용됩니다.
  • HTML 본문과 문자열 속성에서 메시지 메서드를 호출할 수 있습니다. 브라우저 도우미 I18n.number·currency·instant·dateOnly는 형식화를, activate(locale, direction)는 문서의 lang·dir 변경을 담당합니다. SSR·지연 로딩·rich text는 이 계약의 범위가 아닙니다.
  1. 두 항목을 각각 선택하고 올바른 선택 문구가 나타나는지 확인합니다.
  2. 요청 후 loading이 표시되고 중복 요청 버튼이 비활성화되는지 봅니다.
  3. 정상 응답의 status·본문과 API가 받은 header·배열 값을 비교합니다.
  4. HTTP 오류와 네트워크 오류를 분리하고, 지연 요청 취소 후 loading이 해제되는지 확인합니다.
  5. 좁은 화면·키보드·빈 응답에서도 잘림과 접근 불가가 없는지 봅니다.

서버 연결 없는 preview·목 응답은 실제 네트워크 검증이 아닙니다. 이 장의 확인 결과는 실행 확인 범위에서 대상과 함께 읽으세요.