norigin-spatial-navigation

강정우·2025년 9월 10일
0

react.js

목록 보기
47/47
post-thumbnail

norigin-spatial-navigation

리모컨을 웹에서 동작하여 electron 을 사용하여 앱을 만들 수 있다.
그럼 마치 tvOS 처럼 동작하는데 이를 위해 web-client 에서 리모컨을 사용할 수있도록 도와주는 library 가 있다.
이에 Norigin-Spatial-Navigation 을 소개하고자 한다.

Norigin-Spatial-Navigation

Norigin-Spatial-Navigation 은 리모컨 또는 키보드 기반의 UI 탐색(특히 TV 앱)을 위한 React 중심의 spatial navigation 라이브러리이다.
넷플릭스나 유튜브 TV처럼 리모컨 방향키로 UI 요소 간 이동이 필요한 환경에서 매우 유용하다.

동작 방식

위 사진을 보면 이 library 가 어떻게 동작할지 궁금할텐데
이 library 는 기본적으로 DOM 기반으로 동작한다.
즉, 렌더링된 DOM 요소들의 위치와 크기를 계산하고, 어떤 요소가 포커스 가능한지(tabIndex, data-sn 속성 등)를 파악하여 공간적으로 다음 포커스 위치를 결정한다.

참고로 React key prop 과 focusable 의 key 는 전혀 관련이 없다.

useFocusable 훅을 통해 focusKey를 각 컴포넌트에 부여하고 trackChildren 속성을 설정함으로써, 라이브러리는 이 focusKey들을 사용하여 포커스 가능한 요소들을 관리하고, 포커스 이동 시 어떤 요소가 현재 활성화되어 있는지 등을 추적한다.

그리고 이 과정에서 CSS(DOM 요소의 레이아웃 정보)가 포커스 이동 방향을 결정하는 데 중요한 역할을 한다. 예를 들어, 왼쪽/오른쪽 화살표 키를 눌렀을 때, 현재 포커스된 요소의 DOM 위치를 기준으로 "오른쪽에 있는" 다음 포커스 가능한 요소를 찾는다.

install

npm i @noriginmedia/norigin-spatial-navigation --save

usage

init

useEffect(() => {
  init({
    // debug: true,
    // visualDebug: true,
  });

  setKeyMap({
    left: ['ArrowLeft', 37],
    up: ['ArrowUp', 38],
    right: ['ArrowRight', 39],
    down: ['ArrowDown', 40],
    enter: ['Enter', 13]
  });
}, []);

init 은 본인이 navigate 하고 싶은 모든 요소를 포함하는 부모 컴포넌트에 위치하면 된다.

useFocusable

이게 핵심 훅이다.

이때 대개 2가지 종류로 작성할 수 있다.

  1. Provider
const {ref: busPageRef, focusKey: busInfoPageFocusKey, focusSelf: focusBusInfoPage} = useFocusable({
  focusKey: "bus-page",
  trackChildren: true,
});

    // 중략

return (
  <FocusContext.Provider value={busInfoPageFocusKey}>
  <Container ref={busPageRef}>
    <BusCard
direction="left"
stationName={busesInfo.station_1_info.name + " (상행)"}
highlightBusNumber={arrivalSoonBusL}
buses={station1Sorted}
/>
  <BusCard
direction="right"
stationName={busesInfo.station_2_info.name + " (하행)"}
highlightBusNumber={arrivalSoonBusR}
buses={station2Sorted}
/>
  </Container>
</FocusContext.Provider>
);
  1. Consumer
    아래는 위 Provider 의 BusCard 컴포넌트의 일부이다.
const {ref: busCardRef, focused} = useFocusable({
  focusKey: `bus-card-${direction}`,
  onArrowPress: (direction) => {
    if (focused && scrollContainerRef.current) {
      const container = scrollContainerRef.current;
      const scrollAmount = 100;

      if (direction === 'up') {
        container.scrollTop = Math.max(0, container.scrollTop - scrollAmount);
        return false;
      } else if (direction === 'down') {
        container.scrollTop = Math.min(
          container.scrollHeight - container.clientHeight,
          container.scrollTop + scrollAmount
        );
        return false;
      } else if (direction === "left") {
        if (getCurrentFocusKey() === "bus-card-left") {
          setFocus("busArrived");
        }
      }
    }
    return true;
  }
});

return (
  <CardWrapper ref={busCardRef} $focused={focused} $direction={direction}>
    <Title>{stationName}</Title>
    <Header $direction={direction}>
      <div>곧 도착</div>
      <div>
        {highlightBusNumber.map((num, idx) => (
          <span key={idx}>
            <img
              src={direction === "left" ? BusR : BusB}
              alt="버스"
              style={{width: "1.2rem", height: "1.2rem", marginRight: "0.25rem"}}
              />
            {num}
          </span>
        ))}
      </div>
    </Header>
    <Table ref={scrollContainerRef} $direction={direction}>
      // 생략
    </Table>
    </CardWrapper>
  );
  • 아래는 예시로 최대한 다양한 옵션을 적어보았다.
const {
  ref,
  focusSelf,
  focusKey,
} = useFocusable({
  focusable: true, // 컨테이너 자체는 포커스 불가
  saveLastFocusedChild: true,
  trackChildren: true,
  autoRestoreFocus: true,
  isFocusBoundary: true,
  focusBoundaryDirections: ["up", "down", "left"],
  focusKey: currentRemoteFocus,
  preferredChildFocusKey: undefined,
  extraProps: { foo: "bar" },
});
profile
智(지)! 德(덕)! 體(체)!

0개의 댓글