<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Atrix의 웹 개발 및 삽질 블로그</title>
    <link>https://atrix-developer.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sun, 26 Jul 2026 13:15:47 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Atrix</managingEditor>
    <item>
      <title>&amp;quot;일단 시작하고 보자&amp;quot; 라는 생각은 독이될 수 있다...</title>
      <link>https://atrix-developer.tistory.com/10</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;작년 11월부터 주말마다 2시간씩 지인들과 사이드프로젝트를 진행하고있다. 그리고 이 글을 쓰는 현재, 이 진행 중이던 작업물들을 폐기하고 처음부터 다시 만들기로 했다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 철저한 기획과 설계의 부재이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 참여 인원들 중 디자이너가 없다보니 UI를 구성하는게 매우 어려운 일이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다보니 AI의 발전을 발판삼아 UI 구성을 AI에게 전부 맡겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;그 때 그렇게 시작하지 말았어야했다...&lt;/i&gt;&lt;/b&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;서비스의 규모, 페이지 구성 및 설계를 제대로하지 않고 AI가 만들어준 결과물이 일단 보기에 괜찮은 것 같아서 그것을 기반으로 백엔드 개발자는 API를 만들었고 이후 프론트 개발도 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러던 중 AI가 제작한 처음의 구조와 달리 수정이 필요한 것들이 생겨났고, 이 때부터 눈덩이가 커지게되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 하면 할수록 구조가 복잡해지면서 코드가 꼬이는 현상도 종종 발생하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자와 사용자 페이지를 한 곳에서 다루려고 하다가 생각보다 관리자 페이지의 규모가 컸고, 유지보수 및 보안측면에서도 문제가 있어 관리자 페이지를 별도로 분리시켜야 한다고 판단이 되었고 팀원들과 논의 후 그렇게 하기로 결정되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 코드를 들여다보니 이전에 작성하던 코드에서 이를 일일이 분리하기가 매우 복잡해서 수정할 엄두가 나지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고심 끝에 전체적인 UI도 뜯어 고쳐야하고 구조도 변경해야하니 그냥 처음부터 다시 시작하는 편이 수정하는 것보다 수월할 것이라고 판단되어 기획부터 시작하여 구조설계를 완료한 뒤 개발하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 약 6개월간의 작업물이 역사속으로 사라지게 되었다.&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;&quot;일단 시작하고 보자&quot;&lt;/b&gt;해서 실패한 적이 없었고, 어떻게든 문제를 해결했었기 때문에 이렇게 태초마을로 돌아가게 된 것은 처음이다. 이번 경험을 통해 기획과 설계의 중요성을 절실히 느꼈고, 충분한 검토 없이 AI를 전적으로 의지해서는 안된다는 것도 배웠다.&lt;/p&gt;</description>
      <category>삽질기록</category>
      <category>기획</category>
      <category>삽질</category>
      <category>설계</category>
      <category>실패</category>
      <author>Atrix</author>
      <guid isPermaLink="true">https://atrix-developer.tistory.com/10</guid>
      <comments>https://atrix-developer.tistory.com/10#entry10comment</comments>
      <pubDate>Sat, 9 May 2026 22:01:25 +0900</pubDate>
    </item>
    <item>
      <title>[React] forwardRef</title>
      <link>https://atrix-developer.tistory.com/9</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 컴포넌트의 ref는 DOM 요소에만 직접 전달할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Functional Component는 ref를 사용할 수 없기 때문에 이 때 forwardRef를 사용하여 부모 컴포넌트에서 자식 컴포넌트의 DOM 요소를 참조할 수 있다.&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;pre id=&quot;code_1733882886803&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface InputProps extends React.InputHTMLAttributes&amp;lt;HTMLInputElement&amp;gt; {}

const FunctionalInputComponent = React.forwardRef((props: InputProps, ref: React.Ref&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
	return &amp;lt;input {...props} ref={ref} /&amp;gt;
})

export default function ParentComponent() {
	const ref = React.useRef&amp;lt;HTMLInputElement | null&amp;gt;(null);
    
    const handleInputFocus = () =&amp;gt; {
    	ref.current?.focus();
    };
    
    return (
    	&amp;lt;div&amp;gt;
        	&amp;lt;Input ref={ref} /&amp;gt;
            &amp;lt;button onClick={handleInputFocus}&amp;gt;Focus&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    )
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>React</category>
      <category>forwardRef</category>
      <category>react</category>
      <author>Atrix</author>
      <guid isPermaLink="true">https://atrix-developer.tistory.com/9</guid>
      <comments>https://atrix-developer.tistory.com/9#entry9comment</comments>
      <pubDate>Wed, 11 Dec 2024 11:13:29 +0900</pubDate>
    </item>
    <item>
      <title>[React] Flutter webview &amp;harr; React 데이터 통신</title>
      <link>https://atrix-developer.tistory.com/8</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;버전 정보&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;React : 18.3.1&lt;br /&gt;Flutter : 3.24.3&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;이번에 Flutter 앱과 React 웹과의 데이터 통신이 필요하게 되어 Flutter에 문외한인 나는 Chatgpt와 구글링을 열심히 했다.&lt;br /&gt;테스트용 Flutter 코드는 제공받았지만 Dart 문법을 아예 몰랐기 때문에 내가 사용할 부분 만큼은 이해할 필요가 있었다.&lt;br /&gt;이전에 React Native와 데이터 통신을 했던 경험이 있던지라 금방 처리될 것이라 생각했다. &lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;s&gt;(플래그 ON)&lt;/s&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성한 코드는 아래와 같았다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Flutter&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1729045614344&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// main.dart

controller
	..addJavaScriptChannel(
    	'FlutterWebView', // JavaScript에서 호출할 채널 이름
        onMessageReceived: (JavaScriptMessage message) {
        	print(message.message);
            _controller.runJavaScript(&quot;alert('appToWeb')&quot;)
        }
	)
 _controller = controller;&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;b&gt;React&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1729046452041&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useEffect } from 'react';
import './App.css';

function App() {
  useEffect(() =&amp;gt; {
    // @ts-ignore
    window.FlutterWebView.postMessage(&quot;웹에서 앱으로 보냄&quot;, &quot;*&quot;);
  }, []);

  return (
    &amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;
  );
}

export default App;&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;Flutter 쪽에서는 &quot;웹에서 앱으로 보냄&quot;이라는 데이터는 잘 확인이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 앱에서 웹뷰 브라우저로 전송한 데이터를 alert로 확인을 해보려고 했으나 기다리고 기다려도 아무런 동작이 일어나지 않았다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게든 데이터가 잘 넘어오는지 확인하리라 생각하고 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;구글링과 Chatgpt를 열심히 활용했지만 해결은 커녕 점점 미스테리가 되어가고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;a href=&quot;https://github.com/flutter/flutter/issues/30358#issuecomment-512763237&quot;&gt;https://github.com/flutter/flutter/issues/30358#issuecomment-512763237&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1729045811720&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;alert() method doesn't work in WebView &amp;middot; Issue #30358 &amp;middot; flutter/flutter&quot; data-og-description=&quot;I use the plugin webview_flutter to load a html page below: &amp;lt;!DOCTYPE html&amp;gt; &amp;lt;html&amp;gt; &amp;lt;head&amp;gt; &amp;lt;meta charset=&amp;quot;utf-8&amp;quot;&amp;gt; &amp;lt;meta http-equiv=&amp;quot;X-UA-Compatible&amp;quot; content=&amp;quot;IE=edge&amp;quot;&amp;gt; &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/flutter/flutter/issues/30358#issuecomment-512763237&quot; data-og-url=&quot;https://github.com/flutter/flutter/issues/30358&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/TWind/hyXhOVvCFR/cJfL2oTjCDc4BmGOtAyRS0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/dEIATh/hyXhU2ue55/2csAW1BHMupOdAbenwARZk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/flutter/flutter/issues/30358#issuecomment-512763237&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/flutter/flutter/issues/30358#issuecomment-512763237&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/TWind/hyXhOVvCFR/cJfL2oTjCDc4BmGOtAyRS0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/dEIATh/hyXhU2ue55/2csAW1BHMupOdAbenwARZk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;alert() method doesn't work in WebView &amp;middot; Issue #30358 &amp;middot; flutter/flutter&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I use the plugin webview_flutter to load a html page below: &amp;lt;!DOCTYPE html&amp;gt; &amp;lt;html&amp;gt; &amp;lt;head&amp;gt; &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt; &amp;lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot;&amp;gt; &amp;lt;meta name=&quot;viewport&quot; content=...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&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;키에에에에에엨... 애당초에 alert가 안되는거였다... &lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;s&gt;여긴 어디 나는 누구&lt;/s&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 에라 모르겠다 alert로 확인하려 하지 말고 직접 데이터를 뷰에 때려박아보자 하고 아래와 같이 코드를 수정했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Flutter&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1729045821536&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// main.dart

controller
	..addJavaScriptChannel(
    	'FlutterWebView', // JavaScript에서 호출할 채널 이름
        onMessageReceived: (JavaScriptMessage message) {
        	print(message.message);
            _controller.runJavaScript(&quot;window.appToWeb('appToWeb')&quot;)
        }
	)
  _controller = controller;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;React&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1729047269647&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useEffect, useState } from 'react';
import './App.css';

function App() {
  const [data, setData] = useState(null); // 토큰을 저장할 상태

  useEffect(() =&amp;gt; {
    // @ts-ignore
    window.FlutterWebView.postMessage(&quot;웹에서 앱으로 보냄&quot;, &quot;*&quot;);
    (window as any).appToWeb = function (appData: any) {
      setData(appData);
    };
  }, []);

  return (
    &amp;lt;div&amp;gt;
      {data ? (
        &amp;lt;div&amp;gt;
          &amp;lt;p&amp;gt;받은 데이터: {data}&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
      ) : (
        &amp;lt;p&amp;gt;토큰을 기다리는 중...&amp;lt;/p&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  );
}

export default App;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발함에 있어서 삽질이 없을 수는 없지만 이런 간단한 이슈로 인한 삽질은 크나큰 현타를 가지고 온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커피와 함께 리프레시를 해야겠다...&lt;/p&gt;</description>
      <category>React</category>
      <category>Flutter</category>
      <category>react</category>
      <author>Atrix</author>
      <guid isPermaLink="true">https://atrix-developer.tistory.com/8</guid>
      <comments>https://atrix-developer.tistory.com/8#entry8comment</comments>
      <pubDate>Wed, 16 Oct 2024 11:58:53 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js] ws 라이브러리를 이용한 WebSocket 구현</title>
      <link>https://atrix-developer.tistory.com/7</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;버전 정보&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote style=&quot;color: #000000; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;Next.js : 14.2.5&lt;br /&gt;ws : 8.18.0&lt;/blockquote&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;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;App Router&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;이번에는 채팅 서비스를 구현 해보려고 한다.&lt;br /&gt;&lt;a title=&quot;socket.io를 이용한 websocket 구현&quot; href=&quot;https://atrix-developer.tistory.com/4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전에 블로그에 기술했던&lt;/a&gt; socket.io가 아닌 ws로 websocket 통신을 구현해보았다.&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;pre id=&quot;code_1728622780503&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install ws bufferutil utf-8-validate&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※ 여기서 bufferutil을 설치하지 않으면 서버쪽에서 아래와 같은 오류가 발생한다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;TypeError: bufferUtil.unmask is not a function&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;아래는 서버 및 클라이언트 측 코드이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;서버 코드&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1728622152388&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// app/api/ws/route.ts

import { NextResponse } from 'next/server'
import { WebSocketServer, WebSocket } from 'ws'

interface Client {
    id: string;
    ws: WebSocket;
}

let wss: WebSocketServer | null = null
let clients: Client[] = []

export async function GET() {
    if (!wss) {
        console.log('WebSocket server is initializing')
        
        // 포트번호는 원하는 포트로 변경하여 사용
        wss = new WebSocketServer({ port: 3001 })

        wss.on('connection', (ws) =&amp;gt; {
            const clientId = Math.random().toString(36).substring(7) // 고유한 사용자의 ID 값을 임의로 생성, DB를 연동시킨다면 해당 부분을 수정
            clients.push({ id: clientId, ws })
            console.log(`New client connected: ${clientId}`)

            ws.send(JSON.stringify({ type: 'id', id: clientId }))

            ws.on('message', (message: string) =&amp;gt; {
                const data = JSON.parse(message);

				// 개인에게 메시지 전송
                if (data.type === 'private') {
                    const recipient = clients.find(client =&amp;gt; client.id === data.to)
                    if (recipient) {
                        recipient.ws.send(JSON.stringify({
                            type: 'private',
                            from: data.from,
                            message: data.message
                        }))
                    }
                    
                // 전역으로 메시지 전송
                } else if (data.type === 'broadcast') {
                    clients.forEach(client =&amp;gt; {
                        if (client.id !== data.from) {
                            client.ws.send(JSON.stringify({
                                type: 'broadcast',
                                from: data.from,
                                message: data.message
                            }))
                        }
                    })
                }
            })

            ws.on('close', () =&amp;gt; {
                clients = clients.filter(client =&amp;gt; client.ws !== ws)
                console.log(`Client disconnected: ${clientId}`)
            })
        })
    }

    return NextResponse.json({ message: 'WebSocket server is running' })
}

export const config = {
    api: {
        bodyParser: false,
    },
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;클라이언트 코드&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1728622103323&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// app/chat/page.tsx

'use client'

import { useState, useEffect, useRef } from 'react'

interface ChatMessage {
    text: string;
    isSent: boolean;
    from: string;
    to?: string;
}

export default function ChatPage() {
    const [message, setMessage] = useState('')
    const [messages, setMessages] = useState&amp;lt;ChatMessage[]&amp;gt;([])
    const [userId, setUserId] = useState&amp;lt;string | null&amp;gt;(null)
    const [recipientId, setRecipientId] = useState('')
    const socketRef = useRef&amp;lt;WebSocket | null&amp;gt;(null)

    useEffect(() =&amp;gt; {
        const connectWebSocket = async () =&amp;gt; {
            await fetch('/api/ws') // WebSocket 서버 초기화
            const ws = new WebSocket('ws://localhost:3001')
            
            ws.onopen = () =&amp;gt; {
                console.log('WebSocket connection established')
            }

            ws.onmessage = (event) =&amp;gt; {
                const data = JSON.parse(event.data)
                if (data.type === 'id') {
                    setUserId(data.id)
                } else if (data.type === 'private' || data.type === 'broadcast') {
                    setMessages((prevMessages) =&amp;gt; [...prevMessages, { 
                        text: data.message, 
                        isSent: false, 
                        from: data.from 
                    }])
                }
            }

            ws.onerror = (error) =&amp;gt; {
                console.error('WebSocket error:', error)
            }

            ws.onclose = () =&amp;gt; {
                console.log('WebSocket connection closed')
            }

            socketRef.current = ws
        }

        connectWebSocket()

        return () =&amp;gt; {
            if (socketRef.current) {
                socketRef.current.close()
            }
        }
    }, [])

    const sendMessage = (e: React.FormEvent) =&amp;gt; {
        e.preventDefault()
        if (message &amp;amp;&amp;amp; socketRef.current &amp;amp;&amp;amp; userId) {
            const messageData = recipientId 
                ? { type: 'private', from: userId, to: recipientId, message }
                : { type: 'broadcast', from: userId, message }
            
            socketRef.current.send(JSON.stringify(messageData))
            setMessages((prevMessages) =&amp;gt; [...prevMessages, { 
                text: message, 
                isSent: true, 
                from: userId,
                to: recipientId || 'all'
            }])
            setMessage('')
        }
    }

    return (
        &amp;lt;div className=&quot;flex flex-col h-screen bg-gray-100&quot;&amp;gt;
            &amp;lt;div className=&quot;bg-blue-500 text-white p-4 flex justify-between items-center&quot;&amp;gt;
                &amp;lt;h1 className=&quot;text-xl font-bold&quot;&amp;gt;Chat App&amp;lt;/h1&amp;gt;
                {userId &amp;amp;&amp;amp; (
                    &amp;lt;div className=&quot;bg-blue-600 px-3 py-1 rounded&quot;&amp;gt;
                        Your ID: &amp;lt;span className=&quot;font-bold&quot;&amp;gt;{userId}&amp;lt;/span&amp;gt;
                    &amp;lt;/div&amp;gt;
                )}
            &amp;lt;/div&amp;gt;
            &amp;lt;div className=&quot;flex-1 overflow-y-auto p-4&quot;&amp;gt;
                {messages.map((msg, index) =&amp;gt; (
                    &amp;lt;div 
                        key={index} 
                        className={`mb-2 p-2 rounded-lg ${
                        msg.isSent ? 'bg-blue-500 text-white self-end' : 'bg-white text-gray-800 self-start'
                        }`}
                    &amp;gt;
                        &amp;lt;div className=&quot;font-bold&quot;&amp;gt;{msg.isSent ? 'You' : msg.from}&amp;lt;/div&amp;gt;
                        &amp;lt;div&amp;gt;{msg.text}&amp;lt;/div&amp;gt;
                        {msg.to &amp;amp;&amp;amp; &amp;lt;div className=&quot;text-xs&quot;&amp;gt;{msg.isSent ? `To: ${msg.to}` : ''}&amp;lt;/div&amp;gt;}
                    &amp;lt;/div&amp;gt;
                ))}
            &amp;lt;/div&amp;gt;
            &amp;lt;form onSubmit={sendMessage} className=&quot;p-4 bg-white border-t&quot;&amp;gt;
                &amp;lt;div className=&quot;flex mb-2&quot;&amp;gt;
                    &amp;lt;input
                        type=&quot;text&quot;
                        value={recipientId}
                        onChange={(e) =&amp;gt; setRecipientId(e.target.value)}
                        className=&quot;flex-1 border rounded-l px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500&quot;
                        placeholder=&quot;Recipient ID (leave empty for broadcast)&quot;
                    /&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;div className=&quot;flex&quot;&amp;gt;
                    &amp;lt;input
                        type=&quot;text&quot;
                        value={message}
                        onChange={(e) =&amp;gt; setMessage(e.target.value)}
                        className=&quot;flex-1 border rounded-l px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500&quot;
                        placeholder=&quot;Type a message...&quot;
                    /&amp;gt;
                    &amp;lt;button
                        type=&quot;submit&quot;
                        className=&quot;bg-blue-500 text-white px-4 py-2 rounded-r hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500&quot;
                    &amp;gt;
                        Send
                    &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/form&amp;gt;
        &amp;lt;/div&amp;gt;
    )
}&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;실행 화면 (기기 1)&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-11 오후 2.04.59.png&quot; data-origin-width=&quot;358&quot; data-origin-height=&quot;603&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OJnOg/btsJ1EeN6HC/YKdG4r6cpqiuhf2IKvyQok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OJnOg/btsJ1EeN6HC/YKdG4r6cpqiuhf2IKvyQok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OJnOg/btsJ1EeN6HC/YKdG4r6cpqiuhf2IKvyQok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOJnOg%2FbtsJ1EeN6HC%2FYKdG4r6cpqiuhf2IKvyQok%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;358&quot; height=&quot;603&quot; data-filename=&quot;스크린샷 2024-10-11 오후 2.04.59.png&quot; data-origin-width=&quot;358&quot; data-origin-height=&quot;603&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;실행 화면 (기기 2)&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-11 오후 2.06.37.png&quot; data-origin-width=&quot;358&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceOilc/btsJ253gf2C/fwlNaj9dq1jW7mOkefBpUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceOilc/btsJ253gf2C/fwlNaj9dq1jW7mOkefBpUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceOilc/btsJ253gf2C/fwlNaj9dq1jW7mOkefBpUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceOilc%2FbtsJ253gf2C%2FfwlNaj9dq1jW7mOkefBpUK%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;358&quot; height=&quot;600&quot; data-filename=&quot;스크린샷 2024-10-11 오후 2.06.37.png&quot; data-origin-width=&quot;358&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Next.js</category>
      <category>Next.js</category>
      <category>websocket</category>
      <category>WS</category>
      <author>Atrix</author>
      <guid isPermaLink="true">https://atrix-developer.tistory.com/7</guid>
      <comments>https://atrix-developer.tistory.com/7#entry7comment</comments>
      <pubDate>Fri, 11 Oct 2024 14:07:43 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] Elastic Beanstalk, 그리고 Docker</title>
      <link>https://atrix-developer.tistory.com/6</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※ AWS Elastic Beanstalk와 Docker의 자세한 설명은 구글링하면 내가 설명하는 것보다 더 좋은 퀄리티의 가이드가 있으니 여기서는 설명은 생략하도록 하겠다.&lt;/b&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;내 기준에서는 Elastic Beanstalk을 생성할 때 Node.js 플랫폼과 Docker 플랫폼을 가장 많이 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중에서 가장 최근엔 Docker 플랫폼을 사용했는데, Elastic Beanstalk를 생성하면 아래 이미지와 같이 기본적으로 나타나는 페이지가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;817&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/envjK5/btszw5Ahzt3/kPqxVOpBQaLKt1VRLbHQY0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/envjK5/btszw5Ahzt3/kPqxVOpBQaLKt1VRLbHQY0/img.webp&quot; data-alt=&quot;이미지 출처 : https://k21academy.com/amazon-web-services/aws-devops/aws-elastic-beanstalk/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/envjK5/btszw5Ahzt3/kPqxVOpBQaLKt1VRLbHQY0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FenvjK5%2Fbtszw5Ahzt3%2FkPqxVOpBQaLKt1VRLbHQY0%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;1600&quot; height=&quot;817&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;817&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미지 출처 : https://k21academy.com/amazon-web-services/aws-devops/aws-elastic-beanstalk/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 Docker에 프로젝트를 배포하게되면 이 화면은 사라지게 되고 배포한 프로젝트가 출력된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 나는 이번에 일반적인 방법으로 프로젝트를 배포하지 않고 Elastic Beanstalk로 생성된 EC2 인스턴스에 직접 접근해서 &lt;b&gt;grafana&lt;/b&gt;를 사용해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;grafana를 docker에서 실행시키는 방법은 생각보다 간단하다. &lt;a title=&quot;grafana 공식 사이트&quot; href=&quot;https://grafana.com/grafana/download?platform=docker&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 사이트&lt;/a&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;Alpine 기반의 이미지를 사용하려면&lt;/p&gt;
&lt;pre id=&quot;code_1698799252839&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run -d --name=grafana -p 3000:3000 grafana/grafana-enterprise&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu 기반의 이미지를 사용하려면&lt;/p&gt;
&lt;pre id=&quot;code_1698799289683&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run -d --name=grafana -p 3000:3000 grafana/grafana-enterprise:10.2.0-ubuntu&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;grafana의 기본 포트는 3000이다.&lt;/p&gt;
&lt;pre id=&quot;code_1698799481607&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker ps -a&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어로 실행되고 있는 docker image를 확인해보면 grafana가 실행되고 있음을 확인할 수 있다. 그런데 grafana만 있는 것이 아니라 나는 만든 적이 없는 처음부터 존재하는 녀석이 하나 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히&lt;/p&gt;
&lt;pre id=&quot;code_1698799638430&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker stop 도커이미지이름
docker rm 도커이미지이름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 사용해서 Docker 이미지를 실행 중단시키고 삭제시키면 될 줄 알았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 계속해서 자동으로 재실행되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서너번 계속 시도해봤지만 결과는 그대로였고, 뭔가 강제로 삭제시키면 되지않을까?해서&lt;/p&gt;
&lt;pre id=&quot;code_1698801628739&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker rm -f 도커이미지이름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;이 후 Elastic Beanstalk에서 생성된 링크에 접속을 시도해보았으나 접속이 되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 될리가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http 프로토콜은 80포트를 사용하고 https는 443포트를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 위에서 Grafana를 실행할 때 뭐라고 선언했는지 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1699861652133&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;... -p 3000:3000 ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3000번 포트를 사용하고있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 http://url:3000으로 접속하면 동작하지만 어느 웹페이지도 이런식으로 포트번호를 url에서 사용하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Grafana를 아래와 같이 실행시키면 원하는대로 링크에 접속했을 때 포트번호를 기입하지 않아도 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1699861871705&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run -d --name=grafana -p 3000:80 grafana/grafana-enterprise&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매우 쉽게 말하자면 3000번 포트를 80포트로 연결 시켜주는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 사용자가 80포트로 진입하면 3000번 포트로 실행중인 녀석을 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을 포트포워딩이라고 하는데, 포트포워딩에 대한 설명은 &lt;a href=&quot;https://nordvpn.com/ko/blog/port-forwarding-vpn/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt;에서 확인하기 바란다.&lt;/p&gt;</description>
      <category>AWS</category>
      <category>aws</category>
      <category>docker</category>
      <category>elastic beanstalk</category>
      <category>Grafana</category>
      <author>Atrix</author>
      <guid isPermaLink="true">https://atrix-developer.tistory.com/6</guid>
      <comments>https://atrix-developer.tistory.com/6#entry6comment</comments>
      <pubDate>Mon, 13 Nov 2023 16:56:17 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js] 개발 환경에 의한 삽질기...</title>
      <link>https://atrix-developer.tistory.com/5</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개발하면서 현타가 와서 생각 정리겸 푸념하며 몇자 끄적여보고자 한다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그동안 Next.js로 개발을 하면서 로컬 환경과 배포 환경의 차이로 인해 수많은 오류를 발견하고 수정하느라 꽤나 삽질을 많이했고 스트레스도 어지간히 받았었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;먼저 배포 환경에서 Next.js가 실행되는 방법을 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 과정에서 npm run build 명령어를 통해 빌드를 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드가 완료되면 .next 폴더에 웹에서 실행될 javascript 파일들이 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 환경에서는 이 파일들을 실행시키는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 npm run start는 어떨까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 순서를 보면 배포 환경과 같은 과정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 빌드 후에 .next 폴더의 파일들을 실행시키는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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 data-ke-size=&quot;size16&quot;&gt;코드를 수정할 때마다 매번 새로 빌드할 필요없이 실시간으로 변경점이 반영되어 코드가 어떻게 동작하는지 바로 확인할 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 개발환경과 배포환경은 생각보다 많은 차이가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 경우로, 개발환경에는 없었던 오류가 배포환경에서 나타나는 경우가 매우 많다.&lt;/p&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;개발 진행 중에는 npm run dev(개발환경)&lt;/li&gt;
&lt;li&gt;체크포인트 느낌으로 배포환경의 동작을 확인하려면 npm run start(배포환경)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 진행하면 실제 배포를 했을 때도 오류를 최소화 할 수 있다.&lt;/p&gt;</description>
      <category>Next.js</category>
      <category>Next.js</category>
      <category>typescript</category>
      <author>Atrix</author>
      <guid isPermaLink="true">https://atrix-developer.tistory.com/5</guid>
      <comments>https://atrix-developer.tistory.com/5#entry5comment</comments>
      <pubDate>Thu, 20 Jul 2023 10:14:55 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js] node.js와 socket.io를 이용한 websocket</title>
      <link>https://atrix-developer.tistory.com/4</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;클라이언트 버전 정보&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Next.js : 13.4.9&lt;br /&gt;socket.io : 4.7.1&lt;br /&gt;socket.io-client: 4.7.1&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;서버 버전 정보&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;node.js : 18.12.0&lt;br /&gt;express : 4.18.2&lt;br /&gt;socket.io : 4.7.1&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;Next.js로 여러 웹 프로젝트를 개발하던 중 이번엔 websocket을 사용해보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 websocket이 무엇인지부터 알고 넘어가는 것이 중요하다.&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;/b&gt;이며 필요시에 데이터를 송/수신 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그에 비해 websocket은 서버와 클라이언트간에 실시간으로 &lt;b&gt;양방향 통신&lt;/b&gt;을 하게 해주는 녀석이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;socket 통신을 위해 어떤 라이브러리를 사용할지 찾아본 결과 가장 많이 사용되는 &lt;a href=&quot;https://socket.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;socket.io&lt;/a&gt;를 사용하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Next.js의 API Route를 사용하여 API를 작성하려고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 app/api와 같은 경로를 사용할 경우 에러가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 대해 검색을 해보니 Next.js 버전이 13.2.5 이후 버전인 경우에 동작을 하지 않는다는 &lt;a href=&quot;https://stackoverflow.com/a/76255701/18767822&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;stackoverflow의 답변&lt;/a&gt;이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 사용을 하려면 pages/api 경로를 사용해야 한다는 &lt;a href=&quot;https://github.com/vercel/next.js/issues/49334#issuecomment-1608823844&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github의 답변&lt;/a&gt;도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자는 이 app/api 구조를 바꾸고 싶지 않았기도 했고, 궁극적으로 API동작을 분리시켜서 관리하는 것이 더 바람직한 방향이라고 판단하여 &lt;b&gt;node.js&lt;/b&gt;와 &lt;b&gt;express&lt;/b&gt;를 사용하여 API를 구현하기로 했다.&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 data-ke-size=&quot;size16&quot;&gt;새로운 폴더를 생성하고 &lt;b&gt;npm init&lt;/b&gt; 명령어를 입력해 초기셋팅을 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1689135670147&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm init&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;/p&gt;
&lt;pre id=&quot;code_1689135739895&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i express socket.io&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;/p&gt;
&lt;pre id=&quot;code_1689135941829&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const express = require('express');
const app = express();
const server = require('http').createServer(app);
const socketIO = require('socket.io')(server);

const port = process.env.PORT || 5000;

server.listen(port, () =&amp;gt; {
    console.log('Socket IO server listening on port' + port);
})

// socket이 연결되었을 때
socketIO.on('connection', (socket) =&amp;gt; {

	// success라는 이벤트가 발생했을 때
    socketIO.emit('useSuccess', '정상적으로 처리되었습니다.');
});&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;클라이언트측에서 socket.io와 통신하기 위해 socket.io-client 라이브러리를 설치했다.&lt;/p&gt;
&lt;pre id=&quot;code_1689136265154&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i socket.io-client&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 기능 구현만 하기 위해 만든 Next.js의 코드는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1689136822651&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client'

import { useToast } from &quot;@chakra-ui/react&quot;;
import { useEffect } from &quot;react&quot;;
import SocketIOClient from &quot;socket.io-client&quot;;

export default function Home() {
  const toast = useToast();

  useEffect(() =&amp;gt; {
    const socket = SocketIOClient(&quot;http://localhost:5000&quot;);

    socket.on(&quot;connect&quot;, () =&amp;gt; {
      socket.on('useSuccess', () =&amp;gt; {
      	// chakra-ui의 Toast component
        toast({
          title: 'succeed!',
          status: 'success',
          duration: 3000
        });
      })
    });
  }, []);
  
  return (
    &amp;lt;main&amp;gt;&amp;lt;/main&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;그런데 위 Next.js 코드를 실행시켜보면 브라우저의 콘솔 창에서 &lt;b&gt;CORS&lt;/b&gt; 에러가 발생했음을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 따라 서버측 코드에 &lt;b&gt;cors&lt;/b&gt;라이브러리를 설치하여 추가해주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1689137587998&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i cors&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;/p&gt;
&lt;pre id=&quot;code_1689137771641&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const cors = require('cors');
...
const socketIO = require('socket.io')(server, { cors: { origin: &quot;*&quot; } });

app.use(cors())
...&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;이제 실행해보면 문제 없이 websocket이 잘 동작하는 것을 확인할 수 있다.&lt;/p&gt;</description>
      <category>Next.js</category>
      <category>Express</category>
      <category>Next.js</category>
      <category>node.js</category>
      <category>socket.io</category>
      <category>websocket</category>
      <author>Atrix</author>
      <guid isPermaLink="true">https://atrix-developer.tistory.com/4</guid>
      <comments>https://atrix-developer.tistory.com/4#entry4comment</comments>
      <pubDate>Wed, 12 Jul 2023 13:58:31 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js] 내가 겪은 next-auth Error들</title>
      <link>https://atrix-developer.tistory.com/3</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고 문서&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a href=&quot;https://next-auth.js.org/getting-started/example&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;next-auth 공식 문서&lt;/a&gt;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;삽질의 시작...&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 웹페이지에 로그인 기능을 도입하게 되었다. 단, 서비스 자체 가입 및 로그인은 만들지 않기로 했다. 카카오 로그인 등의 간편 로그인만 사용하기로 했다.&lt;br /&gt;하지만 각각의 간편 로그인 서비스들을 일일이 구현하기에는 많은 시간 소모와 삽질이 예고 되어있음을 감지했다.&lt;br /&gt;그래서 어지간한 간편 로그인 서비스들이 모여있고 코딩하기에 간편한 'next-auth' 라이브러리를 사용하기로 했다.&lt;br /&gt;그 중 GoogleProvider(구글 계정으로 로그인), KakaoProvider(카카오 계정으로 로그인)를 추가할 계획이다.&lt;br /&gt;그런 뒤에, 이렇게 저렇게 해서 아기다리 고기다리 던 로그인 기능 구현이 완료되었다.&lt;br /&gt;이제 배포해야지ㅎ 하고 기능 구현이 생각보다 금방 끝나 신나하던 나는 앞으로 마주할 오류와 삽질을 예상하지 못했다.&lt;br /&gt;신나게 GIthub에 push를 했고 Github Actions로 Google Cloud Run에 배포되기를 기다렸고 4분 뒤 배포가 성공적으로 완료되었다는 Github의 메시지에 곧바로 도메인에 접속하여 로그인을 하던 중... 보지 말아야 할, 일어나지 말아야 할 오류가 발생했다...&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫 번째 오류와의 조우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 &lt;b&gt;OAuthCallback&lt;/b&gt;라는 녀석을 처음 마주하게 되었다.&lt;br /&gt;브라우저의 console 창에서는 &lt;b&gt;OAuthCallback&lt;/b&gt;이 돈을 주고 매수를 했는지 아무런 오류 메시지도 출력되지 않아 오류가 없는 것 같이 보인다. 하지만 '해치웠나?'&lt;/p&gt;
&lt;p&gt;&lt;del&gt;(플래그 ON)&lt;/del&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 '그'발언을 함과 동시에 URL 부분을 쳐다보는 순간 내가 설정하지 않은 error라는 문구와 함께 &lt;b&gt;OAuthCallback&lt;/b&gt;녀석이 허락도 없이 내 도메인과 사이좋게 붙어있는 모습을 볼 수 있었다.&lt;br /&gt;나는 당장 둘을 떨어뜨려놓기 위해 Sentry, Cloud Run의 로그를 필사적으로 살펴보았다.&lt;br /&gt;&lt;b&gt;OAuthCallback&lt;/b&gt;에러라는 녀석은 무엇인지, 왜 나타난 것인지 부터해서 이 사이트 저 사이트 다 찾아보고 요즘 핫하다는 요술램프의 지니같은 &lt;b&gt;ChatGPT&lt;/b&gt;한테도 물어봤지만 별다른 소득은 없었다.&lt;br /&gt;Sentry에서도 별다른 로그를 찾지 못했고 Cloud Run에서 아래와 같은 로그를 찾을 수 있었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;error: TypeError: checks.state argument is missing&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인간은 망각의 동물이라고, 앞전에 '해치웠나'를 경험했음에도 '이 에러 메시지에 대한 오류만 처리하면 되겠다!'라는 확신을 가지게 되었고 그 플래그는 발작버튼이 눌린 것 마냥 어김없이 발동되었다.&lt;br /&gt;브라우저 쿠키를 살펴보니 state값도 멀쩡히 살아있었다.&lt;br /&gt;그리하여 삽질의 신호탄이 격발되었고 아래와 같은 삽질들을 모두 진행해보았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NEXTAUTH_URL 변수의 문제일까 싶어 수정&lt;/li&gt;
&lt;li&gt;NEXTAUTH_SECRET 변수의 문제일까 싶어 지우기도 해보고 살려두기도 해봄&lt;/li&gt;
&lt;li&gt;next-auth의 'signIn'에서 'callbackUrl'이 문제일까 싶어 수정&lt;/li&gt;
&lt;li&gt;Google Cloud에서 프로젝트의 'API 및 서비스' 탭에서 사용자 인증 정보의 'OAuth2.0 클라이언트 ID'의 '승인된 자바스크립트 원본', '승인된 리디렉션 URI'문제일까 싶어 수정&lt;/li&gt;
&lt;li&gt;'킹'택오버플로 'stackoverflow'에 질문&lt;del&gt;(도 해봤지만 답글은 아직까지도 달리지 않고 있다... 조회 수는 올라가는데 읽씹 당하는 중)&lt;/del&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 문제는 해결되지 않았다.&lt;br /&gt;이 현상이 특이한 점은 Cloud Run에 배포했을 때 여기서 나온 서비스 URL에서는 아무런 문제 없이 잘 동작하는데 Firebase Hosting을 통해 배포된 도메인에서는 동작하지 않는다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Firebase Hosting 과정에서의 문제라고 결론짓고 배포 환경을 변경하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Cloud Run + Firebase Hosting &amp;rarr; AWS beanstalk + nginx (편ㅡ안한 새로운 환경을 구축해주신 팀장님께 압도적 감사)&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두 번째 오류와의 조우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 배포된 환경에서도 어김없이 오류를 맛보게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;del&gt;(오류감별사 오믈리에로 전직 중)&lt;/del&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 &lt;b&gt;OAuthSignIn&lt;/b&gt;이라는 녀석과 만났다.&lt;br /&gt;내가 할 수 있는 삽질은 정해져있으니 가장 먼저 GOOGLE_CLIENT_ID 값과 GOOGLE_CLIENT_SECRET 값을 수정해보았다.&lt;br /&gt;근데 왠걸? 이 녀석은 &lt;b&gt;OAuthCallback&lt;/b&gt;과 다르게 끈기가 없었다.&lt;br /&gt;단순히 오탈자 문제였다... 이것만 바꿔주니 드디어 도메인에서도 로그인 기능이 정상동작!!!&lt;/p&gt;
&lt;p&gt;&lt;del&gt;(감동의 눈물을 흘릴 뻔... 하지만 난 눈물을 자주 흘리진 않지)&lt;/del&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로써 next-auth의 삽질기가 마무리 되었다. 새로운 영역에 도전하는 것은 흥미롭지만 새로운 오류는 언제나 나에게 흰머리를 가져다주더라...&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내가봐도 글이 길다 요약본은 아래에...&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;OAuthCallback&lt;/b&gt;에러는 Firebase Hosting과정에서 문제가 생기는 것으로 결론지었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OAuthSignIn&lt;/b&gt;에러는 단순히 API Key값의 오탈자였다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Next.js</category>
      <category>Errors</category>
      <category>next-auth</category>
      <category>Next.js</category>
      <category>OAuthCallback</category>
      <category>OAuthSIgnIn</category>
      <author>Atrix</author>
      <guid isPermaLink="true">https://atrix-developer.tistory.com/3</guid>
      <comments>https://atrix-developer.tistory.com/3#entry3comment</comments>
      <pubDate>Fri, 10 Feb 2023 16:09:21 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js] Sentry 에러 net::ERR_BLOCKED_BY_CLIENT</title>
      <link>https://atrix-developer.tistory.com/2</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js 프로젝트에서 발생한 에러를 추적하기 위해 &lt;a title=&quot;Sentry&quot; href=&quot;https://sentry.io&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Sentry&lt;/a&gt; 를 도입했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;local 환경에서 동작 확인을 위해 npm run dev를 실행하여 localhost:3000으로 접속해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(브라우저는 Chrome을 사용했다)&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-filename=&quot;sentry block error.png&quot; data-origin-width=&quot;464&quot; data-origin-height=&quot;63&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QIfca/btrX7Zh2hx9/Ibv9PDZgv9PMI826Tc3lFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QIfca/btrX7Zh2hx9/Ibv9PDZgv9PMI826Tc3lFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QIfca/btrX7Zh2hx9/Ibv9PDZgv9PMI826Tc3lFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQIfca%2FbtrX7Zh2hx9%2FIbv9PDZgv9PMI826Tc3lFK%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;464&quot; height=&quot;63&quot; data-filename=&quot;sentry block error.png&quot; data-origin-width=&quot;464&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;필자의 환경 기준에서 이 오류가 발생하는 원인은 브라우저의 광고 차단 확장프로그램 때문이었다.&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 data-ke-size=&quot;size16&quot;&gt;때문에 브라우저에서 광고를 차단 시켜주는 플러그인을 사용하는 사람들이 많아졌다.&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 기능이 Sentry에서 오류를 추적하는 것까지 차단을 시켜버린다.&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-filename=&quot;adblock.png&quot; data-origin-width=&quot;248&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cV7IPo/btrX4kG7oG4/7UPOKkV7wRuUzyBHxzvxc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cV7IPo/btrX4kG7oG4/7UPOKkV7wRuUzyBHxzvxc0/img.png&quot; data-alt=&quot;전원 버튼 모양의 아이콘을 누르면 광고 차단 플러그인이 꺼진다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cV7IPo/btrX4kG7oG4/7UPOKkV7wRuUzyBHxzvxc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcV7IPo%2FbtrX4kG7oG4%2F7UPOKkV7wRuUzyBHxzvxc0%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;248&quot; height=&quot;380&quot; data-filename=&quot;adblock.png&quot; data-origin-width=&quot;248&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;전원 버튼 모양의 아이콘을 누르면 광고 차단 플러그인이 꺼진다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Next.js</category>
      <category>Next.js</category>
      <category>Sentry</category>
      <author>Atrix</author>
      <guid isPermaLink="true">https://atrix-developer.tistory.com/2</guid>
      <comments>https://atrix-developer.tistory.com/2#entry2comment</comments>
      <pubDate>Sun, 5 Feb 2023 20:15:30 +0900</pubDate>
    </item>
  </channel>
</rss>