SVG가 화면에 렌더링 되지 않는 이유
들어가며
프론트 개발자라면 한두 번쯤 프로젝트를 하며 로고를 넣거나 라이브러리 아이콘을 사용하며 SVG 파일을 마주한 적이 있을 것입니다. 예를 들어, MUI 아이콘을 추가하고 개발자 도구를 열어 아이콘을 클릭해 보면 아래와 같은 코드를 볼 수 있습니다.
HTML은 헤더, 이미지, 테이블 등을 정의하기 위해 HTML 요소를 제공합니다. 이와 유사하게 SVG는 모양이나 경로 등을 그리기 위한 요소를 제공합니다. 예를 들어, SVG는 모양을 정의하기 위해 좌표를 지정할 수 있는데, 이러한 좌표들이 모여 아래와 같이 오른쪽으로 향하는 화살표를 나타내게 됩니다.
SVG는 이처럼 간단한 모양에서부터 복잡한 애니메이션이나 자바스크립트를 통한 인터랙션을 추가하는 것도 가능해 매우 흥미롭고, 프론트엔드 개발자에게 강력한 도구가 될 수 있습니다.
하지만 SVG를 웹에 렌더링하는 것이 항상 직관적인 것은 아닙니다. 간단한 프로젝트를 진행하며 SVG를 렌더링해야 했는데, 파일에 문제가 없었음에도 화면에 제대로 그려지지 않았습니다. 이 글은 <img>
태그로 SVG를 렌더링하면서 마주쳤던 문제들과 그 원인을 파악하고 해결책을 찾아가는 과정을 단계적으로 기록해 보았습니다.
TL;DR
SVG 렌더가 실패하는 경우
- HTTP 헤더의 Content-Type이 잘못 설정된 경우. 렌더링을 위한 값은 Content-Type: image/svg+xml, Content-Disposition: inline.
- CORS 헤더가 잘못 설정된 경우. Access-Control-Allow-Origin 헤더에 현재 도메인 추가하기
- 유효하지 않은 SVG 구조.
SVG 화면에 렌더링하기
SVG 알아보기
-
SVG(Scalable Vector Graphics)란?
SVG는 벡터 그래픽을 표현하는 XML 기반의 마크업 언어입니다. 주로 로고나 일러스트레이션, 차트 등 웹 그래픽에 사용됩니다. 재미있는 예시가 있어서 참고 차 가져왔습니다. 시간, 분, 초를 표현하는 rotate() 함수의 파라미터를 자바스크립트로 변경하면 여기에서 볼 수 있듯이
<svg>
로 움직이는 시계 바늘을 표현할 수 있습니다. 간단하게 루트 요소인<svg>
, 원을 그리는<circle>
, 텍스트를 표시하는<text>
와<line>
들을 그룹화하는<g>
태그로 이루어져 있습니다. -
SVG, 왜 좋을까?
위에서 살펴본 것처럼 동적으로 스크립트를 적용할 수 있다는 점뿐만 아니라 SVG는 많은 장점들을 가지고 있습니다.
- 픽셀로 구성된 JPEG 등의 래스터 파일들과 다르게, SVG와 같은 벡터 그래픽은 크기를 조절해도 품질이 유지됩니다.
- SVG 파일들은 많은 컬러 픽셀로 생성되는 래스터 이미지보다 크기가 작습니다.
- SVG 이미지와 동작 방식은 XML 텍스트 파일에 정의합니다. 따라서 텍스트 기반으로 편집이 가능하고, Google과 같은 검색 엔진이나 스크린 리더가 웹 페이지를 읽을 수 있어 웹 사이트의 검색 순위를 높이는 데 도움이 됩니다.
-
SVG, 좋은 점만 있을까?
- 픽셀이 부족하므로 고품질의 디지털 사진을 표현하기에는 적합하지 않습니다. 디테일이 풍부한 사진에는 JPEG 파일이 더 좋습니다.
- SVG 이미지에 포함된 코드를 이해하기 어려울 수 있습니다.
- 래스터 파일에 비해 렌더링이 복잡할 수 있습니다.
왜 SVG의 렌더링이 JPEG나 PNG와 같은 래스터 파일보다 복잡할까요? 살펴봤던 바와 같이 SVG가 텍스트 기반이기 때문입니다. 래스터 파일은 바이너리 데이터로 특정한 형식을 갖추고 있어 서버가 제공한 응답 헤더가 정확하지 않더라도 파일 타입을 식별하고 읽을 수 있습니다. 반면, SVG는 텍스트 기반이므로 브라우저는 이 raw 텍스트를 무엇으로 해석할지에 대한 힌트를 오로지 HTTP 헤더에서 얻습니다. 따라서 정확하게 이 텍스트는 무엇인지에 대한 힌트를 제공해야 합니다.
SVG를 렌더링하며 겪은 일
-
[MDN] Basic properties of SVG files에 따라 브라우저 동작 확인
<img src={svgUrl}>
형태로 렌더링을 시도했지만, 파일을 로드하지 못해 대체 텍스트가 보였습니다.<object data={svgUrl}>
,<iframe src={svgUrl}>
을 사용했더니 마찬가지로 렌더링 되지 않았고, 브라우저에서 해당 파일을 자동으로 다운로드하였습니다. 이를 통해 파일의 Content-Disposition이 attachment라고 생각했습니다. (아니었음)
-
유효한 SVG 파일인지 확인
-
다운로드 된 파일을 열었을 때 파일이 정상적으로 표시되었습니다.
-
해당 파일의 응답 헤더를 확인하기 위해 브라우저 개발자 도구 콘솔 창에서
fetch(svgUrl)
을 해보았는데CSP(Chrome’s Content Security Policy)
가 발생했습니다. -
코드상에서
fetch(svgUrl)
을 했고, 아래 에러가 발생했습니다.SyntaxError: Unexpected token < in JSON
-
SVG는 XML 기반이기 때문에 JSON으로 파싱하려고 하면 에러가 발생합니다. 대신에, response.text()를 이용해 raw XML 텍스트를 해석해야 합니다.
raw 텍스트는 파싱, 변형, 포맷 되지 않은 형식의 데이터를 뜻합니다. 웹 개발에서는 주로 서버나 파일에서 가져온 데이터가 애플리케이션에 의해 사용 되거나 해석되기 이전의 상태를 가리킵니다.
예를 들어, 아래의 SVG 파일은 raw 텍스트입니다. raw 텍스트는 브라우저가 해당 파일 내용을 DOM 트리로 파싱하거나 시각적으로 렌더링하기 이전에 바라보는 방식이라고 이해할 수 있습니다.
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<circle cx="50" cy="50" r="40" fill="red" />
</svg>
-
또는 응답이 XML인 경우 파싱을 위해 DOMParser API를 사용할 수 있습니다.
const parser = new DOMParser()
const svgDocument = parser.parseFromString(svgText, "application/xml")
console.log(svgDocument)
-
SVG 파일 구조를 파악
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="500" height="500" viewBox="0 0 500 500">
<defs>
<linearGradient id="gradient1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:rgb(255, 0, 150);stop-opacity:1" />
<stop offset="100%" style="stop-color:rgb(0, 150, 255);stop-opacity:1" />
</linearGradient>
</defs>
<rect x="0" y="0" width="500" height="500" fill="url(#gradient1)" />
<circle cx="250" cy="250" r="150" fill="white" />
<polygon points="250,100 310,400 190,400" fill="rgb(0, 150, 255)" />
<line x1="100" y1="250" x2="400" y2="250" stroke="rgb(255, 255, 255)" stroke-width="4" />
</svg>
-
문서를 1.0 버전의 XML로 정의하고, 캐릭터 인코딩은 US-ASCII를 사용함을 명시
<?xml version="1.0" encoding="US-ASCII"?>
-
문서를 SVG 1.0 DTD(Document Type Definition)를 사용하는 SVG 파일로 정의하고, 공식 스펙은 ‘http://www.w3.org/…에 해당하는 주소를 명시
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
- 루트
<svg>
요소xmlns
: SVG 파일의 XML 네임 스페이스를 정의하여 요소와 속성이 일반 XML이 아닌 SVG로 해석되도록 한다.xmlns:xlink
XLink에 대한 네임 스페이스를 제공. XLink는 외부 리소스를 연결하고 참조하는 데 사용.style
: 요소가 렌더링 되어야 할 인라인 스타일<defs>
: 그래디언트, 패턴 등의 재사용 가능한 요소들을 담는 컨테이너<g>
: 여러 SVG 요소들에 공통된 스타일이나 transform(translate, scale)을 적용하기 위해 그룹화.
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="500" height="500" viewBox="0 0 500 500">
<defs><rect x="0" y="0" width="500" height="500" fill="url(#gradient1)" />
<circle cx="250" cy="250" r="150" fill="white" />
</defs>
<polygon points="250,100 310,400 190,400" fill="rgb(0, 150, 255)" />
<line x1="100" y1="250" x2="400" y2="250" stroke="rgb(255, 255, 255)" stroke-width="4" />
</svg> - 루트
- 응답 헤더 확인
- 응답 헤더의 Content-Type이 application/octet-stream이고, Content-Disposition은 없는 것을 확인. 구글링 결과 보통 AWS S3에 파일을 업로드하면 기본 타입이
application/octet-stream
인 것으로 확인. octet-stream - Content-Type이 octet-stream인 경우, 대부분의 브라우저는 일반적인 바이너리 스트림으로 이해하고 렌더가 아닌 다운로드를 실행한다.
- MDN에 따르면, 브라우저들은 파일 확장자가 아닌 Content-Type에 명시하는 미디어 타입인 MIME 타입을 사용하여 URL 처리 방법을 결정한다. 따라서 웹 서버가 응답의 Content-Type 헤더에 올바른 MIME 타입을 보내는 것이 중요하다. 해당 헤더가 올바르게 설정되어 있지 않은 경우, 브라우저가 파일 내용을 잘못 해석하거나 웹사이트가 오동작하거나 다운로드한 파일이 잘못 처리될 수 있다.
- Blob 객체 활용하기
-
서버의 응답 헤더를 수정할 수 없는 상황이었기 때문에 XML 기반의 SVG를 브라우저에 렌더링하기 위해 Blob 파일을 활용
-
파일 타입이 image/svg+xml인 바이너리 객체를 만들고, 이에 대한 URL을 생성하여 사용. type 속성을 지정할 경우, Blob 객체를 업로드/다운로드할 수 있고, 네트워크 통신에서 Content-Type의 역할을 한다. URL.createObjectURL을 Blob을 파라미터로 받아
blob:<origin>/<uuid>
형식으로 해당 Blob에 대한 고유한 URL을 생성.const svgBlob = new Blob([svgText], { type: 'image/svg+xml' })
const svgBlobUrl = URL.createObjectURL(svgBlob)
<img src={svgBlobUrl} alt={'SVG 미리보기'} />
웹 개발자들이 알아야 할 중요한 MIME 타입 가운데 하나로, 바이너리 데이터의 기본 타입입니다. “알 수 없는” 바이너리 파일을 뜻하기 때문에 브라우저는 보통 파일을 실행하거나 실행이 필요한지 질문하지 않습니다. application/octet-stream의 경우, 브라우저는 Content-Disposition 헤더가 attachment로 설정한 것처럼 동작합니다.
대부분의 웹 서버는 인식할 수 없는 자원을 application/octet-stream MIME 타입으로 전송합니다. 대부분의 브라우저는 그러한 리소스에 대해 보안상의 이유로 Word에서 열기와 같은 기본 동작을 설정하지 못하도록 합니다. 따라서 사용자는 파일을 디스크에 직접 저장하여 사용해야 합니다.
SVG 렌더링 문제 디버깅하기
위 과정에서 배운 SVG가 렌더링 되지 않는 현상을 발견했을 경우에 대한 디버깅 과정을 순서대로 정리해 보았습니다.
- 브라우저에서 동작 확인
- SVG 주소를 브라우저 탭에 입력합니다. 파일이 화면에 표시되지 않고, 다운로드된다면 Content-Disposition이 attatchment거나 Content-Type이 application/octet-stream일 수 있습니다.
- HTTP 응답 헤더 확인
-
파일에 문제가 없다면 브라우저 개발자 도구를 열어 Content-Type, Content-Disposition, Access-Control-Allow-Origin을 확인합니다.
-
화면에 렌더링 되지 않는 문제라면 Content-Type: image/svg+xml인지 확인
-
의도치 않은 동작으로 다운로드 된다면, Content-Disposition을 attachment → inline으로 변경
-
CORS 에러가 발생한다면 현재 요청하고 있는 도메인이 Access-Control-Allow-Origin에 포함되어 있는지 확인
// 서버 응답 헤더 예시
Content-Type image/svg+xml
Content-Disposition inline
Access-Control-Allow-Origin *
- SVG 파일이 유효한지 확인
- SVG의 구조가 유효한지 확인
- 서버에서 헤더를 변경할 수 없다면 Blob 객체를 활용
-
서버 헤더를 변경할 수 없는 경우, 올바른 MIME 타입을 가진
Blob
객체를 만들어 활용할 수 있습니다. SVG 파일을 fetch하여image/svg+xml
타입의 바이너리 객체를 만들어 사용합니다.const svgBlob = new Blob([svgText], { type: 'image/svg+xml' }
const url = URL.createObjectURL(svgBlob)
<img src={svgBlobUrl} alt={'SVG 미리보기'} /> -
__dangerouslySetInnerHTML
을 활용하여 인라인 SVG로 HTML에 바로 주입하는 것도 가능합니다. 하지만 SVG가 크거나 여러 번 사용되는 경우 URL 참조 방식이 더 효율적입니다.
나가며
SVG가 렌더링 되지 않는다면, Content-Type과 Content-Disposition 을 먼저 확인해볼 수 있습니다. 서버의 응답을 수정할 수 없다면, Blob을 활용해 렌더링할 수 있습니다.
이 이슈에 대한 디버깅을 통해 브라우저가 SVG와 HTTP 헤더를 처리하는 방법을 배우게 되었습니다. 브라우저의 이러한 동작을 이해하는 것은 파일을 적절하게 다루기 위해 프론트엔드 개발자에게 중요합니다.
추가로, 크롬에서 로컬에서 헤더를 수정하고 볼 수 있는 방법도 필요하다면 활용할 수 있습니다. 또한, SVG에 대해 조금 더 자세히 알아보고 싶으시다면 크리스마스를 기념 어드벤트 캘린더 형식으로 제작된 SVG Tutorial을 통해 24가지의 예제를 참고해보세요 :)
REF
https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Introduction
https://developer.mozilla.org/en-US/docs/Web/SVG
https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types
https://www.adobe.com/kr/creativecloud/file-types/image/vector/svg-file.html
https://medium.com/@benjamin.black/using-blob-from-svg-text-as-image-source-2a8947af7a8e