[태그:] Next.js

  • Context API vs 상태관리 라이브러리

    상태관리는 왜 필요한가?

    리액트는 기본적으로 단방향 데이터 흐름을 가진다.

    부모 → 자식으로 props로 전달하며 UI를 구성하는데, 앱이 커질수록 이 구조에서 문제가 발생한다.

     

    여러 컴포넌트가 같은 데이터를 필요로 하거나, 중첩 전달이 반복되고 이벤트가 깊은 자식에서 발생해 상위 컴포넌트에 영향을 주는 등의 상황을 마주하게 되면 컴포넌트 간 데이터 공유를 쉽게 만드는 구조가 필요하다.

    → 상태 관리 라이브러리 도입의 필요성

     

    Context API

    리액트에서 기본으로 제공하는 전역 상태 컨텍스트로, props drilling(중첩 전달)을 줄이기 위해 설계되었다.

    리액트 내장 기능으로 편하게 사용 가능(createContext, useContext)하며, Provider → Consumer 구조로 전역 데이터를 공유한다.

    const ThemeContext = createContext();
    function App() {
      return (
        <ThemeContext.Provider value="dark">
          <Toolbar />
        </ThemeContext.Provider>
      );
    }
    function Toolbar() {
      const theme = useContext(ThemeContext);
      return <Button theme={theme} />;
    }

    장점

    • 외부 의존성 X
    • 단순한 전역 상태에 적합하다. e.g. 테마, 언어 설정 등

    단점

    • 상태가 자주 바뀌면 Provider 전체가 리렌더링 된다.
    • 규모가 커지면 성능 저하 및 관리 복잡도 증가.

     

    Redux

    상태의 중앙집중 관리 모델.

    Redux는 모든 상태를 하나의 Store에 보관, state를 오직 action과 reducer를 통해서만 수정.

    const initialState = { count: 0 };
    function counterReducer(state = initialState, action) {
      switch (action.type) {
        case 'increment':
          return { count: state.count + 1 };
        default:
          return state;
      }
    }

    장점

    • 명확한 상태 추적: action log, time travel debug 가능.
    • 대규모 팀, 복잡한 상태 흐름에서 일관성 유지 용이.
    • 미들웨어를 통한 확장성(redux-thunk, redux-saga 등).

    단점

    • 보일러플레이트 코드가 많음(action, reducer, dispatch 반복)
    • 초기 진입장벽이 높고, 단순 앱에는 과한 구조.

    → 명확한 구조와 예측 가능성이 필요한 대규모 프로젝트에 적합함.

     

    Zustand

    Redux의 철학을 유지하며 코드 복잡도를 최소화한 라이브러리.

    Hook 기반으로 Store하며 Context나 Provider 불필요,

    React의 리렌더링 성능을 최적화했다.

    const useStore = create((set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }));

    장점

    • 매우 간단한 API
    • 부분 구독(selectors)으로 불필요한 리렌더링 방지
    • Immer, persist(스토리지 저장) 등 기본 지원.

    단점

    • Redux처럼 상태 변경 추적(logging, time travel) 기능은 부족.
    • 너무 자유도가 높음 → 일관된 패턴 유지 어려움

    → 단일 페이지나 컴포넌트 중심의 중간 규모 프로젝트에 이상적.

     

    Recoil

    React에 최적화된 의존성 기반 상태 관리.

    Facebook이 React 전용으로 설계한 상태 그래프 기반 라이브러리로,

    각 상태 단위를 atom으로 관리하며 selector를 통해 파생 상태(derived state)를 계산한다.

    const countState = atom({ key: 'count', default: 0 });
    const doubleCount = selector({
      key: 'doubleCount',
      get: ({ get }) => get(countState) * 2,
    });

    장점

    • useState처럼 자연스러운 사용성
    • atom 간 의존성 자동 관리 → 부분 리렌더링 최소화
    • React Suspense, concurrent feature와 호환.

    단점

    • 조금 불안정한 상태.
    • 디버깅/도구 지원이 Redux만큼 성숙하지 않음.

    → React 내부 동작과 밀접하게 통합, 차세대 React 환경과 잘 맞음

     

    그래서 뭘 써야 할까?

    구분특징적합한 경우
    Context API리액트 기본 내장, 단순한 전역 데이터 공유다크 모드, 로케일, 간단한 설정 값
    Redux중앙집중 + 명확한 상태 흐름, 미들웨어 풍부대규모 SPA, 협업이 많은 프로젝트
    ZustandHook 기반, 코드 간결, 빠른 성능스타트업, MVP, UI 상태 중심 앱
    Recoilatom/selector 기반, React 친화적React 18+ 기반 신규 프로젝트
  • Core Web Vitals 대응

    Core Web Vitals?

    구글이 사용자 경험을 수치화하기 위해 제시하는 핵심 성능 지표.

    2021년부터는 검색 순위에도 직접 반영되고 있어서 단순한 성능 최적화를 넘어 비즈니스 성과와도 직결된다.

    이 지표들은 단순히 빠르고 느리고에서 결정되는 것이 아닌, 실제 사용자 경험에서 반영, 비롯된다.

    Core Web Vitals의 세 가지 주요 지표

    LCP(Largest Contentful Paint)

    사용자가 페이지에서 가장 큰 콘텐츠 요소(이미지, 동영상, 큰 텍스트 블록 등)를 보는데 소요되는 시간.

    페이지를 인식하는 순간을 의미한다.

    (권장 ≤ 2.5초)

    로딩이 늦으면 사용자는 사이트가 느리다고 느끼고,
    첫인상이 중요한 페이지(e.g. 랜딩 페이지)에서 LCP 지연은 이탈율에 직결된다.

    INP(Interaction to Next Paint)

    사용자가 페이지에서 클릭/탭/키 입력을 했을 때, 화면이 반응하는 속도.

    (권장 ≤ 200ms)

    단순히 클릭에 의한 반응 속도가 아닌, 사용자의 모든 입력을
    브라우저가 화면에 그 변화를 반영하는 데 걸린 전체 시간을 측정하는 지표.

    → 사용자가 200ms 이상 기다리면 버벅거린다고 느끼게 된다.

    CLS(Cumulative Layout Shift)

    페이지 로딩 중 예상치 못한 레이아웃 이동 정도.

    (권장 ≤ 0.1)

    페이지 로딩 중 갑자기 버튼이 아래로 밀리거나, 텍스트가 튀면서 광고가 도중에 삽입되는 현상,

    이러한 예기치 못한 움직임으로 인해 사용자 경험이 저해되는지를 측정하는 지표.

    LCP 최적화

    문제 원인

    • 첫 화면에 있는 큰 이미지나 Hero 배너 로딩이 느린 경우
    • 렌더링 차단 리소스(CSS/JS)가 많아 페인트가 지연되는 경우

    *Hero 배너

    웹 사이트 첫 화면의 가장 눈에 띄는 대형 영역(이미지, 비디오 등).

    해결 방법

    1. 이미지 최적화


      • Next.js 의 next/image 컴포넌트 활용. → 자동 압축/리사이징/WebP 변환

      • Hero 영역 이미지는 반드시 priority 속성 추가.
      • next/image는 기본적으로 이미지를 lazy loading 처리. but 첫화면 렌더 이미지는 즉시 불러와야 하는데,priority를 추가하면 네트워크 요청 우선 순위를 올려 해당 이미지를 먼저 다운로드하게 됨.

    *WebP

    구글이 개발한 차세대 이미지 포맷, JPG/PNG 대비 평균 30% ~ 40% 용량 절감.같은 화질을 더 작은 용량으로 표현 가능 → 네트워크 전송 시간을 단축함으로 LCP를 개선할 수 있다.

    대부분의 브라우저에서 지원 중.

    1. 폰트 최적화

      • next/font 를 활용하여 Google Fonts를 직접 번들에 포함시킴
      • font-display: swap으로 폰트 로딩 시 FOUT(깜빡임) 최소화
    2. Critical CSS 최소화


    페이지가 처음 그려지는데 꼭 필요한 최소한의 CSS를 Critical CSS라 한다.

    즉 필수 스타일만 먼저 로드하고, 나머지는 lazy loading 처리하는 것이 효율적.

    • 사용하지 않는 CSS 제거(Tree Shaking)
    • CSS-in-JS 라이브러리 사용 시, 서버사이드에서 Critical CSS를 추출.
    1. 서버 응답 속도 개선

    API 요청 지연을 줄이고, CDN 캐싱 적극 활용.

    INP 최적화

    문제 원인

    • 메인 스레드를 오래 점유하는 무거운 JavaScript 실행.
    • 이벤트 핸들러 내부에서 비효율적인 연산 발생.
    • 애니메이션을 GPU가 아닌 CPU로 처리, 동작.

    해결 방법

    1. JavaScript 최소화

      • dynamic import 로 페이지 진입 시 불필요한 JS 로딩 지연
      • 이벤트 핸들러에서 무거운 연산은 requestIdleCallback 또는 Web Worker로 분리.
    2. Re-render 줄이기

      • React 상태 관리 시 Context 남용 금지 → useMemo, useCallback으로 불필요한 렌더링 방지
      • Zustand, Jotai 같은 fine-grained 상태 관리 도구 고려
    3. 애니메이션 최적화

      • transform/opacity 기반 애니메이션 사용 → GPU 가속 활용
      • requestAnimationFrame으로 프레임 제어

    CLS 최적화

    문제 원인

    • 이미지/광고 영역에 고정 크기 지정 x
    • 웹 폰트 로딩 지연으로 텍스트가 갑자기 재배치.
    • 동적으로 삽입되는 배너, 알림 영역

    해결 방법

    1. 고정 크기 지정

      • 가능하면 이미지/비디오/컴포넌트에 width, height 명시

        but, 반응형 광고가 필요할 수도 있음. 최소/최대 크기 범위를 지정하는 대처가 필요.
    2. 폰트 로딩 전략

      • next/font 를 사용해 FOUT 최소화
      • 또는 fallback 폰트와 유사한 폭을 가진 폰트 선택.
    3. UI 요소 안정화

      • 레이지 로딩(Lazy loading) 요소는 placeholder/skeleton 미리 확보
      • 상단 배너 삽입 시, 최초 레이아웃에서 공간 확보.

    측정과 모니터링

    개발 단계

    Chrome DevTools → Performance 패널 / Lighthouse → 성능 시뮬레이션

    실서비스 단계

    • Google Search Console → Core Web Vitals 리포트
    • Web Vitals JS 라이브러리 → 실제 사용자 데이터(RUM, Real User Monitoring) 수집
  • 구글 크롤러 최적화

    구글 크롤러의 동작 방식

    구글봇(Googlebot)은 단순한 텍스트 수집기가 아니라,

    실제로 크롬 기반의 렌더링 엔진을 이용해

    페이지를 처리한다. 이 과정은 보통 세 단계로 이루어진다.

    크롤링(Crawling)

    새로운 URL을 찾아내고, 서버에 요청을 보내 HTML 문서를 받아온다.

    robots.txt와 같은 파일을 참고하여 접근 가능한 URL만 수집한다.

    렌더링(Rendering)

    받은 HTML을 파싱하고, 필요한 경우 JavaScript도 실행하여 DOM을 완성한다.

    CSS, 이미지, 폰트 등 외부 리소스도 요청하기 때문에, 네트워크 환경이나 차단 정책에 따라

    일부가 누락될 수 있다.

    인덱싱(Indexing)

    완성된 DOM 트리와 메타데이터를 기반으로 페이지를 데이터베이스에 저장한다.

    이후 검색 결과에 노출될 수 있게 된다.

    렌더링 최적화

    CSR 페이지의 문제

    CSR(Client-Side Rendering) 페이지는 초기 HTML이 비어 있어,

    구글봇이 렌더링 큐에서 JS를 실행해야만 인덱싱할 수 있다.

    이 과정이 지연되면 검색 노출이 늦어지거나 일부 페이지는 아예 인덱싱되지 X

    SSR/SSG의 강점

    SSR(Server-Side Rendering)이나 SSG(Static Site Generation)는 완성된 HTML을 제공하므로,

    크롤러가 즉시 콘텐츠를 인식할 수 있다.

    Critical Rendering Path 최적화

    구글봇은 리소스 요청에 제한이 있고, JS/CSS 파일이 크면 크롤링 비용이 증가한다.

    따라서, 필수 리소스만 우선 제공하고, 나머지는 lazy 로딩하는 것이다.

    *next/script의 strategy=”lazyOnload”를 활용하면 비핵심 JS를 늦게 로딩시킬 수 있다.

    인덱싱 최적화

    메타 태그 관리

    <title>, <meta name=”description”>은 검색 결과에서 직접 노출되는 요소다.

    Next.js 13+에서는 app/ 디렉토리의 metadata API를 사용하여 페이지별 메타데이터를 선언할 수 있다.

    Canonical URL

    동일 콘텐츠가 여러 경로로 접근 가능할 경우, rel=”canonical” 태그를 통해 대표 URL을 지정해야 한다.

    e.g. /products/macbook-pro 와 /products?item=123 → canonical을 /products/macbook-pro로 통일

    구조화 데이터 (Structured Data)

    JSON-LD를 이용해 구글에 콘텐츠 의미를 전달한다.

    e.g. 제품 페이지에는 가격, 재고 상태, 리뷰 평점을 포함한 schema.org 마크업을 추가

    Core Web Vitals와 SEO

    구글은 사용자 경험 지표를 검색 랭킹에 반영한다. 대표적인 지표는 다음과 같다.

    • LCP (Largest Contentful Paint): 가장 큰 요소가 화면에 그려지는 시간 (권장 2.5초 이내)
    • FID (First Input Delay): 첫 사용자 입력에 반응하는 시간 (권장 100ms 이내)
    • CLS (Cumulative Layout Shift): 레이아웃이 얼마나 안정적인지 (권장 0.1 이내)

    Next.js에서 할 수 있는 최적화 예시:

    • next/image로 이미지 최적화
    • 코드 스플리팅과 dynamic import
    • prefetch를 통한 라우팅 최적화

    국제화(i18n)와 SEO(글로벌)

    글로벌 서비스를 운영한다면 hreflang 속성을 통해 언어/지역별 페이지를 명확히 구분해야 하며

    Next.js i18n 라우팅을 활용하면 /en, /ko, /jp와 같은 경로를 쉽게 관리할 수 있다.

    그렇지 않으면 다국어 페이지가 중복 콘텐츠로 인식될 수 있다.

  • Hydration과 SEO

    Hydration ?

    SSR이나 SSG를 통해 서버에서 완성된 HTML을 내려주더라도, 브라우저가 곧바로 동작 가능한

    SPA(Single Page Application)로 변환되는 것은 아니다.

    React 기반 프레임워크(Next.js 포함)에서는 이 과정을 Hydration이라고 부른다.

    • 서버에서 내려온 HTML은 정적 콘텐츠일 뿐, 이벤트 핸들러나 상태 관리 기능이 없는 “죽은 페이지”다.
    • 브라우저가 React의 JS 번들을 실행하여, 기존 HTML을 DOM과 연결하고 이벤트를 주입하는 과정이 Hydration이다.
    • 즉, Hydration이 끝나야 비로소 사용자가 버튼을 클릭하거나 상태 변경을 할 수 있다.

     

     

    Hydration 과정에서 발생할 수 있는 문제

    Hydration Mismatch

    • 서버에서 생성된 HTML과 클라이언트에서 렌더링된 결과가 다를 경우, React는 경고를 발생시키거나 DOM을 다시 그린다.
    • 이때 화면이 깜빡이거나, 의도치 않은 레이아웃 깨짐이 발생한다.
    • SEO 차원에서는 검색엔진이 본 HTML과 실제 사용자 화면이 달라질 수 있어, 콘텐츠 신뢰성에 영향을 줄 수 있다.

    JavaScript 번들 크기 문제

    • SSR/SSG를 하더라도 Hydration 단계에서는 JS 번들을 모두 다운로드하고 실행해야 한다.
    • 번들이 크면 TTI(Time To Interactive)가 늦어져 Core Web Vitals 점수가 하락하고, 이는 곧 SEO 순위에 반영된다.

    비동기 데이터 처리

    • useEffect에서 데이터를 불러오는 경우, Hydration 시점에는 아직 데이터가 없어 빈 화면이 잠깐 노출될 수 있다.
    • 구글봇은 기본적으로 HTML만 본다. Hydration 이후 동적으로 채워지는 콘텐츠는 렌더링 큐 처리를 기다려야 하므로 인덱싱이 지연될 수 있다.

     

     

    Hydration과 SEO의 관계

    Hydration 자체는 SEO에 직접적으로 반영되지 않는다.(검색엔진은 서버에서 내려온 HTML을 우선으로 보므로)

    하지만 Hydration 과정이 실패하거나 지연되면, 다음과 같은 문제가 발생할 수 있다.

    • 서버 HTML과 실제 사용자 화면 불일치 → 콘텐츠 신뢰도 저하
    • JS 실행 지연으로 인한 사용자 경험 악화 → Core Web Vitals 점수 하락 → 검색 순위에 간접적 영향
    • 구글봇이 JS 렌더링을 기다려야만 콘텐츠를 완전히 인덱싱 → 노출 지연

    따라서, Next.js에서는 Hydration을 반드시 해결하라고 권장하고 있다.

     

     

    실무에서의 Hydration

    Hydration Mismatch 최소화

    서버와 클라이언트 렌더링 로직을 일치시키고, 조건부 렌더링을 신중히 사용해야 한다.

    e.g. typeof window !== “undefined”

    번들 최적화

    dynamic import로 불필요한 JS를 분리하고, critical path에 해당하는 컴포넌트만

    우선 Hydration하도록 구성한다.

    Skeleton / Placeholder

    비동기 데이터는 빈 HTML을 노출하지 않고, 사용자 경험과 검색엔진 노출을 동시에 보완하는 UI 제공.

    e.g. Skeleton UI 등

     

     

    검색엔진 최적화 관점에서

    Hydration은 단순히 클라이언트 상호작용을 가능하게 하는 과정이 아니라,

    SEO와 성능 지표(Core Web Vitals)에도 직결되는 요소다.

    따라서 SEO를 고려하는 페이지라면 다음과 같은 전략이 필요.

    • 콘텐츠 중심 페이지 → 서버 HTML에 핵심 텍스트/메타정보를 포함하고, JS는 보조적 역할만 담당
    • 인터랙션 중심 페이지 → CSR 비중이 커도 무방하므로, Hydration 최적화보다 사용자 경험에 집중

  • Next js 렌더링 방식과 검색엔진

    검색엔진이 웹페이지를 처리할때

    검색엔진(특히 구글봇)은 단순히 HTML 텍스트만 긁어가는 것이 아니라, 실제 브라우저처럼 페이지를 “렌더링”하여 콘텐츠를 파악한다. 이 과정은 크게 두 단계로 나뉜다.

    이 두 단계에를 거치면서 발생하는 문제는 CSR(Client Side Rendering) 구조로,

    • CSR은 최초 HTML에 <div id=”root”></div> 같은 빈 껍데기만 제공한다.
    • 구글봇이 JS 실행까지 기다려야 하므로, 크롤링/인덱싱 속도가 늦어질 수 있다.
    • 반대로 SSR(서버사이드 렌더링)이나 SSG(정적 사이트 생성)은 완성된 HTML을 즉시 제공하므로 검색엔진 친화적이다.

    HTML 크롤링

    요청 시 서버에서 내려주는 순수 HTML 문서를 파싱한다.

    이때 <title>, <meta> 태그, <a> 링크, 텍스트 콘텐츠 등 렌더링 이전에도 노출된 요소들이 가장 먼저 수집된다.

    따라서 이 시점에 의미 있는 콘텐츠가 HTML에 포함되어 있으면, 검색엔진이 곧바로 인덱싱할 수 있다.

    렌더링 큐 처리

    필요할 경우 렌더링 큐에 페이지를 넣어 두고, 나중에 JavaScript 실행 환경에서 페이지를 완전히 렌더링한다.

    JS가 실행되어야만 생성되는 콘텐츠는 이 단계에서야 인덱싱이 가능하다.

    문제는 이 과정이 시간이 오래 걸리고, 리소스가 제한적이어서 모든 페이지를 즉시 처리하지 못한다는 점이다.

    그래서 CSR 구조의 웹사이트는 크롤링/인덱싱 속도가 현저히 늦어지거나, 심하면 누락되는 경우도 발생한다.

    결국, 검색엔진이 HTML 단계에서 바로 읽을 수 있는 정보를 얼마나 제공하느냐가 SEO의 핵심.

    CSR과 SSR의 SEO

    CSR**(Client-Side Rendering)**

    • 동작 방식: 서버에서 최소한의 HTML만 내려주고, 실제 콘텐츠는 브라우저가 JS 실행 후 생성한다.
    • 장점
      • 클라이언트 상태에 따라 화면을 유연하게 바꿀 수 있어, 사용자 경험이 풍부하다.
      • 초기 로딩 이후에는 페이지 전환이 빠르다.
    • 단점
      • 검색엔진이 최초 HTML을 읽을 때는 비어 있는 상태이므로 SEO에 불리하다.
      • 구글봇이 JS 실행까지 기다려야 하므로 인덱싱 속도가 느리다.
      • 일부 검색엔진이나 SNS 봇(카카오톡, 트위터 카드 등)은 JS 실행을 아예 하지 못해 미리보기조차 나오지 않는 경우가 있다.

    SSR(Server-Side Rendering)

    • 동작 방식: 서버에서 요청이 들어올 때마다 HTML을 완전히 렌더링해서 내려준다.
    • 장점
      • 최초 응답에 완성된 HTML이 포함 → 검색엔진이 즉시 인덱싱 가능.
      • 초기 페이지 로딩 속도가 빠르다(LCP 개선).
      • OG(Open Graph), Twitter Card 같은 소셜 메타태그도 문제없이 동작.
    • 단점
      • 모든 요청마다 서버 연산이 필요해, 트래픽이 많으면 부하가 크다.
      • CDN 캐싱 효과를 잘 활용하지 못하면 확장성에서 불리하다.

    SSG(Static Site Generation)

    • 동작 방식: 빌드 시점에 HTML을 생성해두고, 요청 시 그대로 제공한다.
    • 장점
      • 이미 완성된 HTML을 제공하므로 SEO에 최적.
      • CDN 캐싱과 함께 쓰면 서버 부하가 거의 없다.
    • 단점
      • 데이터가 자주 바뀌는 페이지에는 적합하지 않다.
      • 콘텐츠 업데이트 후 빌드 및 배포 과정이 필요하다.

    *ISR(Incremental Static Regeneration)

    • Next.js가 제공하는 SSG의 단점을 보완한 방식.
    • 특정 페이지를 미리 빌드하되, revalidate 주기마다 백그라운드에서 새 HTML을 생성한다.
    • SEO에는 SSG처럼 완성된 HTML을 제공하면서도, 데이터가 일정 주기로 업데이트된다.

    검색엔진 최적화 관점에서

    실무적으로는 Next.js가 이를 위해 페이지별로 렌더링 전략을 다르게 혼합 적용할 수 있게 설계되어 있다.

    즉, SEO가 중요한 페이지는 SSR/SSG, 로그인 뒤 내부 페이지의 일부는 CSR로 처리하는 식이다.

    즉, SEO를 고려한다면 “페이지의 특성”에 맞는 전략적 선택이 필요.

    • 실시간 업데이트 필요한 영역 → SSR + 클라이언트 하이드레이션
    • 상품 상세 → SSG / ISR
    • 랜딩 페이지 → SSR / SSG

  • Next.js(React)에서 네이버 지도 API 깔끔하게 연동하기

    안녕하세요! 이번 포스트에서는 Next.js (React) 프로젝트에서 네이버 지도 API를 연동하는 방법을 알아보겠습니다. 국내 사용자를 대상으로 하는 서비스를 개발한다면 네이버 지도 연동은 필수적인 기능 중 하나인데요. Next.js의 렌더링 방식과 React의 Hook을 활용하여 깔끔하고 효율적으로 구현하는 과정을 단계별로 소개해 드리겠습니다.

    시작하기 전에: 네이버 클라우드 플랫폼 설정

    가장 먼저 네이버 클라우드 플랫폼에서 애플리케이션을 등록하고 Client ID를 발급받아야 합니다. 이 ID는 API 인증에 사용되므로, 없다면 아래 공식 가이드를 참고하여 준비해 주세요.

    타입스크립트를 사용하는 분은 타입 정의 파일을 설치하고 코드를 작성해야 합니다.

    Step 1: 네이버 지도 API 스크립트 로드하기

    Next.js에서 외부 스크립트를 로드할 때는 next/script 컴포넌트를 사용하는 것이 가장 좋습니다. 페이지 로딩을 최적화하고 스크립트 로딩 시점을 제어할 수 있기 때문입니다.

    app/layout.tsx에 아래와 같이 <Script> 태그를 추가합니다.

    // app/contact/page.tsx 또는 지도가 필요한 다른 페이지
    
    import Script from "next/script";
    
    export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
      return (
        <html lang="ko">
          <body>
            <Script strategy="beforeInteractive" src={`https://oapi.map.naver.com/openapi/v3/maps.js?ncpKeyId=${process.env.NEXT_PUBLIC_NAVER_MAP_CLIENT_ID}`} />
            {children}
          </body>
        </html>
      );
    }

    주요 포인트:

    • strategy="beforeInteractive": 페이지가 상호작용 가능해지기 전에 스크립트를 실행하여, 지도 API(window.naver)가 컴포넌트 렌더링 시점에 존재하도록 보장합니다.
    • Client ID 관리: 실제 ID를 코드에 하드코딩하는 대신, .env.local 파일에 환경변수로 저장하고 process.env를 통해 불러오는 방식을 권장합니다. (NEXT_PUBLIC_ 접두사가 있어야 클라이언트에서 접근 가능합니다.)

    Step 2: 지도 표시를 위한 React 컴포넌트 생성하기

    이제 지도를 실제로 렌더링할 컴포넌트를 만듭니다. useRef로 지도를 담을 DOM 요소를 참조하고, useEffect를 사용해 컴포넌트가 마운트된 후에 지도 생성 로직을 실행합니다.

    // components/ContactUsMap.tsx
    
    "use client";
    
    import { useRef, useEffect } from "react";
    
    export default function ContactUsMap() {
      const naver = typeof window !== "undefined" && window.naver;
    
      // 지도를 담을 DOM 요소에 대한 ref 생성
      const mapElement = useRef<HTMLDivElement | null>(null);
    
      useEffect(() => {
        // window.naver 객체가 로드되었는지, ref가 유효한지 확인
        if (!naver || !mapRef.current) return;
    
        // 위도
        const latitude = 37.5716229;
        // 경도
        const longitude = 126.9767879;
    
        // 지도의 중심 좌표 설정
        const location = new naver.maps.LatLng(latitude, longitude);
    
        // 지도 옵션 설정
        const mapOptions: naver.maps.MapOptions = {
          center: location,
          zoom: 17,
          zoomControl: true, // 줌 컨트롤 표시
          scaleControl: false, // 스케일 컨트롤 비표시
        };
    
        // 지도 인스턴스 생성
        const map = new naver.maps.Map(mapElement.current, mapOptions);
    
        // 지도 위에 마커 생성
        new naver.maps.Marker({
          position: location,
          map: map,
        });
      }, []); // 컴포넌트가 처음 마운트될 때 한 번만 실행
    
      return (
        // 지도가 렌더링될 div 요소
        <div ref={mapElement} style={{ minHeight: "400px" }} />
      );
    }

    Step 3: 코드 핵심 분석

    • "use client": useRef, useEffect와 같은 React Hook과 window 객체에 접근하려면 클라이언트 컴포넌트로 선언해야 합니다.
    • useRef<HTMLDivElement>: mapElement라는 ref를 생성하여 JSX의 <div> 요소와 연결합니다. 네이버 지도 API는 이 DOM 요소를 컨테이너로 사용해 지도를 렌더링합니다.
    • useEffect(() => { ... }, []): 이 Hook은 컴포넌트가 클라이언트에서 렌더링되고 DOM에 마운트된 직후에 실행됩니다. window.naver API와 mapElement.current(실제 <div> 요소)에 안전하게 접근할 수 있는 최적의 시점입니다. 의존성 배열을 []로 비워두어 최초 렌더링 시 한 번만 실행되도록 합니다.
    • new naver.maps.Map(...): naver.maps 라이브러리를 사용하여 지도 인스턴스를 생성합니다. 첫 번째 인자로는 지도를 담을 DOM 요소(mapElement.current)를, 두 번째 인자로는 지도 옵션(중심 좌표, 줌 레벨 등)을 전달합니다.
    • new naver.maps.Marker(...): 지도 위에 표시할 마커를 생성하고, map 속성에 지도 인스턴스를 지정하여 마커를 지도에 추가합니다.

    마치며

    지금까지 Next.js 환경에서 next/script와 React Hook을 활용해 네이버 지도 API를 연동하는 방법을 알아보았습니다. 이 패턴을 활용하면 서버사이드 렌더링(SSR)과 클라이언트 렌더링이 혼합된 Next.js 환경에서도 외부 스크립트 기반 라이브러리를 안정적으로 통합할 수 있습니다.

    여기서 더 나아가 커스텀 컨트롤 추가, 정보 창(InfoWindow) 표시 등 다양한 기능은 네이버 지도 API 공식 문서를 참고하여 구현해 보시길 바랍니다.