← 목록으로

스크롤 멈칫! Threads 앱 반짝이 스포일러, 프론트엔드에서 만드는 비밀

2026. 4. 20.

스크롤 멈칫! Threads 앱 반짝이 스포일러, 프론트엔드에서 만드는 비밀

Meta의 Threads 앱을 모바일 기기에서 사용해 본 분이라면 아마 흥미로운 효과를 발견하셨을 겁니다. 바로 텍스트를 반짝이는 베일 뒤에 숨기는 스포일러 태그죠. 오늘은 이 매력적인 효과를 HTML, CSS, JavaScript를 활용해 웹 브라우저에서 어떻게 구현하는지, 10년 차 IT 실무자로서 쌓아온 노하우를 담아 자세히 설명해 드릴게요.

참고: 전체 내용을 읽기 번거로운 분들을 위해 이 글의 맨 아래에 완성된 HTML, CSS, JavaScript 코드를 모아두었으니 필요하시면 바로 확인해 보세요.

Threads 앱 스포일러 효과, 한눈에 보기

"스포일러" 태그가 낯선 분들을 위해 간략히 설명하자면, Threads의 이 기능은 사용자가 게시물의 특정 부분을 선택적으로 숨겼다가, 다른 사용자가 원할 때만 내용을 볼 수 있도록 하는 기능입니다. 아래 예시를 통해 제가 "Highlight the word “hello” to mark as a spoiler!"라는 문장을 게시하려는 상황을 보시죠.

"hello"라는 단어를 드래그해서 선택했더니 "Mark spoiler" 버튼이 나타난 게 보이시죠? 이 버튼을 클릭한 다음 게시물을 올리면 제 데스크톱 브라우저에서는 다음과 같이 보입니다.

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

위 스크린샷은 독자가 회색 막대를 클릭한 후 보게 되는 화면입니다. 이제 제가 숨겼던 단어를 볼 수 있죠.

이것이 바로 Threads의 "스포일러 표시" 기능이 작동하는 방식입니다. 작성자는 특정 텍스트를 선택해 가리고, 사용자는 숨겨진 텍스트를 보기 위해 의도적인 동작을 취해야 합니다. 이 기능은 이야기의 반전이나 결말에 대해 이야기하고 싶을 때, 아직 그 부분을 보지 못한 독자들에게 "스포일러"를 하지 않도록 설계되었습니다. 독자가 의도치 않게 스포일러를 읽는 것을 막기 위해, 숨겨진 텍스트를 명시적으로 드러내도록 강제하는 것이죠.

하지만 이 스포일러 바는 모바일 앱에서 좀 더 재미있게 표현됩니다. 다음 스크린샷은 동일한 게시물이지만 모바일 Threads 앱에서 본 모습입니다.

보시다시피, 단순한 회색 막대 대신 반짝이는 효과로 가려져 있습니다. 정말 멋지지 않나요? 이번 글에서는 바로 이 반짝이는 스포일러 효과를 만드는 방법을 알려드릴 겁니다.

진짜 스포일러 주의!

이제부터 영화 스타워즈 에피소드 5 – 제국의 역습에 나오는 결정적인 반전에 대해 이야기할 예정입니다. 이 영화는 1980년에 개봉했지만, 아직 보지 못한 분들도 계실 수 있습니다. 혹시라도 스포일러로 인해 영화의 재미를 망치게 될까봐 미리 말씀드립니다. 만약 이 영화를 볼 계획이 있고 줄거리에 대해 전혀 모른다면, 이 글은 여러분에게 확실히 스포일러가 될 것입니다!

⚠️ 계속 진행하는 것은 여러분의 선택입니다!

효과 구현하기

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

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

자, 이제 시작해 볼까요!

텍스트를 스포일러로 지정하기

우선, spoiler_text.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라는 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를 적용하면 브라우저 페이지는 이제 다음과 같이 보일 것입니다.

위 스크린샷은 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 키프레임과 약간의 JavaScript. 전반적인 전략은 다음과 같습니다.

  1. 각각의 sparkle-blur span이 스포일러 텍스트를 덮도록 spark CSS 클래스를 만듭니다.
  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>

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

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

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

다음으로, buildSparkles라는 함수를 만들 겁니다. 이 함수는 span 요소와 우리가 생성하고자 하는 반짝이의 수를 인자로 받습니다. 우리가 생성하는 각 반짝이는 시작 2차원 좌표(left 및 top 속성을 나타내는 x, y), 끝 2차원 좌표, 반짝이가 이 두 좌표 사이를 이동하는 데 걸리는 시간, 반짝이가 "반짝이는" 데 걸리는 시간, 그리고 크기를 조절하는 스케일 변수와 함께 생성됩니다. 이 모든 값은 각 반짝이에 대해 무작위로 생성됩니다.

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에 추가합니다.

처음에 이 반짝임 효과를 만들면서 Math.random()으로 값을 조절하는 게 생각보다 쉽지 않았어요. 어떤 값으로 해야 자연스러운 '반짝임'이 나올까 여러 번 시행착오를 거쳤는데, 이 부분이 사용자 경험을 확 끌어올리는 디테일이죠.

마지막으로, 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 키프레임은 spark 요소들이 오실레이션할 투명도 값을 정의합니다. 투명도는 요소의 투명도 수준이므로, 낮은 값과 높은 값 사이를 오가면 요소가 반짝이는 듯한 착시를 줍니다.

이제 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 속성의 값을 가져와 true인지 확인합니다. 우리가 HTML에 이 줄을 작성했으므로:

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

웹 페이지가 처음 로드될 때 값은 문자열 "true"가 될 것이고, 이는 hidden 변수의 값이 불리언 true가 된다는 의미입니다.

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

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

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

data-hidden 속성이 falsespoiler span 내부의 모든 spark 요소에 대한 규칙 블록을 추가하여 이를 수행합니다. CSS 파일에 다음을 추가하세요.

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

위 코드는 data-hidden 값이 "false"인 스포일러 요소 내부에 있는 spark 요소만 대상으로 합니다. 그런 다음, animation-play-statepaused로 설정하는데, 이는 반짝이가 현재 위치에서 멈춘다는 의미입니다. 또한 display 속성을 none으로 설정하여 반짝이를 본질적으로 숨깁니다.

스포일러 태그를 클릭하면 data-hidden 값이 true일 경우 false로 전환되므로, 우리가 새로 작성한 규칙이 모든 spark span에 적용되어 효과적으로 숨겨집니다. 또한 filter 속성을 blur(7px)로 설정한 규칙은 data-hidden="true" 요소에만 적용되었으므로, spoiler-content 내부의 텍스트도 흐림이 해제됩니다.

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

스스로 해보기

이제 스포일러 효과 구현의 기본적인 아이디어를 익혔으니, 코드를 확장하고 개선할 수 있는 많은 것들이 있습니다. 예를 들어, 위 코드는 toggle 함수를 각각의 반짝이에 대해 spoiler span에 설정하기 때문에 비효율적입니다. 이 문제를 어떻게 해결하시겠어요? (event delegation을 활용하는 것이 좋은 방법이겠죠?)

또한, 각각 자체 클릭으로 흐림을 해제해야 하는 개별 spoiler 태그를 설정하거나, 모든 스포일러 태그가 어떤 하나를 클릭할 때 흐림이 해제되도록 설정하는 것도 실험해 볼 수 있습니다. 애니메이션 스타일과 반짝이 모양을 변경할 수 있는 수많은 방법도 있습니다. 한번 직접 만져보세요!

마치며

이 글에서는 Threads 앱의 스포일러 효과를 모방하는 방법을 배우고, HTML, CSS, 그리고 약간의 JavaScript를 사용해 이를 구현했습니다. 키프레임을 활용하고 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-20 01:21:14