<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ZENMA_OFFICIAL</title>
        <link>https://velog.io/</link>
        <description>개발하면서 습득한 지식 끄적끄적✍️</description>
        <lastBuildDate>Tue, 24 Dec 2024 11:11:25 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ZENMA_OFFICIAL</title>
            <url>https://velog.velcdn.com/images/zenma_official/profile/d457e5c4-7266-4ea3-974b-046939962c31/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ZENMA_OFFICIAL. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/zenma_official" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[API 엔드포인트와 접근 권한 정리]]></title>
            <link>https://velog.io/@zenma_official/API-%EC%97%94%EB%93%9C%ED%8F%AC%EC%9D%B8%ED%8A%B8%EC%99%80-%EC%A0%91%EA%B7%BC-%EA%B6%8C%ED%95%9C-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@zenma_official/API-%EC%97%94%EB%93%9C%ED%8F%AC%EC%9D%B8%ED%8A%B8%EC%99%80-%EC%A0%91%EA%B7%BC-%EA%B6%8C%ED%95%9C-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 24 Dec 2024 11:11:25 GMT</pubDate>
            <description><![CDATA[<h1 id="api-구조-및-설계">API 구조 및 설계</h1>
<h2 id="환경별-api-설정">환경별 API 설정</h2>
<p><code>API_CONFIG</code>는 개발 환경(<code>development</code>)과 배포 환경(<code>production</code>)에 따라 각각 다른 URL을 참조하도록 설정합니다.</p>
<p><strong>코드:</strong></p>
<pre><code class="language-javascript">const API_CONFIG = {
  development: {
    AUTH_API: &#39;http://localhost:8080/api/auth&#39;,
    USER_API: &#39;http://localhost:8080/api/users&#39;,
    ADMIN_API: &#39;http://localhost:8080/api/admin&#39;,
    ADVERTISING_API: &#39;http://localhost:8080/api/advertising&#39;,
    VERIFICATION_API: &#39;http://localhost:8080/api/verification&#39;,
    REFERRAL_API: &#39;http://localhost:8080/api/referral&#39;,
    UNIV_CERT_API: &#39;https://univcert.com/api/v1&#39;,
  },
  production: {
    AUTH_API: process.env.REACT_APP_AUTH_API,
    USER_API: process.env.REACT_APP_USER_API,
    ADMIN_API: process.env.REACT_APP_ADMIN_API,
    ADVERTISING_API: process.env.REACT_APP_ADVERTISING_API,
    VERIFICATION_API: process.env.REACT_APP_VERIFICATION_API,
    REFERRAL_API: process.env.REACT_APP_REFERRAL_API,
    UNIV_CERT_API: process.env.REACT_APP_UNIV_CERT_API,
  },
};</code></pre>
<hr>
<h2 id="axios-설정-및-인터셉터">Axios 설정 및 인터셉터</h2>
<p>API 호출 전, JWT 토큰을 <code>Authorization</code> 헤더에 추가합니다.</p>
<p><strong>코드:</strong></p>
<pre><code class="language-javascript">axiosInstance.interceptors.request.use(
  (config) =&gt; {
    const token = localStorage.getItem(&#39;token&#39;);
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) =&gt; Promise.reject(error)
);</code></pre>
<hr>
<h2 id="api-엔드포인트">API 엔드포인트</h2>
<h3 id="auth-관련-엔드포인트">Auth 관련 엔드포인트</h3>
<table>
<thead>
<tr>
<th>메서드</th>
<th>경로</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>POST</td>
<td><code>/auth/login</code></td>
<td>사용자 로그인</td>
</tr>
<tr>
<td>POST</td>
<td><code>/auth/register</code></td>
<td>사용자 회원가입</td>
</tr>
<tr>
<td>POST</td>
<td><code>/auth/logout</code></td>
<td>사용자 로그아웃</td>
</tr>
<tr>
<td>DELETE</td>
<td><code>/auth/delete</code></td>
<td>계정 삭제 요청</td>
</tr>
</tbody></table>
<p><strong>코드:</strong></p>
<pre><code class="language-javascript">AUTH: {
  LOGIN: `${API_CONFIG[ENV].AUTH_API}/login`,
  REGISTER: `${API_CONFIG[ENV].AUTH_API}/register`,
  LOGOUT: `${API_CONFIG[ENV].AUTH_API}/logout`,
  DELETE: `${API_CONFIG[ENV].AUTH_API}/delete`,
},</code></pre>
<hr>
<h3 id="user-관련-엔드포인트">User 관련 엔드포인트</h3>
<table>
<thead>
<tr>
<th>메서드</th>
<th>경로</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>GET</td>
<td><code>/users/profile</code></td>
<td>프로필 조회</td>
</tr>
<tr>
<td>PATCH</td>
<td><code>/users/update</code></td>
<td>사용자 정보 수정</td>
</tr>
</tbody></table>
<p><strong>코드:</strong></p>
<pre><code class="language-javascript">USER: {
  PROFILE: `${API_CONFIG[ENV].USER_API}/profile`,
  UPDATE: `${API_CONFIG[ENV].USER_API}/update`,
},</code></pre>
<hr>
<h3 id="admin-관련-엔드포인트">Admin 관련 엔드포인트</h3>
<table>
<thead>
<tr>
<th>메서드</th>
<th>경로</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>GET</td>
<td><code>/admin/dashboard</code></td>
<td>대시보드 통계 조회</td>
</tr>
<tr>
<td>GET</td>
<td><code>/admin/list</code></td>
<td>광고 목록 조회</td>
</tr>
<tr>
<td>PATCH</td>
<td><code>/admin/patchRefedReward</code></td>
<td>추천 보상 패치</td>
</tr>
</tbody></table>
<p><strong>코드:</strong></p>
<pre><code class="language-javascript">ADMIN: {
  DASHBOARD: `${API_CONFIG[ENV].ADMIN_API}/dashboard`,
  LIST: `${API_CONFIG[ENV].ADMIN_API}/list`,
  PATCH_REFED_REWARD: `${API_CONFIG[ENV].ADMIN_API}/patchRefedReward`,
},</code></pre>
<hr>
<h2 id="api-함수">API 함수</h2>
<h3 id="auth-api-함수">Auth API 함수</h3>
<table>
<thead>
<tr>
<th>함수</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>login</code></td>
<td>로그인 처리</td>
</tr>
<tr>
<td><code>register</code></td>
<td>회원가입 처리</td>
</tr>
<tr>
<td><code>logout</code></td>
<td>로그아웃 처리</td>
</tr>
<tr>
<td><code>deleteAccount</code></td>
<td>계정 삭제 요청</td>
</tr>
</tbody></table>
<p><strong>코드:</strong></p>
<pre><code class="language-javascript">export const authAPI = {
  login: async (credentials) =&gt; {
    const response = await axiosInstance.post(API_ENDPOINTS.AUTH.LOGIN, credentials);
    return response.data;
  },
  register: async (userData) =&gt; {
    const response = await axiosInstance.post(API_ENDPOINTS.AUTH.REGISTER, userData);
    return response.data;
  },
  logout: async () =&gt; {
    const response = await axiosInstance.post(API_ENDPOINTS.AUTH.LOGOUT);
    return response.data;
  },
  deleteAccount: async () =&gt; {
    const response = await axiosInstance.delete(API_ENDPOINTS.AUTH.DELETE);
    return response.data;
  },
};</code></pre>
<hr>
<h3 id="advertising-api-함수">Advertising API 함수</h3>
<table>
<thead>
<tr>
<th>함수</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>createAd</code></td>
<td>광고 생성</td>
</tr>
<tr>
<td><code>getAdList</code></td>
<td>광고 목록 조회</td>
</tr>
<tr>
<td><code>updateAd</code></td>
<td>광고 수정</td>
</tr>
<tr>
<td><code>deleteAd</code></td>
<td>광고 삭제</td>
</tr>
</tbody></table>
<p><strong>코드:</strong></p>
<pre><code class="language-javascript">export const advertisingAPI = {
  createAd: async (adData) =&gt; {
    const response = await axiosInstance.post(API_ENDPOINTS.ADVERTISING.CREATE, adData);
    return response.data;
  },
  getAdList: async () =&gt; {
    const response = await axiosInstance.get(API_ENDPOINTS.ADVERTISING.LIST);
    return response.data;
  },
  updateAd: async (adData) =&gt; {
    const response = await axiosInstance.patch(API_ENDPOINTS.ADVERTISING.UPDATE, adData);
    return response.data;
  },
  deleteAd: async (adTitleOrIdentifier) =&gt; {
    const response = await axiosInstance.delete(API_ENDPOINTS.ADVERTISING.DELETE, {
      data: adTitleOrIdentifier,
    });
    return response.data;
  },
};</code></pre>
<hr>
<h3 id="verification-api-함수">Verification API 함수</h3>
<table>
<thead>
<tr>
<th>함수</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>upload</code></td>
<td>인증 파일 업로드</td>
</tr>
<tr>
<td><code>getStatus</code></td>
<td>인증 상태 조회</td>
</tr>
</tbody></table>
<p><strong>코드:</strong></p>
<pre><code class="language-javascript">export const verificationAPI = {
  upload: async (verifyUploadDTO) =&gt; {
    const response = await axiosInstance.post(API_ENDPOINTS.VERIFICATION.UPLOAD, verifyUploadDTO);
    return response.data;
  },
  getStatus: async () =&gt; {
    const response = await axiosInstance.get(API_ENDPOINTS.VERIFICATION.STATUS);
    return response.data;
  },
};</code></pre>
<hr>
<h3 id="referral-api-함수">Referral API 함수</h3>
<table>
<thead>
<tr>
<th>함수</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>validate</code></td>
<td>추천 코드 검증</td>
</tr>
<tr>
<td><code>getStats</code></td>
<td>추천 통계 조회</td>
</tr>
</tbody></table>
<p><strong>코드:</strong></p>
<pre><code class="language-javascript">export const referralAPI = {
  validate: async (codeString) =&gt; {
    const response = await axiosInstance.post(API_ENDPOINTS.REFERRAL.VALIDATE, codeString);
    return response.data;
  },
  getStats: async () =&gt; {
    const response = await axiosInstance.get(API_ENDPOINTS.REFERRAL.STATS);
    return response.data;
  },
};</code></pre>
<hr>
<h3 id="에러-처리-유틸리티-함수">에러 처리 유틸리티 함수</h3>
<table>
<thead>
<tr>
<th>함수</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>handleApiError</code></td>
<td>공통 에러 처리</td>
</tr>
</tbody></table>
<p><strong>코드:</strong></p>
<pre><code class="language-javascript">export const handleApiError = (error) =&gt; {
  if (error.response) {
    const status = error.response.status;
    switch (status) {
      case API_STATUS.UNAUTHORIZED:
        return API_ERROR_MESSAGES.UNAUTHORIZED;
      case API_STATUS.FORBIDDEN:
        return API_ERROR_MESSAGES.FORBIDDEN;
      case API_STATUS.NOT_FOUND:
        return API_ERROR_MESSAGES.NOT_FOUND;
      default:
        return error.response.data.message || API_ERROR_MESSAGES.DEFAULT;
    }
  }
  if (error.request) {
    return API_ERROR_MESSAGES.NETWORK_ERROR;
  }
  return API_ERROR_MESSAGES.DEFAULT;
};</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[React와 OpenCage API를 활용한 역지오코딩 구현기 🚀]]></title>
            <link>https://velog.io/@zenma_official/React%EC%99%80-OpenCage-API%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%97%AD%EC%A7%80%EC%98%A4%EC%BD%94%EB%94%A9-%EA%B5%AC%ED%98%84%EA%B8%B0</link>
            <guid>https://velog.io/@zenma_official/React%EC%99%80-OpenCage-API%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%97%AD%EC%A7%80%EC%98%A4%EC%BD%94%EB%94%A9-%EA%B5%AC%ED%98%84%EA%B8%B0</guid>
            <pubDate>Mon, 23 Dec 2024 12:55:02 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>&quot;위치를 알면 주소를 알 수 있다! React와 OpenCage API를 활용한 역지오코딩 기술 구현을 소개함.&quot;</p>
</blockquote>
<hr>
<h2 id="프로젝트-개요와-역지오코딩의-필요성">프로젝트 개요와 역지오코딩의 필요성</h2>
<h3 id="프로젝트-개요-🛠">프로젝트 개요 🛠</h3>
<p>사이트에서 사용자 현재 위치를 기반으로 지역 정보를 설정하는 기능을 구현. 이를 통해 사용자 경험(UX)을 향상시키는 것이 목표.</p>
<h3 id="역지오코딩의-필요성-🌍">역지오코딩의 필요성 🌍</h3>
<ul>
<li>위치 기반 서비스(지도, 검색, 날씨 앱 등)에 필수.</li>
<li>사용자 위치를 지역 정보로 변환해 맞춤형 서비스를 제공 가능.</li>
</ul>
<hr>
<h2 id="역지오코딩의-기본-개념-🌐">역지오코딩의 기본 개념 🌐</h2>
<h3 id="지오코딩-vs-역지오코딩">지오코딩 vs 역지오코딩</h3>
<table>
<thead>
<tr>
<th>개념</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>지오코딩</strong></td>
<td>주소를 좌표(위도, 경도)로 변환</td>
</tr>
<tr>
<td><strong>역지오코딩</strong></td>
<td>좌표(위도, 경도)를 주소로 변환</td>
</tr>
</tbody></table>
<h3 id="역지오코딩-활용-사례">역지오코딩 활용 사례</h3>
<ul>
<li>지도 서비스(Google Maps)</li>
<li>위치 기반 검색(네이버 플레이스)</li>
<li>날씨 정보 제공(AccuWeather)</li>
</ul>
<hr>
<h2 id="사용한-기술-스택-💻">사용한 기술 스택 💻</h2>
<ul>
<li><strong>React</strong>: 컴포넌트 기반 UI 개발.</li>
<li><strong>OpenCage Geocoding API</strong>: 좌표를 주소로 변환.</li>
<li><strong>Axios</strong>: HTTP 요청 처리.</li>
<li><strong>React Spinners</strong>: 로딩 인디케이터.</li>
<li><strong>React Router</strong>: 클라이언트 사이드 라우팅.</li>
</ul>
<hr>
<h2 id="역지오코딩-구현-과정-🔧">역지오코딩 구현 과정 🔧</h2>
<h3 id="1-opencage-geocoding-api-설정">1. OpenCage Geocoding API 설정</h3>
<h4 id="api-키-발급">API 키 발급</h4>
<ol>
<li>OpenCage 데이터 계정 생성.</li>
<li>API 키 발급 후 프로젝트에 환경 변수로 저장.</li>
</ol>
<h4 id="api-요청-방식">API 요청 방식</h4>
<pre><code class="language-javascript">const fetchRegion = async (latitude, longitude) =&gt; {
  try {
    const response = await axios.get(&#39;https://api.opencagedata.com/geocode/v1/json&#39;, {
      params: {
        key: process.env.REACT_APP_OPENCAGE_API_KEY,
        q: `${latitude}+${longitude}`,
        pretty: 1,
        no_annotations: 1,
        language: &#39;ko&#39;,
      },
    });
    return response.data;
  } catch (error) {
    console.error(&#39;API 호출 실패:&#39;, error);
  }
};</code></pre>
<h3 id="2-locationjs-컴포넌트-구현">2. Location.js 컴포넌트 구현</h3>
<pre><code class="language-javascript">import React, { useEffect, useState } from &#39;react&#39;;
import axios from &#39;axios&#39;;
import { ClipLoader } from &#39;react-spinners&#39;;
import { useNavigate } from &#39;react-router-dom&#39;;

const LocationSetter = ({ setRegion }) =&gt; {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const navigate = useNavigate();

  const fetchRegion = async (latitude, longitude) =&gt; {
    try {
      const response = await axios.get(&#39;https://api.opencagedata.com/geocode/v1/json&#39;, {
        params: {
          key: process.env.REACT_APP_OPENCAGE_API_KEY,
          q: `${latitude}+${longitude}`,
          language: &#39;ko&#39;,
        },
      });
      const region = response.data.results[0]?.components.city;
      if (region) {
        setRegion(region);
        localStorage.setItem(&#39;userRegion&#39;, region);
        navigate(`/?in=${encodeURIComponent(region)}`, { replace: true });
      } else {
        setError(true);
      }
    } catch (err) {
      console.error(&#39;역지오코딩 실패:&#39;, err);
      setError(true);
    } finally {
      setLoading(false);
    }
  };

  const getLocation = () =&gt; {
    if (!navigator.geolocation) {
      setError(true);
      return;
    }
    navigator.geolocation.getCurrentPosition(
      (position) =&gt; {
        fetchRegion(position.coords.latitude, position.coords.longitude);
      },
      () =&gt; setError(true),
      { enableHighAccuracy: true }
    );
  };

  useEffect(() =&gt; {
    getLocation();
  }, []);

  if (loading) return &lt;ClipLoader color=&quot;#3b82f6&quot; loading={true} size={50} /&gt;;
  if (error) return &lt;p&gt;위치 정보를 가져올 수 없습니다.&lt;/p&gt;;
  return null;
};

export default Location;</code></pre>
<hr>
<h2 id="주요-구현-내용-✍️">주요 구현 내용 ✍️</h2>
<ol>
<li><strong>Geolocation API 사용</strong>:<ul>
<li>사용자 위치(위도, 경도) 가져오기.</li>
</ul>
</li>
<li><strong>OpenCage API 호출</strong>:<ul>
<li>좌표를 주소로 변환해 지역 정보 추출.</li>
</ul>
</li>
<li><strong>상태 관리</strong>:<ul>
<li>React <code>useState</code>로 로딩 및 오류 상태 관리.</li>
</ul>
</li>
<li><strong>React Router 연동</strong>:<ul>
<li>지역 정보를 URL 파라미터로 추가해 상태 공유.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="배운-점-📚">배운 점 📚</h2>
<ol>
<li><strong>비동기 처리</strong>: API 호출과 상태 업데이트 간의 흐름을 효율적으로 관리하는 방법.</li>
<li><strong>에러 처리</strong>: 사용자에게 명확한 피드백을 제공하는 UI의 중요성.</li>
<li><strong>로컬 스토리지 활용</strong>: 지역 정보를 캐싱해 불필요한 API 호출 방지.</li>
</ol>
<hr>
<h2 id="구현-결과-🎉">구현 결과 🎉</h2>
<ul>
<li>사용자 현재 위치 기반 지역 정보 설정.</li>
<li>브라우저 권한 설정 안내로 UX 개선.</li>
<li>로컬 스토리지를 활용한 성능 최적화.</li>
</ul>
<hr>
<h2 id="참고-자료-📖">참고 자료 📖</h2>
<ul>
<li><a href="https://opencagedata.com/api">OpenCage Geocoding API 공식 문서</a></li>
<li><a href="https://developer.mozilla.org/ko/docs/Web/API/Geolocation_API">React Geolocation 사용 가이드</a></li>
<li><a href="https://reactrouter.com/">React Router 공식 문서</a></li>
</ul>
<p>질문이나 의견은 댓글✌️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드에서 API 로직을 별도로 관리해야 하는 이유 🚀]]></title>
            <link>https://velog.io/@zenma_official/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90%EC%84%9C-API-%EB%A1%9C%EC%A7%81%EC%9D%84-%EB%B3%84%EB%8F%84%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@zenma_official/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90%EC%84%9C-API-%EB%A1%9C%EC%A7%81%EC%9D%84-%EB%B3%84%EB%8F%84%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Fri, 13 Dec 2024 18:36:56 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>&quot;API 요청 로직을 체계적으로 관리하면, 유지보수와 협업이 편해진다! 이 글에서 API 로직 분리의 중요성과 방법을 알아보자.&quot;</p>
</blockquote>
<hr>
<h2 id="왜-api-로직을-별도로-관리해야-할까-🤔">왜 API 로직을 별도로 관리해야 할까? 🤔</h2>
<p>프론트엔드 개발을 진행하다 보면, 컴포넌트 곳곳에 API 호출 로직이 흩어져 있는 경우를 종종 보게 됨. 이런 구조는 프로젝트 규모가 커질수록 다음과 같은 문제를 야기함:</p>
<ul>
<li><strong>유지보수 어려움:</strong> API 엔드포인트나 응답 형식이 바뀌면 관련된 모든 코드를 찾아서 수정해야 함.</li>
<li><strong>중복 코드 증가:</strong> 동일한 API 호출 로직이 여러 컴포넌트에 중복 작성됨.</li>
<li><strong>협업 비효율:</strong> 코드 구조가 비직관적이라 팀원 간 협업이 어려움.</li>
</ul>
<p>API 로직을 별도로 분리해 관리하면 이런 문제를 예방하고, 보다 체계적이고 효율적인 코드를 작성할 수 있음.</p>
<hr>
<h2 id="api-로직-분리가-가져다주는-장점-🌟">API 로직 분리가 가져다주는 장점 🌟</h2>
<h3 id="1-코드-구조화-및-가독성-향상">1. <strong>코드 구조화 및 가독성 향상</strong></h3>
<p>API 요청 로직을 <code>api/</code> 디렉토리로 모아두면 프로젝트 구조가 깔끔해짐. 예를 들어:</p>
<pre><code>📂 src
 ├── 📂 api
 │    ├── auth.js
 │    ├── user.js
 │    ├── admin.js</code></pre><ul>
<li><strong>직관적인 구조:</strong> 각 파일에서 어떤 기능을 다루는지 바로 파악 가능.</li>
<li><strong>코드 리뷰 및 온보딩 간소화:</strong> &quot;인증 관련 API는 auth.js에 있음&quot;처럼 명확한 컨텍스트 제공.</li>
</ul>
<h3 id="2-재사용성-극대화">2. <strong>재사용성 극대화</strong></h3>
<p>공용으로 사용할 API 함수를 정의해두면 중복 코드를 방지 가능. 예:</p>
<pre><code class="language-javascript">// user.js
export const getUserProfile = async () =&gt; {
  return axios.get(&#39;/api/user/profile&#39;);
};

// 컴포넌트에서 호출
import { getUserProfile } from &#39;../api/user&#39;;

const UserProfile = () =&gt; {
  useEffect(() =&gt; {
    getUserProfile().then((data) =&gt; {
      console.log(data);
    });
  }, []);
};</code></pre>
<ul>
<li><strong>중복 제거:</strong> 동일한 API 호출 로직을 재사용 가능.</li>
<li><strong>수정 용이:</strong> 한 곳에서 로직을 수정하면 모든 호출부에 적용됨.</li>
</ul>
<h3 id="3-유지보수성과-확장성-확보">3. <strong>유지보수성과 확장성 확보</strong></h3>
<p>프로젝트가 커질수록 서버 통신 로직이 복잡해짐. 하지만 API 로직을 중앙 관리하면 다음과 같은 장점이 있음:</p>
<ul>
<li><strong>엔드포인트 변경 대응:</strong> 특정 API 엔드포인트가 변경될 때 한 곳만 수정하면 됨.</li>
<li><strong>공통 로직 추가:</strong> 인증 토큰 추가, 에러 처리 등 공통 로직을 쉽게 적용 가능.</li>
</ul>
<pre><code class="language-javascript">// axiosInstance.js
import axios from &#39;axios&#39;;

const instance = axios.create({
  baseURL: &#39;/api&#39;,
});

// 요청에 공통 헤더 추가
instance.interceptors.request.use((config) =&gt; {
  config.headers.Authorization = `Bearer ${localStorage.getItem(&#39;token&#39;)}`;
  return config;
});

export default instance;</code></pre>
<h3 id="4-테스트-용이성-향상">4. <strong>테스트 용이성 향상</strong></h3>
<p>API 호출 로직을 분리하면 단위 테스트 작성이 쉬워짐:</p>
<pre><code class="language-javascript">// user.test.js
import { getUserProfile } from &#39;./user&#39;;
import axios from &#39;axios&#39;;

jest.mock(&#39;axios&#39;);

test(&#39;getUserProfile 호출 시 데이터를 반환해야 함&#39;, async () =&gt; {
  const mockData = { name: &#39;John Doe&#39; };
  axios.get.mockResolvedValue({ data: mockData });

  const result = await getUserProfile();
  expect(result.data).toEqual(mockData);
});</code></pre>
<ul>
<li><strong>독립적 테스트 가능:</strong> 컴포넌트 렌더링과 무관하게 API 로직만 검증 가능.</li>
<li><strong>신뢰성 확보:</strong> API 변경이나 리팩토링 후에도 동작 보장.</li>
</ul>
<h3 id="5-협업-효율성-극대화">5. <strong>협업 효율성 극대화</strong></h3>
<p>API 로직을 모듈화하면 팀원 간 협업이 쉬워짐:</p>
<ul>
<li><strong>명확한 역할 분담:</strong> &quot;인증 API 추가는 auth.js 수정&quot;처럼 작업 영역 구분 가능.</li>
<li><strong>수정사항 반영 용이:</strong> 백엔드 API 변경사항을 한 곳에서 관리 가능.</li>
</ul>
<hr>
<h2 id="구현-예시-모듈화된-api-로직-📂">구현 예시: 모듈화된 API 로직 📂</h2>
<h3 id="디렉토리-구조">디렉토리 구조</h3>
<pre><code>📂 src
 ├── 📂 api
 │    ├── auth.js
 │    ├── user.js
 ├── 📂 components
 │    ├── UserProfile.js
 ├── 📂 utils
 │    ├── axiosInstance.js</code></pre><h3 id="authjs">auth.js</h3>
<pre><code class="language-javascript">import axiosInstance from &#39;../utils/axiosInstance&#39;;

export const login = async (credentials) =&gt; {
  return axiosInstance.post(&#39;/auth/login&#39;, credentials);
};

export const logout = async () =&gt; {
  return axiosInstance.post(&#39;/auth/logout&#39;);
};</code></pre>
<h3 id="userjs">user.js</h3>
<pre><code class="language-javascript">import axiosInstance from &#39;../utils/axiosInstance&#39;;

export const getUserProfile = async () =&gt; {
  return axiosInstance.get(&#39;/user/profile&#39;);
};</code></pre>
<h3 id="axiosinstancejs">axiosInstance.js</h3>
<pre><code class="language-javascript">import axios from &#39;axios&#39;;

const axiosInstance = axios.create({
  baseURL: &#39;/api&#39;,
});

axiosInstance.interceptors.request.use((config) =&gt; {
  const token = localStorage.getItem(&#39;token&#39;);
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

export default axiosInstance;</code></pre>
<hr>
<h2 id="정리-✨">정리 ✨</h2>
<p>API 호출 로직을 별도로 분리해 관리하면 다음과 같은 이점이 있음:</p>
<ol>
<li>코드 가독성과 구조화가 개선됨.</li>
<li>중복된 로직을 줄이고 재사용성을 높일 수 있음.</li>
<li>유지보수와 확장성이 향상됨.</li>
<li>테스트 작성이 쉬워지고 품질 보장이 가능함.</li>
<li>협업 효율성이 극대화됨.</li>
</ol>
<p>프로젝트 초기부터 API 로직을 모듈화하면 향후 발생할 여러 문제를 예방하고, 개발 생산성을 크게 높일 수 있음. 이제부터는 API 로직 관리에 신경 써보자! 🙌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Swiper.js와 React Hooks로 슬라이드 전환 & 텍스트 애니메이션 동기화하기 🚀]]></title>
            <link>https://velog.io/@zenma_official/Swiper.js%EC%99%80-React-Hooks%EB%A1%9C-%EC%8A%AC%EB%9D%BC%EC%9D%B4%EB%93%9C-%EC%A0%84%ED%99%98-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EB%8F%99%EA%B8%B0%ED%99%94%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@zenma_official/Swiper.js%EC%99%80-React-Hooks%EB%A1%9C-%EC%8A%AC%EB%9D%BC%EC%9D%B4%EB%93%9C-%EC%A0%84%ED%99%98-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EB%8F%99%EA%B8%B0%ED%99%94%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 11 Dec 2024 17:19:53 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>&quot;슬라이더 하나로 멋진 인터랙션을 구현하고 싶다면? Swiper.js와 React로 해결 가능! 내가 삽질하며 배운 것들, 여기 다 정리해봄.&quot;</p>
</blockquote>
<hr>
<h2 id="문제-상황-🤯">문제 상황 🤯</h2>
<p>홈페이지 배너 슬라이더를 만들다가 이런 문제들이 터짐:</p>
<ol>
<li><p><strong>텍스트 애니메이션 동기화</strong>:</p>
<ul>
<li>슬라이드 전환 시 텍스트가 자연스럽게 이동하며 페이드 인/아웃되는 애니메이션을 구현하고 싶었음.</li>
<li>Swiper.js의 이벤트를 어떻게 활용해야 할지 막막했음.</li>
</ul>
</li>
<li><p><strong>배경색 전환</strong>:</p>
<ul>
<li>슬라이드가 변경될 때마다 좌우 배경색을 슬라이드의 색상에 맞게 변경하고 싶었음.</li>
</ul>
</li>
<li><p><strong>반응형 디자인</strong>:</p>
<ul>
<li>다양한 디바이스에서 슬라이더가 깨지지 않고 잘 동작해야 했음.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="해결-과정-🔧">해결 과정 🔧</h2>
<h3 id="1-텍스트-애니메이션-동기화">1. 텍스트 애니메이션 동기화</h3>
<p>슬라이드 전환 시점에 맞춰 텍스트 애니메이션을 적용하려면 Swiper.js 이벤트를 잘 써야 함.</p>
<pre><code class="language-javascript">const handleTransitionStart = () =&gt; {
  setTextClass(&#39;animate-slide-text-out&#39;); // 텍스트가 사라지는 애니메이션 실행
};

const handleTransitionEnd = () =&gt; {
  setTextClass(&#39;animate-slide-text-in&#39;); // 텍스트가 나타나는 애니메이션 실행

  // 0.5초 후 초기화
  setTimeout(() =&gt; {
    setTextClass(&#39;&#39;);
  }, 500);
};</code></pre>
<ul>
<li>Swiper.js의 <code>onTransitionStart</code>, <code>onTransitionEnd</code> 이벤트 활용.</li>
<li>React <code>useState</code>로 동적 클래스 관리.</li>
</ul>
<h3 id="2-배경색-동적-변경">2. 배경색 동적 변경</h3>
<p>슬라이드 변경 시 배경색을 동적으로 업데이트하려면 Swiper.js의 <code>realIndex</code>를 사용.</p>
<pre><code class="language-javascript">const handleSlideChange = (swiper) =&gt; {
  const activeIndex = swiper.realIndex;
  setBgColor(slidesData[activeIndex].backgroundColor); // 현재 슬라이드 색상 반영
};</code></pre>
<ul>
<li><code>slidesData</code>로 슬라이드 정보 관리.</li>
<li>Tailwind CSS <code>transition-colors</code>로 부드럽게 배경색 전환.</li>
</ul>
<h3 id="3-반응형-디자인">3. 반응형 디자인</h3>
<p>Tailwind CSS로 화면 크기에 맞는 슬라이더 레이아웃 구현.</p>
<pre><code class="language-javascript">&lt;div className=&quot;w-full md:w-3/4 lg:w-7/10&quot;&gt;
  &lt;Swiper ... /&gt;
&lt;/div&gt;</code></pre>
<ul>
<li>Tailwind 유틸리티 클래스 <code>w-full</code>, <code>md:w-3/4</code>, <code>lg:w-7/10</code> 사용.</li>
<li>커스텀 클래스는 <code>tailwind.config.js</code>에서 추가 가능.</li>
</ul>
<hr>
<h2 id="주요-코드-✍️">주요 코드 ✍️</h2>
<h3 id="swiperjs-설정">Swiper.js 설정</h3>
<pre><code class="language-javascript">&lt;Swiper
  modules={[EffectFade, Navigation, Pagination, Autoplay]}
  effect=&quot;fade&quot;
  fadeEffect={{ crossFade: true }}
  navigation
  pagination={{ clickable: true }}
  autoplay={{ delay: 5000, disableOnInteraction: false }}
  loop
  speed={600}
  onSlideChange={handleSlideChange}
  onTransitionStart={handleTransitionStart}
  onTransitionEnd={handleTransitionEnd}
&gt;
  {slidesData.map((slide, index) =&gt; (
    &lt;SwiperSlide key={index}&gt;
      &lt;div className=&quot;relative w-full h-96 flex items-center justify-center&quot;&gt;
        &lt;div className=&quot;overlay-text absolute inset-0 flex flex-col items-center justify-center text-center&quot;&gt;
          &lt;h2 className=&quot;text-4xl font-bold mb-4&quot;&gt;{slide.title}&lt;/h2&gt;
          &lt;p className=&quot;text-xl&quot;&gt;{slide.subtitle}&lt;/p&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/SwiperSlide&gt;
  ))}
&lt;/Swiper&gt;</code></pre>
<h3 id="css-애니메이션">CSS 애니메이션</h3>
<pre><code class="language-css">@keyframes slide-text-in {
  from {
    opacity: 0;
    transform: translateX(20px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes slide-text-out {
  from {
    opacity: 1;
    transform: translateX(0);
  }
  to {
    opacity: 0;
    transform: translateX(-20px);
  }
}

.animate-slide-text-in {
  animation: slide-text-in 0.3s ease-out forwards;
}

.animate-slide-text-out {
  animation: slide-text-out 0.3s ease-in forwards;
}</code></pre>
<hr>
<h2 id="어려웠던-점-😅">어려웠던 점 😅</h2>
<ul>
<li>Swiper.js의 다양한 이벤트 중 적합한 타이밍을 찾는 데 시간이 걸림.</li>
<li>React 상태 관리와 CSS 애니메이션 타이밍을 맞추는 것이 까다로웠음.</li>
<li>반응형 디자인에서 커스텀 클래스 추가 작업이 필요했음.</li>
</ul>
<hr>
<h2 id="배운-점-📚">배운 점 📚</h2>
<ol>
<li><strong>상태 관리</strong>: React <code>useState</code>를 활용하면 UI 상태 관리가 쉬워짐.</li>
<li><strong>CSS 애니메이션</strong>: Tailwind CSS와 @keyframes로 간단한 애니메이션 구현 가능.</li>
<li><strong>Tailwind CSS 커스터마이징</strong>: 설정 파일을 활용해 프로젝트에 맞는 유틸리티 클래스를 만들 수 있음.</li>
</ol>
<hr>
<h2 id="구현-결과-🎉">구현 결과 🎉</h2>
<p><img src="https://velog.velcdn.com/images/zenma_official/post/8439a129-c073-411f-83d9-83fc540767fa/image.gif" alt="슬라이더 동작 GIF"></p>
<ul>
<li>텍스트가 자연스럽게 슬라이드 인/아웃.</li>
<li>슬라이드 전환 시 배경색 변경.</li>
<li>모든 디바이스에서 반응형으로 동작.</li>
</ul>
<hr>
<h2 id="참고-자료-📖">참고 자료 📖</h2>
<ul>
<li><a href="https://swiperjs.com/">Swiper.js 공식 문서</a></li>
<li><a href="https://tailwindcss.com/">Tailwind CSS 공식 문서</a></li>
</ul>
<p>질문 있으면 댓글✌️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바스크립트 맛보기 튜토리얼 🍭]]></title>
            <link>https://velog.io/@zenma_official/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%A7%9B%EB%B3%B4%EA%B8%B0-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC</link>
            <guid>https://velog.io/@zenma_official/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%A7%9B%EB%B3%B4%EA%B8%B0-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC</guid>
            <pubDate>Tue, 10 Dec 2024 13:07:45 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>&quot;복잡한 건 뒤로 하고, 핵심만 딱딱 찍어서 알려줌. 코드로 직접 해보면 아! 하게 될 거임.&quot;</p>
</blockquote>
<hr>
<h2 id="🔥-1단계-자바스크립트-기본-중의-기본">🔥 1단계: 자바스크립트 기본 중의 기본</h2>
<h3 id="변수-선언">변수 선언</h3>
<p>자바스크립트에서는 변수를 <code>var</code>, <code>let</code>, <code>const</code>로 선언할 수 있음. 근데 요즘은 거의 <code>let</code>이랑 <code>const</code>만 씀.</p>
<pre><code class="language-javascript">let age = 25; // 바뀔 수 있는 값
const name = &#39;JavaScript&#39;; // 절대 안 바뀜</code></pre>
<h4 id="변수-선언-비교표">변수 선언 비교표</h4>
<table>
<thead>
<tr>
<th>선언 키워드</th>
<th>특징</th>
<th>사용 추천 여부</th>
</tr>
</thead>
<tbody><tr>
<td><code>var</code></td>
<td>함수 스코프, 중복 선언 가능</td>
<td>❌</td>
</tr>
<tr>
<td><code>let</code></td>
<td>블록 스코프, 중복 선언 불가</td>
<td>✅</td>
</tr>
<tr>
<td><code>const</code></td>
<td>블록 스코프, 상수 용도</td>
<td>✅</td>
</tr>
</tbody></table>
<hr>
<h2 id="🎯-2단계-조건문과-반복문">🎯 2단계: 조건문과 반복문</h2>
<h3 id="조건문">조건문</h3>
<p>조건에 따라 다른 행동을 해야 한다면?</p>
<pre><code class="language-javascript">if (age &gt; 18) {
  console.log(&#39;성인임&#39;);
} else {
  console.log(&#39;미성년자임&#39;);
}</code></pre>
<h3 id="반복문">반복문</h3>
<p>같은 일을 여러 번 해야 한다면?</p>
<pre><code class="language-javascript">for (let i = 0; i &lt; 5; i++) {
  console.log(`숫자: ${i}`);
}</code></pre>
<h4 id="반복문-흐름-다이어그램">반복문 흐름 다이어그램</h4>
<pre><code>시작 → 조건 확인 → 실행 → 증감 → 조건 확인 → 종료</code></pre><hr>
<h2 id="🚀-3단계-함수-만들기">🚀 3단계: 함수 만들기</h2>
<p>함수는 재사용 가능한 코드 조각임.</p>
<pre><code class="language-javascript">function greet(name) {
  return `안녕, ${name}!`;
}
console.log(greet(&#39;JavaScript&#39;));</code></pre>
<h4 id="함수-호출-흐름">함수 호출 흐름</h4>
<pre><code>함수 호출 → 매개변수 전달 → 내부 코드 실행 → 값 반환</code></pre><hr>
<h2 id="💡-4단계-배열과-객체">💡 4단계: 배열과 객체</h2>
<h3 id="배열">배열</h3>
<p>여러 개의 값을 한 곳에 저장하고 싶을 때.</p>
<pre><code class="language-javascript">const fruits = [&#39;사과&#39;, &#39;바나나&#39;, &#39;체리&#39;];
console.log(fruits[1]); // 바나나 출력</code></pre>
<h3 id="객체">객체</h3>
<p>값들을 키-값 쌍으로 저장할 때.</p>
<pre><code class="language-javascript">const person = {
  name: &#39;John&#39;,
  age: 30,
};
console.log(person.name); // John 출력</code></pre>
<h4 id="배열과-객체-비교표">배열과 객체 비교표</h4>
<table>
<thead>
<tr>
<th>구분</th>
<th>배열</th>
<th>객체</th>
</tr>
</thead>
<tbody><tr>
<td>구조</td>
<td>순서대로 값 저장</td>
<td>키-값 쌍으로 값 저장</td>
</tr>
<tr>
<td>접근법</td>
<td><code>index</code> 사용</td>
<td><code>key</code> 사용</td>
</tr>
<tr>
<td>예시</td>
<td><code>fruits[0]</code></td>
<td><code>person.name</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="🏁-마무리-이걸로-뭘-할-수-있냐고">🏁 마무리: 이걸로 뭘 할 수 있냐고?</h2>
<ul>
<li>DOM 조작: HTML 요소를 컨트롤.</li>
<li>비동기 작업: 서버에서 데이터 가져오기.</li>
<li>게임 만들기? 가능함.</li>
</ul>
<h4 id="dom-조작-예제">DOM 조작 예제</h4>
<pre><code class="language-javascript">const button = document.querySelector(&#39;button&#39;);
button.addEventListener(&#39;click&#39;, () =&gt; {
  alert(&#39;버튼 클릭됨!&#39;);
});</code></pre>
<hr>
<p>이거 직접 따라하면서 코딩하면 자바스크립트 기본은 문제없을 거임.
혹시 이해 안 되는 부분 있으면 댓글 🙌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Router와 react-scroll로 부드러운 스크롤링 네비게이션 구현하기]]></title>
            <link>https://velog.io/@zenma_official/React-Router%EC%99%80-react-scroll%EB%A1%9C-%EB%B6%80%EB%93%9C%EB%9F%AC%EC%9A%B4-%EC%8A%A4%ED%81%AC%EB%A1%A4%EB%A7%81-%EB%84%A4%EB%B9%84%EA%B2%8C%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@zenma_official/React-Router%EC%99%80-react-scroll%EB%A1%9C-%EB%B6%80%EB%93%9C%EB%9F%AC%EC%9A%B4-%EC%8A%A4%ED%81%AC%EB%A1%A4%EB%A7%81-%EB%84%A4%EB%B9%84%EA%B2%8C%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 10 Dec 2024 12:56:28 GMT</pubDate>
            <description><![CDATA[<p>웹 개발하다 보면 특정 섹션으로 부드럽게 스크롤링하는 기능이 필요할 때가 있음. 특히 React로 SPA(Single Page Application) 만들다 보면 이런 기능 하나로 사용자 경험(UX)이 확 좋아질 수 있음. 최근 프로젝트에서 <code>React Router</code>랑 <code>react-scroll</code> 써서 부드러운 스크롤링 네비게이션 구현해봤는데, 삽질 좀 하다가 해결해서 공유해봄.</p>
<hr>
<h2 id="문제-상황">문제 상황</h2>
<p>React로 웹 앱 만들던 중에 이런 요구사항이 나옴:</p>
<ol>
<li>네비게이션 탭 클릭하면 해당 섹션으로 부드럽게 이동해야 함.</li>
<li>현재 보이는 섹션에 따라 네비게이션 탭 상태가 동적으로 변경되어야 함.</li>
<li>고정 헤더 때문에 섹션이 헤더에 가려지지 않아야 함.</li>
</ol>
<p>이거 구현하다 보니까 다음 같은 문제들이 터짐:</p>
<ul>
<li><strong>React Router랑 scroll 충돌</strong>: 특정 경로로 이동하면 스크롤링 기능이 제대로 안 됨.</li>
<li><strong>탭 동기화 문제</strong>: 보이는 섹션이랑 탭 상태가 어긋남.</li>
<li><strong>헤더 오프셋 문제</strong>: 고정 헤더 때문에 섹션 일부가 잘림.</li>
</ul>
<hr>
<h2 id="해결-과정">해결 과정</h2>
<h3 id="1-라이브러리-설정">1. 라이브러리 설정</h3>
<p>일단 <code>react-router-dom</code>이랑 <code>react-scroll</code> 설치함. 각각 라우팅이랑 스크롤 애니메이션 기능 제공해줌.</p>
<pre><code class="language-bash">npm install react-router-dom react-scroll</code></pre>
<h3 id="2-스크롤링-기능-구현">2. 스크롤링 기능 구현</h3>
<p><code>react-scroll</code>의 <code>scroller</code> 사용해서 특정 섹션으로 부드럽게 이동하도록 구현함.</p>
<pre><code class="language-jsx">import { scroller } from &#39;react-scroll&#39;;

const handleScrollTo = (sectionId) =&gt; {
  scroller.scrollTo(sectionId, {
    duration: 800,
    delay: 0,
    smooth: &#39;easeInOutQuart&#39;,
    offset: -80, // 고정 헤더 높이만큼 오프셋
  });
};</code></pre>
<h3 id="3-react-router랑-통합">3. React Router랑 통합</h3>
<p>SPA 특성상 다른 페이지에서 네비게이션 클릭하면 메인 페이지로 돌아가 특정 섹션으로 스크롤해야 했음. React Router의 <code>navigate</code>를 써서 상태 전달함.</p>
<pre><code class="language-jsx">import { useNavigate } from &#39;react-router-dom&#39;;

const navigate = useNavigate();

const handleNavigation = (sectionId) =&gt; {
  navigate(&#39;/&#39;, { state: { scrollTo: sectionId } });
};</code></pre>
<p>메인 페이지에서는 <code>useEffect</code>로 상태 기반 스크롤 처리했음.</p>
<pre><code class="language-jsx">import { useEffect } from &#39;react&#39;;
import { useLocation } from &#39;react-router-dom&#39;;
import { scroller } from &#39;react-scroll&#39;;

const MainPage = () =&gt; {
  const location = useLocation();

  useEffect(() =&gt; {
    if (location.state?.scrollTo) {
      scroller.scrollTo(location.state.scrollTo, {
        duration: 800,
        delay: 0,
        smooth: &#39;easeInOutQuart&#39;,
        offset: -80,
      });
    }
  }, [location]);

  return (
    &lt;div&gt;
      {/* 섹션들 */}
      &lt;section id=&quot;section1&quot;&gt;...&lt;/section&gt;
      &lt;section id=&quot;section2&quot;&gt;...&lt;/section&gt;
      &lt;section id=&quot;section3&quot;&gt;...&lt;/section&gt;
    &lt;/div&gt;
  );
};</code></pre>
<h3 id="4-현재-섹션이랑-탭-동기화">4. 현재 섹션이랑 탭 동기화</h3>
<p><code>IntersectionObserver</code> 써서 현재 보이는 섹션 감지하고, 해당 섹션에 맞는 탭을 활성화함.</p>
<pre><code class="language-jsx">import { useEffect, useState } from &#39;react&#39;;

const useActiveSection = (sections) =&gt; {
  const [activeSection, setActiveSection] = useState(null);

  useEffect(() =&gt; {
    const observer = new IntersectionObserver((entries) =&gt; {
      entries.forEach((entry) =&gt; {
        if (entry.isIntersecting) {
          setActiveSection(entry.target.id);
        }
      });
    }, { threshold: 0.6 });

    sections.forEach((id) =&gt; {
      const element = document.getElementById(id);
      if (element) observer.observe(element);
    });

    return () =&gt; {
      sections.forEach((id) =&gt; {
        const element = document.getElementById(id);
        if (element) observer.unobserve(element);
      });
    };
  }, [sections]);

  return activeSection;
};</code></pre>
<p><code>useActiveSection</code> 훅으로 활성화된 섹션 감지해서 탭 상태 업데이트함.</p>
<hr>
<h2 id="최종-코드-구조">최종 코드 구조</h2>
<h3 id="mainpagejs">MainPage.js</h3>
<pre><code class="language-jsx">const MainPage = () =&gt; {
  const activeSection = useActiveSection([&#39;section1&#39;, &#39;section2&#39;, &#39;section3&#39;]);

  return (
    &lt;div&gt;
      &lt;header&gt;
        &lt;Tabs activeTab={activeSection} /&gt;
      &lt;/header&gt;
      &lt;section id=&quot;section1&quot;&gt;섹션 1&lt;/section&gt;
      &lt;section id=&quot;section2&quot;&gt;섹션 2&lt;/section&gt;
      &lt;section id=&quot;section3&quot;&gt;섹션 3&lt;/section&gt;
    &lt;/div&gt;
  );
};</code></pre>
<h3 id="tabsjs">Tabs.js</h3>
<pre><code class="language-jsx">const Tabs = ({ activeTab }) =&gt; (
  &lt;nav&gt;
    &lt;button onClick={() =&gt; handleScrollTo(&#39;section1&#39;)} className={activeTab === &#39;section1&#39; ? &#39;active&#39; : &#39;&#39;}&gt;
      섹션 1
    &lt;/button&gt;
    &lt;button onClick={() =&gt; handleScrollTo(&#39;section2&#39;)} className={activeTab === &#39;section2&#39; ? &#39;active&#39; : &#39;&#39;}&gt;
      섹션 2
    &lt;/button&gt;
    &lt;button onClick={() =&gt; handleScrollTo(&#39;section3&#39;)} className={activeTab === &#39;section3&#39; ? &#39;active&#39; : &#39;&#39;}&gt;
      섹션 3
    &lt;/button&gt;
  &lt;/nav&gt;
);</code></pre>
<hr>
<h2 id="배운-점">배운 점</h2>
<p>이 작업 하면서 몇 가지 깨달은 점:</p>
<ol>
<li><strong>React에서 상태 관리랑 DOM 조작의 균형</strong>: <code>useEffect</code>랑 라이브러리 조합으로 문제 해결 가능.</li>
<li><strong>IntersectionObserver 활용</strong>: 사용자 인터랙션 없이도 뷰포트 기반 상태 동기화 가능.</li>
<li><strong>라이브러리 통합</strong>: <code>react-scroll</code>이랑 <code>React Router</code> 조합으로 UX 크게 개선 가능.</li>
</ol>
<hr>
<p>이렇게 부드러운 스크롤링 네비게이션 구현하는 과정을 정리해봤음. 혹시 질문이나 피드백 있으면 댓글로 남겨주셈! 😊</p>
]]></description>
        </item>
    </channel>
</rss>