← 목록으로

'스포 주의!' Threads 앱처럼 반짝이는 스포일러, 웹에서 직접 만들어보기

2026. 4. 21.

'스포 주의!' Threads 앱처럼 반짝이는 스포일러, 웹에서 직접 만들어보기

Meta의 Threads 모바일 앱을 사용해본 분이라면 한 번쯤 눈길을 사로잡았던 효과가 있을 겁니다. 바로 텍스트를 반짝이는 베일 뒤에 숨기는 '스포일러 태그' 말이죠. 오늘은 이 멋진 효과를 HTML, CSS, 그리고 JavaScript를 활용해 웹 브라우저에서 어떻게 구현하는지, 저의 10년 실무 경험을 살려 자세히 알려드리겠습니다.

💡 참고: 전체 글을 읽기 전에 완성된 코드만 빠르게 확인하고 싶다면, 글의 맨 아래로 스크롤하시면 HTML, CSS, JavaScript 코드를 바로 찾아볼 수 있습니다.

Threads의 스포일러 효과, 짧게 알아보기

'스포일러' 태그가 어떤 기능인지 아직 익숙하지 않으신가요? Threads에서는 사용자가 게시물의 일부를 선택적으로 가려두었다가, 다른 사용자가 원할 때만 해당 내용을 볼 수 있도록 하는 기능입니다. 아래 제가 "Highlight the word “hello” to mark as a spoiler!"라고 게시물을 작성하는 예시를 보세요.

"hello"라는 단어를 드래그하자마자 "Mark spoiler" 버튼이 나타나는 것을 볼 수 있습니다. 이 버튼을 클릭하고 게시물을 올리면 제 데스크톱 브라우저에서는 이렇게 보이죠.

"hello"라는 단어가 회색 막대로 가려져 있습니다. 만약 이 게시물을 읽는 사람이 제가 숨긴 단어를 보고 싶다면, 회색 막대를 클릭해야 합니다.

위 스크린샷은 독자가 회색 막대를 클릭한 후 보게 될 화면입니다. 이제 제가 숨겨두었던 단어를 명확하게 확인할 수 있습니다.

간단히 말해, Threads의 '스포일러 마크'는 이렇게 작동합니다. 작성자가 특정 텍스트를 선택해 숨기고, 사용자가 의도적으로 클릭하는 행동을 통해 숨겨진 텍스트를 드러내는 방식이죠. 이 기능의 주된 목적은 영화나 소설의 반전, 결말 등을 이야기할 때, 아직 해당 내용을 접하지 못한 독자들에게 '스포일러'가 되지 않도록 보호하는 데 있습니다. 사용자가 수동적으로 스포일러를 읽는 것을 방지하고, 숨겨진 텍스트를 의도적으로 드러내도록 유도하는 거죠.

그런데 이 스포일러 바는 모바일 앱에서 보면 훨씬 더 재미있습니다. 다음 스크린샷은 동일한 게시물이지만, Threads 모바일 앱에서 본 모습입니다.

회색 막대 대신 반짝이는 흐림 효과가 적용된 것을 확인할 수 있습니다. 정말 멋지지 않나요? 이 반짝이는 스포일러 효과가 바로 오늘 우리가 구현할 내용입니다.

진짜 스포일러 주의!

이 글에서는 영화 "스타워즈: 에피소드 V – 제국의 역습"의 결정적인 반전에 대해 이야기할 예정입니다. 1980년에 개봉한 영화지만, 아직 보지 못했거나 줄거리를 모르는 분들에게는 혹시라도 깜짝 놀랄 만한 내용을 망치고 싶지 않습니다. 만약 이 영화를 볼 계획이 있고 줄거리를 전혀 모른다면, 이 글은 확실히 당신에게 스포일러가 될 것입니다!

계속 읽을지 말지는 당신의 선택입니다!

이 효과를 구현하는 방법

기술적인 세부 사항에 들어가기 전에, 먼저 이 효과를 어떻게 구현할지 계획을 세워보겠습니다. 우리가 해야 할 일은 다음과 같습니다.

  1. 텍스트를 스포일러로 표시하기
  2. 반짝이는 효과 만들기
  3. 적절하게 태그된 텍스트에 효과 적용하기
  4. 사용자가 클릭하면 효과 제거하기

자, 시작해볼까요!

텍스트를 스포일러로 표시하기

먼저 spoiler_text.html이라는 HTML 파일을 만들고, 기본적인 HTML 상용구 코드를 채워 넣습니다.

<body>
    <p>
        The biggest plot twist of all time is:
        <span class="spoiler">
            Darth Vader is Luke Skywalker's father!
        </span>
    </p>
</body>

이것은 사상 최고의 반전에 대한 텍스트를 추가한 아주 기본적인 HTML 페이지입니다. 지금은 페이지가 너무 단순해서 브라우저에서는 이렇게 보일 겁니다.

Threads의 배경과 텍스트가 대체로 어두운 테마인 만큼, 우리 HTML 페이지에도 그 테마를 적용해봅시다. 스포일러 효과의 일부는 아니지만, 나중에 보기 좋게 만드는 작업이죠. style.css 파일을 만들고 페이지를 좀 더 눈에 편하게 만듭니다. 제가 작성한 코드는 다음과 같습니다.

:root {
  --bg: #0f1115;
  --text: #f4f7fb;
}

body {
  min-height: 25vh;
  display: grid;
  place-items: center;
  background: var(--bg);
  color: var(--text);
}

먼저 :root 규칙을 만들어 몇 가지 CSS 변수를 저장했습니다. 지금 당장은 bgtext 두 개의 변수만 있지만, 프로젝트가 진행되면서 더 많은 변수가 생길 것이고, 모두 한 곳에 모아두는 것이 좋다고 생각했습니다. 그래서 미리 :root 규칙에 변수를 넣는 습관을 들이는 거죠. 제가 실무에서 프로젝트를 시작할 때 늘 염두에 두는 부분이죠. 작은 디테일 같지만, 나중에 유지보수나 확장에 큰 도움이 됩니다.

다음으로 웹 페이지의 min-height, display, place-items 값을 설정했습니다. 위 CSS에 추가된 특정 값들은 텍스트가 브라우저 창의 중앙에서 약간 아래쪽에 나타나도록 만듭니다. 마지막으로 bgtext 변수를 사용해 웹 페이지의 배경색과 글자색을 지정했습니다. 이제 HTML 파일의 <title> 태그 바로 아래에 다음 한 줄을 추가합니다.

<link rel="stylesheet" href="style.css" />

그러면 브라우저 화면은 이렇게 바뀔 것입니다.

bgtext 변수를 조절하여 페이지가 여러분의 미적 감각에 맞게 보기 좋도록 변경해도 좋습니다.

이제 스포일러 텍스트를 표시해야 합니다. 일단 스포일러 부분을 spoiler 클래스의 span 태그 안에 넣을 것입니다. 이렇게 한다고 해서 아직 시각적으로 어떤 변화가 생기는 것은 아닙니다. 효과를 만들기 전에 스포일러 텍스트를 미리 표시해두는 작업이죠. HTML 파일의 body 태그는 이제 이렇게 보일 것입니다.

<body>
    <p>
        The biggest plot twist of all time is:
        <span class="spoiler">
            Darth Vader is Luke Skywalker's father!
        </span>
    </p>
</body>

이제 효과를 만들어 봅시다!

효과 만들기

효과를 만들기 위해, 우선 태그된 텍스트를 흐리게 만드는 간단한 CSS 코드를 작성하는 작은 단계부터 시작하겠습니다. style.css 파일에 spoiler 클래스에 대한 규칙을 추가해야 합니다. 내장 CSS blur 함수를 활용하고 커서를 포인터로 변경할 것입니다. 제가 작성한 spoiler 클래스 CSS는 다음과 같습니다.

.spoiler {
  filter: blur(7px);
  cursor: pointer;
  position: relative;
}

이 CSS 코드를 통해 spoiler 클래스의 filter 속성을 blur(7px)로, cursor 속성을 pointer로 설정했습니다. blur 함수 이름에서 짐작할 수 있듯이, 이 함수를 filter 값으로 사용하면 요소가 흐려집니다. 함수에 전달하는 숫자는 흐림의 반경을 정의하며, 숫자가 높을수록 흐림 효과가 커집니다. cursor 속성을 pointer로 설정하여 사용자가 효과 위에 마우스를 올리면 커서가 포인터로 바뀌어 클릭하면 어떤 동작이 있을 것임을 알려줍니다.

추가로 position 속성을 relative로 설정했습니다. 이는 span 요소가 페이지의 다른 요소들과 간섭하지 않도록 하기 위함입니다.

새로운 CSS가 적용되면 브라우저의 페이지는 이제 이렇게 보일 것입니다.

위 스크린샷은 filter: blur(7px) 라인 덕분에 스포일러 텍스트가 흐릿하게 처리된 것을 보여줍니다.

반짝이 애니메이션과 토글 기능을 최종적으로 구축하기 전에, HTML 구조에 몇 가지 조정을 가해야 합니다. 사실 이 스포일러 span 내부에 두 개의 span 요소를 더 넣어야 합니다. 하나는 콘텐츠를 감싸고, 다른 하나는 흐림 효과를 담는 역할을 할 것입니다. 이 단계는 반짝이 애니메이션과 함께 토글 효과를 구현하는 것을 훨씬 쉽게 만들어 줄 것입니다.

저는 spoiler-content라는 span 클래스 하나와 spoiler-blur라는 span 클래스 하나를 만들 것입니다. spoiler-content span은 텍스트를 감쌀 것이고, spoiler-blur span은 아무것도 감싸지 않고 그저 spoiler span 안에 위치할 것입니다. 이제 body 태그 안의 HTML은 이렇게 생겼습니다.

<p>
    The biggest plot twist of all time is:
    <span class="spoiler" data-hidden="true">
        <span class="spoiler-content">
            Darth Vader is Luke Skywalker's father!
        </span>
        <span class="spoiler-blur" />
    </span>
</p>

그리고 CSS는 이제 다음과 같이 조정되어야 합니다.

.spoiler {
  cursor: pointer;
  position: relative;
}

.spoiler[data-hidden="true"] .spoiler-content {
  filter: blur(7px);
}

이 업데이트를 통해 저는 data-hidden 속성을 spoiler span"true"로 설정하고, spoiler span 내부에 있는 모든 spoiler-content span에 흐림 효과를 적용하는 CSS 규칙을 만들었습니다. 이 속성은 나중에 효과의 토글 기능을 만들 때 활용할 것입니다.

이제 반짝이는 효과를 추가할 준비가 되었습니다.

반짝이는 효과 추가하기

반짝이는 효과를 추가하기 위해 우리는 두 가지를 활용할 것입니다. 바로 CSS keyframes와 약간의 JavaScript죠. 전반적인 전략은 이렇습니다.

  1. 각 개별 sparkle-blur span을 스포일러 텍스트 위에 덮는 CSS spark 클래스를 만듭니다.
  2. JavaScript를 사용해 spark 요소 여러 개를 spoiler-blur span의 자식으로 추가합니다.
    1. 난수를 생성하는 함수를 만들 것입니다.
    2. 전반적인 "반짝이 효과"를 만들기 위해 CSS 애니메이션/키프레임에서 사용될 몇 가지 커스텀 속성을 임의의 값(위에서 언급한 난수 함수에서 나온 값)으로 요소에 설정할 것입니다.
  3. 스파크가 반짝이는 것처럼 보이게 하는 twinkle CSS 키프레임을 만듭니다.
  4. 스파크가 움직이는 것처럼 보이게 하는 drift CSS 키프레임을 만듭니다.

먼저, spark 클래스를 필요한 몇 가지 속성만으로 만들어 봅시다. 저는 다음과 같이 정의했습니다.

.spark {
  position: absolute;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--spoiler-dot);
}

position: absolute는 개별 반짝임이 오버레이(spoiler span) 안에서 다른 요소들의 위치를 방해하지 않고 공간을 차지할 수 있도록 합니다. width, height, border-radius 세 줄은 반짝임을 완벽한 원으로 만듭니다. 만약 사각형을 선호한다면 border-radius 줄을 제거할 수 있고, 수직선을 원한다면 widthheight 속성을 조절해보세요. background:root 규칙에 설정할 수 있는 CSS 변수를 사용하며, 저는 다음과 같이 흰색 계열의 효과로 설정했습니다. --spoiler-dot: rgba(255,255,255,0.85);

이 요소는 우리의 반짝이는 스포일러 효과에서 각 개별 "반짝임"의 스타일을 정의합니다. 우리는 여러 개의 sparkle 요소를 무작위 크기로 만들고 spoiler-blur span 요소의 무작위 위치에 추가해야 합니다. sparkle.js라는 JavaScript 파일을 만들고 HTML의 body 태그 안에 다음과 같이 가져옵니다.

<script src="sparkle.js"></script>

NOTE: 이 줄은 HTML의 body 태그 안에 있어야 합니다. head 태그에 넣으면 작동하지 않습니다.

이제 sparkle.js 파일에서, 반짝이 효과를 얻기 위해 무작위성에 의존할 것이므로, 무작위 값을 반환하는 편의 함수를 만드는 것으로 시작하겠습니다. 저는 random 함수가 rscale 변수를 받아 Math.random 함수에 의해 생성된 숫자에 곱하도록 작성했습니다.

function random(rscale) {
    return Math.random() * rscale;
}

다음으로, buildSparkles라는 함수를 만들 것입니다. 이 함수는 span 요소와 생성할 반짝임의 개수를 인자로 받습니다. 우리가 생성할 각 반짝임은 시작 2차원 좌표(left와 top 속성을 나타내는 x와 y), 끝 2차원 좌표, 반짝임이 이 두 좌표 사이를 이동하는 데 걸리는 시간, 반짝임이 "반짝이는" 데 걸리는 시간, 그리고 크기를 제어하는 scale 변수로 생성될 것입니다. 이 모든 값은 각 반짝임마다 무작위로 생성됩니다.

sparkle 요소가 생성되면, 이 요소를 함수에 전달된 spoiler-blur span에 추가할 것입니다. 제가 함수를 작성한 방식은 다음과 같습니다.

function buildSparkles(spoilerBlurSpan, sparkleCount) {
    for (let i = 0; i < sparkleCount; i++) {
        const s = document.createElement("span");
        s.className = "spark";
        const x0 = random(100);
        const y0 = random(100);
        const x1 = Math.max(0, Math.min(100, x0 + random(18)));
        const y1 = Math.max(0, Math.min(100, y0 + random(14)));
        s.style.left = `${x0}%`;
        s.style.top = `${y0}%`;
        s.style.setProperty("--x0", "0px");
        s.style.setProperty("--y0", "0px");
        s.style.setProperty("--x1", `${x1 - x0}px`);
        s.style.setProperty("--y1", `${y1 - y0}px`);
        s.style.setProperty("--dur", `${random(60)}s`);
        s.style.setProperty("--twinkle", `${random(1.2)}s`);
        s.style.setProperty("--scale", `${random(1.5)}`);

        spoilerBlurSpan.appendChild(s)
    }
}

함수 시그니처에서 spoilerBlurSpan을 첫 번째 인자로 받습니다. 이것은 웹 페이지에서 스포일러 텍스트를 포함하고 있다고 표시된 span이 될 것입니다. 또한 sparkleCount도 받는데, 이는 하나의 span에 넣을 반짝임의 개수입니다. 그런 다음 for 루프에 진입합니다.

for 루프의 각 반복은 하나의 spark 요소를 생성합니다. 처음 두 줄은 span 요소를 생성하고 spark 클래스 이름을 할당합니다.

다음으로 x0, y0, x1, y1을 설정합니다. 이 변수들은 기본적으로 두 세트의 무작위 x, y 좌표입니다. 나중에 drift라는 CSS 애니메이션을 만들어 sparkle이 이 두 좌표 사이를 이동하도록 할 것입니다.

lefttop을 설정하는 줄은 방금 생성한 무작위 x, y 좌표에 span 요소를 배치하는 것입니다. 다음 네 줄의 setProperty는 방금 생성한 sparkle spanx0, y0, x1, y1의 속성과 초기 값을 할당하는 것입니다. 이러한 속성의 필요성은 CSS 애니메이션을 만들 때 곧 명확해질 것입니다.

마지막으로, 마지막 세 줄을 사용하여 dur, twinkle, scale이라는 속성을 설정합니다. 곧 만들 CSS 애니메이션에서 사용될 이 속성들과 초기 값을 설정해야 합니다. dur 속성은 sparkle이 두 x,y 좌표 사이를 이동하는 데 걸리는 시간을 제어하는 데 사용될 것입니다. twinkle 속성은 각 반짝임이 "반짝이는" 속도(낮은 불투명도와 높은 불투명도 사이를 전환하는 속도)를 제어할 것입니다. 마지막으로 scale 속성은 각 반짝임의 크기에만 영향을 미칠 것입니다.

이 모든 속성을 설정한 후, 내장 appendChild 메서드를 사용하여 새로 생성된 sparkle span을 함수 시그니처에서 받은 spoilerBlurSpan에 추가합니다.

마지막으로 sparkle.js 파일을 몇 줄 더 추가하여 마무리합니다.

function wireSpoiler(spoilerBlurSpan) {
    buildSparkles(spoilerBlurSpan, 75);
}

document.querySelectorAll(".spoiler-blur").forEach(wireSpoiler);

wireSpoiler라는 함수를 하나 더 만듭니다. 이 함수는 spoilerBlurSpan을 인자로 받아 buildSparkles 함수에 전달하며, 생성할 반짝임의 개수(위 예시에서는 75개)도 함께 보냅니다.

마지막으로, querySelectorAll을 사용하여 웹 페이지의 spoiler-blur 클래스를 가진 모든 span을 가져와 wireSpoiler 메서드에 전달합니다. 이렇게 하면 비록 우리 페이지에는 하나의 스포일러 span만 있지만, 페이지의 모든 spoiler span이 반짝이 효과를 얻게 됩니다.

이제 CSS 차례입니다.

먼저, @keyframes를 사용하여 애니메이션을 만들어 봅시다. drifttwinkle 두 가지를 만들 것입니다. 이 CSS를 style.css 파일의 맨 아래에 추가합니다.

@keyframes drift {
  from {
    transform: translate(var(--x0), var(--y0)) scale(var(--scale));
  }
  to {
    transform: translate(var(--x1), var(--y1)) scale(var(--scale));
  }
}

@keyframes twinkle {
  from { opacity: 0.25; }
  to   { opacity: 0.95; }
}

drift 키프레임은 요소를 주어진 위치로 이동시키는 CSS translate 함수를 활용합니다. 이것은 나중에 애니메이션 속성에서 사용될 것이므로, 요소가 이동해야 할 두 위치를 정의하기 위해 fromto 속성을 부여합니다. 이 경우 우리는 x0y0 변수를 사용하여 sparkle의 시작점을 정의하고, x1y1을 끝점으로 사용합니다. 기억하시겠지만, 이 값들은 앞서 JavaScript 파일에서 무작위로 생성했으므로, spoiler-blur 요소에 연결하는 각 spark는 자신만의 시작점과 끝점을 가질 것입니다.

twinkle 키프레임은 스파크 요소들이 주기적으로 변할 불투명도(opacity) 값을 정의합니다. 불투명도는 요소의 투명도 수준을 나타내므로, 낮은 값과 높은 값 사이를 오가게 하면 요소가 반짝이는 듯한 착시 효과를 줍니다. 제가 처음 CSS 애니메이션과 자바스크립트를 연동했을 때 가장 신기했던 부분이 바로 이런 랜덤성을 활용하는 거였어요. 실제 서비스에서는 이런 무작위성이 사용자 경험에 의외의 재미를 주기도 하죠.

이제 spark 요소의 CSS를 업데이트하여 이 키프레임들을 애니메이션으로 포함시킵니다. 요소의 전체 CSS 규칙은 이렇게 보일 것입니다.

.spark {
  position: absolute;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--spoiler-dot);
  animation:
    drift var(--dur) linear infinite,
    twinkle var(--twinkle) ease-in-out infinite alternate;
}

여기서 변경된 점은 spark 클래스에 animation 속성을 추가하고 drifttwinkle을 추가한 것입니다. drift의 경우, JavaScript에서 정의했던 무작위로 생성된 dur 값을 전달합니다. 이 값은 스파크가 시작 좌표에서 끝 좌표까지 이동하는 데 걸리는 시간을 정의합니다. 또한 linearinfinite 인자를 애니메이션에 전달하여 움직임이 직선적이며 페이지가 로드되는 동안 계속 반복되도록 합니다.

마찬가지로, 무작위로 생성된 twinkle 변수를 twinkle 키프레임 애니메이션에 전달합니다. 이 변수는 스파크가 시작 불투명도에서 끝 불투명도까지 가는 데 걸리는 시간을 정의합니다. alternate 인자는 불투명도 값이 끝 불투명도에 도달하면 "다시 시작"하지 않고 대신 시작 값으로 다시 부드럽게 돌아가도록 합니다.

이 마지막 추가로 우리의 스포일러 콘텐츠 위에 멋진 반짝이는 베일이 드리워질 것입니다. 제 화면은 이렇게 보입니다.

많은 값이 무작위로 생성되기 때문에 여러분의 예시가 제 것과 정확히 똑같지는 않을 수 있으며, 페이지를 새로 고치면 또 다르게 보일 수 있습니다. 하지만 흐릿하게 처리된 스포일러 텍스트 위로 반짝이는 효과가 보인다면 지금까지 모든 것을 올바르게 수행한 것입니다!

이제 마지막으로 토글 효과를 추가할 차례입니다!

토글 효과 추가하기

스포일러 태그의 핵심은 사람들이 아직 볼 준비가 되지 않은 것을 보지 못하게 하는 것입니다. 하지만 미디어의 스포일러를 감수할 의향이 있는 사용자들을 위해, 반짝이는 베일을 제거하고 숨겨진 텍스트를 드러내는 기능을 추가해야 합니다. 이제 그 효과를 추가하겠습니다.

이를 구현하기 위한 전략은 다음과 같습니다.

  1. spoiler spandata-hidden 속성을 현재 값의 반대(true면 false로, false면 true로)로 설정하는 함수를 추가합니다.
  2. 이 함수를 spoiler span의 "click" 이벤트 리스너에 추가합니다.
  3. data-hidden="false" 속성을 가진 spoiler span 내부 속성들의 CSS를 업데이트합니다.

첫 번째와 두 번째 부분은 wireSpoiler 함수 내에서 동시에 일어날 것입니다. 우리가 가장 먼저 할 일은 spoiler-blur 요소의 부모 요소(이는 spoiler span이어야 합니다)를 가져오는 것입니다. 이는 다음 JavaScript 한 줄로 수행됩니다.

let spoilerSpan = spoilerBlurSpan.parentElement;

이제 그 줄 바로 아래에 toggle이라는 함수를 만들 것입니다. 이 함수는 spoilerdata-hidden 속성을 가져와 hidden이라는 변수를 반대 값으로 설정합니다. 그런 다음 setAttribute 메서드를 사용하여 data-hidden을 새로운 hidden 변수의 값으로 설정할 것입니다. 이것이 바로 "토글"이 의미하는 바입니다.

마지막으로, addEventListener 메서드를 사용하여 toggle 메서드를 해당 요소의 클릭 이벤트에 추가할 것입니다. 이 모든 것을 합치면 wireSpoiler 함수는 이제 이렇게 보일 것입니다.

function wireSpoiler(spoilerBlurSpan) {
    buildSparkles(spoilerBlurSpan, 75);
    let spoilerSpan = spoilerBlurSpan.parentElement;

    function toggle() {
        const hidden = spoilerSpan.getAttribute("data-hidden") === "true";
        spoilerSpan.setAttribute("data-hidden", String(!hidden))
    }

    spoilerSpan.addEventListener("click", toggle);
}

위 코드에서 wireSpoiler 함수에 몇 줄을 추가했습니다. 먼저, spoilerBlur 요소의 부모 요소인 spoiler span을 가져왔습니다. 그런 다음 toggle 함수를 만들었고, 이 함수의 첫 줄은 data-hidden 속성의 값을 가져와 그것이 참인지 확인합니다. HTML에 다음 줄을 작성했으므로:

<span class="spoiler" data-hidden="true">

웹 페이지가 처음 로드될 때 값은 문자열 "true"가 될 것이고, 이는 hidden 변수의 값이 부울 값 true가 됨을 의미합니다.

다음 줄에서는 !hidden을 사용하여 해당 값의 반대되는 문자열 표현으로 속성을 설정합니다. ! 연산자는 그 옆의 변수를 부정합니다. 따라서 !true ("참이 아니다"라고 읽음)는 false가 되고 !false ("거짓이 아니다"라고 읽음)는 true가 됩니다.

마지막으로, toggle 함수 밖에서 addEventListener 메서드를 사용하여 toggle 함수를 click 이벤트에 추가했습니다. 이는 스포일러 span이 클릭될 때마다 이 함수가 실행된다는 의미입니다.

지금 상태로는 스포일러 태그를 클릭하면 data-hidden 속성만 truefalse 사이를 전환할 뿐입니다. 브라우저에서 요소를 확인하고 클릭할 때마다 HTML의 data-hidden 값이 변하는 것을 볼 수 있습니다. 흐림 효과를 실제로 켜고 끄려면 CSS를 업데이트하고 data-hidden 속성이 falsespoiler 요소 내부의 요소들이 특정 방식으로 동작하도록 지시해야 합니다.

이를 위해 data-hidden 속성이 false인 spoiler span 내부의 모든 spark 요소에 대한 규칙 블록을 만듭니다. CSS 파일에 다음을 추가합니다.

.spoiler[data-hidden="false"] .spark {
  animation-play-state: paused;
  display: none;
}

위 코드는 data-hidden 값이 "false"인 스포일러 요소 내부에 있는 spark 요소만 대상으로 합니다. 그리고 animation-play-statepaused로 설정하여 스파크가 현재 위치에서 멈추도록 합니다. 또한 display 속성을 none으로 설정하여 스파클을 사실상 숨깁니다.

spoiler 태그를 클릭하면 data-hidden 값이 true에서 false로 전환되므로, 위에 작성한 새로운 규칙들이 모든 spark span에 적용되어 스파클을 숨기게 됩니다. 또한 data-hidden="true" 요소에 대해서만 filter 속성을 blur(7px)로 설정했으므로, spoiler-content 내부의 텍스트도 흐림이 해제될 것입니다.

이 최종 업데이트로 이제 웹 페이지에서 흐림 효과를 켜고 끌 수 있게 됩니다! 브라우저를 새로 고치고 흐릿한 스포일러 텍스트를 클릭하여 효과를 직접 확인해 보세요.

스스로 해보기

이제 스포일러 효과를 구현하는 기본적인 아이디어를 익혔으니, 코드를 확장하고 개선할 수 있는 많은 것들이 있습니다. 예를 들어, 위 코드는 비효율적입니다. toggle 함수가 모든 스파클에 대해 spoiler span에 설정되어 있으니까요. 이 문제를 어떻게 해결하시겠습니까? 제가 실무에서 대규모 컴포넌트를 다룰 때 이런 부분은 꼭 개선하려 노력합니다. 예를 들어, event delegation 패턴을 사용하면 부모 요소 하나에만 이벤트 리스너를 붙여 효율성을 높일 수 있죠.

또한, 각각 자체 클릭으로 흐림을 해제해야 하는 개별 spoiler 태그를 설정하거나, 어떤 스포일러 태그든 클릭하면 모든 스포일러 태그가 흐림을 해제하도록 설정하는 것도 시도해볼 수 있습니다. 애니메이션 스타일과 스파클 모양을 변경하는 등 할 수 있는 일도 무궁무진합니다. 마음껏 가지고 놀아보세요!

마무리하며

이번 글에서는 Threads 앱의 스포일러 효과를 HTML, CSS, 그리고 약간의 JavaScript를 활용하여 웹에서 구현하는 방법을 알아보았습니다. @keyframes를 사용한 CSS 애니메이션의 기본 원리를 이해하고, 조건부로 텍스트를 숨기고 표시하는 기본적인 전략을 익혔습니다. 이제 이 효과를 여러분의 웹사이트에 다양한 용도로 적용할 수 있을 것입니다.

최종 코드

이 글의 끝에는 index.html, style.css, sparkle.js라는 세 개의 파일이 있습니다. 제 파일의 내용은 다음과 같습니다.

index.html:

<!doctype html>
<html>
<head>
    <title>Sparkly Spoiler Effect</title>
    <link rel="stylesheet" href="style.css" />
</head>
<body>
    <p>
        The biggest plot twist of all time is:
        <span class="spoiler" data-hidden="true">
            <span class="spoiler-content">
                Darth Vader is Luke Skywalker's father!
            </span>
            <span class="spoiler-blur" />
        </span>
    </p>
    <script src="sparkle.js"></script>
</body>
</html>

style.css:

:root {
  --bg: #0f1115;
  --text: #f4f7fb;
  --spoiler-dot: rgba(255,255,255,0.85);
}

body {
  min-height: 25vh;
  display: grid;
  place-items: center;
  background: var(--bg);
  color: var(--text);
}

.spoiler {
  cursor: pointer;
  position: relative;
}

.spoiler[data-hidden="true"] .spoiler-content {
  filter: blur(7px);
}

.spark {
  position: absolute;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--spoiler-dot);
  animation:
    drift var(--dur) linear infinite,
    twinkle var(--twinkle) ease-in-out infinite alternate;
}

.spoiler[data-hidden="false"] .spark {
  animation-play-state: paused;
  display: none;
}

@keyframes drift {
  from {
    transform: translate(var(--x0), var(--y0)) scale(var(--scale));
  }
  to {
    transform: translate(var(--x1), var(--y1)) scale(var(--scale));
  }
}

@keyframes twinkle {
  from { opacity: 0.25; }
  to   { opacity: 0.95; }
}

sparkle.js:

function random(rscale) {
    return Math.random() * rscale;
}

function buildSparkles(spoilerBlurSpan, sparkleCount) {
    for (let i = 0; i < sparkleCount; i++) {
        const s = document.createElement("span");
        s.className = "spark";
        const x0 = random(100);
        const y0 = random(100);
        const x1 = Math.max(0, Math.min(100, x0 + random(18)));
        const y1 = Math.max(0, Math.min(100, y0 + random(14)));

        s.style.left = `${x0}%`;
        s.style.top = `${y0}%`;
        s.style.setProperty("--x0", "0px");
        s.style.setProperty("--y0", "0px");
        s.style.setProperty("--x1", `${x1 - x0}px`);
        s.style.setProperty("--y1", `${y1 - y0}px`);
        s.style.setProperty("--dur", `${random(60)}s`);
        s.style.setProperty("--twinkle", `${random(1.2)}s`);
        s.style.setProperty("--scale", `${random(1.5)}`);

        spoilerBlurSpan.appendChild(s)
    }
}

function wireSpoiler(spoilerBlurSpan) {
    buildSparkles(spoilerBlurSpan, 75);
    let spoilerSpan = spoilerBlurSpan.parentElement;

    function toggle() {
        const hidden = spoilerSpan.getAttribute("data-hidden") === "true";
        spoilerSpan.setAttribute("data-hidden", String(!hidden))
    }
    spoilerSpan.addEventListener("click", toggle);
}

document.querySelectorAll(".spoiler-blur").forEach(wireSpoiler);

원문: https://dev.to/erikwhiting88/how-to-create-a-sparkly-spoiler-effect-like-the-one-in-threads-mobile-app-19nk 수집일: 2026-04-21 01:18:28