fetch()은Response에 대한 약속을 반환하고 본문을 읽는 데 1초가 걸립니다await.- 404, 500 등의 HTTP 오류는 거부하지 않으므로 항상 확인하세요
response.ok. - JSON을 보내려면
JSON.stringify을 사용하고Content-Type헤더를 설정하세요. - 가져오기에는 기본 시간 초과 옵션이 없으므로
AbortSignal.timeout()을 사용하세요.
대부분의 최신 웹 앱은 항상 서버와 통신합니다. JavaScript Fetch API는 이를 수행하는 표준 방법이며 브라우저와 Node.js 18 이상에 내장되어 있으므로 설치할 것이 없습니다.
기본 사용에는 두 줄이 필요하지만 많은 자습서는 여기서 멈추고 실제 버그를 일으키는 부분인 오류 처리를 생략합니다. 우리는 Node.js 24의 소규모 로컬 서버에 대해 이 가이드의 예제를 테스트했으며 표시된 출력은 해당 실행에서 나온 것입니다. 코드에서 example.com은 자신의 API를 나타냅니다. Promise가 처음이라면 JavaScript Promise 설명부터 시작하세요.
가져오기 API란 무엇입니까?
Fetch는 HTTP 요청을 만들기 위한 약속 기반 인터페이스입니다. 이는 이전의 콜백 기반 XMLHttpRequest을 대체했으며 이제 많은 프로젝트에서 간단한 호출을 위해 라이브러리를 추가하는 대신 이를 사용합니다.
| 특징 | 가져오기 | Axios (라이브러리) |
|---|---|---|
| 설치 | 내장 | 종속성으로 추가됨 |
| JSON 본문 | 직접 문자열화하세요 | 자동 |
| JSON 응답 구문 분석 | 전화 response.json() |
자동 |
| HTTP 오류가 거부됩니까? | 아니요 | 예, 기본적으로 |
| 시간 초과 | AbortSignal을 통해 |
timeout 옵션 |
GET 요청
Fetch는 기본적으로 GET 요청을 수행합니다. 두 단계가 있으며 각 단계에는 await이 필요합니다. 먼저 응답 헤더가 도착한 다음 본문을 읽습니다.
const response = await fetch("https://example.com/users/1");
const user = await response.json();
console.log(user);
// { id: 1, name: 'Asha' }
본문이 크고 헤더보다 늦게 도착할 수 있기 때문에 두 단계가 존재합니다. 브라우저 스크립트에서 최상위 await에는 모듈이 필요하므로 일반적으로 이를 async 함수 안에 넣습니다. JavaScript 비동기/대기 가이드를 참조하세요.
응답 객체
본문을 읽기 전에 응답을 검사할 수 있습니다.
console.log(response.status, response.ok, response.headers.get("content-type"));
// 200 true application/json
| 속성 또는 메서드 | 당신에게 준다 |
|---|---|
response.ok |
true 상태 200~299의 경우 |
response.status |
404와 같은 상태 코드 |
response.headers.get(name) |
응답 헤더 1개 |
response.json() |
JSON으로 구문 분석된 본문 |
response.text() |
몸을 끈으로 |
response.blob() |
이미지와 같은 바이너리 데이터 |
본문은 한 번만 읽을 수 있습니다. 테스트에서 json()을 두 번째로 호출하는 데 실패했습니다.
await res.json();
await res.json();
// TypeError: Body is unusable: Body has already been read
두 번 읽어야 하는 경우 먼저 response.clone()을 호출하세요.
큰 놀라움: 404는 던지지 않습니다.
이것이 이 글에서 가장 중요한 내용입니다. 존재하지 않는 사용자를 요청했습니다.
const res = await fetch("https://example.com/users/999");
console.log(res.status, res.ok);
// 404 false
예외는 발생하지 않았습니다. 가져오기에 관한 한 요청은 작동했습니다. 서버에 도달하여 응답했습니다. Promise는 네트워크 오류와 같이 사용 가능한 응답이 전혀 도착하지 않는 경우에만 거부합니다. 따라서 이와 같은 코드에는 숨겨진 버그가 있습니다.
try {
const res = await fetch(url);
const data = await res.json(); // may be an error payload
showUser(data); // runs anyway for 404 and 500
} catch (error) {
showError(); // does not run for 404 or 500
}
각 코드의 의미를 알아보려면 HTTP 상태 코드 치트 시트를 확인하세요.
재사용 가능한 도우미
어디서나 해당 확인을 반복하지 마십시오. 한 번 포장하세요.
async function getJson(url, options) {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
}
try {
await getJson("https://example.com/users/999");
} catch (error) {
console.log("caught:", error.message);
}
// caught: Request failed with status 404
이제 HTTP 오류와 네트워크 오류는 모두 동일하게 종료됩니다catch. 이 작은 기능은 모든 프로젝트에서 사용할 가치가 있습니다. 실제 버전은 응답 본문에서 오류 메시지를 읽고 구문 분석할 JSON이 없는 204 No Content 응답을 처리할 수도 있습니다.
JSON 데이터 게시
옵션 개체를 두 번째 인수로 전달합니다. JSON의 경우 데이터를 문자열로 변환하고 헤더를 설정하는 두 가지 작업을 직접 수행해야 합니다.
const created = await getJson("https://example.com/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Ben" }),
});
console.log(created);
// { id: 2, name: 'Ben', type: 'application/json' }
테스트 서버는 수신한 콘텐츠 유형을 다시 에코하여 헤더가 도착했음을 확인했습니다. 헤더를 잊어버리는 것은 혼란스러운 서버 오류의 일반적인 원인입니다. 많은 프레임워크가 JSON이 선언될 때만 구문 분석하기 때문입니다. 양식이나 파일을 업로드하려면 FormData 개체를 본문으로 전달하고 Content-Type을 설정하지 않습니다. 브라우저가 경계 값 자체를 추가해야 하기 때문입니다.
기타 방법 및 옵션
| 옵션 | 예 | 목적 |
|---|---|---|
방법 |
"PUT", "PATCH", "DELETE" |
HTTP 메서드 |
헤더 |
{ 승인: "전달자 ..." } |
요청 헤더 |
몸 |
JSON.stringify(data) |
요청 본문 |
신호 |
AbortSignal.timeout(5000) |
취소 중 |
자격 증명 |
"포함하다" |
교차 출처로 쿠키 보내기 |
캐시 |
"무점포" |
브라우저 캐시 동작 |
REST와 다른 API 스타일 중에서 선택하시겠습니까? REST와 GraphQL을 비교하면 fetch가 각각에 얼마나 적합한지 알 수 있습니다.
쿼리 문자열
특수 문자로 인해 쿼리 문자열이 끊어지므로 텍스트를 결합하여 쿼리 문자열을 작성하지 마십시오. URLSearchParams이 인코딩을 처리합니다.
const params = new URLSearchParams({ q: "red shoes", page: 2 });
console.log(params.toString());
// q=red+shoes&page=2
await getJson(`https://example.com/search?${params}`);
// { q: 'red shoes', page: '2' }
서버가 red shoes을(를) 올바르게 수신했으며 공간이 복원되었습니다.
시간 초과 및 취소
별도로 지정하지 않는 한 Fetch는 느린 서버에서 오랜 시간을 기다립니다. AbortSignal.timeout()은 한계를 설정합니다. 테스트 엔드포인트는 200밀리초 제한에 대해 2초가 걸렸습니다.
try {
await fetch("https://example.com/slow", { signal: AbortSignal.timeout(200) });
} catch (error) {
console.log(error.name);
}
// TimeoutError
예를 들어 사용자가 페이지를 떠나거나 새 검색어를 입력할 때 수동으로 취소하려면 AbortController을 사용하세요.
const controller = new AbortController();
const pending = fetch("https://example.com/slow", { signal: controller.signal });
controller.abort();
try {
await pending;
} catch (error) {
console.log(error.name);
}
// AbortError
한 번에 여러 요청을 실행하려면 Promise.all을 사용하세요. 이는 한 번에 하나씩 기다리는 것보다 훨씬 빠릅니다.
const [user, search] = await Promise.all([
getJson("https://example.com/users/1"),
getJson("https://example.com/search?q=a&page=1"),
]);
모든 종류의 실패
다음은 실행에서 각 실패의 모습과 이를 구별하는 방법입니다.
| 상황 | 무슨 일이 일어나는가 | 그것을 감지하는 방법 |
|---|---|---|
| 서버가 404 또는 500을 반환합니다. | 약속 이행 | response.ok은 거짓이다 |
| 서버에 연결할 수 없습니다 | TypeError로 거부 |
잡기 |
| 시간 초과에 도달했습니다. | TimeoutError로 거부 |
error.name |
| 수동으로 취소됨 | AbortError로 거부 |
error.name |
| 본문이 유효한 JSON이 아닙니다. | json()은 SyntaxError로 거부합니다. |
잡기 |
마지막 행은 프로덕션에서 일반적입니다. 502를 반환하는 프록시는 종종 HTML 오류 페이지를 보냅니다. 우리는 이것을 재현했고, res.json()은 SyntaxError를 던졌습니다. 구문 분석하기 전에 response.ok을 확인하면 혼란스러운 구문 분석 오류가 아니라 실제 문제를 보고한다는 의미입니다.
팁: 의도적으로 이러한 실패 경로를 테스트하세요. 브라우저 개발자 도구는 오프라인 상태를 시뮬레이션할 수 있으며 테스트 도구는 500 응답을 모의할 수 있습니다. Postman을 사용한 API 테스트 가이드에서는 API 측면 확인을 다룹니다.
CORS 및 자격 증명
브라우저에서 한 원본에서 다른 원본으로의 요청에는 원본 간 리소스 공유 규칙인 CORS가 적용됩니다. Access-Control-Allow-Origin라는 오류가 표시되면 수정 사항은 서버에 속하며 사이트를 허용해야 합니다. JavaScript에서는 실패가 일반 TypeError으로만 표시되므로 실제 이유는 브라우저 콘솔을 살펴보세요. Node.js는 CORS를 적용하지 않으므로 스크립트에서 작동하는 요청이 브라우저에서 계속 실패할 수 있습니다.
기본적으로 브라우저는 교차 출처 요청에 대해 쿠키를 보내지 않습니다. 필요한 경우 credentials: "include"을 설정하고 서버에서 허용하는지 확인하십시오. 누구나 읽을 수 있으므로 브라우저 JavaScript에 비밀 API 키를 넣지 마십시오. 서버에 보관하세요.
일반적인 실수
- 확인하지 않음
response.ok. 오류 응답은 성공으로 처리됩니다. - 두 번째
await.response.json()을 잊어버리면 약속도 반환됩니다. JSON.stringify를 잊어버림. 몸체가[object Object]을 보내는 것처럼 객체를 전달합니다.Content-Type를FormData으로 설정합니다. 경계가 무너지므로 그대로 두세요.- 시간 초과가 없습니다. 요청이 정지되면 페이지 스피너가 무기한 실행될 수 있습니다.
- 빈 응답에 대해 JSON을 구문 분석합니다. 204에는 본문이 없으며
json()가 발생합니다.
마무리
기본 규칙을 기억하면 가져오기는 간단합니다. 응답은 성공과 동일하지 않습니다. 작은 도우미를 프로젝트에 복사하고 시간 초과를 추가하고 실패 경로를 테스트하세요. 다음 단계에서는 API에서 데이터를 로드하고 명확한 오류 메시지를 표시하며 사용자가 느린 검색을 취소할 수 있는 작은 페이지를 구축합니다.
자주 묻는 질문
가져오기가 404에서 오류를 발생시키나요?
아니요. 가져오기는 네트워크 오류와 같이 응답이 수신되지 않는 경우에만 거부됩니다. HTTP 오류의 경우 response.ok 또는 response.status를 직접 확인하세요.
가져오기를 사용하여 JSON을 어떻게 보내나요?
메소드를 POST로 설정하고, application/json의 Content-Type 헤더를 추가하고, JSON.stringify를 통해 데이터를 본문으로 전달합니다.
가져오기에 두 개의 대기가 필요한 이유는 무엇입니까?
첫 번째는 응답 헤더가 도착하면 해결됩니다. 두 번째는 본문이 다운로드되어 구문 분석될 때까지 기다립니다. 이는 나중에 발생할 수 있습니다.
가져오기 시간 초과를 어떻게 설정하나요?
옵션에서 신호 전달: AbortSignal.timeout(5000). 그런 다음 요청이 해당 밀리초보다 오래 걸리면 TimeoutError와 함께 거부됩니다.
Node.js에서 가져오기를 사용할 수 있나요?
예. Fetch는 Node.js 18 이상에서 전역적으로 사용할 수 있습니다. CORS 규칙은 브라우저에서만 적용되지만 이전 버전에는 패키지가 필요합니다.
계속 학습하세요
테스트, WordPress 또는 SEO에 도움이 필요하십니까?
우리는 팀이 더 나은 소프트웨어를 출시하고 사이트를 성장시킬 수 있도록 돕습니다. 현재 진행 중인 작업을 알려주시면 명확한 다음 단계를 제안해 드리겠습니다.
추가 가이드 테스트, AI 및 최신 개발
이 기사가 마음에 드셨나요? 주제를 선택하고 최신 튜토리얼로 바로 이동하세요.
AI 환각 설명: 언어 모델이 사물을 구성하는 이유와 이를 줄이는 방법
AI 환각은 그것이 무엇인지, 언어 모델이 환각을 생성하는 이유, 일반적인 유형, 접지 및 프롬프트를 통해 환각을 줄이는 방법, 그리고 우리가 실행한 간단한 점검에 대해 설명했습니다.
기사 읽기 →DevOps환경 변수 설명: .env 파일, 비밀 및 구성이 올바르게 완료되었습니다.
우리가 실행한 Python 예제를 통해 설명된 환경 변수: 읽기 및 설정, 모든 값이 문자열인 이유, .env 파일, 프로세스 상속, 비밀 및 유효성 검사.
기사 읽기 →소프트웨어 개발API 속도 제한 설명: 알고리즘, 헤더 및 429 오류 처리
우리가 실행한 시뮬레이션을 통해 설명된 API 속도 제한: 토큰 버킷, 고정 창, 슬라이딩 창, 429 응답, 속도 제한 헤더, 백오프를 사용한 재시도 및 테스트 제한.
기사 읽기 →테스트 자동화Python에서 조롱: unittest.mock, 패치 및 테스트를 거짓말로 만드는 실수
Unittest.mock을 사용하여 Python에서 모의, 테스트됨: 모의 및 return_value, 부작용, 이름이 사용되는 패치, 사양 및 자동 사양, 모의 날짜 및 일반적인 실수.
기사 읽기 →소프트웨어 테스트코드 커버리지 설명: 회선 커버리지와 분기 커버리지 및 100%의 실제 의미
실제 Coverage.py 실행을 통해 설명되는 코드 적용 범위: 라인, 분기 및 조건 적용 범위, 보고서 읽기, 100% 적용 범위가 여전히 버그를 놓칠 수 있는 이유 및 합리적인 대상.
기사 읽기 →데이터베이스 및 SQLSQL의 DELETE vs TRUNCATE vs DROP: 차이점, 롤백 및 사용 시기
테스트된 SQL을 통해 DELETE 대 TRUNCATE 대 DROP에 대해 설명합니다. 각각의 제거, 롤백, 트리거, ID 재설정, 속도 및 PostgreSQL, MySQL, SQL Server 및 SQLite의 차이점.
기사 읽기 →데이터베이스 및 SQLSQL 뷰 설명: 예제를 통해 함정 생성, 사용 및 방지
우리가 실행한 쿼리로 설명된 SQL 뷰: 뷰 생성, 실시간 결과, 열 숨기기, 성능, 업데이트 가능하고 구체화된 뷰, 손상된 종속성 및 모범 사례.
기사 읽기 →소프트웨어 개발TypeScript 인터페이스와 유형: 실제 차이점, 컴파일러로 테스트
TypeScript 인터페이스 대 유형, 실제 컴파일러로 확인: 둘 다 할 수 있는 것, 하나만 할 수 있는 것, 선언 병합, 확장 대 교차점 및 따라야 할 간단한 규칙.
기사 읽기 →소프트웨어 개발Python *args 및 **kwargs 설명: 명확한 예가 포함된 유연한 함수
Python *args 및 **kwargs는 우리가 실행한 코드(패킹 및 언패킹, 매개변수 순서, 키워드 전용 및 위치 전용 인수, 데코레이터 및 일반적인 오류)를 설명했습니다.
기사 읽기 →소프트웨어 개발Python Lambda 함수: 구문, 실제 사용 및 사용하지 말아야 할 경우
Python 람다 함수는 구문, 키를 사용한 정렬, 맵 및 필터, 클로저, 후기 바인딩 트랩, 일반 def가 더 나은 선택인 경우 등 우리가 실행한 코드로 설명되었습니다.
기사 읽기 →인공지능AI 에이전트란 무엇입니까? 작동 방식, 간단한 에이전트 루프를 사용하여 실행할 수 있음
AI 에이전트란 무엇입니까? 우리가 실행한 작은 Python 루프, 실제 위험 및 간단한 챗봇이 더 나은 선택일 때 에이전트 루프, 도구, 메모리 및 가드레일에 대한 일반 가이드입니다.
기사 읽기 →DevOps모니터링과 관찰 가능성: 로그, 지표, 추적을 통한 실제 차이점
모니터링과 관찰 가능성 설명: 알려진 오류와 알려지지 않은 오류, 로그, 지표 및 추적, 결제 사건 안내, 골든 신호, OpenTelemetry 및 유용한 경고.
기사 읽기 →DevOpsDocker 볼륨과 바인딩 마운트: 컨테이너 데이터를 올바른 방식으로 유지
Docker 볼륨과 바인드 마운트 설명: 컨테이너 데이터가 사라지는 이유, 명명된 볼륨, 바인드 마운트, tmpfs, Compose 예제, 백업, 권한 및 각 사용 시기.
기사 읽기 →테스트 자동화TDD와 BDD: 주요 차이점, 두 가지 방식으로 구축된 하나의 기능 및 각 사용 시기
TDD와 BDD는 두 가지 방식으로 구축된 하나의 할인 기능, 즉 우리가 실행한 코드를 사용한 적록 리팩토링, 주어진 시점 시나리오, 도구, 장단점 및 각 사용 시기를 설명합니다.
기사 읽기 →소프트웨어 테스트UAT(사용자 승인 테스트): 프로세스, 체크리스트 및 실제 사례
사용자 승인 테스트는 누가 실행하는지, QA와 어떻게 다른지, 7단계 프로세스, UAT 테스트 사례 템플릿, 체크리스트, 승인 기준 및 일반적인 실수에 대해 설명했습니다.
기사 읽기 →데이터베이스 및 SQLSQL NULL 값 설명: IS NULL, COALESCE, NULLIF 및 피해야 할 함정
우리가 실행한 쿼리를 통해 설명된 SQL NULL 값: 3개 값 논리, IS NULL, COALESCE, NULLIF, 집계, 조인 및 정렬이 NULL을 처리하는 방법 및 조용히 발생하는 버그.
기사 읽기 →데이터베이스 및 SQLSQL CASE WHEN 문: 구문, 실제 예 및 일반적인 실수
우리가 실행한 쿼리로 설명된 SQL CASE: 단순 및 검색 양식, 조건 순서, SUM이 있는 피벗, COUNT 트릭, 사용자 정의 정렬, 업데이트 및 피해야 할 NULL 트랩.
기사 읽기 →소프트웨어 개발JWT란 무엇입니까? 실제 예제로 설명되는 JSON 웹 토큰
JWT란 무엇입니까? JSON 웹 토큰은 세 부분, 즉 우리가 실행한 코드에서 구축 및 변조된 토큰, JWT 대 세션, 저장소, 새로 고침 토큰 및 보안 실수를 설명했습니다.
기사 읽기 →소프트웨어 개발JavaScript 이벤트 루프 설명: 예제가 포함된 호출 스택, 마이크로태스크 및 타이머
호출 스택, 작업 및 마이크로태스크 대기열, setTimeout 0이 즉시 실행되지 않는 이유, 차단, 기아 및 인터뷰 퀴즈 등 우리가 실행한 코드로 JavaScript 이벤트 루프를 설명했습니다.
기사 읽기 →소프트웨어 개발Python 생성기 및 수율 설명: 실제 예제로 메모리 절약
Python 생성기와 항복은 우리가 실행한 코드로 설명됩니다. 일시 중지 및 재개 방법, 측정된 메모리 절약, 파이프라인, 무한 시퀀스, 항복, 전송 및 공통 트랩.
기사 읽기 →인공지능임베딩이란 무엇입니까? 벡터와 코사인 유사성을 간단하게 설명
임베딩이란 무엇입니까? 벡터, 코사인 유사성 및 의미론적 검색에 대한 일반 언어 가이드로, 우리가 실행한 작은 예, 일반적인 용도, 모델 선택 및 알려진 한계를 제공합니다.
기사 읽기 →DevOps블루-그린 배포와 카나리아 배포: 차이점, 장단점 및 선택 방법
블루-그린 배포와 카나리아 배포에 대한 설명: 각각의 작동 방식, 비용, 위험 및 롤백, 테스트한 안정적인 사용자 버킷팅, 데이터베이스 마이그레이션, 롤링 업데이트 및 선택 방법.
기사 읽기 →테스트 자동화불안정한 테스트: 원인과 해결 방법
우리가 실행한 데모를 통해 불안정한 테스트에 대해 설명했습니다. 주요 원인, 확인 및 진단 방법, 시기, 공유 상태 및 날짜, 격리 프로세스에 대한 적절한 수정 사항.
기사 읽기 →소프트웨어 테스트부하 테스트와 스트레스 테스트 비교: 차이점, 측정항목 및 k6 예
부하 테스트와 스트레스 테스트 비교 설명: 각각의 증명, 스파이크 및 흡수 테스트, 중요한 측정항목, 평균이 잘못된 이유, k6 예 및 테스트 계획 방법.
기사 읽기 →소프트웨어 테스트테스트 케이스와 테스트 시나리오: 주요 차이점, 예 및 각각의 사용 시기
테스트 케이스와 테스트 시나리오 설명: 명확한 정의, 병렬 테이블, 시나리오에서 케이스로 가져온 하나의 비밀번호 재설정 기능, 템플릿 및 각 사용 시기.
기사 읽기 →데이터베이스 및 SQLSQL 쿼리 최적화: 타이밍에 따라 실행 계획을 읽고 느린 쿼리를 수정합니다.
백만 개의 행에서 테스트된 SQL 쿼리 최적화: 실행 계획 읽기, 올바른 인덱스 추가, 분할할 수 없는 필터 수정, 복합 인덱스 및 키 세트 페이지 매김 사용.
기사 읽기 →데이터베이스 및 SQLSQL UNION 대 UNION ALL: 테스트된 차이점, 예 및 성능
SQL UNION과 UNION ALL은 우리가 실행한 쿼리(중복, 둘 다 따르는 규칙, 실제 데이터 버그, 측정된 성능, INTERSECT 및 EXCEPT, 각각의 사용 시기)를 설명합니다.
기사 읽기 →소프트웨어 개발JavaScript Fetch API 튜토리얼: GET, POST, 오류 처리 및 시간 초과
로컬 서버에 대해 테스트된 JavaScript Fetch API 튜토리얼: GET 및 POST, 404가 발생하지 않는 이유, 재사용 가능한 도우미, 쿼리 문자열, 시간 초과, CORS 및 일반적인 실수.
기사 읽기 →소프트웨어 개발Python 파일 처리: 읽기, 쓰기, CSV, JSON 및 pathlib 예제 포함
우리가 실행한 코드로 설명된 Python 파일 처리: 개방형 모드, 대용량 파일 읽기, CSV, JSON, pathlib, 바이너리 파일, 안전한 쓰기, 오류 처리 및 일반적인 실수.
기사 읽기 →소프트웨어 개발Git merge vs rebase: 차이점, 예제, 언제 무엇을 쓸까
Git merge vs rebase를 실제 로그 출력, 비교표, rebase의 황금률, 안전한 팀 워크플로로 설명합니다.
기사 읽기 →소프트웨어 개발파이썬 리스트 컴프리헨션: 문법, 예제, 피해야 할 때
파이썬 리스트 컴프리헨션 설명: 문법, 필터링, if-else, 중첩 반복문, 실제 측정 시간, 그리고 일반 for 반복문이 더 나은 경우.
기사 읽기 →데이터베이스 및 SQL예제로 쉽게 배우는 SQL JOIN (Inner, Left, Right, Full)
샘플 테이블과 쿼리로 SQL JOIN을 단계별로 이해하세요. INNER, LEFT, RIGHT, FULL, CROSS, SELF와 흔한 실수, 성능 팁까지 정리했습니다.
기사 읽기 →