Skip to content

특정 컴포넌트 위치에서 헤더 스타일 다르게 적용하는 방법! (”light” | “dark”)

최수연 (SooYeon Choi) edited this page Aug 22, 2024 · 1 revision
<Header type="dark" />

기존에 Header 컴포넌트에서는 type을 Props로 따로 받아서 헤더 스타일을 따로 받아서 적용하도록 구현되어 있었다. 아예 type을 변경하는 부분을 ContextAPI를 사용하여 따로 훅으로 빼서 관리하기로 했다.

우선 총 3개의 랜딩 페이지마다 하나의 공통된 훅을 사용하기 위해 각 섹션에 해당하는 상수 객체와 타입 정의를 통해 섹션 키를 관리하도록 했다.

export const MAIN_SECTIONS = {
    HEADLINE: "HEADLINE",
    LOTTERY: "LOTTERY",
    RUSH: "RUSH",
    LEARN_MORE: "LEARN_MORE",
} as const;

export const LOTTERY_SECTIONS = {
    HEADLINE: "HEADLINE",
    INTRO: "INTRO",
    HEADLAMP: "HEADLAMP",
    PIXEL_DESIGN: "PIXEL_DESIGN",
    WHEEL_DESIGN: "WHEEL_DESIGN",
    CUSTOM_DESIGN: "CUSTOM_DESIGN",
    NEW_COLOR: "NEW_COLOR",
    SMILE_BADGE: "SMILE_BADGE",
    SHORT_CUT: "SHORT_CUT",
} as const;

export const RUSH_SECTIONS = {
    BALANCE_GAME: "BALANCE_GAME",
    INTRO: "INTRO",
    FAQ: "FAQ",
    ELECTRIC_REASON: "ELECTRIC_REASON",
    ELECTRIC_ADVANTAGE: "ELECTRIC_ADVANTAGE",
    REASON_FIRST: "REASON_FIRST",
    CASPER_FAR: "CASPER_FAR",
    CASPER_FAST: "CASPER_FAST",
    CASPER_COMFORTABLE: "CASPER_COMFORTABLE",
    REASON_SECOND: "REASON_SECOND",
    CASPER_WIDE: "CASPER_WIDE",
    CASPER_CHARGE: "CASPER_CHARGE",
    CASPER_SMART_KEY: "CASPER_SMART_KEY",
} as const;

그리고 이 상수들을 사용하여 타입을 다음과 같이 정의해줬는데, 각각의 페이지에 대한 타입과, 이들을 유니온 타입으로 선언하여 하나의 섹션 키로 통합하여 타입을 관리하도록 해주었다.

import {
    LOTTERY_SECTIONS,
    MAIN_SECTIONS,
    RUSH_SECTIONS,
} from "@/constants/PageSections/sections.ts";

type MainSectionKey = keyof typeof MAIN_SECTIONS;
type LotterySectionKey = keyof typeof LOTTERY_SECTIONS;
type RushSectionKey = keyof typeof RUSH_SECTIONS;

export type SectionKey = MainSectionKey | LotterySectionKey | RushSectionKey;

export interface SectionKeyProps {
    id: SectionKey;
}

헤더 타입의 경우에도 아래와 같이 “light”와 “dark”로 나누어서 정의하고, ScrollHeaderStyleContext 사용에 필요한 타입을 정의해주었다.

import { SectionKey } from "@/types/sections.ts";

export type HeaderType = "light" | "dark";

export interface ScrollHeaderStyleType {
    activeSection: SectionKey; // 현재 활성화된 섹션 키
    setActiveSection: (section: SectionKey) => void; // 활성될 섹션 설정하는 함수
    headerType: HeaderType; // 현재 헤더 타입
    setHeaderType: (type: HeaderType) => void; // 헤더 타입 설정 함수
}

그리고 useScrollHeaderStyleContext 라는 훅을 생성하였다. 본 훅은 앞서 말했다시피 각 섹션마다 헤더 타입을 다르게 설정해주기 위한 훅이다.

import { useContext } from "react";
import { ScrollHeaderStyleContext } from "../contexts/scrollHeaderStyleContext.tsx";

export default function useScrollHeaderStyleContext() {
    const context = useContext(ScrollHeaderStyleContext);
    if (context === null) {
        throw new Error(
            "scrollHeaderStyleContext must be used within a useScrollHeaderStyleProvider"
        );
    }
    return context;
}

우선 useContext 훅을 사용하여 ScrollHeaderStyleContext의 현재 값을 가져온다. 여기서 만약 ScrollHeaderStyleContext가 null이면, 이 훅이 ScrollHeaderStyleProvider 내부에서 사용되지 않았다는 의미이므로 에러를 던진다. 만약 null이 아닌 경우 context 값을 반환하는데, 이를 통해 컴포넌트에서는 ScrollHeaderStyleContext의 값을 쉽게 접근할 수 있다.

아래 파일에서는 우선 createContext를 사용하여 ScrollHeaderStyleContext를 생성하고, null로 초기화해준다. 아까 정의해준 ScrollHeaderStyleType 타입을 사용하여 생성해주기 때문에 해당 컨텍스트는 ScrollHeaderStyleType 또는 null 타입을 가질 수 있다.

import { ReactNode, createContext, useMemo, useState } from "react";
import { HeaderType, ScrollHeaderStyleType } from "@/types/scrollHeaderStyle.ts";
import { SectionKey } from "@/types/sections.ts";

export const ScrollHeaderStyleContext = createContext<ScrollHeaderStyleType | null>(null);

export const ScrollHeaderStyleProvider = ({ children }: { children: ReactNode }) => {
    const [activeSection, setActiveSection] = useState<SectionKey>("HEADLINE");
    const [headerType, setHeaderType] = useState<HeaderType>("light");

    const value = useMemo(
        () => ({
            activeSection,
            setActiveSection,
            headerType,
            setHeaderType,
        }),
        [activeSection, headerType]
    );

    return (
        <ScrollHeaderStyleContext.Provider value={value}>
            {children}
        </ScrollHeaderStyleContext.Provider>
    );
};

그리고 ScrollHeaderStyleProvider를 사용하여 자식 컴포넌트들에게 컨텍스트 값을 제공해줄 수 있는데, 여기서 아까 ScrollHeaderStyleType 타입에서 정의한 activeSection과 headerType 총 2개의 상태들을 useState로 관리한다.

여기서 value 객체에 대하여 메모이제이션 해주기 위해 useMemo를 사용했는데, 이를 통해 activeSection 또는 headerType이 변경되지 않는 한, value 객체가 재생성 되지 않도록 해주었다.

새로운 훅을 하나 더 생성해주었는데, 이 훅은 useHeaderStyleObserver 라는 이름으로 생성해주었다. 훅 이름처럼 IntersectionObserver라는 웹 API를 사용하여 특정 섹션에 도달했을 때를 감지하여 해당 섹션에서 헤더 스타일을 light / dark 모드 중 어느 것을 적용할지 분기처리 해주도록 하는 훅이다.

IntersectionObserver 객체 생성할 때 기본 구조는 다음과 같다.

const observer = new IntersectionObserver(callback, options);
  • callback: 교차 상태가 변경될 때 실행될 함수
  • options: 관찰자의 동작을 제어하는 옵션 객체

실제로 각 섹션마다 section 태그가 달려있어서 해당 section에 각 섹션에 대한 id를 부여하고 querySelectorAll를 사용해서 각 섹션에 observe를 설정하여 모두 관찰 대상으로 등록했다.

<Headline id={MAIN_SECTIONS.HEADLINE} />
<Lottery id={MAIN_SECTIONS.LOTTERY} />
<Rush id={MAIN_SECTIONS.RUSH} />
<LearnMore id={MAIN_SECTIONS.LEARN_MORE} />
const sections = document.querySelectorAll("section");
sections.forEach((section) => observer.observe(section));

그리고 dark 모드를 적용할 section들만 따로 Props 배열로 받아서 if 문으로 처리를 통해 헤더 스타일을 지정해주었다.

const observer = new IntersectionObserver(
    (entries) => {
        entries.forEach((entry) => {
            // 항목이 화면에 보이는 경우 실행
            if (entry.isIntersecting) {
                // 현재 활성 섹션으로 설정
                const sectionId = entry.target.id as SectionKey;
                setActiveSection(sectionId);

                /* 해당 섹션 id가 darkSections 배열에 포함되는 경우 
                   헤더 타입을 "dark"로 설정하고, 
                   그렇지 않으면 "light"로 설정 */
                const newHeaderType: HeaderType = config.darkSections.includes(sectionId)
                    ? "dark"
                    : "light";
                setHeaderType(newHeaderType);
            }
        });
    },
    {
        root: containerRef.current,
        threshold: 0.8,
    }
);

📚 학습 정리

🗂️ 멘토링

Clone this wiki locally