<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>띠띠의 개발오답노트</title>
    <link>https://ddiddibbung.tistory.com/</link>
    <description>개인 개발 블로그입니다.</description>
    <language>ko</language>
    <pubDate>Fri, 15 May 2026 02:26:34 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>띠띠뿡</managingEditor>
    <image>
      <title>띠띠의 개발오답노트</title>
      <url>https://tistory1.daumcdn.net/tistory/5819096/attach/b4039cff992b46cdb16f2e9790b1be0f</url>
      <link>https://ddiddibbung.tistory.com</link>
    </image>
    <item>
      <title>[React] 다국어 지원 자동화 - i18next, Google Spreadsheet, i18next-parser</title>
      <link>https://ddiddibbung.tistory.com/150</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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;455&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQj9zM/btsLOpeiAgk/70o08NemVM5AU3quBytRRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQj9zM/btsLOpeiAgk/70o08NemVM5AU3quBytRRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQj9zM/btsLOpeiAgk/70o08NemVM5AU3quBytRRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQj9zM%2FbtsLOpeiAgk%2F70o08NemVM5AU3quBytRRK%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;530&quot; height=&quot;455&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;455&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;현재 작업중인 프로젝트에서는 전체 영어로 개발되었는데,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;국내 결제를 추가로 연동하려면 한국어도 제공되어야 PG사 이용이 가능했다.&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;처음에는 &lt;span style=&quot;background-color: #f4f4f6; color: #50a14f; text-align: left;&quot;&gt;translate&lt;/span&gt;&lt;span style=&quot;background-color: #f4f4f6; color: #50a14f; text-align: left;&quot;&gt;.google.com&lt;/span&gt; 스크립트를 추가해서 구글 전체 번역으로 간단하게 구현을 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;예상했듯 엉망으로 번역되는 녀석,,,,,,,,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;63&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mvtrg/btsLNO6L33N/NVI0Deu4LUh9prmtxPFL7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mvtrg/btsLNO6L33N/NVI0Deu4LUh9prmtxPFL7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mvtrg/btsLNO6L33N/NVI0Deu4LUh9prmtxPFL7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMvtrg%2FbtsLNO6L33N%2FNVI0Deu4LUh9prmtxPFL7K%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;813&quot; height=&quot;63&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;63&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이왕 다국어를 지원하려면 번역자가 실시간으로 직접 번역 데이터를 삽입하면 개발자는 데이터를 가져오기만 하도록 자동화 하기로 했다. 기획자가 번역 관련 텍스트를 수정 요청 시에도 추가적인 코드 작업이 없도록 하고싶었다.&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;분명 까먹을게 뻔하기 때문에 구현 완료 후 미래의 나를 위한 기록을 남긴다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignLeft&quot; data-emoticon-type=&quot;friends2&quot; data-emoticon-name=&quot;005&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends2/large/005.png&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends2/large/005.png&quot; width=&quot;150&quot; /&gt;&lt;/figure&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;font-family: 'Nanum Gothic';&quot;&gt;시작해보자.&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;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. 필요한 라이브러리를 설치한다.&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1736835842161&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install i18next react-i18next
npm install i18next-parser
npm install google-auth-library
npm install google-spreadsheet&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. root에 i18next-parser config 파일을 생성한다.&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;(skipOnVariables: false로 설정해도 변수로 삽입된 키값은 스캔하지 못하더라,,,,,)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1736835964102&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// i18next-parser.config.cjs

module.exports = {
  input: ['src/**/*.{js,jsx,ts,tsx}'],
  output: 'src/locales/$LOCALE/$NAMESPACE.json',
  locales: ['en', 'ko'],
  defaultNamespace: 'common',
  defaultValue: (locale, namespace, key) =&amp;gt; key,
  keySeparator: false,
  namespaceSeparator: false,
  useKeysAsDefaultValue: true,
  failOnWarnings: false,
  createOldCatalogs: false,
  skipOnVariables: false,
  lexers: {
    js: ['JavascriptLexer'],
    jsx: ['JsxLexer'],
    ts: ['TypescriptLexer'],
    tsx: ['JsxLexer'],
  },
  func: {
    list: ['t'],
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
  },
  resource: {
    loadPath: 'src/locales/{{lng}}/{{ns}}.json',
    savePath: 'src/locales/{{lng}}/{{ns}}.json',
  },
};&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3. /src 내 아래 이미지 구조와 같이 폴더 및 파일을 생성하고, 각 로직을 작성한다.&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;285&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEL3gZ/btsLL8rXEmP/qghT6jQzL9prPNq6jpQHLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEL3gZ/btsLL8rXEmP/qghT6jQzL9prPNq6jpQHLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEL3gZ/btsLL8rXEmP/qghT6jQzL9prPNq6jpQHLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEL3gZ%2FbtsLL8rXEmP%2FqghT6jQzL9prPNq6jpQHLK%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;285&quot; height=&quot;148&quot; data-origin-width=&quot;285&quot; data-origin-height=&quot;148&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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; background-color: #99cefa; color: #000000;&quot;&gt;*** 구글 관련 환경 변수 및 &lt;span style=&quot;text-align: start;&quot;&gt;스프레드 시트 세팅은 아래와 같다.&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1) 구글 클라우드 콘솔에서 프로젝트를 생성 후,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;구글 콘솔 -&amp;gt; API 및 서비스 -&amp;gt; 라이브러리 -&amp;gt; google sheets API 검색&lt;/b&gt;해서 사용 버튼을 눌러준다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2) &lt;b&gt;구글 콘솔 -&amp;gt; API 및 서비스 -&amp;gt; 사용자 인증 정보 -&amp;gt; 사용자 인증 정보 만들기 -&amp;gt; API 키&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;API 키를 생성한 후 환경 변수(&lt;span style=&quot;color: #009a87;&quot;&gt;VITE_GOOGLE_API_KEY&lt;/span&gt;)에 넣어준다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3) &lt;b&gt;구글 콘솔 -&amp;gt; IAM 및 관리 -&amp;gt; 서비스 계정 -&amp;gt; 서비스 계정 만들기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;생성한 &lt;b&gt;서비스 계정 클릭&amp;nbsp;-&amp;gt; 키 -&amp;gt; 키 추가 -&amp;gt; 새 키 만들기 (키 유형 : JSON)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;해당 JSON 파일을 다운로드하면 나머지 환경 변수들(&lt;span style=&quot;color: #009a87;&quot;&gt;VITE_GOOGLE_PRIVATE_KEY, VITE_GOOGLE_CLIENT_EMAIL, VITE_GOOGLE_PROJECT_ID, VITE_GOOGLE_TOKEN_URL&lt;/span&gt;)을 얻을 수 있다. 다 쮸셔넣자.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;VITE_SHEET_ID&lt;/span&gt;는 시트 URL에서 얻을 수 있다.&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;1566&quot; data-origin-height=&quot;57&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caExFn/btsLNU6RtZX/Soe6Hwp31gkKLhdkjsxikk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caExFn/btsLNU6RtZX/Soe6Hwp31gkKLhdkjsxikk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caExFn/btsLNU6RtZX/Soe6Hwp31gkKLhdkjsxikk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaExFn%2FbtsLNU6RtZX%2FSoe6Hwp31gkKLhdkjsxikk%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;1566&quot; height=&quot;57&quot; data-origin-width=&quot;1566&quot; data-origin-height=&quot;57&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4) &lt;b&gt;구글 스프레드 시트 -&amp;gt; 공유&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;사용자에 아까 생성한 &lt;b&gt;서비스 계정&lt;/b&gt;을 &lt;u&gt;편집자&lt;/u&gt;로 추가한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;5) 시트명을 common으로 바꾸고 최상단에 en, ko를 세팅한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;568&quot; data-origin-height=&quot;82&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Jcp5x/btsLM3wEYJ2/xpDUJHeQyqUFSa4UoM5RGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Jcp5x/btsLM3wEYJ2/xpDUJHeQyqUFSa4UoM5RGK/img.png&quot; data-alt=&quot;시트명&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jcp5x/btsLM3wEYJ2/xpDUJHeQyqUFSa4UoM5RGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJcp5x%2FbtsLM3wEYJ2%2FxpDUJHeQyqUFSa4UoM5RGK%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;298&quot; height=&quot;43&quot; data-origin-width=&quot;568&quot; data-origin-height=&quot;82&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;시트명&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2324&quot; data-origin-height=&quot;89&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9WFSG/btsLM6GUGR8/TnxvVWrKIGzBEBDcwsAtn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9WFSG/btsLM6GUGR8/TnxvVWrKIGzBEBDcwsAtn1/img.png&quot; data-alt=&quot;열 세팅&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9WFSG/btsLM6GUGR8/TnxvVWrKIGzBEBDcwsAtn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9WFSG%2FbtsLM6GUGR8%2FTnxvVWrKIGzBEBDcwsAtn1%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;2324&quot; height=&quot;89&quot; data-origin-width=&quot;2324&quot; data-origin-height=&quot;89&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;열 세팅&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;pre id=&quot;code_1736836152540&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// i18next.js

import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import ko_translation from '../locales/ko/common.json';
import en_translation from '../locales/en/common.json';

i18next.use(initReactI18next).init({
  resources: {
    en: {
      translation: en_translation,
    },
    ko: {
      translation: ko_translation,
    },
  },
  lng: 'en', // 기본 언어
  fallbackLng: 'en', // 대체 언어
  debug: true,
  interpolation: {
    escapeValue: false,
  },
});

export default i18next;&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;pre id=&quot;code_1736836173285&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// sheetDownload.js

import fs from 'fs';
import { GoogleSpreadsheet } from 'google-spreadsheet';
import dotenv from 'dotenv';

dotenv.config();

(async function makeJson() {
  const doc = new GoogleSpreadsheet(process.env.VITE_SHEET_ID, {
    apiKey: process.env.VITE_GOOGLE_API_KEY,
  });

  await doc.loadInfo();
  const sheets = doc.sheetCount;

  for (let i = 0; i &amp;lt; sheets; i++) {
    const sheet = doc.sheetsByIndex[i];
    await sheet.loadCells();
    const rows = await sheet.getRows();
    const langs = await sheet._headerValues;

    const jsonData = {};

    langs.forEach((language, index) =&amp;gt; {
      for (let j = 1; j &amp;lt; rows.length + 1; j++) {
        jsonData[sheet.getCell(j, 0).value] = sheet.getCell(j, index).value;
      }

      const jsonString = JSON.stringify(jsonData, null, 2);
      fs.writeFileSync(`./src/locales/${language}/${sheet.title}.json`, jsonString);
    });
  }
})();&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;pre id=&quot;code_1736836188283&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// sheetUpload.js

import { GoogleSpreadsheet } from 'google-spreadsheet';
import { JWT } from 'google-auth-library';
import dotenv from 'dotenv';
import fs from 'fs';
import path from 'path';
import { dirname } from 'path';
import { fileURLToPath } from 'url';

dotenv.config();

async function uploadJsonToGoogleSheet() {
  try {
    const serviceAccountEmail = process.env.VITE_GOOGLE_CLIENT_EMAIL;
    const privateKey = process.env.VITE_GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n');
    const sheetId = process.env.VITE_SHEET_ID;

    const __filename = fileURLToPath(import.meta.url);
    const __dirname = dirname(__filename);

    if (!serviceAccountEmail || !privateKey || !sheetId) {
      throw new Error('환경 변수에서 필요한 서비스 계정 정보가 누락되었습니다.');
    }

    const serviceAccountAuth = new JWT({
      email: serviceAccountEmail,
      key: privateKey,
      scopes: ['https://www.googleapis.com/auth/spreadsheets'],
    });

    const doc = new GoogleSpreadsheet(sheetId || '', serviceAccountAuth);
    await doc.loadInfo();
    const sheet = doc.sheetsByIndex[0];
    if (!sheet) {
      throw new Error('첫 번째 시트를 찾을 수 없습니다.');
    }

    const enJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'en', 'common.json'), 'utf-8'));
    await addJsonToSheet(enJson, sheet);
  } catch (error) {
    console.error('Google Sheet 업데이트 중 오류 발생:', error.message);
    console.error(error.stack);
  }
}

async function addJsonToSheet(jsonData, sheet) {
  try {
    await sheet.loadHeaderRow();
    const existingRows = await sheet.getRows();

    const existingKeys = existingRows
      .filter((row) =&amp;gt; row._rawData &amp;amp;&amp;amp; row._rawData[0])
      .map((row) =&amp;gt; row._rawData[0].trim().toLowerCase());

    for (const [key] of Object.entries(jsonData)) {
      const trimmedKey = key.trim().toLowerCase();

      if (existingKeys.includes(trimmedKey)) {
        console.log(`이미 존재하는 키: ${key}, 추가하지 않음.`);
        continue;
      }
      await sheet.addRow({
        en: key,
      });
    }
  } catch (err) {
    console.error('Google Sheet 업데이트 중 오류 발생:', err.message);
  }
}

uploadJsonToGoogleSheet();&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4. package.json에 번역 및 스캔 scripts를 추가한다.&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1736836249410&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    &quot;translate_en&quot;: &quot;node ./src/locales/sheetDownload.js &amp;amp;&amp;amp; powershell -Command \&quot;Get-Content ./src/locales/en/*.json\&quot;&quot;,
    &quot;translate_ko&quot;: &quot;node ./src/locales/sheetDownload.js &amp;amp;&amp;amp; powershell -Command \&quot;Get-Content ./src/locales/ko/*.json\&quot;&quot;,
    &quot;translate&quot;: &quot;npm run translate_en&quot;,
    &quot;scan:i18n&quot;: &quot;npx i18next-parser --config i18next-parser.config.cjs &amp;amp;&amp;amp; node src/locales/sheetUpload.js&quot;&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;5. Language 상태를 바꿔주는 UI 컴포넌트를 생성한다.&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1736838141092&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { language } from '../../store/language';
import { useTranslation } from 'react-i18next';

export const LANGUAGES = [
  { code: 'ko', name: '한국어', flag: 'kr' },
  { code: 'en', name: 'English', flag: 'us' },
];

export const Language = () =&amp;gt; {
  const { i18n } = useTranslation();
  const [chooseCountry, setChooseCountry] = useState({
    code: 'en',
    name: 'English',
    flag: 'us',
  });
  const [isHovered, setIsHovered] = useState(false);
  const setLng = useSetRecoilState(language);

  const handleLanguageChange = async (lang) =&amp;gt; {
    setChooseCountry(lang);
    setLng(lang.code);
    i18n.changeLanguage(lang.code);
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;ButtonCotainer onMouseEnter={() =&amp;gt; setIsHovered(true)} onMouseLeave={() =&amp;gt; setIsHovered(false)}&amp;gt;
        &amp;lt;Flag code={chooseCountry.flag} /&amp;gt;
        {chooseCountry.name}

        {isHovered &amp;amp;&amp;amp; (
          &amp;lt;LanguageList onMouseEnter={() =&amp;gt; setIsHovered(true)} onMouseLeave={() =&amp;gt; setIsHovered(false)}&amp;gt;
            {LANGUAGES.map((lang) =&amp;gt; (
              &amp;lt;LanguageItem key={lang.code} onClick={() =&amp;gt; handleLanguageChange(lang)}&amp;gt;
                &amp;lt;Flag code={lang.flag} /&amp;gt;
                {lang.name}
              &amp;lt;/LanguageItem&amp;gt;
            ))}
          &amp;lt;/LanguageList&amp;gt;
        )}
      &amp;lt;/ButtonCotainer&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;6. 아무 컴포넌트 내 번역을 원하는 부분을 t 함수로 감싸준다.&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;(동적인 텍스트 즉, 변수는 지원되지 않는다. 젠장)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1736837912529&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const { t } = useTranslation();

// 개행을 원할 시 \n 추가 후 white-space: pre-line; 스타일을 먹여준다.
&amp;lt;h3&amp;gt;{t('Master\n the Art of 3D')}&amp;lt;/h3&amp;gt;
&amp;lt;p&amp;gt;{t('test')}&amp;lt;/p&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1555&quot; data-origin-height=&quot;1125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IrfqZ/btsLM2LnJ1T/Yt8zqNRpRhlnmN1eVDfa9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IrfqZ/btsLM2LnJ1T/Yt8zqNRpRhlnmN1eVDfa9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IrfqZ/btsLM2LnJ1T/Yt8zqNRpRhlnmN1eVDfa9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIrfqZ%2FbtsLM2LnJ1T%2FYt8zqNRpRhlnmN1eVDfa9k%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;1555&quot; height=&quot;1125&quot; data-origin-width=&quot;1555&quot; data-origin-height=&quot;1125&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이제 해당 script를 실행해보자!  &lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;npm run scan:i18n 실행 시,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt; parser.config에서 지정한 파일을 스캔 후 t 함수가 사용된 부분을 찾아 키값을 각 common.json 파일에 추가 후 시트에 업로드한다. 구글 스프레드 시트를 직접 확인해보면 키값이 삽입되어 있는 것을 확인할 수 있다.&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이제 번역자가 업로드된 시트의 키값을 확인 후 ko열에 번역한 텍스트를 삽입한다. 작업을 마치고 이번엔 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;npm run &lt;/span&gt;translate를 실행하면, 번역된 파일이 각 common.json에 추가 된 것을 확인할 수 있다.&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;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: center;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;끗! 뿌듯&lt;/span&gt;&lt;/h2&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends2&quot; data-emoticon-name=&quot;026&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends2/large/026.png&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends2/large/026.png&quot; width=&quot;150&quot; /&gt;&lt;/figure&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React.js</category>
      <category>google-auth-library</category>
      <category>google-spreadsheet</category>
      <category>i18n</category>
      <category>i18next</category>
      <category>i18next-parser</category>
      <category>javascript</category>
      <category>react</category>
      <category>다국어 자동화</category>
      <category>다국어 지원</category>
      <category>프론트엔드 개발자</category>
      <author>띠띠뿡</author>
      <guid isPermaLink="true">https://ddiddibbung.tistory.com/150</guid>
      <comments>https://ddiddibbung.tistory.com/150#entry150comment</comments>
      <pubDate>Tue, 14 Jan 2025 16:30:01 +0900</pubDate>
    </item>
    <item>
      <title>[react] Drag And Drop 구현 코드</title>
      <link>https://ddiddibbung.tistory.com/149</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;pre id=&quot;code_1733286943179&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { IoLogoOctocat } from 'react-icons/io';
import styled from 'styled-components';
import testImg from '../../../assets/collection/carousel_01.jpg';
import { FaPlus } from 'react-icons/fa';
import { MdDeleteSweep } from 'react-icons/md';
import { useState } from 'react';
import { BasicButton } from '../../common/BasicButton';
import { theme } from '../../../styles/theme';

export const PropShopMedia = () =&amp;gt; {
  const [draggedItemIndex, setDraggedItemIndex] = useState(null);
  const [carouselData, setCarouselData] = useState([{ id: 1, title: '', description: '', image: testImg }]);

  const addCarouselData = () =&amp;gt; {
    setCarouselData((prevData) =&amp;gt; [
      ...prevData,
      {
        id: prevData.length + 1,
        title: '',
        description: '',
        image: testImg,
      },
    ]);
  };

  const handleDragStart = (index) =&amp;gt; {
    setDraggedItemIndex(index);
  };

  const handleDragOver = (event) =&amp;gt; {
    event.preventDefault();
  };

  const handleDrop = (index) =&amp;gt; {
    if (draggedItemIndex === null) return;

    const updatedData = [...carouselData];
    const [draggedItem] = updatedData.splice(draggedItemIndex, 1);
    updatedData.splice(index, 0, draggedItem);

    setCarouselData(updatedData);
    setDraggedItemIndex(null);
  };

  const handleDropToDelete = () =&amp;gt; {
    if (draggedItemIndex === null) return;

    const updatedData = [...carouselData];
    updatedData.splice(draggedItemIndex, 1);

    setCarouselData(updatedData);
    setDraggedItemIndex(null);
  };

  const handleReset = () =&amp;gt; {
    setCarouselData([{ id: 1, title: '', description: '', image: testImg }]);
  };

  return (
    &amp;lt;PropShopMediaContainer $isDragging={draggedItemIndex !== null}&amp;gt;
      &amp;lt;div className='header'&amp;gt;
        &amp;lt;div className='left'&amp;gt;
          &amp;lt;IoLogoOctocat /&amp;gt;
          INTRO CAROUSEL IMAGES
        &amp;lt;/div&amp;gt;
        &amp;lt;div className='right'&amp;gt;
          &amp;lt;BasicButton onClick={handleReset}&amp;gt;초기화&amp;lt;/BasicButton&amp;gt;
          &amp;lt;BasicButton
            bgColor={theme.colors.blue}
            fontColor={theme.colors.white}
            borderColor={theme.colors.blue}
            hoverBgColor={theme.colors.darkBlue}
          &amp;gt;
            저장
          &amp;lt;/BasicButton&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;div className='carousel-data-list'&amp;gt;
        {carouselData.map((data, index) =&amp;gt; (
          &amp;lt;div
            className='carousel-data'
            key={data.id}
            draggable
            onDragStart={() =&amp;gt; handleDragStart(index)}
            onDragOver={handleDragOver}
            onDrop={() =&amp;gt; handleDrop(index)}
          &amp;gt;
            &amp;lt;div className='carousel-image'&amp;gt;
              &amp;lt;img src={data.image} alt={`carousel-${index + 1}`} /&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div className='index'&amp;gt;{data.id}&amp;lt;/div&amp;gt;
            &amp;lt;div className='title'&amp;gt;
              &amp;lt;input
                type='text'
                name={`title-${data.id}`}
                placeholder='타이틀을 입력하세요.'
                value={data.title}
                onChange={(e) =&amp;gt;
                  setCarouselData((prevData) =&amp;gt;
                    prevData.map((item) =&amp;gt; (item.id === data.id ? { ...item, title: e.target.value } : item))
                  )
                }
              /&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div className='description'&amp;gt;
              &amp;lt;input
                type='text'
                name={`description-${data.id}`}
                placeholder='내용을 입력하세요.'
                value={data.description}
                onChange={(e) =&amp;gt;
                  setCarouselData((prevData) =&amp;gt;
                    prevData.map((item) =&amp;gt; (item.id === data.id ? { ...item, description: e.target.value } : item))
                  )
                }
              /&amp;gt;
            &amp;lt;/div&amp;gt;
          &amp;lt;/div&amp;gt;
        ))}
        &amp;lt;div
          className='add-carousel-data'
          onClick={addCarouselData}
          onDragOver={handleDragOver}
          onDrop={handleDropToDelete}
        &amp;gt;
          {draggedItemIndex !== null ? &amp;lt;MdDeleteSweep className='delete-icon' /&amp;gt; : &amp;lt;FaPlus /&amp;gt;}
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/PropShopMediaContainer&amp;gt;
  );
};

const PropShopMediaContainer = styled.div`
  .header {
    display: flex;
    align-items: center;
    justify-content: space-between;

    .left {
      display: flex;
      align-items: center;
      gap: 6px;
      padding: 12px 0;
      font-size: 24px;
      font-family: Pretendard-SemiBold;

      svg {
        font-size: 28px;
      }
    }
    .right {
      display: flex;
      gap: 12px;
    }
  }

  .carousel-data-list {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;

    .carousel-data {
      position: relative;
      background-color: ${(props) =&amp;gt; props.theme.colors.darkGray};
      padding: 12px;
      border-radius: 4px;

      input {
        width: 100%;
        background-color: ${(props) =&amp;gt; props.theme.colors.lightGray};
        padding: 4px 8px;
        border: none;
        border-radius: 4px;
        margin-top: 8px;
        outline: none;
        color: ${(props) =&amp;gt; props.theme.colors.darkGray};
      }

      .index {
        position: absolute;
        top: 2px;
        left: 3px;
        font-size: 12px;
        color: ${(props) =&amp;gt; props.theme.colors.lightGray}80;
      }

      .carousel-image {
        width: 100%;
        height: calc(450px / 2);
        border: 1px solid ${(props) =&amp;gt; props.theme.colors.darkGray};
        border-radius: 4px;

        img {
          width: 100%;
          height: 100%;
          object-fit: cover;
          border-radius: 4px;
        }
      }
    }
  }

  .add-carousel-data {
    background-color: ${(props) =&amp;gt; props.theme.colors.darkGray}50;
    min-height: 312px;
    border-radius: 4px;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    border: 3px solid ${(props) =&amp;gt; (props.$isDragging ? `${props.theme.colors.red}90` : `inherite`)};

    .delete-icon {
      color: ${(props) =&amp;gt; props.theme.colors.red};
    }

    svg {
      font-size: 48px;
    }

    &amp;amp;:hover {
      background-color: ${(props) =&amp;gt; props.theme.colors.darkGray}90;
    }
  }
`;&lt;/code&gt;&lt;/pre&gt;</description>
      <category>React.js/react</category>
      <author>띠띠뿡</author>
      <guid isPermaLink="true">https://ddiddibbung.tistory.com/149</guid>
      <comments>https://ddiddibbung.tistory.com/149#entry149comment</comments>
      <pubDate>Wed, 4 Dec 2024 13:36:37 +0900</pubDate>
    </item>
    <item>
      <title>[react-slick] currentSlide, slideCount error</title>
      <link>https://ddiddibbung.tistory.com/148</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #ee2323; font-family: 'Nanum Gothic';&quot;&gt; react does not recognize the `currentSlide` prop on a dom element. if you intentionally want it to appear in the dom as a custom attribute, spell it as lowercase `currentslide` instead. if you accidentally passed it from a parent component, remove it from the dom element. &lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #ee2323; font-family: 'Nanum Gothic';&quot;&gt;react does not recognize the `slideCount` prop on a dom element. if you intentionally want it to appear in the dom as a custom attribute, spell it as lowercase `slidecount` instead. if you accidentally passed it from a parent component, remove it from the dom element.&lt;/span&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;react-slick 라이브러리를 사용하는데, 위의 warning을 만났다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;아래는 &lt;b&gt;수정 전&lt;/b&gt; 코드이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1721095635336&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Slider from 'react-slick';
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';
import { IoIosArrowBack, IoIosArrowForward } from 'react-icons/io';

export const BasicCarousel = ({
  imageList,
  alt = 'prop',
  dots = true,
  slidesToShow,
  slidesToScroll,
  height,
  infinite = true,
  prevArrow = &amp;lt;IoIosArrowBack /&amp;gt;,
  nextArrow = &amp;lt;IoIosArrowForward /&amp;gt;,
  onItemClick,
}) =&amp;gt; {
  const settings = {
    dots,
    infinite,
    speed: 500,
    slidesToShow,
    slidesToScroll,
    prevArrow,
    nextArrow,
  };

  return (
    &amp;lt;StyledSlider {...settings} $height={height}&amp;gt;
      ...
    &amp;lt;/StyledSlider&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #1f1f1f; color: #cccccc;&quot;&gt;&amp;nbsp;&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;서칭해보니, Arrow를 커스텀 후 사용하면 해결된다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://github.com/akiran/react-slick/issues/623#issuecomment-629764816&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/akiran/react-slick/issues/623#issuecomment-629764816&lt;/a&gt;&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;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;아래는 &lt;b&gt;수정된&lt;/b&gt; 코드이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1721095940067&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Slider from 'react-slick';
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';
import { IoIosArrowBack, IoIosArrowForward } from 'react-icons/io';

export const BasicCarousel = ({
  imageList,
  alt = 'prop',
  dots = true,
  slidesToShow,
  slidesToScroll,
  height,
  infinite = true,
  onItemClick,
}) =&amp;gt; {
	
    // arrow 커스텀
  const PrevgArrow = ({ currentSlide, slideCount, ...props }) =&amp;gt; &amp;lt;IoIosArrowBack {...props} /&amp;gt;;
  const NextArrow = ({ currentSlide, slideCount, ...props }) =&amp;gt; &amp;lt;IoIosArrowForward {...props} /&amp;gt;;

  const settings = {
    dots,
    infinite,
    speed: 500,
    slidesToShow,
    slidesToScroll,
    prevArrow: &amp;lt;PrevgArrow /&amp;gt;,
    nextArrow: &amp;lt;NextArrow /&amp;gt;,
  };

  return (
    &amp;lt;StyledSlider {...settings} $height={height}&amp;gt;
      ...
    &amp;lt;/StyledSlider&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>error</category>
      <category>carousel</category>
      <category>react</category>
      <category>react-slick</category>
      <category>개발자</category>
      <category>라이브러리</category>
      <category>캐러셀</category>
      <category>프론트엔드</category>
      <author>띠띠뿡</author>
      <guid isPermaLink="true">https://ddiddibbung.tistory.com/148</guid>
      <comments>https://ddiddibbung.tistory.com/148#entry148comment</comments>
      <pubDate>Tue, 16 Jul 2024 11:13:52 +0900</pubDate>
    </item>
    <item>
      <title>[react] 3D carousel 구현해보기!</title>
      <link>https://ddiddibbung.tistory.com/147</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;pre id=&quot;code_1720420451468&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useState } from 'react';
import styled from 'styled-components';

export const Carousel3D = () =&amp;gt; {
  const [rotation, setRotation] = useState(0);

  const cells = Array.from({ length: 9 }, (_, i) =&amp;gt; i + 1);
  const rotateCarousel = (direction) =&amp;gt; {
    setRotation(rotation + (direction === 'next' ? -40 : 40));
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;Scene&amp;gt;
        &amp;lt;Carousel style={{ transform: `translateZ(-288px) rotateY(${rotation}deg)` }}&amp;gt;
          {cells.map((cell, index) =&amp;gt; (
            &amp;lt;Cell key={index} style={{ '--i': index }}&amp;gt;
              {cell}
            &amp;lt;/Cell&amp;gt;
          ))}
        &amp;lt;/Carousel&amp;gt;
      &amp;lt;/Scene&amp;gt;
      &amp;lt;CarouselOptions&amp;gt;
        &amp;lt;button onClick={() =&amp;gt; rotateCarousel('prev')}&amp;gt;&amp;larr;&amp;lt;/button&amp;gt;
        &amp;lt;button onClick={() =&amp;gt; rotateCarousel('next')}&amp;gt;&amp;rarr;&amp;lt;/button&amp;gt;
      &amp;lt;/CarouselOptions&amp;gt;
    &amp;lt;/&amp;gt;
  );
};

const Scene = styled.div`
  border: 1px solid #ccc;
  margin: 40px 0;
  position: relative;
  width: 210px;
  height: 140px;
  margin: 80px auto;
  perspective: 1000px;
`;

const Carousel = styled.div`
  width: 100%;
  height: 100%;
  position: absolute;
  transform-style: preserve-3d;
  transition: transform 1s;
`;

const Cell = styled.div`
  position: absolute;
  width: 190px;
  height: 120px;
  left: 10px;
  top: 10px;
  border: 2px solid black;
  line-height: 116px;
  font-size: 80px;
  font-weight: bold;
  color: white;
  text-align: center;
  transition: transform 1s, opacity 1s;
  background: ${({ style }) =&amp;gt; `hsla(${style['--i'] * 40}, 100%, 50%, 0.8)`};
  transform: ${({ style }) =&amp;gt; `rotateY(${style['--i'] * 40}deg) translateZ(288px)`};
`;

const CarouselOptions = styled.div`
  text-align: center;
  position: relative;
  z-index: 2;
  background: hsla(0, 0%, 100%, 0.8);

  button {
    margin: 10px;
    padding: 5px 10px;
    font-size: 16px;
  }
`;&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNKfAt/btsIrP9h0nD/qSBMuAsiUktvwGlucovWA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNKfAt/btsIrP9h0nD/qSBMuAsiUktvwGlucovWA0/img.png&quot; data-alt=&quot;결과 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNKfAt/btsIrP9h0nD/qSBMuAsiUktvwGlucovWA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNKfAt%2FbtsIrP9h0nD%2FqSBMuAsiUktvwGlucovWA0%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;1128&quot; height=&quot;610&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;610&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>React.js/react</category>
      <author>띠띠뿡</author>
      <guid isPermaLink="true">https://ddiddibbung.tistory.com/147</guid>
      <comments>https://ddiddibbung.tistory.com/147#entry147comment</comments>
      <pubDate>Mon, 8 Jul 2024 15:34:44 +0900</pubDate>
    </item>
    <item>
      <title>[react-native] range slider (feat . @ptomasroos/react-native-multi-slider)</title>
      <link>https://ddiddibbung.tistory.com/146</link>
      <description>&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;숫자의 범위를 지정하는 라이브러리가 필요했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/@ptomasroos/react-native-multi-slider&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.npmjs.com/package/@ptomasroos/react-native-multi-slider&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1713835803222&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;@ptomasroos/react-native-multi-slider&quot; data-og-description=&quot;Android and iOS supported pure JS slider component with multiple markers for React Native. Latest version: 2.2.2, last published: 4 years ago. Start using @ptomasroos/react-native-multi-slider in your project by running &amp;#96;npm i @ptomasroos/react-native-mult&quot; data-og-host=&quot;www.npmjs.com&quot; data-og-source-url=&quot;https://www.npmjs.com/package/@ptomasroos/react-native-multi-slider&quot; data-og-url=&quot;https://www.npmjs.com/package/@ptomasroos/react-native-multi-slider&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cdPIS8/hyVSZYLl8Y/d3SaLVc5KPdJcb2hPv4af1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/@ptomasroos/react-native-multi-slider&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.npmjs.com/package/@ptomasroos/react-native-multi-slider&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cdPIS8/hyVSZYLl8Y/d3SaLVc5KPdJcb2hPv4af1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;@ptomasroos/react-native-multi-slider&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Android and iOS supported pure JS slider component with multiple markers for React Native. Latest version: 2.2.2, last published: 4 years ago. Start using @ptomasroos/react-native-multi-slider in your project by running `npm i @ptomasroos/react-native-mult&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.npmjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;사용법이 어렵지 않았고, 커스텀 하기도 편해서 자주 애용할 듯 하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1713835907636&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const customMarker = (index) =&amp;gt; {
    return (
      &amp;lt;View style={styles.marker}&amp;gt;
        &amp;lt;Text&amp;gt;{ageRange[index]}&amp;lt;/Text&amp;gt;
      &amp;lt;/View&amp;gt;
    );
  };
  
  ...

&amp;lt;MultiSlider
  isMarkersSeparated={true}
  values={ageRange}
  sliderLength={380}
  onValuesChange={(values) =&amp;gt; setAgeRange(values)}
  min={0}
  max={100}
  step={1}
  customMarkerLeft={() =&amp;gt; customMarker(0)}
  customMarkerRight={() =&amp;gt; customMarker(1)}
  selectedStyle={{ backgroundColor: colors.navy }}
  unselectedStyle={{ backgroundColor: colors.lightGray }}
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;218&quot; data-origin-height=&quot;481&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RPFIR/btsGQ1KaMVj/5qzqY6rii9dXUcXFYUp9y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RPFIR/btsGQ1KaMVj/5qzqY6rii9dXUcXFYUp9y1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RPFIR/btsGQ1KaMVj/5qzqY6rii9dXUcXFYUp9y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRPFIR%2FbtsGQ1KaMVj%2F5qzqY6rii9dXUcXFYUp9y1%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;233&quot; height=&quot;514&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;218&quot; data-origin-height=&quot;481&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>range slider</category>
      <category>react native</category>
      <category>개발자</category>
      <category>라이브러리</category>
      <category>숫자 슬라이더</category>
      <category>숫자범위</category>
      <category>프론트엔드</category>
      <author>띠띠뿡</author>
      <guid isPermaLink="true">https://ddiddibbung.tistory.com/146</guid>
      <comments>https://ddiddibbung.tistory.com/146#entry146comment</comments>
      <pubDate>Tue, 23 Apr 2024 10:36:16 +0900</pubDate>
    </item>
    <item>
      <title>[react-native] Nesting navigators</title>
      <link>https://ddiddibbung.tistory.com/145</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;pre id=&quot;code_1713162145403&quot; class=&quot;coffeescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;npm install @react-navigation/stack @react-navigation/bottom-tabs&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Tab navigator로 구성한 상태에서 Tab에 존재하지 않는 스크린을 추가하고 싶었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt; 검색해보니 &lt;span style=&quot;color: #1c1e21; text-align: start;&quot;&gt;새 화면이 스택 위에 배치되는&lt;/span&gt; Stack navigator를 사용하면 될 거라 생각했다.&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;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt; ERROR Error: Another navigator is already registered for this container. You likely have multiple navigators under a single &quot;NavigationContainer&quot; or &quot;Screen&quot;. Make sure each navigator is under a separate &quot;Screen&quot; container. See https://reactnavigation.org/docs/nesting-navigators for a guide on nesting.&lt;/span&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;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;lt;NavigationContainer /&amp;gt; 내에 stack과 tab을 같이 사용해서 오류가 나는 것을 확인했고, 공식 사이트에서 &lt;b&gt;Nesting navigators&lt;/b&gt;를 찾아서 예제 코드를 확인 후 해결할 수 있었다.&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;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://reactnavigation.org/docs/nesting-navigators/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://reactnavigation.org/docs/nesting-navigators/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1713161698391&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Nesting navigators | React Navigation&quot; data-og-description=&quot;Nesting navigators means rendering a navigator inside a screen of another navigator, for example:&quot; data-og-host=&quot;reactnavigation.org&quot; data-og-source-url=&quot;https://reactnavigation.org/docs/nesting-navigators/&quot; data-og-url=&quot;https://reactnavigation.org/docs/nesting-navigators&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://reactnavigation.org/docs/nesting-navigators/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://reactnavigation.org/docs/nesting-navigators/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Nesting navigators | React Navigation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Nesting navigators means rendering a navigator inside a screen of another navigator, for example:&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;reactnavigation.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;걍 stack navigator 안에 tab &lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;navigator를&lt;/span&gt;&amp;nbsp;포함시키면 됨.&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;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1713162239686&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function App() {
  return &amp;lt;Navigator /&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1713162249900&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Stack = createStackNavigator();

export const Navigator = () =&amp;gt; {
  return (
    &amp;lt;NavigationContainer&amp;gt;
      &amp;lt;Stack.Navigator initialRouteName=&quot;Home&quot;&amp;gt;
        &amp;lt;Stack.Screen name=&quot;Tab&quot; component={TabNavigation} options={{ headerShown: false }} /&amp;gt;
        &amp;lt;Stack.Screen name=&quot;PersonProfile&quot; component={PersonProfile} /&amp;gt;
      &amp;lt;/Stack.Navigator&amp;gt;
    &amp;lt;/NavigationContainer&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1713162678909&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Tab = createBottomTabNavigator();

export const TabNavigation = () =&amp;gt; {
  return (
    &amp;lt;Tab.Navigator&amp;gt;
      {TAB_MENU.map((menu) =&amp;gt; {
        return (
          &amp;lt;Tab.Screen
            name={menu.name}
            component={menu.component}
            options={{
              tabBarIcon: ({ size, color }) =&amp;gt; &amp;lt;MaterialIcons name={menu.icon} size={size} color={color} /&amp;gt;,
              headerShown: false,
              tabBarActiveTintColor: 'green',
              tabBarInactiveTintColor: 'gray',
            }}
          /&amp;gt;
        );
      })}
    &amp;lt;/Tab.Navigator&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>react-native</category>
      <category>개발공부</category>
      <category>개발자</category>
      <category>공부</category>
      <category>앱개발</category>
      <category>프론트엔드</category>
      <author>띠띠뿡</author>
      <guid isPermaLink="true">https://ddiddibbung.tistory.com/145</guid>
      <comments>https://ddiddibbung.tistory.com/145#entry145comment</comments>
      <pubDate>Mon, 15 Apr 2024 15:36:42 +0900</pubDate>
    </item>
    <item>
      <title>[react-to-print] 특정 컴포넌트 프린트하기</title>
      <link>https://ddiddibbung.tistory.com/144</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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;버튼 클릭 시, 원하는 컴포넌트를 프린트하는 기능을 넣고 싶었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;역시나 라이브러리가 존재하는군!&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;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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;1. 라이브러리를 설치한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/react-to-print&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.npmjs.com/package/react-to-print&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1708915094708&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;react-to-print&quot; data-og-description=&quot;Print React components in the browser. Latest version: 2.15.1, last published: 12 days ago. Start using react-to-print in your project by running &amp;#96;npm i react-to-print&amp;#96;. There are 282 other projects in the npm registry using react-to-print.&quot; data-og-host=&quot;www.npmjs.com&quot; data-og-source-url=&quot;https://www.npmjs.com/package/react-to-print&quot; data-og-url=&quot;https://www.npmjs.com/package/react-to-print&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bptTFN/hyVqujI5dz/Tj3c7mEbcLVdepSVkZycik/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/lCumK/hyVqmMK6GU/Z0wkpYV0GIw8hPiQdtIgNK/img.png?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/react-to-print&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.npmjs.com/package/react-to-print&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bptTFN/hyVqujI5dz/Tj3c7mEbcLVdepSVkZycik/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/lCumK/hyVqmMK6GU/Z0wkpYV0GIw8hPiQdtIgNK/img.png?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;react-to-print&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Print React components in the browser. Latest version: 2.15.1, last published: 12 days ago. Start using react-to-print in your project by running `npm i react-to-print`. There are 282 other projects in the npm registry using react-to-print.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.npmjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;pre id=&quot;code_1708915086743&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i react-to-print&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;&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;2. useReactToPrint 훅을 가져온다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1708915172764&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useReactToPrint } from 'react-to-print';&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;&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;3. 프린트하길 원하는 컴포넌트에 ref를 달아준다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1708915294567&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;const componentRef = useRef(null);

&amp;lt;PatientListContainer ref={componentRef}&amp;gt;
   ...
&amp;lt;/PatientListContainer&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;&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;4. 버튼에 이벤트를 등록하면 끗!&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1708915208117&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const handlePrint = useReactToPrint({
    content: () =&amp;gt; componentRef.current,
});

...

&amp;lt;span role=&quot;button&quot; onClick={handlePrint}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;결과 화면&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1880&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A0AcH/btsFhq57GnF/5KwWCgqUUCuGOpC8KDiQL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A0AcH/btsFhq57GnF/5KwWCgqUUCuGOpC8KDiQL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A0AcH/btsFhq57GnF/5KwWCgqUUCuGOpC8KDiQL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA0AcH%2FbtsFhq57GnF%2F5KwWCgqUUCuGOpC8KDiQL1%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;1880&quot; height=&quot;768&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1880&quot; data-origin-height=&quot;768&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1294&quot; data-origin-height=&quot;810&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceKB6d/btsFhpsy6Iy/i0NcokAC5clTz7ml94D6Y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceKB6d/btsFhpsy6Iy/i0NcokAC5clTz7ml94D6Y0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceKB6d/btsFhpsy6Iy/i0NcokAC5clTz7ml94D6Y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceKB6d%2FbtsFhpsy6Iy%2Fi0NcokAC5clTz7ml94D6Y0%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;695&quot; height=&quot;435&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1294&quot; data-origin-height=&quot;810&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>React.js/라이브러리</category>
      <category>fe</category>
      <category>react</category>
      <category>react print</category>
      <category>react-to-print</category>
      <category>typescript</category>
      <category>리액트 프린트</category>
      <category>프론트엔드</category>
      <category>프론트엔드개발자</category>
      <category>프린트기능</category>
      <author>띠띠뿡</author>
      <guid isPermaLink="true">https://ddiddibbung.tistory.com/144</guid>
      <comments>https://ddiddibbung.tistory.com/144#entry144comment</comments>
      <pubDate>Mon, 26 Feb 2024 11:45:08 +0900</pubDate>
    </item>
    <item>
      <title>[vercel 빌드 오류] Cannot find module '파일 경로' or its corresponding type declarations.</title>
      <link>https://ddiddibbung.tistory.com/143</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;&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;font-family: 'Nanum Gothic';&quot;&gt;로컬에서는 빌드 오류가 없었는데, Vercel로 배포 시 해당 오류가 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;로컬 저장소와 원격 저장소(git)의 파일명의 대소문자 차이가 원인이었다.&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;git에서 파일명의 대소문자를 구분하도록 설정&lt;/b&gt;하여 해결하였다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1708481805617&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git config core.ignorecase false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;설정 후 파일명 변경 사항을 푸시해준다.&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;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;- 참고&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://velog.io/@shyuuuuni/Vercel-Cannot-find-module-%ED%8C%8C%EC%9D%BC%EB%AA%85-or-its-corresponding-type-declarations-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0?utm_source=oneoneone&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@shyuuuuni/Vercel-Cannot-find-module-%ED%8C%8C%EC%9D%BC%EB%AA%85-or-its-corresponding-type-declarations-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0?utm_source=oneoneone&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>error</category>
      <category>Cannot find module</category>
      <category>Git</category>
      <category>vercel</category>
      <category>vercel 배포</category>
      <category>vercel 빌드</category>
      <category>깃허브</category>
      <category>배포오류</category>
      <category>빌드오류</category>
      <author>띠띠뿡</author>
      <guid isPermaLink="true">https://ddiddibbung.tistory.com/143</guid>
      <comments>https://ddiddibbung.tistory.com/143#entry143comment</comments>
      <pubDate>Wed, 21 Feb 2024 11:17:57 +0900</pubDate>
    </item>
    <item>
      <title>[dayjs] x시간 전</title>
      <link>https://ddiddibbung.tistory.com/142</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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;알림 데이터에 'x시간 전' 기능을 넣고싶었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;dayjs를 활용하면 간단하게 구현이 가능했다.&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;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;font-family: 'Nanum Gothic';&quot;&gt;1. dayjs 라이브러리를 설치한다.&lt;/span&gt;&lt;/b&gt;&lt;/h4&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;font-family: 'Nanum Gothic';&quot;&gt;2. dayjs&lt;span style=&quot;background-color: #ffffff; color: #374151; text-align: left;&quot;&gt;와 필요한 plugin 및 &lt;span style=&quot;background-color: #ffffff; color: #111827; text-align: left;&quot;&gt;locale&lt;/span&gt;을 불러온다.&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1707268423278&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/ko';&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #374151; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;dayjs: 날짜와 시간을 처리하는 라이브러리&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;relativeTime: 상대적인 시간을 표시하는 기능을 추가하는 &lt;span style=&quot;background-color: #ffffff; color: #374151; text-align: left;&quot;&gt;plugin&lt;/span&gt; &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;dayjs/locale/ko: 한국어 &lt;span style=&quot;background-color: #ffffff; color: #111827; text-align: left;&quot;&gt;locale&lt;/span&gt; &lt;/span&gt;&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;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;font-family: 'Nanum Gothic';&quot;&gt;3. &lt;span style=&quot;background-color: #ffffff; color: #111827; text-align: left;&quot;&gt;dayjs에 relativeTime&lt;span style=&quot;background-color: #ffffff; color: #374151; text-align: left;&quot;&gt; 플러그인을 추가하고 한국어로 설정 후 현재 시간을 가져온다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1707268564447&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dayjs.extend(relativeTime);
dayjs.locale('ko');
const now = dayjs();&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;&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;font-family: 'Nanum Gothic';&quot;&gt;4. 날짜를 dayjs 객체로 변환 후 from(now) 메서드로 현재 시간과의 차이를 계산한다.&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1707268708914&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;span&amp;gt;{dayjs(item.date).from(now)}&amp;lt;/span&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #374151; text-align: start;&quot;&gt;날짜를 현재 시간(&lt;/span&gt;now&lt;span style=&quot;background-color: #ffffff; color: #374151; text-align: start;&quot;&gt;)으로부터 얼마나 지났는지를 상대적인 시간으로 변환하여 표시하는 기능을 수행하며, &lt;span style=&quot;background-color: #ffffff; color: #374151; text-align: start;&quot;&gt;&lt;b&gt; &quot;몇 분 전&quot;, &quot;몇 시간 전&quot;, &quot;어제&quot;&lt;/b&gt; 등의 형식으로 시간 차이를 표현한다.&lt;/span&gt;&lt;/span&gt;&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;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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;395&quot; data-origin-height=&quot;349&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIblZX/btsEu7z0pXT/L16IcLs7hOo4Tryaq0lRj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIblZX/btsEu7z0pXT/L16IcLs7hOo4Tryaq0lRj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIblZX/btsEu7z0pXT/L16IcLs7hOo4Tryaq0lRj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIblZX%2FbtsEu7z0pXT%2FL16IcLs7hOo4Tryaq0lRj1%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;395&quot; height=&quot;349&quot; data-origin-width=&quot;395&quot; data-origin-height=&quot;349&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;</description>
      <category>React.js/라이브러리</category>
      <category>dayjs</category>
      <category>x시간 전</category>
      <category>몇시간 전</category>
      <author>띠띠뿡</author>
      <guid isPermaLink="true">https://ddiddibbung.tistory.com/142</guid>
      <comments>https://ddiddibbung.tistory.com/142#entry142comment</comments>
      <pubDate>Wed, 7 Feb 2024 10:23:22 +0900</pubDate>
    </item>
    <item>
      <title>[React] SSE(Server-Sent Event)를 활용한 알림 기능 구현</title>
      <link>https://ddiddibbung.tistory.com/141</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;h2 id=&quot;3-sse-server-sent-event&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;SSE (Server-Sent Event)란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt; &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;서버에서 클라이언트로 데이터를 보내는&amp;nbsp;&lt;/span&gt;&lt;b&gt;단방향 통신&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;실시간 알림 기능과 같이 단방향으로 데이터를 보낼 때 사용된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;서버와 클라이언트의 연결 상태를 유지하고, 서버는 클라이언트로 지속적으로 데이터를 전송할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;eventsource&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;EventSource&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;JS의 EventSource로 연결 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;EventSource.onopen&amp;nbsp;: 서버와 연결 시 호출됨&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;EventSource.onmessage&amp;nbsp;: 서버로부터 메시지 수신 시 &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;호출됨&lt;/span&gt; &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;EventSource.onerror&amp;nbsp;: 에러 감지 시 호출됨&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;실전-프로젝트에서-실시간-알림-구현하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;실시간 알림 구현 (프론트단 코드)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;header에 토큰을 담아 보내야 했는데, &lt;span style=&quot;background-color: #e9ecef; color: #212529; text-align: left;&quot;&gt;event-source-polyfill&lt;/span&gt;&lt;span style=&quot;color: #212529; text-align: left;&quot;&gt; 라이브러리를 사용해서 해결&lt;/span&gt;&lt;span style=&quot;color: #212529; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1707104524228&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const EventSource = EventSourcePolyfill || NativeEventSource;

  useEffect(() =&amp;gt; {
    if (setIsSignIned) {
      console.log('SSE작동 시작!');
      let eventSource: EventSourcePolyfill;
      const fetchSSE = async () =&amp;gt; {
        try {
          eventSource = new EventSource(`/api/notify`, {
            headers: {
              Authorization: `Bearer ${API_TOKEN}`,
            },
            withCredentials: true,
          });

          /* EVENTSOURCE ONMESSAGE ---------------------------------------------------- */
          eventSource.onmessage = async (event) =&amp;gt; {
            const res = await event.data;
            console.log('res', res);
            // 쿼리키를 활용한 refetching
          };

          /* EVENTSOURCE ONERROR ------------------------------------------------------ */
          eventSource.onerror = async () =&amp;gt; {};
        } catch (error) {
          console.log(error);
        }
      };
      fetchSSE();
      return () =&amp;gt; eventSource.close();
    }
  }, [setIsSignIned]);&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>띠띠뿡</author>
      <guid isPermaLink="true">https://ddiddibbung.tistory.com/141</guid>
      <comments>https://ddiddibbung.tistory.com/141#entry141comment</comments>
      <pubDate>Mon, 5 Feb 2024 12:42:53 +0900</pubDate>
    </item>
  </channel>
</rss>