dart:html

dart:html

Dart의 dart:html 라이브러리가 제공하는 주요 기능을 소개해 드릴게요. 이 라이브러리로 브라우저를 프로그래밍하고, DOM의 객체와 요소를 조작하며, HTML5 API를 사용할 수 있어요.

출처: dart:html

본문

경고: dart:html 라이브러리는 더 이상 사용하지 않아요(deprecated). 대신 dart:js_interoppackage:web를 사용하세요.

dart:html 라이브러리를 사용해서 브라우저를 프로그래밍하고, DOM의 객체와 요소를 조작하며, HTML5 API에 접근할 수 있어요. DOM은 Document Object Model의 약자로, HTML 페이지의 계층 구조를 나타내요.

dart:html의 다른 흔한 용도로는 스타일(CSS) 조작, HTTP 요청으로 데이터 가져오기, WebSocket으로 데이터 주고받기 등이 있어요. HTML5(그리고 dart:html)에는 이 섹션에서 다루지 않는 추가 API가 아주 많아요. dart:html은 웹 앱에서만 사용할 수 있고, 커맨드라인(명령줄) 앱에서는 쓸 수 없어요.

참고: 더 큰 애플리케이션이나 이미 Flutter 앱을 만들고 있다면, Flutter for web을 고려해 보세요.

웹 앱에서 HTML 라이브러리를 사용하려면 dart:html을 import 하세요.

import 'dart:html';

DOM 조작하기 (Manipulating the DOM)

DOM을 사용하려면 window(window), document(문서), element(요소), node(노드)에 대해 알아야 해요.

Window 객체는 웹 브라우저의 실제 창을 나타내요. 각 Window에는 현재 로드된 문서를 가리키는 Document 객체가 있어요. Window 객체는 IndexedDB(데이터 저장용), requestAnimationFrame(애니메이션용) 등 다양한 API에 대한 접근자(accessor)도 제공해요. 탭이 있는 브라우저에서는 각 탭마다 자기만의 Window 객체가 있어요.

Document 객체로는 문서 안의 Element 객체를 만들고 조작할 수 있어요. 참고로 문서 자체도 하나의 요소라서 조작할 수 있어요.

DOM은 Node들의 트리를 모델링해요. 이 노드들은 대체로 요소(element)지만, 속성(attribute), 텍스트, 주석(comment) 등 다른 DOM 타입일 수도 있어요. 부모가 없는 루트 노드를 제외하면 DOM의 각 노드는 하나의 부모를 갖고, 여러 자식을 가질 수 있어요.

요소 찾기 (Finding elements)

요소를 조작하려면 먼저 그 요소를 나타내는 객체가 필요해요. 이 객체는 쿼리(query)를 사용해 얻을 수 있어요.

최상위 함수인 querySelector()querySelectorAll()로 하나 이상의 요소를 찾을 수 있어요. ID, 클래스, 태그, 이름, 또는 이들의 조합으로 쿼리할 수 있어요. CSS Selector Specification 가이드는 ID를 지정할 때 # 접두사를, 클래스를 지정할 때 마침표(.)를 쓰는 등 셀렉터의 형식을 정의해요.

querySelector() 함수는 셀렉터와 일치하는 첫 번째 요소를 반환하고, querySelectorAll()은 셀렉터와 일치하는 요소들의 컬렉션을 반환해요.

// Find an element by id (an-id).
Element idElement = querySelector('#an-id')!;

// Find an element by class (a-class).
Element classElement = querySelector('.a-class')!;

// Find all elements by tag (<div>).
List<Element> divElements = querySelectorAll('div');

// Find all text inputs.
List<Element> textInputElements = querySelectorAll('input[type="text"]');

// Find all elements with the CSS class 'class'
// inside of a <p> that is inside an element with
// the ID 'id'.
List<Element> specialParagraphElements = querySelectorAll('#id p.class');

요소 조작하기 (Manipulating elements)

프로퍼티(property)를 사용해서 요소의 상태를 바꿀 수 있어요. Node와 그 하위 타입인 Element는 모든 요소가 갖는 프로퍼티를 정의해요. 예를 들어 모든 요소에는 상태를 설정하는 데 쓸 수 있는 classes, hidden, id, style, title 프로퍼티가 있어요. Element의 하위 클래스들은 추가 프로퍼티를 정의해요. 예를 들어 AnchorElementhref 프로퍼티가 있죠.

HTML에서 앵커 요소를 지정하는 예시를 살펴볼게요.

<a id="example" href="/another/example">link text</a>

<a> 태그는 href 속성과 "link text"라는 문자열을 담은 텍스트 노드(text 프로퍼티로 접근 가능)를 가진 요소를 지정해요. 링크가 가리키는 URL을 바꾸려면 AnchorElementhref 프로퍼티를 사용하면 돼요.

var anchor = querySelector('#example') as AnchorElement;
anchor.href = 'https://dart.dev';

여러 요소에 프로퍼티를 설정해야 할 때도 자주 있어요. 예를 들어 다음 코드는 "mac", "win", "linux" 클래스를 가진 모든 요소의 hidden 프로퍼티를 설정해요. hidden 프로퍼티를 true로 설정하는 것은 CSS에 display: none을 추가하는 것과 같은 효과가 있어요.

<!-- In HTML: -->
<p>
  <span class="linux">Words for Linux</span>
  <span class="macos">Words for Mac</span>
  <span class="windows">Words for Windows</span>
</p>
// In Dart:
const osList = ['macos', 'windows', 'linux'];
final userOs = determineUserOs();

// For each possible OS...
for (final os in osList) {
  // Matches user OS?
  bool shouldShow = (os == userOs);

  // Find all elements with class=os. For example, if
  // os == 'windows', call querySelectorAll('.windows')
  // to find all elements with the class "windows".
  // Note that '.$os' uses string interpolation.
  for (final elem in querySelectorAll('.$os')) {
    elem.hidden = !shouldShow; // Show or hide.
  }
}

적절한 프로퍼티가 없거나 쓰기 불편할 때는 Elementattributes 프로퍼티를 사용할 수 있어요. 이 프로퍼티는 Map<String, String>으로, 키가 속성 이름이에요. 속성 이름과 그 의미의 목록은 MDN Attributes 페이지를 참고하세요. 속성 값을 설정하는 예시는 다음과 같아요.

elem.attributes['someAttribute'] = 'someValue';

요소 만들기 (Creating elements)

새 요소를 만들어 DOM에 붙이면 기존 HTML 페이지에 추가할 수 있어요. 문단(<p>) 요소를 만드는 예시를 살펴볼게요.

var elem = ParagraphElement();
elem.text = 'Creating is easy!';

HTML 텍스트를 파싱해서 요소를 만들 수도 있어요. 자식 요소들도 함께 파싱되고 생성돼요.

var elem2 = Element.html('<p>Creating <em>is</em> easy!</p>');

위 예시에서 elem2ParagraphElement라는 점에 주의하세요.

새로 만든 요소에 부모를 지정하면 그 요소를 문서에 붙일 수 있어요. 어떤 기존 요소의 자식으로든 요소를 추가할 수 있어요. 다음 예시에서 body는 요소이고, 그 자식 요소들은 children 프로퍼티에서 List<Element>로 접근할 수 있어요.

document.body!.children.add(elem2);

노드 추가, 교체, 제거하기 (Adding, replacing, and removing nodes)

요소는 노드의 한 종류라는 점을 기억하세요. Nodenodes 프로퍼티를 사용하면 노드의 모든 자식을 찾을 수 있어요. 이 프로퍼티는 List<Node>를 반환해요(비요소 노드를 생략하는 children과는 달라요). 이 목록을 얻으면 일반적인 List 메서드와 연산자로 노드의 자식들을 조작할 수 있어요.

노드를 부모의 마지막 자식으로 추가하려면 Listadd() 메서드를 사용하세요.

querySelector('#inputs')!.nodes.add(elem);

노드를 교체하려면 NodereplaceWith() 메서드를 사용하세요.

querySelector('#status')!.replaceWith(elem);

노드를 제거하려면 Noderemove() 메서드를 사용하세요.

// Find a node by ID, and remove it from the DOM if it is found.
querySelector('#expendable')?.remove();

CSS 스타일 조작하기 (Manipulating CSS styles)

CSS(cascading style sheets, 계단식 스타일 시트)는 DOM 요소의 표시 스타일을 정의해요. 요소에 ID와 클래스 속성을 붙이면 요소의 모양을 바꿀 수 있어요.

각 요소에는 목록인 classes 필드가 있어요. 이 컬렉션에 문자열을 추가·제거하기만 하면 CSS 클래스도 추가·제거할 수 있어요. 예를 들어 다음 샘플은 요소에 warning 클래스를 추가해요.

var elem = querySelector('#message')!;
elem.classes.add('warning');

요소를 ID로 찾는 경우가 아주 흔하고 효율적이에요. id 프로퍼티로 요소의 ID를 동적으로 설정할 수 있어요.

var message = DivElement();
message.id = 'message2';
message.text = 'Please subscribe to the Dart mailing list.';

메서드 캐스케이드(method cascade)를 사용하면 이 예시의 반복되는 텍스트를 줄일 수 있어요.

var message = DivElement()
  ..id = 'message2'
  ..text = 'Please subscribe to the Dart mailing list.';

ID와 클래스로 요소를 스타일 집합과 연결하는 것이 모범 사례지만, 때로는 특정 스타일을 요소에 직접 붙이고 싶을 때도 있어요.

message.style
  ..fontWeight = 'bold'
  ..fontSize = '3em';

이벤트 처리하기 (Handling events)

클릭, 포커스 변경, 선택 같은 외부 이벤트에 응답하려면 이벤트 리스너(event listener)를 추가해야 해요. 페이지의 어떤 요소에든 이벤트 리스너를 추가할 수 있어요. 이벤트 디스패치(dispatch)와 전파(propagation)는 복잡한 주제라서, 웹 프로그래밍이 처음이라면 세부 내용을 찾아서 공부해 보세요.

element.onEvent.listen(function) 형식으로 이벤트 핸들러를 추가할 수 있어요. 여기서 Event는 이벤트 이름이고 function은 이벤트 핸들러예요.

예를 들어 버튼 클릭을 처리하는 방법은 다음과 같아요.

// Find a button by ID and add an event handler.
querySelector('#submitInfo')!.onClick.listen((e) {
  // When the button is clicked, it runs this code.
  submitData();
});

이벤트는 DOM 트리를 따라 위아래로 전파될 수 있어요. 원래 어느 요소가 이벤트를 발생시켰는지 알아보려면 e.target을 사용하세요.

document.body!.onClick.listen((e) {
  final clickedElem = e.target;
  // ...
});

이벤트 리스너를 등록할 수 있는 모든 이벤트를 보려면 Element와 그 하위 클래스의 API 문서에서 "onEventType" 프로퍼티를 찾아보세요. 흔한 이벤트로는 다음과 같은 것들이 있어요.

  • change
  • blur
  • keyDown
  • keyUp
  • mouseDown
  • mouseUp

HttpRequest로 HTTP 리소스 사용하기 (Using HTTP resources with HttpRequest)

HTTP 요청을 할 때 dart:html을 직접 사용하는 것은 피하는 게 좋아요. dart:htmlHttpRequest 클래스는 플랫폼에 의존적이고 단일 구현에 묶여 있거든요. 대신 package:http 같은 상위 수준의 라이브러리를 사용하세요.

Fetch data from the internet 튜토리얼에서 package:http로 HTTP 요청을 하는 방법을 설명해요.

WebSocket으로 실시간 데이터 주고받기 (Sending and receiving real-time data with WebSockets)

WebSocket을 사용하면 웹 앱이 서버와 양방향으로(interactively) 데이터를 교환할 수 있어요 — 폴링(polling)이 필요 없지요. 서버가 WebSocket을 만들고 ws://로 시작하는 URL(예: ws://127.0.0.1:1337/ws)에서 요청을 기다려요. WebSocket을 통해 전송되는 데이터는 문자열이거나 blob일 수 있어요. 흔히 그 데이터는 JSON 형식의 문자열이에요.

웹 앱에서 WebSocket을 사용하려면 먼저 WebSocket URL을 인자로 넘겨 WebSocket 객체를 만들어요.

var ws = WebSocket('ws://echo.websocket.org');

데이터 보내기 (Sending data)

WebSocket으로 문자열 데이터를 보내려면 send() 메서드를 사용하세요.

ws.send('Hello from Dart!');

데이터 받기 (Receiving data)

WebSocket으로 데이터를 받으려면 message 이벤트에 리스너를 등록하세요.

ws.onMessage.listen((MessageEvent e) {
  print('Received message: ${e.data}');
});

message 이벤트 핸들러는 MessageEvent 객체를 받아요. 이 객체의 data 필드에 서버에서 온 데이터가 있어요.

WebSocket 이벤트 처리하기 (Handling WebSocket events)

앱은 다음 WebSocket 이벤트를 처리할 수 있어요: open, close, error, 그리고 (앞서 본 것처럼) message. 다음은 WebSocket 객체를 만들고 open, close, error, message 이벤트에 핸들러를 등록하는 메서드의 예시예요.

void initWebSocket([int retrySeconds = 1]) {
  var reconnectScheduled = false;

  print('Connecting to websocket');

  void scheduleReconnect() {
    if (!reconnectScheduled) {
      Timer(
        Duration(seconds: retrySeconds),
        () => initWebSocket(retrySeconds * 2),
      );
    }
    reconnectScheduled = true;
  }

  ws.onOpen.listen((e) {
    print('Connected');
    ws.send('Hello from Dart!');
  });

  ws.onClose.listen((e) {
    print('Websocket closed, retrying in $retrySeconds seconds');
    scheduleReconnect();
  });

  ws.onError.listen((e) {
    print('Error connecting to ws');
    scheduleReconnect();
  });

  ws.onMessage.listen((MessageEvent e) {
    print('Received message: ${e.data}');
  });
}

더 알아보기