JDBC DAO/DTO 패턴 완전 가이드¶
📋 목차¶
- 프로젝트 개요
- 아키텍처 다이어그램
- 프로젝트 설정 (pom.xml)
- 데이터베이스 스키마
- DTO (Data Transfer Object)
- DAO (Data Access Object) 인터페이스
- 데이터베이스 연결 유틸리티
- DAO 구현체 (JDBC)
- 서블릿 구현
- JSP 뷰
- 테스트 실행
- Docker 배포
프로젝트 개요¶
이 프로젝트는 JDBC 기반의 DAO/DTO 패턴을 학습하기 위한 메모 애플리케이션입니다.
🎯 학습 목표¶
- DTO(Data Transfer Object): 계층 간 데이터 전달 전용 불변 객체
- DAO(Data Access Object): 영속 계층에 대한 추상 인터페이스
- PreparedStatement: SQL Injection 공격 차단
- 인터페이스 분리: 나중에 JPA로 교체 용이성
🔧 주요 기술 스택¶
- Java 17 (record 사용)
- Maven
- MySQL JDBC Driver
- Jakarta Servlet/JSP
- dotenv-java (환경변수 관리)
아키텍처 다이어그램¶
graph TB
subgraph "Presentation Layer"
A[JSP Views] --> B[Servlet]
end
subgraph "Business Layer"
B --> C[DAO Interface]
end
subgraph "Data Access Layer"
C --> D[JDBC DAO Implementation]
D --> E[(MySQL Database)]
end
subgraph "Data Transfer"
F[DTO Objects] -.-> B
F -.-> D
end
subgraph "Configuration"
G[.env File] --> H[DB Utils]
H --> D
end
프로젝트 설정 (pom.xml)¶
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- Maven 프로젝트 기본 정보 -->
<modelVersion>4.0.0</modelVersion>
<groupId>dev.example</groupId> <!-- 조직/그룹 식별자 -->
<artifactId>memo-app</artifactId> <!-- 프로젝트 이름 -->
<version>1.0.0</version> <!-- 버전 정보 -->
<packaging>war</packaging> <!-- 웹 애플리케이션 패키징 형식 -->
<!-- Java 컴파일 설정 -->
<properties>
<maven.compiler.source>17</maven.compiler.source> <!-- 소스 Java 버전 -->
<maven.compiler.target>17</maven.compiler.target> <!-- 타겟 Java 버전 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- MySQL 데이터베이스 연결 드라이버 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.4.0</version>
</dependency>
<!-- 환경변수 파일(.env) 관리 라이브러리 -->
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>dotenv-java</artifactId>
<version>3.2.0</version>
</dependency>
<!-- 서블릿 API (Tomcat이 런타임에 제공하므로 provided) -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0</version>
<scope>provided</scope>
</dependency>
<!-- JSP API (개발 환경에서 IDE 지원용) -->
<dependency>
<groupId>jakarta.servlet.jsp</groupId>
<artifactId>jakarta.servlet.jsp-api</artifactId>
<version>3.1.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- WAR 파일 생성 플러그인 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
</plugin>
</plugins>
</build>
</project>
데이터베이스 스키마¶
erDiagram
user_account {
BIGINT user_id PK "자동증가"
VARCHAR username UK "사용자명(유니크)"
VARCHAR display_name "표시명"
TIMESTAMP created_at "생성일시"
}
memo {
BIGINT memo_id PK "자동증가"
BIGINT user_id FK "작성자 ID"
VARCHAR title "메모 제목"
TEXT content "메모 내용"
TIMESTAMP created_at "생성일시"
}
user_account ||--o{ memo : "writes"
🗄️ DDL/DML SQL¶
-- 테이블 초기화 (기존 테이블 삭제)
DROP TABLE IF EXISTS memo;
DROP TABLE IF EXISTS user_account;
-- 사용자 테이블 생성
CREATE TABLE user_account (
user_id BIGINT PRIMARY KEY AUTO_INCREMENT, -- 기본키, 자동증가
username VARCHAR(50) NOT NULL UNIQUE, -- 사용자명, 중복불가
display_name VARCHAR(80) NOT NULL, -- 표시명
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -- 생성일시
) ENGINE=InnoDB;
-- 메모 테이블 생성
CREATE TABLE memo (
memo_id BIGINT PRIMARY KEY AUTO_INCREMENT, -- 기본키, 자동증가
user_id BIGINT NOT NULL, -- 작성자 ID
title VARCHAR(200) NOT NULL, -- 메모 제목
content TEXT NOT NULL, -- 메모 내용
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 생성일시
FOREIGN KEY (user_id) REFERENCES user_account(user_id) -- 외래키
) ENGINE=InnoDB;
-- 샘플 데이터 삽입
INSERT INTO user_account (username, display_name) VALUES
('alice', 'Alice Kim'),
('bob', 'Bob Lee');
INSERT INTO memo (user_id, title, content) VALUES
(1, '첫 메모', '알리스의 첫 번째 메모 본문'),
(1, '둘째 메모', '알리스의 두 번째 메모'),
(2, '밥의 메모', '밥의 유일한 메모');
-- JOIN 쿼리 예시 (작성자 이름과 함께 메모 목록)
SELECT m.memo_id, m.title, m.content, m.created_at,
u.user_id, u.username, u.display_name
FROM memo m
JOIN user_account u ON u.user_id = m.user_id
ORDER BY m.memo_id DESC;
DTO (Data Transfer Object)¶
DTO란? 계층 간 데이터 전달을 위한 불변 객체입니다. Java 17의 record
를 사용하여 간결하게 정의합니다.
📝 DTO 구조 다이어그램¶
classDiagram
class UserDTO {
+Long userId
+String username
+String displayName
+LocalDateTime createdAt
}
class MemoDTO {
+Long memoId
+Long userId
+String title
+String content
+LocalDateTime createdAt
}
class MemoWithAuthorDTO {
+Long memoId
+String title
+String content
+LocalDateTime memoCreatedAt
+Long authorId
+String authorUsername
+String authorDisplayName
}
UserDTO ||--o{ MemoDTO : "writes"
MemoDTO ||--|| MemoWithAuthorDTO : "joins with"
💻 DTO 코드 구현¶
import java.time.LocalDateTime;
// 사용자 정보를 담는 DTO
public record UserDTO(
Long userId, // 사용자 고유 ID
String username, // 사용자명 (로그인 ID)
String displayName, // 화면에 표시할 이름
LocalDateTime createdAt // 계정 생성일시
) {}
// 메모 정보를 담는 DTO
public record MemoDTO(
Long memoId, // 메모 고유 ID
Long userId, // 작성자 ID (외래키)
String title, // 메모 제목
String content, // 메모 내용
LocalDateTime createdAt // 메모 작성일시
) {}
// 메모와 작성자 정보를 함께 담는 JOIN 결과 DTO
public record MemoWithAuthorDTO(
Long memoId, // 메모 ID
String title, // 메모 제목
String content, // 메모 내용
LocalDateTime memoCreatedAt, // 메모 작성일시
Long authorId, // 작성자 ID
String authorUsername, // 작성자 사용자명
String authorDisplayName // 작성자 표시명
) {}
📌 Record 사용 이유: - 불변성 보장 (모든 필드가 final) - 자동으로 생성자, getter, equals(), hashCode(), toString() 제공 - 간결한 코드로 가독성 향상
DAO (Data Access Object) 인터페이스¶
DAO란? 데이터베이스 접근을 추상화한 인터페이스입니다. 구현체를 JDBC에서 JPA로 교체해도 서비스 계층 코드는 변경되지 않습니다.
🔄 DAO 흐름도¶
flowchart LR
A[Service Layer] --> B[DAO Interface]
B --> C[JDBC Implementation]
B --> D[JPA Implementation]
C --> E[(MySQL)]
D --> E
style B fill:#e1f5fe
style C fill:#f3e5f5
style D fill:#f3e5f5
💻 DAO 인터페이스 코드¶
import java.util.List;
import java.util.Optional;
// 사용자 데이터 접근 인터페이스
public interface UserDAO {
// 새 사용자 생성, 생성된 사용자 ID 반환
Long create(String username, String displayName);
// ID로 사용자 조회 (없으면 Optional.empty())
Optional<UserDTO> findById(Long userId);
// 사용자명으로 조회 (로그인 등에 사용)
Optional<UserDTO> findByUsername(String username);
// 전체 사용자 목록 조회 (페이징 처리)
List<UserDTO> findAll(int limit, int offset);
}
// 메모 데이터 접근 인터페이스
public interface MemoDAO {
// 새 메모 작성, 생성된 메모 ID 반환
Long create(Long userId, String title, String content);
// ID로 메모 조회
Optional<MemoDTO> findById(Long memoId);
// 특정 사용자의 메모 목록 조회
List<MemoDTO> findByUserId(Long userId, int limit, int offset);
// 모든 메모를 작성자 정보와 함께 조회 (JOIN)
List<MemoWithAuthorDTO> findAllWithAuthor(int limit, int offset);
// 메모 삭제, 삭제된 행 수 반환
int deleteById(Long memoId);
}
📌 인터페이스 설계 원칙: - 단일 책임: 각 DAO는 하나의 엔티티만 담당 - 추상화: 구현 기술(JDBC, JPA)에 독립적 - 표준 메서드 네이밍: create, find, delete 등 일관된 명명
데이터베이스 연결 유틸리티¶
환경변수를 통해 안전하게 DB 연결 정보를 관리하는 유틸리티 클래스입니다.
🔐 환경변수 설정 (.env 파일)¶
# 데이터베이스 연결 정보
DB_URL=jdbc:mysql://localhost:3306/memo_db?allowPublicKeyRetrieval=true
DB_USER=memo_user
DB_PASSWORD=secure_password123
💻 DB 연결 유틸리티 코드¶
import io.github.cdimascio.dotenv.Dotenv;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DB {
// .env 파일에서 환경변수 로드
private static final Dotenv dotenv = Dotenv.load();
// 환경변수에서 DB 연결 정보 읽기
private static final String URL = dotenv.get("DB_URL");
private static final String USER = dotenv.get("DB_USER");
private static final String PASSWORD = dotenv.get("DB_PASSWORD");
/**
* 데이터베이스 연결 생성
* @return Connection 객체
* @throws SQLException DB 연결 실패 시
*/
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USER, PASSWORD);
}
}
📌 보안 고려사항:
- .env
파일은 버전 관리(Git)에서 제외
- 프로덕션에서는 커넥션 풀(HikariCP) 사용 권장
- allowPublicKeyRetrieval=true
는 Aiven MySQL 등에서 필요
DAO 구현체 (JDBC)¶
실제 데이터베이스와 통신하는 JDBC 기반 구현체입니다.
🔄 JDBC 처리 흐름¶
sequenceDiagram
participant S as Service
participant D as DAO
participant DB as Database
S->>D: create("alice", "Alice Kim")
D->>DB: Connection 생성
D->>DB: PreparedStatement 준비
D->>DB: 파라미터 바인딩
D->>DB: executeUpdate()
DB-->>D: 생성된 ID 반환
D-->>S: Long userId
💻 UserJdbcDAO 구현¶
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class UserJdbcDAO implements UserDAO {
@Override
public Long create(String username, String displayName) {
// SQL INSERT 쿼리 (Java 17 Text Blocks 사용)
final String sql = """
INSERT INTO user_account (username, display_name)
VALUES (?, ?)
""";
// try-with-resources로 자동 자원 해제
try (Connection conn = DB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
// 파라미터 바인딩 (SQL Injection 방지)
ps.setString(1, username); // 첫 번째 ? 에 username 대입
ps.setString(2, displayName); // 두 번째 ? 에 displayName 대입
// INSERT 실행
int updated = ps.executeUpdate();
if (updated != 1) {
throw new SQLException("Insert failed for user_account");
}
// 자동 생성된 키(user_id) 조회
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) {
return rs.getLong(1); // 생성된 ID 반환
}
}
throw new SQLException("No generated key returned for user_account");
} catch (SQLException e) {
// 체크 예외를 런타임 예외로 변환
throw new RuntimeException("UserJdbcDAO.create error", e);
}
}
@Override
public Optional<UserDTO> findById(Long userId) {
final String sql = """
SELECT user_id, username, display_name, created_at
FROM user_account
WHERE user_id = ?
""";
try (Connection conn = DB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, userId); // 파라미터 바인딩
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
// 결과가 있으면 DTO로 변환하여 Optional로 감싸기
return Optional.of(mapUser(rs));
}
// 결과가 없으면 빈 Optional 반환
return Optional.empty();
}
} catch (SQLException e) {
throw new RuntimeException("UserJdbcDAO.findById error", e);
}
}
@Override
public Optional<UserDTO> findByUsername(String username) {
final String sql = """
SELECT user_id, username, display_name, created_at
FROM user_account
WHERE username = ?
""";
try (Connection conn = DB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, username);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return Optional.of(mapUser(rs));
}
return Optional.empty();
}
} catch (SQLException e) {
throw new RuntimeException("UserJdbcDAO.findByUsername error", e);
}
}
@Override
public List<UserDTO> findAll(int limit, int offset) {
final String sql = """
SELECT user_id, username, display_name, created_at
FROM user_account
ORDER BY user_id DESC
LIMIT ? OFFSET ?
""";
try (Connection conn = DB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, limit); // 조회할 최대 행 수
ps.setInt(2, offset); // 건너뛸 행 수 (페이징)
try (ResultSet rs = ps.executeQuery()) {
List<UserDTO> list = new ArrayList<>();
// 결과 집합을 순회하며 DTO 리스트 생성
while (rs.next()) {
list.add(mapUser(rs));
}
return list;
}
} catch (SQLException e) {
throw new RuntimeException("UserJdbcDAO.findAll error", e);
}
}
/**
* ResultSet의 한 행을 UserDTO로 변환하는 헬퍼 메서드
*/
private UserDTO mapUser(ResultSet rs) throws SQLException {
return new UserDTO(
rs.getLong("user_id"), // BIGINT -> Long
rs.getString("username"), // VARCHAR -> String
rs.getString("display_name"), // VARCHAR -> String
rs.getTimestamp("created_at").toLocalDateTime() // TIMESTAMP -> LocalDateTime
);
}
}
💻 MemoJdbcDAO 구현¶
public class MemoJdbcDAO implements MemoDAO {
@Override
public Long create(Long userId, String title, String content) {
final String sql = """
INSERT INTO memo (user_id, title, content)
VALUES (?, ?, ?)
""";
try (Connection conn = DB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
// 3개 파라미터 바인딩
ps.setLong(1, userId); // 작성자 ID
ps.setString(2, title); // 메모 제목
ps.setString(3, content); // 메모 내용
int updated = ps.executeUpdate();
if (updated != 1) {
throw new SQLException("Insert failed for memo");
}
// 생성된 memo_id 반환
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) {
return rs.getLong(1);
}
}
throw new SQLException("No generated key returned for memo");
} catch (SQLException e) {
throw new RuntimeException("MemoJdbcDAO.create error", e);
}
}
@Override
public Optional<MemoDTO> findById(Long memoId) {
final String sql = """
SELECT memo_id, user_id, title, content, created_at
FROM memo
WHERE memo_id = ?
""";
try (Connection conn = DB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, memoId);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return Optional.of(mapMemo(rs));
}
return Optional.empty();
}
} catch (SQLException e) {
throw new RuntimeException("MemoJdbcDAO.findById error", e);
}
}
@Override
public List<MemoDTO> findByUserId(Long userId, int limit, int offset) {
final String sql = """
SELECT memo_id, user_id, title, content, created_at
FROM memo
WHERE user_id = ?
ORDER BY memo_id DESC
LIMIT ? OFFSET ?
""";
try (Connection conn = DB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, userId); // 특정 사용자의 메모만 조회
ps.setInt(2, limit);
ps.setInt(3, offset);
try (ResultSet rs = ps.executeQuery()) {
List<MemoDTO> list = new ArrayList<>();
while (rs.next()) {
list.add(mapMemo(rs));
}
return list;
}
} catch (SQLException e) {
throw new RuntimeException("MemoJdbcDAO.findByUserId error", e);
}
}
@Override
public List<MemoWithAuthorDTO> findAllWithAuthor(int limit, int offset) {
// INNER JOIN을 사용하여 메모와 작성자 정보를 함께 조회
final String sql = """
SELECT m.memo_id, m.title, m.content, m.created_at,
u.user_id, u.username, u.display_name
FROM memo m
INNER JOIN user_account u ON u.user_id = m.user_id
ORDER BY m.memo_id DESC
LIMIT ? OFFSET ?
""";
try (Connection conn = DB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, limit);
ps.setInt(2, offset);
try (ResultSet rs = ps.executeQuery()) {
List<MemoWithAuthorDTO> list = new ArrayList<>();
while (rs.next()) {
list.add(mapMemoWithAuthor(rs));
}
return list;
}
} catch (SQLException e) {
throw new RuntimeException("MemoJdbcDAO.findAllWithAuthor error", e);
}
}
@Override
public int deleteById(Long memoId) {
final String sql = "DELETE FROM memo WHERE memo_id = ?";
try (Connection conn = DB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, memoId);
// 삭제된 행의 개수 반환 (0이면 해당 ID의 메모가 없었음)
return ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException("MemoJdbcDAO.deleteById error", e);
}
}
/**
* ResultSet의 한 행을 MemoDTO로 변환
*/
private MemoDTO mapMemo(ResultSet rs) throws SQLException {
return new MemoDTO(
rs.getLong("memo_id"),
rs.getLong("user_id"),
rs.getString("title"),
rs.getString("content"),
rs.getTimestamp("created_at").toLocalDateTime()
);
}
/**
* JOIN 결과를 MemoWithAuthorDTO로 변환
*/
private MemoWithAuthorDTO mapMemoWithAuthor(ResultSet rs) throws SQLException {
return new MemoWithAuthorDTO(
rs.getLong("memo_id"), // 메모 정보
rs.getString("title"),
rs.getString("content"),
rs.getTimestamp("created_at").toLocalDateTime(),
rs.getLong("user_id"), // 작성자 정보
rs.getString("username"),
rs.getString("display_name")
);
}
}
📌 JDBC 구현의 핵심 원칙: - try-with-resources: Connection, PreparedStatement, ResultSet 자동 해제 - PreparedStatement: SQL Injection 공격 방지를 위한 파라미터 바인딩 - 예외 처리: 체크 예외를 런타임 예외로 변환하여 호출자에게 전파 - 매퍼 메서드: ResultSet → DTO 변환 로직 분리
서블릿 구현¶
웹 요청을 처리하고 DAO와 JSP를 연결하는 컨트롤러 역할입니다.
🌐 서블릿 처리 흐름¶
mermaid
sequenceDiagram
participant B as 🖥️ Browser
participant S as 🌐 Servlet (Controller)
participant D as 📦 DAO
participant DB as 🗄️ Database
participant J as 📄 JSP (View)
B->>S: GET /memos 요청
S->>D: findByUserId(userId)
D->>DB: SELECT * FROM memos WHERE user_id=?
DB-->>D: ResultSet (메모 목록)
D-->>S: List<MemoDTO>
S-->>J: request.setAttribute("memos", list)
J-->>B: HTML 렌더링 결과 응답
📝 흐름 설명¶
- 브라우저가
/memos
요청을 보냅니다. - 서블릿이 요청을 받아 해당 유저 ID에 맞는 데이터를 가져오기 위해 DAO 호출.
- DAO는 DB에
SELECT
쿼리를 수행하여 결과(ResultSet)를 받습니다. - DAO는 결과를 DTO(List 형태)로 가공해 서블릿으로 반환합니다.
- 서블릿은 JSP에 데이터를 전달(
setAttribute
)합니다. - JSP는 받은 데이터를 HTML로 변환하여 브라우저에 응답합니다.
🌟 HTML 태그와 속성 완벽 가이드¶
🗂️ 목차¶
섹션 | 내용 | 난이도 |
---|---|---|
🎯 HTML이란? | HTML 기본 개념 | ⭐ |
🏗️ 태그의 기본 구조 | 태그 문법과 구조 | ⭐ |
📝 주요 HTML 태그들 | 필수 태그 종류별 설명 | ⭐⭐ |
🎨 HTML 속성 | 속성의 개념과 활용 | ⭐⭐ |
💡 실전 예제 | 실제 웹페이지 만들기 | ⭐⭐⭐ |
📊 시각화 자료 | 구조 다이어그램 | ⭐⭐ |
🎯 HTML이란?¶
HTML(HyperText Markup Language) 은 웹페이지의 뼈대를 만드는 언어입니다.
graph LR
A[🏠 집 짓기] --> B[🏗️ 뼈대 세우기]
B --> C[🎨 인테리어]
B --> D[⚡ 전기 배선]
E[🌐 웹사이트 만들기] --> F[📝 HTML 구조]
F --> G[🎨 CSS 디자인]
F --> H[⚡ JavaScript 기능]
style A fill:#ffcdd2
style E fill:#c8e6c9
style B fill:#fff3e0
style F fill:#fff3e0
🔍 HTML의 특징¶
특징 | 설명 | 예시 |
---|---|---|
🏷️ 마크업 언어 | 내용에 의미를 부여 | <h1>제목</h1> |
🌐 웹 표준 | 모든 브라우저에서 동작 | Chrome, Safari, Firefox |
📱 반응형 | 다양한 기기에서 호환 | PC, 모바일, 태블릿 |
♿ 접근성 | 모든 사용자가 이용 가능 | 스크린 리더 지원 |
🏗️ HTML 태그의 기본 구조¶
graph TB
A[🏷️ HTML 태그] --> B[📖 여는 태그]
A --> C[💭 내용]
A --> D[🔚 닫는 태그]
B --> E["< 태그명 >"]
D --> F["</ 태그명 >"]
B --> G[⚙️ 속성 추가]
G --> H["< 태그명 속성='값' >"]
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#e8f5e8
style D fill:#fff3e0
style G fill:#ffebee
📝 기본 문법 패턴¶
<!-- ✨ 기본 태그 구조 -->
<태그명>내용이 여기에 들어갑니다</태그명>
<!-- 🎨 속성이 있는 태그 -->
<태그명 속성명="속성값" 속성명2="속성값2">내용</태그명>
<!-- 🚀 단독 태그 (내용이 없는 태그) -->
<태그명 속성명="속성값" />
💡 팁: 태그는 항상
<
로 시작해서>
로 끝나며, 닫는 태그는/
를 포함합니다!
📝 주요 HTML 태그들¶
🏠 1. 문서 구조 태그¶
<!DOCTYPE html> <!-- 📋 HTML5 문서임을 브라우저에게 알려줌 -->
<html lang="ko"> <!-- 🌐 전체 HTML 문서의 시작, 한국어 설정 -->
<head> <!-- 🧠 문서의 메타데이터(정보) 영역 -->
<meta charset="UTF-8"> <!-- 📝 한글 등 문자 표시 설정 -->
<title>페이지 제목</title> <!-- 🏷️ 브라우저 탭에 표시될 제목 -->
</head>
<body> <!-- 👁️ 실제로 화면에 보여질 모든 내용 -->
<!-- 여기에 보여질 내용들이 들어갑니다 -->
</body>
</html> <!-- 🏁 HTML 문서의 끝 -->
✍️ 2. 텍스트 관련 태그¶
<!-- 🏆 제목 태그들 (h1이 가장 크고 중요, h6이 가장 작음) -->
<h1>🎯 메인 제목 (가장 중요한 제목)</h1>
<h2>📌 섹션 제목 (두 번째로 중요)</h2>
<h3>🔹 소제목 (세 번째로 중요)</h3>
<h4>▪️ 작은 제목</h4>
<h5>• 더 작은 제목</h5>
<h6>· 가장 작은 제목</h6>
<!-- 📄 문단과 줄바꿈 -->
<p>📝 이것은 하나의 완전한 문단입니다. 여러 문장이 모여서 하나의 주제를 다룹니다.</p>
<p>📝 이것은 또 다른 문단입니다.</p>
<p>줄바꿈이 필요한 곳에서<br />🔄 이렇게 새로운 줄로 넘어갑니다.</p>
<!-- ✨ 텍스트 강조 태그들 -->
<p>
<strong>💪 정말 중요한 내용</strong> - 의미적으로 중요함을 표현
<em>🎭 강조하고 싶은 내용</em> - 의미적으로 강조함을 표현
<b>🔸 단순히 굵게 보이는 텍스트</b> - 시각적 효과만
<i>🔸 단순히 기울어진 텍스트</i> - 시각적 효과만
</p>
🔗 3. 링크와 미디어 태그¶
<!-- 🌐 다양한 종류의 링크들 -->
<a href="https://www.google.com"
target="_blank" <!-- 🆕 새 창에서 열기 -->
title="구글 검색엔진"> <!-- 💭 마우스 올렸을 때 나타나는 설명 -->
🔍 구글에서 검색하기
</a>
<a href="page2.html">📄 같은 사이트의 다른 페이지로 이동</a>
<a href="#section1">⬇️ 이 페이지의 특정 부분으로 점프</a>
<a href="mailto:contact@example.com">📧 이메일 보내기</a>
<a href="tel:010-1234-5678">📞 전화 걸기</a>
<!-- 🖼️ 이미지 태그 (내용이 없는 단독 태그) -->
<img src="beautiful-sunset.jpg" <!-- 📁 이미지 파일의 위치 -->
alt="바다 위로 지는 아름다운 노을" <!-- 🔤 이미지가 안 보일 때 대신 나타날 텍스트 -->
width="400" <!-- 📏 이미지 가로 크기 (픽셀) -->
height="300" <!-- 📏 이미지 세로 크기 (픽셀) -->
title="제주도에서 촬영한 노을" /> <!-- 💭 마우스 올렸을 때 나타나는 설명 -->
📋 4. 목록 태그¶
<!-- 🔸 순서가 없는 목록 (불릿 포인트로 표시) -->
<ul>
<li>🍎 사과</li> <!-- 각각의 목록 항목 -->
<li>🍌 바나나</li> <!-- 순서가 중요하지 않은 항목들 -->
<li>🍊 오렌지</li>
</ul>
<!-- 🔢 순서가 있는 목록 (숫자로 표시) -->
<ol>
<li>🌅 아침에 일어나기</li> <!-- 1. 아침에 일어나기 -->
<li>🦷 양치질하기</li> <!-- 2. 양치질하기 -->
<li>🍳 아침식사 준비하기</li> <!-- 3. 아침식사 준비하기 -->
</ol>
<!-- 📝 특별한 순서 목록 (시작 번호 지정) -->
<ol start="5"> <!-- 5번부터 시작 -->
<li>🚀 다섯 번째 단계</li> <!-- 5. 다섯 번째 단계 -->
<li>⭐ 여섯 번째 단계</li> <!-- 6. 여섯 번째 단계 -->
</ol>
📦 5. 컨테이너 태그¶
<!-- 🧱 div: 블록 레벨 컨테이너 (세로로 쌓이는 상자) -->
<div class="content-box"> <!-- 💼 여러 요소들을 하나로 묶는 상자 -->
<h2>📦 이 영역의 제목</h2>
<p>📄 이 내용들은 모두 div 안에 들어있어서 하나의 그룹을 이룹니다.</p>
</div>
<!-- 🏷️ span: 인라인 컨테이너 (가로로 나열되는 작은 영역) -->
<p>이 문장에서 <span style="color: red;">🔴 이 부분만</span> 빨간색으로 표시됩니다.</p>
🎨 HTML 속성(Attribute)¶
속성은 태그에게 "어떻게 동작해야 하는지" 알려주는 설정값입니다.
graph LR
A[🏷️ HTML 태그] --> B[⚙️ 속성들]
B --> C[🆔 id<br/>고유 식별자]
B --> D[👥 class<br/>그룹 식별자]
B --> E[🎨 style<br/>스타일 설정]
B --> F[💭 title<br/>툴팁 텍스트]
C --> G[📍 페이지에서<br/>하나만 존재]
D --> H[👫 여러 요소가<br/>같은 클래스 가능]
E --> I[🎨 직접 디자인<br/>적용]
F --> J[🖱️ 마우스 올릴 때<br/>나타나는 설명]
style A fill:#e3f2fd
style C fill:#e8f5e8
style D fill:#fff3e0
style E fill:#fce4ec
style F fill:#f3e5f5
🌍 전역 속성 (모든 태그에서 사용 가능)¶
<!-- 🆔 id: 페이지에서 유일한 식별자 -->
<div id="header">🎯 헤더 영역 (페이지에서 이 ID는 하나만 존재)</div>
<div id="main-content">📄 메인 콘텐츠 영역</div>
<!-- 👥 class: 같은 스타일을 적용할 요소들의 그룹명 -->
<p class="highlight">⭐ 중요한 첫 번째 문단</p>
<p class="highlight">⭐ 중요한 두 번째 문단</p>
<p class="normal">📝 일반적인 문단</p>
<!-- 🎨 style: 직접 디자인을 적용 -->
<p style="color: blue; font-size: 18px; background: yellow;">
🎨 파란 글씨, 18픽셀 크기, 노란 배경
</p>
<!-- 💭 title: 마우스를 올렸을 때 나타나는 도움말 -->
<button title="이 버튼을 클릭하면 페이지가 새로고침됩니다">
🔄 새로고침
</button>
🔗 링크 전용 속성¶
<!-- 🌐 다양한 링크 속성들 -->
<a href="https://www.naver.com" <!-- 🎯 이동할 주소 -->
target="_blank" <!-- 🆕 새 창에서 열기 -->
rel="noopener noreferrer" <!-- 🔒 보안을 위한 설정 -->
title="네이버 메인페이지로 이동" <!-- 💭 링크 설명 -->
download="네이버_바로가기.html"> <!-- 💾 다운로드 파일명 지정 -->
📱 네이버로 이동하기
</a>
🖼️ 이미지 전용 속성¶
<!-- 🖼️ 완벽한 이미지 태그 -->
<img src="photos/beach-sunset.jpg" <!-- 📁 이미지 파일 경로 -->
alt="제주도 해변의 황금빛 노을" <!-- 🔤 이미지 설명 (시각 장애인용) -->
width="500" <!-- 📏 가로 크기 500픽셀 -->
height="300" <!-- 📏 세로 크기 300픽셀 -->
loading="lazy" <!-- ⚡ 스크롤할 때만 로드 (성능 향상) -->
title="제주도 여행 중 촬영" /> <!-- 💭 마우스 올렸을 때 추가 정보 -->
💡 실전 예제¶
🎨 나만의 자기소개 페이지 만들기¶
<!DOCTYPE html>
<html lang="ko"> <!-- 🇰🇷 한국어 페이지임을 명시 -->
<head>
<meta charset="UTF-8"> <!-- 📝 한글 표시를 위한 인코딩 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- 📱 모바일 최적화 -->
<title>🌟 김개발의 포트폴리오</title> <!-- 🏷️ 브라우저 탭 제목 -->
<style>
/* 🎨 간단한 스타일링 */
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
.container { max-width: 800px; margin: 0 auto; }
.profile-img { border-radius: 50%; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
.skill-tag { background: #e3f2fd; padding: 5px 10px; border-radius: 15px; display: inline-block; margin: 5px; }
</style>
</head>
<body>
<div class="container"> <!-- 📦 전체 내용을 감싸는 컨테이너 -->
<!-- 🎯 헤더 영역 -->
<header id="page-header"> <!-- 🎪 페이지 상단 영역 -->
<h1>🌟 안녕하세요! 김개발입니다</h1>
<p class="subtitle">💻 열정적인 신입 웹 개발자</p>
</header>
<!-- 📄 메인 콘텐츠 영역 -->
<main>
<!-- 👤 프로필 섹션 -->
<section id="profile"> <!-- 📋 관련 내용들을 묶는 섹션 -->
<h2>👋 자기소개</h2>
<!-- 📸 프로필 사진 -->
<img src="profile-photo.jpg"
alt="김개발의 프로필 사진"
class="profile-img"
width="150"
height="150"
title="안녕하세요!" />
<!-- 📝 소개 글 -->
<p class="introduction"> <!-- 💡 CSS로 스타일링할 수 있도록 클래스 지정 -->
안녕하세요! 저는 <strong>🔥 웹 개발</strong>에 열정을 가진
<em>✨ 신입 개발자</em> 김개발입니다.
사용자 친화적인 웹사이트를 만드는 것이 제 목표입니다.
</p>
</section>
<!-- 💼 기술 스택 섹션 -->
<section id="skills">
<h2>🛠️ 기술 스택</h2>
<div class="skills-container">
<span class="skill-tag">🌐 HTML5</span> <!-- 🏷️ 기술을 태그 형태로 표시 -->
<span class="skill-tag">🎨 CSS3</span>
<span class="skill-tag">⚡ JavaScript</span>
<span class="skill-tag">⚛️ React</span>
<span class="skill-tag">🔧 Node.js</span>
</div>
<!-- 📊 상세 기술 목록 -->
<h3>📈 학습 중인 기술들</h3>
<ul class="learning-list">
<li>🐍 <strong>Python</strong> - 백엔드 개발을 위해 학습 중</li>
<li>🗄️ <strong>MongoDB</strong> - 데이터베이스 관리 기술</li>
<li>☁️ <strong>AWS</strong> - 클라우드 서비스 활용</li>
</ul>
</section>
<!-- 🎯 프로젝트 섹션 -->
<section id="projects">
<h2>🚀 프로젝트</h2>
<!-- 📊 프로젝트 카드 -->
<article class="project-card"> <!-- 📋 독립적인 콘텐츠 단위 -->
<h3>🛒 온라인 쇼핑몰</h3>
<p>📝 사용자가 쉽게 상품을 구매할 수 있는 반응형 쇼핑몰 웹사이트</p>
<p>
<strong>🔧 사용 기술:</strong>
HTML, CSS, JavaScript, React
</p>
<a href="https://github.com/myproject"
target="_blank"
title="프로젝트 소스코드 보기">
👀 GitHub에서 코드 보기
</a>
</article>
</section>
</main>
<!-- 📞 연락처 영역 -->
<aside id="contact"> <!-- 🔀 메인 콘텐츠와 관련된 부가 정보 -->
<h2>📬 연락처</h2>
<address> <!-- 📮 연락처 정보 전용 태그 -->
📧 <strong>이메일:</strong>
<a href="mailto:kim.dev@example.com">kim.dev@example.com</a><br />
📱 <strong>전화:</strong>
<a href="tel:010-1234-5678">010-1234-5678</a><br />
🌐 <strong>GitHub:</strong>
<a href="https://github.com/kimdev" target="_blank">github.com/kimdev</a>
</address>
</aside>
<!-- 🏁 푸터 영역 -->
<footer id="page-footer">
<hr /> <!-- ➖ 수평선으로 구분 -->
<p style="text-align: center; color: #666;">
📅 © 2025 김개발. 모든 권리 보유.
<small>💝 방문해주셔서 감사합니다!</small> <!-- 🔍 작은 글씨로 표시 -->
</p>
</footer>
</div>
</body>
</html>
📊 태그와 속성 관계도¶
graph TD
A[🌐 HTML 문서] --> B[📋 html 태그]
B --> C[🧠 head 태그]
B --> D[👁️ body 태그]
C --> E[🏷️ title]
C --> F[📝 meta]
D --> G[🎪 header]
D --> H[📄 main]
D --> I[🔀 aside]
D --> J[🏁 footer]
H --> K[🏆 h1~h6]
H --> L[📝 p]
H --> M[🖼️ img]
H --> N[🔗 a]
H --> O[📋 ul/ol]
O --> P[📌 li]
%% 속성 연결
K --> Q[🆔 id 속성]
L --> R[👥 class 속성]
M --> S[📁 src 속성]
M --> T[🔤 alt 속성]
N --> U[🎯 href 속성]
N --> V[🆕 target 속성]
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#fff3e0
style D fill:#e8f5e8
style Q fill:#ffcdd2
style R fill:#ffcdd2
style S fill:#ffcdd2
style T fill:#ffcdd2
style U fill:#ffcdd2
style V fill:#ffcdd2
🏷️ 태그 분류 시스템¶
graph TD
A[🏷️ HTML 태그 분류] --> B[🧱 블록 레벨]
A --> C[🏷️ 인라인]
A --> D[🚀 빈 태그]
B --> E[📦 div<br/>구역 나누기]
B --> F[📝 p<br/>문단]
B --> G[🏆 h1~h6<br/>제목]
B --> H[📋 ul/ol<br/>목록]
B --> I[🎪 header<br/>헤더]
B --> J[📄 main<br/>메인]
C --> K[🏷️ span<br/>인라인 컨테이너]
C --> L[🔗 a<br/>링크]
C --> M[💪 strong<br/>강조]
C --> N[🎭 em<br/>강세]
C --> O[🔸 b/i<br/>스타일]
D --> P[🔄 br<br/>줄바꿈]
D --> Q[➖ hr<br/>수평선]
D --> R[🖼️ img<br/>이미지]
D --> S[📝 input<br/>입력필드]
D --> T[📋 meta<br/>메타데이터]
style B fill:#e3f2fd
style C fill:#f1f8e9
style D fill:#fff3e0
📱 폼(Form) 완벽 가이드¶
🎯 회원가입 폼 예제¶
<!-- 📝 사용자 정보를 입력받는 폼 -->
<form action="/register" <!-- 📤 폼 데이터를 보낼 서버 주소 -->
method="post" <!-- 📮 데이터 전송 방식 (post는 보안이 좋음) -->
id="signup-form"> <!-- 🆔 폼의 고유 식별자 -->
<fieldset> <!-- 📦 관련된 입력 필드들을 그룹화 -->
<legend>👤 기본 정보</legend> <!-- 🏷️ 그룹의 제목 -->
<!-- 📝 이름 입력 필드 -->
<div class="form-group">
<label for="fullname">👤 이름:</label> <!-- 🏷️ 입력 필드 설명 라벨 -->
<input type="text" <!-- ⌨️ 일반 텍스트 입력 -->
id="fullname" <!-- 🆔 label과 연결하기 위한 ID -->
name="fullname" <!-- 📤 서버로 전송될 데이터 이름 -->
placeholder="홍길동" <!-- 💭 입력 힌트 -->
required <!-- ❗ 필수 입력 항목 -->
maxlength="20" /> <!-- 📏 최대 20글자까지만 입력 가능 -->
</div>
<!-- 📧 이메일 입력 필드 -->
<div class="form-group">
<label for="email">📧 이메일:</label>
<input type="email" <!-- 📧 이메일 형식 자동 검증 -->
id="email"
name="email"
placeholder="example@email.com"
required />
</div>
<!-- 🔒 비밀번호 입력 필드 -->
<div class="form-group">
<label for="password">🔒 비밀번호:</label>
<input type="password" <!-- 🔒 입력 내용이 * 으로 숨겨짐 -->
id="password"
name="password"
minlength="8" <!-- 📏 최소 8글자 이상 -->
required />
</div>
</fieldset>
<fieldset>
<legend>🎯 관심 분야</legend>
<!-- ☑️ 체크박스 (여러 개 선택 가능) -->
<div class="checkbox-group">
<input type="checkbox" <!-- ☑️ 체크박스 타입 -->
id="frontend"
name="interests" <!-- 📤 같은 name으로 그룹화 -->
value="frontend" /> <!-- 📋 선택했을 때 전송될 값 -->
<label for="frontend">🎨 프론트엔드</label>
<input type="checkbox"
id="backend"
name="interests"
value="backend" />
<label for="backend">⚙️ 백엔드</label>
<input type="checkbox"
id="design"
name="interests"
value="design" />
<label for="design">🎨 UI/UX 디자인</label>
</div>
<!-- 🔘 라디오 버튼 (하나만 선택 가능) -->
<div class="radio-group">
<p>💼 희망 직무:</p>
<input type="radio" <!-- 🔘 라디오 버튼 타입 -->
id="developer"
name="job" <!-- 📤 같은 name끼리는 하나만 선택됨 -->
value="developer"
checked /> <!-- ✅ 기본으로 선택된 상태 -->
<label for="developer">👨💻 개발자</label>
<input type="radio"
id="designer"
name="job"
value="designer" />
<label for="designer">🎨 디자이너</label>
<input type="radio"
id="pm"
name="job"
value="pm" />
<label for="pm">📊 프로젝트 매니저</label>
</div>
</fieldset>
<!-- 📤 제출 버튼 -->
<div class="button-group">
<button type="submit" <!-- 📤 폼 데이터 전송 -->
class="submit-btn">
🚀 가입하기
</button>
<button type="reset" <!-- 🔄 모든 입력 내용 초기화 -->
class="reset-btn">
🗑️ 초기화
</button>
</div>
</form>
🎭 시맨틱 HTML 태그¶
시맨틱 태그는 태그 이름만 봐도 의미를 알 수 있는 태그들입니다.
graph TD
A[🏠 웹페이지 구조] --> B[🎪 header<br/>상단 영역]
A --> C[🧭 nav<br/>네비게이션]
A --> D[📄 main<br/>메인 콘텐츠]
A --> E[🔀 aside<br/>사이드바]
A --> F[🏁 footer<br/>하단 영역]
D --> G[📋 section<br/>섹션]
D --> H[📰 article<br/>독립 콘텐츠]
G --> I[🏆 header<br/>섹션 헤더]
G --> J[📝 내용]
G --> K[🏁 footer<br/>섹션 푸터]
style A fill:#e8eaf6
style B fill:#ffcdd2
style C fill:#c8e6c9
style D fill:#fff3e0
style E fill:#f8bbd9
style F fill:#d1c4e9
🎨 완전한 시맨틱 웹페이지 구조¶
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>🌟 모던 웹사이트</title>
<style>
/* 🎨 아름다운 스타일링 */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', sans-serif; line-height: 1.6; }
header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 2rem; }
nav ul { list-style: none; display: flex; gap: 2rem; }
nav a { color: white; text-decoration: none; transition: 0.3s; }
nav a:hover { color: #ffd700; }
main { max-width: 1200px; margin: 0 auto; padding: 2rem; }
section { margin: 3rem 0; padding: 2rem; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
aside { background: #f8f9fa; padding: 1.5rem; border-radius: 8px; }
footer { background: #2c3e50; color: white; text-align: center; padding: 2rem; }
</style>
</head>
<body>
<!-- 🎪 사이트 전체 헤더 -->
<header role="banner"> <!-- 🎯 접근성을 위한 role 속성 -->
<h1>🌟 테크 블로그</h1>
<p>💻 개발자를 위한 최신 기술 정보</p>
<!-- 🧭 메인 네비게이션 -->
<nav role="navigation"> <!-- 🧭 네비게이션 역할 명시 -->
<ul>
<li><a href="#home">🏠 홈</a></li>
<li><a href="#articles">📰 글 목록</a></li>
<li><a href="#about">👨💻 소개</a></li>
<li><a href="#contact">📞 연락처</a></li>
</ul>
</nav>
</header>
<!-- 📄 메인 콘텐츠 영역 -->
<main role="main">
<!-- 📋 최신 글 섹션 -->
<section id="latest-posts">
<header> <!-- 📰 섹션만의 헤더 -->
<h2>🔥 최신 글</h2>
<p>📅 가장 최근에 업데이트된 개발 관련 글들</p>
</header>
<!-- 📰 개별 글 (독립적인 콘텐츠) -->
<article class="blog-post">
<header class="post-header">
<h3>⚛️ React 18의 새로운 기능들</h3>
<div class="post-meta"> <!-- 📋 글의 메타 정보 -->
<time datetime="2025-08-19">📅 2025년 8월 19일</time> <!-- ⏰ 시간 정보 -->
<span class="author">✍️ 작성자: 김개발</span>
<span class="category">🏷️ 카테고리: React</span>
</div>
</header>
<div class="post-content">
<p>🎯 React 18에서 도입된 혁신적인 기능들을 알아보겠습니다...</p>
<img src="react18-features.jpg"
alt="React 18 새 기능들을 보여주는 인포그래픽"
width="100%"
loading="lazy" />
</div>
<footer class="post-footer">
<div class="tags"> <!-- 🏷️ 태그 영역 -->
<span class="tag">⚛️ React</span>
<span class="tag">📚 Tutorial</span>
<span class="tag">🆕 New Features</span>
</div>
<a href="react18-guide.html" class="read-more">
📖 전체 글 읽기 →
</a>
</footer>
</article>
<!-- 📰 두 번째 글 -->
<article class="blog-post">
<header class="post-header">
<h3>🎨 CSS Grid vs Flexbox 완벽 비교</h3>
<div class="post-meta">
<time datetime="2025-08-18">📅 2025년 8월 18일</time>
<span class="author">✍️ 작성자: 박디자인</span>
<span class="category">🏷️ 카테고리: CSS</span>
</div>
</header>
<div class="post-content">
<p>🤔 언제 Grid를 쓰고 언제 Flexbox를 써야 할까요?</p>
</div>
<footer class="post-footer">
<div class="tags">
<span class="tag">🎨 CSS</span>
<span class="tag">📐 Layout</span>
<span class="tag">💡 Tips</span>
</div>
<a href="css-grid-flexbox.html" class="read-more">
📖 전체 글 읽기 →
</a>
</footer>
</article>
</section>
</main>
<!-- 🔀 사이드바 (관련 정보) -->
<aside role="complementary"> <!-- 🔀 메인 콘텐츠를 보완하는 역할 -->
<section class="widget">
<h3>🔥 인기 글</h3>
<ol class="popular-posts">
<li><a href="#">🚀 JavaScript ES2025 새 기능</a></li>
<li><a href="#">🎯 Node.js 성능 최적화 방법</a></li>
<li><a href="#">📱 반응형 웹 디자인 완벽 가이드</a></li>
</ol>
</section>
<section class="widget">
<h3>🏷️ 카테고리</h3>
<ul class="categories">
<li><a href="#">⚛️ React (15)</a></li>
<li><a href="#">🎨 CSS (12)</a></li>
<li><a href="#">⚡ JavaScript (20)</a></li>
<li><a href="#">🔧 Node.js (8)</a></li>
</ul>
</section>
</aside>
<!-- 🏁 사이트 푸터 -->
<footer role="contentinfo"> <!-- 📋 사이트 정보 역할 -->
<div class="footer-content">
<div class="footer-section">
<h4>📞 연락처</h4>
<address> <!-- 📮 연락처 정보 전용 태그 -->
📧 <a href="mailto:info@techblog.com">info@techblog.com</a><br />
📱 <a href="tel:02-1234-5678">02-1234-5678</a>
</address>
</div>
<div class="footer-section">
<h4>🔗 소셜 미디어</h4>
<a href="https://github.com/techblog"
target="_blank"
rel="noopener"
aria-label="GitHub 페이지"> <!-- ♿ 접근성을 위한 설명 -->
💻 GitHub
</a>
<a href="https://twitter.com/techblog"
target="_blank"
rel="noopener"
aria-label="트위터 페이지">
🐦 Twitter
</a>
</div>
</div>
<hr style="margin: 2rem 0; border: none; height: 1px; background: #34495e;" />
<p>📅 © 2025 테크블로그. 모든 권리 보유.
<small>💝 Made with ❤️ by developers</small>
</p>
</footer>
</body>
</html>
🎯 속성 활용 고급 팁¶
🔧 데이터 속성 (Custom Data Attributes)¶
<!-- 📊 JavaScript에서 사용할 커스텀 데이터 저장 -->
<div class="product-card"
data-product-id="12345" <!-- 📋 상품 ID 저장 -->
data-price="29900" <!-- 💰 가격 정보 저장 -->
data-category="electronics" <!-- 🏷️ 카테고리 정보 저장 -->
data-in-stock="true"> <!-- 📦 재고 상태 저장 -->
<h3>📱 스마트폰</h3>
<p>💰 가격: 29,900원</p>
<button onclick="addToCart(this)">🛒 장바구니 담기</button>
</div>
<script>
function addToCart(button) {
// 🎯 HTML에서 저장한 데이터를 JavaScript에서 활용
const productCard = button.parentElement;
const productId = productCard.dataset.productId; // "12345"
const price = productCard.dataset.price; // "29900"
console.log(`상품 ${productId}를 장바구니에 추가!`);
}
</script>
♿ 접근성 속성¶
<!-- 🌟 모든 사용자를 배려하는 접근성 속성들 -->
<img src="chart.png"
alt="2025년 1분기 매출 증가 차트. 전년 대비 15% 상승" <!-- 🔤 상세한 이미지 설명 -->
aria-describedby="chart-description" /> <!-- 🔗 추가 설명과 연결 -->
<div id="chart-description" class="sr-only"> <!-- 📋 차트에 대한 자세한 설명 -->
이 차트는 2025년 1분기 매출이 전년 동기 대비 15% 증가했음을 보여줍니다.
1월 100만원, 2월 120만원, 3월 150만원을 기록했습니다.
</div>
<!-- 🎛️ ARIA 속성으로 접근성 향상 -->
<button aria-label="메뉴 열기" <!-- 🔤 버튼의 명확한 설명 -->
aria-expanded="false" <!-- 📋 메뉴 펼침 상태 -->
aria-controls="mobile-menu"> <!-- 🔗 제어하는 요소 지정 -->
🍔
</button>
<nav id="mobile-menu"
aria-hidden="true" <!-- 👁️ 화면에서 숨겨진 상태 -->
role="navigation">
<!-- 메뉴 내용 -->
</nav>
📋 HTML 속성 치트시트¶
🌍 전역 속성 (모든 태그 사용 가능)¶
속성 | 용도 | 예제 | 설명 |
---|---|---|---|
id |
🆔 고유 식별자 | id="header" |
페이지에서 하나만 존재 |
class |
👥 그룹 식별자 | class="button primary" |
여러 클래스 공백으로 구분 |
style |
🎨 인라인 스타일 | style="color: red;" |
CSS 직접 적용 |
title |
💭 툴팁 | title="도움말" |
마우스 오버시 표시 |
lang |
🌐 언어 설정 | lang="ko" |
해당 요소의 언어 |
dir |
➡️ 텍스트 방향 | dir="rtl" |
우→좌 (아랍어 등) |
🔗 링크 속성¶
속성 | 용도 | 값 예시 | 설명 |
---|---|---|---|
href |
🎯 링크 주소 | "https://example.com" |
이동할 URL |
target |
🪟 열기 방식 | "_blank" , "_self" |
새 창 또는 현재 창 |
rel |
🔗 관계 설정 | "noopener" , "nofollow" |
보안 및 SEO |
download |
💾 다운로드 | "report.pdf" |
파일 다운로드 |
🖼️ 이미지 속성¶
속성 | 용도 | 예시 | 설명 |
---|---|---|---|
src |
📁 이미지 경로 | "images/photo.jpg" |
이미지 파일 위치 |
alt |
🔤 대체 텍스트 | "아름다운 풍경" |
접근성 필수 |
width |
📏 가로 크기 | "300" |
픽셀 단위 |
height |
📏 세로 크기 | "200" |
픽셀 단위 |
loading |
⚡ 로딩 방식 | "lazy" , "eager" |
성능 최적화 |
🚀 실무 활용 패턴¶
🎮 인터랙티브 요소들¶
<!-- 🎮 사용자와 상호작용하는 요소들 -->
<details open> <!-- 📂 펼침/접힘 가능한 영역 -->
<summary>🔍 자세히 보기</summary> <!-- 📋 펼침/접힘 버튼 역할 -->
<p>📄 여기에 숨겨진 상세 내용이 들어갑니다.</p>
<p>🎯 사용자가 summary를 클릭하면 이 내용이 보이거나 숨겨집니다.</p>
</details>
<!-- 🎵 미디어 요소들 -->
<audio controls <!-- 🎛️ 재생 컨트롤 표시 -->
preload="metadata" <!-- 📋 메타데이터만 미리 로드 -->
loop> <!-- 🔄 반복 재생 -->
<source src="music.mp3" type="audio/mpeg"> <!-- 🎵 MP3 파일 -->
<source src="music.ogg" type="audio/ogg"> <!-- 🎵 OGG 파일 (브라우저 호환성) -->
❌ 오디오를 지원하지 않는 브라우저입니다.
</audio>
<video width="640"
height="360"
controls <!-- 🎛️ 재생 컨트롤 -->
poster="video-thumbnail.jpg" <!-- 🖼️ 비디오 시작 전 보여줄 이미지 -->
muted <!-- 🔇 음소거 상태로 시작 -->
autoplay> <!-- ▶️ 자동 재생 (음소거와 함께 사용 권장) -->
<source src="demo.mp4" type="video/mp4">
<source src="demo.webm" type="video/webm">
❌ 비디오를 지원하지 않는 브라우저입니다.
</video>
📊 테이블 구조¶
<!-- 📊 정보를 표로 정리하기 -->
<table class="data-table">
<caption>📈 2025년 분기별 매출 현황</caption> <!-- 📋 표의 제목 -->
<thead> <!-- 📋 표의 헤더 영역 -->
<tr> <!-- 📏 표의 한 행 -->
<th scope="col">📅 분기</th> <!-- 🏷️ 열 제목 (굵게 표시) -->
<th scope="col">💰 매출</th>
<th scope="col">📈 증가율</th>
</tr>
</thead>
<tbody> <!-- 📄 표의 본문 영역 -->
<tr>
<td>🌸 1분기</td> <!-- 📋 일반 데이터 셀 -->
<td>150만원</td>
<td style="color: green;">📈 +15%</td>
</tr>
<tr>
<td>☀️ 2분기</td>
<td>180만원</td>
<td style="color: green;">📈 +20%</td>
</tr>
</tbody>
<tfoot> <!-- 🏁 표의 푸터 영역 -->
<tr>
<td><strong>📊 총계</strong></td>
<td><strong>330만원</strong></td>
<td><strong>📈 +17.5%</strong></td>
</tr>
</tfoot>
</table>
🎨 고급 속성 활용법¶
🎭 조건부 속성¶
<!-- 🎮 상태에 따라 달라지는 속성들 -->
<button disabled <!-- ❌ 비활성 상태 -->
title="로그인 후 사용 가능">
🔒 프리미엄 기능
</button>
<button type="submit" <!-- ✅ 활성 상태 -->
formnovalidate <!-- 🚫 폼 검증 건너뛰기 -->
title="임시 저장하기">
💾 임시 저장
</button>
<!-- 📱 반응형 이미지 -->
<img src="small-image.jpg"
srcset="small-image.jpg 300w, <!-- 📱 300px 이하에서 사용할 이미지 -->
medium-image.jpg 600w, <!-- 💻 600px 이하에서 사용할 이미지 -->
large-image.jpg 1200w" <!-- 🖥️ 1200px 이하에서 사용할 이미지 -->
sizes="(max-width: 300px) 100vw, <!-- 📏 화면 크기별 이미지 표시 크기 -->
(max-width: 600px) 50vw,
25vw"
alt="다양한 화면 크기에 최적화된 이미지" />
🔄 동적 콘텐츠 속성¶
<!-- 📊 진행률 표시 -->
<progress value="75" <!-- 📊 현재 진행률 -->
max="100" <!-- 📊 최대값 -->
title="프로젝트 진행률 75%">
75% 완료
</progress>
<!-- 📏 범위 슬라이더 -->
<label for="volume">🔊 볼륨:</label>
<input type="range" <!-- 🎚️ 슬라이더 타입 -->
id="volume"
name="volume"
min="0" <!-- 📉 최소값 -->
max="100" <!-- 📈 최대값 -->
value="50" <!-- 🎯 기본값 -->
step="5" <!-- 📏 증감 단위 -->
oninput="updateVolume(this.value)"> <!-- ⚡ 값 변경시 실행할 함수 -->
<output for="volume" id="volume-display">🔊 50</output> <!-- 📊 현재 값 표시 -->
🌟 최신 HTML5 기능들¶
graph LR
A[🆕 HTML5 신기능] --> B[📱 미디어]
A --> C[📝 폼 개선]
A --> D[🏷️ 시맨틱]
A --> E[🎨 그래픽]
B --> F[🎵 audio<br/>🎬 video]
C --> G[📧 email<br/>📅 date<br/>🔢 number]
D --> H[📰 article<br/>📋 section<br/>🧭 nav]
E --> I[🎨 canvas<br/>📐 svg]
style A fill:#e1f5fe
style B fill:#e8f5e8
style C fill:#fff3e0
style D fill:#f3e5f5
style E fill:#fce4ec
📅 새로운 입력 타입들¶
<!-- 📅 날짜와 시간 입력 -->
<label for="birthday">🎂 생일:</label>
<input type="date" <!-- 📅 달력 선택기 -->
id="birthday"
name="birthday"
min="1900-01-01" <!-- 📉 최소 날짜 -->
max="2025-12-31" /> <!-- 📈 최대 날짜 -->
<label for="meeting-time">⏰ 회의 시간:</label>
<input type="datetime-local" <!-- 📅⏰ 날짜 + 시간 선택 -->
id="meeting-time"
name="meeting-time" />
<!-- 🎨 색상 선택기 -->
<label for="brand-color">🎨 브랜드 색상:</label>
<input type="color" <!-- 🌈 색상 팔레트 -->
id="brand-color"
name="brand-color"
value="#ff6b6b" /> <!-- 🎯 기본 색상 -->
<!-- 📏 숫자 입력 -->
<label for="quantity">📦 수량:</label>
<input type="number" <!-- 🔢 숫자만 입력 가능 -->
id="quantity"
name="quantity"
min="1" <!-- 📉 최소 1개 -->
max="100" <!-- 📈 최대 100개 -->
value="1" <!-- 🎯 기본값 1개 -->
step="1" /> <!-- 📏 1개씩 증감 -->
🎯 성능 최적화 속성¶
⚡ 로딩 최적화¶
<!-- 🚀 빠른 페이지 로딩을 위한 속성들 -->
<!-- 🖼️ 지연 로딩 이미지 -->
<img src="hero-image.jpg"
alt="메인 배너 이미지"
loading="lazy" <!-- ⏰ 스크롤할 때까지 로딩 지연 -->
decoding="async" <!-- 🔄 비동기 디코딩 -->
importance="high" /> <!-- 🎯 중요도 높음 (우선 로딩) -->
<!-- 🔗 링크 미리 로딩 -->
<link rel="preload" <!-- 📥 미리 로딩할 리소스 -->
href="important-font.woff2"
as="font" <!-- 📝 리소스 타입 -->
type="font/woff2"
crossorigin />
<link rel="prefetch" <!-- 📦 다음에 필요할 수 있는 리소스 -->
href="next-page.html" />
<!-- 🔗 DNS 미리 연결 -->
<link rel="dns-prefetch" <!-- 🌐 도메인 미리 연결 -->
href="//fonts.googleapis.com" />
🛡️ 보안 관련 속성¶
웹 개발에서 보안은 매우 중요한 요소입니다. 특히 외부 링크, 사용자 입력 처리, 민감한 리소스 접근 시 주의해야 합니다.
🔒 외부 링크 보안 속성¶
<!-- 🌐 외부 사이트를 새 창에서 열 때 -->
<a href="https://external-site.com"
target="_blank" <!-- 🆕 새 탭에서 열기 -->
rel="noopener noreferrer" <!-- 🛡️ 보안 속성: 탭 간 침입 방지 -->
title="외부 사이트로 이동">
🚀 외부 사이트 방문하기
</a>
📌 속성 설명¶
-
target="_blank"
: 새 창(새 탭)에서 열기 -
rel="noopener"
: 새 탭이 원래 창의window.opener
에 접근하지 못하게 차단 -
rel="noreferrer"
: 원래 페이지의 참조 정보(Referrer)를 전송하지 않음
💡 Tip: 외부 링크를 열 때는 반드시
rel="noopener noreferrer"
를 함께 사용하세요!
🔑 입력 데이터 보안 속성¶
사용자 입력은 SQL Injection, XSS와 같은 공격에 악용될 수 있습니다. HTML 차원에서 다음 속성을 활용하세요.
<!-- 📧 이메일 입력 필드 -->
<input type="email" <!-- 이메일 형식만 허용 -->
name="userEmail"
required /> <!-- 값이 없으면 제출 불가 -->
<!-- 🔒 비밀번호 입력 필드 -->
<input type="password"
name="password"
minlength="8" <!-- 최소 8자 이상 입력 강제 -->
required />
<!-- 🔢 숫자 입력 범위 제한 -->
<input type="number"
name="age"
min="1" max="120" /> <!-- 1 ~ 120 사이 값만 허용 -->
🛡️ 프론트엔드의 입력 제한은 보조 역할일 뿐, 백엔드 검증이 반드시 필요합니다.
🖼️ 보안 속성 - 이미지/리소스¶
<!-- ⚡ 성능 & 보안 최적화된 이미지 로딩 -->
<img src="profile.jpg"
alt="사용자 프로필 사진"
loading="lazy" <!-- 스크롤 시 로드 -->
referrerpolicy="no-referrer" <!-- 리퍼러 정보 숨기기 -->
crossorigin="anonymous" /> <!-- 교차 출처 요청 시 인증 제외 -->
속성 설명¶
-
referrerpolicy
: 외부 리소스에 현재 페이지 URL이 노출되지 않도록 제어 -
crossorigin
: 교차 출처(CORS) 리소스 요청 시 인증 정보를 어떻게 처리할지 결정
📊 보안 관련 속성 관계도¶
graph TD
A["🌐 외부 리소스/링크 보안"] --> B["target=_blank"]
A --> C["rel=noopener"]
A --> D["rel=noreferrer"]
E["📧 사용자 입력 보안"] --> F["type 제한: email, number"]
E --> G["min, max, minlength, maxlength"]
E --> H["required"]
I["🖼️ 리소스 보안"] --> J["referrerpolicy"]
I --> K["crossorigin"]
I --> L["loading=lazy"]
style A fill:#fce4ec
style E fill:#e1f5fe
style I fill:#f3e5f5
✅ 정리¶
- 외부 링크는 반드시
rel="noopener noreferrer"
로 보호 - 입력 필드는 HTML 속성(
type
,required
,minlength
)으로 1차 방어 - 이미지/리소스 요청 시
referrerpolicy
와crossorigin
활용 - 최종적으로는 서버에서 입력 검증과 보안 처리가 필수
🎯 HTML 보안 속성은 완벽한 방패는 아니지만, 첫 번째 안전망 역할을 합니다.