---
title: "복사했더니 빈 값 — innerText는 화면에 보이는 것만 읽는다"
description: "innerText는 화면에 보이는 텍스트만 읽어 숨겨진 요소에서 빈 값을 반환하므로, 데이터 그대로를 가져오려면 textContent를 사용해야 합니다"
date: 2026-08-27
updated: 2026-08-27T02:58:08.394Z
tags: [frontend, dom, javascript, debugging]
canonical: https://blog.wooncloud.com/posts/innertext-hidden-element-empty-copy
---

![접힌 문서 — 윗면만 보이고 접힌 안쪽은 숨겨져 있다](/images/posts/innertext-hidden-element-empty-copy/8b3c9a36-a67c-4c2a-bd59-a551f496b208.webp)

## 상황

접었다 펼치는 긴 글이 있다. 화면에는 요약본이 보이고, "더보기"를 누르면 전문이 나타난다. 마크업은 흔한 형태다 — 요약본과 전문을 **둘 다 DOM에 넣어두고 `display`로 토글**한다.

```html
<div id="full" style="display:none">…전문 5000자…</div>
<div id="summary">…앞부분 200자… <button>더보기</button></div>
```

여기에 "본문 복사" 버튼을 붙였다. 전문을 복사해야 하니 당연히 `#full`을 읽는다.

```js
const text = document.querySelector('#full').innerText;
navigator.clipboard.writeText(text);
```

짧은 글은 잘 된다. 그런데 **긴 글에서만 복사 결과가 빈 값**이었다. 에러는 없다. 클립보드에 그냥 아무것도 안 들어간다.

## 문제

`innerText`와 `textContent`는 이름이 비슷하지만 읽는 대상이 다르다.

- **`textContent`** — DOM 트리의 텍스트 노드를 그대로 잇는다. 렌더링과 무관하다.
- **`innerText`** — **"사람이 화면에서 읽는 대로"의 텍스트**를 돌려준다. 그래서 렌더 결과에 의존한다: CSS로 숨긴 요소는 건너뛰고, `display:none`인 요소는 **빈 문자열**을 반환한다.

`#full`은 접힌 상태에서 `display:none`이다. 그래서 `innerText`가 빈 값을 준 것이다. **화면에 안 보이니 "읽을 텍스트가 없다"고 판단한 것** — 명세대로 동작한 것이지 버그가 아니다.

짧은 글에서 됐던 건 우연이다. 짧으면 접기 자체가 안 일어나 `#full`이 처음부터 펼쳐져(`display` 정상) 있었을 뿐이다. **"됐다"가 조건부였는데 그걸 몰랐다.**

이 실패에는 신호가 없다. 예외도, 콘솔 경고도 없다. 클립보드에 빈 문자열이 정상적으로 쓰인다. 사용자가 다른 곳에 붙여넣기 전까지 아무도 모른다.

## 왜 `innerText`가 이렇게 동작하나

`innerText`는 원래 "선택해서 복사하면 나올 법한 텍스트"를 의도한 API다. 그래서 레이아웃을 반영한다 — `display:none`은 빼고, `<br>`이나 블록 요소의 줄바꿈은 `\n`으로 넣고, `text-transform: uppercase`까지 반영한다. 그 대가로 **읽는 순간 레이아웃 계산(reflow)을 유발**하고, **렌더 상태가 값에 새어든다.**

`textContent`는 정반대다. DOM에 있는 글자를 그대로 준다. 숨겨져 있든 화면 밖이든 상관없다. 대신 `<br>`을 줄바꿈으로 바꿔주지 않는다(그냥 사라진다).

즉 **"화면에 보이는 대로"가 필요하면 `innerText`, "데이터에 있는 대로"가 필요하면 `textContent`**다. 숨긴 요소에서 값을 꺼내는 건 후자의 일이다.

## 해결

숨겨져 있을 수 있는 요소에서 텍스트를 읽을 때는 `innerText`를 쓰지 않는다.

```js
// display 상태와 무관하게 읽힌다
const text = el.textContent;
```

단, `textContent`는 `<br>`이나 블록 경계의 줄바꿈을 살려주지 않는다. 줄바꿈이 의미가 있는 본문이라면 노드를 직접 훑어 `<br>`과 블록 요소를 `\n`으로 바꿔야 한다.

```js
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`는 **렌더 결과**를 읽는다. `display:none` 요소에서는 빈 문자열이다.
- `textContent`는 **DOM의 글자**를 읽는다. 숨김과 무관하지만 줄바꿈을 안 살린다.
- 접기/펼치기처럼 **요소가 숨겨질 수 있는 곳에서 값을 꺼낼 때 `innerText`를 쓰면**, "숨겨진 항목만 조용히 빈 값"이 된다. 에러가 없어 오래 숨는다.
- "짧은 입력에서는 됐다"는 통과 조건이 우연일 수 있다. 됐을 때 **왜 됐는지**를 확인하지 않으면, 안 되는 경우가 나중에 사용자 쪽에서 터진다.
