XSLT 변환기
스타일시트를 적용합니다. WebAssembly로 자동 전환됩니다.
모든 처리는 이 탭 안에서 이루어집니다. 붙여넣은 내용은 업로드되거나 기록되거나 전송되지 않습니다. 네트워크 패널을 열어 확인하세요.
왼쪽 창에 문서를, 오른쪽에 스타일시트를 붙여넣으면 변환이 이 탭 안에서 실행됩니다. 결과는 텍스트로 돌아오고, 출력 위에 그것을 만들어 낸 엔진과 걸린 시간이 표시됩니다. 두 입력 모두 먼저 정형식인지 검사하므로, 스타일시트에서 꺾쇠 하나가 빠졌다면 컴파일 실패가 아니라 바로 그 문제로 보고합니다.
스타일시트가 기대한 대로 움직이지 않아 빠르게 시험해 보고 싶을 때 쓰세요. 떠난 사람에게서 물려받은 변환, 정말 발화하는지 확신이 없는 대응 패턴, 어떤 입력에서는 빈 파일만 만드는 템플릿.
이 도구가 이런 모양인 이유는, 브라우저의 XSLT가 공개된 일정에 따라 제거되고 있기 때문입니다. 네이티브 프로세서가 있는 동안에는 그것을 쓰고, 없으면 libxslt의 WebAssembly 빌드로 물러나며, 어느 엔진이 돌았는지 밝혀 줍니다.
크롬은 XSLT를 제거하는 중이고, 날짜도 공개되어 있다
구글은 2025년에 폐지·제거 의사를 제출했습니다. 근거는 이렇습니다. xml-stylesheet 처리 명령이 나타나는 비율은 대략 8천 번의 페이지 로드에 한 번인데, libxslt는 메모리 안전성 이력이 길고 2025년 6월에 오랜 관리자를 잃었습니다. 모질라와 애플 모두 지지를 표했습니다. 일정에는 날짜가 박혀 있습니다.
파이어폭스와 사파리는 자체 제거를 발표하지 않았으므로 XSLTProcessor는 당장은 거기서 계속 작동합니다. 파이어폭스는 libxslt가 아니라 자사 TransforMiiX 프로세서를 쓰며, 그래서 일부 스타일시트가 이미 브라우저마다 다르게 동작합니다.
이 페이지는 생성자가 존재하는지 확인하는 대신 한 줄짜리 변환을 실행해서 작동하는 네이티브 프로세서가 있는지 떠봅니다. 존재하면서 조용히 실패하는 쪽이 아예 없는 쪽보다 고약하기 때문입니다. 떠보기가 실패하면, 크롬 자신의 이전 안내가 가리키는 폴리필인 libxslt의 WebAssembly 빌드를 불러옵니다.
- 크롬 143, 2025년 12월 2일: 공식 폐지. 콘솔과 Lighthouse에 경고.
- 크롬 145, 2025년 12월: Canary, Dev, Beta에서 XSLT가 기본 비활성.
- 크롬 146, 2026년 3월 10일: 기업 정책이라는 탈출구.
- 크롬 152, 2026년 8월 25일: 오리진 트라이얼 개시.
- 크롬 158, 2026년 11월 17일: 안정판에서 XSLT가 작동을 멈춤. 트라이얼이나 정책 아래는 예외.
- 크롬 176, 2027년 8월 17일: 두 탈출구가 모두 만료. XSLTProcessor와 xml-stylesheet 처리 명령이 사라집니다. 같은 명령의 type="text/css" 형태는 영향을 받지 않습니다.
템플릿은 규칙이지 스크립트가 아니다
스타일시트는 템플릿 규칙의 모음이고, 각 규칙에는 대응 패턴이 있습니다. 프로세서는 문서 루트에서 시작해 가장 잘 맞는 템플릿을 찾아 실행하고, 그 템플릿이 xsl:apply-templates라고 말한 자리에서 선택된 자식들에 대해 같은 과정을 되풀이합니다. 여러분이 적은 순서대로 도는 것은 아무것도 없습니다.
xsl:apply-templates는 제어를 규칙 모음에 되돌려 줍니다. 선택된 각 노드가 자기 템플릿을 찾으므로, 성질이 다른 자식들은 xsl:choose 분기를 줄줄이 잇는 대신 각자의 규칙을 얻습니다. xsl:for-each는 그 자리에서 반복하며 선택된 노드마다 하나의 본문을 적용하는데, 평평한 행 목록에는 이쪽이 더 읽기 좋습니다. 둘 다 문맥 노드를 바꾸며, 「select 식이 갑자기 틀렸다」는 신고는 대부분 여기서 나옵니다.
XSLT에는 여러분이 쓰지 않은 내장 규칙도 있습니다. 요소에 대한 기본값은 자식에 템플릿을 적용하는 것이고, 텍스트 노드에 대한 기본값은 텍스트를 그대로 통과시키는 것입니다. 이 한 쌍 때문에, 패턴이 아무것에도 맞지 않는 스타일시트조차 글자가 줄줄이 붙은 페이지를 내놓습니다.
변환이 아무것도 내놓지 않을 때
빈 결과는 글자가 줄줄이 붙는 문제 다음으로 가장 많이 신고되는 XSLT 문제이고, 압도적인 원인이 하나 있습니다. 이름공간입니다. 이름공간 없는 XML을 위해 쓴 스타일시트는 이름공간 안에 있는 요소와 맞지 않습니다. 두 파일에서 이름이 똑같아 보여도 그렇습니다. 원본 루트가 xmlns="http://example.com/orders"를 달고 있다면, <xsl:template match="order">는 {}order를 찾는데 문서는 {http://example.com/orders}order를 갖고 있습니다. 어떤 템플릿도 발화하지 않고, XSLT는 그것을 오류로 보지 않습니다.
원하는 접두사로 스타일시트에 이름공간을 선언하고, 모든 match와 모든 select에서 그것을 쓰세요. XSLT 1.0은 XPath 1.0의 한계를 공유하므로 접두사는 선택 사항이 아니며, xpath-default-namespace는 XSLT 2.0의 것이라 브라우저가 무시하므로 도움이 되지 않습니다.
version="2.0"이나 "3.0"을 선언한 스타일시트는 실행 전에 표시합니다. 두 엔진 모두 1.0이고 이후 판의 구문은 조용히 실패할 수 있어, 출력이 없는 것이 아니라 미묘하게 틀린 것이 되기 때문입니다. 그리고 스타일시트 루트가 http://www.w3.org/1999/XSL/Transform에 없으면 아무것도 작동하지 않습니다. 이 URI는 XSLT 세 판 모두에서 동일하고, 잘못 적으면 규칙이 하나도 없는 스타일시트로 컴파일됩니다.
어떤 브라우저 XSLT 엔진도 할 수 없는 일
XSLT 1.0뿐입니다. 2.0이나 3.0을 내놓은 브라우저는 한 번도 없었고 libxslt도 그것들을 구현하지 않으므로, WebAssembly 대체로도 천장은 올라가지 않습니다. 따라서 xsl:function, xsl:for-each-group(묶음은 여전히 key()를 쓰는 뮌헨식 기법), xsl:analyze-string, xsl:result-document은 쓸 수 없습니다. 브라우저에서 2.0이나 3.0으로 가는 길은 Saxon-JS뿐입니다.
외부 자원이 또 하나의 단단한 한계입니다. document()와, 원격 URL의 xsl:include·xsl:import는 동일 출처 정책의 지배를 받으므로, HTTP로 조회표를 끌어오는 스타일시트는 실패합니다. 여기서는 아무것도 여러분 대신 가져오지 않고 프록시도 없습니다. 엔진마다 다른 빈틈 몇 가지도 사람들을 걸려 넘어지게 합니다.
- disable-output-escaping은 파이어폭스에 구현되어 있지 않습니다. TransforMiiX는 직렬화된 문자열 대신 트리를 만들고, 명세는 프로세서가 이 속성을 no로 취급해 회복하는 것을 허용합니다. 대신   같은 숫자 문자 참조를 쓰세요.
- xsl:namespace-alias는 파이어폭스에서 지원되지 않으며, XPath의 namespace:: 축도 마찬가지입니다.
- xsl:output은 프로세서를 스크립트로 몰 때는 대체로 권고에 그칩니다. 결과가 DOM이라서 indent, encoding, doctype 관련 속성은 거의 효과가 없습니다. 이 페이지는 거기서 method만 읽어 텍스트와 직렬화된 마크업 중 무엇으로 낼지 정합니다.
- EXSLT 지원 범위가 다릅니다. 크롬·사파리·WebAssembly 대체가 쓰는 libxslt는 대부분을 구현하고, 파이어폭스는 일부만 구현합니다.
- transformToDocument와 transformToFragment는 실패 시 예외를 던지지 않고 null을 돌려주며, 진짜 이유는 콘솔에만 나옵니다.
코드로 하기
스타일시트는 실행되는 입력입니다. 아래의 어떤 엔진이든 document(), xsl:import, 또는 확장 함수를 통해 파일을 읽고 네트워크 연결을 열고 디스크에 쓸 수 있으므로, 각 예제는 그것을 끕니다. 직접 쓰지 않은 스타일시트는 직접 쓰지 않은 스크립트와 똑같이 대하세요.
// Native first, WebAssembly fallback. A probe, not a feature check:
// after Chrome 158 the constructor is gone, but a constructor that
// exists and fails is the case that actually misleads you.
function nativeXsltWorks() {
if (typeof XSLTProcessor === 'undefined') return false;
try {
const p = new DOMParser();
const xsl = p.parseFromString(
'<xsl:stylesheet version="1.0" ' +
'xmlns:xsl="http://www.w3.org/1999/XSL/Transform">' +
'<xsl:template match="/"><ok/></xsl:template></xsl:stylesheet>',
'application/xml');
const proc = new XSLTProcessor();
proc.importStylesheet(xsl);
const out = proc.transformToDocument(
p.parseFromString('<a/>', 'application/xml'));
return out?.documentElement?.nodeName === 'ok';
} catch {
return false;
}
}
async function transform(xmlText, xslText) {
// xslt-polyfill is libxslt compiled to WebAssembly. Importing it
// installs a replacement XSLTProcessor on globalThis.
if (!nativeXsltWorks()) await import('xslt-polyfill');
const p = new DOMParser();
const xml = p.parseFromString(xmlText, 'application/xml');
const xsl = p.parseFromString(xslText, 'application/xml');
if (xml.querySelector('parsererror') || xsl.querySelector('parsererror')) {
throw new Error('one of the two inputs is not well-formed');
}
const proc = new XSLTProcessor();
proc.importStylesheet(xsl);
proc.setParameter(null, 'lang', 'en'); // (namespaceURI, name, value)
// Returns null rather than throwing. Do not skip this check.
const out = proc.transformToDocument(xml);
if (!out) throw new Error('no template matched the document root');
return new XMLSerializer().serializeToString(out);
}from lxml import etree
# lxml wraps libxslt, the same engine Chrome and Safari use.
parser = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)
xml = etree.fromstring(xml_bytes, parser)
xsl = etree.fromstring(xsl_bytes, parser)
# DENY_ALL blocks document(), xsl:include, xsl:import and every EXSLT
# file or network operation. Without it a stylesheet is a file-read
# primitive that runs whatever its author wrote.
transform = etree.XSLT(xsl, access_control=etree.XSLTAccessControl.DENY_ALL)
try:
result = transform(xml, lang="'en'") # params are XPath: quote strings twice
except etree.XSLTApplyError as e:
raise SystemExit(f'transform failed: {e}')
# xsl:message output and warnings land here, not in the result.
for entry in transform.error_log:
print(f'{entry.line}: {entry.message}')
print(str(result)) # honours xsl:output method, encoding and indentimport javax.xml.XMLConstants;
import javax.xml.transform.*;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
TransformerFactory factory = TransformerFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
// An empty string means "no protocol is permitted", which blocks
// document(), xsl:import and xsl:include of anything outside the
// stylesheet you handed in. Neither is restricted by default.
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
// Without a listener, compilation warnings go to stderr and are lost.
factory.setErrorListener(new ErrorListener() {
public void warning(TransformerException e) {
System.err.println("warning: " + e.getMessageAndLocation());
}
public void error(TransformerException e) throws TransformerException {
throw e;
}
public void fatalError(TransformerException e) throws TransformerException {
throw e;
}
});
// Templates is compiled once and is thread-safe; Transformer is not.
Templates templates = factory.newTemplates(new StreamSource(stylesheetStream));
Transformer transformer = templates.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setParameter("lang", "en");
transformer.transform(new StreamSource(xmlStream), new StreamResult(System.out));
// The JDK's built-in processor is Xalan, XSLT 1.0. For 2.0 or 3.0 put
// Saxon-HE on the classpath; it registers itself as the factory.using System.Xml;
using System.Xml.Xsl;
var readerSettings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
};
var transform = new XslCompiledTransform();
// XsltSettings.Default disables both the document() function and
// embedded <msxsl:script>. XsltSettings.TrustedXslt enables both and
// should never be used on a stylesheet from outside your codebase.
// The null resolver blocks xsl:import and xsl:include as well.
using (var xslReader = XmlReader.Create(stylesheetPath, readerSettings))
{
transform.Load(xslReader, XsltSettings.Default, null);
}
var args = new XsltArgumentList();
args.AddParam("lang", "", "en");
using var xmlReader = XmlReader.Create(inputPath, readerSettings);
using var writer = XmlWriter.Create(Console.Out, transform.OutputSettings);
transform.Transform(xmlReader, args, writer);
// XslCompiledTransform is XSLT 1.0. Microsoft never shipped 2.0 for
// .NET; Saxonica's Saxon-HE has a .NET build if you need it.<?php
// Requires ext-xsl, which wraps libxslt.
$xml = new DOMDocument();
$xml->loadXML($xmlSource, LIBXML_NONET);
$xsl = new DOMDocument();
$xsl->loadXML($xslSource, LIBXML_NONET);
$proc = new XSLTProcessor();
// Block every filesystem and network operation a stylesheet can reach
// for. Only the write prefs are blocked by default.
$proc->setSecurityPrefs(
XSL_SECPREF_READ_FILE
| XSL_SECPREF_WRITE_FILE
| XSL_SECPREF_CREATE_DIRECTORY
| XSL_SECPREF_READ_NETWORK
| XSL_SECPREF_WRITE_NETWORK
);
$proc->importStylesheet($xsl);
$proc->setParameter('', 'lang', 'en');
$result = $proc->transformToXml($xml);
if ($result === false) {
fwrite(STDERR, "transformation failed\n");
exit(1);
}
echo $result;# xsltproc ships with libxslt: the same engine Chrome and Safari use,
# and the same one this page falls back to in WebAssembly.
# --nonet stops it fetching anything the stylesheet references.
xsltproc --nonet style.xsl doc.xml > out.html
# --stringparam passes a string. --param takes an XPath expression, so
# a literal string needs its own quotes inside the shell quotes.
xsltproc --nonet --stringparam lang en style.xsl doc.xml
xsltproc --nonet --param year "'2026'" style.xsl doc.xml
# xsl:message output goes to stderr, so separate it before piping.
xsltproc --nonet style.xsl doc.xml 2> messages.log | xmllint --format -
# XSLT 2.0 and 3.0 need Saxon. libxslt will never implement them.
java -cp saxon-he.jar net.sf.saxon.Transform \
-s:doc.xml -xsl:style.xsl -o:out.html lang=en매개변수 문법은 이들 모두에서 다르고, 겉모습만의 차이도 아닙니다. lxml, xsltproc의 --param, Saxon에서는 매개변수 값이 XPath 식입니다. 그래서 문자열 en은 "'en'"으로 써야 하며, 그러지 않으면 en이라는 이름의 요소를 고르는 경로 단계로 읽힙니다. 자바스크립트의 setParameter와 .NET의 XsltArgumentList는 그냥 문자열을 받습니다.
자주 묻는 질문
크롬이 XSLT를 제거하면 이 도구는 멈추나요?
아닙니다. 2026년 11월 17일의 크롬 158에서 안정판의 XSLT가 작동을 멈추고, 2027년 8월 17일의 크롬 176에서 XSLTProcessor와 xml-stylesheet 처리 명령이 아예 제거됩니다. 네이티브 프로세서만으로 만든 도구라면 그 날짜에 깨집니다.
이 페이지는 먼저 한 줄짜리 떠보기 변환을 돌립니다. 여러분의 브라우저에 작동하는 네이티브 엔진이 있는 동안에는 그것을 쓰고, 떠보기가 실패하면 대신 libxslt의 WebAssembly 빌드를 불러온 뒤 어느 엔진이 돌았는지 패널에 적습니다. 대체는 필요할 때만 받아 오므로, 파이어폭스와 사파리 사용자는 내려받는 일이 없습니다.
여기서 XSLT 2.0이나 3.0을 돌릴 수 있나요?
할 수 없고, 어떤 브라우저도 할 수 없습니다. 크롬과 사파리는 libxslt를, 파이어폭스는 자사 TransforMiiX를 쓰며, 셋 다 1.0 구현입니다. WebAssembly 대체도 libxslt라서 천장은 그대로입니다.
스타일시트가 version="2.0"이나 "3.0"을 선언하면 이 페이지는 실행 뒤가 아니라 앞에서 경고합니다. 1.0 프로세서는 2.0 스타일시트를 단칼에 거부하지 않고, 아는 것만 실행한 뒤 나머지를 조용히 잘못 다루기 때문입니다. 현실적인 답은 Saxon입니다. 브라우저에서는 Saxon-JS, JVM이나 .NET에서는 Saxon-HE.
스타일시트 결과가 왜 비어 있나요?
거의 언제나 이름공간 불일치입니다. 원본이 루트에 기본 이름공간을, 예컨대 xmlns="http://example.com/orders"를 선언하면, 그 안의 접두사 없는 모든 요소가 그 이름공간에 속합니다. <xsl:template match="order">로 쓴 템플릿은 이름공간 없는 order 요소를 찾으므로 어떤 규칙도 발화하지 않고 출력은 비게 됩니다. XSLT는 그것을 오류로 취급하지 않습니다.
스타일시트에 접두사와 함께 이름공간을 선언하고(예컨대 xmlns:o="http://example.com/orders"), 어디서나 match="o:order"와 select="o:line"으로 쓰세요. 배제해 둘 원인이 둘 더 있습니다. 스타일시트 루트가 http://www.w3.org/1999/XSL/Transform에 없는 경우, 그리고 문서 루트에 맞는 템플릿이 없는 경우입니다.
제 XML과 스타일시트가 업로드되나요?
둘 다 아닙니다. 두 창은 이 탭에 머뭅니다. 정형식 검사는 웹 워커에서 돌고, 변환은 브라우저 내장 프로세서나 페이지에 불러온 WebAssembly 모듈에서 돕니다. 여기에는 그것을 받을 서버가 없습니다.
이 질문은 다른 대부분의 도구보다 XSLT에서 더 날카롭습니다. 값나가는 쪽이 흔히 스타일시트이기 때문입니다. 협력사의 스키마를 내부 스키마로 옮기는 변환에는 업무 규칙, 필드 이름, 때로는 엔드포인트가 적혀 있습니다. 로컬에서 돈다는 점의 한 가지 결과로, 출력은 텍스트로 보여 주고 결코 마크업으로 페이지에 끼워 넣지 않습니다. 스타일시트가 script 요소를 만들어 낼 수 있기 때문입니다.
출력에 문서의 텍스트가 전부 나오고 마크업은 없는 이유는 무엇인가요?
XSLT의 내장 템플릿 규칙에 부딪힌 것입니다. 명세는 여러분이 쓰든 쓰지 않든 존재하는 기본값을 정의합니다. 요소에 대한 규칙은 자식에 템플릿을 적용하고, 텍스트 노드에 대한 규칙은 텍스트를 곧바로 통과시킵니다.
그래서 여러분의 템플릿이 하나도 맞지 않을 때 프로세서는 멈추지 않습니다. 그 기본값으로 트리 전체를 걸어 다니며 만나는 텍스트 노드를 원본의 들여쓰기까지 포함해 모두 찍어 냅니다. 원인은 보통 이름공간입니다. 빠른 진단은 <xsl:template match="text()"/>를 넣어 보는 것입니다. 텍스트 기본값을 억누르므로, 이때 출력이 비면 여러분의 규칙은 하나도 발화하지 않았던 것입니다.
apply-templates와 for-each의 차이는 무엇인가요?
xsl:for-each는 그 자리에서 반복합니다. 노드 목록을 고르고 문서 순서대로 노드마다 한 번씩 본문을 돌리는데, 그 본문은 고정이므로 모든 노드가 똑같이 다뤄집니다. 루프처럼 읽히고, row 요소를 모두 표의 행으로 바꾸는 것처럼 평평하고 동질적인 목록에 맞습니다.
xsl:apply-templates는 제어를 규칙 모음에 되돌려, 선택된 각 노드가 자기에게 가장 잘 맞는 템플릿을 찾게 합니다. 그래서 요소 종류가 다르면 조건문 없이 처리도 달라집니다. 또 mode 속성을 받으므로 같은 노드를 다른 규칙으로 두 번, 목차용으로 한 번, 본문용으로 한 번 처리할 수 있습니다. for-each에는 이에 해당하는 것이 없습니다.
스타일시트가 document()나 xsl:import로 다른 파일을 불러올 수 있나요?
여기서는 안 됩니다. document(), xsl:include, xsl:import는 모두 URL을 해석하고, 브라우저에서는 그 불러오기가 동일 출처 정책의 대상이므로 원격인 것은 전부 막힙니다. 이 페이지에는 가져오기 프록시도 일부러 두지 않았습니다. 프록시를 둔다는 것은 대상 문서와 URL 자체가 이 사이트가 관리하는 서버를 지나간다는 뜻이기 때문입니다.
두 번째 문서를 원본에 끼워 넣고 평범한 XPath로 고르거나, 조회표를 스타일시트 매개변수로 옮기거나, xsltproc으로 로컬에서 변환을 돌리세요. 거기서는 document()가 여러분의 파일 시스템을 기준으로 해석됩니다.