<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>코로 넘어져도 헤딩만 하면 그만</title>
    <link>https://hj97codeart.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 3 Jul 2026 14:41:42 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>꼬드리</managingEditor>
    <image>
      <title>코로 넘어져도 헤딩만 하면 그만</title>
      <url>https://tistory1.daumcdn.net/tistory/5693664/attach/c21569a3420a4238bb45ac2cbdeb4742</url>
      <link>https://hj97codeart.tistory.com</link>
    </image>
    <item>
      <title>KSTQB-AI 신청부터 합격까지 후기</title>
      <link>https://hj97codeart.tistory.com/196</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;업무 분야를 깊이 파다 보니 &lt;b&gt;AI를 활용한 QA&lt;/b&gt;에도 자연스럽게 관심이 생겼다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;특히 재직 중인 회사에서 &lt;u&gt;테스트 문서를 작성하는 과정에 생성형 AI를 자주 활용&lt;/u&gt;하는데, 이에 대한 자격증이 있어서 따로 지원하게 되었다. 정확히는 kstqb에서 진행하는 관련 수업을 신청했고, 마지막에 현장에서 관련 시험을 보기로 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;609&quot; data-origin-height=&quot;433&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byENXd/btsQiVhDsjh/km3lZZ82kKznHsuEk5ie60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byENXd/btsQiVhDsjh/km3lZZ82kKznHsuEk5ie60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byENXd/btsQiVhDsjh/km3lZZ82kKznHsuEk5ie60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyENXd%2FbtsQiVhDsjh%2Fkm3lZZ82kKznHsuEk5ie60%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;406&quot; height=&quot;289&quot; data-origin-width=&quot;609&quot; data-origin-height=&quot;433&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;figure id=&quot;og_1756963772253&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;KSTQB&quot; data-og-description=&quot;About KSTQB KSTQB 주관 자격 시험 자격증 활용 현황 시험 안내 및 일정 학습 자료실 News &amp;amp; Events 특징 및 현황 KSTQB SW 테스팅 자격시험 ISTQB SW 테스팅 자격 시험 IREB CPRE 요구공학 TMMi Professional&quot; data-og-host=&quot;www.kstqb.org&quot; data-og-source-url=&quot;https://www.kstqb.org/sw/sw2_4.asp&quot; data-og-url=&quot;https://www.kstqb.org/sw/sw2_4.asp&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.kstqb.org/sw/sw2_4.asp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.kstqb.org/sw/sw2_4.asp&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;KSTQB&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;About KSTQB KSTQB 주관 자격 시험 자격증 활용 현황 시험 안내 및 일정 학습 자료실 News &amp;amp; Events 특징 및 현황 KSTQB SW 테스팅 자격시험 ISTQB SW 테스팅 자격 시험 IREB CPRE 요구공학 TMMi Professional&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.kstqb.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;국내에서 발급하는 QA 자격증에 KSTQB가 있는데, 해당 업체에서 AI 분야의 시험도 신설하여 진행하고 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;위 사이트에서 마침 열린 Gen-AI (생성형 AI) 활용 테스팅 교육이라는 수업을 신청하여 나름 알차게 준비하였다. 살펴보니 다른 QA 자격증보다 테스트에 대해서는 가볍게 다루고, 생성형 AI 쪽을 더 깊게 파고드는 느낌이 강하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;사실 수업 내에서도 전 범위를 하나씩 짚으며 예시와 함께 보여주긴 하지만, 시험에는 실제 적용에 대한 프롬프트 응용이 들어가기 때문에... 배우자마자 개념이 잡히지 않은 뇌로 &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;바로 시험 치는 건 무리라고 생각해서 &lt;b&gt;일주일&lt;/b&gt; 정도 사전에 투자해서 공부한 것 같다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 문제유형 : 객관식&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문항수 : 40문항&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시험시간 : 한글 60분&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;합격기준 : 백분율 65% 이상(65점 이상 합격) &lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;위에 첨부한 공식 사이트를 보면, 실라버스에서 시험 교재로 쓰이는 50p 정도의 내용과 예시 문제들을 볼 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;생소한 단어들을 키워드 중심으로 익히고 이후 문제를 풀어보면서 머릿속으로 실제 테스트를 하면 어떻게 응용할 지 생각해보는 게 많은 도움이 되었다. 예시가 40문제로 많지 않아서 100문제 씩 문제 은행 돌리던 나는 약간 당황하였다(...)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;그래서 더욱 개념을 정확하게 구분하는 게 중요하며, 어떤 경우에 어떤 테스트 방법을 쓸 지 생각하면서 풀어야 한다.&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;예시를 주고 이에 배운 기법을 적용해야 하는 긴 텍스트의 문제들이 15문항 정도 있었던 것 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그나마 실기는 아니고 모두 객관식이었다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;시험 시간은 1시간으로, 길다면 길고 짧다면 짧다. 끝나기 10분 전 3번 정도 검토하고 나왔다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;수능처럼 답지에 마킹해서 답지랑 문제지를 함께 내야 하는데, 내 경우 컴싸가 아니어도 볼펜으로 체크해도 된다고 했다. 총 40문제, 65점 이상 합격이지만 의외로 헷갈리는 문제가 꽤 되어서... 누가 봐도 공부만 했다면 답이 확실한 개념 문제는 15개 정도? 그 뒤에는 다 응용이었다.&amp;nbsp;보니까 사전에 외운 샘플 문제들과 비슷하거나 약간 어렵게 구성되어 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;샘플 정도는 수월하게 풀 수 있어야 안정적인 점수를 받을 것 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;시행된 지 얼마 되지 않아 그나마 쉽게 내는 것 같은데 시험이 그렇듯이 점점 난이도가 올라가지 않을 지 예상해본다. ...&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;헷갈리는 문제들이 있어서 난감했는데 열흘 뒤 결과를 보니 무난하게 패스.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;한번 따고 나면 평생 갖고 갈 수 있다고 한다. 사실 자격증 소유 여부보다는 현장에서 프롬프트 적용법을 배우는 게 유익했다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-04 144440.png&quot; data-origin-width=&quot;280&quot; data-origin-height=&quot;411&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dfRNoA/btsQlXre30E/wjSQXkx7B6hurYDdxCKqm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dfRNoA/btsQlXre30E/wjSQXkx7B6hurYDdxCKqm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dfRNoA/btsQlXre30E/wjSQXkx7B6hurYDdxCKqm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdfRNoA%2FbtsQlXre30E%2FwjSQXkx7B6hurYDdxCKqm0%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;183&quot; height=&quot;269&quot; data-filename=&quot;스크린샷 2025-09-04 144440.png&quot; data-origin-width=&quot;280&quot; data-origin-height=&quot;411&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>자격증</category>
      <author>꼬드리</author>
      <guid isPermaLink="true">https://hj97codeart.tistory.com/196</guid>
      <comments>https://hj97codeart.tistory.com/196#entry196comment</comments>
      <pubDate>Thu, 4 Sep 2025 14:56:03 +0900</pubDate>
    </item>
    <item>
      <title>VS code 확장 제작(탭 및 하이라이트 편집기)</title>
      <link>https://hj97codeart.tistory.com/195</link>
      <description>&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;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;601&quot; data-origin-height=&quot;159&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QpzqV/btsN1ML3sTn/nFSDB3KC8F7BHLa0jO3Z4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QpzqV/btsN1ML3sTn/nFSDB3KC8F7BHLa0jO3Z4k/img.png&quot; data-alt=&quot;예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QpzqV/btsN1ML3sTn/nFSDB3KC8F7BHLa0jO3Z4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQpzqV%2FbtsN1ML3sTn%2FnFSDB3KC8F7BHLa0jO3Z4k%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;601&quot; height=&quot;159&quot; data-origin-width=&quot;601&quot; data-origin-height=&quot;159&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시&lt;/figcaption&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;일단 메모장보다는 VS code가 낫다. 스페이스는 점으로, 탭은 화살표로 구분된다. 그런데... 너무 작아서 잘 보이지 않고 모니터 속 세상으로 머리를 집어 넣는 꼴이 되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 확장 프로그램 만들기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참다 참다 미쳐버리기 전에 확장 프로그램을 하나 빨리 만들어서 적용하기로 했다. 사실 확장 프로그램은 받아서 써보기만 했지, 직접 만든 적은 없다. 그런데 뭐 ... 어렵겠나 싶기도 하고... 어차피 간단한(그러나 지금 내겐 꼭 필요한) 기능 코드인데...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에디터 필수 기능은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 공백의 탭과 스페이스를 직관적으로 구분할 것&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 줄 마지막에 텍스트 대신 공백이 있다면 알려줄 것&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) 현재 선택해서 보고 있는 줄을 하이라이트 해서 알아보기 쉽게 할 것&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;b&gt;- 확장 프로그램 세팅&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VS code에서 확장 프로그램을 만드는 건 어렵지 않다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1747381611120&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install -g yo generator-code&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;Node.js에서 yo를 사용해서 확장 파일을 생성한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1747381627721&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;yo code&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;그러면 자동으로 확장을 만들 수 있는 편집기가 열린다. extension.ts에서 자신이 확장 프로그램에 넣길 원하는 기능을 코드로 짜서 개발하면 된다. 이후 F5를 눌러 개발 환경을 띄우면 적용이 잘 되는지 볼 수 있다.&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;- 원하는 기능 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탭의 경우 보라색, 스페이스는 파란색으로 배경이 구분되도록 설정하였다. 사실 점과 화살표도 보기 좋게 키우고 싶었는데, 이건 VS code&amp;nbsp; 에서 지원하지 않는다고 하더라... before나 after같은 가상 선택자로도 넣어봤지만 결론적으론 색만 구분하기로 했다.&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;pre id=&quot;code_1747381829793&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  const tabDecoration = vscode.window.createTextEditorDecorationType({
    backgroundColor: 'rgba(103, 58, 183, 0.8)', // 보라색
    borderRadius: '1px'
  });

  const spaceDecoration = vscode.window.createTextEditorDecorationType({
    backgroundColor: 'rgba(144, 202, 249, 0.4)', // 파란색
    borderRadius: '1px'
  });

  const trailingWhitespaceDecoration = vscode.window.createTextEditorDecorationType({
    backgroundColor: 'rgba(255, 138, 128, 0.4)', // 빨간색
    borderRadius: '1px'
  });

  const highlightDecoration = vscode.window.createTextEditorDecorationType({
    isWholeLine: true,
    backgroundColor: 'rgba(255, 215, 0, 0.3)', // 노란색
  });

  let whitespaceHighlightEnabled = true;
  let selectionChangeDisposable: vscode.Disposable | undefined;

  const updateHighlight = (editor: vscode.TextEditor | undefined) =&amp;gt; {
    if (!editor || !whitespaceHighlightEnabled) {return;}

    const selections = editor.selections;

    const ranges = selections.map(selection =&amp;gt; {
      const start = selection.start.line;
      const end = selection.end.line;
      const rangeList: vscode.Range[] = [];

      for (let i = start; i &amp;lt;= end; i++) {
        rangeList.push(new vscode.Range(i, 0, i, 0));
      }

      return rangeList;
    }).flat();

    editor.setDecorations(highlightDecoration, ranges);
  };

  const updateWhitespaceHighlights = (editor: vscode.TextEditor | undefined) =&amp;gt; {
    if (!editor || !whitespaceHighlightEnabled) {return;}

    const doc = editor.document;
    const tabRanges: vscode.DecorationOptions[] = [];
    const spaceRanges: vscode.DecorationOptions[] = [];
    const trailingRanges: vscode.DecorationOptions[] = [];

    for (let lineNum = 0; lineNum &amp;lt; doc.lineCount; lineNum++) {
      const line = doc.lineAt(lineNum);
      const text = line.text;

      for (let i = 0; i &amp;lt; text.length; i++) {
        const char = text[i];

        if (char === '\t') {
          tabRanges.push({ range: new vscode.Range(lineNum, i, lineNum, i + 1) });
        }

        if (char === ' ') {
          spaceRanges.push({ range: new vscode.Range(lineNum, i, lineNum, i + 1) });
        }
      }

      const match = text.match(/[\t ]+$/);
      if (match) {
        const start = new vscode.Position(lineNum, text.length - match[0].length);
        const end = new vscode.Position(lineNum, text.length);
        trailingRanges.push({ range: new vscode.Range(start, end) });
      }
    }

    editor.setDecorations(tabDecoration, tabRanges);
    editor.setDecorations(spaceDecoration, spaceRanges);
    editor.setDecorations(trailingWhitespaceDecoration, trailingRanges);
  };

  const editor = vscode.window.activeTextEditor;
  if (editor) {
    updateHighlight(editor);
    updateWhitespaceHighlights(editor);
  }

  // 초기 리스너 등록 (줄 선택 하이라이트)
  selectionChangeDisposable = vscode.window.onDidChangeTextEditorSelection(e =&amp;gt; {
    updateHighlight(e.textEditor);
  });
  context.subscriptions.push(selectionChangeDisposable);

  context.subscriptions.push(
    vscode.workspace.onDidChangeTextDocument(e =&amp;gt; {
      if (vscode.window.activeTextEditor?.document === e.document) {
        updateWhitespaceHighlights(vscode.window.activeTextEditor);
      }
    }),
    vscode.window.onDidChangeActiveTextEditor(editor =&amp;gt; {
      updateHighlight(editor);
      updateWhitespaceHighlights(editor);
    }),
    highlightDecoration,
    tabDecoration,
    spaceDecoration,
    trailingWhitespaceDecoration
  );

  // 줄 끝 공백 제거 커맨드
  const trimCommand = vscode.commands.registerCommand(
    'extension.trimTrailingWhitespace',
    () =&amp;gt; {
      const editor = vscode.window.activeTextEditor;
      if (!editor) {return;}

      const doc = editor.document;
      editor.edit(editBuilder =&amp;gt; {
        for (let lineNum = 0; lineNum &amp;lt; doc.lineCount; lineNum++) {
          const line = doc.lineAt(lineNum);
          const trimmed = line.text.replace(/[\t ]+$/, '');
          if (trimmed.length &amp;lt; line.text.length) {
            const range = new vscode.Range(
              new vscode.Position(lineNum, 0),
              new vscode.Position(lineNum, line.text.length)
            );
            editBuilder.replace(range, trimmed);
          }
        }
      });
    }
  );

  // 토글 커맨드
  const toggleCommand = vscode.commands.registerCommand(
    'extension.toggleWhitespaceHighlight',
    () =&amp;gt; {
      whitespaceHighlightEnabled = !whitespaceHighlightEnabled;
      const editor = vscode.window.activeTextEditor;
      if (!editor) {return;}

      if (!whitespaceHighlightEnabled) {
        editor.setDecorations(tabDecoration, []);
        editor.setDecorations(spaceDecoration, []);
        editor.setDecorations(trailingWhitespaceDecoration, []);
        editor.setDecorations(highlightDecoration, []);
        selectionChangeDisposable?.dispose(); // 리스너 제거
        selectionChangeDisposable = undefined;
      } else {
        updateWhitespaceHighlights(editor);
        updateHighlight(editor);
        selectionChangeDisposable = vscode.window.onDidChangeTextEditorSelection(e =&amp;gt; {
          updateHighlight(e.textEditor);
        });
        context.subscriptions.push(selectionChangeDisposable);
      }
    }
  );

  context.subscriptions.push(trimCommand, toggleCommand);
}

export function deactivate() {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할 점...npm tsc로 직접 타입 스크립트 파일 변환 해준 뒤 적용이 되긴 했다.(이거 때문에 삽질 좀 함)&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;원하는 기능 관련 명령어는 package.json에&lt;b&gt; command&lt;/b&gt;로 등록한다. publisher(배포자)도 등록하면 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포자와 LICENCE.txt를 등록하지 않았더니 파일로 내보낼 때 에러가 떴다.&lt;/p&gt;
&lt;pre id=&quot;code_1747382179813&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  &quot;contributes&quot;: {
	&quot;commands&quot;: [
    {
      &quot;command&quot;: &quot;extension.trimTrailingWhitespace&quot;,
      &quot;title&quot;: &quot;Trim Trailing Whitespace&quot;
    },
    {
      &quot;command&quot;: &quot;extension.toggleWhitespaceHighlight&quot;,
      &quot;title&quot;: &quot;Toggle Whitespace Highlight&quot;
    }
  ]}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능이 다 완성되면 f5를 눌러 실제로 잘 되는지 테스트를 해본다.&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;완성된 확장을 Extension 등록하려면 개발자 계정 인증도 해야 되고 귀찮아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 빠르게 파일로 팀원들에게만 배포하기로 했다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1747382433422&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install -g vsce&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1747382452378&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;vsce package&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에 해당 명령어로 vsix 파일을 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일을 다른 사람에게 공유하고 그쪽에서 VS code 확장 프로그램으로 받아서 설치해주면 적용 완료.&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;699&quot; data-origin-height=&quot;389&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjva5e/btsNZYHAzlT/s0seYWKK1KZuaee3UqkIj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjva5e/btsNZYHAzlT/s0seYWKK1KZuaee3UqkIj1/img.png&quot; data-alt=&quot;여기서 vsix 파일을 선택해서 적용 가능하다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjva5e/btsNZYHAzlT/s0seYWKK1KZuaee3UqkIj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcjva5e%2FbtsNZYHAzlT%2Fs0seYWKK1KZuaee3UqkIj1%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;625&quot; height=&quot;348&quot; data-origin-width=&quot;699&quot; data-origin-height=&quot;389&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;여기서 vsix 파일을 선택해서 적용 가능하다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 확장에 등록을 했는데 기능 수정한 버전으로 재배포해야 한다면,&amp;nbsp; vsix로 만들기 전에 package.json에서 version을 높인 뒤 파일로 만든다. 버전이 바뀌지 않으면 VS code는 동일 프로그램으로 인지하여 수정본이 덮어 씌워지지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굳이 삭제하지 않아도 버전만 높아지면 알아서 기존 버전을 삭제하고 재설치 해준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 결과물&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;577&quot; data-origin-height=&quot;89&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A0KA0/btsN1Owr1bU/jk2cDsZDXEykyW9bT0nV1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A0KA0/btsN1Owr1bU/jk2cDsZDXEykyW9bT0nV1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A0KA0/btsN1Owr1bU/jk2cDsZDXEykyW9bT0nV1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA0KA0%2FbtsN1Owr1bU%2Fjk2cDsZDXEykyW9bT0nV1K%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;577&quot; height=&quot;89&quot; data-origin-width=&quot;577&quot; data-origin-height=&quot;89&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 두 번째 줄을 클릭해서 전체 줄이 노란 색으로 색칠되었다. 현재 보고 있는 줄을 바로 알 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 띄어쓰기(점)은 파란색, 탭(화살표)은 보라색으로 섞어 써도 구분할 수 있게 되었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) 세 번째 줄 0 뒤에 잘못 들어간 무의미한 공백은 붉은 색 에러를 띄워주고 있다. Ctrl+Shift+P 에서 명령어를 입력하면 줄 뒤에 들어간 공백을 한 번에 모두 정리할 수 있다. &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) 전체 하이라이트 끄기 명령어로 사용자가 필요할 때만 에디터 기능을 적용한다. 일반적인 코드를 다룰 때는 확장 기능을 적용하지 않은 것처럼 작업할 수 있다. 매번 프로그램을 지우지 않아도 괜찮아진 것이다.&amp;nbsp;&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;/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;a href=&quot;https://github.com/Raros17/whitespace-highlighter&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Raros17/whitespace-highlighter&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project</category>
      <author>꼬드리</author>
      <guid isPermaLink="true">https://hj97codeart.tistory.com/195</guid>
      <comments>https://hj97codeart.tistory.com/195#entry195comment</comments>
      <pubDate>Fri, 16 May 2025 17:16:46 +0900</pubDate>
    </item>
    <item>
      <title>파일이 전부 1KB로 받아지는 오류(서버 인증)</title>
      <link>https://hj97codeart.tistory.com/194</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;제작 중인 클라우드에 올려둔 파일을 다시 다운로드 했을 때 1KB의 파일만 받아지고, 내용은 보이지 않는 문제가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존까지는 원본대로 다운로드 되었는데 코드를 대거 정리하면서 한 번 꼬인 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTEUYg/btsNtqbVw4J/hL4NFMiivwG4f9lJV0xX1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTEUYg/btsNtqbVw4J/hL4NFMiivwG4f9lJV0xX1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTEUYg/btsNtqbVw4J/hL4NFMiivwG4f9lJV0xX1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTEUYg%2FbtsNtqbVw4J%2FhL4NFMiivwG4f9lJV0xX1K%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;560&quot; height=&quot;418&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;348&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bd78S0/btsNtkXkDq2/ND0w9nDh8HTF3QIpKKIo20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bd78S0/btsNtkXkDq2/ND0w9nDh8HTF3QIpKKIo20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bd78S0/btsNtkXkDq2/ND0w9nDh8HTF3QIpKKIo20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbd78S0%2FbtsNtkXkDq2%2FND0w9nDh8HTF3QIpKKIo20%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;348&quot; height=&quot;190&quot; data-origin-width=&quot;348&quot; data-origin-height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운 받은 모든 파일이 1KB인 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 사진 파일은 403KB 정도로, 확실하게 다른 파일을 받고 있다. txt로 바꿔 확인해보니 아래와 같은 메세지가 뜬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1745204460447&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{&quot;detail&quot;:&quot;Authentication credentials were not provided.&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 인증 관련 오류가 생겼을 거라는 판단 하에, download 관련 기능이 구현된 액션 코드를 열어 보았다.&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;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; fetch를 axios.get 형식으로 변경&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745205617916&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        fetch(response.data.download_url)
          .then((response) =&amp;gt; response.blob())
          .then((blob) =&amp;gt; {
            const url = window.URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = filename;
            document.body.appendChild(a);
            a.click();
            window.URL.revokeObjectURL(url);
            document.body.removeChild(a);
          });

        return { type: 'single', fileId: fileIds[0], filename };&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;왜인지 다운로드 액션에서 fetch로 받아오고 있는 것을 확인했다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;fetch로 받아올 시 blob 처리를 해줘도 따로 인증 정보를 같이 보네지 않으면 서버에서는 인증이 되지 않았다고 생각하고 에러를 표시하는 JSON을 반환하게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;blob&lt;/b&gt;는 서버에서 파일을 보내줬을 시, 일반 문자열 text나 JSON으로 받지 않고 Raw binary 데이터로 받게 한다. 이후 데이터를 Blob 객체로 포장한 뒤 다운로드 가능하게 만들어준다.&amp;nbsp;&lt;u&gt;즉 파일을 다룰 때는 깨지지 않도록 blob 처리를 해주어야 한다.&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그러나 이렇게 처리를 해도 &lt;b&gt;서버에서 제대로 인증이 되지 않으면 403 에러를 뜻하는 JSON만 반환&lt;/b&gt;한다. fetch() 요청에는 인증 정보가 들어가지 않는다. 기본적으로 쿠키와 헤더 같은 인증 정보를 전송하지 않기 때문이다. axios를 쓰지 않겠다면 fetch에 헤더로 토큰을 같이 보내주면 되지만, 어차피 프로젝트의 다른 곳에서 axios를 쓰고 있기 때문에 axios를 써서 형식을 통일하기로 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이때 responseType는 blob로 따로 지정해주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1745214375426&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;} else if (response.data.download_url) {
  let filename = response.data.filename || 'download';
  const blobResponse = await axiosInstance.get(response.data.download_url, {
    responseType: 'blob'
  });

  const blob = blobResponse.data;
  const url = window.URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  window.URL.revokeObjectURL(url);
  document.body.removeChild(a);

  return { type: 'single', fileId: fileIds[0], filename };
}&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;2개 이상의 파일을 압축하여 받아도 전과 달리 에러가 나거나 빈 파일이 넘어오지 않는다. fetch를 잘못 사용하고 있어서 사용자 권한 인증이 되지 않은 상황이었다고 정리할 수 있겠다.&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>Project</category>
      <author>꼬드리</author>
      <guid isPermaLink="true">https://hj97codeart.tistory.com/194</guid>
      <comments>https://hj97codeart.tistory.com/194#entry194comment</comments>
      <pubDate>Mon, 21 Apr 2025 15:09:04 +0900</pubDate>
    </item>
    <item>
      <title>VScode 터미널 단축키 설정으로 더 쉽게 빌드하기</title>
      <link>https://hj97codeart.tistory.com/193</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;기존까지는 도커를 사용하여 해당 프로젝트를 빌드하기 위해 아래와 같은 명령어를 사용하고 있었다.&lt;/p&gt;
&lt;pre id=&quot;code_1741051024652&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker-compose -f docker-compose.dev.yml up --build&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;기존 이미지가 있어도 새 코드가 바로 반영될 수 있도록, 이미지를 최신 코드로 강제 빌드한 뒤 컨테이너를 실행한다.(--build) 도커를 굳이 쓰지 않던 토이 플젝과 다르게 세이브 할 때마다 바로 내용이 반영되지 않는 건 아쉽다. 빌드에 시간이 좀 오래 걸리기도 하고.....&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;어쨌든 ... 핵심은, 단순한 css만 고친 뒤에도 화면에 반영된 걸 보고 싶다면 매번 저 긴 명령어를 터미널에 입력해야 한다는 것이다. 두 달간 해오긴 했는데 더이상 속이 터져 못 버티겠다 싶어 아예 VScode 설정을 손보기로 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;분명히 다른 방법이 있을 거야&lt;/i&gt;...... (라고 생각하면 늘 있다. 언제나 누군가 만들어뒀다. 천재들.)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; VScode에서 터미널 명령어 단축키 설정&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- tasks.json 설정&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;Ctrl+Shift+P&lt;/i&gt;(Mac의 경우 Cmd+Shift+P)를 눌러 Tasks: Configure Task를 선택한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;tasks.json 파일에서 아래와 같이 설정한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;해당 파일에서 각자 원하는대로 필요한 명령어를 바꾸거나 수정할 수 있을 것이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1741081838370&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;version&quot;: &quot;2.0.0&quot;,
  &quot;tasks&quot;: [
    {
      &quot;label&quot;: &quot;Docker Compose Up (Dev)&quot;,
      &quot;type&quot;: &quot;shell&quot;,
      &quot;command&quot;: &quot;docker-compose -f docker-compose.dev.yml up --build&quot;,
      &quot;problemMatcher&quot;: [],
      &quot;group&quot;: {
        &quot;kind&quot;: &quot;build&quot;,
        &quot;isDefault&quot;: true
      }
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 키 바인딩 설정&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;똑같이 열어서 Preferences:Open Keyboard Shortcuts(JSON)을 선택해서 연다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;해당 json 파일에 다음과 같이 추가해서 설정할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;key에 적힌 키를 다른 것으로 바꾸면 원하는 키를 눌러 실행 가능하다. 내 경우 &quot;f6&quot;으로 설정해 두었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;앞서 설정한 label가 args가 일치해야 하는 것으로 보인다.&lt;/p&gt;
&lt;pre id=&quot;code_1741081879270&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[
  {
    &quot;key&quot;: &quot;ctrl+alt+d&quot;,
    &quot;command&quot;: &quot;workbench.action.tasks.runTask&quot;,
    &quot;args&quot;: &quot;Docker Compose Up (Dev)&quot;
  }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;매번 터미널에 긴 명령어를 복붙할 필요가 없이 자신이 설정한 단축키만 누르면 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 쉬운 걸 왜 진작 안 하고 고생했나 싶다.... 역시 불편하다 싶으면 방법이 다 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project</category>
      <author>꼬드리</author>
      <guid isPermaLink="true">https://hj97codeart.tistory.com/193</guid>
      <comments>https://hj97codeart.tistory.com/193#entry193comment</comments>
      <pubDate>Tue, 4 Mar 2025 18:53:53 +0900</pubDate>
    </item>
    <item>
      <title>VScode의 Replace... ␍⏎ 해결</title>
      <link>https://hj97codeart.tistory.com/192</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; 갑자기 화면 가득 떠오른 Replace ␍⏎&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1740709269989&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Replace &amp;middot;fetchSearchData with ␍⏎&amp;middot;&amp;middot;fetchSearchData&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;어제까지만 해도 멀쩡하던 프로젝트에......&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;갑자기 열자마자 모든 파일에서 노란 줄이 뻑뻑 그이면서 위와 유사한 Replace 에러들을 가득 띄우기 시작했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;VScode 업데이트 하라고 해서 냉큼 했더니 그런가...? eslint 설정은 전혀 건드리지 않았는데...&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;어쨌든 기능을 수정하려면 당장 이 VScode의 에러를 해결해야 한다!!!&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 우선 Replace␍⏎ 라는 키워드로 유사한 문제를 겪은 글을 찾아보았다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740708780642&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Prettier ask me to replace ⏎↹↹ with &amp;middot;&quot; data-og-description=&quot;I have no clue what's going on, I cloned a github repo and literally just tried to change like one line but I got hit by this prettier error which makes no sense to me (I've never used prettier). R...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/67702186/prettier-ask-me-to-replace-with&quot; data-og-url=&quot;https://stackoverflow.com/questions/67702186/prettier-ask-me-to-replace-with&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/7PihD/hyYjhJDodl/J6zkcKxfcqZat0Lsk9oVPk/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316,https://scrap.kakaocdn.net/dn/ZiEra/hyYjhJDod2/IkxjNGCUOntOk0KbMk0z61/img.png?width=2346&amp;amp;height=830&amp;amp;face=0_0_2346_830&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/67702186/prettier-ask-me-to-replace-with&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/67702186/prettier-ask-me-to-replace-with&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/7PihD/hyYjhJDodl/J6zkcKxfcqZat0Lsk9oVPk/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316,https://scrap.kakaocdn.net/dn/ZiEra/hyYjhJDod2/IkxjNGCUOntOk0KbMk0z61/img.png?width=2346&amp;amp;height=830&amp;amp;face=0_0_2346_830');&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;Prettier ask me to replace ⏎↹↹ with &amp;middot;&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I have no clue what's going on, I cloned a github repo and literally just tried to change like one line but I got hit by this prettier error which makes no sense to me (I've never used prettier). R...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740708669079&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;editor.codeActionsOnSave&quot;: { &quot;source.fixAll&quot;: true },
  &quot;editor.formatOnSave&quot;: false,
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;해당 글에서는 VScode 설정인 &lt;b&gt;setting.json&lt;/b&gt;에 이 설정을 넣어 수정하라고 했다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;물론 나는 해결 중간 과정에서 기존에 생각 없이 쓰던 prettierrc 파일을 지우고... setting.json을 손 보고 eslintrc 파일을 prettier 설정과 합치며... 이것저것 건드리긴 했지만 최종적으로 해당 코드를 setting.json에 넣어서 문제가 해결되기는 한 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이건 prettier과 eslint 설정이 충돌하면서... 또 lf와 crlf가 충돌하며 흔히 생기는 문제라고 하는데, ...&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;내 경우 setting.json의 formatOnSave가 기존까지 true로 되어 있었기 때문에 false로 바꾸니 노란 줄이 사라졌다.&amp;nbsp;&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 이걸 false로 해두면 세이브 할 때마다 포맷팅이 아예 안 되는 것 아니냐... 싶은데 놀랍게도, 적용이 된다. 윗줄의 fixAll:true가 수정 역할을 가져가기 때문이라고. &lt;u&gt;formatOnSave가 true이면 Prettier가 먼저 포맷팅을 하고 이후 ESLint가 수정을 담당하며 두 설정이 충돌하게 된다.&lt;/u&gt; 내 경우 줄바꿈 Replace하라는 에러가 뜨는데 Prettier은 가로 정렬을, ESLint는 줄바꿈을 요구하며 계속 해결되지 않는 수정 요청을 하게 되는 것이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이제 false로 변경했으니 저장할 때마다 Prettier가 자동 포맷팅을 하지 않고, ESlint만 작동하게 된다. &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;즉 세이브마다 나타나던 충돌 에러가 사라진다.&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;i&gt;포맷터 충돌&lt;/i&gt;&lt;/b&gt;이라니 원리만 놓고 보면 어려운 건 아닌데...&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;코드 포맷을 유지하는 prettier과 eslint, setting.json의 우선 순위와 작동 방식을 어설프게 알고 있어 문제 해결에 시간이 걸렸던 것 같다. (덕분에 한 줄씩 뜯어봐야 했음...)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그래도 급하게 어디서 긁어와 불필요하게 어지럽던 설정들을 리펙토링 했으니 다행일지도?&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; 그럼 혹시... 이렇게 해결하면 빌드 전에 에러가 발생하진 않을까?&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;에디터에서 멀쩡하게 코드를 잘 작성한 것처럼 보여도 빌드 전에 에러를 우수수 뱉으면 난감해진다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;혹시나 하는 걱정에 알아보았다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;다행히 Prettier는 코드 스타일만 관리하기 때문에 저장할 때 포맷팅이 되지 않을 뿐, &lt;u&gt;빌드 도구에서 영향을 주지 않는다&lt;/u&gt;. 즉 런타임 에러, 빌드 에러는 발생하지 않기 때문에 이 부분에서는 걱정하지 않아도 될 것 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Etc</category>
      <author>꼬드리</author>
      <guid isPermaLink="true">https://hj97codeart.tistory.com/192</guid>
      <comments>https://hj97codeart.tistory.com/192#entry192comment</comments>
      <pubDate>Fri, 28 Feb 2025 11:43:13 +0900</pubDate>
    </item>
    <item>
      <title>[MD]Next에서 Quill의 CSS FOUC 깜빡임 문제</title>
      <link>https://hj97codeart.tistory.com/191</link>
      <description>&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;늦게 나타나는 ui 요소의 깜빡임은 사용자의 눈을 피로하게 만든다.&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;어쨌든 끔찍하게 못생긴 기본 폰트와 html 구조가 나오다가 3초 뒤 좀 있어 보이는(?) 화면으로 마법처럼 교체되는 경험을 누구나 한번쯤 해봤을 것이다. 전문 용어로는 flash of unstyled content, &lt;b&gt;FOUC&lt;/b&gt;라고 부르는데, &lt;u&gt;쉽게 말해서 외부의 CSS가 불러오기 전 잠시 스타일이 적용되지 않은 웹 페이지가 나타나는 현상&lt;/u&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;SSR 불일치 툴바2.gif&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dre9rP/btsL91MMS1j/YsMOGbdTfFHM4owgQHrY21/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dre9rP/btsL91MMS1j/YsMOGbdTfFHM4owgQHrY21/img.gif&quot; data-alt=&quot;아래서 깜빡! 깜빡! 깜빡!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dre9rP/btsL91MMS1j/YsMOGbdTfFHM4owgQHrY21/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dre9rP/btsL91MMS1j/YsMOGbdTfFHM4owgQHrY21/img.gif&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;717&quot; height=&quot;176&quot; data-filename=&quot;SSR 불일치 툴바2.gif&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아래서 깜빡! 깜빡! 깜빡!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안그래도 못생긴 툴바가 계속 깜빡거리니까 심각한 상태가 되었다. 내 정서적으로도 좋지 않다.&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; 그래서 간단히 시도해본 방법:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;1. useEffect로 2초 정도의 시간이 지난 뒤 툴바 보여주기 -&lt;/b&gt; &lt;/i&gt;기본 툴바 노출 시간이 줄긴 했는데 그럼에도 살짝 보이는 게 더 거슬렸다. 그렇다고 네트워크 환경에 따라 어떻게 될 지 모르는데 무제한으로 대기하게 할 수도 없고...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;2. useState로 상태 설정해서 툴바의 로딩이 끝날 때 0이던 opacity가 1로 바뀌도록 조건 주기 -&lt;/b&gt;&lt;/i&gt; 솔직히 될 것 같았는데. css가 엉망이라고 해도 기본 툴바는 이미 존재하고 있어서 그런지, 생각처럼 작동하지 않았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 좀더 근본적인 부분을 뜯어봐야 할 것 같아서 원인을 찾던 찰나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;next가 SSR 기반이기 때문에 간극이 생길 수도 있겠다는 생각이 들었다&lt;/u&gt;. 이 부분이 의심스러워서 Next에서 dynamic으로 quill 라이브러리를 가져오는 부분에 loading을 표시해보았다.&amp;nbsp;그리고 Network 탭을 열어 어떤 순서로 받아오는지 천천히 살펴보았다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1738915359211&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const QuillWrapper = dynamic(() =&amp;gt; import('react-quill-new'), {
  ssr: false,
  loading: () =&amp;gt; &amp;lt;p&amp;gt;Loading editor...&amp;lt;/p&amp;gt;
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;SSR 불일치 툴바.gif&quot; data-origin-width=&quot;1336&quot; data-origin-height=&quot;1178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKJkRH/btsMa4Iw4p3/dfjAdtMTKbNcGkWjOY8aek/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKJkRH/btsMa4Iw4p3/dfjAdtMTKbNcGkWjOY8aek/img.gif&quot; data-alt=&quot;좀더 자세히 보기 위해 Fast 4G로 슬로우 걸어놨다. 하단의 툴바에 주목하길 바란다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKJkRH/btsMa4Iw4p3/dfjAdtMTKbNcGkWjOY8aek/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cKJkRH/btsMa4Iw4p3/dfjAdtMTKbNcGkWjOY8aek/img.gif&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;1336&quot; height=&quot;1178&quot; data-filename=&quot;SSR 불일치 툴바.gif&quot; data-origin-width=&quot;1336&quot; data-origin-height=&quot;1178&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;좀더 자세히 보기 위해 Fast 4G로 슬로우 걸어놨다. 하단의 툴바에 주목하길 바란다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상했던 문제가 맞았다. 초기 quill loading 상태에서 표시되는 'Loading editor...'과 못생긴 기본 툴바의 유지 시간이 일치한다. 즉, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;quill을 완전하게 불러와 커스텀 CSS 툴바가 적용되기까지 1초 정도 텀이 생기고, 바로 이 텀 동안 Quill의 기본 툴바 스타일을 보여주고 있다&lt;/span&gt;는 뜻이 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크를 열어 보아도 다른 필수 요소들이 불러와진 뒤, 마지막에 가서야 quill 관련을 받아오는 것을 확인할 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react quill은 document 등 브라우저 기반으로 이루어진 라이브러리이기 때문에, Next에서 쓰려면 무조건 dynamic을 통해 클라이언트 사이드에서만 되도록 조치를 해주어야 한다. 그러니까 클라이언트&amp;nbsp; 사이드에서 quill을 쓸 수 있을 때까지 어찌 되었건 조금 기다려야 한다는 건데, ... ...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조 자체를 어떻게 손볼 방법은 지금으로선 없어 보인다. 그럼... 어떻게 해결하지?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고민하다가 아래와 같은 &lt;s&gt;어찌 보면 좀 무식한 방법&lt;/s&gt;을 시도해보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1738910753112&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  useEffect(() =&amp;gt; {
    const checkQuillLoaded = setInterval(() =&amp;gt; {
      if (document.querySelector('.ql-toolbar')) {
        setIsQuillLoaded(true);
        clearInterval(checkQuillLoaded);
      }
    }, 100);&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;현재 깜빡임 문제의 핵심 원인은 Quill이 클라이언트에서 비동기적으로 로드되면서 스타일이 늦게 적용되기 때문이다. 그래서 아예 &lt;u&gt;Quill이 DOM에 렌더링되었는지 체크하는 방식&lt;/u&gt;으로 해결해보기로 했다. 위 코드는 로딩 여부를 담는 useState와 useEffect를 사용하여 quill에서 제공하는 .ql-toolbar가 무사히 로딩 되었는지 100ms마다 감시하며 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 로딩이 끝났다면 클래스 이름을 바꿔주어 기존까지 opacity가 0이었던 툴바가 담긴 div를 가시화 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1738916223263&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;            &amp;lt;div
              className={
                isQuillLoaded ? styles.visibleToolbar : styles.hiddenToolbar
              }
            &amp;gt;
              &amp;lt;CustomToolbar /&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;p data-ke-size=&quot;size16&quot;&gt;CSS는 하단과 같이 주어, 자연스럽게 드러나도록 최선을 다했다.&lt;/p&gt;
&lt;pre id=&quot;code_1738912129695&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.hiddenToolbar {
  display: none;
}

.visibleToolbar {
  display: flex;
  opacity: 0;
  animation: fadeIn 0.3s forwards;
}

@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 고친 결과&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;SSR 불일치 툴바3.gif&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YibwC/btsMbCri87e/EKRjZkXYK6VGUobXWiGvEk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YibwC/btsMbCri87e/EKRjZkXYK6VGUobXWiGvEk/img.gif&quot; data-alt=&quot;quill이 완전히 준비된 뒤 fade로 하단 툴바가 나타난다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YibwC/btsMbCri87e/EKRjZkXYK6VGUobXWiGvEk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/YibwC/btsMbCri87e/EKRjZkXYK6VGUobXWiGvEk/img.gif&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;509&quot; height=&quot;758&quot; data-filename=&quot;SSR 불일치 툴바3.gif&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;1058&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;quill이 완전히 준비된 뒤 fade로 하단 툴바가 나타난다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React를 썼으면 좀 나았을까? 이론상으로는 문제 없었을 것 같기도 하다. (대체 어쩌다 내가 Next로...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 툴바만 맨 마지막에 나오는 게 보기 좀 거슬리긴 하는데, 네트워크가 원활하다면 1초 정도기도 하고 어차피 quill이 로드 끝날 때까지는 에디터를&amp;nbsp; 쓸 수도 없을 뿐더러...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 화면에 아무것도 안 나오고 빈 공백인 게 더 사용자 입장에서는 불편할 듯 하여 지금은 이 정도로 보류하기로 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 페이지 들어오자마자 곧바로 에디터에 글을 입력할 수 있다면 가장 좋을 텐데... 아쉽...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러려면 아예 자체 구현을 해야할 것 같기도 하고... ... 작동하지 않는 가짜 툴바만 먼저 보여주는 방법도 생각해봤는데, 일단은 중요도가 낮다 생각하여 보류.&amp;nbsp;영 거슬리면 loading 동안 기다려달라는 메세지를 보여주다가 로딩이 끝난 뒤 툴바와 전체 에디터를 한번에 제공하는 방법도 괜찮을 것 같다. 어차피 로딩 되는 중에는 에디터에 글자 입력도 안 된다.&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;</description>
      <category>Project</category>
      <author>꼬드리</author>
      <guid isPermaLink="true">https://hj97codeart.tistory.com/191</guid>
      <comments>https://hj97codeart.tistory.com/191#entry191comment</comments>
      <pubDate>Fri, 7 Feb 2025 17:17:59 +0900</pubDate>
    </item>
    <item>
      <title>React 리펙토링 중에 만난 defaultProps 경고 에러</title>
      <link>https://hj97codeart.tistory.com/190</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;회사에서 받은 코드를 리펙토링 하기 위해 뜯다가 콘솔 창에서 생소한 경고를 마주쳤다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Warning:&amp;nbsp;MyComponent:&amp;nbsp;Support&amp;nbsp;for&amp;nbsp;defaultProps&amp;nbsp;will&amp;nbsp;be&amp;nbsp;removed&amp;nbsp;from&amp;nbsp;function&amp;nbsp;components&amp;nbsp;in&amp;nbsp;a&amp;nbsp;future&amp;nbsp;major&amp;nbsp;release.&amp;nbsp;Use&amp;nbsp;JavaScript&amp;nbsp;default&amp;nbsp;parameters&amp;nbsp;instead.&lt;/span&gt; &lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; defaultProps가 무엇이고, 관련 에러는 왜 발생할까?&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;기존까지&lt;b&gt; defaultProps&lt;/b&gt;는 React 컴포넌트, 특히 클래스형 컴포넌트에서 &lt;i&gt;props의 기본값을 설정하는 방법&lt;/i&gt;이었다. 그러나 React 17 이후로 함수형 컴포넌트가 보편화되면서 더 이상 defaultProps가 권장되지 않았고, 대신 &lt;u&gt;ES6의 기본 매개변수 방식을 사용하는 것이 표준이 되었다.&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 해당 기능은 더 이상 사용하지 않을 것이라고 경고 에러가 뜨는 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;.... 정리하자면 옛날 코드에서 쓰던 거니까 그거 이제 그만 쓰라는 경고 메세지이다. 미래에는 아예 사용이 불가능해질 수도 있으니 빠르게 리펙토링이 되어야 하는 부분이다. 사실 기능 구현만 급하게 짠 뒤 받은 코드라 전체적으로 함수형 컴포넌트를 사용하고 있음에도 이런 에러까지는 체크하지 못한 것 같다. 하필 메인 페이지에서 뜨는 게 거슬려, 마침 다른 hotfix 처리하는 김에 에러도 빨리 해결해서 합치기로 했다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 해결 방법 : 기본 매개변수 넣어주기&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;defaultProps 에러를 해결하는 방법은 간단하다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;아래 예시와 같이&amp;nbsp;&lt;u&gt;&lt;b&gt;기본 매개변수(default parameters)&lt;/b&gt;를 사용해주면 된다.&lt;/u&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738725515475&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 기존까지는 이렇게 사용되었다.
class MyComponent extends React.Component {
  render() {
    return (
      &amp;lt;div&amp;gt;
        &amp;lt;p&amp;gt;Name: {this.props.name}&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;Age: {this.props.age}&amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
    );
  }
}

MyComponent.defaultProps = {
  name: 'Guest',
  age: 25,
};


&amp;lt;MyComponent /&amp;gt;  // Name: Guest, Age: 25
&amp;lt;MyComponent name=&quot;Alice&quot; /&amp;gt;  // Name: Alice, Age: 25&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1738725559516&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//기본 매개변수를 사용하면 훨씬 깔끔하다.
const MyComponent = ({ name = &quot;Guest&quot;, age = 25 }) =&amp;gt; {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;p&amp;gt;Name: {name}&amp;lt;/p&amp;gt;
      &amp;lt;p&amp;gt;Age: {age}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;관련 코드만 검색해서 전부 기본 매개변수로 바꿨더니 에러가 사라졌다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이전보다 개선된 코드로 바뀐 것 같다.&lt;/p&gt;</description>
      <category>Project</category>
      <author>꼬드리</author>
      <guid isPermaLink="true">https://hj97codeart.tistory.com/190</guid>
      <comments>https://hj97codeart.tistory.com/190#entry190comment</comments>
      <pubDate>Wed, 5 Feb 2025 12:25:12 +0900</pubDate>
    </item>
    <item>
      <title>[MD]Next의 SSR과 Quill의 module 충돌 문제 (500 Server Error)</title>
      <link>https://hj97codeart.tistory.com/189</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;언젠가부터 콘솔에&amp;nbsp;도통 알 수 없는 에러 하나가 괴롭히고 있었으니......&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;500번대 에러가 갑자기 뜰 게 뭐란 말인가?&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;/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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;803&quot; data-origin-height=&quot;125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wNomv/btsL5iHi4P3/mWIvS9v061eKHmgP0osCEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wNomv/btsL5iHi4P3/mWIvS9v061eKHmgP0osCEK/img.png&quot; data-alt=&quot;화면 작업 중에도 끈질기게 거슬리던 500...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wNomv/btsL5iHi4P3/mWIvS9v061eKHmgP0osCEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwNomv%2FbtsL5iHi4P3%2FmWIvS9v061eKHmgP0osCEK%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;803&quot; height=&quot;125&quot; data-origin-width=&quot;803&quot; data-origin-height=&quot;125&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;화면 작업 중에도 끈질기게 거슬리던 500...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;309&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KXKgc/btsL452i6q0/hOgUxAweSF6jooWATfhjWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KXKgc/btsL452i6q0/hOgUxAweSF6jooWATfhjWk/img.png&quot; data-alt=&quot;write 페이지에서 document 받아오면서 서버 500가 뜨는 것을 확인할 수 있음.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KXKgc/btsL452i6q0/hOgUxAweSF6jooWATfhjWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKXKgc%2FbtsL452i6q0%2FhOgUxAweSF6jooWATfhjWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;813&quot; height=&quot;309&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;309&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;write 페이지에서 document 받아오면서 서버 500가 뜨는 것을 확인할 수 있음.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반에 어디서 문제가 생기는지 알 수가 없어서 한창 삽질을 했다. 결국 페이지를 기본 세팅으로 돌려두고 기능을 하나씩 추가하는 과정에서 문제가 maxLengthModule(저번에 직접 세팅해서 넣은 quill 최대 글자수 제한하는 모듈)에서 발생하는 것을 알게 되었다.&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;Next는 React와 달리 SSR로 동작하는 구간이 있는데&lt;/b&gt; 여기서 뭔가..... 문제가 생겨서 500이 발생했을 확률이 높아 보였다. 따라서 &lt;u&gt;maxLengthModule()이 서버 환경에서도 실행되어 Next.js SSR과 충돌이 발생했을 가능성&lt;/u&gt;을 최우선으로 두고 해결하기로 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 최상단에서 import 하던 maxLengthModule를 useEffect 내부로 옮기고 내부에서 모듈 등록&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useEffect 내부에서만 불러올 때 비동기로 await를 써서 불러온다. (이렇게 하지 않고 최상위에서 불러올 경우 똑같이 서버 에러를 뱉는 것을 확인. useEffect에서 실행하면 &lt;u&gt;모듈 등록을 클라이언트 렌더링 이후에만 하도록 처리&lt;/u&gt;할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1738574357758&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;maxLengthModule(); //기존까지 코드 상단 useEffect 외부에서 실행함
const MAX_TEXT_LENGTH = 1500;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1738574502968&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; useEffect(() =&amp;gt; { //내부로 수정
    const initializeModules = async () =&amp;gt; {
      const { maxLengthModule } = await import(
        '@/configs/tanstack-query/quill/maxlengthModule'
      );
      if (typeof window !== 'undefined') {
        maxLengthModule();
      }

      setModules({
        toolbar: {
          container: '#toolbar',
          handlers: {
            createVote: handleAddVote,
            addHashTag: handleAddHashTag
          }
        },
        maxlength: { maxLength: MAX_TEXT_LENGTH }
      });
    };

    initializeModules();
  }, [handleAddVote]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 서버 사이드 렌더링 방지하기 (typeof window !== 'undefined') &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈 maxLengthModule를 구현한 코드 내부에 클라이언트에서만 실행되게 조건을 따로 주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것저것 지우면서 봤는데 모듈을 등록하기 위해 쓰는 Quill.register 부분이 문제가 되는 것 같다. 와중에 모듈 중복 등록 문제도 있는 것 같아서 조건에 포함시켰다.&lt;/p&gt;
&lt;pre id=&quot;code_1738574461997&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Quill } from 'react-quill-new';

interface MaxLengthOptions {
  maxLength: number;
}

export const maxLengthModule = () =&amp;gt; {
  if (typeof window !== 'undefined' &amp;amp;&amp;amp; !Quill.imports['modules/maxlength']) {
  //이 부분
    Quill.register(
      'modules/maxlength',
      function (quill: InstanceType&amp;lt;typeof Quill&amp;gt;, options: MaxLengthOptions) {
        quill.on('text-change', () =&amp;gt; {
          const currentText = quill.getText().trim();
          if (currentText.length &amp;gt; options.maxLength) {
            quill.deleteText(options.maxLength, currentText.length);
          }
        });
      }
    );
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;b&gt;코드 비교: useEffect를 사용하던 전과 후이다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글자수 모듈 관련 코드를 useEffect 내부로 옮겨온 것을 볼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 useEffect 내부에서 전부 처리하는 게 효율적인지에 관해서는 좀더 생각을 해봐야겠지만......&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1738573389110&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  useEffect(() =&amp;gt; {
    setModules({
      toolbar: {
        container: '#toolbar',
        handlers: {
          createVote: handleAddVote,
          addHashTag: handleAddHashTag
        }
      },
      maxlength: { maxLength: MAX_TEXT_LENGTH }
    });
  }, [handleAddVote]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1738573724573&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; useEffect(() =&amp;gt; { //내부로 수정
    const initializeModules = async () =&amp;gt; {
      const { maxLengthModule } = await import(
        '@/configs/tanstack-query/quill/maxlengthModule'
      );
      if (typeof window !== 'undefined') {
        maxLengthModule();
      }

      setModules({
        toolbar: {
          container: '#toolbar',
          handlers: {
            createVote: handleAddVote,
            addHashTag: handleAddHashTag
          }
        },
        maxlength: { maxLength: MAX_TEXT_LENGTH }
      });
    };

    initializeModules();
  }, [handleAddVote]);&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;같은 코드지만 아래처럼 두 개의 useEffect로 그냥 분리도 가능할 것 같고.....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복 렌더링 방지가 나은지 가독성이 나은지... 가독성이 낫겠지...&lt;/p&gt;
&lt;pre id=&quot;code_1738575607542&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  const registerMaxLengthModule = async () =&amp;gt; {
    const { maxLengthModule } = await import('@/configs/tanstack-query/quill/maxlengthModule');
    if (typeof window !== 'undefined') {
      maxLengthModule();
    }
  };

  registerMaxLengthModule();
}, []);  // 초기 한번


useEffect(() =&amp;gt; {
  setModules({
    toolbar: {
      container: '#toolbar',
      handlers: {
        createVote: handleAddVote,
        addHashTag: handleAddHashTag
      }
    },
    maxlength: { maxLength: MAX_TEXT_LENGTH }
  });
}, [handleAddVote]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 결론: Next와 다른 브라우저 라이브러리를 쓸 때 SSR과 충돌 나서 500 에러가 발생 가능하다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저 전용 라이브러리(Quill) 사용할 때 항상 &lt;u&gt;typeof window !== 'undefined'&lt;/u&gt; 가 필요한지 체크하기. 서버 사이드 렌더링(SSR) 환경과 충돌하지 않도록 클라이언트 전용 코드로 처리하면 된다.&lt;/li&gt;
&lt;li&gt;&lt;u&gt;모듈 등록은 useEffect 내부에서만 처리한다&lt;/u&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;797&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0Ha01/btsL4KK2tiK/K9mB7qqaB5EPmTCD5pabW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0Ha01/btsL4KK2tiK/K9mB7qqaB5EPmTCD5pabW1/img.png&quot; data-alt=&quot;끝.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0Ha01/btsL4KK2tiK/K9mB7qqaB5EPmTCD5pabW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0Ha01%2FbtsL4KK2tiK%2FK9mB7qqaB5EPmTCD5pabW1%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;797&quot; height=&quot;245&quot; data-origin-width=&quot;797&quot; data-origin-height=&quot;245&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;끝.&lt;/figcaption&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;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+) 하루 지나고 생각해보니 quill 처음 적용할 때도 이와 비슷한 문제가 발생해서 dynamic으로 적용했던 것 같다.&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;Next.js와 같은 서버 사이드 렌더링(SSR) 환경에서 발생할 수 있는 문제&lt;/u&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;. quill editor은 기본적으로 document 객체를 조작하여 동작한다. 그런데 이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;u&gt;document 객체는 브라우저 환경에서만 사용할 수 있지만&lt;/u&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;, Next의 경우 SSR로 작동한다. 따라서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;서버에서는 브라우저 관련 객체들이 정의되어 있지 않아&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: justify;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이러한 객체를 참조하면 에러가 발생한다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Project</category>
      <author>꼬드리</author>
      <guid isPermaLink="true">https://hj97codeart.tistory.com/189</guid>
      <comments>https://hj97codeart.tistory.com/189#entry189comment</comments>
      <pubDate>Mon, 3 Feb 2025 18:41:52 +0900</pubDate>
    </item>
    <item>
      <title>[MD]ReactQuill register로 텍스트 글자 수 제한(MaxLength)</title>
      <link>https://hj97codeart.tistory.com/187</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Quill을 사용하면서 &lt;u&gt;텍스트 수를 1500자까지만 받아야 되는 조건&lt;/u&gt;이 붙었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;input 라면 아래처럼 &lt;b&gt;maxLength&lt;/b&gt;를 사용해서 처리하면 되는데... ...&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1736323095219&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;      &amp;lt;input
          ref={inputRef}
          type=&quot;text&quot;
          value={value}
          maxLength={15}
        /&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;이상하게도 React Quill에는 maxLength가 존재하지 않아 당혹스러웠다. 텍스트 에디터인데 글자 수 max 조건을 거는 게 이렇게 어려워서야. 가장 자주 쓰는 기능이 아닌가? (아직도 왜 maxLength가 없는지 모르겠다.)&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. onChange의 함수로 통제 시도... but...&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 별 생각 없이 &lt;b&gt;onChange&lt;/b&gt;를 통해 텍스트를 MAX_TEXT_LENGTH를 넘으면 잘라내는 함수를 전달하는 방식을 시도했다. 물론 MAX_TEXT_LENGTH는 1500자로 상위에 상수를 설정했다.&lt;/p&gt;
&lt;pre id=&quot;code_1736323176826&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const handleTextChange = (content: string, source: string) =&amp;gt; {
    if (source === 'user') {
      if (content.length &amp;gt; MAX_TEXT_LENGTH) {
        setValues(content.slice(0, MAX_TEXT_LENGTH));
      } else {
        setValues(content);
      }
    }
  };&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;QuillWrapper에다가 &lt;u&gt;onChange를 걸어서 변화할 때마다 handeTextChange를 실행시킨다.&amp;nbsp;&lt;/u&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1736323218750&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;            &amp;lt;QuillWrapper
              theme=&quot;snow&quot;
              modules={modules}
              formats={['bold', 'color']}
              value={values}
              onFocus={() =&amp;gt; setIsFocused(true)}
              onBlur={() =&amp;gt; setIsFocused(false)}
              className={styles.customQuill}
              onChange={handleTextChange}
            /&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;결론적으로는 텍스트에 slice가 되기는 하는데... &lt;b&gt;onChange가 실시간으로 먹히지 않아 텍스트를 쓰던 중 1500자를 넘었는데도 계속 이어서 작성이 가능한 문제&lt;/b&gt;가 생겼다. onChange는 외부를 클릭하여 focus가 에디터로부터 벗어났을 때 적용되며, 이때 handleTextChange함수가 실행되면서 텍스트를 MAX LENGTH만큼 잘라내게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 유저는 1500자 넘었는데도 warning 없이 계속 글을 이어서 쓰다가... 드디어 다 썼다고 생각하며 외부를 클릭하면...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&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;끔찍하다. 이것만은 안 된다는 생각에 다른 방법을 찾기 시작했다.&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;size18&quot;&gt;&lt;b&gt;2. Ref를 적용하는 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서치하다가 발견한 것인데 QuillWrapper에 ref를 걸어 직접 글자 수를 감시하는 방법이 있었다. Quill 에디터 인스턴스를 참조하고 text-change 이벤트를 통해 실시간으로 텍스트 길이를 제한하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 QuillWrapper에 직접 ref를 쓸 수 없다고 에러가 떴다. &lt;u&gt;ReactQuill 컴포넌트가 기본적으로 ref를 지원하지 않기 때문이다&lt;/u&gt;. ref가 아니라 forwardRef로 ReactQuill을 감싸는 래퍼 컴포넌트를 작성해야 한다고 하는데, .......&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  3.&amp;nbsp; Quill.register로 커스텀 모듈 추가 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 적용한 방식이다. Quill의 register을 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1736324508986&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Quill } from 'react-quill-new';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt; Quill.register를 사용해 글자 수 제한을 처리하는 커스텀 모듈을 정의하는 방식&lt;/u&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 Quill 내부에서 &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;타입 스크립트를 쓰고 있기 때문에 interface로 따로 타입도 지정해주었다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1736324828876&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const MAX_TEXT_LENGTH = 12;

interface MaxLengthOptions {
  maxLength: number;
}

Quill.register(
  'modules/maxlength',
  function (quill: InstanceType&amp;lt;typeof Quill&amp;gt;, options: MaxLengthOptions) {
    quill.on('text-change', function () {
      const currentText = quill.getText().trim();
      if (currentText.length &amp;gt; options.maxLength) {
        quill.deleteText(
          options.maxLength,
          currentText.length - options.maxLength
        );
      }
    });
  }
);&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;Quill.register로 Quill 에디터에 새로운 모듈을 등록한다. &lt;u&gt;모듈의 이름은 maxlength로 첫 번째 매개변수이며 두 번째 매개변수는 모듈 기능의 구현이다&lt;/u&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;text-change는 에디터의 내용이 변경될 때마다 호출한다는 뜻이다. getText를 통해 순수 텍스트만 가져온 뒤 trim으로 공백 제거한다. 이렇게 가공된 텍스트가 options.maxLength보다 크면 넘친 만큼 삭제한다. 원리 자체는 함수와 다를 것도 없는데, quill의 모듈을 커스텀하여 텍스트가 변경될 때마다 감시하게 한다는 점이 다른 것 같다.&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;이후 아래와 같이 modules에 maxlength란을 추가하고 QuillWrapper에 modules를 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 경우 툴바 기능을 커스텀하느라 이미 modules를 따로 만들어 사용 중이었기 때문에 maxlength만 추가하면 되었다.&lt;/p&gt;
&lt;pre id=&quot;code_1736324920415&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const modules = {
  toolbar: [['bold', 'italic'], [{ list: 'bullet' }]],
  maxlength: { maxLength: 12 }, // 최대 글자 수 12자로 설정
};
  
&amp;lt;QuillWrapper
 modules={modules}
 value={values}
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Animation.gif&quot; data-origin-width=&quot;520&quot; data-origin-height=&quot;740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cl78io/btsLHQ42R2I/Nl6F9ZXPqHEqevDBiMUeo1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cl78io/btsLHQ42R2I/Nl6F9ZXPqHEqevDBiMUeo1/img.gif&quot; data-alt=&quot;maxLength를 임시로 12자만 두고 적용한 결과.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cl78io/btsLHQ42R2I/Nl6F9ZXPqHEqevDBiMUeo1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cl78io/btsLHQ42R2I/Nl6F9ZXPqHEqevDBiMUeo1/img.gif&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;520&quot; height=&quot;740&quot; data-filename=&quot;Animation.gif&quot; data-origin-width=&quot;520&quot; data-origin-height=&quot;740&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;maxLength를 임시로 12자만 두고 적용한 결과.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에디터 외부를 클릭하지 않아도, 특정 length를 넘어선 순간 아예 입력되지 않게 적용된 것을 확인할 수 있다. 현재까지 몇 자를 썼고 얼마나 더 쓸 수 있는지 UI로 유저에게 표시할 수 있다면 더 좋을 것도 같다.&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;style2&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1129&quot; data-origin-height=&quot;1638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcY026/btsLGaKvyOa/NlnaUtU93rgcnEQK1UYhi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcY026/btsLGaKvyOa/NlnaUtU93rgcnEQK1UYhi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcY026/btsLGaKvyOa/NlnaUtU93rgcnEQK1UYhi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcY026%2FbtsLGaKvyOa%2FNlnaUtU93rgcnEQK1UYhi1%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;716&quot; height=&quot;1039&quot; data-origin-width=&quot;1129&quot; data-origin-height=&quot;1638&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://quilljs.com/docs/api#selection&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;에도 modules 제작에 관련해 나와 있기 때문에 필요하면 참조한다.&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;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+) 25.02.05 추가: Next.js를 쓰는 경우 모듈 충돌 에러가 발생할 수 있다. 한번 확인해볼 것.&lt;/p&gt;
&lt;figure id=&quot;og_1738723697224&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;[MD]Next의 SSR과 Quill의 module 충돌 문제 (500 Server Error)&quot; data-og-description=&quot;언젠가부터 콘솔에&amp;nbsp;도통 알 수 없는 에러 하나가 괴롭히고 있었으니......&amp;nbsp;500번대 에러가 갑자기 뜰 게 뭐란 말인가?&amp;nbsp;심지어 개발 환경에서 구현할 때에는 문제 없어서 흐린 눈 하고 끝까지 미&quot; data-og-host=&quot;hj97codeart.tistory.com&quot; data-og-source-url=&quot;https://hj97codeart.tistory.com/189&quot; data-og-url=&quot;https://hj97codeart.tistory.com/189&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bZnJMj/hyYcjMXizv/g9iGkxt0gtwFlFIcrLDxSk/img.png?width=800&amp;amp;height=124&amp;amp;face=0_0_800_124,https://scrap.kakaocdn.net/dn/7OLjC/hyYb8LpxFT/JOJoUkow20Z81y9pv2Yk31/img.png?width=800&amp;amp;height=124&amp;amp;face=0_0_800_124,https://scrap.kakaocdn.net/dn/cMse1Q/hyYb6fLRxu/YPxnJVvPxKLhyLPkR6YAr1/img.jpg?width=1000&amp;amp;height=1000&amp;amp;face=296_190_528_444&quot;&gt;&lt;a href=&quot;https://hj97codeart.tistory.com/189&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hj97codeart.tistory.com/189&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bZnJMj/hyYcjMXizv/g9iGkxt0gtwFlFIcrLDxSk/img.png?width=800&amp;amp;height=124&amp;amp;face=0_0_800_124,https://scrap.kakaocdn.net/dn/7OLjC/hyYb8LpxFT/JOJoUkow20Z81y9pv2Yk31/img.png?width=800&amp;amp;height=124&amp;amp;face=0_0_800_124,https://scrap.kakaocdn.net/dn/cMse1Q/hyYb6fLRxu/YPxnJVvPxKLhyLPkR6YAr1/img.jpg?width=1000&amp;amp;height=1000&amp;amp;face=296_190_528_444');&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;[MD]Next의 SSR과 Quill의 module 충돌 문제 (500 Server Error)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;언젠가부터 콘솔에&amp;nbsp;도통 알 수 없는 에러 하나가 괴롭히고 있었으니......&amp;nbsp;500번대 에러가 갑자기 뜰 게 뭐란 말인가?&amp;nbsp;심지어 개발 환경에서 구현할 때에는 문제 없어서 흐린 눈 하고 끝까지 미&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hj97codeart.tistory.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;</description>
      <category>Project</category>
      <author>꼬드리</author>
      <guid isPermaLink="true">https://hj97codeart.tistory.com/187</guid>
      <comments>https://hj97codeart.tistory.com/187#entry187comment</comments>
      <pubDate>Wed, 8 Jan 2025 17:48:16 +0900</pubDate>
    </item>
    <item>
      <title>[MD]Quill의 placeholder에서 한글 IME 인식 문제: 기능 구현</title>
      <link>https://hj97codeart.tistory.com/186</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트에서 &lt;b&gt;React Quill new&lt;/b&gt;를 다루는 과정에서 에디터를 만지면서 만난 에러들을 해결하는 포스팅이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-quill-new를 쓰기로 선택한 이유는 특정 이벤트에 대한 지원이 중단 되면서 기본 react quill에서는 아래와 같은 경고 표시가 떴기 때문이다. 지원 중단 부분만 개선되고 지금까지는 기존 quill과 크게 다르지 않은 것 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1736212392372&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;react-quill.js?v=d23b9689:5562 [Deprecation] Listener added for a 
'DOMNodeInserted' mutation event. Support for this event type has been removed, 
and this event will no longer be fired. See https://chromestatus.com/feature/5083947249172480 
for more information.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 주어진 React Quill의 placeholder 기능에 문제가 생겼다.&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;407&quot; data-origin-height=&quot;377&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oNvmX/btsLFh2Xu5D/Bib8FjiTOwV35ASFsSi07K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oNvmX/btsLFh2Xu5D/Bib8FjiTOwV35ASFsSi07K/img.png&quot; data-alt=&quot;기본으로 주어진 placeholder 기능 사용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oNvmX/btsLFh2Xu5D/Bib8FjiTOwV35ASFsSi07K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoNvmX%2FbtsLFh2Xu5D%2FBib8FjiTOwV35ASFsSi07K%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;407&quot; height=&quot;377&quot; data-origin-width=&quot;407&quot; data-origin-height=&quot;377&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기본으로 주어진 placeholder 기능 사용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c62OLh/btsLEqzxlrF/KhjIKCP8CsK1aC7eB1lsTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c62OLh/btsLEqzxlrF/KhjIKCP8CsK1aC7eB1lsTK/img.png&quot; data-alt=&quot;한 글자를 입력했는데 사라지지 않는다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c62OLh/btsLEqzxlrF/KhjIKCP8CsK1aC7eB1lsTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc62OLh%2FbtsLEqzxlrF%2FKhjIKCP8CsK1aC7eB1lsTK%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;439&quot; height=&quot;382&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;한 글자를 입력했는데 사라지지 않는다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당혹스럽게도, 두 글자까지 입력한 뒤에야 placeholder가 사라지는 상황을 맞닥뜨린다. 영어의 경우 문제가 없는데, 한국어의 경우에만 처음 텍스트 입력시 placeholder가 남아있는 상황. &lt;u&gt;&lt;b&gt;한글 입력 방식&lt;/b&gt;(IME, Input Method Editor)이 영어와 다르기 때문에 입력을 처리하는 과정이 달라 발생하는 문제&lt;/u&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;
&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; 해결법: placeholder 유사하게 자체 구현&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마음 같아서는 placeholder을 그냥 빼버리면 더없이 좋겠으나... 사전 만들어진 디자인에 따르면 이 페이지는 placeholder을 제공해야 한다. 따라서 유사하게라도 구현을 해보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원리 자체는 어렵지 않다. &lt;u&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;아래와 같이 값이 존재하는가, focus 되었는가&lt;/b&gt; &lt;/span&gt;두 가지 조건의 useState를 사용한다.&lt;/u&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1736213213911&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const [values, setValues] = useState&amp;lt;string&amp;gt;('');
  const [isFocused, setIsFocused] = useState&amp;lt;boolean&amp;gt;(false);&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;onFocus와 onBlur를 사용하여 해당 영역에 focus가 되었는지 여부를 감시한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;값이 없고 focus 되지 않은 경우&lt;/b&gt;만 placeholder라는 class를 가진 div가 드러나도록 조건을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;!value로 처리하지 않으면 값을 입력하다가 focus만 벗어나도 다시 placeholder가 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;!isFocused로 처리하지 않으면 quill의 value가 여전히 한글의 IME를 2글자부터 인식해서 제대로 작동하지 않는다.&lt;/p&gt;
&lt;pre id=&quot;code_1736213093139&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;          &amp;lt;div className={styles.quillWrapper}&amp;gt;
            {!isFocused &amp;amp;&amp;amp; !values &amp;amp;&amp;amp; (
              &amp;lt;div className={styles.placeholder}&amp;gt;
                데스크무드를 소개해주세요!                 
              &amp;lt;/div&amp;gt; //기능을 따로 구현한 placeholder
            )}
            &amp;lt;QuillWrapper
              theme={'snow'}
              modules={modules}
              formats={['bold', 'color']}
              value={values}
              onChange={handleTextChange}
              onFocus={() =&amp;gt; setIsFocused(true)}
              onBlur={() =&amp;gt; setIsFocused(false)}
              className={styles.customQuill}
              placeholder=&quot;데스크무드를 소개해주세요!&quot; //이렇게 입력한 placeholder이 문제여서 후에 삭제
            /&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Animation.gif&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKweq6/btsLG4VWICC/zpzKFjSfzhyNRM1Ux2xf6K/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKweq6/btsLG4VWICC/zpzKFjSfzhyNRM1Ux2xf6K/img.gif&quot; data-alt=&quot;적용된 사항&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKweq6/btsLG4VWICC/zpzKFjSfzhyNRM1Ux2xf6K/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cKweq6/btsLG4VWICC/zpzKFjSfzhyNRM1Ux2xf6K/img.gif&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;824&quot; height=&quot;418&quot; data-filename=&quot;Animation.gif&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;적용된 사항&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;i&gt;&lt;b&gt;P.S.&lt;/b&gt; &lt;/i&gt;사실 이 기능은 한 달 전에 해결하며 급하게 기능만 구현해둔 것인데, 불현듯 '왜 에디터에 기본 placeholder 기능이 분명 있을 텐데 내가 쓰지 않았지...?' 하며 까먹고 다시 적용 해봤다가 '아...' 하고 깨달아 기록해둔다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;기본 placeholder을 한글도 인식 되게 라이브러리 자체를 수정하는 방법이 있으면 더 좋을 것 같다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Project</category>
      <author>꼬드리</author>
      <guid isPermaLink="true">https://hj97codeart.tistory.com/186</guid>
      <comments>https://hj97codeart.tistory.com/186#entry186comment</comments>
      <pubDate>Tue, 7 Jan 2025 16:39:24 +0900</pubDate>
    </item>
  </channel>
</rss>