복사했더니 빈 값 — innerText는 화면에 보이는 것만 읽는다
content-visibility:auto 로 접어 둔 긴 글을 innerText 로 읽으면 화면 밖 부분이 렌더되지 않아 빈 문자열이 복사됩니다. display:none 은 전문을 돌려주니, 데이터 그대로가 필요하면 textContent 를 씁니다.
상황
접었다 펼치는 긴 글이 있습니다. 화면에는 요약본이 보이고, "더보기"를 누르면 전문이 나타납니다. 요약본과 전문을 둘 다 DOM에 넣어두고 토글하는 흔한 형태입니다.
<div id="full" style="content-visibility:auto">…전문 5000자…</div>
<div id="summary">…앞부분 200자… <button>더보기</button></div>여기에 "본문 복사" 버튼을 붙였습니다. 전문을 복사해야 하니 당연히 #full을 읽습니다.
const text = document.querySelector('#full').innerText;
navigator.clipboard.writeText(text);짧은 글은 잘 됩니다. 그런데 긴 글에서만 복사 결과가 빈 값이었습니다. 에러는 없습니다. 클립보드에 그냥 아무것도 안 들어갑니다.
문제
innerText와 textContent는 이름이 비슷하지만 읽는 대상이 다릅니다.
textContent— DOM 트리의 텍스트 노드를 그대로 잇습니다. 렌더링과 무관합니다.innerText— "사람이 화면에서 읽는 대로"의 텍스트를 돌려줍니다. 그래서 렌더 결과에 의존합니다: 렌더된 요소 안에 숨겨진 자식이 있으면 그 부분을 건너뜁니다. 다만 요소 자신이 렌더되지 않는 경우(display:none, 문서에서 분리)는 예외라, 명세가textContent를 그대로 돌려주도록 정해 두었습니다.
접힌 #full 이 렌더 트리에서 빠져 있었기 때문입니다. innerText 는 렌더 결과를 읽으므로, 브라우저가 그 서브트리를 그리지 않았으면 읽을 것도 없습니다. 명세대로 동작한 것이지 버그가 아닙니다.
주의할 점은 display:none 은 이 증상을 만들지 않는다는 것입니다. 명세는 "렌더되지 않는 요소를 직접 읽으면 textContent 를 반환한다"고 정해 두었고, 실제로 헤드리스 크롬으로 확인해도 전문이 그대로 나옵니다. 빈 문자열이 나오는 건 따로 있습니다.
| 숨기는 방법 | innerText |
|---|---|
display:none (자신 또는 조상) | 전문 그대로 |
hidden 속성 | 전문 그대로 |
visibility:hidden | 빈 문자열 |
content-visibility:hidden | 빈 문자열 |
content-visibility:auto + 화면 밖 | 빈 문자열 |
닫힌 <details> 안 | 빈 문자열 |
"긴 글에서만" 이라는 조건이 범인을 좁혀 줍니다. content-visibility:auto 는 화면 밖 서브트리의 렌더를 건너뛰는 성능 최적화라, 짧은 글은 전부 화면에 들어와 멀쩡하고 긴 글만 뒷부분이 통째로 비어 나옵니다. 닫힌 <details> 로 접는 구현도 같은 결과를 냅니다.
짧은 글에서 됐던 건 우연입니다. 짧으면 접기 자체가 안 일어나 #full이 처음부터 펼쳐져(display 정상) 있었을 뿐입니다. "됐다"가 조건부였는데 그걸 몰랐습니다.
이 실패에는 신호가 없습니다. 예외도, 콘솔 경고도 없습니다. 클립보드에 빈 문자열이 정상적으로 쓰입니다. 사용자가 다른 곳에 붙여넣기 전까지 아무도 모릅니다.
왜 innerText가 이렇게 동작하나
innerText는 원래 "선택해서 복사하면 나올 법한 텍스트"를 의도한 API입니다. 그래서 레이아웃을 반영합니다 — 렌더되지 않은 자식은 빼고, <br>이나 블록 요소의 줄바꿈은 \n으로 넣고, text-transform: uppercase까지 반영합니다. 그 대가로 읽는 순간 레이아웃 계산(reflow)을 유발하고, 렌더 상태가 값에 새어듭니다.
textContent는 정반대입니다. DOM에 있는 글자를 그대로 줍니다. 숨겨져 있든 화면 밖이든 상관없습니다. 대신 <br>을 줄바꿈으로 바꿔주지 않습니다(그냥 사라집니다).
즉 **"화면에 보이는 대로"가 필요하면 innerText, "데이터에 있는 대로"가 필요하면 textContent**입니다. 숨긴 요소에서 값을 꺼내는 건 후자의 일입니다.
해결
숨겨져 있을 수 있는 요소에서 텍스트를 읽을 때는 innerText를 쓰지 않습니다.
// display 상태와 무관하게 읽힌다
const text = el.textContent;단, textContent는 <br>이나 블록 경계의 줄바꿈을 살려주지 않습니다. 줄바꿈이 의미가 있는 본문이라면 노드를 직접 훑어 <br>과 블록 요소를 \n으로 바꿔야 합니다.
function readText(root) {
let out = '';
root.childNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE) out += node.textContent;
else if (node.nodeName === 'BR') out += '\n';
else if (node.nodeType === Node.ELEMENT_NODE) {
out += readText(node);
if (getComputedStyle(node).display === 'block') out += '\n';
}
});
return out;
}숨긴 요소를 잠깐 보이게 했다가 innerText로 읽고 되돌리는 방법도 이론상 되지만, reflow를 강제로 두 번 일으키고 그 사이 화면이 깜빡일 수 있어 권하지 않습니다.
정리
innerText는 렌더 결과를 읽습니다. 브라우저가 그리지 않은 서브트리(content-visibility, 닫힌<details>,visibility:hidden)는 빈 문자열이 됩니다. 반대로display:none요소를 직접 읽으면 전문이 그대로 나옵니다 — 헷갈리기 쉬운 지점입니다.textContent는 DOM의 글자를 읽습니다. 숨김과 무관하지만 줄바꿈을 안 살립니다.- 접기/펼치기처럼 요소가 숨겨질 수 있는 곳에서 값을 꺼낼 때
innerText를 쓰면, "숨겨진 항목만 조용히 빈 값"이 됩니다. 에러가 없어 오래 숨습니다. - "짧은 입력에서는 됐다"는 통과 조건이 우연일 수 있습니다. 됐을 때 왜 됐는지를 확인하지 않으면, 안 되는 경우가 나중에 사용자 쪽에서 터집니다.