Essentials/Highlight

김동현·2026년 3월 22일

하이라이트 (Highlight)

React | Vue | Angular | Web Components | 그 외(More)

스토리북의 하이라이트(Highlight) 기능은 컴포넌트를 눈으로 직접 보며 디버깅할 때 아주 유용한 도구입니다. 이 기능을 사용하면 스토리 안의 특정 DOM 노드를 시각적으로 강조(highlight)할 수 있죠. 직접 사용할 수도 있고, 접근성 애드온(Accessibility addon)처럼 컴포넌트의 접근성 문제 위치를 콕 집어 알려주는 다른 애드온과 결합해서 사용할 수도 있습니다.

Story with highlighted elements

DOM 엘리먼트 하이라이트 하기 (Highlighting DOM Elements)

DOM 엘리먼트를 하이라이트 하려면 스토리나 애드온 내부에서 HIGHLIGHT 이벤트를 발생(emit)시켜야 합니다. 이때 이벤트 페이로드(payload) 안에는 강조하고 싶은 엘리먼트와 일치하는 선택자(selector)들의 배열을 담은 selectors 속성을 꼭 넣어주어야 해요. 예를 들어볼게요:

// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { useChannel } from 'storybook/preview-api';
import { HIGHLIGHT } from 'storybook/highlight';
 
import { MyComponent } from './MyComponent';
 
const meta = {
  component: MyComponent,
} satisfies Meta<typeof MyComponent>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const Highlighted: Story = {
  decorators: [
    (storyFn) => {
      const emit = useChannel({});
      emit(HIGHLIGHT, {
        selectors: ['h2', 'a', '.storybook-button'],
      });
      return storyFn();
    },
  ],
};

💡 팁: 다른 애드온이 사용하는 엘리먼트까지 엉뚱하게 하이라이트되는 일을 막으려면, 최대한 구체적인(specific) 선택자를 사용하는 것이 좋습니다. 이 기능은 전체 DOM 트리를 상대로 선택자를 일치시키려고 시도하거든요.

스타일 커스터마이징 (Customize style)

기본적으로 하이라이트 된 엘리먼트들에는 미리 정해진 깔끔한 외곽선(outline) 스타일이 적용됩니다. 하지만 페이로드 객체에 속성을 추가해서 이 하이라이트 스타일을 여러분의 입맛에 맞게 바꿀 수도 있어요. 예를 들면 이렇습니다:

// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { useChannel } from 'storybook/preview-api';
import { HIGHLIGHT } from 'storybook/highlight';
 
import { MyComponent } from './MyComponent';
 
const meta = {
  component: MyComponent,
} satisfies Meta<typeof MyComponent>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const StyledHighlight: Story = {
  decorators: [
    (storyFn) => {
      const emit = useChannel({});
      emit(HIGHLIGHT, {
        selectors: ['h2', 'a', '.storybook-button'],
        styles: {
          backgroundColor: `color-mix(in srgb, hotpink, transparent 90%)`,
          outline: '3px solid hotpink',
          animation: 'pulse 3s linear infinite',
          transition: 'outline-offset 0.2s ease-in-out',
        },
        hoverStyles: {
          outlineOffset: '3px',
        },
        focusStyles: {
          backgroundColor: 'transparent',
        },
        keyframes: `@keyframes pulse {
          0% { outline-color: rgba(255, 105, 180, 1); }
          50% { outline-color: rgba(255, 105, 180, 0.2); }
          100% { outline-color: rgba(255, 105, 180, 1); }
        }`,
      });
      return storyFn();
    },
  ],
};

ℹ️ 이러한 스타일 관련 속성들은 선택 사항(optional)입니다. hoverStylesfocusStyles 속성은 아래에서 설명할 menu 속성과 함께 쓸 때 특히 빛을 발하죠. (단, 가상 클래스(pseudo-classes)와 가상 요소(pseudo-elements)는 지원하지 않습니다.)

하이라이트 메뉴 띄우기 (Highlight menu)

하이라이트 기능에는 내장된 유용한 디버깅 옵션이 있습니다. 하이라이트 된 엘리먼트를 '클릭'했을 때 이를 선택할 수 있게 해주는 기능인데요. 제공한 선택자와 일치하는 엘리먼트들의 목록을 미리 보고 검사할 때 아주 유용합니다.

이 메뉴를 활성화하려면, 페이로드 객체에 해당 엘리먼트들에 대한 추가 정보를 담거나 액션을 실행시킬 수 있는 menu 속성을 추가하면 됩니다. 각 메뉴 아이템에는 idtitle이 필수로 들어가야 하며, 특정 하이라이트 엘리먼트에만 메뉴를 띄우고 싶다면 선택적으로 selectors 속성을 제공할 수도 있습니다.

Menu with custom items

// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { useChannel } from 'storybook/preview-api';
import { HIGHLIGHT } from 'storybook/highlight';
 
import { MyComponent } from './MyComponent';
 
const meta = {
  component: MyComponent,
} satisfies Meta<typeof MyComponent>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const StyledHighlight: Story = {
  decorators: [
    (storyFn) => {
      const emit = useChannel({});
      emit(HIGHLIGHT, {
        selectors: ['h2', 'a', '.storybook-button'],
        menu: [
          [
            {
              id: 'button-name',
              title: 'Login',
              description: 'Navigate to the login page',
              clickEvent: 'my-menu-click-event',
            },
            {
              id: 'h2-home',
              title: 'Acme',
              description: 'Navigate to the home page',
            },
          ],
        ],
      });
      return storyFn();
    },
  ],
};

ℹ️ 참고: useChannel API 훅을 통해 만들어진 emit 함수는 스토리북 UI 안에 통신 채널을 엽니다. 이 채널은 이벤트를 듣고 그에 맞춰 UI를 업데이트하는 역할을 하죠. 하이라이트 기능도 바로 이 채널을 통해 커스텀 이벤트를 듣고 하이라이트 엘리먼트를 업데이트하는 원리입니다.

메뉴 기능을 켜두면, 제공한 선택자와 일치하는 엘리먼트를 클릭할 때마다 짜잔 하고 메뉴가 나타납니다. 만약 특별히 보여줄 정보가 없다면 메뉴 아이템을 생략하거나 menu 속성에 빈 배열([])을 넣어서 기본 메뉴만 보여줄 수도 있어요.

Menu of selectable targets

하이라이트 지우기 (Remove highlights)

스토리북은 다른 스토리로 넘어갈 때(transition) 기존에 있던 하이라이트들을 알아서 깔끔하게 지워줍니다. 하지만 직접 수동으로 지워야 할 상황이 생긴다면, 스토리나 애드온 내부에서 지우고 싶은 하이라이트의 id와 함께 REMOVE_HIGHLIGHT 이벤트를 발생시키면 됩니다. 아예 모든 하이라이트를(다른 애드온이 만든 것까지 전부!) 날려버리고 싶다면 RESET_HIGHLIGHT 이벤트를 발생시키면 되고요. 예를 들어볼게요:

// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { useChannel } from 'storybook/preview-api';
import { HIGHLIGHT, REMOVE_HIGHLIGHT } from 'storybook/highlight';
 
import { MyComponent } from './MyComponent';
 
const meta = {
  component: MyComponent,
} satisfies Meta<typeof MyComponent>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const RemoveHighlight: Story = {
  decorators: [
    (storyFn) => {
      const emit = useChannel({});
      emit(HIGHLIGHT, {
        id: 'my-unique-id',
        selectors: ['header', 'section', 'footer'],
      });
      emit(REMOVE_HIGHLIGHT, 'my-unique-id');
      return storyFn();
    },
  ],
};
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { useChannel } from 'storybook/preview-api';
import { HIGHLIGHT, RESET_HIGHLIGHT } from 'storybook/highlight';
 
import { MyComponent } from './MyComponent';
 
const meta = {
  component: MyComponent,
} satisfies Meta<typeof MyComponent>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const ResetHighlight: Story = {
  decorators: [
    (storyFn) => {
      const emit = useChannel({});
      emit(RESET_HIGHLIGHT); //👈 이전에 켜져 있던 모든 하이라이트를 초기화합니다!
      emit(HIGHLIGHT, {
        selectors: ['header', 'section', 'footer'],
      });
      return storyFn();
    },
  ],
};

화면으로 끌어와 하이라이트 하기 (Scroll element into view)

특정 엘리먼트가 화면 밖에 있어서 안 보일 때, 그 녀석을 스크롤해서 화면 안으로 끌고 온 다음 살짝 강조해 주는 기능도 있습니다. 이 기능을 쓰려면 스토리나 애드온에서 SCROLL_INTO_VIEW 이벤트를 발생시키면 돼요. 이벤트 페이로드에는 내가 화면으로 끌고 오고 싶은 엘리먼트의 선택자를 selector 속성에 담아 보내야 합니다. 엘리먼트가 화면에 나타나면, 찰나의 순간 동안 눈에 띄게 하이라이트 됩니다.

// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { useChannel } from 'storybook/preview-api';
import { SCROLL_INTO_VIEW } from 'storybook/highlight';
 
import { MyComponent } from './MyComponent';
 
const meta = {
  component: MyComponent,
} satisfies Meta<typeof MyComponent>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const ScrollIntoView: Story = {
  decorators: [
    (storyFn) => {
      const emit = useChannel({});
      // 👉 #footer 요소를 찾아서 화면 안으로 스크롤한 뒤 하이라이트합니다!
      emit(SCROLL_INTO_VIEW, '#footer');
      return storyFn();
    },
  ],
};

API

파라미터 (Parameters)

이 기능은 스토리북의 파라미터(parameters) 영역 중 highlight 네임스페이스 아래에 다음과 같은 설정을 제공합니다:

disable

타입: boolean

이 하이라이트 기능의 동작을 끕니다. 만약 스토리북 전체에서 기능을 꺼버리고 싶다면, 메인 설정 파일(main.js|ts)에서 기능을 비활성화하는 것을 권장합니다.

이 파라미터는 보통 전체 프로젝트 수준에서는 기능을 꺼두고(true), 필요한 특정 컴포넌트나 스토리에서만 다시 켤 때(false) 요긴하게 쓰입니다.

내보내기 (Exports)

이 모듈에서는 스토리북을 위해 다음 변수들을 내보냅니다(export):

import { HIGHLIGHT, REMOVE_HIGHLIGHT, RESET_HIGHLIGHT, SCROLL_INTO_VIEW } from 'storybook/highlight';

HIGHLIGHT

이벤트 이름입니다. DOM 엘리먼트를 하이라이트 할 때 쓰여요. 이벤트 페이로드에는 반드시 selectors 속성(하이라이트할 엘리먼트의 선택자 배열)이 포함되어야 합니다. 여기에 추가적인 옵션 객체를 더해서 하이라이트 모양을 꾸밀 수도 있어요. 위에서 다룬 사용 예시(usage example)를 참고해 보세요.

import { HIGHLIGHT, type HighlightOptions } from 'storybook/highlight';
 
channel.emit(
  HIGHLIGHT,
  options // HighlightOptions API를 상속받는 설정 옵션들
);

여기서 options 객체 안에 들어갈 수 있는 속성들은 다음과 같습니다:

interface HighlightOptions {
  /** 나중에 이 하이라이트를 지우고 싶다면 필수! 하이라이트의 고유 식별자입니다. */
  id?: string;
  /** 엘리먼트들을 가리키는 HTML 선택자(selectors)들입니다. */
  selectors: string[];
  /** 하이라이트의 우선순위. 숫자가 높을수록 우선 적용됩니다. (기본값: 0) */
  priority?: number;
  /** 하이라이트에 씌울 CSS 스타일입니다. */
  styles?: Record<string, string>;
  /** 하이라이트에 마우스를 올렸을 때(hover) 씌울 CSS 스타일입니다. */
  hoverStyles?: Record<string, string>;
  /** 하이라이트가 포커스(focus) 되거나 선택(selected)되었을 때 씌울 CSS 스타일입니다. */
  focusStyles?: Record<string, string>;
  /** 애니메이션에 필요한 키프레임(Keyframes) 문자열입니다. */
  keyframes?: string;
  /** 하이라이트가 선택되었을 때 보여줄 메뉴 아이템들의 그룹입니다. */
  menu?: HighlightMenuItem[][];
}
 
interface HighlightMenuItem {
  /** 메뉴 아이템의 고유 식별자입니다. */
  id: string;
  /** 메뉴 아이템에 표시될 제목입니다. */
  title: string;
  /** 메뉴 아이템에 대한 설명입니다. (선택 사항) */
  description?: string;
  /** 메뉴 아이템 왼쪽에 들어갈 아이콘입니다. (선택 사항) */
  iconLeft?: "chevronLeft" | "chevronRight" | "info" | "shareAlt";
  /** 메뉴 아이템 오른쪽에 들어갈 아이콘입니다. (선택 사항) */
  iconRight?: "chevronLeft" | "chevronRight" | "info" | "shareAlt";
  /** 메뉴 아이템을 클릭했을 때 채널(channel)로 쏠(trigger) 이벤트 이름입니다. (선택 사항) */
  clickEvent?: string;
  /** 이 메뉴 아이템이 나타나야 할 HTML 선택자들입니다. (HighlightOptions['selectors']의 일부분이어야 함) (선택 사항) */
  selectors?: HighlightOptions['selectors'];
}

메뉴 아이템에 clickEvent를 설정해 두면, 사용자가 그 메뉴를 클릭했을 때 채널을 통해 해당 이벤트가 발송(emit)됩니다. 이때 채널 이벤트는 두 가지 인자를 받게 되는데요, 바로 메뉴 아이템의 id와 다음과 같은 속성을 가진 ClickEventDetails 객체입니다:

interface ClickEventDetails {
  // 페이지 안에서 해당 엘리먼트의 위치와 크기 정보
  top: number;
  left: number;
  width: number;
  height: number;
  // 해당 엘리먼트와 일치했던 선택자(들)
  selectors: string[];
  // 실제 DOM 엘리먼트의 상세 정보
  element: {
    attributes: Record<string, string>;
    localName: string;
    tagName: string;
    outerHTML: string;
  };
}

이 이벤트를 감지(listen)하려면 이렇게 하시면 됩니다. (예를 들어 clickEvent: 'MY_CLICK_EVENT' 라고 설정했을 때):

import type { ClickEventDetails } from 'storybook/highlight';
 
const handleClickEvent = (itemId: string, details: ClickEventDetails) => {
  // 메뉴 아이템 클릭 이벤트를 여기서 처리하세요!
}
 
// channel 인스턴스를 직접 쓸 수 있을 때:
channel.on('MY_CLICK_EVENT', handleClickEvent)
 
// 또는 데코레이터 안에서 사용할 때:
useChannel({
  MY_CLICK_EVENT: handleClickEvent,
}, [handleClickEvent])

REMOVE_HIGHLIGHT

이전에 만들었던 하이라이트를 콕 집어 지울 때 쓰는 이벤트입니다. 이벤트 페이로드로 지우고 싶은 하이라이트의 id를 꼭 넘겨주어야 합니다. 위에서 다룬 사용 예시(usage example)를 참고하세요.

import { REMOVE_HIGHLIGHT } from 'storybook/highlight';
 
channel.emit(
  REMOVE_HIGHLIGHT,
  id // 지우고 싶은 기존 하이라이트의 id
);

RESET_HIGHLIGHT

화면에 켜진 모든 하이라이트를 한 방에 지워버리는 시원한 이벤트입니다. 위에서 다룬 사용 예시(usage example)를 참고하세요.

import { RESET_HIGHLIGHT } from 'storybook/highlight';
 
channel.emit(RESET_HIGHLIGHT);

SCROLL_INTO_VIEW

DOM 엘리먼트를 화면 안으로 스크롤해서 끌고 온 다음, 잠깐 동안 하이라이트 해주는 이벤트입니다. 이벤트 페이로드로 스크롤해서 찾고 싶은 엘리먼트의 selector를 넘겨줘야 합니다. 추가로 스크롤 동작을 부드럽게 하는 등의 설정을 원한다면 options 객체(ScrollIntoViewOptions API)를 함께 넘겨줄 수 있어요. 위에서 다룬 사용 예시(usage example)를 참고하세요.

import { SCROLL_INTO_VIEW } from 'storybook/highlight';
 
channel.emit(
  SCROLL_INTO_VIEW,
  selector // 화면으로 끌고 올 엘리먼트의 선택자
  options // 스크롤 동작을 커스텀하기 위한 ScrollIntoViewOptions API 상속 객체 (선택 사항)
);

이 페이지가 유용했나요? 👍 👎
✍️ 깃허브에서 편집하기 (Edit on Github)

profile
프론트에_가까운_풀스택_개발자

0개의 댓글