<scshin />
👋 환영합니다

생각을 기록하고
경험을 공유합니다

기술, 일상, 그리고 다양한 생각들을 담은 개인 블로그입니다. 함께 성장하고 배워가는 공간이 되길 바랍니다.

10게시글
3카테고리
0태그
Linux iptables 완벽 가이드카테고리

Linux iptables 완벽 가이드

<br /> Linux iptables 완벽 가이드 리눅스 서버를 운영하다 보면 가장 많이 접하게 되는 보안 기능 중 하나가 iptables입니다. iptables는 서버로 들어오고 나가는 네트워크 패킷을 제어하여 허용하거나 차단하는 리눅스 방화벽(Firewall) 입니다. 이번 글에서는 iptables의 기본 개념부터 체인(Chain), NAT, 사용자 정의 체인, 실무 예제까지 한 번에 정리해보겠습니다. *** iptables란? iptables는 Linux Kernel의 Netfilter 프레임워크를 제어하는 사용자 공간(User Space) 프로그램입니다. 쉽게 말하면 * 특정 IP만 접속 허용 * 특정 포트 차단 * NAT(Network Address Translation) * 포트포워딩 * 패킷 필터링 등을 수행할 수 있습니다. 대표적인 활용 예시는 다음과 같습니다. * SSH(22)만 허용 * HTTP(80), HTTPS(443) 허용 * FTP(21) 차단 * 특정 국가 또는 IP 차단 * Docker, KVM NAT 구성 *** iptables 동작 구조 패킷은 서버에 도착하면 다음과 같은 순서로 처리됩니다. 인터넷 │ ▼ PREROUTING │ ┌─-────-───┴───-─────-──┐ ▼ ▼ INPUT FORWARD │ │ ▼ ▼ Local Host 다른 서버 전달 │ ▼ OUTPUT │ ▼ POSTROUTING │ ▼ 인터넷 *** Table과 Chain iptables는 여러 개의 Table과 Chain으로 구성됩니다. Table 실무에서는 대부분 filter와 nat를 사용합니다. *** Chain filter 테이블에는 기본적으로 3개의 Chain이 존재합니다. INPUT 인터넷 │ ▼ 내 서버 OUTPUT 내 서버 │ ▼ 인터넷 FORWARD 인터넷 │ ▼ 내 서버 │ ▼ 다른 서버 *** 기본 정책(Default Policy) 현재 정책 확인 iptables -L 또는 iptables -L -n 모든 INPUT 허용 iptables -P INPUT ACCEPT 모든 INPUT 차단 iptables -P INPUT DROP *** 규칙(Rule) iptables는 위에서 아래 순서대로 규칙을 검사합니다. 예를 들어 iptables -A INPUT -p tcp --dport 22 -j ACCEPT 의 의미는 * INPUT 체인 * TCP 프로토콜 * 22번 포트 * 허용 입니다. *** 자주 사용하는 옵션 *** 자주 사용하는 명령어 SSH 허용 iptables -A INPUT -p tcp --dport 22 -j ACCEPT *** HTTP 허용 iptables -A INPUT -p tcp --dport 80 -j ACCEPT *** HTTPS 허용 iptables -A INPUT -p tcp --dport 443 -j ACCEPT *** Ping 허용 iptables -A INPUT -p icmp -j ACCEPT *** 특정 IP 차단 iptables -A INPUT -s 192.168.0.100 -j DROP *** 특정 IP만 SSH 허용 iptables -A INPUT \ -p tcp \ -s 192.168.0.100 \ --dport 22 \ -j ACCEPT *** FTP 차단 iptables -A INPUT -p tcp --dport 21 -j DROP *** Telnet 차단 iptables -A INPUT -p tcp --dport 23 -j DROP *** Stateful Firewall 대부분의 서버에서는 이미 연결된 세션은 자동으로 허용합니다. iptables -A INPUT \ -m conntrack \ --ctstate ESTABLISHED,RELATED \ -j ACCEPT 이 규칙이 없으면 내 서버 → 인터넷 으로 요청을 보내도 응답 패킷이 차단될 수 있습니다. *** NAT KVM, Docker에서 가장 많이 사용하는 기능입니다. iptables -t nat \ -A POSTROUTING \ -o eth0 \ -j MASQUERADE 인터넷 공유기의 NAT 기능과 동일한 역할을 수행합니다. *** 포트포워딩 80포트를 8080으로 변경 iptables -t nat \ -A PREROUTING \ -p tcp \ --dport 80 \ -j REDIRECT \ --to-port 8080 *** 사용자 정의 체인(User Defined Chain) iptables는 직접 체인을 만들어 관리할 수 있습니다. 실무에서는 서비스별로 체인을 분리하여 관리하는 경우가 많습니다. 예) * SSH_CHAIN * WEB_CHAIN * DB_CHAIN * LOG_DROP *** 체인 생성 WEB 체인 생성 iptables -N WEB 확인 iptables -L Chain WEB (0 references) *** 체인에 규칙 추가 iptables -A WEB -p tcp --dport 80 -j ACCEPT iptables -A WEB -p tcp --dport 443 -j ACCEPT 조회 iptables -L WEB -n *** INPUT에 연결(Jump) 체인을 생성했다고 자동 실행되지는 않습니다. INPUT에서 호출해야 합니다. iptables -A INPUT -j WEB 동작 순서 인터넷 ↓ INPUT ↓ WEB ↓ 80 → 허용 443 → 허용 그 외 → INPUT으로 복귀 *** RETURN 조건이 맞지 않으면 원래 체인으로 복귀합니다. iptables -A WEB \ -p tcp \ --dport 80 \ -j ACCEPT iptables -A WEB -j RETURN 흐름 INPUT ↓ WEB ↓ 80? YES → ACCEPT NO ↓ RETURN ↓ INPUT 다음 규칙 *** 체인 삭제 체인 비우기 iptables -F WEB INPUT에서 제거 iptables -D INPUT -j WEB 체인 삭제 iptables -X WEB *** 규칙 조회 패킷 수까지 보기 iptables -L -n -v 규칙 번호 보기 iptables -L --line-numbers *** 규칙 삭제 번호로 삭제 iptables -D INPUT 3 규칙으로 삭제 iptables -D INPUT \ -p tcp \ --dport 23 \ -j DROP *** 전체 삭제 iptables -F NAT 삭제 iptables -t nat -F *** 설정 저장 Rocky / CentOS iptables-save > /etc/sysconfig/iptables 또는 service iptables save *** Ubuntu iptables-save > /etc/iptables/rules.v4 일반적으로 "iptables-persistent" 패키지를 함께 사용합니다. ***

August 6, 2026
Linux 서버에 Oracle Database 19c 설치하기카테고리

Linux 서버에 Oracle Database 19c 설치하기

*** Linux 서버에 Oracle Database 19c 설치하기 이번 글에서는 Linux 서버에 Oracle Database 19c를 설치하는 방법을 정리한다. 서버 환경에서는 GUI 화면을 사용할 수 없는 경우가 많으므로, Oracle 설치 프로그램인 "runInstaller"를 Silent 모드로 실행한다. 설치 과정은 다음 순서로 진행한다. 1. 운영체제 및 서버 사양 확인 2. Oracle 설치에 필요한 패키지 설치 3. Oracle 그룹 및 사용자 생성 4. Oracle 설치 디렉터리 생성 5. 환경변수 설정 6. Oracle Database 소프트웨어 설치 7. Listener 생성 8. 데이터베이스 생성 9. 외부 접속 및 방화벽 설정 10. 설치 상태 확인 *** 1. 설치 환경 이번 글에서 사용하는 설치 환경은 다음과 같다. Oracle Database 19c 공식 지원 목록에는 Oracle Linux, Red Hat Enterprise Linux, SUSE Linux Enterprise Server 등이 포함되어 있다. Rocky Linux는 공식 지원 목록에 직접 명시되어 있지 않으므로 운영 환경에서는 Oracle Linux 또는 RHEL을 사용하는 것이 안전하다. «Rocky Linux에서도 설치를 시도할 수 있지만 운영체제 검사, 패키지 호환성, "libnsl.so.1" 등의 문제가 발생할 수 있다.» *** 2. 서버 사양 확인 Oracle Database 19c는 최소 1GB 이상의 메모리가 필요하며 2GB 이상이 권장된다. 실습용 가상머신이라면 최소 4GB 정도를 할당하는 것이 좋다. "/tmp"에는 최소 1GB 이상의 여유 공간이 필요하다. 서버 사양을 확인한다. free -h df -h df -h /tmp df -h /dev/shm uname -m 정상적인 64비트 x86 서버라면 다음과 같이 출력된다. x86_64 메모리와 Swap 크기를 별도로 확인하려면 다음 명령어를 사용한다. grep MemTotal /proc/meminfo grep SwapTotal /proc/meminfo 운영체제 정보도 확인한다. cat /etc/os-release uname -r *** 3. 호스트 이름 설정 Oracle 설치 전에 서버의 호스트 이름과 "/etc/hosts" 설정을 확인한다. hostnamectl hostname 필요한 경우 호스트 이름을 설정한다. hostnamectl set-hostname oracle19c.localdomain 서버 IP를 확인한다. ip addr "/etc/hosts" 파일을 수정한다. vi /etc/hosts 다음과 같이 서버 IP와 호스트 이름을 등록한다. 127.0.0.1 localhost localhost.localdomain 192.168.0.100 oracle19c.localdomain oracle19c "192.168.0.100" 부분은 실제 Oracle 서버 IP로 변경한다. 설정 결과를 확인한다. ping -c 3 oracle19c *** 4. 필수 패키지 설치 Oracle Linux 8 Oracle Linux에서는 Oracle이 제공하는 사전 설치 패키지를 사용하는 것이 가장 간단하다. dnf install -y oracle-database-preinstall-19c dnf install -y unzip "oracle-database-preinstall-19c" 패키지는 Oracle 설치에 필요한 패키지와 커널 파라미터, 사용자 제한 설정 등을 자동으로 구성한다. 설치 상태를 확인한다. rpm -qa | grep oracle-database-preinstall RHEL 또는 Rocky Linux 계열 Oracle Linux 사전 설치 패키지를 사용할 수 없다면 필요한 패키지를 직접 설치해야 한다. dnf install -y \ bc \ binutils \ elfutils-libelf \ elfutils-libelf-devel \ fontconfig \ gcc \ gcc-c++ \ glibc \ glibc-devel \ ksh \ libaio \ libaio-devel \ libgcc \ libnsl \ libstdc++ \ libstdc++-devel \ libX11 \ libXau \ libXi \ libXrender \ libXtst \ libxcb \ make \ policycoreutils \ policycoreutils-python-utils \ smartmontools \ sysstat \ unzip RHEL 9 계열에서는 다음 패키지가 추가로 필요할 수 있다. dnf install -y \ compat-openssl11 \ libxcrypt-compat \ libasan \ liblsan \ libibverbs \ librdmacm \ libvirt-libs RHEL 9에서 Oracle Database 19c를 설치할 경우 19.19 이상의 릴리스 업데이트가 필요하며, Oracle 공식 문서에 필요한 패키지 목록이 정리되어 있다. libnsl.so.1 오류 확인 설치 중 다음 오류가 발생할 수 있다. error while loading shared libraries: libnsl.so.1 이 경우 "libnsl" 패키지를 설치한다. dnf install -y libnsl 라이브러리가 등록되었는지 확인한다. ldconfig -p | grep libnsl *** 5. Oracle 그룹 생성 Oracle 설치를 위한 그룹을 생성한다. groupadd oinstall groupadd dba 각 그룹의 역할은 다음과 같다. *** 6. Oracle 사용자 생성 Oracle 설치 전용 사용자를 생성한다. useradd -g oinstall -G dba oracle Oracle 사용자 비밀번호를 설정한다. passwd oracle 사용자 정보를 확인한다. id oracle 정상적인 출력 예시는 다음과 같다. uid=1001(oracle) gid=1001(oinstall) groups=1001(oinstall),1002(dba) 기존 Oracle 사용자의 그룹 변경 "oracle" 사용자가 이미 존재한다면 다음 명령어를 사용한다. usermod -g oinstall -aG dba oracle 옵션의 의미는 다음과 같다. 다음 명령어도 사용할 수 있다. usermod -g oinstall -G dba oracle 하지만 "-G"만 사용하면 기존에 등록되어 있던 다른 보조 그룹이 제거될 수 있다. 기존 그룹을 유지하려면 "-aG"를 사용하는 것이 안전하다. 그룹 설정 후에는 "oracle" 사용자가 다시 로그인해야 변경 내용이 적용된다. su - oracle id *** 7. Oracle 설치 디렉터리 생성 "root" 사용자로 Oracle 설치 디렉터리를 생성한다. mkdir -p /u01/app/oracle/product/19.0.0/dbhome_1 mkdir -p /u01/app/oraInventory mkdir -p /u01/app/oracle/oradata mkdir -p /u01/app/oracle/fast_recovery_area 각 디렉터리의 용도는 다음과 같다. 디렉터리 소유자를 변경한다. chown -R oracle:oinstall /u01/app chmod -R 775 /u01/app 설정 결과를 확인한다. ls -ld /u01/app ls -ld /u01/app/oracle ls -ld /u01/app/oraInventory *** 8. Oracle 설치 파일 압축 해제 Oracle 공식 사이트에서 다음 설치 파일을 내려받는다. LINUX.X64_193000_db_home.zip Oracle Database 19c는 설치 파일을 최종 "ORACLE_HOME" 디렉터리에 압축 해제한 후 해당 위치에서 "runInstaller"를 실행하는 이미지 기반 설치 방식이다. 설치 파일을 서버로 전송한 후 압축을 해제한다. su - oracle cd /u01/app/oracle/product/19.0.0/dbhome_1 unzip /home/oracle/LINUX.X64_193000_db_home.zip 설치 파일을 확인한다. ls -l runInstaller ls -l install/response/db_install.rsp 정상적으로 압축이 해제되었다면 다음 파일이 존재한다. /u01/app/oracle/product/19.0.0/dbhome_1/runInstaller /u01/app/oracle/product/19.0.0/dbhome_1/install/response/db_install.rsp *** 9. Oracle 환경변수 설정 "oracle" 사용자의 환경변수를 설정한다. su - oracle vi ~/.bash_profile 파일 마지막에 다음 내용을 추가한다. export TMP=/tmp export TMPDIR=$TMP export ORACLE_BASE=/u01/app/oracle export ORACLE_HOME=/u01/app/oracle/product/19.0.0/dbhome_1 export ORACLE_SID=ORCL export PATH=$ORACLE_HOME/bin:$PATH export LD_LIBRARY_PATH=$ORACLE_HOME/lib:/lib64:/usr/lib64 export CLASSPATH=$ORACLE_HOME/jlib:$ORACLE_HOME/rdbms/jlib 환경변수를 적용한다. source ~/.bash_profile 설정값을 확인한다. echo $ORACLE_BASE echo $ORACLE_HOME echo $ORACLE_SID echo $PATH 정상적인 결과는 다음과 같다. /u01/app/oracle /u01/app/oracle/product/19.0.0/dbhome_1 ORCL *** 10. Oracle Database 소프트웨어 설치 Oracle 사용자로 전환한다. su - oracle source ~/.bash_profile Oracle Home으로 이동한다. cd $ORACLE_HOME 다음 명령어를 실행해 Oracle Database 소프트웨어를 설치한다. ./runInstaller \ -silent \ -waitforcompletion \ -responseFile "$ORACLE_HOME/install/response/db_install.rsp" \ oracle.install.option=INSTALL_DB_SWONLY \ UNIX_GROUP_NAME=oinstall \ INVENTORY_LOCATION=/u01/app/oraInventory \ ORACLE_HOME="$ORACLE_HOME" \ ORACLE_BASE="$ORACLE_BASE" \ oracle.install.db.InstallEdition=EE \ oracle.install.db.OSDBA_GROUP=dba \ oracle.install.db.OSOPER_GROUP=dba \ oracle.install.db.OSBACKUPDBA_GROUP=dba \ oracle.install.db.OSDGDBA_GROUP=dba \ oracle.install.db.OSKMDBA_GROUP=dba \ oracle.install.db.OSRACDBA_GROUP=dba \ SECURITY_UPDATES_VIA_MYORACLESUPPORT=false \ DECLINE_SECURITY_UPDATES=true "-silent" 옵션을 사용하면 "DISPLAY" 환경변수 없이도 설치할 수 있다. 응답 파일은 반드시 상대 경로가 아닌 절대 경로로 지정해야 한다. 설치 옵션 설명 Standard Edition 2를 설치하려면 다음 값을 사용한다. oracle.install.db.InstallEdition=SE2 *** 11. INS-32013 오류 해결 다음과 같은 오류가 발생할 수 있다. [FATAL] [INS-32013] Oracle 기본 위치가 비어 있습니다. 이 오류는 "ORACLE_BASE"가 설정되지 않았거나 "runInstaller" 실행 시 Oracle 기본 위치가 전달되지 않아 발생한다. 환경변수를 확인한다. echo $ORACLE_BASE echo $ORACLE_HOME 값이 비어 있다면 설정한다. export ORACLE_BASE=/u01/app/oracle export ORACLE_HOME=/u01/app/oracle/product/19.0.0/dbhome_1 다음과 같이 설치 명령어에도 "ORACLE_BASE"와 "ORACLE_HOME"을 명시한다. ./runInstaller \ -silent \ -responseFile "$ORACLE_HOME/install/response/db_install.rsp" \ ORACLE_BASE="$ORACLE_BASE" \ ORACLE_HOME="$ORACLE_HOME" 단, 위 명령어에는 설치에 필요한 나머지 옵션도 함께 지정해야 하므로 실제 설치 시에는 앞에서 작성한 전체 명령어를 사용한다. *** 12. root 스크립트 실행 Oracle 소프트웨어 설치가 완료되면 다음과 같은 안내가 출력된다. As a root user, execute the following script(s): 1. /u01/app/oraInventory/orainstRoot.sh 2. /u01/app/oracle/product/19.0.0/dbhome_1/root.sh "root" 사용자로 전환한다. su - 안내된 스크립트를 실행한다. /u01/app/oraInventory/orainstRoot.sh /u01/app/oracle/product/19.0.0/dbhome_1/root.sh Oracle Universal Installer 설치 후에는 "orainstRoot.sh"와 "root.sh"를 실행해야 한다. *** 13. Oracle Listener 생성 Oracle 데이터베이스에 외부 프로그램이 접속하려면 Listener가 필요하다. "oracle" 사용자로 전환한다. su - oracle source ~/.bash_profile NETCA를 Silent 모드로 실행한다. netca -silent \ -responseFile "$ORACLE_HOME/assistants/netca/netca.rsp" Listener 상태를 확인한다. lsnrctl status Listener가 실행되지 않았다면 다음 명령어로 시작한다. lsnrctl start 중지하려면 다음 명령어를 사용한다. lsnrctl stop 기본 Listener 포트는 "1521"이다. 포트가 열려 있는지 확인한다. ss -lntp | grep 1521 *** 14. Oracle 데이터베이스 생성 Oracle 소프트웨어를 설치한 후 DBCA를 사용해 데이터베이스를 생성한다. Oracle DBCA는 "-silent -createDatabase" 옵션을 사용해 화면 없이 데이터베이스를 생성할 수 있다. 다음 예제에서는 아래와 같이 데이터베이스를 생성한다. 다음 명령어를 실행한다. dbca -silent -createDatabase \ -templateName General_Purpose.dbc \ -gdbname ORCL \ -sid ORCL \ -createAsContainerDatabase true \ -numberOfPDBs 1 \ -pdbName ORCLPDB \ -sysPassword 'Oracle비밀번호변경1!' \ -systemPassword 'Oracle비밀번호변경1!' \ -pdbAdminPassword 'Oracle비밀번호변경1!' \ -databaseType MULTIPURPOSE \ -memoryMgmtType auto_sga \ -totalMemory 2048 \ -storageType FS \ -datafileDestination /u01/app/oracle/oradata \ -recoveryAreaDestination /u01/app/oracle/fast_recovery_area \ -characterSet AL32UTF8 \ -nationalCharacterSet AL16UTF16 \ -emConfiguration NONE «예제에 작성된 비밀번호는 반드시 실제 운영 환경에 맞는 안전한 비밀번호로 변경해야 한다.» 명령어에 비밀번호를 직접 작성하면 Shell History에 남을 수 있으므로 운영 환경에서는 응답 파일이나 별도의 보안 관리 방식을 사용하는 것이 좋다. *** 15. 데이터베이스 실행 상태 확인 Oracle 프로세스를 확인한다. ps -ef | grep pmon | grep -v grep 정상적으로 데이터베이스가 실행되고 있다면 다음과 같이 출력된다. oracle 12345 1 0 16:20 ? 00:00:00 ora_pmon_ORCL "sqlplus"로 접속한다. sqlplus / as sysdba 인스턴스 상태를 확인한다. SELECT instance_name, status FROM v$instance; 정상적인 결과는 다음과 같다. INSTANCE_NAME STATUS ---------------- ------------ ORCL OPEN 데이터베이스 이름과 상태를 확인한다. SELECT name, open_mode FROM v$database; *** 16. PDB 상태 확인 Oracle 19c에서는 CDB와 PDB 구조를 사용한다. PDB 목록을 확인한다. SHOW PDBS; 다음과 같이 "MOUNTED" 상태로 표시될 수 있다. CON_ID CON_NAME OPEN MODE ------ --------- ---------- 2 PDB$SEED READ ONLY 3 ORCLPDB MOUNTED PDB를 실행한다. ALTER PLUGGABLE DATABASE ORCLPDB OPEN; 서버가 다시 시작된 후에도 자동으로 열리도록 상태를 저장한다. ALTER PLUGGABLE DATABASE ORCLPDB SAVE STATE; 다시 확인한다. SHOW PDBS; 정상 상태는 다음과 같다. ORCLPDB READ WRITE SQL*Plus를 종료한다. EXIT; *** 17. Listener 서비스 등록 확인 Listener에 데이터베이스 서비스가 등록됐는지 확인한다. lsnrctl status 정상이라면 다음과 같은 서비스가 표시된다. Service "ORCL" has 1 instance(s). Service "ORCLPDB" has 1 instance(s). 서비스가 표시되지 않는다면 SQL*Plus에 접속한다. sqlplus / as sysdba 서비스를 Listener에 다시 등록한다. ALTER SYSTEM REGISTER; 다시 확인한다. lsnrctl status *** 18. 외부 접속을 위한 방화벽 설정 DBeaver, SQL Developer 등의 외부 프로그램에서 Oracle 서버에 접속하려면 TCP 1521 포트를 허용해야 한다. "root" 사용자로 실행한다. firewall-cmd --permanent --add-port=1521/tcp firewall-cmd --reload 허용된 포트를 확인한다. firewall-cmd --list-ports 다음과 같이 표시되면 정상이다. 1521/tcp 방화벽 서비스가 실행 중인지 확인한다. systemctl status firewalld *** . 20. 데이터베이스 시작 및 종료 데이터베이스 시작 sqlplus / as sysdba STARTUP; PDB도 실행한다. ALTER PLUGGABLE DATABASE ALL OPEN; 데이터베이스 종료 sqlplus / as sysdba SHUTDOWN IMMEDIATE; Listener 시작 lsnrctl start Listener 종료 lsnrctl stop Listener 재시작 lsnrctl stop lsnrctl start *** 21. 서버 시작 시 Oracle 자동 실행 설정 서버가 재부팅되었을 때 Oracle 데이터베이스가 자동으로 실행되도록 설정할 수 있다. "root" 사용자로 "/etc/oratab" 파일을 수정한다. vi /etc/oratab 다음 내용을 확인한다. ORCL:/u01/app/oracle/product/19.0.0/dbhome_1:N 마지막 값을 "N"에서 "Y"로 변경한다. ORCL:/u01/app/oracle/product/19.0.0/dbhome_1:Y Systemd 서비스 파일을 생성한다. vi /etc/systemd/system/oracle-db.service 다음 내용을 작성한다. [Unit] Description=Oracle Database 19c After=network.target [Service] Type=forking User=oracle Group=oinstall ExecStart=/u01/app/oracle/product/19.0.0/dbhome_1/bin/dbstart /u01/app/oracle/product/19.0.0/dbhome_1 ExecStop=/u01/app/oracle/product/19.0.0/dbhome_1/bin/dbshut /u01/app/oracle/product/19.0.0/dbhome_1 RemainAfterExit=yes TimeoutStartSec=300 TimeoutStopSec=300 [Install] WantedBy=multi-user.target Systemd 설정을 다시 읽는다. systemctl daemon-reload Oracle 서비스를 활성화하고 실행한다. systemctl enable oracle-db systemctl start oracle-db 상태를 확인한다. systemctl status oracle-db ***

August 5, 2026
Ubuntu에서 libvirt를 이용한 KVM 가상머신(VM) 생성 방법카테고리

Ubuntu에서 libvirt를 이용한 KVM 가상머신(VM) 생성 방법

Ubuntu에서 libvirt를 이용한 KVM 가상머신(VM) 생성 방법 개요 KVM(Kernel-based Virtual Machine)은 Linux 커널에 포함된 하이퍼바이저로, 리눅스 서버를 가상화 호스트(Hypervisor)로 사용할 수 있도록 지원합니다. 하지만 KVM만으로는 가상머신을 생성하거나 관리하기 어렵기 때문에 일반적으로 libvirt를 함께 사용합니다. libvirt는 KVM, QEMU 등의 가상화 기술을 통합 관리하는 프레임워크이며, "virsh", "virt-install", "virt-manager" 등의 도구는 모두 libvirt를 통해 가상머신을 제어합니다. *** KVM과 libvirt 구조 +---------------------+ | virt-manager | +----------+----------+ | +----------v----------+ | virsh | +----------+----------+ | +----------v----------+ | libvirt | +----------+----------+ | +----------v----------+ | QEMU/KVM | +----------+----------+ | +----------v----------+ | Virtual Machine | +---------------------+ 각 구성 요소의 역할은 다음과 같습니다. *** 1. CPU 가상화 지원 확인 먼저 CPU가 가상화를 지원하는지 확인합니다. egrep -c '(vmx|svm)' /proc/cpuinfo 또는 lscpu | grep Virtualization 출력 예시 Virtualization: VT-x * Intel → VT-x * AMD → AMD-V *** 2. KVM 설치 패키지 목록을 최신화합니다. sudo apt update 필요한 패키지를 설치합니다. sudo apt install -y \ qemu-kvm \ libvirt-daemon-system \ libvirt-clients \ bridge-utils \ virtinst \ cpu-checker *** 3. 설치 확인 kvm-ok 정상이라면 INFO: /dev/kvm exists KVM acceleration can be used *** 4. libvirt 서비스 실행 sudo systemctl enable --now libvirtd 상태 확인 systemctl status libvirtd *** 5. 사용자 권한 추가 현재 사용자를 libvirt 그룹에 추가합니다. sudo usermod -aG libvirt $USER sudo usermod -aG kvm $USER 로그아웃 후 다시 로그인하거나 newgrp libvirt 를 실행합니다. *** 6. libvirt 동작 확인 virsh list --all 출력 Id Name State *** 7. Storage Pool 생성 Ubuntu에서는 기본 Storage Pool이 없는 경우가 있습니다. 먼저 디렉터리를 생성합니다. sudo mkdir -p /var/lib/libvirt/images Storage Pool 정의 virsh pool-define-as \ default \ dir \ --target /var/lib/libvirt/images 시작 virsh pool-start default 자동 시작 virsh pool-autostart default 확인 virsh pool-list --all 예시 Name State Autostart -------------------------------- default active yes *** 8. 기본 Network 확인 virsh net-list --all 기본 네트워크가 비활성화되어 있다면 virsh net-start default 자동 시작 virsh net-autostart default *** 9. ISO 파일 준비 ISO 파일을 Storage Pool에 복사합니다. sudo cp ubuntu-24.04.iso /var/lib/libvirt/images/ *** 10. VM 생성 virt-install \ --name ubuntu24 \ --memory 4096 \ --vcpus 2 \ --disk path=/var/lib/libvirt/images/ubuntu24.qcow2,size=40 \ --cdrom /var/lib/libvirt/images/ubuntu-24.04.iso \ --os-variant ubuntu24.04 \ --network default \ --graphics vnc 옵션 설명 *** 11. VM 관리 전체 목록 virsh list --all 실행 virsh start ubuntu24 종료 virsh shutdown ubuntu24 강제 종료 virsh destroy ubuntu24 삭제 virsh undefine ubuntu24 자동 시작 virsh autostart ubuntu24 *** 12. VM 콘솔 접속 virsh console ubuntu24 종료 Ctrl + ] *** 13. VM 정보 확인 VM 정보 virsh dominfo ubuntu24 CPU 정보 virsh vcpuinfo ubuntu24 메모리 정보 virsh dommemstat ubuntu24 XML 설정 확인 virsh dumpxml ubuntu24 *** 14. 디렉터리 구조 /var/lib/libvirt/ ├── images/ │ ├── ubuntu24.qcow2 │ └── ubuntu-24.04.iso ├── dnsmasq/ ├── network/ └── storage/ *** 실무에서 권장하는 구성 *** 마무리 KVM은 Linux 커널에 내장된 강력한 가상화 기술이며, libvirt를 함께 사용하면 CLI와 GUI 환경 모두에서 효율적으로 가상머신을 관리할 수 있습니다. 특히 Ubuntu Server 환경에서는 "virsh"와 "virt-install"을 이용한 VM 생성 방식이 가장 널리 사용되며, 운영 환경에서는 Bridge 네트워크와 qcow2 디스크 포맷을 함께 사용하는 구성이 일반적입니다.

August 4, 2026
Docker와 KVM의 차이점 알아보기카테고리

Docker와 KVM의 차이점 알아보기

Docker와 KVM의 차이점 알아보기 가상화 기술을 공부하다 보면 가장 많이 접하게 되는 것이 Docker와 **KVM(Kernel-based Virtual Machine)**입니다. 처음에는 둘 다 가상화 기술이라 비슷해 보이지만, 실제로는 동작 방식과 사용 목적이 크게 다릅니다. 이번 글에서는 Docker와 KVM의 차이점을 쉽게 이해할 수 있도록 정리해보겠습니다. *** Docker란? Docker는 컨테이너(Container) 기반 가상화 기술입니다. 애플리케이션을 실행하는 데 필요한 라이브러리와 설정 파일만 함께 묶어 실행하며, 호스트 운영체제의 커널을 공유합니다. 즉, 운영체제를 새로 설치하지 않고 애플리케이션만 독립적으로 실행하는 기술입니다. 예를 들어 하나의 Linux 서버에서 다음과 같이 여러 서비스를 동시에 실행할 수 있습니다. Linux Host │ ├── Docker │ ├── Nginx │ ├── MariaDB │ ├── Redis │ └── Spring Boot 각 컨테이너는 서로 독립적으로 동작하지만 모두 동일한 Linux 커널을 사용합니다. *** KVM이란? KVM(Kernel-based Virtual Machine)은 리눅스 커널에 포함된 하이퍼바이저 기반의 가상화 기술입니다. 하나의 물리 서버에서 여러 개의 독립적인 운영체제를 실행할 수 있습니다. 예를 들어 다음과 같이 구성할 수 있습니다. 물리 서버 │ ├── Ubuntu VM ├── Rocky Linux VM ├── Windows Server VM └── Debian VM 각 VM은 자체 운영체제와 커널을 가지고 있으므로 실제 서버와 거의 동일하게 동작합니다. *** Docker와 KVM의 가장 큰 차이 Docker는 애플리케이션을 가상화합니다. KVM은 운영체제 자체를 가상화합니다. 이 차이가 두 기술의 가장 큰 차이입니다. *** 구조 비교 Docker Linux │ └── Docker ├── Container 1 ├── Container 2 └── Container 3 * 하나의 Linux 커널을 공유합니다. * 컨테이너마다 필요한 라이브러리만 포함합니다. * 매우 가볍고 실행 속도가 빠릅니다. *** KVM Linux │ └── KVM ├── Ubuntu VM ├── Windows VM └── Rocky VM 각 VM에는 다음과 같은 요소가 모두 존재합니다. * Kernel * Driver * System Library * System Service 즉, 각각 하나의 독립적인 컴퓨터라고 생각하면 이해하기 쉽습니다. *** Docker와 KVM 비교 *** 메모리 사용량 비교 Docker는 운영체제를 따로 실행하지 않기 때문에 메모리 사용량이 매우 적습니다. 예를 들어 Docker Spring Boot Redis MariaDB → 총 1~2GB 정도 반면 KVM은 운영체제를 각각 실행해야 하므로 메모리를 많이 사용합니다. Ubuntu VM RAM 4GB Windows VM RAM 8GB Rocky VM RAM 4GB VM마다 별도로 메모리를 할당해야 합니다. *** 실행 속도 비교 Docker는 컨테이너만 실행하면 되므로 매우 빠릅니다. docker compose up -d 몇 초 안에 실행됩니다. 반면 KVM은 운영체제를 부팅해야 하므로 시간이 더 오래 걸립니다. *** 실제 사용 사례 Docker 다음과 같은 서비스 배포에 많이 사용됩니다. * Spring Boot * Node.js * Redis * MariaDB * MySQL * Nginx 예를 들어 services: nginx: spring: mariadb: 와 같이 Docker Compose 하나만으로 전체 서비스를 실행할 수 있습니다. *** KVM 다음과 같은 경우 많이 사용됩니다. * Windows 서버 운영 * 테스트 서버 여러 대 구성 * 개발 서버 분리 * 운영 서버 분리 * 클라우드 인프라 구축 예를 들어 물리 서버 ↓ Ubuntu (Web) ↓ Rocky (DB) ↓ Windows (업무 프로그램) 처럼 하나의 서버를 여러 대의 서버처럼 사용할 수 있습니다. *** Docker와 KVM을 함께 사용할 수 있을까? 가능합니다. 실제로 기업에서는 다음과 같은 구조를 가장 많이 사용합니다. 물리 서버 ↓ KVM ↓ Ubuntu VM ↓ Docker ↓ Nginx Spring Boot Redis MariaDB 즉, * KVM으로 서버를 분리하고 * Docker로 애플리케이션을 배포합니다. 이 구조는 클라우드 환경에서도 매우 많이 사용됩니다. *** 언제 Docker를 사용할까? 다음과 같은 경우 Docker를 사용하는 것이 좋습니다. * 웹 서비스 개발 * API 서버 개발 * CI/CD * MSA(Microservice) * 개발 환경 구축 * 빠른 배포 *** 언제 KVM을 사용할까? 다음과 같은 경우 KVM을 사용하는 것이 좋습니다. * Windows 서버가 필요한 경우 * 운영체제를 여러 개 실행해야 하는 경우 * 서버를 완전히 분리해야 하는 경우 * 테스트 서버를 여러 대 운영해야 하는 경우 *** 정리 Docker와 KVM은 경쟁 관계가 아니라 서로 다른 목적을 가진 기술입니다. Docker는 애플리케이션을 실행하기 위한 컨테이너 기술이며, KVM은 운영체제를 가상화하는 서버 가상화 기술입니다. 최근에는 대부분의 기업에서 KVM 위에 Docker를 설치하여 사용하는 구조를 채택하고 있습니다. 즉, KVM으로 서버를 구성하고 Docker로 서비스를 배포하는 방식이 가장 일반적인 운영 형태입니다.

August 3, 2026
Prometheus, Grafana, Node Exporter 모니터링 환경 구축카테고리

Prometheus, Grafana, Node Exporter 모니터링 환경 구축

Podman Compose로 Prometheus, Grafana, Node Exporter 모니터링 환경 구축하기 서버를 운영하다 보면 다음과 같은 정보를 계속 확인해야 합니다. * CPU 사용률이 갑자기 높아지지 않았는지 * 메모리가 부족하지 않은지 * 디스크 용량이 가득 차지 않았는지 * 네트워크 송수신량이 비정상적으로 증가하지 않았는지 * 모니터링 대상 서버가 정상적으로 살아 있는지 명령어로 서버에 접속하여 "top", "free", "df", "ss" 등을 실행하면 현재 상태를 확인할 수 있습니다. 하지만 과거 사용량 변화와 장애 발생 시점을 확인하거나 여러 서버를 한 화면에서 관리하기에는 불편합니다. 이때 많이 사용하는 조합이 다음 세 가지입니다. * Node Exporter: 리눅스 서버의 시스템 상태를 수집 가능한 형태로 제공 * Prometheus: Node Exporter의 메트릭을 주기적으로 수집하고 저장 * Grafana: Prometheus에 저장된 메트릭을 대시보드와 그래프로 시각화 이번 글에서는 Podman Compose를 사용하여 세 서비스를 한 번에 구성하고, 리눅스 서버의 CPU, 메모리, 디스크 및 네트워크 상태를 확인하는 방법을 정리합니다. *** 1. 전체 구성 이해하기 전체 흐름은 다음과 같습니다. ┌──────────────────────────────────────────┐ │ 리눅스 호스트 서버 │ │ │ │ Node Exporter │ │ └─ CPU, 메모리, 디스크, 네트워크 메트릭 │ └───────────────────┬──────────────────────┘ │ 9100/metrics 수집 ▼ ┌──────────────────────────────────────────┐ │ Prometheus │ │ └─ 메트릭 주기적 수집 및 시계열 저장 │ └───────────────────┬──────────────────────┘ │ PromQL 조회 ▼ ┌──────────────────────────────────────────┐ │ Grafana │ │ └─ 차트, 게이지, 표, 대시보드 시각화 │ └──────────────────────────────────────────┘ 각 서비스의 기본 포트는 다음과 같습니다. *** 2. Prometheus, Grafana, Node Exporter 역할 2.1 Node Exporter Node Exporter는 리눅스 서버의 운영체제 및 하드웨어 관련 메트릭을 "/metrics" 형식으로 제공합니다. 대표적으로 다음 항목을 수집할 수 있습니다. * CPU 사용 시간과 Load Average * 전체 메모리와 사용 가능 메모리 * Swap 사용량 * 파일 시스템 전체 용량과 여유 공간 * 디스크 읽기 및 쓰기 * 네트워크 송수신량 * 네트워크 오류 및 드롭 패킷 * 서버 부팅 시간 * 파일 디스크립터와 커널 관련 정보 Node Exporter 자체가 데이터를 장기간 저장하는 것은 아닙니다. 현재 시스템 상태를 Prometheus가 읽을 수 있는 형태로 노출하는 역할을 합니다. 2.2 Prometheus Prometheus는 Node Exporter의 "/metrics" 주소에 일정한 간격으로 접속하여 메트릭을 가져옵니다. 수집한 데이터는 시간 정보와 함께 저장되므로 다음과 같은 분석이 가능합니다. * 최근 5분간 CPU 평균 사용률 * 지난 24시간 메모리 사용량 변화 * 일주일 동안 디스크 사용량 증가 추세 * 특정 시점에 서버가 응답하지 않았는지 확인 * 네트워크 트래픽 급증 시점 확인 Prometheus 데이터는 PromQL이라는 전용 쿼리 언어로 조회합니다. 2.3 Grafana Grafana는 Prometheus를 데이터 소스로 연결하여 수집된 데이터를 시각화합니다. 다음과 같은 형태로 표현할 수 있습니다. * CPU 사용률 선 그래프 * 메모리 사용률 게이지 * 디스크 사용률 표 * 네트워크 송수신량 차트 * 서버 정상 여부 상태 패널 * 임계치 초과 알림 즉, Node Exporter가 측정값을 제공하고, Prometheus가 저장하며, Grafana가 화면으로 보여주는 구조입니다. *** 3. 디렉터리 구성 작업할 디렉터리를 생성합니다. mkdir -p monitoring cd monitoring 최종 디렉터리 구조는 다음과 같습니다. monitoring/ ├── docker-compose.yml └── prometheus.yml Podman Compose 환경에 따라 파일명을 "compose.yml" 또는 "compose.yaml"로 사용해도 됩니다. *** 4. Docker Compose 파일 작성 "docker-compose.yml" 파일을 생성합니다. version: '3.8' services: node-exporter: image: prom/node-exporter:latest container_name: node-exporter restart: always network_mode: "host" pid: "host" volumes: - /:/host:ro,rslave command: - '--path.rootfs=/host' prometheus: image: prom/prometheus:latest container_name: prometheus restart: always ports: - "9090:9090" volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:Z - prometheus-data:/prometheus grafana: image: grafana/grafana:latest container_name: grafana restart: always ports: - "3000:3000" volumes: - grafana-data:/var/lib/grafana:Z volumes: prometheus-data: grafana-data: «최신 Docker Compose에서는 최상단의 "version" 항목이 사실상 필요하지 않습니다. 기존 Compose 및 Podman Compose 환경과의 호환성을 고려하여 그대로 사용할 수 있으며, 경고가 표시되는 경우 "version: '3.8'" 줄만 제거해도 됩니다.» *** 5. Compose 설정 상세 설명 5.1 Node Exporter 설정 node-exporter: image: prom/node-exporter:latest container_name: node-exporter restart: always network_mode: "host" pid: "host" volumes: - /:/host:ro,rslave command: - '--path.rootfs=/host' "network_mode: "host"" Node Exporter가 컨테이너의 네트워크가 아니라 호스트의 네트워크 환경을 기준으로 동작하도록 설정합니다. 이 설정을 사용하면 별도의 다음 포트 매핑은 필요하지 않습니다. ports: - "9100:9100" Node Exporter는 호스트의 "9100" 포트에서 직접 실행됩니다. "pid: "host"" Node Exporter가 호스트의 PID 네임스페이스를 사용하도록 설정합니다. 컨테이너 내부 프로세스가 아니라 호스트 시스템 기준으로 일부 정보를 확인하기 위한 설정입니다. "/:/host:ro,rslave" 호스트의 루트 파일 시스템 "/"을 컨테이너의 "/host"에 읽기 전용으로 연결합니다. 옵션의 의미는 다음과 같습니다. * "ro": 읽기 전용 마운트 * "rslave": 호스트의 하위 마운트 변경 사항을 컨테이너에서도 확인 Node Exporter는 호스트 시스템을 모니터링해야 하므로 컨테이너 내부 파일 시스템이 아닌 실제 호스트 파일 시스템을 참조해야 합니다. «"/" 전체를 마운트하는 항목에는 ":Z"를 추가하지 않는 것이 좋습니다. 시스템 전체 디렉터리에 SELinux 재라벨링을 적용하면 다른 서비스에 영향을 줄 수 있습니다.» "--path.rootfs=/host" Node Exporter가 "/host"를 호스트의 루트 파일 시스템으로 인식하도록 지정합니다. 이 옵션이 없으면 Node Exporter가 호스트가 아닌 컨테이너 파일 시스템을 기준으로 일부 메트릭을 수집할 수 있습니다. *** 5.2 Prometheus 설정 prometheus: image: prom/prometheus:latest container_name: prometheus restart: always ports: - "9090:9090" volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:Z - prometheus-data:/prometheus "9090:9090" 호스트의 "9090" 포트를 Prometheus 컨테이너의 "9090" 포트에 연결합니다. 브라우저에서는 다음 주소로 접속합니다. http://서버-IP:9090 "prometheus.yml" 마운트 - ./prometheus.yml:/etc/prometheus/prometheus.yml:Z 현재 디렉터리의 "prometheus.yml" 파일을 컨테이너 내부의 Prometheus 설정 파일 위치에 연결합니다. Podman과 SELinux를 사용하는 환경에서는 ":Z" 옵션으로 해당 파일을 현재 컨테이너가 접근할 수 있도록 재라벨링합니다. Prometheus 데이터 볼륨 - prometheus-data:/prometheus Prometheus가 수집한 시계열 데이터를 Named Volume에 저장합니다. 이 볼륨이 없으면 컨테이너 삭제 후 기존 모니터링 데이터가 함께 사라질 수 있습니다. *** 5.3 Grafana 설정 grafana: image: grafana/grafana:latest container_name: grafana restart: always ports: - "3000:3000" volumes: - grafana-data:/var/lib/grafana:Z "3000:3000" 호스트의 "3000" 포트를 Grafana 컨테이너의 "3000" 포트에 연결합니다. 브라우저에서는 다음 주소로 접속합니다. http://서버-IP:3000 Grafana 데이터 볼륨 - grafana-data:/var/lib/grafana:Z Grafana는 다음 정보를 "/var/lib/grafana"에 저장합니다. * 사용자 계정 * 데이터 소스 설정 * 대시보드 * 폴더 * 알림 설정 * 플러그인 및 내부 데이터 Named Volume을 연결하면 컨테이너를 다시 생성해도 설정과 대시보드가 유지됩니다. *** 6. Prometheus 수집 설정 작성 "prometheus.yml" 파일을 생성합니다. global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: "prometheus" static_configs: - targets: - "localhost:9090" - job_name: "node-exporter" static_configs: - targets: - "host.containers.internal:9100" "scrape_interval" scrape_interval: 15s Prometheus가 각 모니터링 대상에서 메트릭을 가져오는 기본 주기입니다. 위 설정에서는 15초마다 Node Exporter의 메트릭을 수집합니다. Prometheus 자체 모니터링 - job_name: "prometheus" static_configs: - targets: - "localhost:9090" Prometheus가 자기 자신의 상태를 수집합니다. 이때 "localhost:9090"은 Prometheus 컨테이너 내부의 Prometheus 자신을 의미하므로 사용할 수 있습니다. Node Exporter 모니터링 - job_name: "node-exporter" static_configs: - targets: - "host.containers.internal:9100" Node Exporter는 "network_mode: host"로 실행되고, Prometheus는 기본 컨테이너 네트워크에서 실행됩니다. 따라서 Prometheus 설정에서 다음과 같이 작성하면 안 됩니다. targets: - "localhost:9100" Prometheus 컨테이너에서 "localhost"는 호스트 서버가 아니라 Prometheus 컨테이너 자신을 가리키기 때문입니다. Podman은 일반적으로 컨테이너에서 호스트로 접근할 수 있도록 다음 호스트명을 제공합니다. host.containers.internal 환경에 따라 이 이름이 동작하지 않는 경우에는 호스트 서버의 실제 IP를 사용합니다. - job_name: "node-exporter" static_configs: - targets: - "192.168.0.10:9100" "192.168.0.10" 부분은 실제 서버 IP로 변경해야 합니다. *** 7. 컨테이너 실행 Compose 파일이 있는 디렉터리에서 실행합니다. podman compose up -d 환경에 따라 다음 명령어를 사용해야 할 수도 있습니다. podman-compose up -d "-d" 옵션은 컨테이너를 백그라운드에서 실행한다는 의미입니다. 실행 상태를 확인합니다. podman ps 정상적으로 실행되면 다음 세 컨테이너가 표시됩니다. node-exporter prometheus grafana *** 8. Node Exporter 확인 서버에서 Node Exporter 메트릭 주소를 확인합니다. curl http://127.0.0.1:9100/metrics 정상이라면 다음과 같은 메트릭이 출력됩니다. node_cpu_seconds_total{cpu="0",mode="idle"} 12345.67 node_memory_MemTotal_bytes 1.6651070464e+10 node_memory_MemAvailable_bytes 9.834479616e+09 node_filesystem_size_bytes{device="/dev/mapper/root"} 1.07294887936e+11 출력량이 많으므로 처음 몇 줄만 확인하려면 다음 명령어를 사용합니다. curl -s http://127.0.0.1:9100/metrics | head <br /> *** 9. Prometheus 확인 브라우저에서 다음 주소로 접속합니다. http://서버-IP:9090 Prometheus 메뉴에서 다음 경로로 이동합니다. Status → Targets 다음 두 대상이 "UP" 상태여야 합니다. Prometheus 쿼리 화면에서 다음 쿼리를 실행할 수도 있습니다. up 결과가 "1"이면 해당 대상에서 메트릭을 정상적으로 수집하고 있다는 의미입니다. up{job="prometheus"} 1 up{job="node-exporter"} 1 결과가 "0"이면 대상이 등록되어 있지만 현재 수집에 실패한 상태입니다. <br /> *** 10. Grafana 접속 브라우저에서 다음 주소로 접속합니다. http://서버-IP:3000 초기 관리자 계정은 기본 설정 기준으로 다음과 같습니다. 아이디: admin 비밀번호: admin 처음 로그인한 뒤에는 관리자 비밀번호를 반드시 변경합니다. 운영 환경에서는 Compose 파일에 초기 관리자 계정을 환경 변수로 설정할 수도 있습니다. grafana: image: grafana/grafana:latest container_name: grafana restart: always environment: GF_SECURITY_ADMIN_USER: admin GF_SECURITY_ADMIN_PASSWORD: 변경할-강력한-비밀번호 ports: - "3000:3000" volumes: - grafana-data:/var/lib/grafana:Z 비밀번호를 Git 저장소에 그대로 저장하는 방식은 권장하지 않습니다. 실제 운영 환경에서는 환경 파일, Secret 또는 별도의 비밀정보 관리 방법을 사용하는 것이 좋습니다. <br />*** 11. Grafana에 Prometheus 연결 Grafana 로그인 후 다음 메뉴로 이동합니다. Connections → Data sources → Add new data source "Prometheus"를 선택하고 서버 URL에 다음 값을 입력합니다. http://prometheus:9090 Grafana와 Prometheus는 같은 Compose 프로젝트의 기본 네트워크에 연결되므로 서비스 이름인 "prometheus"로 접근할 수 있습니다. 다음 주소를 입력하면 안 됩니다. http://localhost:9090 Grafana 컨테이너에서 "localhost"는 Grafana 컨테이너 자신을 의미하기 때문입니다. 마지막으로 "Save & test"를 실행하여 연결 성공 여부를 확인합니다. *** 12. Grafana 대시보드 만들기 Grafana에서는 직접 패널을 만들거나 기존 대시보드 JSON을 가져올 수 있습니다. 직접 대시보드를 만들려면 다음 메뉴로 이동합니다. Dashboards → New → New dashboard 패널을 추가한 후 데이터 소스로 Prometheus를 선택하고 PromQL을 입력합니다. 12.1 서버 정상 여부 up{job="node-exporter"} * "1": 정상 * "0": 수집 실패 또는 서버 응답 없음 12.2 CPU 사용률 100 - ( avg by (instance) ( rate(node_cpu_seconds_total{mode="idle"}[5m]) ) * 100 ) 최근 5분간 CPU 유휴 시간을 기준으로 전체 CPU 사용률을 계산합니다. 12.3 메모리 사용률 ( 1 - ( node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes ) ) * 100 전체 메모리 중 현재 사용 중인 메모리 비율을 계산합니다. 12.4 파일 시스템 사용률 100 - ( node_filesystem_avail_bytes{ fstype!~"tmpfs|overlay|squashfs" } / node_filesystem_size_bytes{ fstype!~"tmpfs|overlay|squashfs" } * 100 ) 임시 파일 시스템과 컨테이너 Overlay 파일 시스템을 제외한 디스크 사용률을 확인합니다. 마운트 위치별로 구분하려면 Grafana 범례에 다음 값을 사용할 수 있습니다. {{instance}} - {{mountpoint}} 12.5 네트워크 수신량 rate(node_network_receive_bytes_total{device!="lo"}[5m]) Loopback 인터페이스를 제외한 초당 네트워크 수신 바이트를 확인합니다. 12.6 네트워크 송신량 rate(node_network_transmit_bytes_total{device!="lo"}[5m]) 초당 네트워크 송신 바이트를 확인합니다. 12.7 서버 부하 node_load1 1분 Load Average를 확인합니다. 다음 메트릭도 함께 사용할 수 있습니다. node_load5 node_load15 <br />

July 28, 2026
Milkdown 에디터 사용법 정리카테고리

Milkdown 에디터 사용법 정리

마크다운 기반 WYSIWYG 에디터를 프로젝트에 적용하는 방법 웹 서비스를 개발하다 보면 게시글 작성, 공지사항 작성, 매뉴얼 작성, 위키 문서 작성처럼 사용자가 긴 글을 입력해야 하는 기능이 필요합니다. 이때 단순한 "<textarea>"만으로는 편집 기능이 부족하고, 일반적인 리치 텍스트 에디터를 사용하면 데이터 저장 방식이나 마크다운 관리가 복잡해질 수 있습니다. 이러한 상황에서 사용할 수 있는 에디터 중 하나가 Milkdown입니다. Milkdown은 마크다운을 기반으로 동작하는 WYSIWYG 에디터 프레임워크입니다. WYSIWYG는 “What You See Is What You Get”의 약자로, 사용자가 화면에서 보는 형태 그대로 결과물이 만들어지는 편집 방식을 의미합니다. Milkdown은 사용자가 보기 좋은 편집 화면에서 글을 작성할 수 있도록 지원하면서도, 개발자는 결과 데이터를 마크다운 형태로 저장하고 관리할 수 있도록 도와주는 에디터입니다. 1. Milkdown이란 무엇인가요? Milkdown은 웹 프로젝트에 적용할 수 있는 마크다운 중심의 에디터 프레임워크입니다. 일반적인 리치 텍스트 에디터는 HTML 중심으로 동작하는 경우가 많습니다. 사용자가 글을 작성하면 내부 데이터가 HTML이 되거나, 별도의 JSON 구조로 저장되는 경우도 있습니다. 반면 Milkdown은 마크다운을 핵심 데이터로 다룹니다. 즉, 사용자는 편집 화면에서 보기 좋게 글을 작성하고, 개발자는 작성된 내용을 마크다운 문자열로 저장할 수 있습니다. Milkdown은 내부적으로 ProseMirror와 Remark를 기반으로 동작합니다. ProseMirror는 웹 기반 문서 편집기를 만들기 위한 프레임워크이며, Remark는 마크다운을 처리하기 위한 도구입니다. Milkdown은 이 두 가지 구조를 바탕으로 마크다운 기반의 편집 경험을 제공합니다. 2. Milkdown을 사용하는 이유 Milkdown을 사용하는 이유는 여러 가지가 있습니다. 첫 번째 이유는 마크다운 기반 저장이 가능하다는 점입니다. 게시글, 기술 문서, 공지사항, 매뉴얼 같은 데이터는 HTML보다 마크다운으로 저장하는 것이 더 깔끔한 경우가 많습니다. 마크다운은 사람이 읽기 쉽고, 다른 플랫폼으로 옮기거나 HTML로 변환하기도 편리합니다. 두 번째 이유는 WYSIWYG 편집 경험을 제공한다는 점입니다. 사용자가 "# 제목", "**굵게**", "- 목록" 같은 마크다운 문법을 정확히 몰라도 편집 화면에서 자연스럽게 글을 작성할 수 있습니다. 개발자는 마크다운 데이터를 관리할 수 있고, 사용자는 일반 문서 편집기처럼 글을 작성할 수 있습니다. 세 번째 이유는 플러그인 기반 구조를 가진다는 점입니다. Milkdown은 필요한 기능을 플러그인 방식으로 추가할 수 있습니다. CommonMark, GFM, 히스토리, 클립보드, 리스너, 업로드, Slash 명령어, 툴팁, 테이블, 코드 블록 같은 기능을 프로젝트 상황에 맞게 조합할 수 있습니다. 네 번째 이유는 커스터마이징이 자유롭다는 점입니다. 완성형 에디터처럼 빠르게 붙여서 사용할 수도 있고, 프로젝트 요구사항에 맞게 직접 에디터 구성을 조립할 수도 있습니다. 관리자 화면, 블로그 작성 화면, 문서 관리 시스템처럼 다양한 형태의 화면에 적용하기 좋습니다. 다섯 번째 이유는 React, Vue, Svelte, Solid, Next.js, Nuxt 같은 프레임워크와 함께 사용할 수 있다는 점입니다. 프론트엔드 프레임워크를 사용하는 프로젝트에서도 Milkdown을 적용할 수 있으며, Vanilla TypeScript 환경에서도 사용할 수 있습니다. 3. Milkdown과 Crepe의 차이 Milkdown을 처음 사용할 때 헷갈릴 수 있는 개념이 있습니다. 바로 Milkdown과 Crepe의 차이입니다. 간단히 정리하면 다음과 같습니다. * Milkdown은 에디터를 만들기 위한 핵심 프레임워크입니다. * Crepe는 Milkdown 위에 만들어진 완성형 에디터입니다. Milkdown을 직접 사용하면 필요한 플러그인, 테마, 명령어, UI를 직접 조합해야 합니다. 반면 Crepe를 사용하면 기본 UI와 주요 기능이 포함된 에디터를 빠르게 적용할 수 있습니다. 처음 Milkdown을 프로젝트에 적용한다면 Crepe부터 사용하는 방식을 추천합니다. Crepe는 기본적인 편집 UI, 마크다운 작성 기능, 테마, 주요 편집 기능을 포함하고 있기 때문에 빠르게 결과물을 확인할 수 있습니다. 4. 설치 방법 가장 빠르게 시작하려면 "@milkdown/crepe"를 설치하면 됩니다. npm install @milkdown/crepe pnpm을 사용한다면 다음과 같이 설치합니다. pnpm add @milkdown/crepe yarn을 사용한다면 다음과 같이 설치합니다. yarn add @milkdown/crepe Crepe를 설치하면 Milkdown 기반의 완성형 마크다운 에디터를 바로 사용할 수 있습니다. 5. 기본 사용 예제 먼저 HTML에 에디터가 들어갈 영역을 생성합니다. <div id="app"></div> 그다음 TypeScript 또는 JavaScript 파일에서 Crepe 인스턴스를 생성합니다. import { Crepe } from '@milkdown/crepe' import '@milkdown/crepe/theme/common/style.css' import '@milkdown/crepe/theme/frame.css' const crepe = new Crepe({ root: '#app', defaultValue: '# Hello Milkdown\n\nMilkdown 에디터를 시작합니다.', }) await crepe.create() 위 코드를 실행하면 "#app" 영역에 Milkdown 기반 에디터가 생성됩니다. 여기서 중요한 설정은 세 가지입니다. 첫 번째는 "root"입니다. "root"는 에디터가 붙을 DOM 영역을 지정하는 옵션입니다. "'#app'"처럼 CSS 선택자를 사용할 수도 있고, "document.getElementById('app')"처럼 실제 DOM 객체를 넘길 수도 있습니다. 두 번째는 "defaultValue"입니다. "defaultValue"는 에디터가 처음 열릴 때 표시할 기본 마크다운 내용을 설정하는 옵션입니다. 세 번째는 CSS import입니다. Crepe는 테마 CSS를 import해야 화면이 정상적으로 표시됩니다. 공통 스타일을 먼저 import하고, 그다음 원하는 테마 CSS를 import하면 됩니다. 6. 테마 적용 방법 Crepe는 여러 가지 테마를 제공합니다. 대표적인 테마는 다음과 같습니다. * "frame" 테마입니다. * "crepe" 테마입니다. * "nord" 테마입니다. * "frame-dark" 테마입니다. * "crepe-dark" 테마입니다. * "nord-dark" 테마입니다. 테마를 적용할 때는 공통 스타일을 먼저 import하고, 그다음 사용할 테마 CSS를 import합니다. import '@milkdown/crepe/theme/common/style.css' import '@milkdown/crepe/theme/frame.css' 다크 테마를 사용하고 싶다면 다음과 같이 변경합니다. import '@milkdown/crepe/theme/common/style.css' import '@milkdown/crepe/theme/frame-dark.css' 관리자 페이지나 CMS 화면에서는 "frame" 또는 "nord" 계열 테마가 무난합니다. 다크 테마 기반의 대시보드나 관제 화면에 적용한다면 "frame-dark" 테마도 좋은 선택입니다. 7. 현재 작성된 마크다운 가져오기 에디터에서 작성된 내용을 DB에 저장하려면 현재 내용을 마크다운 문자열로 가져와야 합니다. Crepe에서는 "getMarkdown()" 메서드를 사용할 수 있습니다. const markdown = crepe.getMarkdown() console.log(markdown) 저장 버튼과 연결하면 다음과 같이 사용할 수 있습니다. const saveButton = document.getElementById('saveButton') saveButton?.addEventListener('click', async () => { const markdown = crepe.getMarkdown() await fetch('/api/posts', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ title: '게시글 제목', content: markdown, }), }) }) 이 방식으로 사용자가 작성한 내용을 마크다운 문자열로 가져온 뒤 서버 API로 전송할 수 있습니다. 8. 내용 변경 이벤트 감지하기 에디터 내용이 변경될 때마다 특정 작업을 해야 하는 경우가 있습니다. 예를 들어 자동 저장, 글자 수 계산, 미리보기 갱신, 임시 저장 기능을 구현할 때 내용 변경 이벤트가 필요합니다. Crepe에서는 "crepe.on()"을 사용하여 리스너를 등록할 수 있습니다. crepe.on((listener) => { listener.markdownUpdated((ctx, markdown, prevMarkdown) => { console.log('현재 마크다운:', markdown) console.log('이전 마크다운:', prevMarkdown) }) listener.focus((ctx) => { console.log('에디터 포커스') }) listener.blur((ctx) => { console.log('에디터 블러') }) }) 자동 저장 기능은 다음과 같이 구현할 수 있습니다. let timer: ReturnType<typeof setTimeout> | null = null crepe.on((listener) => { listener.markdownUpdated((ctx, markdown) => { if (timer) { clearTimeout(timer) } timer = setTimeout(() => { localStorage.setItem('draft-content', markdown) console.log('임시 저장 완료') }, 500) }) }) 위 코드는 사용자가 글을 입력할 때마다 바로 저장하지 않고, 0.5초 동안 입력이 멈추면 임시 저장을 수행하는 방식입니다. 이러한 구조를 사용하면 서버 요청을 너무 자주 보내지 않으면서도 자동 저장 기능을 구현할 수 있습니다. 9. 읽기 전용 모드 적용하기 게시글 상세보기 화면에서는 사용자가 내용을 수정하지 못하도록 읽기 전용 모드를 적용해야 할 수 있습니다. Crepe에서는 "setReadonly()" 메서드를 사용할 수 있습니다. crepe.setReadonly(true) 다시 수정 가능한 상태로 변경하려면 다음과 같이 사용합니다. crepe.setReadonly(false) 읽기 전용 모드는 게시글 상세보기, 공지사항 상세보기, 매뉴얼 조회 화면 등에 사용할 수 있습니다. 10. 에디터 제거하기 SPA 환경에서는 화면을 이동하거나 컴포넌트가 사라질 때 에디터 인스턴스를 정리해야 합니다. 이때는 "destroy()" 메서드를 사용합니다. crepe.destroy() 에디터 인스턴스를 정리하지 않으면 이벤트 리스너나 DOM 참조가 남아 메모리 누수가 발생할 수 있습니다. React, Vue 같은 프레임워크에서는 컴포넌트가 unmount될 때 에디터를 정리하는 구조를 잡는 것이 좋습니다. 11. React에서 Milkdown 사용하기 React 프로젝트에서는 "@milkdown/react"를 함께 사용할 수 있습니다. 먼저 필요한 패키지를 설치합니다. npm install @milkdown/crepe @milkdown/react @milkdown/kit 기본 구조는 다음과 같습니다. import React from 'react' import { Crepe } from '@milkdown/crepe' import { Milkdown, MilkdownProvider, useEditor } from '@milkdown/react' import '@milkdown/crepe/theme/common/style.css' import '@milkdown/crepe/theme/frame.css' const CrepeEditor = () => { useEditor((root) => { return new Crepe({ root, defaultValue: '# 제목\n\n내용을 입력하세요.', }) }, []) return <Milkdown /> } export default function EditorPage() { return ( <MilkdownProvider> <CrepeEditor /> </MilkdownProvider> ) } React에서 Milkdown을 사용할 때는 "MilkdownProvider"로 에디터 영역을 감싸고, "useEditor()"를 사용하여 에디터 인스턴스를 생성합니다. "Milkdown" 컴포넌트는 실제 에디터가 렌더링되는 영역입니다. 12. React에서 저장 버튼 만들기 React에서 저장 버튼을 만들려면 에디터 인스턴스에 접근해야 합니다. "useInstance()"를 사용하면 현재 생성된 에디터 인스턴스를 가져올 수 있습니다. import React from 'react' import { Crepe } from '@milkdown/crepe' import { Milkdown, MilkdownProvider, useEditor, useInstance } from '@milkdown/react' import { getMarkdown } from '@milkdown/kit/utils' import '@milkdown/crepe/theme/common/style.css' import '@milkdown/crepe/theme/frame.css' const Editor = () => { useEditor((root) => { return new Crepe({ root, defaultValue: '# 게시글 제목\n\n내용을 입력하세요.', }) }, []) return <Milkdown /> } const EditorControls = () => { const [loading, getEditor] = useInstance() const handleSave = () => { if (loading) { return } const editor = getEditor() if (!editor) { return } const markdown = editor.action(getMarkdown()) console.log('저장할 마크다운:', markdown) } return ( <button type="button" onClick={handleSave}> 저장 </button> ) } export default function EditorPage() { return ( <MilkdownProvider> <Editor /> <EditorControls /> </MilkdownProvider> ) } 여기서 중요한 점은 "EditorControls" 컴포넌트가 반드시 "MilkdownProvider" 내부에 있어야 한다는 점입니다. 이 구조를 사용하면 React 화면에서 에디터와 저장 버튼을 분리해서 관리할 수 있습니다. 13. 기존 게시글 수정 화면 만들기 게시글 수정 화면에서는 서버에서 가져온 마크다운 내용을 에디터에 넣어야 합니다. 가장 단순한 방식은 "defaultValue"에 서버에서 조회한 내용을 넣는 방식입니다. const Editor = ({ content }: { content: string }) => { useEditor((root) => { return new Crepe({ root, defaultValue: content, }) }, [content]) return <Milkdown /> } 하지만 실제 프로젝트에서는 게시글 내용이 API 호출 이후 늦게 들어오는 경우가 많습니다. 이 경우 에디터가 이미 생성된 뒤에 내용을 교체해야 할 수 있습니다. Milkdown에서는 전체 내용을 교체할 때 "replaceAll" 매크로를 사용할 수 있습니다. import { replaceAll } from '@milkdown/kit/utils' const handleLoadContent = () => { const editor = getEditor() if (!editor) { return } editor.action(replaceAll('# 서버에서 가져온 제목\n\n서버 내용입니다.')) } 수정 화면의 일반적인 처리 흐름은 다음과 같습니다. 1. 게시글 상세 API를 호출합니다. 2. 서버에서 마크다운 내용을 가져옵니다. 3. 에디터를 생성합니다. 4. 조회된 마크다운을 에디터에 반영합니다. 5. 사용자가 내용을 수정합니다. 6. 저장 버튼 클릭 시 "getMarkdown()"으로 현재 내용을 가져옵니다. 7. 수정 API로 마크다운 내용을 전송합니다. 이 흐름으로 구성하면 등록 화면과 수정 화면을 동일한 에디터 구조로 관리할 수 있습니다. 14. "@milkdown/kit"으로 직접 에디터 만들기 Crepe는 바로 사용할 수 있는 완성형 에디터입니다. 하지만 기능을 더 세밀하게 제어하고 싶다면 "@milkdown/kit"을 사용하여 직접 에디터를 구성할 수 있습니다. 먼저 패키지를 설치합니다. npm install @milkdown/kit 기본 에디터는 다음과 같이 만들 수 있습니다. import { Editor } from '@milkdown/kit/core' import { commonmark } from '@milkdown/kit/preset/commonmark' import '@milkdown/kit/prose/view/style/prosemirror.css' const editor = await Editor.make() .use(commonmark) .create() 이 방식은 Crepe보다 초기 설정이 많지만, 필요한 기능만 선택하여 구성할 수 있다는 장점이 있습니다. 프로젝트에서 툴바, 명령어, 업로드, 미리보기, 단축키 등을 직접 제어해야 한다면 "@milkdown/kit" 기반으로 구성하는 방식이 적합합니다. 15. 히스토리 기능 추가하기 사용자가 글을 작성하다가 실행 취소와 다시 실행을 할 수 있어야 한다면 history 플러그인을 추가합니다. import { Editor } from '@milkdown/kit/core' import { commonmark } from '@milkdown/kit/preset/commonmark' import { history } from '@milkdown/kit/plugin/history' import { nord } from '@milkdown/theme-nord' import '@milkdown/theme-nord/style.css' const editor = await Editor.make() .config(nord) .use(commonmark) .use(history) .create() "history" 플러그인을 추가하면 사용자가 입력한 내용을 되돌리거나 다시 실행할 수 있습니다. 문서 작성 화면에서는 실행 취소 기능이 거의 필수이기 때문에 기본적으로 추가하는 것이 좋습니다. 16. 리스너 플러그인으로 자동 저장 구현하기 "@milkdown/kit"을 직접 사용할 때는 listener 플러그인을 붙여 내용 변경을 감지할 수 있습니다. import { Editor, rootCtx } from '@milkdown/kit/core' import { commonmark } from '@milkdown/kit/preset/commonmark' import { listener, listenerCtx } from '@milkdown/kit/plugin/listener' const editor = await Editor.make() .config((ctx) => { ctx.set(rootCtx, document.getElementById('app')) ctx.get(listenerCtx).markdownUpdated((ctx, markdown) => { console.log('변경된 마크다운:', markdown) localStorage.setItem('draft', markdown) }) }) .use(commonmark) .use(listener) .create() 이 구조를 활용하면 사용자가 글을 작성하는 동안 내용을 자동으로 임시 저장할 수 있습니다. 관리자 공지사항, 매뉴얼 작성, 블로그 작성처럼 긴 글을 작성하는 화면에서는 자동 저장 기능을 넣는 것이 좋습니다. 17. 코드 하이라이팅 적용하기 기술 블로그나 개발 문서에서는 코드 블록 하이라이팅이 중요합니다. Milkdown에서는 코드 블록 하이라이팅을 위한 플러그인을 사용할 수 있습니다. 설치 예시는 다음과 같습니다. npm install @milkdown/plugin-highlight Shiki를 사용하는 예시는 다음과 같습니다. import { Editor } from '@milkdown/core' import { commonmark } from '@milkdown/preset-commonmark' import { highlight, highlightPluginConfig } from '@milkdown/plugin-highlight' import { createParser } from '@milkdown/plugin-highlight/shiki' async function createEditor() { const parser = await createParser({ theme: 'github-light', langs: ['javascript', 'typescript', 'python', 'html', 'css', 'json'], }) const editor = await Editor.make() .config((ctx) => { ctx.set(highlightPluginConfig.key, { parser, }) }) .use(commonmark) .use(highlight) .create() return editor } 이렇게 설정하면 다음과 같은 코드 블록에 문법 강조가 적용됩니다. ```typescript const message: string = 'Hello Milkdown' console.log(message) ``` 개발 블로그나 기술 문서 관리 시스템을 만든다면 코드 하이라이팅 기능은 꼭 고려하는 것이 좋습니다. 18. 명령어 사용하기 Milkdown은 명령어 시스템을 제공합니다. 명령어를 사용하면 버튼 클릭 시 선택한 텍스트를 굵게 처리하거나, 제목으로 변경하거나, 목록을 삽입하는 기능을 만들 수 있습니다. 예를 들어 강조 명령을 실행하는 코드는 다음과 같습니다. import { Editor, commandsCtx } from '@milkdown/kit/core' import { commonmark, toggleEmphasisCommand, } from '@milkdown/kit/preset/commonmark' const editor = await Editor.make() .use(commonmark) .create() const toggleItalic = () => { editor.action((ctx) => { const commandManager = ctx.get(commandsCtx) commandManager.call(toggleEmphasisCommand.key) }) } 버튼과 연결하면 다음과 같이 사용할 수 있습니다. <button id="italicButton">기울임</button> document.getElementById('italicButton')?.addEventListener('click', () => { toggleItalic() }) 이 구조를 활용하면 프로젝트에 맞는 커스텀 툴바를 만들 수 있습니다. 예를 들어 관리자 화면에서 제목, 굵게, 목록, 코드 블록, 이미지 삽입 버튼만 제공하는 간단한 툴바를 직접 구성할 수 있습니다. 19. 매크로 사용하기 Milkdown에는 에디터를 쉽게 조작할 수 있는 매크로가 있습니다. 자주 사용하는 매크로는 다음과 같습니다. 19-1. 현재 커서 위치에 내용 삽입하기 import { insert } from '@milkdown/kit/utils' editor.action(insert('## 새 제목')) 현재 커서 위치에 원하는 마크다운 내용을 삽입할 수 있습니다. 19-2. 전체 내용 교체하기 import { replaceAll } from '@milkdown/kit/utils' editor.action(replaceAll('# 새 문서\n\n내용을 다시 작성합니다.')) 기존 내용을 모두 지우고 새로운 마크다운 내용으로 교체할 수 있습니다. 19-3. 현재 내용을 마크다운으로 가져오기 import { getMarkdown } from '@milkdown/kit/utils' const markdown = editor.action(getMarkdown()) 현재 에디터 내용을 마크다운 문자열로 가져올 수 있습니다. 19-4. 현재 내용을 HTML로 가져오기 import { getHTML } from '@milkdown/kit/utils' const html = editor.action(getHTML()) 현재 에디터 내용을 HTML 문자열로 가져올 수 있습니다. 다만 HTML을 화면에 출력할 때는 XSS 보안 처리를 반드시 고려해야 합니다. 20. 이미지 업로드 처리 방법 실제 게시판이나 CMS에서는 이미지 업로드 기능이 필요합니다. Milkdown은 에디터 프레임워크이기 때문에 이미지 업로드 정책은 프로젝트에 맞게 별도로 설계해야 합니다. 일반적인 이미지 업로드 흐름은 다음과 같습니다. 1. 사용자가 이미지를 선택하거나 드래그 앤 드롭합니다. 2. 프론트엔드에서 이미지 파일을 서버 업로드 API로 전송합니다. 3. 서버는 파일을 저장하고 접근 가능한 URL을 반환합니다. 4. 프론트엔드는 반환받은 이미지 URL을 에디터에 삽입합니다. 5. 에디터에는 마크다운 이미지 문법으로 이미지가 표시됩니다. 예를 들어 서버에서 "/uploads/sample.png"라는 URL을 반환했다면 다음과 같은 마크다운을 에디터에 넣을 수 있습니다. ![이미지 설명](/uploads/sample.png) 삽입 코드는 다음과 같이 작성할 수 있습니다. import { insert } from '@milkdown/kit/utils' const imageUrl = '/uploads/sample.png' editor.action(insert(`![이미지 설명](${imageUrl})`)) 관리자 페이지에서 이미지 업로드 기능을 구현할 때는 다음 항목을 함께 고려해야 합니다. * 파일 확장자 제한이 필요합니다. * 파일 크기 제한이 필요합니다. * 이미지 MIME 타입 검증이 필요합니다. * 저장 경로 관리가 필요합니다. * 원본 파일명과 저장 파일명을 분리해야 합니다. * XSS 방지를 위한 URL 검증이 필요합니다. * 게시글 삭제 시 첨부 이미지 정리 정책이 필요합니다. * 사용되지 않는 고아 파일 정리 정책이 필요합니다. 이미지 업로드는 단순히 파일을 올리는 기능이 아니라, 저장 정책과 보안 정책까지 함께 설계해야 하는 기능입니다. 21. DB에는 무엇을 저장해야 하나요? Milkdown을 게시판에 적용한다면 보통 DB에는 마크다운 원문을 저장합니다. 예를 들어 게시글 테이블은 다음과 같이 구성할 수 있습니다. CREATE TABLE board_post ( post_id BIGSERIAL PRIMARY KEY, title VARCHAR(200) NOT NULL, content_markdown TEXT NOT NULL, content_html TEXT, created_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW() ); 여기서 핵심 컬럼은 "content_markdown"입니다. Milkdown에서 가져온 마크다운 원문을 "content_markdown"에 저장합니다. "content_html"은 선택 사항입니다. HTML을 매번 렌더링할 때 변환해도 되고, 조회 성능이 중요하다면 저장 시점에 HTML로 변환해서 함께 저장할 수도 있습니다. 다만 HTML을 저장하거나 화면에 출력할 때는 반드시 XSS 처리를 고려해야 합니다. 개인적으로는 원본 데이터는 마크다운으로 저장하고, 화면 출력 시 필요한 경우에만 HTML로 변환하는 방식을 추천합니다. 22. Spring Boot API 예시 프론트에서 Milkdown으로 작성한 마크다운을 Spring Boot API로 저장하는 예시는 다음과 같습니다. @RestController @RequestMapping("/api/posts") @RequiredArgsConstructor public class PostController { private final PostService postService; @PostMapping public ResponseEntity<Long> createPost(@RequestBody PostCreateRequest request) { Long postId = postService.createPost(request); return ResponseEntity.ok(postId); } } 요청 DTO는 다음과 같이 작성할 수 있습니다. @Getter @Setter public class PostCreateRequest { private String title; private String contentMarkdown; } 서비스에서는 제목과 내용을 검증한 뒤 저장합니다. @Service @RequiredArgsConstructor public class PostService { private final PostMapper postMapper; @Transactional public Long createPost(PostCreateRequest request) { if (request.getTitle() == null || request.getTitle().isBlank()) { throw new IllegalArgumentException("제목은 필수입니다."); } if (request.getContentMarkdown() == null || request.getContentMarkdown().isBlank()) { throw new IllegalArgumentException("내용은 필수입니다."); } Post post = new Post(); post.setTitle(request.getTitle()); post.setContentMarkdown(request.getContentMarkdown()); postMapper.insertPost(post); return post.getPostId(); } } 프론트에서는 다음과 같이 전송할 수 있습니다. const markdown = crepe.getMarkdown() await fetch('/api/posts', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ title: titleInput.value, contentMarkdown: markdown, }), }) 이 구조는 일반적인 게시글 등록, 공지사항 등록, 매뉴얼 등록 화면에 적용할 수 있습니다. 23. 게시글 상세보기에서는 어떻게 보여주나요? 게시글 상세보기에서는 두 가지 방식으로 내용을 보여줄 수 있습니다. 첫 번째 방식은 Milkdown을 읽기 전용 모드로 사용하는 방식입니다. const crepe = new Crepe({ root: '#viewer', defaultValue: markdownFromServer, }) await crepe.create() crepe.setReadonly(true) 이 방식은 에디터에서 작성한 형태와 상세보기 화면의 표현을 최대한 비슷하게 유지할 수 있다는 장점이 있습니다. 두 번째 방식은 마크다운을 HTML로 변환해서 보여주는 방식입니다. 블로그나 문서 화면처럼 단순 조회가 많은 화면에서는 마크다운을 HTML로 변환해서 출력하는 방식도 괜찮습니다. 다만 HTML을 직접 출력할 경우 XSS 처리가 반드시 필요합니다. 관리자 화면의 상세보기나 수정 화면에서는 Milkdown의 readonly 모드를 사용하는 방식이 편리합니다. 일반 사용자에게 공개되는 블로그 화면에서는 마크다운을 HTML로 변환해서 보여주는 방식이 더 가볍게 동작할 수 있습니다. 참고 자료 * Milkdown GitHub: "https://github.com/Milkdown/milkdown" (https://github.com/Milkdown/milkdown) * Milkdown Examples: "https://github.com/Milkdown/examples" (https://github.com/Milkdown/examples) * Milkdown 공식 문서: "https://milkdown.dev" (https://milkdown.dev/)

July 3, 2026
Next.js란? React 개발자가 알아야 할 풀스택 프레임워크카테고리

Next.js란? React 개발자가 알아야 할 풀스택 프레임워크

Next.js란? React 개발자가 알아야 할 풀스택 프레임워크 React로 웹 서비스를 만들다 보면 자연스럽게 다음과 같은 고민이 생깁니다. - 페이지 라우팅은 어떻게 구성할까? - 검색엔진 최적화, 즉 SEO는 어떻게 처리할까? - 초기 로딩 속도를 더 빠르게 만들 수 없을까? - 프론트엔드 프로젝트 안에서 간단한 API도 같이 만들 수 없을까? - 서버 사이드 렌더링과 정적 페이지 생성을 쉽게 적용할 방법은 없을까? 이런 문제를 해결하기 위해 많이 사용하는 프레임워크가 Next.js입니다. Next.js는 React 기반의 웹 애플리케이션 프레임워크입니다. 단순히 화면을 만드는 React 라이브러리에서 한 단계 더 나아가, 라우팅, 서버 렌더링, 정적 페이지 생성, 이미지 최적화, API 처리, 배포 최적화까지 웹 서비스 개발에 필요한 기능을 함께 제공합니다. *** 목차 1. "Next.js를 사용하는 이유" (#1-nextjs를-사용하는-이유) 2. "Next.js의 핵심 개념" (#2-nextjs의-핵심-개념) 3. "Next.js의 렌더링 방식" (#3-nextjs의-렌더링-방식) 4. "Route Handlers로 API 만들기" (#4-route-handlers로-api-만들기) 5. "Metadata API로 SEO 설정하기" (#5-metadata-api로-seo-설정하기) 6. "Image Optimization" (#6-image-optimization) 7. "프로젝트 생성 방법" (#7-프로젝트-생성-방법) 8. "기본 폴더 구조 예시" (#8-기본-폴더-구조-예시) 9. "Next.js가 적합한 경우" (#9-nextjs가-적합한-경우) 10. "Next.js 사용 시 주의할 점" (#10-nextjs-사용-시-주의할-점) 11. "간단한 예제 페이지" (#11-간단한-예제-페이지) 12. "마무리" (#12-마무리) *** 1. Next.js를 사용하는 이유 React만으로도 웹 화면은 충분히 만들 수 있습니다. 하지만 실제 서비스를 만들다 보면 화면 구성 외에도 처리해야 할 일이 많습니다. 예를 들어 일반적인 React 프로젝트에서는 라우팅을 위해 "react-router-dom"을 별도로 설정해야 하고, SEO를 위해 메타 태그 관리 방식을 따로 고민해야 하며, 서버에서 데이터를 미리 가져오는 구조도 직접 설계해야 합니다. Next.js는 이런 부분들을 프레임워크 차원에서 제공합니다. 대표적인 장점은 다음과 같습니다. *** 2. Next.js의 핵심 개념 2.1 App Router 최근 Next.js 프로젝트에서는 "app" 디렉터리를 사용하는 App Router 방식이 많이 사용됩니다. App Router는 파일 시스템 기반 라우팅을 제공합니다. 즉, 폴더 구조가 곧 URL 구조가 됩니다. 예를 들어 다음과 같은 구조가 있다고 가정합니다. app ├─ page.tsx ├─ layout.tsx ├─ about │ └─ page.tsx └─ posts ├─ page.tsx └─ [id] └─ page.tsx 위 구조는 다음 경로로 연결됩니다. 이처럼 Next.js에서는 라우터 설정 파일을 따로 만들지 않아도 폴더 구조만으로 페이지를 구성할 수 있습니다. *** 2.2 Layout "layout.tsx"는 여러 페이지가 공통으로 사용하는 레이아웃을 정의하는 파일입니다. 예를 들어 헤더, 사이드바, 푸터처럼 여러 페이지에서 반복되는 UI를 "layout.tsx"에 작성할 수 있습니다. // app/layout.tsx import type { Metadata } from "next"; export const metadata: Metadata = { title: "My Next App", description: "Next.js로 만든 웹 서비스입니다.", }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="ko"> <body> <header>공통 헤더</header> <main>{children}</main> <footer>공통 푸터</footer> </body> </html> ); } 이 구조를 사용하면 모든 페이지에서 공통 UI를 재사용할 수 있고, 페이지별로 중복 코드를 줄일 수 있습니다. *** 2.3 Server Components와 Client Components Next.js의 App Router에서는 기본적으로 컴포넌트가 Server Component로 동작합니다. Server Component는 서버에서 실행되는 컴포넌트입니다. 데이터베이스 조회, 서버 API 호출, 파일 시스템 접근처럼 브라우저에서 직접 처리하면 안 되는 작업을 서버에서 안전하게 처리할 수 있습니다. // app/posts/page.tsx async function getPosts() { const res = await fetch("https://jsonplaceholder.typicode.com/posts"); return res.json(); } export default async function PostsPage() { const posts = await getPosts(); return ( <div> <h1>게시글 목록</h1> <ul> {posts.slice(0, 5).map((post: any) => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ); } 반대로 브라우저에서 상태 관리, 클릭 이벤트, 입력값 처리 등이 필요한 컴포넌트는 Client Component로 만들어야 합니다. Client Component를 만들 때는 파일 상단에 ""use client""를 작성합니다. // app/components/Counter.tsx "use client"; import { useState } from "react"; export default function Counter() { const [count, setCount] = useState(0); return ( <button onClick={() => setCount(count + 1)}> 클릭 수: {count} </button> ); } 정리하면 다음과 같습니다. *** 3. Next.js의 렌더링 방식 Next.js를 이해할 때 가장 중요한 개념 중 하나는 렌더링 방식입니다. Next.js는 페이지 성격에 따라 여러 렌더링 방식을 선택할 수 있습니다. *** 3.1 SSR: Server Side Rendering SSR은 사용자가 페이지에 접근할 때마다 서버에서 HTML을 생성하는 방식입니다. 사용자별 데이터가 다르거나, 항상 최신 데이터를 보여줘야 하는 페이지에 적합합니다. 예를 들면 다음과 같은 화면에 사용할 수 있습니다. - 관리자 대시보드 - 로그인 사용자별 마이페이지 - 실시간 데이터 조회 화면 - 권한에 따라 내용이 달라지는 페이지 장점은 최신 데이터를 보여주기 좋고 SEO에도 유리하다는 점입니다. 단점은 요청마다 서버에서 HTML을 만들어야 하므로 서버 부하가 생길 수 있다는 점입니다. *** 3.2 SSG: Static Site Generation SSG는 빌드 시점에 HTML을 미리 생성해두는 방식입니다. 내용이 자주 바뀌지 않는 페이지에 적합합니다. 예를 들면 다음과 같습니다. - 회사 소개 페이지 - 서비스 소개 페이지 - 문서 페이지 - 블로그 글 상세 페이지 이미 만들어진 HTML을 제공하기 때문에 속도가 빠르고 서버 부하가 적습니다. 하지만 데이터가 자주 바뀌는 화면에는 적합하지 않을 수 있습니다. *** 3.3 ISR: Incremental Static Regeneration ISR은 정적 페이지의 장점과 데이터 갱신의 장점을 함께 가져가는 방식입니다. 정적 페이지를 제공하되, 일정 시간이 지나면 페이지를 다시 생성할 수 있습니다. 예를 들어 60초마다 데이터를 갱신하고 싶다면 다음처럼 사용할 수 있습니다. const res = await fetch("https://api.example.com/posts", { next: { revalidate: 60 }, }); 이 방식은 블로그, 상품 상세, 공지사항처럼 빠른 응답이 필요하지만 데이터가 가끔 바뀌는 페이지에 적합합니다. *** 4. Route Handlers로 API 만들기 Next.js에서는 "app/api" 경로 안에 "route.ts" 파일을 만들면 API를 구성할 수 있습니다. 예를 들어 "/api/health" API를 만들고 싶다면 다음처럼 작성합니다. // app/api/health/route.ts import { NextResponse } from "next/server"; export async function GET() { return NextResponse.json({ status: "ok", message: "서버가 정상 동작 중입니다.", }); } 이제 브라우저에서 "/api/health"로 접근하면 JSON 응답을 받을 수 있습니다. 간단한 백엔드 기능, 인증 체크, 외부 API 프록시, 관리자용 내부 API 등을 만들 때 유용합니다. 다만 규모가 큰 백엔드, 복잡한 배치 처리, 대규모 트랜잭션 처리가 필요한 시스템이라면 별도의 백엔드 서버를 분리하는 것이 더 적합할 수 있습니다. *** 5. Metadata API로 SEO 설정하기 Next.js에서는 "metadata" 객체나 "generateMetadata" 함수를 사용해 페이지의 SEO 정보를 설정할 수 있습니다. // app/blog/[id]/page.tsx import type { Metadata } from "next"; export const metadata: Metadata = { title: "Next.js 입문 가이드", description: "Next.js의 핵심 개념과 사용 이유를 정리한 글입니다.", openGraph: { title: "Next.js 입문 가이드", description: "React 기반 풀스택 프레임워크 Next.js 알아보기", type: "article", }, }; 페이지별 제목, 설명, OG 태그를 체계적으로 관리할 수 있기 때문에 블로그, 랜딩 페이지, 서비스 소개 페이지에서 특히 유용합니다. *** 6. Image Optimization 웹 서비스에서 이미지 최적화는 성능에 큰 영향을 줍니다. Next.js는 "next/image" 컴포넌트를 제공합니다. import Image from "next/image"; export default function Profile() { return ( <Image src="/profile.png" width={500} height={500} alt="프로필 이미지" /> ); } "next/image"를 사용하면 이미지 크기 최적화, lazy loading, 레이아웃 안정성 개선 등에 도움을 받을 수 있습니다. 일반 "<img>" 태그보다 설정은 조금 더 필요할 수 있지만, 실제 서비스에서는 성능과 사용자 경험 측면에서 장점이 큽니다. *** 7. 프로젝트 생성 방법 Next.js 프로젝트는 다음 명령어로 생성할 수 있습니다. npx create-next-app@latest my-next-app 실행하면 TypeScript 사용 여부, ESLint 사용 여부, Tailwind CSS 사용 여부, App Router 사용 여부 등을 선택할 수 있습니다. 프로젝트 생성 후 실행은 다음과 같습니다. cd my-next-app npm run dev 기본 개발 서버는 보통 다음 주소에서 확인할 수 있습니다. http://localhost:3000 *** 8. 기본 폴더 구조 예시 일반적인 App Router 기반 프로젝트 구조는 다음과 같이 구성할 수 있습니다. my-next-app ├─ app │ ├─ api │ │ └─ health │ │ └─ route.ts │ ├─ components │ │ └─ Counter.tsx │ ├─ posts │ │ ├─ page.tsx │ │ └─ [id] │ │ └─ page.tsx │ ├─ layout.tsx │ └─ page.tsx ├─ public │ └─ images ├─ styles ├─ next.config.ts ├─ package.json └─ tsconfig.json 프로젝트 규모가 커지면 "components", "lib", "hooks", "types", "services" 같은 폴더를 추가해서 역할별로 분리하는 것이 좋습니다. 예를 들어 다음과 같이 구성할 수 있습니다. src ├─ app ├─ components ├─ lib ├─ services ├─ hooks ├─ types └─ styles *** 9. Next.js가 적합한 경우 Next.js는 다음과 같은 프로젝트에 잘 어울립니다. - SEO가 중요한 서비스 - 블로그, 문서, 랜딩 페이지 - 관리자 페이지와 사용자 페이지가 함께 있는 웹 서비스 - React 기반으로 빠르게 서비스를 만들고 싶은 경우 - 서버 렌더링과 정적 생성을 함께 사용하고 싶은 경우 - 프론트엔드 중심이지만 간단한 API도 같이 필요한 경우 특히 페이지별로 렌더링 전략을 다르게 가져갈 수 있다는 점이 큰 장점입니다. 예를 들어 서비스 소개 페이지는 SSG로 만들고, 관리자 대시보드는 SSR로 만들고, 게시글 상세는 ISR로 구성할 수 있습니다. *** 10. Next.js 사용 시 주의할 점 Next.js가 모든 상황에서 정답은 아닙니다. 다음과 같은 점은 미리 고려하는 것이 좋습니다. 10.1 서버와 클라이언트 경계 이해가 필요합니다 App Router에서는 Server Component와 Client Component의 차이를 이해해야 합니다. 처음에는 왜 "useState"가 안 되는지, 왜 "window" 객체를 바로 사용할 수 없는지 헷갈릴 수 있습니다. 서버에서 실행되는 코드와 브라우저에서 실행되는 코드를 구분하는 습관이 필요합니다. *** 10.2 캐싱 정책을 신중하게 잡아야 합니다 Next.js는 성능을 위해 여러 캐싱 기능을 제공합니다. 하지만 어떤 데이터는 항상 최신이어야 하고, 어떤 데이터는 일정 시간 캐싱해도 됩니다. 서비스 특성에 맞게 캐싱 전략을 정하지 않으면 화면에 오래된 데이터가 보이거나, 반대로 서버 요청이 너무 많아질 수 있습니다. *** 10.3 백엔드 역할을 어디까지 맡길지 정해야 합니다 Next.js의 Route Handlers로 API를 만들 수 있지만, 모든 백엔드 기능을 Next.js 안에 넣는 것이 항상 좋은 것은 아닙니다. 대규모 서비스에서는 Spring Boot, NestJS, Django, FastAPI 같은 별도 백엔드와 Next.js 프론트엔드를 분리하는 구조도 많이 사용합니다. 예를 들어 화면은 Next.js로 구성하고, 업무 로직과 데이터 처리는 Spring Boot API 서버에서 처리하는 구조를 많이 사용합니다. 사용자 브라우저 ↓ Next.js 프론트엔드 ↓ Spring Boot API 서버 ↓ Database 이 구조를 사용하면 프론트엔드와 백엔드 역할을 명확히 분리할 수 있습니다. *** 11. 간단한 예제 페이지 다음은 Next.js에서 작성할 수 있는 간단한 메인 페이지 예시입니다. // app/page.tsx import Link from "next/link"; export default function HomePage() { return ( <main> <h1>Next.js 블로그</h1> <p> Next.js는 React 기반의 풀스택 웹 프레임워크입니다. </p> <ul> <li> <Link href="/posts">게시글 목록 보기</Link> </li> <li> <Link href="/about">서비스 소개</Link> </li> </ul> </main> ); } "Link" 컴포넌트를 사용하면 페이지 이동을 최적화할 수 있습니다. *** 12. 마무리 Next.js는 React를 기반으로 실제 서비스 개발에 필요한 기능을 통합해서 제공하는 프레임워크입니다. React만 사용할 때 직접 구성해야 했던 라우팅, 서버 렌더링, 정적 페이지 생성, 이미지 최적화, SEO 설정, API 구성 등을 Next.js에서는 비교적 체계적으로 처리할 수 있습니다. 처음에는 App Router, Server Component, Client Component, 캐싱 정책이 조금 어렵게 느껴질 수 있습니다. 하지만 이 개념들을 이해하면 단순한 프론트엔드 화면뿐만 아니라 SEO가 필요한 웹 서비스, 블로그, 관리자 페이지, 풀스택 서비스까지 더 효율적으로 만들 수 있습니다. React를 어느 정도 알고 있고 실제 서비스를 만들고 싶다면 Next.js는 충분히 배워볼 만한 프레임워크입니다. *** 참고 자료 - Next.js 공식 문서: https://nextjs.org/docs - Next.js App Router 문서: https://nextjs.org/docs/app - Next.js Route Handlers 문서: https://nextjs.org/docs/app/getting-started/route-handlers - Next.js Metadata 문서: https://nextjs.org/docs/app/getting-started/metadata-and-og-images - Next.js Image Optimization 문서: https://nextjs.org/docs/app/getting-started/images - Next.js 공식 블로그: https://nextjs.org/blog

June 30, 2026
PyTorch Tensor 기초 정리: 딥러닝의 핵심 데이터 구조 이해하기카테고리

PyTorch Tensor 기초 정리: 딥러닝의 핵심 데이터 구조 이해하기

1. PyTorch Tensor란? PyTorch Tensor는 PyTorch에서 데이터를 표현하는 가장 기본적인 자료구조입니다. 쉽게 말하면 NumPy 배열과 비슷하지만, 딥러닝 학습에 필요한 기능이 추가된 다차원 배열입니다. PyTorch에서 모델의 입력값, 출력값, 가중치, 손실값, 기울기 계산은 대부분 Tensor를 중심으로 동작합니다. 이미지 데이터 텍스트를 숫자로 바꾼 데이터 모델의 가중치 예측 결과 손실값 기울기 이런 것들이 모두 Tensor 형태로 처리됩니다. PyTorch를 공부할 때 Tensor를 먼저 이해해야 하는 이유는 간단합니다. PyTorch 모델 학습 = Tensor 연산의 반복 즉, Tensor는 PyTorch 딥러닝의 출발점입니다. *** 2. Tensor와 NumPy 배열의 차이 PyTorch Tensor는 NumPy 배열과 매우 비슷합니다. 둘 다 숫자 데이터를 다차원 배열 형태로 다룰 수 있습니다. import numpy as np import torch np_arr = np.array([1, 2, 3]) torch_tensor = torch.tensor([1, 2, 3]) print(np_arr) print(torch_tensor) 하지만 Tensor는 NumPy 배열과 비교했을 때 딥러닝에 필요한 중요한 기능을 제공합니다. 정리하면 다음과 같습니다. NumPy 배열은 일반 수치 계산에 많이 사용됩니다. PyTorch Tensor는 딥러닝 학습과 GPU 연산에 최적화되어 있습니다. *** 3. Tensor 생성하기 PyTorch를 사용하려면 먼저 "torch"를 import합니다. import torch 3.1 리스트로 Tensor 만들기 import torch x = torch.tensor([1, 2, 3]) print(x) print(type(x)) 출력 예시는 다음과 같습니다. tensor([1, 2, 3]) <class 'torch.Tensor'> 3.2 2차원 Tensor 만들기 x = torch.tensor([ [1, 2, 3], [4, 5, 6] ]) print(x) 출력 결과는 다음과 같습니다. tensor([[1, 2, 3], [4, 5, 6]]) 이 Tensor는 2행 3열 구조입니다. *** 4. 자주 사용하는 Tensor 생성 함수 PyTorch에서는 다양한 방식으로 Tensor를 만들 수 있습니다. 4.1 0으로 채운 Tensor x = torch.zeros(3, 4) print(x) 출력 결과는 다음과 같습니다. tensor([[0., 0., 0., 0.], [0., 0., 0., 0.], [0., 0., 0., 0.]]) 4.2 1로 채운 Tensor x = torch.ones(2, 3) print(x) 4.3 랜덤 Tensor x = torch.rand(2, 3) print(x) "torch.rand()"는 0 이상 1 미만의 랜덤값을 만듭니다. 4.4 정규분포 랜덤 Tensor x = torch.randn(2, 3) print(x) "torch.randn()"은 평균이 0이고 표준편차가 1인 정규분포 기반 랜덤값을 만듭니다. 4.5 연속된 숫자 Tensor x = torch.arange(0, 10) print(x) 출력 결과는 다음과 같습니다. tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) 간격을 지정할 수도 있습니다. x = torch.arange(0, 10, 2) print(x) 출력 결과는 다음과 같습니다. tensor([0, 2, 4, 6, 8]) *** 5. Tensor의 기본 속성 Tensor를 다룰 때는 다음 속성을 자주 확인합니다. shape dtype device requires_grad 예제를 보겠습니다. x = torch.tensor([ [1, 2, 3], [4, 5, 6] ]) print("shape:", x.shape) print("dtype:", x.dtype) print("device:", x.device) print("requires_grad:", x.requires_grad) 출력 예시는 다음과 같습니다. shape: torch.Size([2, 3]) dtype: torch.int64 device: cpu requires_grad: False 각 속성의 의미는 다음과 같습니다. *** 6. shape 이해하기 "shape"는 Tensor의 모양을 나타냅니다. x = torch.zeros(2, 3) print(x.shape) 출력 결과는 다음과 같습니다. torch.Size([2, 3]) 이 Tensor는 2행 3열입니다. 딥러닝에서 shape는 매우 중요합니다. 모델에 들어가는 입력 데이터의 shape가 맞지 않으면 오류가 발생합니다. 이미지 분류에서는 보통 다음과 같은 shape를 자주 봅니다. [batch_size, channels, height, width] 예를 들어 이미지 32장을 한 번에 학습하고, 각 이미지가 RGB 3채널이며 크기가 224x224라면 shape는 다음과 같습니다. [32, 3, 224, 224] 여기서 의미는 다음과 같습니다. *** 7. dtype 이해하기 "dtype"은 Tensor 안의 데이터 타입입니다. x = torch.tensor([1, 2, 3]) print(x.dtype) 출력 결과는 다음과 같습니다. torch.int64 실수 Tensor를 만들면 다음과 같습니다. x = torch.tensor([1.0, 2.0, 3.0]) print(x.dtype) 출력 결과는 다음과 같습니다. torch.float32 직접 dtype을 지정할 수도 있습니다. x = torch.tensor([1, 2, 3], dtype=torch.float32) print(x) print(x.dtype) 딥러닝에서는 보통 "torch.float32"를 많이 사용합니다. 분류 문제의 정답 라벨은 "torch.long" 타입을 요구하는 경우가 많습니다. 예를 들어 "nn.CrossEntropyLoss()"를 사용할 때 정답 라벨은 클래스 인덱스 형태의 "torch.long" 타입이어야 합니다. labels = torch.tensor([0, 1, 2], dtype=torch.long) *** 8. device 이해하기 "device"는 Tensor가 어디에서 연산되는지를 나타냅니다. x = torch.tensor([1, 2, 3]) print(x.device) 출력 결과는 보통 다음과 같습니다. cpu GPU를 사용할 수 있다면 다음과 같이 device를 지정합니다. device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(device) Tensor를 GPU로 이동할 수 있습니다. x = torch.tensor([1, 2, 3], dtype=torch.float32) x = x.to(device) print(x.device) 모델과 데이터는 같은 device에 있어야 합니다. model = model.to(device) images = images.to(device) labels = labels.to(device) 모델은 GPU에 있고 데이터는 CPU에 있으면 연산 오류가 발생합니다. *** 9. Tensor 연산 Tensor는 사칙연산을 지원합니다. a = torch.tensor([1, 2, 3]) b = torch.tensor([10, 20, 30]) print(a + b) print(a - b) print(a * b) print(b / a) 출력 결과는 다음과 같습니다. tensor([11, 22, 33]) tensor([ -9, -18, -27]) tensor([10, 40, 90]) tensor([10., 10., 10.]) 스칼라 값과도 연산할 수 있습니다. x = torch.tensor([1, 2, 3]) print(x + 10) print(x * 2) *** 10. 브로드캐스팅 브로드캐스팅은 서로 다른 shape의 Tensor끼리 연산할 때 자동으로 크기를 맞춰주는 기능입니다. x = torch.tensor([ [1, 2, 3], [4, 5, 6] ]) y = torch.tensor([10, 20, 30]) print(x + y) 출력 결과는 다음과 같습니다. tensor([[11, 22, 33], [14, 25, 36]]) "y"가 각 행에 자동으로 더해진 것입니다. 브로드캐스팅은 편리하지만 shape를 제대로 이해하지 못하면 의도와 다른 연산이 될 수 있습니다. 따라서 연산 전에 "shape"를 확인하는 습관이 중요합니다. *** 11. 인덱싱과 슬라이싱 Tensor도 리스트나 NumPy 배열처럼 인덱싱과 슬라이싱을 할 수 있습니다. x = torch.tensor([10, 20, 30, 40, 50]) print(x[0]) print(x[2]) print(x[-1]) 2차원 Tensor에서는 행과 열 기준으로 접근합니다. x = torch.tensor([ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]) print(x[0, 0]) print(x[1, 2]) print(x[:, 0]) 출력 결과는 다음과 같습니다. tensor(1) tensor(6) tensor([1, 4, 7]) "x[:, 0]"은 모든 행의 0번째 열을 가져온다는 의미입니다. *** 12. Tensor 모양 바꾸기 딥러닝에서는 Tensor의 shape를 바꿔야 하는 경우가 많습니다. 12.1 reshape x = torch.arange(0, 6) print(x) 출력 결과는 다음과 같습니다. tensor([0, 1, 2, 3, 4, 5]) 2행 3열로 바꿔보겠습니다. y = x.reshape(2, 3) print(y) 출력 결과는 다음과 같습니다. tensor([[0, 1, 2], [3, 4, 5]]) 12.2 view "view()"도 Tensor의 shape를 바꿀 때 사용합니다. x = torch.arange(0, 6) y = x.view(2, 3) print(y) "view()"는 메모리 연속성에 영향을 받을 수 있습니다. 처음 공부할 때는 "reshape()"를 먼저 사용하는 것이 편합니다. 12.3 unsqueeze "unsqueeze()"는 특정 위치에 차원을 하나 추가합니다. x = torch.tensor([1, 2, 3]) print(x.shape) y = x.unsqueeze(0) print(y) print(y.shape) 출력 결과는 다음과 같습니다. torch.Size([3]) tensor([[1, 2, 3]]) torch.Size([1, 3]) 딥러닝에서는 배치 차원을 추가할 때 자주 사용합니다. 예를 들어 이미지 한 장을 모델에 넣으려면 "[C, H, W]"를 "[1, C, H, W]"로 바꿔야 하는 경우가 많습니다. image = image.unsqueeze(0) 12.4 squeeze "squeeze()"는 크기가 1인 차원을 제거합니다. x = torch.zeros(1, 3, 1, 4) print(x.shape) y = x.squeeze() print(y.shape) 출력 예시는 다음과 같습니다. torch.Size([1, 3, 1, 4]) torch.Size([3, 4]) *** 13. Tensor 합치기 Tensor를 합칠 때는 "torch.cat()"과 "torch.stack()"을 자주 사용합니다. 13.1 torch.cat "torch.cat()"은 기존 차원 방향으로 Tensor를 이어 붙입니다. a = torch.tensor([[1, 2], [3, 4]]) b = torch.tensor([[5, 6]]) result = torch.cat([a, b], dim=0) print(result) 출력 결과는 다음과 같습니다. tensor([[1, 2], [3, 4], [5, 6]]) 열 방향으로 붙일 수도 있습니다. a = torch.tensor([[1, 2], [3, 4]]) b = torch.tensor([[10], [20]]) result = torch.cat([a, b], dim=1) print(result) 출력 결과는 다음과 같습니다. tensor([[ 1, 2, 10], [ 3, 4, 20]]) 13.2 torch.stack "torch.stack()"은 새로운 차원을 만들어서 Tensor를 쌓습니다. a = torch.tensor([1, 2, 3]) b = torch.tensor([4, 5, 6]) result = torch.stack([a, b], dim=0) print(result) print(result.shape) 출력 결과는 다음과 같습니다. tensor([[1, 2, 3], [4, 5, 6]]) torch.Size([2, 3]) *** 14. NumPy와 Tensor 변환 PyTorch Tensor와 NumPy 배열은 서로 변환할 수 있습니다. 14.1 NumPy 배열을 Tensor로 변환 import numpy as np import torch np_arr = np.array([1, 2, 3]) tensor = torch.from_numpy(np_arr) print(tensor) 14.2 Tensor를 NumPy 배열로 변환 tensor = torch.tensor([1, 2, 3]) np_arr = tensor.numpy() print(np_arr) GPU에 있는 Tensor는 바로 NumPy로 변환할 수 없습니다. 먼저 CPU로 이동해야 합니다. tensor = tensor.cpu().numpy() 또한 자동 미분을 추적 중인 Tensor는 "detach()" 후 변환하는 경우가 많습니다. np_arr = tensor.detach().cpu().numpy() *** 15. Autograd와 requires_grad PyTorch Tensor의 중요한 기능 중 하나는 자동 미분입니다. 딥러닝 모델은 손실값을 기준으로 가중치를 업데이트하는데, 이때 기울기 계산이 필요합니다. PyTorch에서는 "requires_grad=True"를 설정하면 해당 Tensor에 대한 연산을 추적합니다. x = torch.tensor(2.0, requires_grad=True) y = x ** 2 y.backward() print(x.grad) 출력 결과는 다음과 같습니다. tensor(4.) 수학적으로 보면 다음과 같습니다. y = x² dy/dx = 2x x = 2일 때 dy/dx = 4 PyTorch가 이 미분값을 자동으로 계산한 것입니다. *** 16. backward 이해하기 "backward()"는 계산 그래프를 따라 기울기를 계산합니다. x = torch.tensor(3.0, requires_grad=True) y = x * x + 2 * x + 1 y.backward() print(x.grad) 수식은 다음과 같습니다. y = x² + 2x + 1 dy/dx = 2x + 2 x = 3일 때 dy/dx = 8 출력 결과는 다음과 같습니다. tensor(8.) 딥러닝 학습에서는 이 과정이 모델의 모든 가중치에 대해 자동으로 수행됩니다. *** 17. detach와 no_grad 학습 중에는 기울기 계산이 필요하지만, 예측할 때는 필요하지 않습니다. 17.1 detach "detach()"는 Tensor를 계산 그래프에서 분리합니다. x = torch.tensor(2.0, requires_grad=True) y = x ** 2 z = y.detach() print(z.requires_grad) 출력 결과는 다음과 같습니다. False 17.2 torch.no_grad 모델 예측 시에는 보통 "torch.no_grad()"를 사용합니다. model.eval() with torch.no_grad(): outputs = model(inputs) 이렇게 하면 불필요한 기울기 계산을 하지 않으므로 메모리를 절약할 수 있습니다. *** 18. Tensor와 딥러닝 모델 입력 PyTorch 모델에 들어가는 입력 데이터는 Tensor입니다. 예를 들어 이미지 분류 모델에서는 입력 shape가 보통 다음과 같습니다. [batch_size, channels, height, width] 예시: images = torch.randn(32, 3, 224, 224) print(images.shape) 출력 결과는 다음과 같습니다. torch.Size([32, 3, 224, 224]) 모델 출력은 보통 다음과 같습니다. outputs = torch.randn(32, 10) print(outputs.shape) 출력 결과는 다음과 같습니다. torch.Size([32, 10]) 이 의미는 다음과 같습니다. 32개 이미지에 대해 10개 클래스 점수를 출력 *** 19. CrossEntropyLoss에서 Tensor 형태 분류 문제에서 자주 사용하는 손실 함수는 "nn.CrossEntropyLoss()"입니다. 이 함수는 보통 다음 형태를 기대합니다. outputs shape: [batch_size, num_classes] labels shape : [batch_size] 예시는 다음과 같습니다. import torch import torch.nn as nn outputs = torch.tensor([ [2.0, 0.5, 0.1], [0.2, 1.5, 0.3] ]) labels = torch.tensor([0, 1], dtype=torch.long) criterion = nn.CrossEntropyLoss() loss = criterion(outputs, labels) print(loss) 여기서 "outputs"는 각 클래스에 대한 점수이고, "labels"는 정답 클래스 인덱스입니다. 첫 번째 데이터의 정답: 0번 클래스 두 번째 데이터의 정답: 1번 클래스 주의할 점은 "CrossEntropyLoss"에 넣는 "outputs"는 softmax 결과가 아니라 raw score입니다. PyTorch의 "CrossEntropyLoss" 내부에서 softmax에 해당하는 처리가 함께 이루어집니다. *** 20. Tensor에서 자주 나는 오류 20.1 shape 오류 가장 흔한 오류는 shape가 맞지 않는 경우입니다. RuntimeError: mat1 and mat2 shapes cannot be multiplied 이런 오류가 나면 먼저 shape를 출력해봐야 합니다. print(x.shape) print(outputs.shape) print(labels.shape) 20.2 device 오류 모델은 GPU에 있고 데이터는 CPU에 있으면 오류가 발생합니다. Expected all tensors to be on the same device 이럴 때는 모델과 데이터를 같은 device로 이동해야 합니다. model = model.to(device) inputs = inputs.to(device) labels = labels.to(device) 20.3 dtype 오류 손실 함수가 기대하는 dtype과 다를 때 오류가 발생할 수 있습니다. 분류 라벨은 보통 "torch.long"이어야 합니다. labels = labels.long() 회귀 문제의 입력과 정답은 보통 "torch.float32"를 사용합니다. x = x.float() y = y.float() *** 21. Tensor 기본 실습 예제 아래 코드는 Tensor의 기본 흐름을 한 번에 연습할 수 있는 예제입니다. import torch # 1. Tensor 생성 x = torch.tensor([ [1.0, 2.0], [3.0, 4.0] ]) # 2. 기본 정보 확인 print("x:") print(x) print("shape:", x.shape) print("dtype:", x.dtype) print("device:", x.device) # 3. 연산 y = x * 2 + 1 print("y:") print(y) # 4. 평균 mean_value = y.mean() print("mean:", mean_value) # 5. shape 변경 z = y.reshape(4) print("z:") print(z) print("z shape:", z.shape) # 6. GPU 사용 가능 여부 확인 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") z = z.to(device) print("z device:", z.device) *** 22. Autograd 기본 실습 예제 import torch # requires_grad=True 설정 x = torch.tensor(2.0, requires_grad=True) # 계산식 y = x ** 3 + 2 * x # 역전파 y.backward() print("x:", x) print("y:", y) print("x.grad:", x.grad) 수식으로 보면 다음과 같습니다. y = x³ + 2x dy/dx = 3x² + 2 x = 2일 때 dy/dx = 14 따라서 "x.grad"는 "14"가 됩니다.

June 18, 2026
Seaborn 예제 데이터로 배우는 지도학습 기초카테고리

Seaborn 예제 데이터로 배우는 지도학습 기초

1. 지도학습이란? **지도학습(Supervised Learning)**은 정답이 있는 데이터를 이용해서 모델을 학습시키는 머신러닝 방법입니다. 지도학습 데이터는 보통 입력 데이터와 정답 데이터로 나눌 수 있습니다. X = 입력 데이터, 문제 데이터, feature y = 정답 데이터, target, label 모델은 입력 데이터 "X"를 보고 정답 데이터 "y"를 맞히는 규칙을 학습합니다. 예를 들어 다음과 같은 문제가 지도학습에 해당합니다. 꽃의 길이와 너비를 보고 꽃 품종 예측하기 고객 정보를 보고 이탈 여부 예측하기 메일 내용을 보고 스팸 여부 예측하기 식사 금액과 인원 수를 보고 팁 금액 예측하기 주택 정보를 보고 집값 예측하기 지도학습은 크게 분류와 회귀로 나눌 수 있습니다. 이번 글에서는 Seaborn 예제 데이터셋을 이용해서 지도학습의 두 가지 유형을 모두 실습합니다. iris 데이터셋 → 분류 문제 tips 데이터셋 → 회귀 문제 *** 2. 실습에 사용할 라이브러리 이번 실습에서는 "seaborn", "pandas", "matplotlib", "scikit-learn"을 사용합니다. import seaborn as sns import pandas as pd import matplotlib.pyplot as plt 머신러닝 모델링에는 "scikit-learn"을 사용합니다. from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import accuracy_score, classification_report, confusion_matrix from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score 각 라이브러리의 역할은 다음과 같습니다. 설치가 필요하다면 다음 명령어를 사용할 수 있습니다. pip install seaborn pandas matplotlib scikit-learn *** 3. 지도학습의 기본 흐름 지도학습은 보통 다음 순서로 진행합니다. 1. 데이터 불러오기 2. 데이터 구조 확인하기 3. 입력 데이터 X와 정답 데이터 y 분리하기 4. 학습 데이터와 테스트 데이터로 나누기 5. 모델 선택하기 6. 모델 학습하기 7. 테스트 데이터로 예측하기 8. 모델 성능 평가하기 9. 새로운 데이터 예측하기 분류와 회귀 모두 큰 흐름은 같습니다. 다만 정답 데이터의 형태와 평가 지표가 다릅니다. *** Part 1. Iris 데이터로 분류 모델 만들기 4. Iris 데이터셋이란? "iris" 데이터셋은 붓꽃의 꽃받침과 꽃잎의 길이, 너비 정보를 담고 있는 대표적인 머신러닝 예제 데이터입니다. Seaborn에서 바로 불러올 수 있습니다. import seaborn as sns iris = sns.load_dataset("iris") 데이터를 확인합니다. iris.head() 데이터는 대략 다음과 같은 형태입니다. sepal_length sepal_width petal_length petal_width species 0 5.1 3.5 1.4 0.2 setosa 1 4.9 3.0 1.4 0.2 setosa 2 4.7 3.2 1.3 0.2 setosa 3 4.6 3.1 1.5 0.2 setosa 4 5.0 3.6 1.4 0.2 setosa 컬럼의 의미는 다음과 같습니다. 여기서 "species"가 모델이 맞혀야 하는 정답 데이터입니다. *** 5. 분류 문제 정의하기 이번 분류 문제의 목표는 다음과 같습니다. 꽃받침 길이, 꽃받침 너비, 꽃잎 길이, 꽃잎 너비를 이용해서 붓꽃 품종을 예측하는 모델을 만듭니다. 입력 데이터와 정답 데이터는 다음과 같습니다. X = sepal_length, sepal_width, petal_length, petal_width y = species 정답인 "species"는 다음 세 가지 품종을 가집니다. iris["species"].value_counts() setosa 50 versicolor 50 virginica 50 정답이 숫자가 아니라 품종 이름이므로 이 문제는 분류 문제입니다. *** 6. Iris 데이터 간단히 확인하기 모델을 만들기 전에 데이터를 간단히 확인합니다. iris.info() 기초 통계도 확인합니다. iris.describe() 품종별 데이터 개수를 확인합니다. iris["species"].value_counts() 시각화하면 다음과 같습니다. sns.countplot(data=iris, x="species") plt.title("Iris Species Count") plt.show() 품종별 꽃잎 길이와 꽃잎 너비의 관계도 확인할 수 있습니다. sns.scatterplot( data=iris, x="petal_length", y="petal_width", hue="species" ) plt.title("Petal Length and Petal Width by Species") plt.show() 시각화를 해보면 "petal_length"와 "petal_width"가 품종을 구분하는 데 중요한 변수처럼 보일 수 있습니다. 이처럼 간단한 EDA를 통해 어떤 변수가 정답을 구분하는 데 도움이 될지 감을 잡을 수 있습니다. *** 7. 입력 데이터 X와 정답 데이터 y 분리하기 지도학습에서는 입력 데이터와 정답 데이터를 분리해야 합니다. X = iris.drop(columns=["species"]) y = iris["species"] "X"에는 문제 데이터가 들어갑니다. sepal_length sepal_width petal_length petal_width "y"에는 정답 데이터가 들어갑니다. species 확인해보겠습니다. print(X.head()) print(y.head()) *** 8. 학습 데이터와 테스트 데이터 나누기 모델을 학습할 때 모든 데이터를 학습에만 사용하면 안 됩니다. 모델이 처음 보는 데이터에 대해서도 잘 예측하는지 확인해야 하기 때문입니다. 그래서 데이터를 학습용과 테스트용으로 나눕니다. from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) 각 옵션의 의미는 다음과 같습니다. 분리된 데이터 크기를 확인합니다. print(X_train.shape) print(X_test.shape) print(y_train.shape) print(y_test.shape) *** 9. 분류 모델 만들기 이번에는 "LogisticRegression"을 사용합니다. "LogisticRegression"은 이름에 Regression이 들어가지만, 실제로는 분류 문제에 많이 사용하는 모델입니다. 스케일링을 함께 적용하기 위해 "Pipeline"을 사용합니다. from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression clf_model = Pipeline([ ("scaler", StandardScaler()), ("classifier", LogisticRegression(max_iter=1000)) ]) 위 코드는 두 단계를 하나로 묶은 것입니다. 1. StandardScaler로 데이터 스케일 조정 2. LogisticRegression으로 분류 모델 학습 "StandardScaler"는 각 컬럼의 스케일을 비슷하게 맞춰주는 역할을 합니다. 머신러닝 모델은 변수의 단위나 범위 차이에 영향을 받을 수 있기 때문에 스케일링이 도움이 되는 경우가 많습니다. *** 10. 분류 모델 학습하기 모델 학습은 "fit()" 메서드로 진행합니다. clf_model.fit(X_train, y_train) 이 코드는 모델에게 다음을 학습시키는 과정입니다. X_train을 보고 y_train을 맞히는 규칙을 찾아라. 즉, 꽃받침 길이, 꽃받침 너비, 꽃잎 길이, 꽃잎 너비를 보고 붓꽃 품종을 맞히는 규칙을 학습합니다. *** 11. 테스트 데이터로 예측하기 학습이 끝났으면 테스트 데이터로 예측을 수행합니다. y_pred = clf_model.predict(X_test) "X_test"에는 정답 컬럼이 없습니다. 모델은 입력 데이터만 보고 품종을 예측합니다. 예측 결과 일부를 확인합니다. print(y_pred[:5]) 실제 정답과 비교할 수도 있습니다. result = pd.DataFrame({ "actual": y_test.values, "predicted": y_pred }) print(result.head()) *** 12. 분류 모델 평가하기 분류 모델에서는 대표적으로 다음 지표를 사용합니다. Accuracy Precision Recall F1-score Confusion Matrix 먼저 정확도를 확인합니다. from sklearn.metrics import accuracy_score accuracy = accuracy_score(y_test, y_pred) print("정확도:", accuracy) 정확도는 전체 데이터 중 모델이 맞힌 비율입니다. 정확도 = 맞힌 개수 / 전체 개수 더 자세한 분류 평가 결과는 "classification_report()"로 확인할 수 있습니다. from sklearn.metrics import classification_report print(classification_report(y_test, y_pred)) 출력 결과에는 품종별 Precision, Recall, F1-score가 표시됩니다. *** 13. 혼동 행렬 확인하기 혼동 행렬은 모델이 어떤 클래스를 어떤 클래스로 예측했는지 보여주는 표입니다. from sklearn.metrics import confusion_matrix cm = confusion_matrix(y_test, y_pred, labels=clf_model.classes_) cm_df = pd.DataFrame( cm, index=clf_model.classes_, columns=clf_model.classes_ ) print(cm_df) 시각화하면 더 보기 쉽습니다. sns.heatmap(cm_df, annot=True, fmt="d", cmap="Blues") plt.title("Confusion Matrix") plt.xlabel("Predicted") plt.ylabel("Actual") plt.show() 혼동 행렬에서 대각선 값은 모델이 맞힌 개수입니다. 대각선 밖의 값은 모델이 잘못 예측한 개수입니다. *** 14. 새로운 Iris 데이터 예측하기 학습된 모델을 이용하면 새로운 데이터도 예측할 수 있습니다. new_flower = pd.DataFrame({ "sepal_length": [5.1], "sepal_width": [3.5], "petal_length": [1.4], "petal_width": [0.2] }) prediction = clf_model.predict(new_flower) print("예측 품종:", prediction[0]) 이렇게 하면 새로운 꽃의 측정값을 이용해서 품종을 예측할 수 있습니다. *** 15. Iris 분류 전체 코드 위 내용을 하나로 합친 전체 코드는 다음과 같습니다. import seaborn as sns import pandas as pd import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, classification_report, confusion_matrix # 1. 데이터 불러오기 iris = sns.load_dataset("iris") # 2. 입력 데이터와 정답 데이터 분리 X = iris.drop(columns=["species"]) y = iris["species"] # 3. 학습 데이터와 테스트 데이터 분리 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 4. 모델 생성 clf_model = Pipeline([ ("scaler", StandardScaler()), ("classifier", LogisticRegression(max_iter=1000)) ]) # 5. 모델 학습 clf_model.fit(X_train, y_train) # 6. 예측 y_pred = clf_model.predict(X_test) # 7. 평가 accuracy = accuracy_score(y_test, y_pred) print("정확도:", accuracy) print() print(classification_report(y_test, y_pred)) # 8. 혼동 행렬 cm = confusion_matrix(y_test, y_pred, labels=clf_model.classes_) cm_df = pd.DataFrame( cm, index=clf_model.classes_, columns=clf_model.classes_ ) sns.heatmap(cm_df, annot=True, fmt="d", cmap="Blues") plt.title("Confusion Matrix") plt.xlabel("Predicted") plt.ylabel("Actual") plt.show() # 9. 새로운 데이터 예측 new_flower = pd.DataFrame({ "sepal_length": [5.1], "sepal_width": [3.5], "petal_length": [1.4], "petal_width": [0.2] }) prediction = clf_model.predict(new_flower) print("예측 품종:", prediction[0]) *** Part 2. Tips 데이터로 회귀 모델 만들기 16. Tips 데이터셋이란? 이번에는 Seaborn의 "tips" 데이터셋을 이용해서 회귀 문제를 풀어보겠습니다. tips = sns.load_dataset("tips") 데이터를 확인합니다. tips.head() "tips" 데이터셋은 식당에서 발생한 식사 금액과 팁 정보를 담고 있습니다. 컬럼의 의미는 다음과 같습니다. 이번 회귀 문제의 목표는 다음과 같습니다. 식사 금액, 성별, 흡연 여부, 요일, 시간대, 인원 수를 이용해서 팁 금액을 예측합니다. 정답인 "tip"은 숫자 데이터입니다. 따라서 이 문제는 회귀 문제입니다. *** 17. Tips 데이터 간단히 확인하기 먼저 데이터 구조를 확인합니다. tips.info() 기초 통계를 확인합니다. tips.describe() 팁 금액의 분포를 확인합니다. sns.histplot(data=tips, x="tip", bins=20, kde=True) plt.title("Tip Distribution") plt.show() 전체 식사 금액과 팁 금액의 관계를 확인합니다. sns.scatterplot(data=tips, x="total_bill", y="tip") plt.title("Total Bill and Tip") plt.show() 일반적으로 식사 금액이 커질수록 팁 금액도 증가하는 경향이 있을 수 있습니다. 이처럼 회귀 문제에서는 입력 변수와 정답 숫자 사이의 관계를 확인하는 것이 중요합니다. *** 18. 입력 데이터 X와 정답 데이터 y 분리하기 "tip"은 예측해야 하는 정답이므로 "y"에 넣습니다. 나머지 필요한 컬럼은 "X"에 넣습니다. X = tips[["total_bill", "sex", "smoker", "day", "time", "size"]] y = tips["tip"] 여기서 다음 컬럼은 범주형 변수입니다. sex smoker day time 머신러닝 모델은 문자열을 그대로 처리하지 못하는 경우가 많기 때문에 숫자로 변환해야 합니다. 이 작업을 인코딩이라고 합니다. *** 19. 범주형 데이터 인코딩 준비하기 이번에는 "ColumnTransformer"와 "OneHotEncoder"를 사용해서 범주형 변수를 자동으로 인코딩하겠습니다. 먼저 숫자형 컬럼과 범주형 컬럼을 나눕니다. numeric_features = ["total_bill", "size"] categorical_features = ["sex", "smoker", "day", "time"] 전처리 객체를 만듭니다. from sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder preprocessor = ColumnTransformer( transformers=[ ("num", "passthrough", numeric_features), ("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features) ] ) 여기서 "OneHotEncoder(handle_unknown="ignore")"는 학습 때 보지 못한 새로운 카테고리가 들어와도 오류가 나지 않도록 해줍니다. *** 20. 학습 데이터와 테스트 데이터 나누기 회귀 문제에서도 학습 데이터와 테스트 데이터를 분리합니다. from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) *** 21. 회귀 모델 만들기 이번에는 "RandomForestRegressor"를 사용하겠습니다. from sklearn.ensemble import RandomForestRegressor from sklearn.pipeline import Pipeline reg_model = Pipeline([ ("preprocessor", preprocessor), ("regressor", RandomForestRegressor( n_estimators=100, random_state=42 )) ]) "RandomForestRegressor"는 여러 개의 결정 트리를 이용해서 예측하는 회귀 모델입니다. 복잡한 패턴도 어느 정도 잘 학습할 수 있어서 입문용 실습에도 많이 사용됩니다. *** 22. 회귀 모델 학습하기 모델을 학습합니다. reg_model.fit(X_train, y_train) 모델은 입력 데이터와 팁 금액 사이의 관계를 학습합니다. total_bill, size, sex, smoker, day, time → tip *** 23. 회귀 모델 예측하기 테스트 데이터로 팁 금액을 예측합니다. y_pred = reg_model.predict(X_test) 실제값과 예측값을 비교해보겠습니다. result = pd.DataFrame({ "actual": y_test.values, "predicted": y_pred }) print(result.head()) *** 24. 회귀 모델 평가하기 회귀 모델에서는 분류 모델의 정확도 대신 오차 기반 지표를 사용합니다. 대표적인 평가 지표는 다음과 같습니다. 코드는 다음과 같습니다. from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score mae = mean_absolute_error(y_test, y_pred) mse = mean_squared_error(y_test, y_pred) rmse = mse ** 0.5 r2 = r2_score(y_test, y_pred) print("MAE:", mae) print("MSE:", mse) print("RMSE:", rmse) print("R2:", r2) 각 지표는 다음과 같이 해석할 수 있습니다. MAE가 작을수록 예측 오차가 작습니다. RMSE가 작을수록 예측 오차가 작습니다. R2는 1에 가까울수록 모델 설명력이 좋습니다. 다만 R2가 항상 높다고 무조건 좋은 모델은 아닙니다. 데이터의 특성과 문제 목적에 따라 여러 지표를 함께 봐야 합니다. *** 25. 실제값과 예측값 시각화하기 회귀 모델에서는 실제값과 예측값이 얼마나 비슷한지 산점도로 확인할 수 있습니다. result = pd.DataFrame({ "actual": y_test.values, "predicted": y_pred }) sns.scatterplot(data=result, x="actual", y="predicted") plt.title("Actual vs Predicted Tip") plt.xlabel("Actual Tip") plt.ylabel("Predicted Tip") plt.show() 점들이 대각선에 가까울수록 예측이 잘 된 것입니다. *** 26. 새로운 Tips 데이터 예측하기 학습된 회귀 모델로 새로운 데이터의 팁 금액을 예측할 수 있습니다. new_data = pd.DataFrame({ "total_bill": [30.0], "sex": ["Male"], "smoker": ["No"], "day": ["Sun"], "time": ["Dinner"], "size": [2] }) predicted_tip = reg_model.predict(new_data) print("예측 팁 금액:", predicted_tip[0]) "Pipeline" 안에 전처리 단계가 포함되어 있기 때문에 새로운 데이터도 자동으로 같은 방식으로 인코딩됩니다. 이 방식은 실무에서도 자주 사용하는 구조입니다. *** 27. Tips 회귀 전체 코드 위 내용을 하나로 합친 전체 코드는 다음과 같습니다. import seaborn as sns import pandas as pd import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split from sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder from sklearn.pipeline import Pipeline from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score # 1. 데이터 불러오기 tips = sns.load_dataset("tips") # 2. 입력 데이터와 정답 데이터 분리 X = tips[["total_bill", "sex", "smoker", "day", "time", "size"]] y = tips["tip"] # 3. 숫자형 / 범주형 컬럼 구분 numeric_features = ["total_bill", "size"] categorical_features = ["sex", "smoker", "day", "time"] # 4. 전처리 설정 preprocessor = ColumnTransformer( transformers=[ ("num", "passthrough", numeric_features), ("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features) ] ) # 5. 학습 데이터와 테스트 데이터 분리 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) # 6. 모델 생성 reg_model = Pipeline([ ("preprocessor", preprocessor), ("regressor", RandomForestRegressor( n_estimators=100, random_state=42 )) ]) # 7. 모델 학습 reg_model.fit(X_train, y_train) # 8. 예측 y_pred = reg_model.predict(X_test) # 9. 평가 mae = mean_absolute_error(y_test, y_pred) mse = mean_squared_error(y_test, y_pred) rmse = mse ** 0.5 r2 = r2_score(y_test, y_pred) print("MAE:", mae) print("MSE:", mse) print("RMSE:", rmse) print("R2:", r2) # 10. 실제값과 예측값 비교 result = pd.DataFrame({ "actual": y_test.values, "predicted": y_pred }) sns.scatterplot(data=result, x="actual", y="predicted") plt.title("Actual vs Predicted Tip") plt.xlabel("Actual Tip") plt.ylabel("Predicted Tip") plt.show() # 11. 새로운 데이터 예측 new_data = pd.DataFrame({ "total_bill": [30.0], "sex": ["Male"], "smoker": ["No"], "day": ["Sun"], "time": ["Dinner"], "size": [2] }) predicted_tip = reg_model.predict(new_data) print("예측 팁 금액:", predicted_tip[0]) *** Part 3. 분류와 회귀 비교하기 28. 분류와 회귀의 차이 지도학습은 정답 데이터의 형태에 따라 분류와 회귀로 나뉩니다. 이번 글에서 다룬 예제를 정리하면 다음과 같습니다.

June 16, 2026
EDA 방법 정리: 탐색적 데이터 분석으로 데이터 이해하기카테고리

EDA 방법 정리: 탐색적 데이터 분석으로 데이터 이해하기

1. EDA란? EDA는 "Exploratory Data Analysis"의 줄임말이며, 우리말로는 탐색적 데이터 분석이라고 합니다. EDA는 본격적으로 모델링을 하기 전에 데이터를 다양한 관점에서 살펴보는 과정입니다. 즉, 데이터를 바로 모델에 넣기 전에 데이터의 구조, 분포, 결측치, 이상치, 변수 간 관계 등을 확인하는 작업입니다. 데이터 분석이나 머신러닝에서는 모델을 만드는 것만큼 데이터 이해가 중요합니다. 데이터를 제대로 이해하지 못한 상태에서 모델을 만들면 성능이 낮거나, 잘못된 결론을 내릴 수 있습니다. EDA의 핵심은 다음과 같습니다. 데이터가 어떻게 생겼는지 확인합니다. 결측치와 이상치를 찾습니다. 숫자형 변수와 범주형 변수를 구분합니다. 변수의 분포를 확인합니다. 변수 간 관계를 확인합니다. 모델링에 필요한 전처리 방향을 정합니다. *** 2. EDA를 해야 하는 이유 실제 데이터는 대부분 깔끔하지 않습니다. CSV나 데이터베이스에서 가져온 데이터에는 다양한 문제가 숨어 있을 수 있습니다. 예를 들면 다음과 같습니다. 값이 비어 있는 결측치가 있을 수 있습니다. 같은 데이터가 중복되어 있을 수 있습니다. 숫자 데이터가 문자열로 저장되어 있을 수 있습니다. 날짜 형식이 제각각일 수 있습니다. 이상하게 큰 값이나 작은 값이 있을 수 있습니다. 카테고리 값이 잘못 입력되어 있을 수 있습니다. EDA를 하면 이런 문제를 미리 찾을 수 있습니다. 또한 EDA를 통해 다음과 같은 판단을 할 수 있습니다. 어떤 컬럼을 사용할지 판단할 수 있습니다. 어떤 컬럼을 제거할지 판단할 수 있습니다. 결측치를 어떻게 처리할지 결정할 수 있습니다. 이상치를 제거할지 유지할지 결정할 수 있습니다. 변수 변환이나 파생 변수가 필요한지 판단할 수 있습니다. 어떤 모델이 적합할지 감을 잡을 수 있습니다. 따라서 EDA는 데이터 분석과 머신러닝의 출발점이라고 볼 수 있습니다. *** 3. EDA 전체 흐름 EDA는 정해진 하나의 정답이 있는 과정은 아닙니다. 하지만 일반적으로 다음 순서로 진행하면 좋습니다. 1. 데이터 불러오기 2. 데이터 크기와 구조 확인 3. 컬럼명과 데이터 타입 확인 4. 결측치 확인 5. 중복 데이터 확인 6. 숫자형 변수와 범주형 변수 구분 7. 기초 통계 확인 8. 단변량 분석 9. 이상치 확인 10. 이변량 분석 11. 상관관계 분석 12. 타깃 변수와의 관계 분석 13. 파생 변수 생성 14. 분석 결과 정리 15. 전처리 방향 결정 처음에는 이 순서를 체크리스트처럼 따라가면 됩니다. *** 4. 실습에 사용할 라이브러리 EDA에서는 보통 "pandas", "numpy", "matplotlib", "seaborn"을 함께 사용합니다. import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns 각 라이브러리의 역할은 다음과 같습니다. 한글 그래프를 그릴 경우에는 폰트 설정이 필요할 수 있습니다. import matplotlib.pyplot as plt plt.rcParams["font.family"] = "Malgun Gothic" plt.rcParams["axes.unicode_minus"] = False 윈도우 환경에서는 "Malgun Gothic"을 많이 사용합니다. Mac이나 Linux 환경에서는 사용하는 한글 폰트 이름에 맞게 변경해야 합니다. *** 5. 예제 데이터 불러오기 이번 글에서는 Seaborn에서 제공하는 "titanic" 예제 데이터셋을 사용하겠습니다. import seaborn as sns df = sns.load_dataset("titanic") 데이터가 잘 불러와졌는지 확인합니다. df.head() "titanic" 데이터셋은 타이타닉 탑승객 정보를 담고 있는 데이터입니다. 생존 여부, 좌석 등급, 성별, 나이, 요금 등의 컬럼을 포함합니다. 대표적인 컬럼은 다음과 같습니다. *** 6. 데이터 크기 확인하기 먼저 데이터가 몇 행, 몇 열인지 확인합니다. df.shape 결과 예시는 다음과 같습니다. (891, 15) 이 결과는 "891행 15열"이라는 뜻입니다. EDA를 시작할 때 데이터 크기를 먼저 확인하는 이유는 데이터의 전체 규모를 파악하기 위해서입니다. print("행 개수:", df.shape[0]) print("열 개수:", df.shape[1]) *** 7. 데이터 미리보기 상위 데이터를 확인합니다. df.head() 하위 데이터를 확인합니다. df.tail() 무작위 데이터를 확인할 수도 있습니다. df.sample(5) "head()"만 보면 앞쪽 데이터만 보이기 때문에 전체 데이터의 느낌을 놓칠 수 있습니다. 그래서 "sample()"을 함께 사용하면 좋습니다. *** 8. 컬럼명 확인하기 데이터에 어떤 컬럼이 있는지 확인합니다. df.columns 컬럼명을 리스트로 보고 싶다면 다음과 같이 작성합니다. list(df.columns) 컬럼명이 너무 길거나 의미가 불명확하면 분석하기 어렵습니다. 필요하다면 컬럼명을 변경할 수 있습니다. df = df.rename(columns={ "survived": "is_survived", "pclass": "passenger_class" }) 다만 예제에서는 원래 컬럼명을 그대로 사용하겠습니다. *** 9. 데이터 타입 확인하기 각 컬럼의 데이터 타입을 확인합니다. df.dtypes 또는 전체 정보를 한 번에 확인할 수 있습니다. df.info() "info()"를 보면 다음 내용을 확인할 수 있습니다. 전체 행 개수 컬럼 개수 컬럼별 결측치 여부 컬럼별 데이터 타입 메모리 사용량 데이터 타입은 EDA에서 매우 중요합니다. 예를 들어 숫자처럼 보이는 컬럼이 문자열 타입이면 평균이나 분산을 바로 계산할 수 없습니다. 날짜 컬럼이 문자열 타입이면 날짜 계산을 하려면 "datetime" 타입으로 변환해야 합니다. *** 10. 숫자형 변수와 범주형 변수 구분하기 EDA에서는 숫자형 변수와 범주형 변수를 구분해서 분석하는 것이 좋습니다. 숫자형 변수는 평균, 분산, 표준편차 같은 통계량을 확인할 수 있습니다. 범주형 변수는 값의 종류와 개수를 확인하는 것이 중요합니다. numeric_cols = df.select_dtypes(include=["int64", "float64"]).columns category_cols = df.select_dtypes(include=["object", "category", "bool"]).columns print("숫자형 컬럼:") print(numeric_cols) print("범주형 컬럼:") print(category_cols) 숫자형 변수 예시는 다음과 같습니다. survived pclass age sibsp parch fare 범주형 변수 예시는 다음과 같습니다. sex embarked class who adult_male deck embark_town alive alone *** 11. 기초 통계 확인하기 숫자형 변수의 기초 통계를 확인합니다. df.describe() "describe()"는 숫자형 컬럼에 대해 다음 값을 보여줍니다. 범주형 변수의 기초 통계도 확인할 수 있습니다. df.describe(include="object") 범주형 변수에서는 다음 값을 확인할 수 있습니다. 전체 타입에 대해 확인하려면 다음과 같이 작성합니다. df.describe(include="all") *** 12. 결측치 확인하기 결측치는 값이 비어 있는 데이터를 의미합니다. pandas에서는 보통 "NaN"으로 표시됩니다. 컬럼별 결측치 개수를 확인합니다. df.isnull().sum() 결측치 비율을 확인하면 더 좋습니다. missing_ratio = df.isnull().mean() * 100 missing_ratio.sort_values(ascending=False) 결측치가 있는 컬럼만 보고 싶다면 다음과 같이 작성합니다. missing = df.isnull().sum() missing[missing > 0] 결측치를 시각화할 수도 있습니다. plt.figure(figsize=(10, 5)) sns.heatmap(df.isnull(), cbar=False) plt.title("Missing Values") plt.show() 결측치 처리 방법은 데이터의 성격에 따라 달라집니다. 결측치가 적으면 행을 제거할 수 있습니다. 숫자형 변수는 평균이나 중앙값으로 채울 수 있습니다. 범주형 변수는 최빈값이나 'Unknown'으로 채울 수 있습니다. 결측 자체가 의미 있을 수 있으므로 별도 컬럼으로 표시할 수도 있습니다. *** 13. 중복 데이터 확인하기 중복 데이터가 있는지 확인합니다. df.duplicated().sum() 중복 데이터가 있다면 다음과 같이 확인할 수 있습니다. df[df.duplicated()] 중복을 제거하려면 다음과 같이 작성합니다. df = df.drop_duplicates() 다만 무조건 제거하면 안 됩니다. 도메인에 따라 같은 값이 여러 번 등장하는 것이 정상일 수도 있기 때문입니다. *** 14. 단변량 분석이란? 단변량 분석은 변수 하나를 기준으로 데이터를 분석하는 방법입니다. 즉, 각 컬럼이 어떤 분포를 가지고 있는지 확인하는 과정입니다. 단변량 분석은 크게 두 가지로 나눌 수 있습니다. 숫자형 변수 분석 범주형 변수 분석 숫자형 변수는 평균, 중앙값, 표준편차, 분포, 이상치를 확인합니다. 범주형 변수는 값의 종류와 빈도를 확인합니다. *** 15. 숫자형 변수 분포 확인하기 숫자형 변수의 분포를 확인할 때는 히스토그램을 많이 사용합니다. sns.histplot(data=df, x="age", bins=30, kde=True) plt.title("Age Distribution") plt.show() 위 코드는 나이 분포를 보여줍니다. "kde=True"를 설정하면 분포를 부드러운 곡선으로 함께 볼 수 있습니다. 요금 분포도 확인할 수 있습니다. sns.histplot(data=df, x="fare", bins=30, kde=True) plt.title("Fare Distribution") plt.show() 분포를 확인할 때는 다음 내용을 봅니다. 값이 한쪽으로 치우쳐 있는지 확인합니다. 정규분포와 비슷한지 확인합니다. 특정 구간에 값이 몰려 있는지 확인합니다. 이상하게 큰 값이나 작은 값이 있는지 확인합니다. *** 16. 범주형 변수 분포 확인하기 범주형 변수는 값별 개수를 확인합니다. df["sex"].value_counts() 시각화는 "countplot()"을 사용할 수 있습니다. sns.countplot(data=df, x="sex") plt.title("Count by Sex") plt.show() 객실 등급별 개수도 확인할 수 있습니다. sns.countplot(data=df, x="class") plt.title("Count by Class") plt.show() 범주형 변수에서는 다음 내용을 확인합니다. 어떤 값이 있는지 확인합니다. 각 값이 몇 개씩 있는지 확인합니다. 특정 값에 데이터가 지나치게 몰려 있는지 확인합니다. 오타나 잘못된 카테고리가 있는지 확인합니다. *** 17. 이상치 확인하기 이상치는 일반적인 범위에서 크게 벗어난 값을 의미합니다. 이상치를 확인할 때는 박스플롯을 많이 사용합니다. sns.boxplot(data=df, x="age") plt.title("Age Boxplot") plt.show() 요금 컬럼도 확인해보겠습니다. sns.boxplot(data=df, x="fare") plt.title("Fare Boxplot") plt.show() 박스플롯에서 상자 밖의 점들은 이상치 후보입니다. 이상치를 수치적으로 확인할 때는 IQR 방식을 사용할 수 있습니다. Q1 = df["fare"].quantile(0.25) Q3 = df["fare"].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR outliers = df[(df["fare"] < lower_bound) | (df["fare"] > upper_bound)] print("이상치 개수:", len(outliers)) print("하한값:", lower_bound) print("상한값:", upper_bound) 다만 이상치를 무조건 제거하면 안 됩니다. 실제로 중요한 의미를 가진 값일 수 있기 때문입니다. 예를 들어 요금이 매우 높은 승객은 1등석 승객일 수 있습니다. 이런 값은 오류가 아니라 중요한 정보일 수 있습니다. *** 18. 이변량 분석이란? 이변량 분석은 두 변수 사이의 관계를 확인하는 과정입니다. 분석할 수 있는 조합은 다음과 같습니다. 숫자형 변수 vs 숫자형 변수 범주형 변수 vs 숫자형 변수 범주형 변수 vs 범주형 변수 각 조합마다 사용하는 방법이 조금씩 다릅니다. *** 19. 숫자형 변수와 숫자형 변수 관계 숫자형 변수끼리의 관계는 산점도를 사용해서 확인할 수 있습니다. sns.scatterplot(data=df, x="age", y="fare") plt.title("Age and Fare") plt.show() 산점도를 보면 두 변수 사이에 어떤 패턴이 있는지 확인할 수 있습니다. x가 증가할 때 y도 증가하는지 확인합니다. x가 증가할 때 y가 감소하는지 확인합니다. 특정 구간에 데이터가 몰려 있는지 확인합니다. 이상치가 있는지 확인합니다. *** 20. 범주형 변수와 숫자형 변수 관계 범주형 변수에 따라 숫자형 값이 어떻게 달라지는지 확인할 때는 박스플롯이나 막대 그래프를 사용할 수 있습니다. 성별에 따른 요금 분포를 확인해보겠습니다. sns.boxplot(data=df, x="sex", y="fare") plt.title("Fare by Sex") plt.show() 객실 등급에 따른 요금 분포도 확인할 수 있습니다. sns.boxplot(data=df, x="class", y="fare") plt.title("Fare by Class") plt.show() 평균값을 비교하고 싶다면 "barplot()"을 사용할 수 있습니다. sns.barplot(data=df, x="class", y="fare") plt.title("Average Fare by Class") plt.show() 주의할 점은 Seaborn의 "barplot()"은 기본적으로 합계가 아니라 평균을 보여준다는 것입니다. *** 21. 범주형 변수와 범주형 변수 관계 범주형 변수끼리의 관계는 교차표를 사용해서 확인할 수 있습니다. pd.crosstab(df["sex"], df["survived"]) 비율로 확인하려면 다음과 같이 작성합니다. pd.crosstab(df["sex"], df["survived"], normalize="index") 시각화는 "countplot()"에 "hue"를 사용할 수 있습니다. sns.countplot(data=df, x="sex", hue="survived") plt.title("Survival Count by Sex") plt.show() 객실 등급별 생존 여부도 확인할 수 있습니다. sns.countplot(data=df, x="class", hue="survived") plt.title("Survival Count by Class") plt.show() 이런 분석을 통해 어떤 범주가 타깃 변수와 관련이 있는지 확인할 수 있습니다. *** 22. 상관관계 분석하기 상관관계는 두 숫자형 변수 사이의 관계를 수치로 나타낸 것입니다. 상관계수는 보통 -1에서 1 사이의 값을 가집니다. 1에 가까움 : 강한 양의 상관관계 0에 가까움 : 상관관계가 약함 -1에 가까움 : 강한 음의 상관관계 숫자형 컬럼만 선택해서 상관관계를 계산합니다. corr = df.corr(numeric_only=True) corr 히트맵으로 시각화하면 더 쉽게 볼 수 있습니다. plt.figure(figsize=(8, 6)) sns.heatmap(corr, annot=True, cmap="coolwarm", fmt=".2f") plt.title("Correlation Heatmap") plt.show() 상관관계를 볼 때는 다음을 주의해야 합니다. 상관관계가 높다고 해서 반드시 인과관계가 있는 것은 아닙니다. 상관관계는 주로 선형 관계를 보여줍니다. 이상치가 상관계수에 큰 영향을 줄 수 있습니다. 범주형 변수는 별도 인코딩 없이는 상관계산에 포함되지 않습니다. *** 23. 타깃 변수 분석하기 머신러닝을 위한 EDA라면 타깃 변수를 반드시 분석해야 합니다. "titanic" 데이터에서는 "survived"가 타깃 변수입니다. 먼저 생존 여부 비율을 확인합니다. df["survived"].value_counts() 비율로 확인합니다. df["survived"].value_counts(normalize=True) 시각화합니다. sns.countplot(data=df, x="survived") plt.title("Survival Count") plt.show() 타깃 변수의 분포를 확인하는 이유는 데이터 불균형 여부를 보기 위해서입니다. 예를 들어 한쪽 클래스가 너무 많고 다른 클래스가 너무 적으면 모델이 다수 클래스만 잘 맞히는 문제가 생길 수 있습니다. *** 24. 타깃 변수와 입력 변수 관계 확인하기 성별에 따른 생존율을 확인해보겠습니다. df.groupby("sex")["survived"].mean() 시각화는 다음과 같이 할 수 있습니다. sns.barplot(data=df, x="sex", y="survived") plt.title("Survival Rate by Sex") plt.show() 객실 등급별 생존율도 확인합니다. df.groupby("class")["survived"].mean() sns.barplot(data=df, x="class", y="survived") plt.title("Survival Rate by Class") plt.show() 나이와 생존 여부의 관계는 박스플롯으로 볼 수 있습니다. sns.boxplot(data=df, x="survived", y="age") plt.title("Age by Survival") plt.show() 이런 분석을 통해 어떤 변수가 예측에 중요할 가능성이 있는지 감을 잡을 수 있습니다. *** 25. 파생 변수 만들기 EDA를 하다 보면 기존 컬럼을 조합해서 새로운 변수를 만들 수 있습니다. 이런 변수를 파생 변수라고 합니다. 예를 들어 함께 탑승한 가족 수를 만들 수 있습니다. df["family_size"] = df["sibsp"] + df["parch"] + 1 가족 수와 생존율의 관계를 확인합니다. df.groupby("family_size")["survived"].mean() 시각화합니다. sns.barplot(data=df, x="family_size", y="survived") plt.title("Survival Rate by Family Size") plt.show() 나이를 구간으로 나누는 파생 변수도 만들 수 있습니다. df["age_group"] = pd.cut( df["age"], bins=[0, 12, 18, 35, 60, 100], labels=["Child", "Teen", "Young Adult", "Adult", "Senior"] ) 나이 그룹별 생존율을 확인합니다. sns.barplot(data=df, x="age_group", y="survived") plt.title("Survival Rate by Age Group") plt.xticks(rotation=30) plt.show() 파생 변수는 모델 성능을 높이는 데 도움이 될 수 있습니다. 다만 너무 많은 파생 변수를 만들면 오히려 모델이 복잡해질 수 있으므로 주의해야 합니다. *** 26. EDA 결과 정리하기 EDA는 그래프를 많이 그리는 것에서 끝나면 안 됩니다. 마지막에는 분석 결과를 정리해야 합니다. 예시는 다음과 같습니다. age 컬럼에는 결측치가 존재합니다. deck 컬럼은 결측치가 많아 제거를 고려할 수 있습니다. fare 컬럼은 오른쪽으로 치우친 분포를 보입니다. fare 컬럼에는 매우 큰 값이 존재하지만 1등석 요금일 가능성이 있습니다. 성별에 따라 생존율 차이가 큽니다. 객실 등급이 높을수록 생존율이 높게 나타납니다. family_size 파생 변수가 생존율과 관련이 있을 가능성이 있습니다. 이렇게 정리하면 다음 단계인 전처리와 모델링 방향을 잡기 쉬워집니다. *** 27. EDA 후 전처리 방향 정하기 EDA 결과를 바탕으로 전처리 계획을 세웁니다. 예시는 다음과 같습니다. age 결측치는 중앙값으로 대체합니다. embarked 결측치는 최빈값으로 대체합니다. deck 컬럼은 결측치가 너무 많으므로 제거를 고려합니다. fare는 분포가 치우쳐 있으므로 로그 변환을 고려합니다. sex, embarked, class 같은 범주형 변수는 인코딩합니다. family_size, age_group 같은 파생 변수를 추가합니다. 중복 데이터가 있다면 제거 여부를 검토합니다. EDA는 단순한 시각화 작업이 아니라 전처리와 모델링 전략을 세우기 위한 과정입니다. *** 28. EDA 체크리스트 EDA를 할 때 아래 체크리스트를 참고하면 좋습니다. 데이터 크기를 확인했는가? 컬럼명을 확인했는가? 데이터 타입을 확인했는가? 숫자형 변수와 범주형 변수를 구분했는가? 결측치 개수와 비율을 확인했는가? 중복 데이터를 확인했는가? 기초 통계량을 확인했는가? 숫자형 변수의 분포를 확인했는가? 범주형 변수의 빈도를 확인했는가? 이상치를 확인했는가? 변수 간 관계를 확인했는가? 상관관계를 확인했는가? 타깃 변수의 분포를 확인했는가? 타깃 변수와 입력 변수의 관계를 확인했는가? 필요한 파생 변수를 검토했는가? 전처리 방향을 정리했는가? *** 29. EDA에서 자주 사용하는 pandas 함수 *** 30. EDA에서 자주 사용하는 시각화 함수 *** 31. EDA 기본 템플릿 코드 아래 코드는 EDA를 시작할 때 기본 템플릿으로 사용할 수 있습니다. import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns # 데이터 불러오기 df = sns.load_dataset("titanic") # 데이터 기본 확인 print(df.shape) print(df.head()) print(df.info()) print(df.describe()) # 결측치 확인 missing = df.isnull().sum() print(missing[missing > 0]) # 중복 확인 print("중복 개수:", df.duplicated().sum()) # 숫자형 / 범주형 컬럼 구분 numeric_cols = df.select_dtypes(include=["int64", "float64"]).columns category_cols = df.select_dtypes(include=["object", "category", "bool"]).columns print("숫자형 컬럼:", numeric_cols) print("범주형 컬럼:", category_cols) # 숫자형 변수 분포 확인 for col in numeric_cols: plt.figure(figsize=(6, 4)) sns.histplot(data=df, x=col, kde=True) plt.title(f"{col} Distribution") plt.show() # 범주형 변수 분포 확인 for col in category_cols: plt.figure(figsize=(6, 4)) sns.countplot(data=df, x=col) plt.title(f"{col} Count") plt.xticks(rotation=30) plt.show() # 상관관계 확인 corr = df.corr(numeric_only=True) plt.figure(figsize=(8, 6)) sns.heatmap(corr, annot=True, cmap="coolwarm", fmt=".2f") plt.title("Correlation Heatmap") plt.show() 이 템플릿을 바탕으로 데이터에 맞게 분석을 확장하면 됩니다.

June 16, 2026