Copy & Run

Trivy로 컨테이너 이미지 취약점을 스캔하고 빌드 게이트 만들기

오래된 베이스 이미지와 고정 버전 패키지로 작은 이미지를 빌드해 Trivy로 스캔하고, 결과 표 읽는 법과 HIGH,CRITICAL 필터, --ignore-unfixed, --exit-code 1 게이트, 만료일이 있는 .trivyignore 예외를 거쳐 베이스 이미지를 올려 통과시킵니다.

실습 노트 · 2026-10-16 발행 · 12분 · 2026-10 확인

오래된 베이스 이미지와 오래된 파이썬 패키지로 작은 이미지를 빌드하고 Trivy로 스캔합니다. 결과 표에서 심각도, 설치 버전, 수정 버전을 읽고, 심각도 필터와 종료 코드로 빌드를 멈추는 게이트를 만든 다음, 예외 처리와 베이스 이미지 교체로 게이트를 통과시킵니다.

이 글은 이미지 스캔과 게이트만 다룹니다. requirements.txt 같은 의존성 목록의 스캔과 SBOM은 취약한 의존성 스캔하고 CycloneDX SBOM 만들기에서 다룹니다. 이 글의 취약한 이미지는 스캔 연습용이므로 실제 서비스에 배포하지 마세요.

준비물

항목 버전 용도
Docker 29.5.3 (Docker Desktop) 이미지 빌드, Trivy 실행
Trivy 0.76.0 (Docker 이미지 aquasec/trivy:0.76.0, 다이제스트로 고정) 이미지 취약점 스캔
대상 베이스 이미지 python:3.9-slim (Debian 13.1) 일부러 오래된 쪽
교체할 베이스 이미지 python:3.13-slim (Debian 13.7) 게이트 통과용

명령은 Git Bash와 리눅스에서 확인했고, macOS는 확인하지 않았습니다. 예상 소요 시간은 25분 안팎이고, 처음 한 번은 Trivy가 취약점 데이터베이스(약 130MB)를 내려받습니다. Trivy는 0.76.0 버전의 이미지를 쓰되 다이제스트(@sha256:...)까지 함께 지정합니다. 2026년 3월에 Trivy의 일부 릴리스, 이미지, GitHub Action 태그가 악성 파일로 바뀌는 공급망 사고가 있었고, 공식 공지(GHSA-69fq-xp46-6x23)는 v0.69.4, Docker Hub의 v0.69.5와 v0.69.6, trivy-action 0.34.2 이하 태그를 영향 범위로 안내합니다. 공지는 이미지를 다이제스트로 참조한 경우에는 영향이 없다고 밝히고, 안전한 버전으로 trivy-action v0.35.0, setup-trivy v0.2.6, Trivy 0.69.2와 0.69.3을 안내합니다. 버전 태그는 나중에 다른 내용으로 바뀔 수 있어서 고정 효과에 한계가 있으므로 이 글은 다이제스트를 함께 씁니다. 다이제스트는 docker pull aquasec/trivy:0.76.0 뒤에 docker inspect aquasec/trivy:0.76.0 --format '{{index .RepoDigests 0}}'로 확인할 수 있고, 이 글의 값은 2026-10-16에 확인한 것입니다.

1. 취약한 이미지 빌드해서 tar로 저장하기

스캔 대상은 파이썬 3.9 베이스에 오래된 flask, requests, pyyaml을 고정한 이미지입니다.

mkdir trivy-lab && cd trivy-lab

cat > Dockerfile <<'EOT'
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
EOT

printf 'flask==2.0.1\nrequests==2.25.1\npyyaml==5.4.1\n' > requirements.txt

cat > app.py <<'EOT'
from flask import Flask
app = Flask(__name__)

@app.get("/")
def index():
    return "hello"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000)
EOT

docker build -q -t cr-trivy-demo:old .
docker save -o old.tar cr-trivy-demo:old
du -h old.tar

Trivy는 컨테이너 안에서 실행합니다. 호스트의 /var/run/docker.sock을 컨테이너에 마운트하면 Trivy 컨테이너가 Docker 데몬 전체를 다룰 수 있게 되므로, 이 글에서는 docker save로 만든 tar 파일만 마운트해서 읽게 합니다. 아래 함수를 한 번 정의해 두면 이후 명령이 짧아집니다. 취약점 데이터베이스는 cr-trivy-cache 볼륨에 보관되어 두 번째 실행부터 다시 받지 않습니다.

trivy() {
  MSYS_NO_PATHCONV=1 docker run --rm -v "$PWD:/work" -w /work \
    -v cr-trivy-cache:/root/.cache/ \
    aquasec/trivy:0.76.0@sha256:2416d1f74045b57274df1a340eee6b510cacd73c66a6ccb8da092b117eb7d222 "$@"
}

MSYS_NO_PATHCONV=1은 Git Bash에서 /root/.cache/ 같은 경로가 윈도우 경로로 바뀌는 것을 막습니다. 리눅스에서는 있어도 영향이 없습니다. 호스트에 trivy가 설치되어 있으면 이 셸 함수가 먼저 실행되어 호스트의 명령을 가립니다.

2. 이미지를 스캔하고 전체 건수 확인하기

옵션 없이 tar 파일을 스캔합니다. 결과가 길어서 요약 줄만 추려 봅니다.

trivy image --input old.tar --scanners vuln | grep -B2 "^Total"

기대 출력(건수는 데이터베이스가 갱신되면 달라지며, 아래는 2026-10-16 기준입니다):

old.tar (debian 13.1)
=====================
Total: 455 (UNKNOWN: 3, LOW: 155, MEDIUM: 172, HIGH: 119, CRITICAL: 6)
--
Python (python-pkg)
===================
Total: 26 (UNKNOWN: 0, LOW: 2, MEDIUM: 14, HIGH: 10, CRITICAL: 0)

위쪽이 Debian 운영체제 패키지, 아래쪽이 pip로 설치한 파이썬 패키지입니다. 베이스 이미지만으로 455건이 나오는 것이 이미지 스캔의 특징입니다. 의존성 목록만 스캔할 때는 보이지 않던 부분입니다.

베이스 이미지의 Debian 패키지에서 455건, 파이썬 패키지에서 26건이 나오는 것을 확인합니다.
베이스 이미지의 Debian 패키지에서 455건, 파이썬 패키지에서 26건이 나오는 것을 확인합니다.

3. 결과 표 읽기

이 글에서는 파이썬 패키지 쪽 표를 예로 읽습니다. 심각도가 HIGH 이상이고 수정 버전이 있는 항목만 보려고 --severity와 --ignore-unfixed를 붙였습니다.

trivy image -q --input old.tar --pkg-types library \
  --severity HIGH,CRITICAL --ignore-unfixed | grep -E "Library|CVE-" | cut -c1-175

기대 출력(앞부분):

│          Library          │ Vulnerability  │ Severity │ Status │ Installed Version │ Fixed Version │                            Title                          
│ Flask (METADATA)          │ CVE-2023-30861 │ HIGH     │ fixed  │ 2.0.1             │ 2.3.2, 2.2.5  │ flask: Possible disclosure of permanent session cookie due
│ jaraco.context (METADATA) │ CVE-2026-23949 │          │        │ 5.3.0             │ 6.1.0         │ jaraco.context: jaraco.context: Path traversal via malicio
│ urllib3 (METADATA)        │ CVE-2025-66418 │          │        │ 1.26.20           │ 2.6.0         │ urllib3: urllib3: Unbounded decompression chain leads to
...

열마다 의미가 있습니다.

열 읽는 법
Library 취약한 패키지 이름입니다. 괄호 안은 버전 정보를 읽은 파일 종류입니다.
Vulnerability CVE 또는 GHSA 번호입니다. 예외 처리할 때 이 값을 씁니다.
Severity 위험도입니다. 같은 패키지의 아래 줄은 값이 같아 비워 두고 표시합니다.
Status fixed는 수정 버전이 배포되었다는 뜻입니다.
Installed Version, Fixed Version 지금 버전과 올려야 할 버전입니다. 수정 버전이 여러 개면 쓰는 메이저 계열에 맞는 것을 고릅니다.

이 표에서 눈여겨볼 곳은 urllib3입니다. 고정하지 않은 urllib3가 1.26.20으로 들어왔고, 수정 버전은 모두 2.x입니다. requests 2.25.1이 urllib3 1.x만 허용하기 때문에, urllib3만 따로 올릴 수 없고 requests를 올려야 합니다. 표의 Fixed Version은 직접 의존성뿐 아니라 함께 설치된 패키지까지 보여 줍니다.

Flask의 CVE-2023-30861이 HIGH로 표시되고 설치 버전 2.0.1과 수정 버전 2.3.2를 확인합니다.
Flask의 CVE-2023-30861이 HIGH로 표시되고 설치 버전 2.0.1과 수정 버전 2.3.2를 확인합니다.

4. 빌드 게이트 만들기

CI에서 빌드를 멈추려면 종료 코드가 필요합니다. 다음 세 옵션을 함께 씁니다.

옵션 효과
--severity HIGH,CRITICAL 해당 심각도만 결과와 종료 코드에 반영합니다.
--ignore-unfixed 수정 버전이 아직 없는 취약점을 뺍니다. 지금 당장 고칠 방법이 없는 항목으로 빌드가 막히는 것을 피합니다.
--exit-code 1 위 조건에 걸리는 취약점이 하나라도 있으면 종료 코드를 1로 합니다. 기본값은 0입니다.
trivy image -q --input old.tar --scanners vuln --severity HIGH,CRITICAL \
  --ignore-unfixed --exit-code 1 > report.txt; echo "exit=$?"
grep -B2 "^Total" report.txt

기대 출력:

exit=1
old.tar (debian 13.1)
=====================
Total: 81 (HIGH: 75, CRITICAL: 6)
--
Python (python-pkg)
===================
Total: 10 (HIGH: 10, CRITICAL: 0)

전체 481건(Debian 455건과 파이썬 26건)이 91건(Debian 81건과 파이썬 10건)으로 줄었지만 CRITICAL이 여섯 건 남아 있어 종료 코드가 1입니다. --ignore-unfixed는 공식 문서 기준으로 --ignore-status affected,will_not_fix,fix_deferred,end_of_life의 줄임말이어서, 수정 버전이 없는 항목뿐 아니라 수정하지 않기로 한 항목과 지원이 끝난(end_of_life) 항목도 함께 빠집니다. 그래서 지원이 끝난 베이스 이미지를 쓰고 있어도 게이트가 통과할 수 있으므로 베이스 이미지의 지원 종료 여부는 따로 확인합니다. 파이프(| grep)를 붙이면 $?가 grep의 종료 코드가 되므로, 위처럼 파일로 받고 종료 코드를 바로 확인합니다.

5. .trivyignore로 예외 처리하기

당장 고칠 수 없는 항목은 .trivyignore에 적어 게이트에서 뺍니다. 각 줄 끝에 exp:날짜를 붙이면 그 날짜가 지난 뒤에는 예외가 사라져 다시 걸립니다. 예외가 영구 면제로 남는 것을 막는 장치입니다. .trivyignore는 ID만 적기 때문에 같은 ID가 나오는 모든 패키지에 적용됩니다. 범위를 좁히려면 .trivyignore.yaml에서 purls(패키지)나 paths(파일 경로)를 함께 지정할 수 있습니다(공식 문서의 filtering 항목에서 확인했고, 이 글에서는 실행하지 않았습니다). 현재 디렉터리의 .trivyignore는 자동으로 읽힙니다(위 함수는 -w /work로 작업 디렉터리를 맞춰 두었습니다).

cat > .trivyignore <<'EOT'
# flask 세션 쿠키 취약점은 2.3.2 로 올리기 전까지 임시 허용
CVE-2023-30861 exp:2026-12-31
EOT

trivy image -q --input old.tar --scanners vuln --severity HIGH,CRITICAL \
  --ignore-unfixed --exit-code 1 > report.txt; echo "exit=$?"
grep -B2 "^Total" report.txt | tail -3
grep -c CVE-2023-30861 report.txt

기대 출력:

exit=1
Python (python-pkg)
===================
Total: 9 (HIGH: 9, CRITICAL: 0)
0

파이썬 패키지 쪽이 10건에서 9건으로 줄고 CVE-2023-30861이 보고서에서 빠졌습니다. Debian 쪽 81건은 그대로라서 종료 코드는 여전히 1입니다. 예외로 빼는 항목은 적을수록 좋습니다.

.trivyignore를 쓰기 전에는 파이썬 패키지 10건, 쓴 뒤에는 9건이 되고 종료 코드는 여전히 1인 것을 확인합니다.
.trivyignore를 쓰기 전에는 파이썬 패키지 10건, 쓴 뒤에는 9건이 되고 종료 코드는 여전히 1인 것을 확인합니다.

6. 베이스 이미지와 패키지를 올려 재스캔하기

이 단계의 결과도 2026-10-16 기준입니다. python:3.9-slim과 python:3.13-slim은 계속 갱신되는 태그이므로 시간이 지나면 건수와 예외 ID가 달라지고, 지원이 끝난 파이썬 3.9 태그는 나중에 사라질 수도 있습니다. 7단계의 예외 ID는 본인이 스캔한 결과에서 다시 확인합니다.

Dockerfile의 베이스 이미지를 올리고, 패키지는 이 글을 쓴 시점의 최신 안정 버전으로 올립니다. 이전 .trivyignore는 지우고 시작합니다.

rm .trivyignore
sed 's/python:3.9-slim/python:3.13-slim/' Dockerfile > Dockerfile.new && mv Dockerfile.new Dockerfile
printf 'flask==3.1.2\nrequests==2.32.5\npyyaml==6.0.3\n' > requirements.txt

docker build -q -t cr-trivy-demo:fixed . && docker save -o fixed.tar cr-trivy-demo:fixed
trivy image -q --input fixed.tar --scanners vuln --severity HIGH,CRITICAL \
  --ignore-unfixed --exit-code 1 > report.txt; echo "exit=$?"
grep -E "Library|CVE-|GHSA-" report.txt | cut -c1-175

기대 출력:

exit=1
│  Library   │    Vulnerability    │ Severity │ Status │ Installed Version │ Fixed Version │                           Title                            │
│ msgpack    │ GHSA-6v7p-g79w-8964 │ HIGH     │ fixed  │ 1.1.2             │ 1.2.1         │ MessagePack for Python: Out-of-bounds read / crash on      │
│ setuptools │ CVE-2025-47273      │          │        │ 70.3.0            │ 78.1.1        │ setuptools: Path Traversal Vulnerability in setuptools     │
│ urllib3    │ CVE-2026-97687      │          │        │ 2.7.0             │ 2.8.0         │ urllib3: urllib3: Traffic interception via HTTPS proxy TLS │
│            │ CVE-2026-97689      │          │        │                   │               │ urllib3: urllib3: Denial of Service via unbounded memory   │

Debian 쪽은 수정 가능한 HIGH 이상이 0건이 되었고, 앞서 여섯 건이던 CRITICAL도 사라졌습니다. 남은 네 건은 앱이 설치한 패키지가 아니라 pip 안에 포함된 vendored 라이브러리(msgpack, setuptools, urllib3)입니다. 앱의 urllib3는 이미 2.8.0이지만 pip가 내부에 2.7.0 사본을 포함하고 있어서 requirements.txt를 고쳐도 사라지지 않습니다. 런타임에 pip가 필요 없다면 최종 이미지에서 pip를 빼는 쪽이 예외보다 안전합니다(멀티 스테이지 빌드는 아래 이어서 해 볼 것에 있습니다). pip를 남겨야 한다면 만료일이 있는 예외를 씁니다. 사유와 만료일을 적어 두고, pip가 새 버전을 내면 이미지를 다시 빌드해 예외를 지웁니다.

베이스 이미지를 올린 뒤 남은 네 건의 라이브러리 이름과 수정 버전을 확인합니다.
베이스 이미지를 올린 뒤 남은 네 건의 라이브러리 이름과 수정 버전을 확인합니다.

7. 예외를 등록하고 게이트 통과 확인하기

cat > .trivyignore <<'EOT'
# pip 26.2.1 에 들어 있는 vendored 라이브러리. pip 새 버전이 나올 때까지 임시 허용
GHSA-6v7p-g79w-8964 exp:2026-12-31
CVE-2025-47273 exp:2026-12-31
CVE-2026-97687 exp:2026-12-31
CVE-2026-97689 exp:2026-12-31
EOT

trivy image -q --input fixed.tar --scanners vuln --severity HIGH,CRITICAL \
  --ignore-unfixed --exit-code 1 > report.txt; echo "exit=$?"
grep -E "debian|Python" report.txt | cut -c1-120

기대 출력:

exit=0
│ fixed.tar (debian 13.7)                                                          │   debian   │        0
│ Python                                                                           │ python-pkg │        0

종료 코드가 0이 되어 게이트를 통과했습니다. 예외 날짜인 2026-12-31이 지나면 위 네 항목이 다시 걸리므로 그 전에 pip를 올리거나 예외를 갱신해야 합니다. .trivyignore는 저장소에 커밋해서 변경 이력이 리뷰에 남게 합니다.

예외 네 줄을 등록한 뒤 종료 코드가 0이고 debian과 python-pkg 모두 0건인 것을 확인합니다.
예외 네 줄을 등록한 뒤 종료 코드가 0이고 debian과 python-pkg 모두 0건인 것을 확인합니다.

8. 취약점 데이터베이스와 GitHub Actions

Trivy는 취약점 데이터베이스를 컨테이너 이미지 형태로 내려받으며, 공식 문서 기준 기본 저장소는 mirror.gcr.io/aquasec와 ghcr.io/aquasecurity 순서입니다. 일시적인 오류(429, 5xx)가 나면 다음 저장소로 넘어갑니다. 다른 곳을 쓰려면 --db-repository를 지정하는데, 지정하면 기본 저장소가 대체되므로 공식 저장소도 함께 적어야 대체 경로가 유지됩니다. 아래는 기본값과 같은 두 저장소를 쉼표로 이어서 지정하고 데이터베이스 내려받기만 실행한 예시이며, 이 글에서 실행해 정상 종료를 확인했습니다.

docker run --rm -v cr-trivy-cache:/root/.cache/ \
  aquasec/trivy:0.76.0@sha256:2416d1f74045b57274df1a340eee6b510cacd73c66a6ccb8da092b117eb7d222 \
  image -q --download-db-only \
  --db-repository mirror.gcr.io/aquasec/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2

같은 게이트를 GitHub Actions에 붙일 때는 aquasecurity/trivy-action을 씁니다. 아래는 공식 저장소 README의 이미지 스캔 예시에 actions/checkout 단계와 version 고정을 더한 것입니다. version 입력은 사용할 Trivy 버전이며 README 기준 기본값은 v0.76.0입니다. 이 글에서는 Actions 위에서 실행하지 않았습니다.

- uses: actions/checkout@v4
- name: Build an image from Dockerfile
  run: docker build -t my-app:${{ github.sha }} .
- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@<고정할 커밋 SHA>
  with:
    version: 'v0.76.0'
    image-ref: 'my-app:${{ github.sha }}'
    format: 'table'
    exit-code: '1'
    ignore-unfixed: true
    vuln-type: 'os,library'
    severity: 'CRITICAL,HIGH'

공급망 사고에서 trivy-action의 기존 태그가 악성 커밋으로 바뀌었던 만큼, 공식 공지는 액션을 태그가 아닌 전체 커밋 SHA로 고정하라고 권고합니다. 사용 시점의 안전한 릴리스를 확인한 뒤 SHA로 고정하세요. 공지는 version: latest를 명시한 경우도 노출 기간에는 위험했다고 설명하므로, version은 위처럼 구체적인 값으로 지정합니다.

자주 막히는 곳

trivy image cr-trivy-demo:old가 docker error: unable to inspect the image로 실패합니다 Trivy 컨테이너 안에는 Docker 데몬이 없어서 이미지를 찾지 못합니다. 이 실습에서는 로컬 이미지 이름으로 스캔하면 Docker, containerd, Podman 소켓을 차례로 찾다가 모두 실패했습니다. docker.sock을 마운트하는 대신 docker save로 만든 tar를 --input으로 넘깁니다.

로컬 이미지 이름으로 스캔하면 docker, containerd, podman 연결 오류가 차례로 나오는 것을 확인합니다.
로컬 이미지 이름으로 스캔하면 docker, containerd, podman 연결 오류가 차례로 나오는 것을 확인합니다.

| grep 뒤의 echo $?가 항상 0입니다 파이프의 마지막 명령(grep)의 종료 코드가 나오기 때문입니다. Trivy의 출력을 파일로 받고 바로 echo "exit=$?"를 확인하거나 set -o pipefail을 씁니다.

.trivyignore에 적었는데도 취약점이 계속 나옵니다 exp: 날짜가 이미 지났을 때입니다. 이 실습에서 CVE-2023-30861 exp:2026-10-01로 바꿔 스캔하면 지난 날짜의 예외가 적용되지 않고 항목이 다시 나왔습니다. 그리고 ID 철자와 파일 위치(현재 디렉터리)도 확인합니다.

Git Bash에서 -v cr-trivy-cache:/root/.cache/ 경로가 윈도우 경로로 바뀌어 실패합니다 명령 앞에 MSYS_NO_PATHCONV=1을 붙입니다. 이 글의 trivy 함수에 이미 들어 있습니다.

requirements.txt를 올렸는데 pip 내부 라이브러리 취약점이 남습니다 pip, setuptools가 내부에 가진 vendored 사본은 앱의 의존성 목록을 고쳐도 바뀌지 않습니다. 6단계처럼 새 베이스 이미지와 pip 업데이트를 기다리거나, 만료일을 둔 예외로 관리합니다.

이어서 해 볼 것

  • 6단계의 Dockerfile을 멀티 스테이지로 바꿔 최종 이미지에서 pip를 빼고, 4개 예외가 필요 없어지는지 같은 게이트로 확인합니다.
  • --severity HIGH,CRITICAL에서 CRITICAL만으로 바꿔 보고, 팀이 허용할 수 있는 기준이 어디인지 정해 봅니다.
  • --format json으로 결과를 받아 CI 산출물로 보관하고, 날짜별 건수 변화를 비교해 봅니다.

확인 범위

1~7단계의 명령은 Windows의 Git Bash와 Docker Desktop(Docker 29.5.3)에서 Trivy 0.76.0 이미지로 직접 실행했고, 취약점 건수는 2026-10-16 기준이며 데이터베이스가 갱신되면 달라집니다. 8단계의 --db-repository 지정은 기본 두 저장소를 모두 적어 데이터베이스 내려받기까지만 실행해 확인했고 스캔에 이어 쓰지는 않았습니다. 이미지 다이제스트는 docker inspect와 docker buildx imagetools inspect로 확인했습니다. .trivyignore.yaml의 범위 지정은 공식 문서만 읽었고 실행하지 않았으며, 6, 7단계의 건수와 Debian 13.7은 이동 태그 기준이라 시점에 따라 달라집니다. 요청이 제한되는 상황은 재현하지 못했으며 대체 저장소 동작은 공식 문서(trivy.dev의 데이터베이스 설정)에 적힌 내용입니다. GitHub Actions 워크플로는 aquasecurity/trivy-action 문서의 예시를 줄인 것으로 실제 Actions에서 실행하지 않았고, SHA 고정은 사용 시점에 확인해야 합니다. sed 대신 파일을 다시 쓰는 방식으로 바꾼 부분은 Git Bash에서만 확인했습니다. docker.sock을 마운트하는 방식은 실행하지 않았습니다. 공급망 사고 내용은 Trivy 공식 저장소의 보안 공지(GHSA-69fq-xp46-6x23)를 읽어 정리했습니다.

이 글의 다음 단계

  • Checkov로 Terraform 설정 오류를 찾고 고치기연습용 main.tf를 apply 없이 Checkov로 점검해 실패 항목 표를 읽고, 설정을 고쳐 통과시킵니다. 사유를 남기는 skip 주석, soft-fail과 종료 코드로 만드는 CI 게이트, json과 sarif 출력, Trivy config와의 결과 비교까지 다룹니다.
  • gitleaks로 저장소의 시크릿을 찾고 커밋 전에 막기임시 git 저장소에 가짜 키를 넣고 gitleaks로 커밋 이력을 스캔합니다. JSON과 SARIF 리포트, baseline, 오탐 제외, pre-commit 훅까지 차례로 설정하고 GitHub Actions 예시를 붙입니다.

DevSecOps 실무 과정의 일부입니다

이 실습은 교육에서 참가자마다 독립된 환경으로 진행합니다. 원하는 일정을 고르면 문의 양식에 미리 채워집니다.

과정 보기

교육 진행 순서와 자주 묻는 질문 보기

같은 카테고리의 글