<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Coding Planet</title>
    <link>https://sharonprogress.tistory.com/</link>
    <description>Believe in the process</description>
    <language>ko</language>
    <pubDate>Sun, 12 Apr 2026 10:53:40 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>jhj.sharon</managingEditor>
    <image>
      <title>Coding Planet</title>
      <url>https://tistory1.daumcdn.net/tistory/5797270/attach/5591db8fff524aa492809ae73a4f3fc0</url>
      <link>https://sharonprogress.tistory.com</link>
    </image>
    <item>
      <title>[Next.js]기상관련 PWA 웹앱 회고 - FCM을 중점으로</title>
      <link>https://sharonprogress.tistory.com/376</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트 개요&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;프로젝트 개요&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기상청 API를 활용하여 기상 데이터를 수집 및 가공한 후, 특정 산업 사용자에게 미세 기상 정보를 제공하는 웹앱 개발 프로젝트.&lt;/li&gt;
&lt;li&gt;급격한 기상 변동 시, 웹앱이 활성화되어 있지 않은 상황에서도 사용자에게 Push 알림을 통해 실시간 정보를 제공하는 기능 구현.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기술 스택&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프론트엔드&lt;/b&gt;: Next.js v13, TailwindCSS&lt;/li&gt;
&lt;li&gt;&lt;b&gt;백엔드&lt;/b&gt;: Spring, JPA&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터베이스&lt;/b&gt;: PostgreSQL&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로젝트 기간&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;5주&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;참여 분야&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프론트엔드 공통 모듈 개발&lt;/li&gt;
&lt;li&gt;Next.js 환경 설정 및 구성&lt;/li&gt;
&lt;li&gt;Redux 환경 구현 및 상태 관리 로직 개발&lt;/li&gt;
&lt;li&gt;FCM&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;회고&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Next.js 적응&lt;/b&gt;: 기본적인 리액트 지식만을 가지고 시작한 프로젝트였기 때문에 Next.js 프레임워크의 모듈 구조와 기능에 적응하는 데 시간이 필요했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버사이드 랜더링(SSR)과 클라이언트사이드 랜더링(CSR) 구분&lt;/b&gt;: Next.js가 SSR에 특화된 프레임워크임에도 불구하고, Redux를 활용한 상태 관리 및 일부 공통 모듈의 특성상 클라이언트 사이드 랜더링(CSR)을 병행할 필요가 있었다. 이로 인해 SSR과 CSR을 적절히 구분하여 사용해야 했으며, 두 랜더링 방식을 혼합한 구조를 설계하는 부분에서 어려움을 겪었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설계의 중요성 인식&lt;/b&gt;: 초기 기획 단계에서 SSR과 CSR의 사용 범위 및 컴포넌트 렌더링 방식을 명확히 정의하지 못한 점이 프로젝트 중반에 문제가 되었다. 프로젝트 진행 중 요구사항이 일부 변경되면서 통합적인 설계가 부족했던 부분을 인지하게 되었고, 향후 유사 프로젝트에서는 기획 단계에서 철저한 조사를 통해 설계를 체계적으로 구성하는 것의 중요성을 깨달았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회고 - PWA Push 구현 및 FCM 사용&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;PWA 및 Push 알림 구현&lt;/b&gt;: 해당 프로젝트에서는 사용자들이 웹앱을 사용하고 있지 않은 상황에서도 급격한 기상 변동에 대해 실시간으로 알림을 받을 수 있도록 PWA(Progressive Web App) 기능을 구현했다. 이를 통해 사용자가 웹앱을 설치하지 않고 브라우저에서 접속하더라도 알림을 받을 수 있는 환경을 구축하였다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FCM(Firebase Cloud Messaging) 도입 배경&lt;/b&gt;: Push 알림 기능 구현을 위해 Firebase Cloud Messaging(이하 FCM)을 사용했다. FCM은 다양한 플랫폼에서의 푸시 메시지 전송을 지원하고 있으며, 특히 브라우저 기반의 알림 전송이 용이하기 때문에 이를 채택하였다. 초기에는 서버에서 직접 알림을 전송하기 위해 다른 오픈소스를 검토했지만, 설정의 복잡성과 안정성 이슈로 인해 FCM이 적합하다고 판단했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FCM 연동 과정&lt;/b&gt;: FCM 연동 시 고려해야 할 점은 웹 앱의 인증 절차 및 사용자 토큰 관리였다. 사용자가 웹앱에 접속할 때마다 FCM 토큰을 생성하고, 이를 서버에 저장하여 기상 변동 발생 시 알림을 전송할 수 있도록 설계했다. 이 과정에서 주의해야 할 점은 토큰이 주기적으로 갱신되기 때문에, 토큰 갱신 시 기존 토큰을 무효화하고 새로운 토큰으로 교체하는 로직을 구현하는 것이었다. 이를 Redux를 통해 상태를 관리하고, 서버에서는 Spring을 활용해 REST API로 클라이언트에서 전송된 토큰을 저장 및 관리했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;알림 전송 및 처리&lt;/b&gt;: 기상청 API에서 기상 변동 정보를 주기적으로 받아 분석한 후, 특정 임계값을 초과할 때 FCM을 통해 실시간으로 사용자의 기기로 알림을 전송했다. 이 때 알림 내용은 사용자가 설정한 지역 및 관심 기상 요소(예: 강풍, 폭우 등)에 따라 달라지도록 설계했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구현 시 어려웠던 점 및 해결&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초기 FCM 설정 시, 서비스워커(Service Worker)와의 연동에서 예상치 못한 에러가 발생했다. 이는 HTTPS 환경에서만 FCM이 정상 작동하기 때문이었는데, 로컬 개발 환경에서는 HTTPS 설정이 어려운 상황이었다. 이를 해결하기 위해 &lt;b&gt;ngrok&lt;/b&gt;을 활용해 로컬 서버를 HTTPS 환경으로 배포하여 테스트를 진행했다. ngrok 무료 플랜의 제한 사항(짧은 세션 유지 시간)으로 인해 서비스 워커가 지속적으로 재연결되는 문제가 발생했으나, 유료 플랜을 결제하여 해결했다. 이로써 로컬 환경에서도 안정적인 HTTPS 연결을 통한 FCM 테스트가 가능했다.&lt;/li&gt;
&lt;li&gt;또한, 알림이 전송된 후 사용자가 이를 클릭했을 때 웹앱이 적절한 라우트로 이동하도록 처리하는 부분도 어려움이 있었다. 알림 클릭 이벤트를 통해 사용자의 관심 지역 페이지로 이동하도록 로직을 추가하여 사용자 경험을 개선하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과 및 개선점&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FCM을 활용한 Push 알림 기능은 웹앱의 실시간성을 크게 높여주었으며, 사용자들이 기상 변동 정보를 빠르게 확인할 수 있도록 하였다. 다만, FCM 의존성으로 인해 네트워크 상태가 좋지 않거나, 토큰 갱신이 원활하지 않은 경우 일부 알림이 누락되는 문제가 있었다. 이러한 부분은 추후 자체 푸시 서버를 구축하거나, FCM 외부의 대안을 모색하는 방향으로 개선할 필요가 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;알림 중복 발송 문제&lt;/b&gt;: 실제 운영 서버에 배포하기 전까지 Push 알림이 무조건 2번 발송되는 문제가 있었다. 이 문제의 원인은 아직 명확히 파악하지 못했지만, 비슷한 문제가 다른 프로젝트에서도 자주 발생하는 것을 확인하였다. 이는 FCM 메시지 처리 시 중복 이벤트 발생 혹은 서비스워커 등록 및 이벤트 리스너의 중복 등록 문제일 가능성이 높다고 판단된다. 향후 이러한 중복 발송의 원인을 정확히 파악하고 이를 개선하는 것이 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;추가적으로 고려할 사항&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 관심 지역의 기상 정보를 사전에 캐싱하여 알림 전송 시 응답 속도를 높일 수 있는 방안을 연구할 필요가 있다.&lt;/li&gt;
&lt;li&gt;다수의 사용자가 한꺼번에 알림을 수신할 때 서버의 부하를 줄이기 위한 큐잉(Queueing) 시스템 도입도 고려해볼만 하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category> 프로젝트</category>
      <author>jhj.sharon</author>
      <guid isPermaLink="true">https://sharonprogress.tistory.com/376</guid>
      <comments>https://sharonprogress.tistory.com/376#entry376comment</comments>
      <pubDate>Mon, 30 Sep 2024 13:51:34 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js]스크롤 컨테이너에 대한 'ScrollToTop' 컴포넌트 구현하기 - 맨 위로 버튼</title>
      <link>https://sharonprogress.tistory.com/375</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일반적으로 ScrollToTop 컴포넌트는 window 객체의 스크롤을 감지하여 구현한다. 하지만 특정 div 내에서만 스크롤이 발생하는 경우 window.pageYOffset은 항상 0을 반환하여 스크롤 위치를 정확히 파악할 수 없다. 무한 스크롤 게시판의 경우 맨 위로 가기 버튼이 필수적이므로 이 기능을 스크롤 컨테이너에서만 적용되도록 구현했다. 다만 예시코드에서는 무한 스크롤 부분은 코드의 가독성을 위해 제거하고 ScrollToTop 기능만 나와있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&amp;nbsp;| 접근 방법&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 스크롤 가능한 컨테이너에 &lt;b&gt;ref&lt;/b&gt;를 할당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 이 ref를 '&lt;b&gt;ScrollToTop&lt;/b&gt;' 컴포넌트에 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 'ScrollToTop' 컴포넌트 내에서 해당 ref의 &lt;b&gt;scrollTop&lt;/b&gt; 속성을 사용하여 스크롤 위치를 감지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;nbsp;| useRef &lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useRef는 참조를 저장하기 위해 사용된다. 주로 DOM 요소에 접근하거나, 렌더링 사이에서 값을 유지하기 위해 사용하는데 이 경우 스크롤값을 참조하고 저장한다.&lt;/li&gt;
&lt;li&gt;값이 변경되어도 컴포넌트가 다시 렌더링되지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;nbsp;| useCallback &lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모이제이션된 콜백 함수를 반환한다. 컴포넌트가 재렌더링될 때, 동일한 콜백 함수를 다시 생성하지 않도록 함으로써 성능을 최적화한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&amp;nbsp;|&lt;span&gt;&amp;nbsp;소스&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;Board&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1723420651856&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useState, useRef } from 'react';
import ScrollToTop from &quot;@/app/components/util/ScrollToTop&quot;;

export default function Board() {
    const scrollContainerRef = useRef(null);

    return (
        &amp;lt;&amp;gt;
            &amp;lt;div 
                ref={scrollContainerRef}
                className=&quot;w-full bg-white p-4 pb-20 box-border overflow-y-auto h-[calc(100vh-105px)] mt-[80px] focus:outline-none&quot;
            &amp;gt;
                &amp;lt;ScrollToTop scrollContainerRef={scrollContainerRef} /&amp;gt;
                {/* 컨텐츠 */}
            &amp;lt;/div&amp;gt;
        &amp;lt;/&amp;gt;
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;ScrollToTop&lt;span&gt; Component&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1723420710359&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useEffect, useState, useCallback } from 'react';

const ScrollToTop = ({ scrollContainerRef }) =&amp;gt; {
    const [isVisible, setIsVisible] = useState(false);

    const handleScroll = useCallback(() =&amp;gt; {
        const scrollContainer = scrollContainerRef.current;
        if (scrollContainer) {
            console.log('Scroll position:', scrollContainer.scrollTop);
            if (scrollContainer.scrollTop &amp;gt; 100) {
                setIsVisible(true);
            } else {
                setIsVisible(false);
            }
        }
    }, [scrollContainerRef]);

    const scrollToTop = () =&amp;gt; {
        const scrollContainer = scrollContainerRef.current;
        if (scrollContainer) {
            scrollContainer.scrollTo({
                top: 0,
                behavior: 'smooth',
            });
        }
    };

    useEffect(() =&amp;gt; {
        const scrollContainer = scrollContainerRef.current;
        if (scrollContainer) {
            scrollContainer.addEventListener('scroll', handleScroll);
            return () =&amp;gt; {
                scrollContainer.removeEventListener('scroll', handleScroll);
            };
        }
    }, [handleScroll, scrollContainerRef]);

    return (
        &amp;lt;&amp;gt;
            {isVisible &amp;amp;&amp;amp; (
                &amp;lt;div 
                    onClick={scrollToTop} 
                    className=&quot;fixed bottom-5 right-5 bg-blue-600 text-white p-3 rounded-full cursor-pointer shadow-lg&quot; 
                    style={{ zIndex: 9999 }}
                &amp;gt;
                    위로
                &amp;lt;/div&amp;gt;
            )}
        &amp;lt;/&amp;gt;
    );
};

export default ScrollToTop;&lt;/code&gt;&lt;/pre&gt;</description>
      <category>front</category>
      <author>jhj.sharon</author>
      <guid isPermaLink="true">https://sharonprogress.tistory.com/375</guid>
      <comments>https://sharonprogress.tistory.com/375#entry375comment</comments>
      <pubDate>Mon, 12 Aug 2024 09:03:31 +0900</pubDate>
    </item>
    <item>
      <title>TS2307: Cannot find module '@prisma/client/runtime' or its corresponding type declarations.</title>
      <link>https://sharonprogress.tistory.com/374</link>
      <description>&lt;pre id=&quot;code_1722391888599&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';

@Catch()
export class PrismaClientExceptionFilter implements ExceptionFilter {
    catch(exception: unknown, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse();
        const request = ctx.getRequest();

        let status = 500;
        let message = 'Internal server error';

        if (exception instanceof PrismaClientKnownRequestError) {
            if ((exception as PrismaClientKnownRequestError).code === 'P2002') {
                status = 409;
                message = 'Unique constraint failed on the fields: ' + (exception as PrismaClientKnownRequestError).meta?.target;
            } else {
                status = 400;
                message = (exception as PrismaClientKnownRequestError).message.replace(/\n/g, '');
            }
        } else if (exception instanceof HttpException) {
            status = exception.getStatus();
            message = exception.message;
        }

        response.status(status).json({
            statusCode: status,
            message,
            timestamp: new Date().toISOString(),
            path: request.url,
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;오류발생내역&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TS2307: Cannot find module '@prisma/client/runtime' or its corresponding type declarations.&lt;/li&gt;
&lt;li&gt;TS2339: Property 'code' does not exist on type 'unknown'&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Prisma 버전 변경: 최근 Prisma 버전에서 일부 내부 모듈의 경로가 변경되었음. &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@prisma/client/runtime에서 @prisma/client/runtime/library로 변경!&lt;/p&gt;</description>
      <category>Nest.js</category>
      <author>jhj.sharon</author>
      <guid isPermaLink="true">https://sharonprogress.tistory.com/374</guid>
      <comments>https://sharonprogress.tistory.com/374#entry374comment</comments>
      <pubDate>Wed, 31 Jul 2024 11:13:03 +0900</pubDate>
    </item>
    <item>
      <title>AJAX를 이용한 파일 다운로드 시 한글 파일명 깨짐 문제 해결하기</title>
      <link>https://sharonprogress.tistory.com/373</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제 상황 설명&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 프로젝트에서 AJAX를 사용하여 서버로부터 파일을 다운로드할 때 한글 파일명이 깨지는 현상을 겪었다. 동일한 파일 다운로드 기능을 form 제출 방식으로 구현했을 때는 이러한 문제가 발생하지 않았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;882ECA99EEB84A7ECB18FEDB5B3%84_240715-240726.xlsx -&amp;gt; 이런식으로 한글 깨짐&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;원인 분석&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;AJAX와 form 제출 방식의 차이점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Form 제출 방식&lt;/b&gt;: 브라우저가 기본적으로 제공하는 다운로드 기능을 활용한다. 브라우저는 &lt;code&gt;Content-Disposition&lt;/code&gt; 헤더를 자동으로 처리하고, 파일명 인코딩을 알아서 처리한다. 따라서 Form 형식의 파일다운로드에서는 파일명이 제대로 출력됬다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AJAX 방식&lt;/b&gt;: XMLHttpRequest 또는 Fetch API를 사용하여 비동기적으로 파일을 요청하고, JavaScript로 파일 다운로드를 처리한다. &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;이 경우 브라우저가 기본 다운로드 기능을 사용하지 않기 때문에, 파일명 인코딩 처리를 수동으로 해야 한다. &lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;AJAX 방식에서는 서버에서 보내는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Content-Disposition&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;헤더를 그대로 받아 처리하기 때문에, 추가적인 인코딩 처리가 필요하다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Content-Disposition 헤더 처리 방식&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Content-Disposition&lt;/code&gt; 헤더는 파일 다운로드 시 파일명을 지정하는 데 사용된다. 이 헤더는 ASCII 문자만을 지원하기 때문에, 한글 파일명은 인코딩되어 전달되어야 한다.&lt;/li&gt;
&lt;li&gt;RFC 5987 표준을 사용하면 파일명을 UTF-8로 인코딩할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;** RFC 5987에 대한 간단한 설명 : &lt;/b&gt;RFC 5987은 HTTP 헤더 필드에 국제화된 문자열을 사용할 수 있도록 정의한 표준이다. 이 표준을 따르면, UTF-8로 인코딩된 문자열을 헤더 필드에 포함시킬 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;해결 방법&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;클라이언트 측 코드 수정 내용&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 측에서는 서버로부터 받은 파일명을 올바르게 인코딩하여 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;서버 측 코드 수정 내용&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 측에서는 Content-Disposition 헤더에 파일명을 UTF-8로 인코딩하여 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코드 예시&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 문제가 있던 원래 코드&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클라이언트 측 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;fetch('/download', {
    method: 'GET',
    headers: {
        'Content-Type': 'application/json',
    },
})
.then(response =&amp;gt; response.blob())
.then(blob =&amp;gt; {
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = '파일명.xlsx'; // 한글 파일명 깨짐
    document.body.appendChild(a);
    a.click();
    a.remove();
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서버 측 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;response.setContentType(&quot;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet&quot;);
response.setHeader(&quot;Content-Disposition&quot;, &quot;attachment; filename=\&quot;파일명.xlsx\&quot;&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 수정된 코드&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클라이언트 측 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;fetch('/download', {
    method: 'GET',
    headers: {
        'Content-Type': 'application/json',
    },
})
.then(response =&amp;gt; {
    const disposition = response.headers.get('Content-Disposition');
    const filename = disposition.match(/filename\*?=['&quot;]?([^;\n&quot;]*)['&quot;]?/)[1];
    const decodedFilename = decodeURIComponent(filename.replace(/\+/g, ' '));
    return response.blob().then(blob =&amp;gt; {
        const url = window.URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = decodedFilename;
        document.body.appendChild(a);
        a.click();
        a.remove();
    });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서버 측 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;import org.apache.commons.codec.net.URLCodec;

response.setContentType(&quot;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet&quot;);
String fileName = &quot;파일명.xlsx&quot;;
URLCodec codec = new URLCodec();
String encodedFileName = codec.encode(fileName, &quot;UTF-8&quot;);
response.setHeader(&quot;Content-Disposition&quot;, &quot;attachment; filename*=UTF-8''&quot; + encodedFileName);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 RFC 문서 링크&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://tools.ietf.org/html/rfc5987&quot;&gt;RFC 5987&lt;/a&gt;: HTTP 헤더 필드에 대한 국제화된 문자열 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기타 도움이 된 자료들&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition&quot;&gt;MDN Web Docs: Content-Disposition&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/web.html&quot;&gt;Spring Framework Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  Java Study/Java 이론 정리</category>
      <author>jhj.sharon</author>
      <guid isPermaLink="true">https://sharonprogress.tistory.com/373</guid>
      <comments>https://sharonprogress.tistory.com/373#entry373comment</comments>
      <pubDate>Thu, 25 Jul 2024 14:39:42 +0900</pubDate>
    </item>
    <item>
      <title>[jira] 보드/칸반 사이드바, 팝업 모달 설정하기</title>
      <link>https://sharonprogress.tistory.com/372</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lsNLm/btsIH2nWyv8/aiS8H0F6gcXdbubj7dvH2k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lsNLm/btsIH2nWyv8/aiS8H0F6gcXdbubj7dvH2k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lsNLm/btsIH2nWyv8/aiS8H0F6gcXdbubj7dvH2k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlsNLm%2FbtsIH2nWyv8%2FaiS8H0F6gcXdbubj7dvH2k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;480&quot; height=&quot;361&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;361&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이드 바로 자세한 일정나오는거 너무 불편...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;911&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwA1st/btsIJAcWM5m/OqHEWk6ac9YZgDj34zNoC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwA1st/btsIJAcWM5m/OqHEWk6ac9YZgDj34zNoC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwA1st/btsIJAcWM5m/OqHEWk6ac9YZgDj34zNoC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwA1st%2FbtsIJAcWM5m%2FOqHEWk6ac9YZgDj34zNoC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;911&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;911&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스크 디테일에 들어가서 설정( &lt;span style=&quot;background-color: #ffffff; color: #202122; text-align: start;&quot;&gt;&lt;span&gt; &lt;span style=&quot;background-color: #ffffff; color: #202122; text-align: left;&quot;&gt;&amp;middot;&lt;/span&gt; &lt;span style=&quot;background-color: #ffffff; color: #202122; text-align: left;&quot;&gt;&amp;middot;&lt;/span&gt; &lt;span style=&quot;background-color: #ffffff; color: #202122; text-align: left;&quot;&gt;&amp;middot;&lt;/span&gt; ) 선택 후 알림창의 이슈열기 선택&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;911&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1VyKG/btsIKhKtVq5/OQkqQootdB9HP9Pg1toerK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1VyKG/btsIKhKtVq5/OQkqQootdB9HP9Pg1toerK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1VyKG/btsIKhKtVq5/OQkqQootdB9HP9Pg1toerK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1VyKG%2FbtsIKhKtVq5%2FOQkqQootdB9HP9Pg1toerK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;911&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;911&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;편안...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;911&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cADx8v/btsIJVgA0FG/7ZjgwQFoXRUhzwkr3IhiF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cADx8v/btsIJVgA0FG/7ZjgwQFoXRUhzwkr3IhiF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cADx8v/btsIJVgA0FG/7ZjgwQFoXRUhzwkr3IhiF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcADx8v%2FbtsIJVgA0FG%2F7ZjgwQFoXRUhzwkr3IhiF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;911&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;911&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzbLqW/btsIHCwh5sa/Et6Kn43A3V2bYRCUNpljH1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzbLqW/btsIHCwh5sa/Et6Kn43A3V2bYRCUNpljH1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzbLqW/btsIHCwh5sa/Et6Kn43A3V2bYRCUNpljH1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzbLqW%2FbtsIHCwh5sa%2FEt6Kn43A3V2bYRCUNpljH1%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;412&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;412&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 백로그나 타임라인에서는 아직 안됨....&lt;/p&gt;</description>
      <category>etc</category>
      <author>jhj.sharon</author>
      <guid isPermaLink="true">https://sharonprogress.tistory.com/372</guid>
      <comments>https://sharonprogress.tistory.com/372#entry372comment</comments>
      <pubDate>Tue, 23 Jul 2024 08:42:09 +0900</pubDate>
    </item>
    <item>
      <title>[git] 하나의 로컬에 다수의 원격 레파지토리 연결하기</title>
      <link>https://sharonprogress.tistory.com/371</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 회사에서 동기와 스터디를 하고 있다. react로 pwa를 구현하는 중인데 환경설정에서 에러가 많이 나서 바로 동기와 공유하고 있는 git repository에 올리기가 애매했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;commit을 해두는 방법, stash를 해놓은 방법도 있지만 제대로 구현되지 않은 작업을 commit하기도 그렇고 stack에 임시로&amp;nbsp; stash 하기도 싫었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 git에 내 private repository를 하나 파 놓고 관리하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 현재 연결된 원격 레파지토리 확인 : git remote -v&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;588&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RBsx4/btsIyfOqg1p/Vjwh0yKyPrqOgEL99kzMSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RBsx4/btsIyfOqg1p/Vjwh0yKyPrqOgEL99kzMSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RBsx4/btsIyfOqg1p/Vjwh0yKyPrqOgEL99kzMSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRBsx4%2FbtsIyfOqg1p%2FVjwh0yKyPrqOgEL99kzMSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;588&quot; height=&quot;101&quot; data-origin-width=&quot;588&quot; data-origin-height=&quot;101&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 새로운 원격 레파지토리 등: git&amp;nbsp;remote&amp;nbsp;add&amp;nbsp;&amp;lt;remote-name&amp;gt;&amp;nbsp;&amp;lt;remote-url&amp;gt; &lt;br /&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;67&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daT01E/btsIy6caa8Y/qxbZKXP0ZDDFtP5ipA1rA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daT01E/btsIy6caa8Y/qxbZKXP0ZDDFtP5ipA1rA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daT01E/btsIy6caa8Y/qxbZKXP0ZDDFtP5ipA1rA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaT01E%2FbtsIy6caa8Y%2FqxbZKXP0ZDDFtP5ipA1rA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;682&quot; height=&quot;67&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;67&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 연결 확인: &lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;git remote -v&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내가 지정한 sharon-boilerplate 별명으로 원격 저장소가 생성되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btEVOu/btsIzq2vae6/NJvKGlKNLo9tFn1f9R5dU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btEVOu/btsIzq2vae6/NJvKGlKNLo9tFn1f9R5dU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btEVOu/btsIzq2vae6/NJvKGlKNLo9tFn1f9R5dU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtEVOu%2FbtsIzq2vae6%2FNJvKGlKNLo9tFn1f9R5dU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;666&quot; height=&quot;138&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. push하기:&lt;span&gt; &amp;nbsp;git&amp;nbsp;push&amp;nbsp;sharon-boilerplate&amp;nbsp;main &lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;576&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcjCq4/btsIxGTiZ2i/88QQWWKzZvKzhRp7h0oZr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcjCq4/btsIxGTiZ2i/88QQWWKzZvKzhRp7h0oZr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcjCq4/btsIxGTiZ2i/88QQWWKzZvKzhRp7h0oZr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcjCq4%2FbtsIxGTiZ2i%2F88QQWWKzZvKzhRp7h0oZr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;576&quot; height=&quot;190&quot; data-origin-width=&quot;576&quot; data-origin-height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>etc</category>
      <author>jhj.sharon</author>
      <guid isPermaLink="true">https://sharonprogress.tistory.com/371</guid>
      <comments>https://sharonprogress.tistory.com/371#entry371comment</comments>
      <pubDate>Mon, 15 Jul 2024 09:27:45 +0900</pubDate>
    </item>
    <item>
      <title>[React] PWA를 위한 webpack 설정 : create-app-react를 사용하지 않은 경우</title>
      <link>https://sharonprogress.tistory.com/370</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PWA 구현시 create-app-react로 생성하지않아 아래와 같은 폴더 구조를 가지고 있지 않았다. 그래서 루트에 public 폴더를 생성하고 하위에 서비스 워커 설정을 위한 js를 추가했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Auk6E/btsIwye1V9A/0fkia8RBVWjxlTmHN947h0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Auk6E/btsIwye1V9A/0fkia8RBVWjxlTmHN947h0/img.png&quot; data-origin-width=&quot;245&quot; data-origin-height=&quot;155&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;28.94&quot; style=&quot;width: 28.6064%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Auk6E/btsIwye1V9A/0fkia8RBVWjxlTmHN947h0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAuk6E%2FbtsIwye1V9A%2F0fkia8RBVWjxlTmHN947h0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;245&quot; height=&quot;155&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d1Dr38/btsIvvDo7OB/9VEKgUdOjzWZMWfkcXZLZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d1Dr38/btsIvvDo7OB/9VEKgUdOjzWZMWfkcXZLZK/img.png&quot; data-origin-width=&quot;260&quot; data-origin-height=&quot;67&quot; data-is-animation=&quot;false&quot; style=&quot;width: 70.2308%;&quot; data-widthpercent=&quot;71.06&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d1Dr38/btsIvvDo7OB/9VEKgUdOjzWZMWfkcXZLZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd1Dr38%2FbtsIvvDo7OB%2F9VEKgUdOjzWZMWfkcXZLZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;260&quot; height=&quot;67&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;왼: create-react-app 구조, 오: 내가 루트에 직접 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;service-worker.js 를 제대로 설정하고, indext.html에 아래와 같이 serviceWorker를활성화 하는 코트를 넣었는데도 브라우저에서 스크립트를 찾지 못하는 오류가 났다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const registerServiceWorker = async () =&amp;gt; {
  if ('serviceWorker' in navigator) {
    try {
      const registration = await navigator.serviceWorker.register(
        '/service-worker.js',
      );
      if (registration.installing) {
        console.log('Service worker installing');
      } else if (registration.waiting) {
        console.log('Service worker installed');
      } else if (registration.active) {
        console.log('Service worker active');
      }
    } catch (error) {
      console.error(`Registration failed with ${error}`);
    }
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #faffb1;&quot;&gt;&lt;b&gt;오류&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f0eee5; color: #3d3929; text-align: start;&quot;&gt;app.js:28 Registration failed with SecurityError: Failed to register a ServiceWorker for scope ('&lt;/span&gt;&lt;a style=&quot;background-color: #f0eee5; color: #000000; text-align: start;&quot; href=&quot;http://localhost:3000/&quot;&gt;http://localhost:3000/&lt;/a&gt;&lt;span style=&quot;background-color: #f0eee5; color: #3d3929; text-align: start;&quot;&gt;') with script ('&lt;/span&gt;&lt;a style=&quot;background-color: #f0eee5; color: #000000; text-align: start;&quot; href=&quot;http://localhost:3000/service-worker.js&quot;&gt;http://localhost:3000/service-worker.js&lt;/a&gt;&lt;span style=&quot;background-color: #f0eee5; color: #3d3929; text-align: start;&quot;&gt;'): The script has an unsupported MIME type ('text/html').&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #faffb1;&quot;&gt;&lt;b&gt;오류원인&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;PWA(Progressive Web App)와 같은 고급 기능을 구현하려면 Webpack을 통해 정적 파일을 올바르게 처리하고, 서비스 워커를 설정하는 작업이 필요하다. Create React App에서는 이러한 설정이 기본적으로 포함되어 있지만, 직접 프로젝트를 설정할 경우 이러한 부분을 수동으로 설정해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1)Webpack 설정 파일 찾기&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;파일 이름은 보통 webpack.config.js, webpack.prod.js, webpack.dev.js , webpack-base-babel.js&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) CopyWebpackPlugin 설치&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 파일이나 디렉터리를 빌드 과정에서 다른 디렉터리로 복사해주는 역할. public 폴더의 내용을 빌드 디렉터리로 복사하는지 확인해야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720683972329&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const CopyWebpackPlugin = require('copy-webpack-plugin');

// plugins 배열에 추가
plugins: [
  new CopyWebpackPlugin({
    patterns: [
      { from: 'public', to: '' }
    ],
  }),
  // 기타 플러그인...
]

//Webpack이 service-worker.js 파일을 처리하지 않도록 설정
module: {
  rules: [
    {
      test: /service-worker\.js$/,
      use: [{ loader: 'file-loader', options: { name: '[name].[ext]' } }],
    },
    // 기타 rules...
  ],
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #faffb1;&quot;&gt; &lt;b&gt;정상작동 확인!&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;315&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpRxgr/btsIv8ufi3w/ZmGsAmmEWGJDB7VXIPD4Mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpRxgr/btsIv8ufi3w/ZmGsAmmEWGJDB7VXIPD4Mk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpRxgr/btsIv8ufi3w/ZmGsAmmEWGJDB7VXIPD4Mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpRxgr%2FbtsIv8ufi3w%2FZmGsAmmEWGJDB7VXIPD4Mk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;846&quot; height=&quot;315&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;315&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;** 버전 호환 확인 필수!&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;- &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;CopyWebpackPlugin&lt;span&gt; 버전에 따라 환경설정 문법이 다르다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Webpack 4.x&lt;/b&gt;: CopyWebpackPlugin 5.x&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Webpack 5.x&lt;/b&gt;: CopyWebpackPlugin 6.x 이상&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>front</category>
      <author>jhj.sharon</author>
      <guid isPermaLink="true">https://sharonprogress.tistory.com/370</guid>
      <comments>https://sharonprogress.tistory.com/370#entry370comment</comments>
      <pubDate>Thu, 11 Jul 2024 16:51:21 +0900</pubDate>
    </item>
    <item>
      <title>[React] redux-persist 설치 및 환경설정 (store.js 예시)</title>
      <link>https://sharonprogress.tistory.com/369</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;redux-persist 상태를 유지함으로써 페이지를 새로고침하거나 탐색할 때도 사용자 설정이나 임시 데이터(예: 폼 입력 내용)를 유지할 수 있다. 또한 백엔드 서버와의 통신을 줄여주어 사용자 경험을 크게 향상시킨다.&lt;br /&gt;보통 로컬스토리지에 저장하게 된다. 다만 이런 클라이언트상의 저장은 문제가 있으므로 보안이 요구되는 데이터 보다는 임시데이터 위주로 저장한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;설치&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1720575122649&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;yarn add redux-persist&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;환경설정&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;store.js는 Redux 스토어를 생성하고 설정하는 파일이다. 여기에 persistConfig를 포함시켜 저장소와 관련된 모든 설정을 한 곳에서 관리하는 것이 좋.&lt;/li&gt;
&lt;li&gt;스토어 생성 과정에서 persistReducer와 함께 persistConfig를 사용하여 일관된 상태 저장 및 복원을 보장한다.&lt;/li&gt;
&lt;li&gt;기본적으로 로컬 저장소를 사용하지만, &lt;b&gt;redux-persist는 localStorage, sessionStorage, AsyncStorage(React Native&lt;/b&gt;) 등 다양한 저장소를 지원한다. 아래 예제에서는 로컬 저장소(store)를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720575474190&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// redux/store.js
import { useMemo } from 'react';
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { composeWithDevTools } from 'redux-devtools-extension';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import rootReducer from './rootReducer';
import rootSaga from './saga';

// persistConfig 정의
const persistConfig = {
    key: 'root', //로컬 저장소에 저장될 때의 상태 키, 일반적으로 루트 리듀서
    storage, //태를 저장할 저장소 엔진을 정의
    whitelist: ['auth'], // 예시로 'auth' 리듀서만 저장
    blacklist: ['temporaryData'], // 저장하지 않을 리듀서
    version: 1, // 상태 구조가 변경될 때 사용하여 이전 버전과 호환되지 않는 상태를 무효화
        migrate: (state) =&amp;gt; {
        // 상태 변환 로직 :상태 구조가 변경될 때 상태를 변환하는 함수
        return Promise.resolve(newState);
    },
      timeout: 10000, //복원 시간 제한(밀리초 단위)입니다.

};

const persistedReducer = persistReducer(persistConfig, rootReducer);

let store;

function initStore(initialState) {
    const sagaMiddleware = createSagaMiddleware();
    const store = createStore(
        persistedReducer,
        initialState,
        composeWithDevTools(applyMiddleware(sagaMiddleware))
    );

    store.sagaTask = sagaMiddleware.run(rootSaga);

    return store;
}

export const initializeStore = (preloadedState) =&amp;gt; {
    let _store = store ?? initStore(preloadedState);

    if (preloadedState &amp;amp;&amp;amp; store) {
        _store = initStore({
            ...store.getState(),
            ...preloadedState,
        });
        store = undefined;
    }

    if (typeof window === 'undefined') return _store;
    if (!store) store = _store;

    return _store;
};

export const useStore = (initialState) =&amp;gt; {
    const store = useMemo(() =&amp;gt; initializeStore(initialState), [initialState]);
    return store;
};

// persistStore를 사용하여 스토어를 지속적으로 유지
export const persistor = persistStore(store);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;사용예시&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1720575901863&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from 'react';
import { Provider } from 'react-redux';
import { useStore, persistor } from '../redux/store';
import { PersistGate } from 'redux-persist/integration/react';
import Header from '../src/app/components/layout/Header';
import BottomNavBar from '../src/app/components/layout/BottomNavBar';
import '../src/app/assets/styles/global.css';

const GreenScreen = ({ Component, pageProps }) =&amp;gt; (
    &amp;lt;div className=&quot;green-border&quot;&amp;gt;
        &amp;lt;Header /&amp;gt;
        &amp;lt;Component {...pageProps} /&amp;gt;
        &amp;lt;BottomNavBar /&amp;gt;
    &amp;lt;/div&amp;gt;
);

const MyApp = ({ Component, pageProps }) =&amp;gt; {
    const store = useStore(pageProps.initialReduxState);

    return (
        &amp;lt;Provider store={store}&amp;gt;
            &amp;lt;PersistGate loading={null} persistor={persistor}&amp;gt;
            &amp;lt;GreenScreen Component={Component} pageProps={pageProps} /&amp;gt;
            &amp;lt;/PersistGate&amp;gt;
        &amp;lt;/Provider&amp;gt;
    );
};

export default MyApp;&lt;/code&gt;&lt;/pre&gt;</description>
      <category>front</category>
      <author>jhj.sharon</author>
      <guid isPermaLink="true">https://sharonprogress.tistory.com/369</guid>
      <comments>https://sharonprogress.tistory.com/369#entry369comment</comments>
      <pubDate>Wed, 10 Jul 2024 10:43:27 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] 단축 평가(Short-circuit Evaluation) 문법 - const weatherData = state.home.weatherData || {};</title>
      <link>https://sharonprogress.tistory.com/368</link>
      <description>&lt;pre id=&quot;code_1720143729224&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const mapStateToProps = state =&amp;gt; {
  const weatherData = state.home.weatherData || {};
  return {
    temperature: weatherData.temperature,
    humidity: weatherData.humidity,
    windSpeed: weatherData.wind_speed,
    description: weatherData.description,
  };
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #fffeaa;&quot;&gt;&lt;b&gt; const weatherData = state.home.weatherData || {};&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 구문은 JavaScript에서 흔히 사용하는 단축 평가(Short-circuit Evaluation) 문법이다.&lt;/li&gt;
&lt;li&gt;이 표현식은 state.home.weatherData가 &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;null, undefined, false, 0, NaN, &quot;&quot; 등 falsy한 값&lt;/b&gt;&lt;/span&gt;일 경우, 오른쪽의 빈 객체 {}를 weatherData 변수에 할당해. 만약 state.home.weatherData가 &lt;span style=&quot;color: #5733b1;&quot;&gt;&lt;b&gt;truthy한 값&lt;/b&gt;&lt;/span&gt;이라면 그 값을 weatherData 변수에 할당하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>jhj.sharon</author>
      <guid isPermaLink="true">https://sharonprogress.tistory.com/368</guid>
      <comments>https://sharonprogress.tistory.com/368#entry368comment</comments>
      <pubDate>Fri, 5 Jul 2024 16:36:49 +0900</pubDate>
    </item>
    <item>
      <title>[React] Redux-saga - 비동기 작업을 더 쉽게 관리할 수 있게 해주는 미들웨어</title>
      <link>https://sharonprogress.tistory.com/367</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;redux-saga는 Redux에서 비동기 작업(예: API 호출)을 더 쉽게 관리할 수 있게 해주는 미들웨어이다. redux-saga는 제너레이터 함수(generator function)를 사용하여 비동기 로직을 작성할 수 있으며, 이를 통해 코드의 가독성과 유지보수성을 높일 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 예시로, redux-saga가 어떤 역할을 하는지 설명하겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1720075216147&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { call, put, takeLatest, all } from 'redux-saga/effects';
import axios from 'axios';
import { fetchWeatherSuccess, fetchWeatherFailure } from './config/action';
import { FETCH_WEATHER_REQUEST } from './config/constant';

// 비동기 작업을 처리하는 제너레이터 함수
function* fetchWeatherSaga() {
  try {
    // API 호출
    const response = yield call(
      axios.get,
      'http://localhost:3001/current_weather',
    );
    // API 호출이 성공하면 성공 액션을 디스패치
    yield put(fetchWeatherSuccess(response.data));
  } catch (error) {
    // API 호출이 실패하면 실패 액션을 디스패치
    yield put(fetchWeatherFailure(error.message));
  }
}

// 루트 사가: 모든 사가를 합치는 곳
function* rootSaga() {
  // 특정 액션 타입이 디스패치될 때 지정된 사가를 실행
  yield all([takeLatest(FETCH_WEATHER_REQUEST, fetchWeatherSaga)]);
}

export default rootSaga;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 개념&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;제너레이터 함수&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;function* fetchWeatherSaga() { ... }와 같은 제너레이터 함수를 사용하여 비동기 로직을 작성한다. 제너레이터 함수는 yield 키워드를 사용하여 함수 실행을 일시 중지하고 재개할 수 있다.' &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;function*&lt;span&gt; ' 접두사가 이 함수가 제너레이터 함수라는 뜼이다. 이 키워드를 사용하여 만든 제너레이터 함수는 호출 했을 때 한 객체를 반환하는데, 이를 제너레이터 혹은 이터레이터 객체라고 부른다. 제너레이터의 내부 코드를 실행하기 위해서는 이 객체가 가지고 있느 next메서드를 호출하는데 enxt 메서드 호출 시마다 순차적으로 원소들을 탐색하고 yield키워드에서 반환한다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;call 이펙트&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;yield call(axios.get, 'http://localhost:3001/current_weather')는 axios.get 함수를 호출하여 API 요청을 수행한다. call 이펙트를 사용하면 함수 호출을 효과적으로 제어할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;put 이펙트&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;yield put(fetchWeatherSuccess(response.data))는 Redux 액션을 디스패치한다. put 이펙트를 사용하여 사가에서 Redux 상태를 업데이트할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;takeLatest 이펙트&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;takeLatest(FETCH_WEATHER_REQUEST, fetchWeatherSaga)는 FETCH_WEATHER_REQUEST 액션이 디스패치될 때마다 fetchWeatherSaga를 실행다. 만약 FETCH_WEATHER_REQUEST 액션이 여러 번 디스패치되면, 마지막 요청만 처리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;all 이펙트&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;yield all([...])는 여러 사가를 병렬로 실행한다. 여기서는 takeLatest를 사용하여 특정 액션이 디스패치될 때 실행할 사가를 지정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1738&quot; data-origin-height=&quot;961&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pIcX5/btsInDH7Bll/w9zIPKsI2CyrXB2YeO8041/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pIcX5/btsInDH7Bll/w9zIPKsI2CyrXB2YeO8041/img.png&quot; data-alt=&quot;출처 ㅣ https://tech.trenbe.com/2022/05/25/Redux-Saga.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pIcX5/btsInDH7Bll/w9zIPKsI2CyrXB2YeO8041/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpIcX5%2FbtsInDH7Bll%2Fw9zIPKsI2CyrXB2YeO8041%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1738&quot; height=&quot;961&quot; data-origin-width=&quot;1738&quot; data-origin-height=&quot;961&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 ㅣ https://tech.trenbe.com/2022/05/25/Redux-Saga.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예제 동작 과정&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;액션 디스패치&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트나 다른 곳에서 FETCH_WEATHER_REQUEST 액션이 디스패치된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사가 실행&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;rootSaga에서 takeLatest 이펙트를 통해 FETCH_WEATHER_REQUEST 액션을 감지하고 fetchWeatherSaga를 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;API 호출&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fetchWeatherSaga는 axios.get을 사용하여 API 호출을 수행한다. 이 때 call 이펙트를 사용하여 호출한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성공/실패 처리&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API 호출이 성공하면 fetchWeatherSuccess 액션을 디스패치하고, 실패하면 fetchWeatherFailure 액션을 디스패치한다. put 이펙트를 사용하여 액션을 디스패치한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태 업데이트&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디스패치된 성공/실패 액션에 따라 Redux 리듀서가 상태를 업데이트한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 통해 redux-saga를 사용하여 비동기 작업을 효과적으로 관리할 수 있다. redux-saga는 복잡한 비동기 로직을 간결하게 작성할 수 있게 해주며, 테스트하기도 용이다.&lt;/p&gt;</description>
      <category>front</category>
      <author>jhj.sharon</author>
      <guid isPermaLink="true">https://sharonprogress.tistory.com/367</guid>
      <comments>https://sharonprogress.tistory.com/367#entry367comment</comments>
      <pubDate>Thu, 4 Jul 2024 16:26:27 +0900</pubDate>
    </item>
  </channel>
</rss>