<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>dev.ellug</title>
    <link>https://ellug.tistory.com/</link>
    <description>Web / App / Game Dev</description>
    <language>ko</language>
    <pubDate>Mon, 13 Jul 2026 18:43:05 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>ellug</managingEditor>
    <image>
      <title>dev.ellug</title>
      <url>https://tistory1.daumcdn.net/tistory/8159409/attach/8d302dc591214c4999709b53b40ee772</url>
      <link>https://ellug.tistory.com</link>
    </image>
    <item>
      <title>디벨로켓 Unity 게임 클라이언트 개발자 8개월 과정 후기</title>
      <link>https://ellug.tistory.com/96</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;배경을 이야기 하자면 나는 비전공자 웹/앱 프론트엔드 개발자 출신이고, 게임 개발자로 커리어 전환을 준비하던 중 디벨로켓의 온라인 부트캠프 과정을 발견해 지원하게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에 필요한 커리큘럼들 위주로 갖추어져 있었고, 무엇보다 온라인이라는 부분이 매력적이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 경우에는 이미 C#을 제법 학습해둔 상태였기에 기초부터 시작하는 건 원치 않았지만, 그렇다고 다른 과정이라고 해서 개강, 수료 더 빨리 끝나거나 메리트 있는게 아니었기 때문에 심사숙고 끝에 디벨로켓으로 빠르게 결정했다.&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커리큘럼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 모든 학습은 강의 &amp;rarr; 과제 구조로 이루어진다.&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;커리큘럼에 대해서는 크게 C#, Unity, 네트워크, 프로젝트 로 나눠서 보면 될 거 같다.&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;h3 data-ke-size=&quot;size23&quot;&gt;C#&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C# 언어와 간단한 CS 지식, 알고리즘, 자료구조 등을 학습한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 이 부분은 거의 다 알고있는 내용 위주라서 복습하는 마음으로 임했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 잘못 알고 있거나, 대충 넘겨짚어 알고 있는 부분들을 교정할 수 있는 기회가 되었기에 생각했던 것 보다 유효한 학습이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 인원이 객체지향, 제네릭이 등장하면서부터 꺾이는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실히 처음 보면 저게 뭔 소린가 싶긴 한데...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 여기서 꺾이면 개발자 못 한다. 아직 기초다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C#이 기초라고 해도 알파이자 오메가이기도 하다.&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;h3 data-ke-size=&quot;size23&quot;&gt;Unity&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유니티 기초도 멘토와 다른 인터넷 강의를 통해 다소 학습을 한 상태였지만 여기서부터 제법 어려움을 겪게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유니티 파트는 5와 6를 모두 진행 된다.&lt;br /&gt;(아마 다음 강의는 6만 진행할지도 모른다. 더 이상 6.4 이상부터는 빌트인을 지원하지 않는다는 오피셜로 인해...)&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 사용법에서 시작해서 네비매시, 타일맵, 시네머신, 타임라인 등 실무에서 사용될 법한 기술 위주로 학습을 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 인원이 고통을 호소하기 시작했지만, 반대로 제대로 게임을 만들기 시작하기 때문에 재밌어지는 시기다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간 중간 실무 팁도 많이 알려주시며, 최적화 관련된 내용은 필시 기억해두고 이 시기부터 준비해두는 게 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 취업 포트폴리오에서 구현 보다는 설계, 최적화, 문제해결 이런 키워드 쪽이 가치가 훨씬 높다.&lt;/p&gt;
&lt;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;h3 data-ke-size=&quot;size23&quot;&gt;네트워크&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 자체는 어렵지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Firebase 같은 서버리스는 웹 개발자를 할 때도 많이 다뤄봤기 때문에 아주 자신있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 Photon 인데... 이것도 이론 자체는 어렵지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로 프로젝트에 적용하다 보면 예상치 못한 문제가 많이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크의 불확실성이라고 하는데 처음 겪으면 제법 헤매게 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 하던 설계에서 한 차원 더 설계가 복잡해지는 것이기에 머리도 조금 아프다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 가장 재밌게 한 프로젝트가 네트워크 였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;온라인 학습&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온라인 과정이라는 부분은 나한테 아주 메리트가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 나는 장거리 이동을 매우 싫어하고 그 시간과 에너지도 전부 비용이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 온라인 과정으로 비대면 학습을 진행한다면 그 에너지를 전부 학습에 투자할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전력으로 공부에 임할 생각이 있다면 아주 유리한 조건이라 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 자신이 누군가가 잡아줘야하고, 쉽게 나태해지거나 의지가 약하거나 가벼운 마음으로 시작했다면 비추한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 수업 중 다른 짓을 하거나 부정한 행위(?)를 하다가 제적되는 학생들을 몇 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제적되면 다행이지 어떻게든 살아남아서 몰래 숨어서 다른 짓 하는 학생도 간혹 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 학생들 때문에 진짜 공부하고자 하는 사람들이 손해보지 않았으면 좋겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 스스로 통제해야하기에 자신 나름대로 규칙을 세우는게 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 나는 오랜 프리랜서 생활과 재택근무 경험이 있어 쉽게 적응할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 절대로 침실과 작업실은 분리해야 하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일과를 전부 학습에 투자한다는 원칙 하에 세부적인 규칙을 자신에 맞게 구성하고 지키는게 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;콘솔 프로젝트&lt;/li&gt;
&lt;li&gt;유니티5 프로젝트 (팀)&lt;/li&gt;
&lt;li&gt;2D 개인 프로젝트&lt;/li&gt;
&lt;li&gt;기획 사전합반 프로젝트 (팀)&lt;/li&gt;
&lt;li&gt;네트워크 프로젝트 (팀)&lt;/li&gt;
&lt;li&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;이렇게 6번의 프로젝트로 구성되어있다.&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;s&gt;리드 프로그래머라는 말이 다소 허세스럽긴 하지만 간결하게 역할을 설명하기에 딱이었던 것 같다.&lt;/s&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;콘솔 프로젝트는 말 그대로 터미널상에서 C# 스크립트를 이용해 게임처럼 보이게 만드는 간단한 프로젝트다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 1주일 이내로 진행되는데 나는 여기서부터 욕심을 부렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도적으로 많은 객체를 다루는 게임을 기획하여 부하상태를 유도하고 그 부하상태를 해결하는 방향으로 프로젝트 자체를 학습과 증명의 장으로 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지 모든 프로젝트에서도 동일한 시각으로 접근했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 저냥 돌아가는 대충 만든 프로젝트가 아니라 포트폴리오에 한 줄이라도 쓸 수 있는 내용을 만들고자 고민했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;취업 준비&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기업협약 프로젝트가 끝나면 취업 준비 시즌에 돌입한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 난 이 기간을 기다리지 않고 기업협약 프로젝트 기간 비는 시간을 활용해 강사님들을 독점하면서 이력서, 자기소개서, 포트폴리오 준비를 선제적으로 해결했다.&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;취업 시즌에 돌입하면 다들 이력서, 자기소개서 부터 시작하기 바쁘다.&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;프로젝트 하면서 그럴 시간이 어떻게 있냐고 한다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 아침부터 새벽2시까지 전부 사용하고 수면은 6시간 이내로, 나머지 아무것도 안 하고 개발 관련에만 투자하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별로 이상할 거 없다.&lt;/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;이후 취업지원팀에서도 봐주시지만, 이미 거의 문서를 완성해 놓은 상태라 문서적으로는 크게 피드백 받을 게 없었다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;총평&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난 개인적으로 강사, 교수 이런 사람들을 신뢰하지 못하는 편이라 디벨로켓에 올 때도 의심의 날을 세우고 강사가 부족하면 내가 알아서 내 몫을 챙기겠단 마인드로 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;내심 &quot;이런 건 바로 대답 못하고 같이 RnD 해봐야할지도 모르겠다&quot;라고 생각하며 질문했던 내용에 대해서도 빠르고 정확하게 해결책이나 방향성을 제시해주시기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면서도 하나의 답에 매몰되지 않도록 직접 고민할 기회를 주시는 것도 좋은 부분이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획 합반 프로젝트 과정에서 이런저런 문제가 발생하긴 하지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획반 또한 배우고 있는 학생이라는 점과 실제로 현업에 가서도 비슷한 문제, 혹은 더 심각한 문제도 발생한다는 걸 고려하면 이런 트러블 조차 전부 경험이며 그 해결 과정 또한 포트폴리오라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;대부분 C# 언어 학습 단계에서 꺾이고, 결국 과정 막바지가 되어서도 제대로 된 코드 하나 작성하지 못하는 경우도 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심지어 AI를 쓰는데도 말이다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 열심히 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게임 개발은 재미로 하는 블럭쌓기가 아니라 엔지니어링이라는 걸 명심해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시장 상황도 좋지 않고 전망도 어떻게 될지 몰라 무조건적으로 추천할 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 취업률 몇% 이런 건 어느 학원이든 대학이든 절대 믿으면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발업계는 지금 현업자 경력자들도 이직이 쉽지 않은 불지옥 상태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 다른 업계도 쉽지 않은 상태긴 하지만 내가 개발업계 종사자다 보니 더 피부로 느끼는 바는 이쪽이 심하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;유니티까지 사전학습 할 필요는 없고 최소한 C# 혹은 C계열 언어들을 중급 이상으로 학습한 뒤에 시작한다면 비교적 수월하게 진행할 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;소정의 경품을 제공받아 작성된 후기입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 빈말은 안 했읍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ETC</category>
      <category>8개월국비지원</category>
      <category>IT취업</category>
      <category>경일게임아카데미</category>
      <category>디벨로켓</category>
      <category>온라인교육</category>
      <category>온라인부트캠프</category>
      <category>취업</category>
      <author>ellug</author>
      <guid isPermaLink="true">https://ellug.tistory.com/96</guid>
      <comments>https://ellug.tistory.com/96#entry96comment</comments>
      <pubDate>Mon, 20 Apr 2026 20:48:46 +0900</pubDate>
    </item>
    <item>
      <title>Node.js : GameJob Notifier - Firebase Function &amp;amp; Discord WebHook</title>
      <link>https://ellug.tistory.com/95</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 GameJob 공고를 놓치지 않기 위해 C# WPF 데스크톱 앱(트레이 상주 + 알림)으로 운영했다.&lt;br /&gt;기능 자체는 잘 동작했지만, 실제 사용에서는 몇 가지 불편이 분명했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;알림을 받으려면 앱이 켜진 PC가 항상 켜져 있어야 함&lt;/li&gt;
&lt;li&gt;외부에서 모바일로 확인하기가 번거로움&lt;/li&gt;
&lt;li&gt;OS/실행 환경에 의존적인 관리 포인트가 늘어남&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &amp;ldquo;앱을 켜두지 않아도&amp;rdquo;, &amp;ldquo;데스크톱/모바일 어디서나&amp;rdquo; 알림을 받을 수 있는 방식으로 구조를 바꿨다.&lt;br /&gt;핵심 아이디어는 서버리스 폴링 + Discord Webhook 조합이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Firebase Functions(2nd gen)가 5분마다 자동 실행&lt;/li&gt;
&lt;li&gt;Firestore에 이전 상태를 저장해 변경 감지&lt;/li&gt;
&lt;li&gt;신규 공고(Added)만 Discord로 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 클라이언트 앱 없이도, Discord만 켜져 있으면 동일한 알림 경험을 유지할 수 있게 됐다.&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용 기술 스택&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Runtime/Language: Node.js 20 + TypeScript&lt;/li&gt;
&lt;li&gt;Serverless: Firebase Functions v2 (&lt;code&gt;onSchedule&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Database: Firestore&lt;/li&gt;
&lt;li&gt;Crawling/Parsing: HTTP Fetch + Cheerio&lt;/li&gt;
&lt;li&gt;Notification: Discord Webhook&lt;/li&gt;
&lt;li&gt;Secret: Firebase Secret Manager&lt;/li&gt;
&lt;li&gt;Region/Cost Config: &lt;code&gt;asia-northeast3&lt;/code&gt;, &lt;code&gt;256MiB&lt;/code&gt;, &lt;code&gt;timeoutSeconds=60&lt;/code&gt;, &lt;code&gt;maxInstances=1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 기술 포인트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 상태 비교 기반 변경 감지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트도 단순 목록 수집이 아니라, &amp;ldquo;이전 상태와 현재 상태의 비교&amp;rdquo;를 중심으로 설계했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Added: 이번 실행에서 처음 발견된 공고&lt;/li&gt;
&lt;li&gt;Hidden: 이전에는 보였지만 이번엔 사라진 공고&lt;/li&gt;
&lt;li&gt;Restored: 숨김 상태였다가 다시 노출된 공고&lt;/li&gt;
&lt;/ul&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;h3 data-ke-size=&quot;size23&quot;&gt;2. 스코프 분리(필터/URL 기반 해시)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 사이트라도 필터가 다르면 결과 집합이 완전히 달라진다.&lt;br /&gt;그래서 URL + 필터 조합을 해시해 &lt;code&gt;scopeKey&lt;/code&gt;를 만들고, 스코프 단위로 상태를 분리 저장했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;jobScopes/{scopeKey}&lt;/code&gt;: 스코프 메타&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jobScopes/{scopeKey}/jobs/{jobId}&lt;/code&gt;: 공고 상태&lt;/li&gt;
&lt;/ul&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;h3 data-ke-size=&quot;size23&quot;&gt;3. 알림 노이즈 제어 + 중복 방지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림 정책은 &amp;ldquo;Added만 전송&amp;rdquo;으로 단순화했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Hidden/Restored는 저장만 하고 알림은 보내지 않음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;notificationLedger&lt;/code&gt;로 idempotency key를 관리해 중복 전송 차단&lt;/li&gt;
&lt;li&gt;동일 공고에 대해 재실행/재시도 상황에서도 한 번만 전송되도록 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 운영성 중심 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버리스는 실패를 전제로 운영해야 한다. 그래서 아래 항목을 기본으로 넣었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스케줄 중복 실행 방지 락(&lt;code&gt;pollLocks&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;HTTP/Discord 전송 재시도(간단 백오프)&lt;/li&gt;
&lt;li&gt;구조화 로그(&lt;code&gt;fetched&lt;/code&gt;, &lt;code&gt;matched&lt;/code&gt;, &lt;code&gt;added&lt;/code&gt;, &lt;code&gt;hidden&lt;/code&gt;, &lt;code&gt;restored&lt;/code&gt;, &lt;code&gt;notified&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;비정상 종료 시에도 다음 주기에 복구 가능한 TTL 기반 락/상태 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현 방법 요약&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Cloud Scheduler가 5분마다 &lt;code&gt;pollGameJobs&lt;/code&gt; 실행&lt;/li&gt;
&lt;li&gt;스코프 락 획득 후 목록 HTML 수집&lt;/li&gt;
&lt;li&gt;Cheerio 파싱으로 공고 모델 정규화&lt;/li&gt;
&lt;li&gt;필터(직종/경력/조건) 매칭&lt;/li&gt;
&lt;li&gt;Firestore 기존 상태와 diff 계산(Added/Hidden/Restored)&lt;/li&gt;
&lt;li&gt;상태 upsert 저장&lt;/li&gt;
&lt;li&gt;Added만 idempotency 선점 후 Discord 전송&lt;/li&gt;
&lt;li&gt;실행 메트릭/로그 기록 후 락 해제&lt;/li&gt;
&lt;/ol&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;알림 연결 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Discord Webhook&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Discord 채널 Webhook URL 발급&lt;/li&gt;
&lt;li&gt;Secret Manager에 &lt;code&gt;DISCORD_WEBHOOK_URL&lt;/code&gt; 저장&lt;/li&gt;
&lt;li&gt;Functions에서 &lt;code&gt;defineSecret()&lt;/code&gt;으로 주입 받아 전송&lt;/li&gt;
&lt;/ul&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;h3 data-ke-size=&quot;size23&quot;&gt;2. 메시지 포맷&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신규 공고 알림은 아래 형식으로 고정했다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;[신규 공고] 제목
회사: ...
경력: ...
지역: ...
링크: ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모바일에서도 빠르게 읽히는 형태를 우선했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 실패 처리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전송 실패 시 에러 로그 + ledger 상태 &lt;code&gt;error&lt;/code&gt;로 기록&lt;/li&gt;
&lt;li&gt;다음 실행에서 stale claim 기준으로 재시도 가능&lt;/li&gt;
&lt;li&gt;함수가 실패해도 전체 상태 일관성은 유지되도록 분리 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배포/운영 포인트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 배포 구성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Firebase Functions 2nd gen, 리전 &lt;code&gt;asia-northeast3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;스케줄: &lt;code&gt;every 5 minutes&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;리소스 제한: &lt;code&gt;256MiB&lt;/code&gt;, &lt;code&gt;60s&lt;/code&gt;, &lt;code&gt;maxInstances=1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&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;h3 data-ke-size=&quot;size23&quot;&gt;2. 비용 최적화&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;불필요한 고사양 인스턴스 사용 억제&lt;/li&gt;
&lt;li&gt;Added 중심 알림 정책으로 외부 호출 최소화&lt;/li&gt;
&lt;li&gt;Ledger TTL/인덱스 최적화로 Firestore 저장/인덱스 비용 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 관측성과 트러블슈팅&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스코프 시작/완료 로그로 실행 흐름 추적&lt;/li&gt;
&lt;li&gt;필터 실패 샘플 로그(&lt;code&gt;duty_mismatch&lt;/code&gt;, &lt;code&gt;career_mismatch&lt;/code&gt;) 제공&lt;/li&gt;
&lt;li&gt;운영 중 파싱 구조 변경 시에도 로그 기반으로 빠르게 원인 파악 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;학습 포인트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 전환에서 가장 크게 얻은 포인트는 아래 4가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데스크톱 자동화 문제도 서버리스 이벤트 파이프라인으로 단순화할 수 있다.&lt;/li&gt;
&lt;li&gt;스크래핑 품질은 &amp;ldquo;파싱 정확도&amp;rdquo;보다 &amp;ldquo;상태 모델 + diff 설계&amp;rdquo;가 더 중요하다.&lt;/li&gt;
&lt;li&gt;알림 시스템은 전송 성공보다 중복 방지와 노이즈 제어가 핵심이다.&lt;/li&gt;
&lt;li&gt;운영 환경에서는 기능 구현보다 로그/복구/비용 설계가 장기 품질을 좌우한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 기존 WPF 알림 앱의 한계를 실제 사용 관점에서 다시 정의하고,&lt;br /&gt;클라이언트 의존형 구조를 서버리스 이벤트 구조로 치환한 작업이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &amp;ldquo;수집&amp;rdquo;이 아니라 &amp;ldquo;판단(변화 감지)&amp;rdquo;이고,&lt;br /&gt;완성도는 &amp;ldquo;알림 전송&amp;rdquo;이 아니라 &amp;ldquo;운영 가능한 구조&amp;rdquo;에서 나온다는 점을 체감했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계에서는 다중 스코프 운영, 알림 채널 다변화(예: Slack/Email), 대시보드화까지 확장할 수 있다.&lt;/p&gt;</description>
      <category>Javascript (React, Next)</category>
      <category>discord</category>
      <category>firebase</category>
      <category>function</category>
      <category>JavaScript</category>
      <category>JS</category>
      <category>node</category>
      <category>node.js</category>
      <category>TS</category>
      <category>TypeScript</category>
      <category>webhook</category>
      <author>ellug</author>
      <guid isPermaLink="true">https://ellug.tistory.com/95</guid>
      <comments>https://ellug.tistory.com/95#entry95comment</comments>
      <pubDate>Fri, 17 Apr 2026 15:59:24 +0900</pubDate>
    </item>
    <item>
      <title>C# : GameJob 신규 공고 알람 앱 - WPF(MVVM), HttpClient, SQLite, Windows Toast</title>
      <link>https://ellug.tistory.com/94</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Ellug/GameJobNotifier&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Ellug/GameJobNotifier&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;837&quot; data-origin-height=&quot;687&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcxnT3/dJMcabcKfd2/3TIAZvTF4poymjicOyV6X0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcxnT3/dJMcabcKfd2/3TIAZvTF4poymjicOyV6X0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcxnT3/dJMcabcKfd2/3TIAZvTF4poymjicOyV6X0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcxnT3%2FdJMcabcKfd2%2F3TIAZvTF4poymjicOyV6X0%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;374&quot; height=&quot;307&quot; data-origin-width=&quot;837&quot; data-origin-height=&quot;687&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;h2 data-ke-size=&quot;size26&quot;&gt;1. 프로젝트 배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채용 사이트를 수시로 확인하는 반복 작업을 줄이기 위해, GameJob 공고를 주기적으로 수집하고 &lt;b&gt;새로운 공고가 등록되면 즉시 알림&lt;/b&gt;을 주는 데스크톱 앱을 만들었다.&lt;br /&gt;단순히 &amp;ldquo;목록 보여주기&amp;rdquo;가 아니라, 로컬에 상태를 저장하고 이전 상태와 비교해 &lt;b&gt;변화(신규/비노출/복원)&lt;/b&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 사용 기술 스택&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Language: &lt;code&gt;C#&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Runtime: &lt;code&gt;.NET 10 (net10.0-windows)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;UI: &lt;code&gt;WPF + MVVM (CommunityToolkit.Mvvm)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Hosting/DI: &lt;code&gt;Microsoft.Extensions.Hosting&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Data: &lt;code&gt;SQLite (Microsoft.Data.Sqlite)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Crawling: &lt;code&gt;HttpClient + HtmlAgilityPack&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Notification:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Windows Toast 알림 (PowerShell + WinRT 호출)&lt;/li&gt;
&lt;li&gt;Tray Balloon 알림 (&lt;code&gt;NotifyIcon&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 핵심 기술 포인트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 상태 비교 기반 변경 감지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &amp;ldquo;현재 목록&amp;rdquo;만 보는 게 아니라, 이전 실행에서 저장한 데이터와 비교하는 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Added&lt;/code&gt;: 새로 발견된 공고&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Hidden&lt;/code&gt;: 이전에 보였는데 이번엔 사라진 공고&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Restored&lt;/code&gt;: 숨김 상태였다가 다시 노출된 공고&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식 덕분에 단순 스크래핑을 넘어 &amp;ldquo;변화 이벤트&amp;rdquo;를 만들 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. 스코프 분리(필터 조합 해시)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터 조합(직종/지역/조건 등)이 바뀌면 결과 집합도 달라진다.&lt;br /&gt;그래서 &lt;code&gt;URL + 필터 조합&lt;/code&gt;을 해시해 &lt;code&gt;source scope key&lt;/code&gt;를 만들고, 스코프 단위로 상태를 추적해 데이터 섞임을 방지했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. 알림 노이즈 제어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 실행 때 기존 공고를 전부 알림으로 보내면 노이즈가 너무 크다.&lt;br /&gt;&lt;code&gt;마지막 성공 검사 시각&lt;/code&gt;과 현재 시각 사이에 등록된 항목만 신규 알림 대상으로 제한해 신뢰도를 높였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-4. 데스크톱에서도 Host/DI 구조 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WPF에서도 &lt;code&gt;Host + DI + HostedService&lt;/code&gt;를 적용해 UI와 도메인 로직을 분리했다.&lt;br /&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 구현 방법 요약&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;대상 URL 수집&lt;/li&gt;
&lt;li&gt;HTML 파싱 후 공고 모델 정규화&lt;/li&gt;
&lt;li&gt;설정된 필터 적용&lt;/li&gt;
&lt;li&gt;SQLite upsert&lt;/li&gt;
&lt;li&gt;이전 상태와 diff 계산&lt;/li&gt;
&lt;li&gt;변경 이벤트 생성&lt;/li&gt;
&lt;li&gt;알림 전송&lt;/li&gt;
&lt;li&gt;마지막 실행 상태 저장&lt;/li&gt;
&lt;/ol&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 알림 연결 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-1. Toast 알림&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Toast XML 생성&lt;/li&gt;
&lt;li&gt;PowerShell에서 WinRT Toast API 호출&lt;/li&gt;
&lt;li&gt;알림 액션에 &lt;code&gt;공고 열기(URL)&lt;/code&gt; 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-2. Tray Balloon 알림&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;NotifyIcon.ShowBalloonTip()&lt;/code&gt; 사용&lt;/li&gt;
&lt;li&gt;풍선 클릭 시 해당 공고 URL 오픈&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-3. 사용자 설정 연동&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Toast On/Off&lt;/li&gt;
&lt;li&gt;Tray Balloon On/Off&lt;/li&gt;
&lt;li&gt;둘 다 꺼진 경우 테스트 알림 차단 및 안내 메시지 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 백그라운드/부팅 연동&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-1. 트레이 상주 동작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;창 닫기/최소화를 &amp;ldquo;종료&amp;rdquo;가 아닌 &amp;ldquo;숨김&amp;rdquo;으로 처리해 백그라운드 상주를 유지했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-2. 윈도우 시작 시 자동 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;HKCU\Software\Microsoft\Windows\CurrentVersion\Run&lt;/code&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 학습 포인트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서 특히 도움이 됐던 학습 포인트는 아래 4가지다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데스크톱 앱에서도 서버형 아키텍처(Host/DI/BackgroundService)를 충분히 적용할 수 있다.&lt;/li&gt;
&lt;li&gt;스크래핑은 &amp;ldquo;수집&amp;rdquo;보다 &amp;ldquo;변화 감지 모델&amp;rdquo; 설계가 더 중요하다.&lt;/li&gt;
&lt;li&gt;알림 시스템은 기능 구현보다 노이즈 제어와 사용자 경험이 핵심이다.&lt;/li&gt;
&lt;li&gt;로컬 앱이라도 상태 저장소(SQLite + runtime state) 설계가 품질을 좌우한다.&lt;/li&gt;
&lt;/ol&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 작은 토이 프로젝트처럼 시작했지만, 실제로는 &lt;b&gt;수집 &amp;rarr; 판단 &amp;rarr; 이벤트 &amp;rarr; 통지&lt;/b&gt;의 흐름을 끝까지 구현한 형태다.&lt;br /&gt;데이터 신뢰성과 운영성(백그라운드, 부팅 연동, 알림 품질)에 집중하면서 구조를 정리한 경험이 가장 큰 수확이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>C#</category>
      <category>C#</category>
      <category>GameJob</category>
      <category>HttpClient</category>
      <category>SQLite</category>
      <category>WPF</category>
      <category>공고</category>
      <category>알람</category>
      <category>앱</category>
      <category>토이 프로젝트</category>
      <author>ellug</author>
      <guid isPermaLink="true">https://ellug.tistory.com/94</guid>
      <comments>https://ellug.tistory.com/94#entry94comment</comments>
      <pubDate>Tue, 14 Apr 2026 15:11:27 +0900</pubDate>
    </item>
    <item>
      <title>C# : Chzzk(치지직) VOD / Clip 다운로더</title>
      <link>https://ellug.tistory.com/93</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;CHZZK VOD/CLIP 다운로더 개발기 (C# WinForms)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;치지직 VOD와 CLIP을 로컬로 저장하고, VOD 구간 채팅까지 함께 보관하는 도구를 개발했다.&lt;br /&gt;목표는 단순 다운로드 기능이 아니라, &lt;b&gt;응답 구조 변경에 덜 깨지는 안정성&lt;/b&gt;과 &lt;b&gt;유지보수 가능한 코드 구조&lt;/b&gt;를 확보하는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇년전 트위치에서는 다시보기 영상을 구간 다운로드, 클립 다운, 영상 채팅 로그 수집 등이 가능한 앱들이 있었다.&lt;/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;그래서 다음 기능을 하나의 앱으로 통합했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;VOD/CLIP 모드 전환&lt;/li&gt;
&lt;li&gt;해상도 선택 + 구간 다운로드&lt;/li&gt;
&lt;li&gt;VOD 채팅 저장(JSON/SRT/TXT)&lt;/li&gt;
&lt;li&gt;ffmpeg 기반 미디어 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 기술&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) API 복원력 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ChzzkApiClient&lt;/code&gt;를 단일 호출 구조로 두지 않았다.&lt;br /&gt;CLIP 조회는 여러 경로를 순차 시도하는 fallback 체인으로 구성했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;service clips&lt;/code&gt; 계열 호출&lt;/li&gt;
&lt;li&gt;&lt;code&gt;play-info/clip&lt;/code&gt; 호출&lt;/li&gt;
&lt;li&gt;페이지/임베드 파싱&lt;/li&gt;
&lt;li&gt;프로브 엔드포인트 탐색&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &quot;하나가 실패해도 다음 경로로 복구&quot;하는 구조다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) ffmpeg 인자 분기 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 입력 타입과 무관하게 동일 옵션을 넣어 오류가 발생했다.&lt;br /&gt;HLS(&lt;code&gt;.m3u8&lt;/code&gt;)와 direct mp4 입력을 분리해 인자를 다르게 적용했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HLS 입력: &lt;code&gt;allowed_extensions&lt;/code&gt;, &lt;code&gt;extension_picky&lt;/code&gt;, &lt;code&gt;protocol_whitelist&lt;/code&gt; 적용&lt;/li&gt;
&lt;li&gt;direct mp4 입력: HLS 전용 옵션 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 CLIP/VOD 모두 안정적으로 처리되도록 정리했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) UI-도메인-인프라 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WinForms 특성상 한 파일에 로직이 몰리기 쉽다.&lt;br /&gt;유지보수를 위해 화면 코드를 역할별 partial로 분리했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;MainForm.Layout.cs&lt;/code&gt;: 배치/레이아웃&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MainForm.Actions.cs&lt;/code&gt;: 이벤트/동작&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MainForm.cs&lt;/code&gt;: 상태/초기화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 계층은 &lt;code&gt;VideoDownloadService&lt;/code&gt;로 묶고, 외부 연동은 &lt;code&gt;Infrastructure&lt;/code&gt;로 분리했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발 결과&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다운로드 모델 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 VOD 중심 모델을 CLIP에도 적용하려 했으나 실패했다.&lt;br /&gt;리팩토링 후 &lt;code&gt;DownloadRequest&lt;/code&gt;를 &lt;code&gt;StreamUrl&lt;/code&gt; 중심으로 단순화해 VOD/CLIP 공통 경로를 만들었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;VOD: 해상도 조회, 구간 다운로드, 오디오 추출 동작&lt;/li&gt;
&lt;li&gt;CLIP: URL 기반 메타 조회 및 다운로드 동작&lt;/li&gt;
&lt;li&gt;채팅: 구간 기준 JSON/SRT/TXT 저장 동작&lt;/li&gt;
&lt;li&gt;UI: 모드 전환과 작업 로그 흐름 안정화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실사용 기준으로 주요 흐름은 정상 동작하는 상태를 확보했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한계와 다음 단계&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ffmpeg는 외부 의존이라 배포 환경에서 PATH 이슈가 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;API 응답 포맷 변경은 계속 발생할 수 있어 fallback 경로를 주기적으로 보완해야 한다.&lt;/li&gt;
&lt;li&gt;대용량 VOD 처리 시 재시도/네트워크 복구 정책을 더 강화할 여지가 있다.&lt;/li&gt;
&lt;/ul&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;883&quot; data-origin-height=&quot;708&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n4dOi/dJMcadnL7yA/tEWp3I79JIhPmWiz6koWGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n4dOi/dJMcadnL7yA/tEWp3I79JIhPmWiz6koWGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n4dOi/dJMcadnL7yA/tEWp3I79JIhPmWiz6koWGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn4dOi%2FdJMcadnL7yA%2FtEWp3I79JIhPmWiz6koWGK%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;883&quot; height=&quot;708&quot; data-origin-width=&quot;883&quot; data-origin-height=&quot;708&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub 링크 : &lt;a href=&quot;https://github.com/Ellug/ChzzkLiveDownloader&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Ellug/ChzzkLiveDownloader&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>C#</category>
      <category>C#</category>
      <category>CHZZK</category>
      <category>Chzzk Download</category>
      <category>clip</category>
      <category>WinForm</category>
      <category>채팅 로그</category>
      <category>치지직</category>
      <category>치지직 다운로더</category>
      <category>치지직 다운로드</category>
      <category>클립</category>
      <author>ellug</author>
      <guid isPermaLink="true">https://ellug.tistory.com/93</guid>
      <comments>https://ellug.tistory.com/93#entry93comment</comments>
      <pubDate>Fri, 20 Mar 2026 15:46:18 +0900</pubDate>
    </item>
    <item>
      <title>Unity : 마샬링(Marshalling) 과 메모리 풀링(Memory Pooling)</title>
      <link>https://ellug.tistory.com/92</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;마샬링(Marshalling) 이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C#과 같은 Managed 환경은 가비지 컬렉터(GC)가 메모리를 관리하며, 데이터가 메모리에 배치되는 방식의 런타임에 의해 유동적으로 결정될 수 있다. 반면 C/C++ 같은 Unmanaged 환경은 개발자가 직접 메모리를 제어하며, 메모리 레이아웃이 고정되어 있다.&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;예를 들어 C#의 string 타입은 내부적으로 UTF-16 인코딩을 사용하는 객체지만, C++ 에서는 const char* (UTF-8 또는 ASCII) 형태의 포인터 배열로 돼있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 C#의 문자열을 C++이 이해할 수 있는 메모리 구조로 복사하고 변환해 주는 작업이 마샬링이다.&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;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Blittable 타입과 Non-Blittable 타입&lt;/h2&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;h3 data-ke-size=&quot;size23&quot;&gt;Blittable 타입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Managed 와 Unmanaged 양쪽에서 메모리 표현 방식이 완전히 동일한 타입. 변환 과정 없이 메모리 복사(Bit-block transfer)만으로 전달되므로 오버헤드가 거의 없다. (byte, sbyte, short, ushort, int, uint, long, ulong, float, double, IntPtr 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Non-Blittable 타입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양쪽의 메모리 표현 방식이 달라 마샬러(Marshaler)가 개입해 데이터를 변환해야 하는 타입으로 오버헤드가 발생한다. (bool, stirng, object, 클래스, 가변 배열 등)&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;참고로 C#의 bool은 4바이트를 차지하지만 C++의 bool은 일반적으로 1바이트다.&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;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Photon Fusion 에서의 실전 활용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유니티 멀티플레이어 개발에서 널리 사용되는 Photon Fusion 같은 네트워크 엔진을 사용할 때, 이 Blittable/Non-Blittable 개념을 활용해 메모리 최적화가 가능하다.&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;Fusion은 완벽한 클라이언트 예측과 롤백을 위해 매 틱마다 게임의 전체 상태를 저장하고 복원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 이 상태 데이터가 C#의 매니지드 힙(Managed Heap)에 파편화되어 있다면, 매 틱마다 데이터를 직렬화하고 마샬링하는 데 엄청난 CPU 연산과 GC가 발생할 것이다.&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;따라서 Fusion은 상태 데이터를 통째로 언매니지드 메모리*Unmanaged Memory) 버퍼에 연속적으로 배치하여, 마샬링 오버헤드 없이 Memcpy (메모리 블록 복사) 만으로 스냅샷을 찍고 롤백하는 방식을 취한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Case 1 : 지옥의 string 마샬링 피하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅 메시지나 플레이어의 닉네임을 동기화해야 한다고 가정해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Non-Blittable 타입인 string을 아무 생각 없이 사용했다간 네트워크 버퍼에 C# string을 밀어 넣는 과정에서 인코딩 변환과 힙 메모리 할당(마샬링)이 발생한다.&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;이 때 고정 크기 버퍼(Fixed Buffer) 을 사용하는 것으로 최적화 전략을 취할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 틱마다 갱신될 수 있는 핫 패스(Hot path)에서 순수 string의 사용은 쥐약이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Fusion 에서는 이를 해결하기 위해 NetworkString&amp;lt;_16&amp;gt;, NetworkString&amp;lt;_32&amp;gt; 같은 전용 타입을 제공한다.&lt;/p&gt;
&lt;pre id=&quot;code_1772445277565&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 최악의 접근: 매 틱마다 GC 할당 및 마샬링 오버헤드 발생
[Networked] public string PlayerName { get =&amp;gt; default; set; } 

// 최적화된 접근: 32바이트 언매니지드 메모리 공간을 직접 차지함 (Blittable)
[Networked] public NetworkString&amp;lt;_32&amp;gt; PlayerName { get =&amp;gt; default; set; }&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;NetworkString은 내부적으로 C#의 fixed 키워드와 unsafe 구조체를 활용하여, 포인터 기반의 고정 크기 바이트 배열로 문자를 처리한다. 즉, Non-Blittable인 문자열을 강제로 Blittable한 구조체로 포장하여 마샬링 비용을 0으로 만든 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Case 2 : bool 대신 비트 마스킹(Bit-masking)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C#의 bool은 4바이트를 차지하는 Non-Blittable 타입이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 캐릭터의 상태를 동기화하기 위해 bool 변수를 여러개 선언하면, 메모리 낭비는 물론 Unmanaged 버퍼로 넘어갈 때 오버헤드가 누적된다.&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;이 때 상태를 하나의 Blittable 타입(int, byte)으로 압축 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bool 대신에 Blittable 타입인 byte나 int 하나를 선언하고, 비트 연산자를 이용해 여러 상태를 플래그(Flag)로 관리하면 마샬링 오버헤드 없이 깔끔하게 메모리가 복사된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1772447033614&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 비효율적인 상태 동기화 (마샬링 대상이며 메모리 낭비 심함)
[Networked] public NetworkBool IsJumping { get; set; }
[Networked] public NetworkBool IsAttacking { get; set; }
[Networked] public NetworkBool IsCrouching { get; set; }

// 최적화된 접근: 1개의 int(4바이트)에 32가지 상태를 Blittable하게 압축
[Networked] public int PlayerStateFlags { get; set; }

// 비트 연산을 통한 상태 제어 (마샬링 없음, Bit-block transfer 보장)
public void SetJumping(bool state) {
    if (state)
    	PlayerStateFlags |= (1 &amp;lt;&amp;lt; 0);
    else
    	PlayerStateFlags &amp;amp;= ~(1 &amp;lt;&amp;lt; 0);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Case 3 : 가변 데이터 통신을 위한 메모리 풀링 (Memory Pooling)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 언급한 NetworkString&amp;lt;_16&amp;gt; 이나 int 비트 마스킹은 데이터의 크기가 고정되어 있을 때 사용하는 고정 크기 버퍼(Fixed Buffer) 방식이다. 구조체 내부에 메모리를 인라인으로 박아 넣어 힙 할당 자체를 없애는 것이다.&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;하지만 네트워크 통신이나 C++ 플러그인과 데이터를 주고받다 보면, 크기가 매번 변하는 가변 배열을 마샬링 하는 경우가 생긴다.&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;이때 매 프레임 new byte[size]를 호출해 배열을 생성하면 GC 부하가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하는 방법이 바로 메모리 풀링(Memory Pooling) 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 할당은 하드에 읽기 쓰기를 빼면 가장 느린 작업 중 하나이기에 메모리 할당 자체를 줄이는 게 좋다는 발상으로 근본적으로 오브젝트 풀링과 같은 발상이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체의 논리적 껍데기를 재사용하는 오브젝트 풀링과 달리, 메모리 풀링은 거대한 순수 메모리 덩어리 (바이트 배열이나 언매니지드 포인터 공간)를 하나 통쨰로 빌려두고, 필요한 만큼 잘라서 쓰고 반납하는 기법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C# 최신 버전과 유니티에서는 이를 위해 System.Buffers.ArrayPool&amp;lt;T&amp;gt; 를 기본적으로 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 Managed 영역에서 발생하는 배열 할당을 극단적으로 줄여주며, 이를 바탕으로 Marshal.UnsafeAddrOfPinnedArrayElement나 Fixed 구문을 조합해 마샬링 오버헤드를 최소화할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1772448572276&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using System.Buffers;
using System.Runtime.InteropServices;

// 최악의 접근: 매번 새로운 배열을 생성하여 GC 폭발 및 마샬링 오버헤드 발생
void SendVariableData_Bad(int requiredSize) 
{
    byte[] buffer = new byte[requiredSize]; // 매 프레임 GC 할당
    NativePlugin.Send(buffer, requiredSize); 
}

// 최적화된 접근: ArrayPool을 이용한 메모리 풀링
void SendVariableData_Good(int requiredSize) 
{
    // 1. ArrayPool에서 필요한 크기 '이상'의 메모리 덩어리를 빌려온다. (GC 할당 0)
    byte[] pooledBuffer = ArrayPool&amp;lt;byte&amp;gt;.Shared.Rent(requiredSize);

    try 
    {
        // 2. 관리형 배열을 고정(Pinning)하여 포인터만 C++(Unmanaged)로 넘긴다.
        // 메모리 복사(Bit-block transfer)조차 발생하지 않는 Zero-Copy 마샬링
        unsafe 
        {
            fixed (byte* pBuffer = pooledBuffer) 
            {
                NativePlugin.Send(pBuffer, requiredSize);
            }
        }
    }
    finally 
    {
        // 3. 사용이 끝난 메모리 덩어리를 풀에 반납한다.
        ArrayPool&amp;lt;byte&amp;gt;.Shared.Return(pooledBuffer);
    }
}&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누군가가 비트단위 쪼개가며 메모리 풀링을 하고 있다면 높은 확률로 연배가 있으실 거라는 이야기를 들었다... (응애 개발자는 존경스러울 따름이다...)&lt;/p&gt;</description>
      <category>Unity</category>
      <category>C#</category>
      <category>C++</category>
      <category>Fusion</category>
      <category>Marshalling</category>
      <category>Memory Pooling</category>
      <category>Photon</category>
      <category>unity</category>
      <category>마샬링</category>
      <category>메모리 풀링</category>
      <category>최적화</category>
      <author>ellug</author>
      <guid isPermaLink="true">https://ellug.tistory.com/92</guid>
      <comments>https://ellug.tistory.com/92#entry92comment</comments>
      <pubDate>Mon, 2 Mar 2026 19:53:12 +0900</pubDate>
    </item>
    <item>
      <title>Unity : C#과 Unity에서의 암묵적 Boolean 값 변환 =&amp;gt; if (gameObject) 가능??</title>
      <link>https://ellug.tistory.com/91</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;JS, Phyton 같은 타 언어에서는 암묵적 Bool 변환을 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if 조건문에 넣은 값에 따라 true나 false로 평가되는 방식으로 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;span style=&quot;color: #f6e199;&quot;&gt;C#은 엄격한 타입 안정성을 지향하기에 반드시 명시적으로 bool 타입을 사용하지 않으면 컴파일 에러가 발생&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Unity 환경에서는 특이하게도&lt;span style=&quot;color: #f6e199;&quot;&gt; if (gameObject)&lt;/span&gt; 나&lt;span style=&quot;color: #f6e199;&quot;&gt; if (transform)&lt;/span&gt; 과 같이 객체 자체를 조건문에 넣어도 정상 작동이 되는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 말한 C#의 기본적인 정책과 상충하는 것으로 보이는 이 현상에 대해 간략하게 정리해보자.&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Unity 에서의 내부 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C# 언어 자체에서는 지원하지 않는 문법을 유니티가 쓸 수 있는 이유는, C#의 연산자 오버로딩(Operator Overloading)을 활용해 UnityEngine.Object 클래스 내부에 변환 로직을 직접 구현해 두었기 때문이다.&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;실제 유니티 엔진의 UnityEngine.Object C# 래퍼 코드를 살펴보면 다음과 같이 정의되어 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1772441928212&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;namespace UnityEngine
{
    public partial class Object
    {
        // ... 생략 ...

        // 객체가 null이거나 파괴되었는지 확인하는 내부 로직
        private static bool CompareBaseObjects(Object lhs, Object rhs)
        {
            bool lhsNull = ((object)lhs == null) || !lhs.IsNativeObjectAlive();
            bool rhsNull = ((object)rhs == null) || !rhs.IsNativeObjectAlive();
            return lhsNull == rhsNull;
        }

        // 핵심: Object 형을 bool 형으로 암묵적 변환해주는 연산자 오버로딩
        public static implicit operator bool(Object exists)
        {
            return !CompareBaseObjects(exists, null);
        }
        
        // 추가로 ==, != 연산자도 동일한 방식으로 오버로딩되어 있다.
        public static bool operator ==(Object x, Object y)
        {
            return CompareBaseObjects(x, y);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 볼 수 있듯이 유니티 내부에서 &lt;span style=&quot;color: #f6e199;&quot;&gt;implicit operator bool&lt;/span&gt; 을 정의하여 유니티 객체가 if 문 안에 들어갈 때 자동으로 내부의 CompareBaseObjects 함수를 호출하도록 설계했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그렇다면 굳이 왜 표준 C#의 규칙을 벗어나면서까지 이런 오버로딩을 구현했을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유니티 객체는 C# 영역의 관리형 객체(Managed Object)와 C++ 영역의 네이티브 객체(Native Object)로 분리되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 Destroy(gameObject)를 호출하면 C++ 영역의 실제 메모리는 파괴되지만, 이를 가리키는 C# 객체 껍데기는 GC가 수거하기 전까지 메모리에 남아있게 된다.&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;만약 유니티가 순수 C#의 null 체크 방식을 그대로 쓴다면, 이미 파괴된 객체임에도 C# 껍데기가 남아있어 if (gameObject != null) 조건문을 통과해 버리는 치명적인 버그가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 막기 위해 유니티는 암묵적 Bool 변환 발생 시, C# 껍데기 뿐만 아니라 C++ 네이티브 객체가 실제로 살아있는지 (IsNativeObjectAlive)까지 검사하도록 내부 동작을 덮어씌운 것이다.&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;이러한 암묵적 변환은 UnityEngine.Object를 상속받는 모든 유니티 엔진 기반 객체들에게 적용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GameObject, Component, MonoBehavior, ScriptableObject 등 그 외 유니티 에셋 타입들 모두 적용된다.&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;반대로 기본 자료형들과 순수 C# 클래스, 인터페이스 들은 지원하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 자료형의 경우 num != 0 같은 형태로 비교해야하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;string의 경우 반드시 string.IsNullOrEmpty 등으로 체크해야한다.&lt;/p&gt;</description>
      <category>Unity</category>
      <category>bool</category>
      <category>boolean</category>
      <category>C#</category>
      <category>unity</category>
      <category>암묵적 Bool 변환</category>
      <author>ellug</author>
      <guid isPermaLink="true">https://ellug.tistory.com/91</guid>
      <comments>https://ellug.tistory.com/91#entry91comment</comments>
      <pubDate>Mon, 2 Mar 2026 18:09:28 +0900</pubDate>
    </item>
    <item>
      <title>Unity : 같은 PC에서 클라이언트 여러개 실행시 멈추는 현상 (Firestore)</title>
      <link>https://ellug.tistory.com/90</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;현상&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unity 프로젝트에서 Firebase Cloud Firestore를 사용할 때, 한 컴퓨터에서 클라이언트를 여러개 동시에 실행하면 완전히 멈춰버리거나 크래시가 발생하는 문제가 있었다.&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 크게 기술적인 원인 분석을 한 부분은 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데도 왜 Firestore 라고 확신했냐면, Firebase Auth 도입 직후까지는 정상적으로 멀티 클라이언트 실행이 가능했던 프로젝트가 Firestore 도입 시점부터 바로 이런 현상이 발생했기 때문이다...&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;하지만 Firestore가 원인이라고 해도 이해가 가질 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 에서 Firestore를 사용했던 경험으로는 하나의 컴퓨터에서 여러개의 브라우저, 여러개의 계정을 통해 Firestore에 아무리 접근해도 그 어떤 문제가 되지 않았기 때문이다.&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 과정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 재현 조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일 PC에서 클라이언트를 2개 이상 동시에 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Firestore 사용 지점, 즉 로그인 지점 (해당 프로젝트에서는 로그인 직후 DB에서 회원 정보를 조회) 에서 문제가 발생하는 것으로 보임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 조사&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Firestore는 성능 오프라인 동작을 위해 로컬에 캐시/DB를 유지(오프라인 지속성) 하는 구조를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 로컬 DB는 일반적으로 동일한 저장 경로에 대해 하나의 프로세스만 배타적으로 접근할 수 있다.&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;즉, 동일 PC에서 동일한 앱을 여러개 띄우게 되면&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;1번 인스턴스가 로컬 캐시 DB를 열고 &quot;LOCK&quot;을 잡는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번 인스턴스도 동일한 경로의 DB를 열려고 시도한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LOCK을 얻지 못해 초기화에 실패하고 프리즈/크래시로 이어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 그런데 왜 웹에서는 멀쩡했나?&lt;/h3&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;웹 SDK는 브라우저 저장소 기반이고, 탬 / 프로필 / 브라우저 단위로 저장소 분리 또는 조정이 되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Unity나 일반적인 어플리케이션의 경우 동일한 실행 파일을 공유하는 형태로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Firestore 로컬 캐시가 단일 경로를 점유해버리면 다중 프로세스에서 충돌이 발생한다.&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;즉, Firestore가 의도적으로 멀티 접근을 막은 게 아니라, 동일 PC 동일 앱의 로컬 캐시 저장소가 멀티 프로세스에 취약한 구조였다고 보는 편이 정답에 가깝다.&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결법은 매우 간단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Firestore 초기화 단계에서&lt;/p&gt;
&lt;pre id=&quot;code_1770533456790&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FirestoreSettings.PersistenceEnabled = 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;한 줄만 초기화 해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 로컬 DB를 사용하지 않게 되어, 동일 PC 다중 클라이언트 실행시 충돌이 발생하지 않게된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론&amp;nbsp;일반적으로&amp;nbsp;다중&amp;nbsp;클라이언트&amp;nbsp;실행을&amp;nbsp;허가하는&amp;nbsp;게임은&amp;nbsp;많지&amp;nbsp;않기&amp;nbsp;때문에&amp;nbsp;개발&amp;nbsp;테스트&amp;nbsp;단계가&amp;nbsp;아니라면&amp;nbsp;굳이&amp;nbsp;필요&amp;nbsp;없어&amp;nbsp;보이긴&amp;nbsp;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;부작용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오프라인 캐시 기반 동작이 약해지기 때문에 네트워크 호출 효율성이 떨어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 네트워크 상황이 안 좋을 때 체감 성능이 떨어질 수 있기 때문에 사용 여부 및, 실제 배포 빌드에서 어떻게 할지에 대해 고민해볼 필요가 있다.&lt;/p&gt;</description>
      <category>Unity</category>
      <category>auth</category>
      <category>C#</category>
      <category>DB</category>
      <category>firebase</category>
      <category>firestore</category>
      <category>PersistenceEnabled</category>
      <category>unity</category>
      <category>멀티 클라이언트</category>
      <category>크래시</category>
      <category>프리징</category>
      <author>ellug</author>
      <guid isPermaLink="true">https://ellug.tistory.com/90</guid>
      <comments>https://ellug.tistory.com/90#entry90comment</comments>
      <pubDate>Sun, 8 Feb 2026 15:54:09 +0900</pubDate>
    </item>
    <item>
      <title>Unity : 관리자 콘솔 구현 및 프리징 현상 개선</title>
      <link>https://ellug.tistory.com/89</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Unity DevConsole 프리징 개선 기록 : 반복 에러 콜랩스 + Flush 예산화&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) DevConsole의 기존 작동방식과 핵심 기술&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 DevConsole은 유니티 로그를 받아서 콘솔 UI에 텍스트로 뿌리는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 흐름은 아래와 같았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Application.logMessageReceivedThreaded 콜백으로 로그를 수신한다.&lt;/li&gt;
&lt;li&gt;유니티는 싱글쓰레드이기에 백그라운드 상의 멀티 쓰레드의 로그를 모두 수집하면 데드락 위험이 있어 lock 키워드를 통해 pending 버퍼에 로그를 쌓는 방식으로 데드락을 방지했다.&lt;/li&gt;
&lt;li&gt;콘솔이 열리면 버퍼에 쌓인 로그를 실제 콘솔 UI에 출력하는 lines 리스트에 옮긴다.&lt;/li&gt;
&lt;li&gt;lines 전체를 StringBuilder에 합쳐서 출력한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로, &amp;ldquo;연속 중복(바로 직전 로그와 동일)&amp;rdquo;은 체크해서 count++ 형태로만 병합했다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) 문제 발생: 장문의 반복 에러 + 콘솔 오픈 순간 병목&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 &amp;ldquo;연속 중복&amp;rdquo;이 아닌 형태로 반복이 터질 때다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 같은 예외가 프레임마다 계속 찍히지만, 중간에 다른 로그가 섞이거나 스택/메시지가 조금씩 달라지면 pending이 계속 커진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 병목이 크게 터지는 지점은 콘솔을 키는 순간이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pending이 많아진 상태에서 버퍼에서 라인으로 옮겨 출력하는 메서드들이 한 번에 몰아서 실행된다&lt;/li&gt;
&lt;li&gt;BuildText()가 lines 전체를 매번 문자열로 재구성한다&lt;/li&gt;
&lt;li&gt;기존 코드에서 pending &amp;rarr; lines 병합 시 count만큼 반복하는 루프가 있다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, &amp;ldquo;반복 수가 커질수록&amp;rdquo; 병합 비용이 기하급수적으로 늘어난다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 &lt;span style=&quot;color: #ee2323;&quot;&gt;&amp;ldquo;콘솔을 켰을 뿐인데 프레임이 몇 초 멈추는&amp;nbsp;현상&quot;&lt;/span&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) 해결 아이디어 정리&lt;/h2&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;b&gt;1. 반복 로그를 출력 순간에 합치니 부하가 생긴다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;콘솔을 열 때 몰아서 처리하니까 스파이크가 생긴다.&lt;/li&gt;
&lt;li&gt;해결 방향: &lt;b&gt;로그가 들어오는 시점에서 &amp;ldquo;동일 로그&amp;rdquo;를 바로 집계(콜랩스)한다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. &lt;/b&gt;&lt;b&gt;Flush/병합을 한 프레임에 다 한다&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버퍼 이동, 병합, 문자열 출력을 한 프레임에 다 처리하니 부하가 한 곳에 몰려 프리징 현상이 생긴다.&lt;/li&gt;
&lt;li&gt;해결 방향: &lt;b&gt;프레임 예산(time/개수)을 두고 나눠 처리한다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부가로, 기존 count만큼 반복해서 증가시키는 로직도 꼭 필요해 보이진 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;count를 통째로 더하는 AddCount()로 O(1) 병합하는 방향으로 수정해보자.&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4) 해결법 실적용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) pending을 List &amp;rarr; Dictionary 집계로 변경한다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일 로그를 구분하는 키를 만든다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Key 구조체: LogType + msg + stack&lt;/li&gt;
&lt;li&gt;문자열 비교지만 해시를 미리 계산해둬서 딕셔너리 조회를 빠르게 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pending에 쌓는 방식이 이렇게 바뀐다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존: List&amp;lt;LogItem&amp;gt; _pending에 계속 append&lt;/li&gt;
&lt;li&gt;변경: Dictionary&amp;lt;LogKey, LogItem&amp;gt; _pendingDict에서 동일 키면 count++, 없으면 새 항목 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 표시 순서는 유지하고 싶으니 별도의 _pendingOrder로 키 삽입 순서를 기록한다.&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;(2) Flush를 프레임 예산으로 쪼갠다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘솔이 열렸을 때 pending을 lines로 옮기는 작업을 한 번에 하지 않는다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MaxFlushPerFrame: 프레임당 최대 처리 개수&lt;/li&gt;
&lt;li&gt;MaxFlushTimeMs: 프레임당 최대 처리 시간(ms)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 pending이 아무리 많아도 프리징 대신 천천히 반영된다.&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;UI는 즉시 열고 [Loading logs...] 같은 메시지를 먼저 보여주면 체감도 좋아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 프론트엔드에서 사용하던 Lazy Loading 에서 출발한 아이디어다.&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;(3) count 병합을 O(1)로 바꾼다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 병합 시 for (k &amp;lt; it.count) Increment()로 반복했는데, 이게 반복 폭탄에서 최악이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 아래처럼 바꾼다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AddCount(now, add)로 count += add를 한 번에 처리&lt;/li&gt;
&lt;li&gt;반복 수가 많아도 병합 비용은 동일하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(4) 요약&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수집한 쓰레드를 List에 append하지 않고 dict에 집계한다.&lt;/li&gt;
&lt;li&gt;flush 연산을 분할시켜서 부하를 여러 프레임에 분산시킨다.&lt;/li&gt;
&lt;li&gt;Increment 반복 제거 &amp;rarr; AddCount로 한 번에 더한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5) 기대효과&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식으로 기대하는 효과는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;콘솔을 켜도 순간적인 프리징 현상이 일어나지 않는다.&lt;/li&gt;
&lt;li&gt;반복 에러는 &amp;ldquo;동일 키 1줄 + count 증가&amp;rdquo;로 수렴한다.&lt;/li&gt;
&lt;li&gt;pending이 길게 쌓여도 오픈 순간에 폭발하지 않는다.&lt;/li&gt;
&lt;li&gt;최악 상황에서 성능이 &amp;ldquo;스파이크&amp;rdquo;가 아니라 &amp;ldquo;점진 처리&amp;rdquo;로 바뀐다.&lt;/li&gt;
&lt;/ul&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;h4 data-ke-size=&quot;size20&quot;&gt;6) 결과&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프리징 현상을 수치로 기록하지 않았던 것은 아쉽지만 명백히 프리징이 발생한다, 발생하지 않는다로 관측할 수 있었다.&lt;/li&gt;
&lt;li&gt;에러 로그가 과도하게 쌓였을때 콘솔을 열어도 더이상 프리징 현상이 발생하지 않는 것을 확인했다.&lt;/li&gt;
&lt;li&gt;가시적으로 프리징 현상이 사라졌기 때문에 별도의 성능 차 기록은 생략했다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Unity</category>
      <category>LogMessageReceived</category>
      <category>logMessageReceivedThreaded</category>
      <category>Network</category>
      <category>Thread</category>
      <category>unity</category>
      <category>데드락</category>
      <category>쓰레드</category>
      <category>최적화</category>
      <category>프리징</category>
      <category>해결</category>
      <author>ellug</author>
      <guid isPermaLink="true">https://ellug.tistory.com/89</guid>
      <comments>https://ellug.tistory.com/89#entry89comment</comments>
      <pubDate>Sun, 8 Feb 2026 15:22:27 +0900</pubDate>
    </item>
    <item>
      <title>Unity : Firebase 등록 및 Auth로 (이메일/소셜) 로그인 구현</title>
      <link>https://ellug.tistory.com/88</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Firebase&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 개발 시 백엔드 인프라 구축의 번거로움을 최소화하는 &lt;b data-index-in-node=&quot;54&quot; data-path-to-node=&quot;3&quot;&gt;BaaS(Backend as a Service)&lt;/b&gt; 플랫폼이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 코드 작성 및 인프라 관리 부담을 줄여 개발 효율성을 극대화하는 것을 목적으로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 우리는 Firebase를 이용해 간단한 인증부터 DB, API까지 여러 방면으로 서버 개발 없이 빠르게 활용할 수 있다.&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;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Firebase 등록 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Firebase 검색 후 Firebase - Google 페이지로 들어간다&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1048&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xpkec/dJMcacPma2t/KmRnbeExTFxTEjZ5omDD4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xpkec/dJMcacPma2t/KmRnbeExTFxTEjZ5omDD4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xpkec/dJMcacPma2t/KmRnbeExTFxTEjZ5omDD4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxpkec%2FdJMcacPma2t%2FKmRnbeExTFxTEjZ5omDD4K%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;659&quot; height=&quot;440&quot; data-origin-width=&quot;1048&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Get started in console&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2470&quot; data-origin-height=&quot;1208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FOoA5/dJMcadm8HcL/pRwpzHSr0Qp9wDTPx9zfK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FOoA5/dJMcadm8HcL/pRwpzHSr0Qp9wDTPx9zfK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FOoA5/dJMcadm8HcL/pRwpzHSr0Qp9wDTPx9zfK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFOoA5%2FdJMcadm8HcL%2FpRwpzHSr0Qp9wDTPx9zfK0%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;662&quot; height=&quot;324&quot; data-origin-width=&quot;2470&quot; data-origin-height=&quot;1208&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;h3 data-ke-size=&quot;size23&quot;&gt;3. 새 Firebase 프로젝트 만들기 클릭해서 프로젝트 만들기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2494&quot; data-origin-height=&quot;1257&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wP104/dJMcacPma3r/mk6kaLkm3KrmgeTF7H8Yl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wP104/dJMcacPma3r/mk6kaLkm3KrmgeTF7H8Yl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wP104/dJMcacPma3r/mk6kaLkm3KrmgeTF7H8Yl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwP104%2FdJMcacPma3r%2Fmk6kaLkm3KrmgeTF7H8Yl1%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;721&quot; height=&quot;363&quot; data-origin-width=&quot;2494&quot; data-origin-height=&quot;1257&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;h3 data-ke-size=&quot;size23&quot;&gt;4. 프로젝트 생성&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2481&quot; data-origin-height=&quot;1126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5KjAX/dJMcaiaYaeb/pAcNx7nN7YB1dh45mVxiV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5KjAX/dJMcaiaYaeb/pAcNx7nN7YB1dh45mVxiV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5KjAX/dJMcaiaYaeb/pAcNx7nN7YB1dh45mVxiV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5KjAX%2FdJMcaiaYaeb%2FpAcNx7nN7YB1dh45mVxiV1%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;783&quot; height=&quot;355&quot; data-origin-width=&quot;2481&quot; data-origin-height=&quot;1126&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;프로젝트 이름을 지정하고 그 뒤에 Gemini 애널리틱스 사용 여부를 묻는데 그냥 둘다 사용 안 해도 된다.&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;h3 data-ke-size=&quot;size23&quot;&gt;5. 프로젝트 생성 완료&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2515&quot; data-origin-height=&quot;1265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bE20EL/dJMcaaqs9ir/0MKTRFWemvArPOkbJGKszK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bE20EL/dJMcaaqs9ir/0MKTRFWemvArPOkbJGKszK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bE20EL/dJMcaaqs9ir/0MKTRFWemvArPOkbJGKszK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbE20EL%2FdJMcaaqs9ir%2F0MKTRFWemvArPOkbJGKszK%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;769&quot; height=&quot;387&quot; data-origin-width=&quot;2515&quot; data-origin-height=&quot;1265&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성 완료시 최초로 다음과 같은 화면이 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 프로젝트 세팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌상단의 프로젝트 개요 옆에 있는 톱니바퀴를 클릭하면 프로젝트 설정 메뉴가 나온다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;641&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CQKue/dJMcabv85Ui/Huzt7fTvgsStV8htWXeKdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CQKue/dJMcabv85Ui/Huzt7fTvgsStV8htWXeKdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CQKue/dJMcabv85Ui/Huzt7fTvgsStV8htWXeKdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCQKue%2FdJMcabv85Ui%2FHuzt7fTvgsStV8htWXeKdk%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;660&quot; height=&quot;547&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;641&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 설정을 클릭하고 들어가면 아래와 같은 창이 뜬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1983&quot; data-origin-height=&quot;1134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXLCxp/dJMcai9O7Vj/UP2863h6ua7ftvNnc7awA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXLCxp/dJMcai9O7Vj/UP2863h6ua7ftvNnc7awA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXLCxp/dJMcai9O7Vj/UP2863h6ua7ftvNnc7awA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXLCxp%2FdJMcai9O7Vj%2FUP2863h6ua7ftvNnc7awA1%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;767&quot; height=&quot;439&quot; data-origin-width=&quot;1983&quot; data-origin-height=&quot;1134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유니티 프로젝트에 사용할 것이니 하단의 내 앱 쪽에서 유니티 로고를 클릭해준다.&lt;/p&gt;
&lt;p 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;1103&quot; data-origin-height=&quot;1183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ln8pt/dJMcabQrvWd/OoQ9ZjM0aLWIGmJOCbRUZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ln8pt/dJMcabQrvWd/OoQ9ZjM0aLWIGmJOCbRUZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ln8pt/dJMcabQrvWd/OoQ9ZjM0aLWIGmJOCbRUZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fln8pt%2FdJMcabQrvWd%2FOoQ9ZjM0aLWIGmJOCbRUZK%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;771&quot; height=&quot;827&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;1183&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적당히 앱 이름 등록해주고&lt;/p&gt;
&lt;p 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;808&quot; data-origin-height=&quot;923&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dh6T0O/dJMcai9O72G/5trFxz1EkxnUNtvj8I3dCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dh6T0O/dJMcai9O72G/5trFxz1EkxnUNtvj8I3dCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dh6T0O/dJMcai9O72G/5trFxz1EkxnUNtvj8I3dCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdh6T0O%2FdJMcai9O72G%2F5trFxz1EkxnUNtvj8I3dCk%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;724&quot; height=&quot;827&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;923&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;google-services.json 다운로드를 클릭해 json 파일을 받아 Assets 폴더에 넣어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;json 파일은 파이어베이스에 접근하기 위한 key 등 주요 데이터가 들어있기 때문에 git에 업로드 하거나 함부로 노출하면 안되니 주의하자.&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;966&quot; data-origin-height=&quot;797&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TdDNK/dJMcafefJyv/JX5zI4zanNsoF3Wh0Tfc9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TdDNK/dJMcafefJyv/JX5zI4zanNsoF3Wh0Tfc9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TdDNK/dJMcafefJyv/JX5zI4zanNsoF3Wh0Tfc9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTdDNK%2FdJMcafefJyv%2FJX5zI4zanNsoF3Wh0Tfc9k%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;755&quot; height=&quot;623&quot; data-origin-width=&quot;966&quot; data-origin-height=&quot;797&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;SDK zip을 다운받아준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SDK zip 안에는 Firebase의 각종 서비스에 대한 패키지 파일들이 들어있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;445&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k2F7Z/dJMcadtUDf3/SP3nj2mlFKTNSGMpQVKl70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k2F7Z/dJMcadtUDf3/SP3nj2mlFKTNSGMpQVKl70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k2F7Z/dJMcadtUDf3/SP3nj2mlFKTNSGMpQVKl70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk2F7Z%2FdJMcadtUDf3%2FSP3nj2mlFKTNSGMpQVKl70%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;525&quot; height=&quot;360&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;445&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크기가 그리 가볍진 않으니 내가 필요한 서비스만 유니티 프로젝트에 임포트 해주자.&lt;/p&gt;
&lt;p 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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Authentication 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;0&quot;&gt;Firebase Authentication&lt;/b&gt;은 애플리케이션의 사용자 인증 시스템을 구축하기 위한 관리형 서비스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 인증 로직(암호화, 세션 유지, 보안 토큰 관리 등)을 직접 구현하지 않고 SDK 호출만으로 처리할 수 있도록 설계되어있는 매우 편리한 기능이다.&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;h3 data-ke-size=&quot;size23&quot;&gt;1. Authentication 활성화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 사용해볼 건 Auth, 즉 인증(회원가입/로그인) 기능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌측의 빌드 탭을 클릭하면 리스트가 나온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;633&quot; data-origin-height=&quot;762&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/75CTu/dJMcafyxJlz/vSFal5MEPepEWgZ5HYIkMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/75CTu/dJMcafyxJlz/vSFal5MEPepEWgZ5HYIkMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/75CTu/dJMcafyxJlz/vSFal5MEPepEWgZ5HYIkMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F75CTu%2FdJMcafyxJlz%2FvSFal5MEPepEWgZ5HYIkMk%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;633&quot; height=&quot;762&quot; data-origin-width=&quot;633&quot; data-origin-height=&quot;762&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;Authentication 을 클릭하면 다음과 같은 창이 뜬다.&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;2484&quot; data-origin-height=&quot;1252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oNVCw/dJMcahpByMo/fbMtqbWMf4WjFqW5SknTG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oNVCw/dJMcahpByMo/fbMtqbWMf4WjFqW5SknTG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oNVCw/dJMcahpByMo/fbMtqbWMf4WjFqW5SknTG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoNVCw%2FdJMcahpByMo%2FfbMtqbWMf4WjFqW5SknTG0%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;2484&quot; height=&quot;1252&quot; data-origin-width=&quot;2484&quot; data-origin-height=&quot;1252&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;시작하기 버튼을 눌러 Authentication을 활성화 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 로그인 방법 설정&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1967&quot; data-origin-height=&quot;1162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6KR9r/dJMcafFjNsh/t9Ey9Jxn9aiJA2H4c4iFp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6KR9r/dJMcafFjNsh/t9Ey9Jxn9aiJA2H4c4iFp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6KR9r/dJMcafFjNsh/t9Ey9Jxn9aiJA2H4c4iFp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6KR9r%2FdJMcafFjNsh%2Ft9Ey9Jxn9aiJA2H4c4iFp1%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;1967&quot; height=&quot;1162&quot; data-origin-width=&quot;1967&quot; data-origin-height=&quot;1162&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;우선 이메일/비밀번호와 추가 제공업체 Google을 클릭해서 활성화 해주자&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;1105&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkjESx/dJMcac2RbAV/s3SH7KtPXodd1KPLhaGKIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkjESx/dJMcac2RbAV/s3SH7KtPXodd1KPLhaGKIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkjESx/dJMcac2RbAV/s3SH7KtPXodd1KPLhaGKIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkjESx%2FdJMcac2RbAV%2Fs3SH7KtPXodd1KPLhaGKIK%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;1105&quot; height=&quot;666&quot; data-origin-width=&quot;1105&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이런 창이 뜬다.&lt;/p&gt;
&lt;p 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;h3 data-ke-size=&quot;size23&quot;&gt;3. 사용자 탭에서 유저 리스트를 확인할 수 있다.&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1663&quot; data-origin-height=&quot;838&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8wB96/dJMcabbP9Ts/njSdrsejr5dwtOUEdrTfTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8wB96/dJMcabbP9Ts/njSdrsejr5dwtOUEdrTfTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8wB96/dJMcabbP9Ts/njSdrsejr5dwtOUEdrTfTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8wB96%2FdJMcabbP9Ts%2FnjSdrsejr5dwtOUEdrTfTK%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;1663&quot; height=&quot;838&quot; data-origin-width=&quot;1663&quot; data-origin-height=&quot;838&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;여기서 사용자의 고유값은 최 우측의 사용자 UID 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;식별자, 닉네임 등 다른 수치는 사용자의 고유함을 보장하기 어려울 수 있기에 유저 식별에는 이 UID값을 활용하도록 하자.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이메일 / 비밀번호 기반 로그인 구현하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. FirebaseApp 초기화 및 의존성 검사&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Firebase 기능을 사용하기 전, 가장 먼저 실행해야하는 단계다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기기에 구글 플레이 서비스가 최신 상태인지 확인하고 SDK를 초기화 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1768109062991&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FirebaseApp.CheckAndFixDependenciesAsync();&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;위 메서드는 유니티 실행시 비동기로 환경을 체크하며, DependencyStatus.Available 상태일 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FirebaseAuth.DefaultInstance 를 통해 인증 인스턴스를 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 회원 가입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신규 사용자를 등록하는 단계다. 이메일과 비밀번호를 파라미터로 전달한다.&lt;/p&gt;
&lt;pre id=&quot;code_1768109235428&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CreateUserWithEmailAndPasswordAsync(email, password);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;놀랍도록 직관적인 메서드명...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 사용자 프로필 업데이트 (Nickname 설정)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 닉네임도 설정해줄 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1768109299908&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UpdateUserProfileAsync(UserProfile);&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;UserProfile을 생성하고, DisplayName 속성에 원하는 닉네임을 할당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;user.UpdateUserProfileAsync(profile) 을 호출해 사용자 정보를 갱신할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 로그인&lt;/h3&gt;
&lt;pre id=&quot;code_1768109380810&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SignInWithEmailAndPasswordAsync(email, password);&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;입력한 정보 (email, password)가 일치하면 FirebaseUser 객체를 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패시 FirebaseException을 통해 비밀번호 불일치, 존재하지 않는 사용자 등의 구체적인 에러 코드를 확인할 수 있습니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1768109847852&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using System;
using System.Collections;
using System.Threading.Tasks;
using Firebase;
using Firebase.Auth;
using Firebase.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class FirebaseAuthManager : MonoBehaviour
{
    [Header(&quot;UI References&quot;)]
    [SerializeField] private TMP_InputField _emailField;
    [SerializeField] private TMP_InputField _pwField;
    [SerializeField] private TMP_InputField _nicknameField;
    [SerializeField] private Button _startButton;
    [SerializeField] private TMP_Text warningText;
    [SerializeField] private TMP_Text confirmText;

    // 인증 관련 핵심 객체
    public static FirebaseAuth auth; // Firebase 인증 서비스 인스턴스
    public static FirebaseUser user; // 현재 인증된 사용자 정보 (UID, DisplayName 등 포함)

    void Awake()
    {
        // [초기화] 구글 서비스 의존성 검사 및 Firebase 초기화
        FirebaseApp.CheckAndFixDependenciesAsync().ContinueWithOnMainThread(task =&amp;gt;
        {
            var dependencyStatus = task.Result;
            if (dependencyStatus == DependencyStatus.Available)
            {
                auth = FirebaseAuth.DefaultInstance;
                Debug.Log(&quot;Firebase Auth Init Complete&quot;);
            }
            else
            {
                Debug.LogError($&quot;Firebase dependencies not resolved: {dependencyStatus}&quot;);
            }
        });
    }

    void Start()
    {
        _startButton.interactable = false;
        warningText.text = &quot;&quot;;
        confirmText.text = &quot;&quot;;
    }

    // --- Public Interface (버튼 연결용) ---

    public void OnClickLogin()
    {
        StartCoroutine(LoginCoroutine(_emailField.text, _pwField.text));
    }

    public void OnClickSignUp()
    {
        StartCoroutine(SignUpCoroutine(_emailField.text, _pwField.text, _nicknameField.text));
    }

    // --- Authentication Logic ---

    /*
     * [로그인] 이메일과 비밀번호를 이용한 인증 메서드
     * 성공 시 유저 정보를 획득하고 시작 버튼을 활성화함
     */
    private IEnumerator LoginCoroutine(string email, string pw)
    {
        Task&amp;lt;AuthResult&amp;gt; loginTask = auth.SignInWithEmailAndPasswordAsync(email, pw);
        yield return new WaitUntil(() =&amp;gt; loginTask.IsCompleted);

        if (loginTask.Exception != null)
        {
            HandleAuthError(loginTask.Exception);
        }
        else
        {
            user = loginTask.Result.User;
            warningText.text = &quot;&quot;;
            confirmText.text = &quot;Login Success!&quot;;
            _nicknameField.text = user.DisplayName;
            _startButton.interactable = true;
        }
    }

    /*
     * [회원가입] 계정 생성 및 닉네임 설정 로직
     * CreateUser 후 UpdateUserProfileAsync를 통해 DisplayName(닉네임)을 서버에 등록
     */
    private IEnumerator SignUpCoroutine(string email, string pw, string nickName)
    {
        // 1. 계정 생성 시도
        Task&amp;lt;AuthResult&amp;gt; signUpTask = auth.CreateUserWithEmailAndPasswordAsync(email, pw);
        yield return new WaitUntil(() =&amp;gt; signUpTask.IsCompleted);

        if (signUpTask.Exception != null)
        {
            HandleAuthError(signUpTask.Exception);
        }
        else
        {
            user = signUpTask.Result.User;

            if (user != null)
            {
                // 2. 닉네임(UserProfile) 설정
                UserProfile profile = new UserProfile { DisplayName = nickName };
                Task profileTask = user.UpdateUserProfileAsync(profile);
                yield return new WaitUntil(() =&amp;gt; profileTask.IsCompleted);

                if (profileTask.Exception != null)
                {
                    HandleAuthError(profileTask.Exception);
                }
                else
                {
                    warningText.text = &quot;&quot;;
                    confirmText.text = &quot;Sign Up &amp;amp; Profile Set Complete!&quot;;
                    _startButton.interactable = true;
                }
            }
        }
    }

    /*
     * [에러 핸들링] Firebase 인증 과정에서 발생하는 에러 코드를 분석하여 UI에 출력
     */
    private void HandleAuthError(AggregateException exception)
    {
        FirebaseException firebaseEx = exception.GetBaseException() as FirebaseException;
        AuthError errorCode = (AuthError)firebaseEx.ErrorCode;

        string message = errorCode switch
        {
            AuthError.MissingEmail =&amp;gt; &quot;이메일을 입력해주세요.&quot;,
            AuthError.MissingPassword =&amp;gt; &quot;비밀번호를 입력해주세요.&quot;,
            AuthError.WrongPassword =&amp;gt; &quot;비밀번호가 틀렸습니다.&quot;,
            AuthError.InvalidEmail =&amp;gt; &quot;유효하지 않은 이메일 형식입니다.&quot;,
            AuthError.UserNotFound =&amp;gt; &quot;존재하지 않는 사용자입니다.&quot;,
            AuthError.WeakPassword =&amp;gt; &quot;비밀번호가 너무 취약합니다.&quot;,
            AuthError.EmailAlreadyInUse =&amp;gt; &quot;이미 가입된 이메일입니다.&quot;,
            _ =&amp;gt; &quot;알 수 없는 에러가 발생했습니다.&quot;
        };

        warningText.text = message;
        confirmText.text = &quot;&quot;;
        Debug.LogWarning($&quot;Auth Error: {errorCode}&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;대부분의 에러도 Firebase에서 정의해뒀으므로 관리가 편리한 편이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유니티에 간단하게 로그인창 만들어서 적용해보면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;GIF.gif&quot; data-origin-width=&quot;1218&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6PnhP/dJMcaiWi2Ny/GxQr8jWFjTzaifVYdaxqbk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6PnhP/dJMcaiWi2Ny/GxQr8jWFjTzaifVYdaxqbk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6PnhP/dJMcaiWi2Ny/GxQr8jWFjTzaifVYdaxqbk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/6PnhP/dJMcaiWi2Ny/GxQr8jWFjTzaifVYdaxqbk/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;803&quot; height=&quot;452&quot; data-filename=&quot;GIF.gif&quot; data-origin-width=&quot;1218&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 작동 한다&lt;/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;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Google 소셜 로그인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 로그인 방법에서 Google이 활성화 되어있어야 한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 안드로이드 빌드시 SHA-1 등록을 해야하니 주의하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 코드&lt;/h3&gt;
&lt;pre id=&quot;code_1768110380532&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using Google; // Google Sign-In SDK 필요

// ... 기존 변수 선언부 아래에 추가
[SerializeField] private string _webClientId = &quot;YOUR_WEB_CLIENT_ID.apps.googleusercontent.com&quot;;

public void OnClickGoogleLogin()
{
    Configuration configuration = new Configuration
    {
        WebClientId = _webClientId,
        RequestIdToken = true
    };
    GoogleSignIn.Configuration = configuration;
    GoogleSignIn.Configuration.UseGameSignIn = false;
    GoogleSignIn.Configuration.RequestIdToken = true;

    GoogleSignIn.DefaultInstance.SignIn().ContinueWithOnMainThread(task =&amp;gt;
    {
        if (task.IsFaulted)
        {
            Debug.LogError(&quot;Google Sign-In Failed&quot;);
        }
        else if (task.IsCanceled)
        {
            Debug.Log(&quot;Google Sign-In Canceled&quot;);
        }
        else
        {
            // Google에서 획득한 ID 토큰으로 Firebase 인증 시도
            StartCoroutine(SignInWithGoogleCoroutine(task.Result.IdToken));
        }
    });
}

/*
 * [Google 인증] Google ID 토큰을 Firebase Credential로 변환하여 로그인
 */
private IEnumerator SignInWithGoogleCoroutine(string idToken)
{
    // 1. Google 인증 정보를 Firebase용 Credential로 변환
    AuthCredential credential = GoogleAuthProvider.GetCredential(idToken, null);

    // 2. 변환된 Credential로 Firebase 로그인
    Task&amp;lt;AuthResult&amp;gt; loginTask = auth.SignInWithCredentialAsync(credential);
    yield return new WaitUntil(() =&amp;gt; loginTask.IsCompleted);

    if (loginTask.Exception != null)
    {
        HandleAuthError(loginTask.Exception);
    }
    else
    {
        user = loginTask.Result.User;
        confirmText.text = $&quot;Google Login Success: {user.DisplayName}&quot;;
        _startButton.interactable = true;
        
        // 소셜 로그인은 이미 닉네임이 있으므로 필드에 자동 채우기
        _nicknameField.text = user.DisplayName;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드에 이 코드를 추가하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글 로그인에 사용되는 각 주요 메서드들은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1768111922076&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. Google 로그인 설정 (Configuration) : Google SDK에 인증 요청 사양을 전달.
Configuration configuration = new Configuration
{
    WebClientId = &quot;YOUR_WEB_CLIENT_ID&quot;, // Firebase와 연동하기 위한 필수 키
    RequestIdToken = true,               // Firebase 인증에 필요한 ID 토큰 요청 여부
    RequestEmail = true                  // 유저의 이메일 정보 접근 권한 요청
};

// 2. Google 계정 선택 및 인증 (SignIn) : 
// 비동기(Task)로 동작하며, 성공 시 유저의 신원 정보가 담긴 Task 결과를 반환.
GoogleSignIn.DefaultInstance.SignIn().ContinueWithOnMainThread(task =&amp;gt; {
    // task.Result를 통해 GoogleSignInUser 객체에 접근 가능
});

// 3. Firebase용 자격 증명 생성 (GetCredential)
// Google로부터 받은 ID 토큰을 Firebase 시스템이 인식할 수 있는 'Credential'로 변환
// idToken: GoogleSignInUser로부터 획득한 고유 토큰
AuthCredential credential = GoogleAuthProvider.GetCredential(idToken, null);

// 4. 자격 증명을 이용한 최종 로그인 (SignInWithCredentialAsync)
// 변환된 Credential을 Firebase 서버에 제출하여 최종 세션을 획득
Task&amp;lt;AuthResult&amp;gt; loginTask = auth.SignInWithCredentialAsync(credential);

/* 5. 사용자 정보 참조 (FirebaseUser)
 * 주요 프로필 속성: 
 * - user.UserId: 서비스 내 고유 식별자 (UID)
 * - user.DisplayName: Google 계정에 설정된 이름
 * - user.PhotoUrl: Google 프로필 이미지 경로
 */
FirebaseUser user = loginTask.Result.User;&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;여기서 ContinueWithOnMainThread를 쓰는 이유에 대해 의문을 품을 수 있다.&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;ContinueWith : 작업 완료 후 백그라운드 스레드에서 후속 로직을 실행 &amp;rarr; Unity에서 에러 발생&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ContinueWithOnMainThread : 작업 완료 후 실행 흐름을 유니티 메인 스레드로 강제 전환하여 안전하게 실행&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;간단하게 Unity는 라이프사이클 정책상 싱글쓰레드로 동작하기 때문에 ContinueWithOnMainThread를 사용하는 것이다.&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;</description>
      <category>Unity</category>
      <category>auth</category>
      <category>C#</category>
      <category>ContinueWithOnMainThread</category>
      <category>firebase</category>
      <category>Network</category>
      <category>unity</category>
      <category>구글로그인</category>
      <category>로그인</category>
      <category>소셜로그인</category>
      <author>ellug</author>
      <guid isPermaLink="true">https://ellug.tistory.com/88</guid>
      <comments>https://ellug.tistory.com/88#entry88comment</comments>
      <pubDate>Sun, 11 Jan 2026 15:17:19 +0900</pubDate>
    </item>
    <item>
      <title>Unity Network : Photon Pun2 - PhotonView, IPunObservable, RPC</title>
      <link>https://ellug.tistory.com/87</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;PhotonView&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Photon 네트워크에서 동기화 단위의 기준점&lt;/li&gt;
&lt;li&gt;네트워크 상에서 하나의 오브젝트를 식별하는 ID&lt;/li&gt;
&lt;li&gt;RPC, 변수 동기화, Ownership 판단의 기준&lt;/li&gt;
&lt;li&gt;PhotonView가 없는 오브젝트는 네트워크에 존재하지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 49.0694%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 19.4466%;&quot;&gt;역할&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 24.449%;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.4466%;&quot;&gt;네트워크 ID&lt;/td&gt;
&lt;td style=&quot;width: 24.449%;&quot;&gt;ViewID로 객체 식별&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.4466%;&quot;&gt;소유권 관리&lt;/td&gt;
&lt;td style=&quot;width: 24.449%;&quot;&gt;Owner / IsMine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.4466%;&quot;&gt;RPC 송신&lt;/td&gt;
&lt;td style=&quot;width: 24.449%;&quot;&gt;View 기준 호출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.4466%;&quot;&gt;변수 동기화&lt;/td&gt;
&lt;td style=&quot;width: 24.449%;&quot;&gt;Observe 대상 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1767772033333&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PhotonView pv;

pv.IsMine				// 내가 소유한 오브젝트인가
pv.Owner				// 소유 Player
pv.Owner.ActorNumber	// pv의 인덱스처럼 1000씩 증가하는 고유 값
pv.ViewID				// 네트워크 고유 ID

pv.RPC(함수명, RpcTarget.Others, 함수 파라미터);&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;IPunObservable : 변수 동기화&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변수의 상태를 지속적으로 동기화하는 방식&lt;/li&gt;
&lt;li&gt;위치, 회전, 체력, 속도 등&lt;/li&gt;
&lt;li&gt;상태(State) 동기화 활용&lt;/li&gt;
&lt;li&gt;매 프레임 or 고정 주기로 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1767772276480&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class PlayerSync : MonoBehaviour, IPunObservable
{
    public float hp;

    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting)
            stream.SendNext(hp);
        else
            hp = (float)stream.ReceiveNext();
    }
}&lt;/code&gt;&lt;/pre&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;Writing : Owner가 데이터 전송&lt;/li&gt;
&lt;li&gt;Readding : 다른 클라이언트가 수신&lt;/li&gt;
&lt;li&gt;Owner &amp;rarr; Others 단방향&lt;/li&gt;
&lt;li&gt;PhotonView의 Observe 대상에 등록되어야 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 53.3721%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.5114%; text-align: center;&quot;&gt;적합한 경우&lt;/td&gt;
&lt;td style=&quot;width: 26.1345%; text-align: center;&quot;&gt;부적합한 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.5114%;&quot;&gt;위치, 회전&lt;/td&gt;
&lt;td style=&quot;width: 26.1345%;&quot;&gt;단발성 이벤트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.5114%;&quot;&gt;체력, 쿨타임&lt;/td&gt;
&lt;td style=&quot;width: 26.1345%;&quot;&gt;버튼 클릭&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.5114%;&quot;&gt;애니메이션 파라미터&lt;/td&gt;
&lt;td style=&quot;width: 26.1345%;&quot;&gt;공격 판정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RPC (Remote Procedure Call) : 함수 동기화&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드 호출 사실을 네트워크로 전파하는 방식&lt;/li&gt;
&lt;li&gt;이벤트 기반&lt;/li&gt;
&lt;li&gt;한 번만 실행&lt;/li&gt;
&lt;li&gt;명확한 타이밍 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1767772495163&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[PunRPC]
void PlayHitEffect(int damage)
{
    Debug.Log($&quot;Hit: {damage}&quot;);
}

pv.RPC(&quot;PlayHitEffect&quot;, RpcTarget.All, 10);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RPC에 사용할 메서드에는 [PunRPC] 해줘야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RPC Target 종류&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 56.9767%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.2558%;&quot;&gt;All&lt;/td&gt;
&lt;td style=&quot;width: 33.6046%;&quot;&gt;모든 플레이어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.2558%;&quot;&gt;Others&lt;/td&gt;
&lt;td style=&quot;width: 33.6046%;&quot;&gt;나 제외&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.2558%;&quot;&gt;MasterClient&lt;/td&gt;
&lt;td style=&quot;width: 33.6046%;&quot;&gt;방장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.2558%;&quot;&gt;AllBuffered&lt;/td&gt;
&lt;td style=&quot;width: 33.6046%;&quot;&gt;늦게 들어온 플레이어도 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;IPunObservable vs RPC&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 67.093%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 17.4419%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;IPunObservable&lt;/td&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;RPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 17.4419%;&quot;&gt;성격&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;상태 동기화&lt;/td&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;이벤트 전달&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 17.4419%;&quot;&gt;호출 방식&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;자동&lt;/td&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;수동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 17.4419%;&quot;&gt;빈도&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;지속적&lt;/td&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;단발&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 17.4419%;&quot;&gt;신뢰성&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;상태 기준&lt;/td&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;호출 기준&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 17.4419%;&quot;&gt;대표 용도&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;위치, 체력&lt;/td&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;공격, 이펙트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;IPunObservable 과 RPC 중 무엇을 어디에 사용할지 적절히 설계하는 것이 중요&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작 예시 시나리오&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;입력은 IsMine 에서만 처리&lt;/p&gt;
&lt;pre id=&quot;code_1767772681977&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (!pv.IsMine) return;
HandleInput();&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;size18&quot;&gt;상태는 IPunObservable, 결과는 RPC&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;체력 감소 : IPunObservable&lt;/li&gt;
&lt;li&gt;피격 이펙트 : RPC&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;RPC 남발은 네트워크 부하의 원인이 될 수 있으므로 상태 동기화는 Observable로 해결하는 게 좋다고 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 모든 클라이언트에서 로직을 실행하지 않도록 주의하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 로직을 누가 실행할지를 잘 설계하고 방어코드를 작성해야 한다.&lt;/p&gt;</description>
      <category>Unity</category>
      <category>C#</category>
      <category>IPunObservable</category>
      <category>Network</category>
      <category>Photon</category>
      <category>PhotonView</category>
      <category>RPC</category>
      <category>unity</category>
      <category>네트워크</category>
      <category>유니티</category>
      <category>포톤</category>
      <author>ellug</author>
      <guid isPermaLink="true">https://ellug.tistory.com/87</guid>
      <comments>https://ellug.tistory.com/87#entry87comment</comments>
      <pubDate>Wed, 7 Jan 2026 17:04:32 +0900</pubDate>
    </item>
  </channel>
</rss>