2020년 2월 23일 일요일

[ML] LightGBM 파라미터 정리.

LightGBM은 더 적은 자원을 쓰면서 빠르기까지 해서 xgboost의 대안 알고리즘으로 자주 거론된다. xgboost의 성능은 매우 뛰어나지만 파라미터 튜닝이 복잡하고 학습 시간이 느린 단점이 있다. LightGBM은 xgboost 대비 비슷한 성능을 내면서도 빠른 학습 속도를 제공한다.

ML 알고리즘의 경우 성능이 조금 떨어지더라도 속도가 빠르면 여러가지 장점이 있는데, 속도가 빠르면 FE에 더 많은 시간을 할애할 수 있기도 하고, grid search 등으로 빠르게 튜닝해서 오히려 더 나은 결과를 뽑아낼 수도 있기 때문이다.

LightGBM은 기존 알고리즘의 level-wise(균형 트리 방식)가 아닌 leaf-wise 방식으로 학습이 진행된다. 기존 방식에선 트리의 균형을 우선하는데 트리의 균형을 위해 추가되는 연산이 학습 속도를 떨어뜨리는 요인이 되었다. LightGBM은 트리의 규형보다는 리프 노드를 지속적으로 분할하면서 학습을 진행한다. 결과로 비대칭적인 깊은 트리가 생성되지만 level-wise 방식에서 리프 노드가 생성될 때보다 손실을 줄이는 장점이 있다고 한다.


자주 참조되는 파라미터는 아래와 같다. 지원되는 부스터 중 dart는 따로 공부해야 할 주제로 보인다. 여기서 속도 개선을 위한 CPU, GPU 관련 파라미터도 별도로 존재하는데 관련 파라미터는 아래의 링크로 대신한다.
https://testlightgbm.readthedocs.io/en/latest/Parameters.html

• objective: 목표 설정. 기본 값은 'regression'.
 - regression : 회귀.
   - regression_l2 (별칭: mean_squared_error, mse)
  - regression_l1 (별칭: mean_absolute_error, mae)
 - binary : 이진 분류.
 - multiclass : 멀티-클래스 분류. num_class로 분류 가짓수를 정해주어야 한다.

• boosting: 부스팅 방식을 설정. gbdt가 가장 많이 쓰인다.
 - gbdt : gradient boosting decision tree
 - dart : dropouts meet multiple additive regression trees
 - goss : gradient-based one-side sampling

• data (별칭: train, train_data) : 학습용 데이터

• valid (별칭: test, valid_data, test_data) : 검증용 데이터

• num_iterations: 부스팅 반복 횟수. 학습에 사용할 트리 개수를 뜻한다. 멀티-클래스 분류 문제인 경우 num_class * num_iterations 만큼의 트리가 사용된다.

• learning_rate: 학습 속도. 기본 값은 0.1. 값이 크면 건성건성 학습하고, 작으면 학습용 데이터에 너무 피팅되므로 실험을 통해 적정값으로 조절해주어야 한다.

• num_leaves: 하나의 트리에서 사용될 수 있는 최대 리프 노드 개수. 기본 값은 31.

• max_depth: 트리 깊이의 제한. 데이터가 적은 가운데 트리 깊이가 깊어지면 오버-핏 되는 경향이 있다. 기본 값은 제한 없음인데 -1로 표시한다. 기본 값은 -1.

• min_data_in_leaf: 리프 노드가 되기 위한 최소 샘플 데이터 수. 기본 값은 20. 이것 역시 오버-핏이 발생하지 않게끔 해주는 파라미터. 기본 값은 20.

• feature_fraction: 각 이터레이션에서 사용할 feature의 비율. LightGBM은 각 이터레이션마다 여기에 설정된 비율 만큼의 feature를 랜덤하게 선택해 학습을 진행한다. 오버-핏을 다루는 파라미터이지만 부가적으로 학습 속도를 높이는 효과도 있다. 0~1 사이의 값을 지정해야 하고, 기본 값은 1.

• bagging_fraction: feature_fraction이 column 방향으로의 random selection이라면 bagging_fraction은 row 방향으로의 random selection이다. LightGBM이 각 이터레이션마다 쓸 데이터의 비율을 뜻한다. 기본 값은 1.

• bagging_freq: bagging 수행의 빈도. 몇 번의 이터레이션 마다 bagging을 할 것인지 지정한다. 기본 값은 0으로 사용하지 않음을 의미.

• early_stopping_round: 학습 과정에서 측정된 스코어가 향상되지 않으면 중간에 학습을 중단하게 한다. 기본 값은 0.

• lamgda_l1, lambda_l2: l1 정규화, l2 정규화. 어느 정도의 값을 지정해야 하는지 난 아직 잘 모르겠다.

• min_gain_to_split: 트리 분기를 수행하기 위한 최소 기준. 무작정 분기할 순 없으니 의미 있는 feature의 의미 있는 값 기준으로 트리가 분기할 수 있도록 알고리즘에 알려주기 위함이다. 마찬가지로 적정 값을 어떻게 정해주어야 하는지 잘 모르겠음.

• categorical_feature: 카테고리 변수를 LightGBM에 알려주는 용도. 카테고리 변수를 기존엔 원핫인코딩 방식으로 지정해주곤 했는데 이렇게 했을 경우 속도도 느려지고 트리의 밸런스가 안좋게 만들어지는 경우가 생긴다. LightGBM에 카테고리 변수를 따로 지정해주면 LightGBM이 알아서 잘 처리해준다.

• metric: 평가식. regression인 경우 l2(mean_squared_error)가 기본값. 이진 분류에선 binary_logloss가 기본 값이다. 멀티-클래스 분류인 경우 multi_logloss, multi_error가 쓰인다. auc 평가식도 많이 쓰이는 값.

https://lightgbm.readthedocs.io/en/latest/Python-Intro.html


2020년 2월 20일 목요일

[Java] MyBatis #{}과 ${} 차이점.

#{}는 MyBatis가 제공하는 매개 변수 표기법으로 MyBatis에게 PreparedStatement 매개 변수를 작성하도록 알려준다. SQL Injection을 예방해주기 때문에 MyBatis에서 권장되는 방식이다.

${}는 문자열 대체 기능으로 (매개 변수에 사용할 수도 있지만) 주 용도는 SQL 문의 메타 데이터(테이블 이름 또는 컬럼 이름)가 동적일 때 일부 문자를 대체해주기 위해 쓰인다. MyBatis는 ${} 내 데이터를 변경하거나 이스케이프 처리하지 않는다.

${} 방식을 쓸 때엔 주의할점이 있다. 사용자로부터 입력 받은 값을 확인하지 않고 바로 구문에 전달해서는 안되는데 SQL 주입 공격으로부터 안전하지 않기 때문이다. 사용자 입력값을 ${}를 이용해 구문으로 전달해야만 하는 로직이 있다면(구체적으로 그럴 일이 있는진 모르겠다만.) 자체적으로 값에 이상이 없는지 검증 과정을 거쳐야 한다.

별도로 데이터베이스 수행 계획에 영향을 주기도 하는데 아래 링크에서 내용을 확인할 수 있다.
https://lng1982.tistory.com/246

테이블에 담긴 데이터의 성격에 따라 ${} 방식을 사용했을 때 성능 상 이점이 생길 수 있다는 내용이다.
그러나 이 정도까지의 튜닝이 필요한 환경에서 일하는 경우는 드물기 때문에 #{}로 사용이 가능한 경우엔 가급적 #{}를 쓰는게 나을 것이다.


공식 페이지: https://mybatis.org/mybatis-3/ko/sqlmap-xml.html

2020년 2월 12일 수요일

규모에 따라 서비스 확장하기 (Application Scaling)

웹과 모바일 앱 애플리케이션은 일반적으로 3개의 컴포넌트로 구성된다.

  • 데이터베이스
  • API
  • 클라이언트

데이터베이스는 데이터를 보관하고, API는 요청에 따른 데이터를 서빙한다. 그리고 클라이언트는 데이터를 렌더링해 사용자에게 표시한다. 클라이언트와 API를 구분하는 이유는, 클라이언트와 API를 완전히 별개의 개체로 생각하고 미리 구분해두면 애플리케이션 확장에 대한 추론이 훨씬 간단해지기 때문이다.


다음은 서비스를 시작한 뒤 서서히 사용자(트래픽)가 증가하게 될 때 애플리케이션을 어떻게 구성(대응)해야 하느냐에 대한 가이드다. 원문은 아래 링크에 있다. (아래의 내용은 원문에 대한 번역이 아니기 때문에 영어에 익숙하다면 원문의 내용을 참고하는 것이 좋다.)
https://alexpareto.com/scalability/systems/2020/02/03/scaling-100k.html


1~10 Users : 하나의 서버

아주 적은 사용자(1~10명)를 대상으로 할 땐 위의 세가지 컴포넌트를 하나의 서버에서 실행되게 하는 것이 좋을 것이다. 한 명의 엔지니어가 데이터베이스, API, 클라이언트를 모두 통제할 수 있기 때문이다.


10~100 Users : 데이터베이스 분리

사용자가 10~100명 수준이 된다면 가장 먼저 고려할 것은 데이터베이스를 물리적으로 분리하는 것이다. 클라우드 환경이라면 아마존 RDS 같은 것이 될 수 있는데, 비용이 조금 비싸지지만 멀티-리젼 중복, 읽기 전용 복제본 생성, 자동 백업 등 다양한 기능을 쉽게 추가할 수 있다.


100~1000 Users : 클라이언트 분리

사용자가 100명을 넘어서면서 트래픽이 안정적으로 나와주면 클라이언트 컴포넌트 까지도 분리를 고려해야 한다. 앞서 적힌 내용처럼 최초 구현 단계에서부터 클라이언트와 API 컴포넌트를 명확히 구분지어 놓으면 확장이 쉬워진다. (API 입장에선 클라이언트가 웹이든 데스크탑이든 그저 같은 API를 사용하는 클라이언트일 뿐이다.)


1000+ Users : Load balancer

하나의 API 인스턴스가 모든 트래픽을 감당하기 어려워지게 되므로 더 많은 컴퓨팅 파워가 필요하게 된다.

로드 밸런서가 투입된다. API 앞에 로드 밸런서를 배치하고 트래픽을 분산시키는 방식이다. 이를 통해 수평적 확장이 가능하게 된다. 동일한 코드를 실행하는 서버를 추가할수록 처리할 수 있는 요청량은 늘어나게 된다. 로드 밸런서는 트래픽이 가장 적은 인스턴스로 요청을 라우팅한다. (물론 처음 연결맺은 API 인스턴스와 계속 통신하게 할수도 있다.)


10,000+ Users : CDN

정적 컨텐츠를 API 서버로부터 구분한다. 이 작업은 초기에 해두는 것이 좋긴 하다. 그리고 CDN 도입을 고려해야 한다. CDN은 전 세계의 다른 데이터 센터에 정적인 데이터를 자동으로 캐시한다.


100,000+ Users: 데이터베이스에 대한 확장

데이터베이스에 대한 스케일링 작업은 매우 어려운 편이다. API 서버의 경우 무상태(stateless)로 구성할 수 있어 확장이 간편하지만 데이터베이스의 경우 '상태'를 갖기 때문이다.

데이터베이스의 부하를 낮추기 위한 가장 간편하고 효과적인 선택은 데이터 보관에 대한 캐시 레이어를 마련하는 것이다. Redis 또는 Memcached와 같은 키-밸류 기반의 메모리 저장 솔루션이 이를 지원한다. 특히 Redis의 경우 자체로 클러스터링을 지원하기 때문에 확장으로부터 안전하다. 여기서 더 할 수 있는 일은 읽기에 대한 복제본(Read replica)를 추가하거나 파티셔닝, 샤딩 등의 기법을 사용하는 것이 도움이 된다.


+ 모니터링 도구
New Relic 또는 Datadog과 같은 서비스를 이용해 시스템을 모니터링할 수 있다. 이를 통해 요청이 느리고 개선이 필요한 부분을 파악할 수 있을 것이다.

2020년 2월 11일 화요일

[WPF] ContentControl과 Template

들여다볼 때마다 헷갈리는 개념.
네이버 쪽 블로그 보니 이미 작년에 한번 정리했던 내용이다.
https://blog.naver.com/seokcrew/221584014717

ContentControl은 단일 내용으로 된 컨트롤을 나타낸다. Button의 부모인 ButtonBase나 ScrollViewer 같은 컨트롤은 이 클래스를 직접 상속받고 있다.

ContentControl은 외형을 결정짓는 속성으로 Template과 ContentTemplate을 가질 수 있다.
  • Template의 값 유형은 ControlTemplate이며 컨트롤의 모양을 정의한다.
  • ContentTemplate의 값 유형은 DataTemplate이며 데이터의 모양을 정의한다.

ContentPresenter는 ContrentControl의 ControlTemplate에 배치될 때 ContentControl의 Content를 자동으로 가져와서 표시해 준다. ContentControl 클래스에 의해 표시되는 Content가 런타임에 결합될 위치를 지정해준다.
  • ContentControl 상속 구조 : FrameworkElement → Control → ContentControl
  • ContentPresenter 상속 구조 : FrameworkElement → ContentPresenter

* ContentControl의 ContentTemplate을 정의하는 DataTemplate은 다시 ContentControl을 갖을 수 있고, ContentControl은 별도로 하나의 자식 요소를 가질 수 있는데 다시 각각의 ContentControl은 ControlTemplate을 갖을 수 있기 때문에 여러 단계로 중첩된 구조가 생기면 한 눈에 파악하기 어려워진다.


2020년 2월 9일 일요일

Socket.io 요약

socket.io는 브라우저와 서버 간 이벤트 기반으로 양방향 통신을 가능하게 하는 라이브러리다. 내부적인 heartbeat 메커니즘을 통해 자동 재접속과 연결 끊어짐 감지를 해주기 때문에 사용하는 입장에서 라이브러리를 믿고 간편하게 쓸 수 있다.

socket.io는 사용자가 원하는 어떤 형식으로든 이벤트와 데이터 형식을 만들어 쓸 수 있게끔 지원하고 있다. 일반적으론 JSON 포맷을 쓰게 되겠지만 binary 데이터 형식의 송수신도 지원한다.
브라우저와 서버 간 이벤트를 주고받는 방법은 단순하다. 기본적으로 아래 2개의 메서드로 구현된다.
  • emit: 이벤트를 발생시킨다.
  • on: 이벤트를 수신한다.

참조: https://socket.io/docs/


Room

room 기능도 제공한다. 임의의 채널을 정의해서 특정 소켓을 그루핑 시킬 수 있는데 간단하게 생각하면 채팅방과 같은 개념으로 이해할 수 있을 것이다. namespace로 구분되는 room을 한정해서 제한된 데이터 교환이 가능하다. 이는 응용 측면에서만 아니라 설계 측면에서 리소스를 줄여주는 역할도 하기 때문에 유용하다.
기본 namespace 이름은 '/' 이다. namespace를 지정하고 싶으면 io.of() 함수를 이용하면 된다. (클라이언트에선 io()의 파라미터로 namespace 값을 전달.)


Broadcasting

브로드캐스팅의 경우 io객체의 emit() 함수를 이용하면 되는데, 발송자를 제외한 브로드캐스팅이 필요한 경우엔 socket.broadcast.emit() 함수를 이용하여야 한다. namespace를 한정해 이벤트를 발생시킬 수도 있는데 io.to() 함수를 이용하면 된다.

참조: https://socket.io/docs/rooms-and-namespaces/



** 채팅 솔루션이 급하다면 아래 링크를. 코드를 보면 알겠지만 FE가 할 일이 더 많다. 그만큼 편하게 되어있다는 뜻.
https://github.com/socketio/socket.io/tree/master/examples/chat



서버 기본 코드. 아래 꼴에서 크게 달라지지 않는다.

// Express initializes 'app' to be a function handler that you can supply to an HTTP server.
var app = require('express')();
var http = require('http').createServer(app);

var io = require('socket.io')(http);

// define a route handler '/' that gets called when we hit our website home.
app.get('/', function (req, res) {
    res.sendFile(__dirname + '/index.html');
});

// listen on the 'connection' event for incoming sockets.
io.on('connection', function (socket) {
    console.log('a user connected');
    socket.on('disconnect', function() {
        console.log('user disconnected');
    });

    socket.on('chat message', function (msg) {
        io.emit('chat message', msg);
    });
});

// http server listen on port 3000
http.listen(3000, function () {
    console.log('listening on *:3000');
});


위 코드와 대응되는 클라이언트 측 스크립트.

// client
$(function() {
    var socket = io();
    $('form').submit(function (e) {
        e.preventDefault();
        socket.emit('chat message', $('#m').val());
        $('#m').val('');
        return false;
    });

    socket.on('chat message', function (msg) {
        $('#messages').append($('<li>').text(msg));
    });
});


아래는 공식 페이지에서 제공하는 emit cheatsheet.

io.on('connect', onConnect);

function onConnect(socket){

  // sending to the client
  socket.emit('hello', 'can you hear me?', 1, 2, 'abc');

  // sending to all clients except sender
  socket.broadcast.emit('broadcast', 'hello friends!');

  // sending to all clients in 'game' room except sender
  socket.to('game').emit('nice game', "let's play a game");

  // sending to all clients in 'game1' and/or in 'game2' room, except sender
  socket.to('game1').to('game2').emit('nice game', "let's play a game (too)");

  // sending to all clients in 'game' room, including sender
  io.in('game').emit('big-announcement', 'the game will start soon');

  // sending to all clients in namespace 'myNamespace', including sender
  io.of('myNamespace').emit('bigger-announcement', 'the tournament will start soon');

  // sending to a specific room in a specific namespace, including sender
  io.of('myNamespace').to('room').emit('event', 'message');

  // sending to individual socketid (private message)
  io.to(`${socketId}`).emit('hey', 'I just met you');

  // WARNING: `socket.to(socket.id).emit()` will NOT work, as it will send to everyone in the room
  // named `socket.id` but the sender. Please use the classic `socket.emit()` instead.

  // sending with acknowledgement
  socket.emit('question', 'do you think so?', function (answer) {});

  // sending without compression
  socket.compress(false).emit('uncompressed', "that's rough");

  // sending a message that might be dropped if the client is not ready to receive messages
  socket.volatile.emit('maybe', 'do you really need it?');

  // specifying whether the data to send has binary data
  socket.binary(false).emit('what', 'I have no binaries!');

  // sending to all clients on this node (when using multiple nodes)
  io.local.emit('hi', 'my lovely babies');

  // sending to all connected clients
  io.emit('an event sent to all connected clients');

};


2020년 2월 6일 목요일

[Java] Callable, Future, FutureTask

Callable, Futre는 Java 1.5에 추가된 Concurrency API 사양이다.

Callable은 Runnable과 주로 비교된다.

Callable vs Runnable
Runnable의 run() 메서드는 반환 값이 없으나 Callable의 call()은 완료시 결과를 리턴하게 되어 있다. 기존 Runnable의 run()은 반환 값이 없기 때문에 run() 메서드의 실행 결과를 구하기 위해서는 공용 메모리나 파이프 같은 것들이 필요하였다. Callable은 이를 개선하기 위해 마련된 인터페이스이다.

Callable
Callable 객체는 ExecutorService.submit() (submit 메서드는 Future 타입으로 리턴되고, execute 메서드는 void 타입이다.)과 같은 메서드에 던져진다. 던져진 작업은 큐에 보관되었다가 가용한 쓰레드가 생길 때 call() 메서드가 실행될 것이다. 쓰레드 풀에 던져진 작업은 바로 실행되는 것이 아니기 때문에 리턴 값은 미래의 어느 시점에 구할 수 있다.

Future
이러한 구조를 만들기 위해 Java에선 Future라는 인터페이스를 만들어 제공하고 있다. Future는 당장은 아니지만 언젠간 결과를 갖게되는 객체 정도로 여기면 될 것이다. Future는 기본적으로 Main 쓰레드와 분리된 쓰레드의 결과를 추적할 수 있는 한 가지 방법이 되는 것이다. Future의 get() 메서드는 Callable 인터페이스의 call() 메서드 실행이 완료될 때까지 블로킹 된다.

FutureTask
Future는 Runnable과 함께 작동하도록 만들 수 있는데 FutureTask라는 concrete 클래스가 이러한 역할을 해준다. FutureTask는 Runnable과 Future를 모두 구현하고 있다. FutureTask는 작업을 수행하는 쓰레드와 결과를 받는 쓰레드가 분리된 구조로 동작하고, 중간에 작업 완료 여부를 체크하거나 작업을 취소시킬 수도 있다.

참고: https://www.geeksforgeeks.org/future-and-futuretask-in-java/

[Java] ExecutorService, CountDownLatch

적절한 쓰레드 풀의 사용은 오래 걸리는 작업의 시간을 단축 시킨다.

아래와 같은 코드가 있고, doWork()이 완료되기까지 비교적 오랜 시간이 소요된다면.

public void batchWork(Collection<Work> works) {
    for (Work work : works) {
        work.doWork();
    }
}

위의 코드는 아래와 같은 형식으로 개선이 가능하다.
execute() 내부 동작이 병렬로 실행되므로 work 개체는 쓰레드 안정성을 확보하고 있어야 한다.

public void batchWork(Collection<Work> works) {
    ExecutorService executor = Executor.newFixedThreadPool(4);
    CountDownLatch latch = new CountDownLatch(collection.size());

    for (Work work : works) {
        executor.execute(() -> {
            work.doWork();

            latch.countDown();
        })
    }

    latch.await();
}

CountDownLatch가 전체 작업의 시작과 끝의 타이밍 제어를 해주고 있다.

참고: https://www.baeldung.com/java-countdown-latch

HTML/CSS Margin 속성

마진은 태그 요소와 바깥 요소 간 간격(외부 여백, 패딩은 내부 여백.)을 지정한다. 1개부터 4개까지 값이 올 수 있다. 음수 값도 허용하는데, 음수 값을 적절히 이용하면 재미있는 배치를 표현할 수 있다.

margin을 지정하는 방법은 대략 아래와 같이 정리될 것이다. margin-top, margin-right 식으로 하나 하나 값을 따로 지정할 수도 있다.

1. 하나의 값
margin: 10px;
 top, right, bottom, left 모두 10px.

2. 두개의 값 : 상하-우좌
margin: 10px 5px;
 top, bottom: 10px
 right, left: 5px

3. 세개의 값 : 상-우좌-하
margin: 10px 5px 15px;
 top : 10px
 right, left : 5px
 bottom : 15px

4. 네개의 값 : 상-우-하-좌
margin: 10px 5px 15px 20px;
 top 10px
 right 5px
 left 15px
 bottom 20px

웹 환경에서만 개발하는 사람이라면 margin 값 지정에 별다른 어려움이 없겠지만 WPF 개발을 주로 하고 있어서 개인적으론 항상 헷갈린다.
(WPF에선 margin 적용이 left-top-righ-bottom 순이라 CSS의 top-right-bottom-left 순과 차이가 있다.)

2020년 2월 4일 화요일

HTML 주요 태그 정리.

• 문서를 구성하는 큰 골격에서의 주요 태그.

<doctype>
웹문서의 가장 위에 doctype 선언이 온다. doctype은 웹문서의 형식을 알려주기 위해 사용된다.
html5에선 doctype을 간단히 <!DOCTYPE html> 한가지로 정의한다.

<html>
doctype 다음에 html 태그가 오는데, 다른 모든 태그는 이 태그의 자손이 된다.

<head>
링크, 스크립트, 스타일시트의 정의 및 메타 태그가 들어간다.

<body>
실제 콘텐츠가 배치되는 곳, html 문서에서 본문에 해당한다.

<div>
<div> 자체론 아무 의미가 없지만 웹 페이지의 블럭을 지정하는 용도로 자주 쓰인다.

<script>
웹 문서에서 실행 가능한 스크립트를 넣거나 외부 스크립트를 참조할 때 쓴다. type 속성은 생략 가능하며 생략된 경우 JavaScript로 처리된다.



• 문서의 내용을 구성하는데 쓰이는 태그.

<article>
재배포, 재사용이 가능한 컨텐츠를 포함할 때 쓴다.
기사, 포럼 게시물, 사용자 코멘트 등이 대표적.

<br>, <hr>
<br>은 문서에서 줄바꿈이 필요할 때 사용된다.
<hr>은 문서 단락들 사이에 의미적인 분리를 위해 쓴다. 수평선으로 표시된다.

<canvas>
html5에 추가된 태그로 스크립트 언어를 이용해 그래픽을 표현할 수 있다.

<code>
프로그래밍 코드 조각을 표현할 때 사용된다.

<dl>, <dt>, <dd>
용어와 설명을 표시하는데 사용한다.

<embed>
플러그인 컨텐츠를 표시하는데 사용된다.

<figure>, <figcaption>
그림이나 도표의 제목 또는 범례를 표시하는데 사용된다.

<h1>, <h2>, <h3>, <h4>, <h5>, <h6>
문서의 제목에 사용된다.

<img>
사진을 표시할 때 사용된다. alt 속성으로 이미지가 표시되지 않을 때 대체 정보를 사용자에게 전달해줄 수 있다.

<li>, <ul>, <ol>
목록을 표시하는데 사용한다. 정렬 순서가 있는 경우 <li>를 <ol>로 감싸고, 정렬 순서가 없는 경우 <ul>로 감싸준다.

<p>
문서의 단락 정의에 사용된다.

<pre>
html 파일에 입력된 문자 그대로 보여줄 때 사용한다.

<section>
일반 문서 및 애플리케이션 영역을 표시한다. <article> 태그와 다르게 웹상에서 재배포를 할 수 없다는 의미를 담고 있다.



• <head>와 관련된 태그들.

<meta>
메타 정보를 정의한다.

<link>
CSS와 같은 외부 자원에 대한 링크를 정의한다.

<style>
CSS를 정의하는데 사용된다.



• 폼과 관련된 태그들

<form>
클라이언트와 서버 간 정보를 교환하기 위해 사용된다. method 속성으로 http 전송 방식을 지정해줄 수 있다.

<fieldset>
폼 양식에서 레이블과 컨트롤을 그룹으로 묶어 주기 위해 사용된다.

<input>
폼 양식에서 사용자 입력을 받을 때 사용한다. type으로 text, password, checkbox, radio, file, submit 등의 형식을 부여할 수 있다. (기본 값은 텍스트 박스.)

<label>
폼 양식에서 input 등 항목에 대한 설명을 붙일 때 사용한다.

<select>
폼 양식에서 <option> 태그와 함께 옵션을 선택하는 드롭다운 목록을 제공할 때 사용된다.

<textarea>
폼 양식에서 여러 줄로 구성된 텍스트를 입력받는 용도로 쓰인다. cols, rows 속성으로 표시되는 크기를 지정할 수 있다.



• 표와 관련된 태그들

<table>
데이터를 2차원 표로 표시할 때 사용된다.

<thead>
표에서 머리글 행을 표시할 때 사용된다.

<tbody>
표에서 내용에 대한 행을 표시할 때 사용된다.

<tr>
표에서 표의 행을 표시할 때 사용한다. <th> 또는 <td> 태그를 포함한다.

<td>
표의 셀. colspan, rowspan 등으로 셀 병합을 할 수 있다.

<th>
표에서 헤더, 제목이 되는 셀은 <th>로 표현된다.

<title>
문서의 제목을 정의한다. <head> 요소 내에 위치한다.

<caption>
표에서 제목을 정의할 때 사용된다.

<col>
표에서 열을 정의한다.

<colgroup>
표에서 열의 그룹을 정의할 떄 쓴다. <col> 태그를 포함한다.



• 블록 레벨 태그와 인라인 레벨 태그의 구분

# 블록 레벨 태그
<address>, <article>, <aside>, <blockquote>, <canvas>,
<dd>, <div>, <dl>, <dt>, <fieldset>, <figcaption>, <figure>,
<footer>, <form>, <h1>-<h6>, <header>, <hr>, <li>, <main>,
<nav>, <noscript>, <ol>, <p>, <pre>, <section>, <table>,
<tfoot>, <ul>, <video>

# 인라인 레벨 태그
<a>, <abbr>, <acronym>, <b>, <bdo>, <big>, <br>, <button>,
<cite>, <code>, <dfn>, <em>, <i>, <img>, <input>, <kbd>, <label>,
<map>, <object>, <output>, <q>, <samp>, <script>, <select>,
<small>, <span>, <strong>, <sub>, <sup>, <textarea>, <time>,
<tt>, <var>