<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>부귀영화</title>
    <link>https://mannered.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Mon, 29 Jun 2026 14:42:00 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Jinhoda</managingEditor>
    <image>
      <title>부귀영화</title>
      <url>https://tistory1.daumcdn.net/tistory/4390987/attach/f70b527efa9b4a3a8d18ba6dbf90b633</url>
      <link>https://mannered.tistory.com</link>
    </image>
    <item>
      <title>Sullog 개인정보취급방침</title>
      <link>https://mannered.tistory.com/16</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 전진호 &amp;gt;('Sullog')은(는) 「개인정보 보호법」 제30조에 따라 정보주체의 개인정보를 보호하고 이와 관련한 고충을 신속하고 원활하게 처리할 수 있도록 하기 위하여 다음과 같이 개인정보 처리방침을 수립&amp;middot;공개합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;○ 이 개인정보처리방침은&lt;span&gt;&amp;nbsp;&lt;/span&gt;2022년&lt;span&gt;&amp;nbsp;&lt;/span&gt;1월&lt;span&gt;&amp;nbsp;&lt;/span&gt;1부터 적용됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제1조(개인정보의 처리 목적)&lt;br /&gt;&lt;br /&gt;&amp;lt; 전진호 &amp;gt;('Sullog')은(는) 다음의 목적을 위하여 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며 이용 목적이 변경되는 경우에는 「개인정보 보호법」 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제2조(개인정보의 처리 및 보유 기간)&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;①&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt; 전진호 &amp;gt;은(는) 법령에 따른 개인정보 보유&amp;middot;이용기간 또는 정보주체로부터 개인정보를 수집 시에 동의받은 개인정보 보유&amp;middot;이용기간 내에서 개인정보를 처리&amp;middot;보유합니다.&lt;br /&gt;&lt;br /&gt;② 각각의 개인정보 처리 및 보유 기간은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;1.&amp;lt;재화 또는 서비스 제공&amp;gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&amp;lt;재화 또는 서비스 제공&amp;gt;와 관련한 개인정보는 수집.이용에 관한 동의일로부터&amp;lt;지체없이 파기&amp;gt;까지 위 이용목적을 위하여 보유.이용됩니다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;보유근거 : 서비스 제공&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;관련법령 : 소비자의 불만 또는 분쟁처리에 관한 기록 : 3년&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;예외사유 :&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제3조(개인정보의 제3자 제공)&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;①&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt; 전진호 &amp;gt;은(는) 개인정보를 제1조(개인정보의 처리 목적)에서 명시한 범위 내에서만 처리하며, 정보주체의 동의, 법률의 특별한 규정 등 「개인정보 보호법」 제17조 및 제18조에 해당하는 경우에만 개인정보를 제3자에게 제공합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;②&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #4374ac;&quot;&gt;&amp;lt; 전진호 &amp;gt;&lt;/span&gt;은(는) 다음과 같이 개인정보를 제3자에게 제공하고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;1. &amp;lt; 전진호 &amp;gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;개인정보를 제공받는 자 : 전진호&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;제공받는 자의 개인정보 이용목적 : 서비스 이용 기록&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;제공받는 자의 보유.이용기간: 지체없이 파기&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제4조(개인정보처리 위탁)&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;①&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt; 전진호 &amp;gt;은(는) 원활한 개인정보 업무처리를 위하여 다음과 같이 개인정보 처리업무를 위탁하고 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;②&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #4374ac;&quot;&gt;&amp;lt; 전진호 &amp;gt;&lt;/span&gt;은(는) 위탁계약 체결시 「개인정보 보호법」 제26조에 따라 위탁업무 수행목적 외 개인정보 처리금지, 기술적․관리적 보호조치, 재위탁 제한, 수탁자에 대한 관리․감독, 손해배상 등 책임에 관한 사항을 계약서 등 문서에 명시하고, 수탁자가 개인정보를 안전하게 처리하는지를 감독하고 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;③ 위탁업무의 내용이나 수탁자가 변경될 경우에는 지체없이 본 개인정보 처리방침을 통하여 공개하도록 하겠습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제5조(정보주체와 법정대리인의 권리&amp;middot;의무 및 그 행사방법)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;① 정보주체는 전진호에 대해 언제든지 개인정보 열람&amp;middot;정정&amp;middot;삭제&amp;middot;처리정지 요구 등의 권리를 행사할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;② 제1항에 따른 권리 행사는전진호에 대해 「개인정보 보호법」 시행령 제41조제1항에 따라 서면, 전자우편, 모사전송(FAX) 등을 통하여 하실 수 있으며 전진호은(는) 이에 대해 지체 없이 조치하겠습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;③ 제1항에 따른 권리 행사는 정보주체의 법정대리인이나 위임을 받은 자 등 대리인을 통하여 하실 수 있습니다.이 경우 &amp;ldquo;개인정보 처리 방법에 관한 고시(제2020-7호)&amp;rdquo; 별지 제11호 서식에 따른 위임장을 제출하셔야 합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;④ 개인정보 열람 및 처리정지 요구는 「개인정보 보호법」 제35조 제4항, 제37조 제2항에 의하여 정보주체의 권리가 제한 될 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;⑤ 개인정보의 정정 및 삭제 요구는 다른 법령에서 그 개인정보가 수집 대상으로 명시되어 있는 경우에는 그 삭제를 요구할 수 없습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;⑥ 전진호은(는) 정보주체 권리에 따른 열람의 요구, 정정&amp;middot;삭제의 요구, 처리정지의 요구 시 열람 등 요구를 한 자가 본인이거나 정당한 대리인인지를 확인합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제6조(처리하는 개인정보의 항목 작성)&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;①&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt; 전진호 &amp;gt;은(는) 다음의 개인정보 항목을 처리하고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;1&amp;lt; 재화 또는 서비스 제공 &amp;gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;필수항목 : 서비스 이용 기록&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;선택항목 :&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제7조(개인정보의 파기)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;① &amp;lt; 전진호 &amp;gt; 은(는) 개인정보 보유기간의 경과, 처리목적 달성 등 개인정보가 불필요하게 되었을 때에는 지체없이 해당 개인정보를 파기합니다.&lt;br /&gt;&lt;br /&gt;② 정보주체로부터 동의받은 개인정보 보유기간이 경과하거나 처리목적이 달성되었음에도 불구하고 다른 법령에 따라 개인정보를 계속 보존하여야 하는 경우에는, 해당 개인정보를 별도의 데이터베이스(DB)로 옮기거나 보관장소를 달리하여 보존합니다.&lt;br /&gt;1. 법령 근거 :&lt;br /&gt;2. 보존하는 개인정보 항목 : 계좌정보, 거래날짜&lt;br /&gt;&lt;br /&gt;③ 개인정보 파기의 절차 및 방법은 다음과 같습니다.&lt;br /&gt;1. 파기절차&lt;br /&gt;&amp;lt; 전진호 &amp;gt; 은(는) 파기 사유가 발생한 개인정보를 선정하고, &amp;lt; 전진호 &amp;gt; 의 개인정보 보호책임자의 승인을 받아 개인정보를 파기합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 파기방법&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전자적 파일 형태의 정보는 기록을 재생할 수 없는 기술적 방법을 사용합니다&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제8조(개인정보의 안전성 확보 조치)&lt;br /&gt;&lt;br /&gt;&amp;lt; 전진호 &amp;gt;은(는) 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취하고 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 개인정보에 대한 접근 제한&lt;br /&gt;개인정보를 처리하는 데이터베이스시스템에 대한 접근권한의 부여,변경,말소를 통하여 개인정보에 대한 접근통제를 위하여 필요한 조치를 하고 있으며 침입차단시스템을 이용하여 외부로부터의 무단 접근을 통제하고 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제9조(개인정보 자동 수집 장치의 설치&amp;bull;운영 및 거부에 관한 사항)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;전진호 은(는) 정보주체의 이용정보를 저장하고 수시로 불러오는 &amp;lsquo;쿠키(cookie)&amp;rsquo;를 사용하지 않습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제10조 (개인정보 보호책임자)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;①&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #4374ac;&quot;&gt;전진호&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;은(는) 개인정보 처리에 관한 업무를 총괄해서 책임지고, 개인정보 처리와 관련한 정보주체의 불만처리 및 피해구제 등을 위하여 아래와 같이 개인정보 보호책임자를 지정하고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;▶ 개인정보 보호책임자&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;성명 :전진호&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;직책 :개발자&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;직급 :개발자&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;연락처 :01023709940, jinhoda_@naver.com,&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;※ 개인정보 보호 담당부서로 연결됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;▶ 개인정보 보호 담당부서&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;부서명 :개인정보 보호 담당부서&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;담당자 :전진호&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;연락처 :01023709940, jinhoda_@naver.com,&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;② 정보주체께서는 전진호 의 서비스(또는 사업)을 이용하시면서 발생한 모든 개인정보 보호 관련 문의, 불만처리, 피해구제 등에 관한 사항을 개인정보 보호책임자 및 담당부서로 문의하실 수 있습니다. 전진호 은(는) 정보주체의 문의에 대해 지체 없이 답변 및 처리해드릴 것입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제11조(개인정보 열람청구)&lt;br /&gt;정보주체는 ｢개인정보 보호법｣ 제35조에 따른 개인정보의 열람 청구를 아래의 부서에 할 수 있습니다.&lt;br /&gt;&amp;lt; 전진호 &amp;gt;은(는) 정보주체의 개인정보 열람청구가 신속하게 처리되도록 노력하겠습니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;▶ 개인정보 열람청구 접수&amp;middot;처리 부서&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;부서명 : 개발부서&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;담당자 : 전진호&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;연락처 : 01023709940, jinhoda_@naver.com,&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제12조(권익침해 구제방법)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;정보주체는 개인정보침해로 인한 구제를 받기 위하여 개인정보분쟁조정위원회, 한국인터넷진흥원 개인정보침해신고센터 등에 분쟁해결이나 상담 등을 신청할 수 있습니다. 이 밖에 기타 개인정보침해의 신고, 상담에 대하여는 아래의 기관에 문의하시기 바랍니다.&lt;br /&gt;&lt;br /&gt;1. 개인정보분쟁조정위원회 : (국번없이) 1833-6972 (www.kopico.go.kr)&lt;br /&gt;2. 개인정보침해신고센터 : (국번없이) 118 (privacy.kisa.or.kr)&lt;br /&gt;3. 대검찰청 : (국번없이) 1301 (www.spo.go.kr)&lt;br /&gt;4. 경찰청 : (국번없이) 182 (ecrm.cyber.go.kr)&lt;br /&gt;&lt;br /&gt;「개인정보보호법」제35조(개인정보의 열람), 제36조(개인정보의 정정&amp;middot;삭제), 제37조(개인정보의 처리정지 등)의 규정에 의한 요구에 대 하여 공공기관의 장이 행한 처분 또는 부작위로 인하여 권리 또는 이익의 침해를 받은 자는 행정심판법이 정하는 바에 따라 행정심판을 청구할 수 있습니다.&lt;br /&gt;&lt;br /&gt;※ 행정심판에 대해 자세한 사항은 중앙행정심판위원회(www.simpan.go.kr) 홈페이지를 참고하시기 바랍니다.&lt;/p&gt;</description>
      <author>Jinhoda</author>
      <guid isPermaLink="true">https://mannered.tistory.com/16</guid>
      <comments>https://mannered.tistory.com/16#entry16comment</comments>
      <pubDate>Sun, 10 Sep 2023 14:59:40 +0900</pubDate>
    </item>
    <item>
      <title>전역 상태 관리</title>
      <link>https://mannered.tistory.com/15</link>
      <description>&lt;h1&gt;전역 상태 관리&lt;/h1&gt;
&lt;h3&gt;상태란?&lt;/h3&gt;
&lt;p&gt;React에서 말하는 상태란 &lt;code&gt;Plain JavaScritp Object hold information influences the output of render&lt;/code&gt; . 즉, &lt;strong&gt;렌더링 결과에 영향을 미치는&lt;/strong&gt; 정보를 가진 자바스크립트 객체를 의미한다. &lt;/p&gt;
&lt;p&gt;더 쉽고 직관적으로 나타내자면 시간에 따라서 동적으로 변할 수 있는 정보라고 할 수 있다.&lt;/p&gt;
&lt;h3&gt;전역이란?&lt;/h3&gt;
&lt;p&gt;어떠한 곳에서도 접근할 수 있는 영역을 의미한다. 단순히 하나의 페이지를 의미하는 것이 아닌 컴포넌트의 외부와 애플리케이션 전체에서 접근이 가능하다는 것이다.&lt;/p&gt;
&lt;p&gt;그렇다면 전역 상태란 어떠한 곳에서도 접근할 수 있으면서 렌더링 결과에 영향을 미치는 것을 의미한다. &lt;/p&gt;
&lt;p&gt;프론트엔드를 개발하다보면 서로 다른 맥락의 상태를 사용하게 된다. 이는 어디에서 정보를 접근 또는 변경할 수 있는 권한을 가지고 있는 지에 따라 달라진다. 서버에서 변경되며 서버로부터 받아오는 정보와, 클라이언트단에서 생성하고 변경할 수 있는 정보가 있다.&lt;/p&gt;
&lt;p&gt;전자는 최신 글 목록, 프로필 정도 등이 있을 것이고, 후자는 다크모드/라이트모드 또는 검색바 등이 있을 것이다.&lt;/p&gt;
&lt;p&gt;후자의 경우는 관리하기 쉬우니 전자의 경우를 다루어 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;function App() {
    const [todos, setTodos] = useState([])

    useEffect(()=&amp;gt; {
        const fetchTodos = async() =&amp;gt; {
            const res = await fetch(&amp;#39;/api/v/todos&amp;#39;)
            const data = await res.json()
            setTodos(data)
        }
    }, [])

    return(
        &amp;lt;div&amp;gt;
            {todos.map((todo) =&amp;gt; {
                &amp;lt;h1 key={todo.id}&amp;gt;{todo.title}&amp;lt;/h1&amp;gt;
            }
        &amp;lt;/div&amp;gt;
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;서버 API로부터 데이터를 받아 렌더링하는 react 코드를 작성할 때 우리는 대부분 위와 같은 방식을 사용한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;데이터를 비동기적으로 받아온다&lt;/li&gt;
&lt;li&gt;데이터를 컴포넌트의 상태로 정의한다&lt;/li&gt;
&lt;li&gt;상태를 불러와서 렌더링한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;하지만 매번 이와 같은 방식으로 데이터를 호출해서 사용하는 것은 너무 불편해보인다. 따라서 우리는 지금까지 redux를 사용해 한 곳에서 데이터를 요청하고 그것을 전역에서 접근할 수 있는 store에 저장해서 사용했다. 하지만 이것은 여러가지 문제점을 발생시킨다.&lt;/p&gt;
&lt;h3&gt;필요한 값이 store에 존재한다는 확신&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;function TransactionListA() {
  const transactions = useSelector(state =&amp;gt; state.transaction)

  return &amp;lt;ul&amp;gt;{/* render transaction */}&amp;lt;/ul&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;우리는 이런 식으로 컴포넌트를 재사용할 수 있을 것이라는 기대를 한다. 하지만 store.transaction이 비어있다면 어떻게 할 것인가? 이전에 단 한번도 dispatch를 통해 fetchTransactions 액션을 발생시키지 않았다면 어떨까?&lt;/p&gt;
&lt;p&gt;이 컴포넌트가 마운트 되기 이전에 dispatch가 되어야 state에 접근할 수 있도록 하는 것이 필요하지만, redux는 이를 강요하지 않는다. &lt;/p&gt;
&lt;p&gt;우리가 필요로 할 때 데이터를 불러와야 하지만 이 방식으로는 필요로 할 때 데이터가 불러와지지 않는 것이다.&lt;/p&gt;
&lt;h3&gt;현재 접근하는 데이터가 가장 최신의 것이라는 확신&lt;/h3&gt;
&lt;p&gt;만약 앱을 처음 실행해서 홈 화면에 접속했을 때 dispatch하고 이후 다른 화면에서 다시 그 정보를 불러올 때, 지금 store에서 불러온 정보가 fresh하다는 것을 보장하지 못한다. 클라이언트 단에서 보고 있는 정보와 서버에 있는 정보가 일치하지 않게 되는 문제가 발생하는 것이다. 이러한 정보를 ‘stale response’라고 한다.&lt;/p&gt;
&lt;h3&gt;해결방법&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;stale-while-revalidate&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;캐싱 전략&lt;/li&gt;
&lt;li&gt;stale-while-revalidate가 유지되는 동안은 캐시된 값이 이미 stale이지만 그냥 반환하고, 백그라운드에서 캐시를 fresh하게 만들기 위한 재검증 요청이 이루어진다.&lt;/li&gt;
&lt;li&gt;stale-while-revalidate가 끝나면 서버에 새로운 요청을 보낸다.&lt;/li&gt;
&lt;li&gt;이 전략을 사용한 라이브러리가 SWR, React Query.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;근본적인 해결 방법&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;공통된 상태를 공유하는 컴포넌트들을 Provider로 묶어서 전역이 아닌 해당 컴포넌트들에서만 사용할 수 있도록 제한&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/React</category>
      <author>Jinhoda</author>
      <guid isPermaLink="true">https://mannered.tistory.com/15</guid>
      <comments>https://mannered.tistory.com/15#entry15comment</comments>
      <pubDate>Wed, 6 Apr 2022 23:46:33 +0900</pubDate>
    </item>
    <item>
      <title>토스 프론트엔드 기술면접 후기</title>
      <link>https://mannered.tistory.com/14</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;22.04.06 토스 기술면접 불같이   뜨거운 (불)합격 &amp;nbsp; 후기&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서류 전형과 기술 과제를 통과하고 기술 면접을 열심히 준비했지만 아쉽게도 뜨거운 합격을 맛보게 되었다. 사전 과제에서 답변한 항목과 관련된 JS, React 전반적인 지식과 실무 과제로 제출한 코드를 커밋 별로 혼자 리뷰하면서 준비했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술 면접은 시작하기 20분 전 리쿠르팅 매니저분께서 도와주셔서 구글 미트를 통해 진행했다. 곧이어 면접관분들이 들어오셨고 간단한 인사와 함께 곧바로 사전 과제 리뷰를 시작했다. 에디터 화면을 공유하면서 문제의 요구 사항을 만족하기 위해 어떤 코드를 작성했는 지를 나름 열심히 대답했다.&lt;/p&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;그다음으로는 다른 사전 과제의 답변들에 대한 추가 질문을 받았는데, 응집도와 결합도를 반대로 말하거나 Redux 라이브러리를 깊게 이해하지 못하고 엉뚱한 답변을 하고 말았다. 전역 상태를 남발하면 안 된다고 답변을 했는데 이유는 잘 대답했지만 그에 대한 해결법은 무엇인지를 제시하지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금에서야 나름 대답하자면 전역으로 상태를 관리하는 것이 아닌 해당 상태를 공유하는 컴포넌트들을 Provider로 묶어서 관리하거나 children props를 이용해 prop drilling 하는 것을 해결법으로 제시할 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 전역 상태 관리 라이브러리를 사용할 때의 문제점 또한 질문 받았는데 어렴풋이 기억하는 얄팍한 지식으로 서버에 있는 데이터와 클라이언트에 있는 데이터의 상태가 일관적이지 않을 수 있다는 답변을 했다. 아쉽게도 자세하기 설명하지는 못해서 이를 &lt;a href=&quot;https://mannered.tistory.com/15&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;정리하는 글&lt;/a&gt;을 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전과제에 대한 리뷰가 끝나고 이력서에 적힌 프로젝트와 관련된 질문을 받았는데, Mount 앱을 개발하면서 스크롤이 될 때 헤더가 줄어드는 기능을 구현할 때 로컬에서 전역 상태로 변환하니 개선되었다고 답변을 했는데, &lt;b&gt;왜? 어떻게?&lt;/b&gt; 전역 상태로 구현하지 그러한 효과가 나타났는 지를 전혀 설명하지 못했다. 또 스크롤 시 짧은 시간 내에 많은 액션을 생성하게 될 때 전역 상태 관리 라이브러리에서 변경 사항의 순서를 보장할 수 있는 방법을 물어보셨는데 구체적인 대답을 하지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(면접이 끝나고 해당 기능을 리팩토링 하면서 로컬/전역 상태가 원인이 아닌 React Native Animated의 useNativeDriver를 사용하지 않은 것이 원인이었다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 역으로 면접관분들에게 물어보는 시간이 있었는데, 평범한 질문들을 하고 난 다음 토스 사이트에서 발견했던 오류를 제보했다. &lt;a href=&quot;https://toss.im/&quot;&gt;https://toss.im/&lt;/a&gt; 에서 콘솔을 보면 알겠지만 로드되지 않는 이미지들이 있는데, 이는 newtossim이 new tossim로 space문자가 URL Encoding 돼서 new%20 tossim로 인식되어 나타나는 문제였다. 제보해 주셔서 고맙다는 말씀을 들었다.&lt;/p&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>
      <author>Jinhoda</author>
      <guid isPermaLink="true">https://mannered.tistory.com/14</guid>
      <comments>https://mannered.tistory.com/14#entry14comment</comments>
      <pubDate>Wed, 6 Apr 2022 23:44:24 +0900</pubDate>
    </item>
    <item>
      <title>Chimha 배포 완료</title>
      <link>https://mannered.tistory.com/13</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;App Store 심사를 무사히 통과했다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히도 이전 CopyCats 문제는 침착맨님 매니저분께 직접 연락드려 동의서를 받아 해결할 수 있었다.&lt;/p&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-filename=&quot;스크린샷 2022-03-24 오후 5.12.59.png&quot; data-origin-width=&quot;1716&quot; data-origin-height=&quot;914&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ETe0q/btrxcgXFz5U/w6gzKSHJAndz1RTWS6qZM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ETe0q/btrxcgXFz5U/w6gzKSHJAndz1RTWS6qZM0/img.png&quot; data-alt=&quot;22일 오후에 다시 심사를 요청했는데 일처리가 아주 빠르다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ETe0q/btrxcgXFz5U/w6gzKSHJAndz1RTWS6qZM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FETe0q%2FbtrxcgXFz5U%2Fw6gzKSHJAndz1RTWS6qZM0%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;1716&quot; height=&quot;914&quot; data-filename=&quot;스크린샷 2022-03-24 오후 5.12.59.png&quot; data-origin-width=&quot;1716&quot; data-origin-height=&quot;914&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;22일 오후에 다시 심사를 요청했는데 일처리가 아주 빠르다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 사람들은 심사에 막 1주일 걸리고 3~4번 리젝 먹는다는데 어찌된 일인지 단번에 통과했다. 아쉽게도 이력서에는 배포 중이라고 적었지만 만약 서류에 통과한다면 자신있게 App Store에서 배포 중이라고 말할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2022-03-24-17-16-33.png&quot; data-origin-width=&quot;151&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Gx97P/btrxcxrxGy7/Q1wSeu1HYN43ZKCSG37tRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Gx97P/btrxcxrxGy7/Q1wSeu1HYN43ZKCSG37tRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Gx97P/btrxcxrxGy7/Q1wSeu1HYN43ZKCSG37tRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGx97P%2FbtrxcxrxGy7%2FQ1wSeu1HYN43ZKCSG37tRk%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;151&quot; height=&quot;124&quot; data-filename=&quot;KakaoTalk_Photo_2022-03-24-17-16-33.png&quot; data-origin-width=&quot;151&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심지어 엔터테인먼트앱 분야에서 92위! 침착맨 카페 횐님들의 공이 크다. 끝까지 개발해서 앱스토어에 배포한 것도 자랑스럽고, &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;한달이라는 짧은 시간 동안 진행한 프로젝트이지만 새로운 기술도 많이 배우고 직접 도입할 수 있었던 정말 뜻깊은 프로젝트였다. 다음에는 재밌는 GraphQl API를 써서 NextJS와 GraphQl를 적용한 프로젝트를 진행해볼 것이다.&lt;/p&gt;</description>
      <category>개발/프로젝트</category>
      <author>Jinhoda</author>
      <guid isPermaLink="true">https://mannered.tistory.com/13</guid>
      <comments>https://mannered.tistory.com/13#entry13comment</comments>
      <pubDate>Thu, 24 Mar 2022 17:21:03 +0900</pubDate>
    </item>
    <item>
      <title>App Store 4.1.0 Design: Copycats</title>
      <link>https://mannered.tistory.com/12</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Chimha 앱스토어 심사가 떨어졌다. 거절 사유는 &lt;span style=&quot;background-color: #fafafa;&quot;&gt;4.1.0 Design: Copycats&lt;/span&gt; 로 전문은 아래와 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Guideline 4.1 - Design - Copycats&lt;br /&gt;&lt;br /&gt;Your app or its metadata appears to contain misleading content.&lt;br /&gt;&lt;br /&gt;Specifically, your app includes content that resembles 침착맨.&lt;br /&gt;&lt;br /&gt;Next Steps&lt;br /&gt;&lt;br /&gt;You may attach documentary evidence in the App Review Information section in App Store Connect. In accordance with section 3.2(f) of the Apple Developer Program License Agreement, you acknowledge that submitting falsified or fraudulent documentation can result in the termination of your Apple Developer Program account and the removal of your apps from the App Store. Once Legal has reviewed your documentation and confirms its validity, we will proceed with the review of your app.&lt;br /&gt;&lt;br /&gt;Alternatively, please remove the third-party content from your app and its metadata.&lt;/blockquote&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>개발/프로젝트</category>
      <author>Jinhoda</author>
      <guid isPermaLink="true">https://mannered.tistory.com/12</guid>
      <comments>https://mannered.tistory.com/12#entry12comment</comments>
      <pubDate>Fri, 18 Mar 2022 15:48:27 +0900</pubDate>
    </item>
    <item>
      <title>Chimha 프로젝트 회고 + 문제 해결 경험..</title>
      <link>https://mannered.tistory.com/11</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-03-16 오후 5.11.41.png&quot; data-origin-width=&quot;1702&quot; data-origin-height=&quot;122&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zBElY/btrv9KMykxA/EDuXNfdKowRgiMwLerXdV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zBElY/btrv9KMykxA/EDuXNfdKowRgiMwLerXdV1/img.png&quot; data-alt=&quot;침투부 카페&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zBElY/btrv9KMykxA/EDuXNfdKowRgiMwLerXdV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzBElY%2Fbtrv9KMykxA%2FEDuXNfdKowRgiMwLerXdV1%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;1702&quot; height=&quot;122&quot; data-filename=&quot;스크린샷 2022-03-16 오후 5.11.41.png&quot; data-origin-width=&quot;1702&quot; data-origin-height=&quot;122&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;침투부 카페&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년 11월, 침착맨 팬 카페에서 어떤 분이 만드신 어플 디자인을 보고 만들기 시작했다. 하지만 당시에는 동아리 WIT에서 한창 Mount 앱을 개발 중이었고, 기말고사도 남아있어 프로젝트가 마무리되면 바로 시작하기로 마음 먹었다. 그렇게 2월 12일 동아리 최종 발표가 끝나고 곧바로 만들기 시작하여 3월 15일 앱스토어 등록 심사 중에 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;span&gt;+ 3월 17일 심사 거부가 떴다.. 또르르&lt;/span&gt;  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mount 앱이 거의 4개월 동안 개발했던 것을 생각하면 정말정말 생각했던 것보다 빠른 시간 내에 만들었다. 방학이어서 개백수였던 것과 여러가지 신기술(Typescript, React Query)의 도입, 그리고 이전 Mount 개발하면서 쌓인 노하우들이 기간 단축이 도움이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;+ Boilerplate.. ✨&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면은 유튜브 화면, 트위치 화면, 웹툰 화면, 카페 화면, 스토어 화면으로 순서대로 구현했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;chimha2.jpg&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;2688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgMFtX/btrv6eVCZyT/HTB4EcNpkvK0087OYaHQb1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgMFtX/btrv6eVCZyT/HTB4EcNpkvK0087OYaHQb1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgMFtX/btrv6eVCZyT/HTB4EcNpkvK0087OYaHQb1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgMFtX%2Fbtrv6eVCZyT%2FHTB4EcNpkvK0087OYaHQb1%2Fimg.jpg&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;350&quot; height=&quot;757&quot; data-filename=&quot;chimha2.jpg&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;2688&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;상단의 YouTube 컴포넌트의 '침착맨', '침착맨+', '침착맨 원본 박물관' 버튼을 누르면 해당 유튜브 채널로 이동한다. React Native에서 기본적으로 제공하는 Linking을 이용해서 구현해서 만약 유튜브 앱이 있어서 앱을 통해 열 수 있으면 앱을 열고, 만약 그렇게 할 수 없다면 브라우저를 통해 열리도록 구현했다.&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;상단에 위치하게 되는 채널들은 이미 정해져있기 때문에 썸네일, 채널명, 채널 설명 등을 미리 저장해서 쓸 수 있었지만, 채널 id만을 저장해서 Youtube Data API에 그때그때 요청해서 불러오는 방식을 선택했다. 현재 침착맨 플러스라는 이름의 채널이 긴착맨, 짤착맨, 침착맨 더보기, 침착맨+ 로 변모해왔기 때문에...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하단에 Subcontents 컴포넌트에는 침착맨이 출연하는 다른 유튜브 채널들을 보여준다. 위의 방식과 동일하게 유튜브 채널의 id를 저장해두고 그때그때 불러와서 보여주도록 구현했다. 2열로 보여주기 위해 FlatList에 column을 2로 지정하여 구현하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;chimha3.jpg&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;2688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mEupC/btrwahcP9hM/IwlBNpZVeRejNuk8A3PBv0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mEupC/btrwahcP9hM/IwlBNpZVeRejNuk8A3PBv0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mEupC/btrwahcP9hM/IwlBNpZVeRejNuk8A3PBv0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmEupC%2FbtrwahcP9hM%2FIwlBNpZVeRejNuk8A3PBv0%2Fimg.jpg&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;351&quot; height=&quot;760&quot; data-filename=&quot;chimha3.jpg&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;2688&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;# 트위치 화면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 현재 스트리밍 중이라면 방송 중인 화면이 이미지로 보여지고, 오프라인이라면 저 이미지를 보여준다. 클릭하면 침착맨 트위치 채널로 이동한다. 유튜브 화면과 비슷하게 ID를 저장하고 Twitch API를 사용해서 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하단의 Twitch Crew 컴포넌트는 배도라지 멤버들의 트위치 채널들을 보여주며 클릭하면 이동한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;chimha4.jpg&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;2688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FFGzr/btrv8EsQopP/P47jlxhIvCZyrhw4fulrAk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FFGzr/btrv8EsQopP/P47jlxhIvCZyrhw4fulrAk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FFGzr/btrv8EsQopP/P47jlxhIvCZyrhw4fulrAk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFFGzr%2Fbtrv8EsQopP%2FP47jlxhIvCZyrhw4fulrAk%2Fimg.jpg&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;451&quot; height=&quot;976&quot; data-filename=&quot;chimha4.jpg&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;2688&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;# 웹툰 화면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;침착맨이 이말년 시절 연재하던 네이버 웹툰들을 보여준다. 클릭했을 때 네이버 웹툰 앱이 있다면 앱을 열고 해당 웹툰으로 이동한다. 제일 구현하기 쉽다고 생각했지만 의외의 곳에서 고전했는데 바로 네이버 웹툰의 Url Scheme(&lt;span style=&quot;background-color: #ffffff; color: #111111;&quot;&gt;fb455753897775430://&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #111111;&quot;&gt;)&lt;/span&gt;은 알고 있지만 특정한 웹툰 ID로 이동할 수 있는 방법을 알아낼 수 없었던 것이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1647438126579&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;twitch://stream/${value.data?.data[0].login}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트위치 앱은 위와 같이 /stream/{channel_login_id}로 해당 채널로 이동할 수 있다고&amp;nbsp;&lt;a href=&quot;https://dev.twitch.tv/docs/mobile-deeplinks&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;에 자세히 설명되어있다. 하지만 네이버 웹툰은 이러한 Deep Links Formats을 설명하는 공식 문서가 없었다. 첫 번째로 시도해본 방법은 무차별 대입이었다. &lt;a href=&quot;https://developers.naver.com/docs/utils/mobileapp/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;네이버 앱 URL Scheme연동 가이드&lt;/a&gt;&amp;nbsp;참고해서 &lt;span style=&quot;background-color: #ffffff; color: #111111;&quot;&gt;fb455753897775430://명령어?파라미터 형식에 맞추어 아래와 같은 방법을 시도해보았다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1647438387597&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fb455753897775430://list?titleid=790453&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-03-16 오후 10.45.48.png&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;74&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oGnX6/btrv4zenAbi/wYawzCOeUK5LZMBGH7m2JK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oGnX6/btrv4zenAbi/wYawzCOeUK5LZMBGH7m2JK/img.png&quot; data-alt=&quot;네이버 웹툰 url을 참고해서 위의 포맷 말고도 여러개를 시도해봤으나...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oGnX6/btrv4zenAbi/wYawzCOeUK5LZMBGH7m2JK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoGnX6%2Fbtrv4zenAbi%2FwYawzCOeUK5LZMBGH7m2JK%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;802&quot; height=&quot;74&quot; data-filename=&quot;스크린샷 2022-03-16 오후 10.45.48.png&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;74&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;네이버 웹툰 url을 참고해서 위의 포맷 말고도 여러개를 시도해봤으나...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전부 실패로 돌아가고 다른 방법을 고민했다. 그러던 중 모바일에서 네이버 웹툰에 접속하면 &lt;b&gt;앱으로 열기&lt;/b&gt; 버튼이 있는 것을 기억하고 곧바로 확인해보았다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-03-16 오후 10.49.33.png&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bajZP2/btrwb6BC4jK/YmQr3BRaTPlgwrpv2gSmE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bajZP2/btrwb6BC4jK/YmQr3BRaTPlgwrpv2gSmE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bajZP2/btrwb6BC4jK/YmQr3BRaTPlgwrpv2gSmE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbajZP2%2Fbtrwb6BC4jK%2FYmQr3BRaTPlgwrpv2gSmE0%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;610&quot; height=&quot;218&quot; data-filename=&quot;스크린샷 2022-03-16 오후 10.49.33.png&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시 웹툰앱으로 이동할 수 있는 요소가 있음을 확인했고 어떤 방식으로 앱을 여는지 분석해보았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-03-16 오후 10.50.28.png&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFRg5k/btrv7POhMuI/yN01kTkXGhf5p30PSnplHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFRg5k/btrv7POhMuI/yN01kTkXGhf5p30PSnplHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFRg5k/btrv7POhMuI/yN01kTkXGhf5p30PSnplHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFRg5k%2Fbtrv7POhMuI%2FyN01kTkXGhf5p30PSnplHk%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;1210&quot; height=&quot;456&quot; data-filename=&quot;스크린샷 2022-03-16 오후 10.50.28.png&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;456&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드의 경우 아래와 같이&amp;nbsp;intent scheme을 사용하는 것 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1647438780704&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;webtoonkr://contentList?version=2&amp;amp;titleId=769209&amp;amp;league=WEBTOON&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ios도 위와 비슷할 것이라고 생각해서&lt;/p&gt;
&lt;pre id=&quot;code_1647438993691&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;315795555://contentList?version=2&amp;amp;titleId=769209&amp;amp;league=WEBTOON&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 시도해보았으나 실패...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 data-ios-universal은 무엇인지 궁금했는데 &lt;a href=&quot;https://developer.apple.com/ios/universal-links/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.apple.com/ios/universal-links/&lt;/a&gt; 를 참고해보니 &lt;span style=&quot;background-color: #ffffff; color: #1f1f1f;&quot;&gt;Apple이 웹사이트를 거쳐 iOS 앱을 열 수 있게 하는 방식이라고 한다. 클릭했을 때는 data-ios-scheme-query가 뒤에 붙여져서 접속되었다. Deep Link로 &lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;a href=&quot;https://apps.comic.naver.com/launchApp/contentList?version=2&amp;amp;titleId=704595&amp;amp;league=WEBTOON&quot;&gt;https://apps.comic.naver.com/launchApp/contentList?version=2&amp;amp;titleId=704595&amp;amp;league=WEBTOON&lt;/a&gt;&lt;/span&gt;&amp;nbsp;를 해봤더니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침내 웹툰 앱을 실행하면서 해당 titleId를 가진 웹툰으로 이동했다!&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;URL scheme은 정말 웹툰앱을 '실행'만 시킬 수 있던 것일까?... Naver Developer Forum에도 물어봤지만 답변이 달리지는 않았다. 만약 나중에 네이버에 들어간다면(....) 아니 들어갈 수 있다면 어떻게 사용할 수 있는지 찾아보고 다른 개발자들이 참고할 수 있게 오픈하고 싶다. 나같은 사람이 있을 수 있으니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모바일에서 네이버 웹툰에 접속했을 때 앱으로 열기 버튼이 있다는 것을 생각해서 문제를 해결한건 좀 뿌듯했다 ☺️&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;# 네이버 카페 화면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;chimha5.jpg&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;2688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHARn4/btrwhchDmt8/0KDdb1CdQcNtogoSo3byK1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHARn4/btrwhchDmt8/0KDdb1CdQcNtogoSo3byK1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHARn4/btrwhchDmt8/0KDdb1CdQcNtogoSo3byK1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHARn4%2FbtrwhchDmt8%2F0KDdb1CdQcNtogoSo3byK1%2Fimg.jpg&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;450&quot; height=&quot;974&quot; data-filename=&quot;chimha5.jpg&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;2688&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현하기 전부터 제일 어려울 것이라 생각했던 화면이다. 네이버 카페는 글 불러오는 API가 없어서(가입/글쓰기만 있음) html 파일을 불러와서 파싱하고 Promise 객체로 넘겨서 React-Query의 InfiniteQuery를 사용해 사용자가 FlatList의 끝으로 스크롤하면 다음 페이지 데이터를 요청해서 추가하도록 구현해야 했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항이 상당했고 구현 과정에서도 애로사항이 많았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 네이버 카페 크롤링부터 살펴보자&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-03-17 오후 5.18.13.png&quot; data-origin-width=&quot;2100&quot; data-origin-height=&quot;1410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mVsIc/btrwgC8qsJq/7qqpTgMlW241eaNLNkxpYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mVsIc/btrwgC8qsJq/7qqpTgMlW241eaNLNkxpYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mVsIc/btrwgC8qsJq/7qqpTgMlW241eaNLNkxpYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmVsIc%2FbtrwgC8qsJq%2F7qqpTgMlW241eaNLNkxpYk%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;2100&quot; height=&quot;1410&quot; data-filename=&quot;스크린샷 2022-03-17 오후 5.18.13.png&quot; data-origin-width=&quot;2100&quot; data-origin-height=&quot;1410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 도구의 네트워크 탭에서 &lt;span style=&quot;background-color: #ffffff; color: #ffffff;&quot;&gt;&lt;a href=&quot;https://cafe.naver.com/ArticleList.nhn?search.clubid=29646865&amp;amp;userDisplay=30&amp;amp;search.boardtype=L&amp;amp;search.cafeId=29646865&amp;amp;search.page=1&amp;amp;search.menuid=1&quot;&gt;https://cafe.naver.com/ArticleList.nhn?search.clubid=29646865&amp;amp;userDisplay=30&amp;amp;search.boardtype=L&amp;amp;search.cafeId=29646865&amp;amp;search.page=1&amp;amp;search.menuid=1&lt;/a&gt;&lt;/span&gt; 로 문서를 불러오는 것을 확인했다. search.clubid로 카페를 구분하고 이외의 옵션으로는 몇 개의 글을 불러올 지를 결정하는 search.userDisplay, 게시글들을 보여주는 방식을 나타내는 search.boardtype, 앞의 clubid와 동일한 search.cafeId, 페이지네이션을 나타내는 search.page, 게시판 종류를 구분하는 search.menuid 등이 있었다. 그렇다면 search.clubid, search.userDisplay, search.boardtype, search.cafeId는 고정하고 페이지와 게시판 종류를 달리하여 요청하면 일반적인 게시판 기능을 구현할 수 있을 것이다. 그래서 테스트를 위해 postman을 활용해서 해당 url로 GET 요청을 보냈더니....&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-03-17 오후 7.04.22.png&quot; data-origin-width=&quot;1748&quot; data-origin-height=&quot;1216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5U5WQ/btrweyFPxzz/kSaqKVsuP6xIHi5fkemAp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5U5WQ/btrweyFPxzz/kSaqKVsuP6xIHi5fkemAp1/img.png&quot; data-alt=&quot;수많은 인코딩 오류.....&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5U5WQ/btrweyFPxzz/kSaqKVsuP6xIHi5fkemAp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5U5WQ%2FbtrweyFPxzz%2FkSaqKVsuP6xIHi5fkemAp1%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;759&quot; height=&quot;529&quot; data-filename=&quot;스크린샷 2022-03-17 오후 7.04.22.png&quot; data-origin-width=&quot;1748&quot; data-origin-height=&quot;1216&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수많은 인코딩 오류.....&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음.. Response Headers를 확인해보니 &lt;span style=&quot;background-color: #ffffff; color: #212121;&quot;&gt;content-type이 &lt;span style=&quot;background-color: #ffffff; color: #212121;&quot;&gt;text/html;charset=ms949였다. 따라서 axios로 받아온 데이터를 iconv로 decode해야 정상적인 html 파일을 받을 수 있었다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1647511960221&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const getHTML = async (url: string): Promise&amp;lt;string&amp;gt; =&amp;gt; {
  const response = await axios.request({
    method: &quot;GET&quot;,
    url: url,
    responseType: &quot;arraybuffer&quot;,
  });

  let html = response.data;
  let data = Buffer.from(html);
  return iconv.decode(data, &quot;cp949&quot;);
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ms949가 아닌 cp949를 사용한 이유는 ms949는 자바에서 사용하는 한글 확장 완성형 인코딩 방식이었기 때문이다. 아마 네이버에서 Spring으로 서비스하기 때문에 그렇게 온 것 같다. iconv로 dcode 하기 전에 string인 html 변수를 Buffer로 변환한 이유는 decode 함수가 Buffer형만 받았기 때문이다. 이렇게 정상적인 html 파일을 받아고 나서는 아래와 같이 cheerio로 크롤링해서 반환했다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1647512257299&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const getArticles = async (
  menuId: number,
  pageId: number,
): Promise&amp;lt;FetchArticle&amp;gt; =&amp;gt; {
  const url = `${BASE_URL}/ArticleList.nhn?search.clubid=29646865&amp;amp;userDisplay=50&amp;amp;search.boardtype=L&amp;amp;search.cafeId=29646865&amp;amp;search.page=${pageId}&amp;amp;search.menuid=${menuId}`;
  const html = await getHTML(url);
  const $ = await cheerio.load(html);
  const trs = $(&quot;#main-area &amp;gt; div:nth-child(6) &amp;gt; table &amp;gt; tbody &amp;gt; tr&quot;);

  const data = trs
    .map((index, element) =&amp;gt; {
      return {
        title: $(&quot;.article&quot;, element).text().replace(/\s+/g, &quot; &quot;).trim(),
        author: $(&quot;.m-tcol-c&quot;, element).text(),
        date: $(&quot;.td_date&quot;, element).text(),
        link: &quot;https://m.cafe.naver.com&quot; + $(&quot;.article&quot;, element).attr(&quot;href&quot;),
      };
    })
    .get();

  return {
    result: data,
    page: pageId,
  };
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;menuId와 pageId를 입력 받아 해당하는 게시글 목록을 반환하는 getArticles 함수이다. 각 게시글의 title, author, data, link를 data에, 현재 pageId를 page에 넣어 반환했다. page 프로퍼티를 포함한 이유는 이후 React Query의 Infinite Query를 사용할 때 getNextPageParam 옵션을 채우는데 필요하기 때문이다.&lt;/p&gt;
&lt;pre id=&quot;code_1647512561378&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const [category, setCategory] = useState(0);

  const profile = useQuery(&quot;profile&quot;, getProfile);

  const posts = MENU_IDS.map((menu) =&amp;gt; {
    return useInfiniteQuery(
      menu.category,
      ({ pageParam = 1 }) =&amp;gt; getArticles(menu.id, pageParam),
      {
        getNextPageParam: (lastPage, pages) =&amp;gt; {
          return lastPage.page + 1;
        },
      },
    );
  });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 CafeScreen.tsx에서 위의 api 코드를 불러와서 위와 같이 사용했다. category라는 상태를 만들었는데, 이는 여러 게시판의 글들을 전부 posts에 넣어서 사용했기 때문이다. posts는 각 게시판 마다 useInfinteQuery로 요청한 게시글들을 배열로 감싼 형태이다.&lt;/p&gt;
&lt;pre id=&quot;code_1647514133683&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;posts = [
    { // 게시판 타입 0
    	&quot;data&quot;: {&quot;pageParams&quot;: [undefined], &quot;pages&quot;: [[Object]]}, 
        &quot;fetchNextPage&quot;: [Function bound fetchNextPage], 
        &quot;fetchPreviousPage&quot;: [Function bound fetchPreviousPage], 
        &quot;hasNextPage&quot;: true, 
        ...
    },
    { // 게시판 타입 1
    	&quot;data&quot;: {&quot;pageParams&quot;: [undefined], &quot;pages&quot;: [[Object]]}, 
        &quot;fetchNextPage&quot;: [Function bound fetchNextPage], 
        &quot;fetchPreviousPage&quot;: [Function bound fetchPreviousPage], 
        &quot;hasNextPage&quot;: true, 
        ...
    },
    ...
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략 이런 느낌.. (Typescript에서 자동으로 타입 추론을 해줘서 따로 타입을 정의하지는 않았다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 posts에서 원하는 게시판의 글만 쏙쏙 꺼내오기 위해 category 상태를 만들고 아래와 같이 사용했다.&lt;/p&gt;
&lt;pre id=&quot;code_1647514274707&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  {posts[category].data?.pages
    .map((page) =&amp;gt; {
      return page.result;
    })
    .flat()
    .map((item, index) =&amp;gt; {
      return (
        &amp;lt;Content
          post={item}
          scrollToLastPosition={scrollToLastPosition}
          setLoading={setLoading}
          key={index}
        /&amp;gt;
      );
    })}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;posts에서 category에 해당하는 useInfinteQueryResult 객체의 data에서 pages를 불러와 result 만을 뽑아와서 flat으로 배열을 꺼내주고 map으로 돌려 Content 컴포넌트를 반환했다. 가독성은 좋지 않지만 짧은 코드로 구현했으니 만족해야 될지.. 아니면 길더라도 이해가 쉽게 할지 고민했다. 하지만 사용자에게 보여주고 싶은 게시판을 추가하고 싶을 때 단순히 MENU_IDS 배열에 추가하면 되니 이러한 방식으로 구현했다.&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;이제 위의 코드를 ScrollView로 감싼 다음 사용자가 스크롤 해서 바닥에 닿는 것을 감지하면 posts 배열 중 해당하는 게시판 객체의 fetchNextPage를 호출하면 되는 것이었다. 그렇게 쉽게 마무리 할 수 있을 것이라 생각했지만... &lt;b&gt;바닥에 닿아서 fetchNextPage를 실행해 데이터를 추가하는 순간 ScrollView가 최상단으로 스크롤&lt;/b&gt; 되는 것이었다. 자세하게는 상태가 변하는 것을 react가 감지해 rerendering하게 되는 문제였다.... ScrollView의 옵션 중에서 Content가 변해도 position을 유지하는 maintainVisibleContentPosition을 설정했지만 해결되지 않았다. 열심히 구글링(Prevent scroll to top after rerender in React Native, How to prevent scroll to top when rerender component)을 해도 다른 사람들도 똑같이 겪고 있는 문제였다. 다른 개발자들이 내놓은 해결 방안으로는 onScroll을 통해 스크롤 마다 scrollPositionRef를 갱신하고 만약 바닥에 닿는 것을 감지하면 fetchNextPage를 실행하는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1647515196609&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const scrollViewRef = useRef&amp;lt;ScrollView&amp;gt;(null);
  const scrollPositionRef = useRef(0);
  const [loading, setLoading] = useState(f
  
  const isCloseToBottom = ({
    layoutMeasurement,
    contentOffset,
    contentSize,
  }: any) =&amp;gt; {
    const paddingToBottom = 20;
    return (
      layoutMeasurement.height + contentOffset.y &amp;gt;=
      contentSize.height - paddingToBottom
    );
  };
  
    const scrollToLastPosition = () =&amp;gt; {
    if (scrollViewRef.current != null) {
      scrollViewRef.current.scrollTo({
        x: 0,
        y: scrollPositionRef.current,
        animated: false,
      });
    }
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 Ref를 선언하고 isClosetoBottom, scrollToLastPosition 함수를 정의했다. ScrollView의 props는 아래와 같이 작성했다.&lt;/p&gt;
&lt;pre id=&quot;code_1647515291785&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;ScrollView
              ref={scrollViewRef}
              scrollEventThrottle={0}
              onScroll={async ({ nativeEvent }) =&amp;gt; {
                scrollPositionRef.current = nativeEvent.contentOffset.y;
                if (isCloseToBottom(nativeEvent)) {
                  setLoading(true);
                  await posts[category].fetchNextPage();
                  setLoading(false);
                }
              }}
              onContentSizeChange={() =&amp;gt; {
                if (scrollViewRef.current != null) scrollToLastPosition();
              }}
              refreshControl={
                &amp;lt;RefreshControl refreshing={refreshing} onRefresh={onRefresh} /&amp;gt;
              }
            &amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크롤 마다 scrollPositionRef를 갱신하고, 만약 바닥에 닿는 것이 감지되면 Loading을 true로 바꾸고 데이터를 불러온 뒤 다시 Loading을 false로 바꾼다. onContentSizeChange는 내부 Content의 사이즈가 변경되면 실행되며 fetchNextPage로 데이터가 추가되었을 때 다시 원래 위치로 스크롤하도록 한다. loading 상태를 추가한 이유는 데이터를 불러오고 아래로 스크롤하는 과정을 숨기기 위함이다. Loading이 true일 경우 ActivityIndicator를 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;refreshControl은 ScrollView 맨 위에서 아래로 당겼을 때 실행되며 이때 새로고침을 수행하도록 구현했다.&lt;/p&gt;
&lt;pre id=&quot;code_1647515505661&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const onRefresh = React.useCallback(async () =&amp;gt; {
    setRefreshing(true);
    await posts[category].refetch();
    setRefreshing(false);
  }, []);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 구현한 방식과 유사하게 refetch를 수행한다.&lt;/p&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-03-17 오후 8.13.13.png&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;1814&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEu75y/btrwdknIGeY/wIF8Uuz5u43MPz2KMKLdDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEu75y/btrwdknIGeY/wIF8Uuz5u43MPz2KMKLdDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEu75y/btrwdknIGeY/wIF8Uuz5u43MPz2KMKLdDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEu75y%2FbtrwdknIGeY%2FwIF8Uuz5u43MPz2KMKLdDk%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;322&quot; height=&quot;641&quot; data-filename=&quot;스크린샷 2022-03-17 오후 8.13.13.png&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;1814&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 디자인과 조금 다르게 구현했는데, 이는 굿즈 사이트에 침착맨만의 굿즈가 있는 것도 아니고 수시로 굿즈 구성이 달라져서 이를 매번 반영하기 어려울 것이라 판단했기 때문이다. 따라서 바로 굿즈 사이트로 이동할 수 있도록 보여주는 정도로만 구현했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;# 느낀점&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;끗&lt;/p&gt;</description>
      <category>개발/프로젝트</category>
      <author>Jinhoda</author>
      <guid isPermaLink="true">https://mannered.tistory.com/11</guid>
      <comments>https://mannered.tistory.com/11#entry11comment</comments>
      <pubDate>Thu, 17 Mar 2022 20:22:02 +0900</pubDate>
    </item>
    <item>
      <title>Chimha 개인정보취급방침</title>
      <link>https://mannered.tistory.com/10</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 전진호 &amp;gt;('&lt;a href=&quot;https://github.com/Jinho1011'이하&quot;&gt;https://github.com/Jinho1011'이하&lt;/a&gt; 'Chimha')은(는) 「개인정보 보호법」 제30조에 따라 정보주체의 개인정보를 보호하고 이와 관련한 고충을 신속하고 원활하게 처리할 수 있도록 하기 위하여 다음과 같이 개인정보 처리방침을 수립&amp;middot;공개합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;○ 이 개인정보처리방침은&lt;span&gt;&amp;nbsp;&lt;/span&gt;2022년&lt;span&gt;&amp;nbsp;&lt;/span&gt;1월&lt;span&gt;&amp;nbsp;&lt;/span&gt;1부터 적용됩니다.&lt;/p&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;br /&gt;&lt;br /&gt;&amp;lt; 전진호 &amp;gt;('&lt;a href=&quot;https://github.com/Jinho1011'이하&quot;&gt;https://github.com/Jinho1011'이하&lt;/a&gt; 'Chimha')은(는) 다음의 목적을 위하여 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며 이용 목적이 변경되는 경우에는 「개인정보 보호법」 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제2조(개인정보의 처리 및 보유 기간)&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;①&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt; 전진호 &amp;gt;은(는) 법령에 따른 개인정보 보유&amp;middot;이용기간 또는 정보주체로부터 개인정보를 수집 시에 동의받은 개인정보 보유&amp;middot;이용기간 내에서 개인정보를 처리&amp;middot;보유합니다.&lt;br /&gt;&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;1.&amp;lt;재화 또는 서비스 제공&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;재화 또는 서비스 제공&amp;gt;와 관련한 개인정보는 수집.이용에 관한 동의일로부터&amp;lt;지체없이 파기&amp;gt;까지 위 이용목적을 위하여 보유.이용됩니다.&lt;/li&gt;
&lt;li&gt;보유근거 : 서비스 제공&lt;/li&gt;
&lt;li&gt;관련법령 : 소비자의 불만 또는 분쟁처리에 관한 기록 : 3년&lt;/li&gt;
&lt;li&gt;예외사유 :&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제3조(개인정보의 제3자 제공)&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;①&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt; 전진호 &amp;gt;은(는) 개인정보를 제1조(개인정보의 처리 목적)에서 명시한 범위 내에서만 처리하며, 정보주체의 동의, 법률의 특별한 규정 등 「개인정보 보호법」 제17조 및 제18조에 해당하는 경우에만 개인정보를 제3자에게 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;②&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #4374ac;&quot;&gt;&amp;lt; 전진호 &amp;gt;&lt;/span&gt;은(는) 다음과 같이 개인정보를 제3자에게 제공하고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1. &amp;lt; 전진호 &amp;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;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제4조(개인정보처리 위탁)&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;①&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt; 전진호 &amp;gt;은(는) 원활한 개인정보 업무처리를 위하여 다음과 같이 개인정보 처리업무를 위탁하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;②&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #4374ac;&quot;&gt;&amp;lt; 전진호 &amp;gt;&lt;/span&gt;은(는) 위탁계약 체결시 「개인정보 보호법」 제26조에 따라 위탁업무 수행목적 외 개인정보 처리금지, 기술적․관리적 보호조치, 재위탁 제한, 수탁자에 대한 관리․감독, 손해배상 등 책임에 관한 사항을 계약서 등 문서에 명시하고, 수탁자가 개인정보를 안전하게 처리하는지를 감독하고 있습니다.&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;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제5조(정보주체와 법정대리인의 권리&amp;middot;의무 및 그 행사방법)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;① 정보주체는 전진호에 대해 언제든지 개인정보 열람&amp;middot;정정&amp;middot;삭제&amp;middot;처리정지 요구 등의 권리를 행사할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;② 제1항에 따른 권리 행사는전진호에 대해 「개인정보 보호법」 시행령 제41조제1항에 따라 서면, 전자우편, 모사전송(FAX) 등을 통하여 하실 수 있으며 전진호은(는) 이에 대해 지체 없이 조치하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;③ 제1항에 따른 권리 행사는 정보주체의 법정대리인이나 위임을 받은 자 등 대리인을 통하여 하실 수 있습니다.이 경우 &amp;ldquo;개인정보 처리 방법에 관한 고시(제2020-7호)&amp;rdquo; 별지 제11호 서식에 따른 위임장을 제출하셔야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;④ 개인정보 열람 및 처리정지 요구는 「개인정보 보호법」 제35조 제4항, 제37조 제2항에 의하여 정보주체의 권리가 제한 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⑤ 개인정보의 정정 및 삭제 요구는 다른 법령에서 그 개인정보가 수집 대상으로 명시되어 있는 경우에는 그 삭제를 요구할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⑥ 전진호은(는) 정보주체 권리에 따른 열람의 요구, 정정&amp;middot;삭제의 요구, 처리정지의 요구 시 열람 등 요구를 한 자가 본인이거나 정당한 대리인인지를 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제6조(처리하는 개인정보의 항목 작성)&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;①&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt; 전진호 &amp;gt;은(는) 다음의 개인정보 항목을 처리하고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1&amp;lt; 재화 또는 서비스 제공 &amp;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;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제7조(개인정보의 파기)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;① &amp;lt; 전진호 &amp;gt; 은(는) 개인정보 보유기간의 경과, 처리목적 달성 등 개인정보가 불필요하게 되었을 때에는 지체없이 해당 개인정보를 파기합니다.&lt;br /&gt;&lt;br /&gt;② 정보주체로부터 동의받은 개인정보 보유기간이 경과하거나 처리목적이 달성되었음에도 불구하고 다른 법령에 따라 개인정보를 계속 보존하여야 하는 경우에는, 해당 개인정보를 별도의 데이터베이스(DB)로 옮기거나 보관장소를 달리하여 보존합니다.&lt;br /&gt;1. 법령 근거 :&lt;br /&gt;2. 보존하는 개인정보 항목 : 계좌정보, 거래날짜&lt;br /&gt;&lt;br /&gt;③ 개인정보 파기의 절차 및 방법은 다음과 같습니다.&lt;br /&gt;1. 파기절차&lt;br /&gt;&amp;lt; 전진호 &amp;gt; 은(는) 파기 사유가 발생한 개인정보를 선정하고, &amp;lt; 전진호 &amp;gt; 의 개인정보 보호책임자의 승인을 받아 개인정보를 파기합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 파기방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전자적 파일 형태의 정보는 기록을 재생할 수 없는 기술적 방법을 사용합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제8조(개인정보의 안전성 확보 조치)&lt;br /&gt;&lt;br /&gt;&amp;lt; 전진호 &amp;gt;은(는) 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취하고 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 개인정보에 대한 접근 제한&lt;br /&gt;개인정보를 처리하는 데이터베이스시스템에 대한 접근권한의 부여,변경,말소를 통하여 개인정보에 대한 접근통제를 위하여 필요한 조치를 하고 있으며 침입차단시스템을 이용하여 외부로부터의 무단 접근을 통제하고 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제9조(개인정보 자동 수집 장치의 설치&amp;bull;운영 및 거부에 관한 사항)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;전진호 은(는) 정보주체의 이용정보를 저장하고 수시로 불러오는 &amp;lsquo;쿠키(cookie)&amp;rsquo;를 사용하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제10조 (개인정보 보호책임자)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;①&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #4374ac;&quot;&gt;전진호&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&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;/li&gt;
&lt;li&gt;직책 :개발자&lt;/li&gt;
&lt;li&gt;직급 :개발자&lt;/li&gt;
&lt;li&gt;연락처 :01023709940, jinhoda_@naver.com,&lt;/li&gt;
&lt;/ul&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;부서명 :개인정보 보호 담당부서&lt;/li&gt;
&lt;li&gt;담당자 :전진호&lt;/li&gt;
&lt;li&gt;연락처 :01023709940, jinhoda_@naver.com,&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;&lt;b&gt;제11조(개인정보 열람청구)&lt;br /&gt;정보주체는 ｢개인정보 보호법｣ 제35조에 따른 개인정보의 열람 청구를 아래의 부서에 할 수 있습니다.&lt;br /&gt;&amp;lt; 전진호 &amp;gt;은(는) 정보주체의 개인정보 열람청구가 신속하게 처리되도록 노력하겠습니다.&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;▶ 개인정보 열람청구 접수&amp;middot;처리 부서&lt;/li&gt;
&lt;li&gt;부서명 : 개발부서&lt;/li&gt;
&lt;li&gt;담당자 : 전진호&lt;/li&gt;
&lt;li&gt;연락처 : 01023709940, jinhoda_@naver.com,&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제12조(권익침해 구제방법)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;정보주체는 개인정보침해로 인한 구제를 받기 위하여 개인정보분쟁조정위원회, 한국인터넷진흥원 개인정보침해신고센터 등에 분쟁해결이나 상담 등을 신청할 수 있습니다. 이 밖에 기타 개인정보침해의 신고, 상담에 대하여는 아래의 기관에 문의하시기 바랍니다.&lt;br /&gt;&lt;br /&gt;1. 개인정보분쟁조정위원회 : (국번없이) 1833-6972 (www.kopico.go.kr)&lt;br /&gt;2. 개인정보침해신고센터 : (국번없이) 118 (privacy.kisa.or.kr)&lt;br /&gt;3. 대검찰청 : (국번없이) 1301 (www.spo.go.kr)&lt;br /&gt;4. 경찰청 : (국번없이) 182 (ecrm.cyber.go.kr)&lt;br /&gt;&lt;br /&gt;「개인정보보호법」제35조(개인정보의 열람), 제36조(개인정보의 정정&amp;middot;삭제), 제37조(개인정보의 처리정지 등)의 규정에 의한 요구에 대 하여 공공기관의 장이 행한 처분 또는 부작위로 인하여 권리 또는 이익의 침해를 받은 자는 행정심판법이 정하는 바에 따라 행정심판을 청구할 수 있습니다.&lt;br /&gt;&lt;br /&gt;※ 행정심판에 대해 자세한 사항은 중앙행정심판위원회(www.simpan.go.kr) 홈페이지를 참고하시기 바랍니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제13조(개인정보 처리방침 변경)&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;① 이 개인정보처리방침은 2022년 1월 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;예시 ) - 20XX. X. X ~ 20XX. X. X 적용 (클릭)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 ) - 20XX. X. X ~ 20XX. X. X 적용 (클릭)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 ) - 20XX. X. X ~ 20XX. X. X 적용 (클릭)&lt;/p&gt;</description>
      <author>Jinhoda</author>
      <guid isPermaLink="true">https://mannered.tistory.com/10</guid>
      <comments>https://mannered.tistory.com/10#entry10comment</comments>
      <pubDate>Wed, 16 Mar 2022 11:47:12 +0900</pubDate>
    </item>
    <item>
      <title>React Navigation collapsible header 구현기</title>
      <link>https://mannered.tistory.com/9</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-03-09 오후 2.48.50.png&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;679&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zI3tE/btrvocqPrvW/AP0ECW8CiejpTjTujwvFWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zI3tE/btrvocqPrvW/AP0ECW8CiejpTjTujwvFWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zI3tE/btrvocqPrvW/AP0ECW8CiejpTjTujwvFWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzI3tE%2FbtrvocqPrvW%2FAP0ECW8CiejpTjTujwvFWk%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;720&quot; height=&quot;679&quot; data-filename=&quot;스크린샷 2022-03-09 오후 2.48.50.png&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;679&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;홈화면에서 '더보기'를 클릭하면 음식세트페이지(HomeFoodDetail), 레크세트페이지(HomeRecDetail)로 이동하는데, 디자이너분들의 요구사항은&amp;nbsp;&lt;b&gt;상단에 뒤로가기, 로고, 기획서를 포함한 헤더 부분이 아래로 스크롤 함에 따라 높이가 줄어두는 것&lt;/b&gt;이었다&lt;b&gt;.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ed5525f3ae33a4ff867662630259feea.jpeg&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;281&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YYEkz/btrvvuwH1i9/r1HeDchc49Ifjni6VIyFGK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YYEkz/btrvvuwH1i9/r1HeDchc49Ifjni6VIyFGK/img.jpg&quot; data-alt=&quot;대략난감&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YYEkz/btrvvuwH1i9/r1HeDchc49Ifjni6VIyFGK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYYEkz%2FbtrvvuwH1i9%2Fr1HeDchc49Ifjni6VIyFGK%2Fimg.jpg&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;432&quot; height=&quot;243&quot; data-filename=&quot;ed5525f3ae33a4ff867662630259feea.jpeg&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;281&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;대략난감&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미리 인지하고 있었다면 Header와 본문 Component를 나누지 않고 한 곳에서 처리해서 쉽게 구현했겠지만 이미 React Navigation에서 제공하는 createMaterialTopTabNavigator로 음식 / 레크를 나누는 TabBar를 구현해둔 상태였다. 이렇게 나뉘어진 상황에서 collapsible한 header를 구현하는 것이 어려울 것이라고 생각했던 이유는 스크롤이 발생하는 곳(HomeFoodDetail)과 높이가 줄어드는 애니메이션이 작동하는 곳(TabBar)이 달랐기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Slide 4_3 - 1.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1ygsK/btrvvbELiqY/xS3H1BcrWOsnZ9j7PYNQY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1ygsK/btrvvbELiqY/xS3H1BcrWOsnZ9j7PYNQY0/img.png&quot; data-alt=&quot;대략 이런 식이다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1ygsK/btrvvbELiqY/xS3H1BcrWOsnZ9j7PYNQY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1ygsK%2FbtrvvbELiqY%2FxS3H1BcrWOsnZ9j7PYNQY0%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;508&quot; height=&quot;381&quot; data-filename=&quot;Slide 4_3 - 1.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;대략 이런 식이다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 y축 변화값을 전달하기 위한 방법으로는&lt;/p&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. TabNavigator에서 offset_y 변수를 생성하고 HomeFoodDetail에서 스크롤 이벤트가 발생하면 navigation.setParams로 y값을 업데이트한다. 그리고 TabBar에 props로 정보를 전달해서 state.routes.params.offsetY 값을 받는다.&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;2. 글로벌 저장소(redux)에 저장하고 TabBar에서 useSelector로 불러온다.&lt;/p&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;일단 먼저 1번의 방법으로 구현해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646816327706&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// navigation/Homes
const HomeTab = createMaterialTopTabNavigator();

function HomeTabs(navigation) {
  let offsetY_F = 0;
  let offsetY_R = 0;
  return (
    &amp;lt;HomeTab.Navigator
      initialRouteName={navigation.route.params.initialRoute}
      tabBar={props =&amp;gt; &amp;lt;TabBar {...props} /&amp;gt;}&amp;gt;
      &amp;lt;HomeTab.Screen
        name=&quot;HomeFoodDetail&quot;
        component={FoodDetail}
        options={{title: '음식'}}
        initialParams={{offsetY: offsetY_F}}
      /&amp;gt;
      &amp;lt;HomeTab.Screen
        name=&quot;HomeRecDetail&quot;
        component={RecDetail}
        options={{title: '레크'}}
        initialParams={{offsetY: offsetY_R}}
      /&amp;gt;
    &amp;lt;/HomeTab.Navigator&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Home Navigator를 반환하는 HomeTabs 함수에서 각 화면의 y offset을 나타내는 변수를 만들고 initialParams로 넘겨준다.&lt;/p&gt;
&lt;pre id=&quot;code_1646816404867&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// screens/Main/Home/FoodDetail/HomeFoodDetailPresenter
  const navigation = useNavigation();

  const onScroll = e =&amp;gt; {
    navigation.setParams({offsetY: e.nativeEvent.contentOffset.y});
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크롤 시 setParams 함수로 y값을 업데이트한다.&lt;/p&gt;
&lt;pre id=&quot;code_1646816557757&quot; class=&quot;pf&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// components/Header/TabBar

export default function TabBar({state, descriptors, navigation}) {
  const headerHeight = 58 * 2;
  let index = descriptors[state.routes[0].key].navigation.isFocused() ? 0 : 1;

  const scrollY = useRef(new Animated.Value(0));

  useEffect(() =&amp;gt; {
    scrollY.current.setValue(state.routes[index].params.offsetY);
  }, [state]);

  const scrollYClamped = Animated.diffClamp(scrollY.current, 0, headerHeight, {
    useNativeDriver: true,
  });

  const translateY = scrollYClamped.interpolate({
    inputRange: [0, headerHeight],
    outputRange: [0, -(headerHeight / 2)],
  });

  const translateYNumber = useRef();

  translateY.addListener(({value}) =&amp;gt; {
    translateYNumber.current = value;
  });

  ...
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;state로 넘어오는 offset Y 값을 받아 animated value로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별 문제 없어 보이고, 실제로 동작했지만 큰 문제가 생겼다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;iframe src=&quot;https://giphy.com/embed/UFxhLauwp8BoPt0TxX&quot; width=&quot;266&quot; height=&quot;480&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646829602582&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; LOG  0
 LOG  77.81818389892578
 LOG  642.5454711914062
 LOG  664.3636474609375
 LOG  719.272705078125&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;p data-ke-size=&quot;size16&quot;&gt;HomeFoodDetailPresenter -&amp;gt; Navigator -&amp;gt; TabBar로 데이터를 오가는데 많은 딜레이가 생기는 것으로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 다음으로는 redux를 사용하는 방법을 적용해보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1646832418701&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// store/actions/scrolls
export const UPDATE = 'UPDATE';

export const updateY = (screen, y) =&amp;gt; {
  return {type: UPDATE, screen, y};
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1646832466602&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// store/reducers/scrolls
const initialState = {
  HomeFoodDetail: 0,
  HomeRecDetail: 0,
};

export default (state = initialState, action) =&amp;gt; {
  switch (action.type) {
    case 'UPDATE': {
      const screen = action.screen;
      const y = action.y;
      return {...state, [screen]: y};
    }
    default:
      return state;
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선&amp;nbsp; action과 reducer를 정의했다&lt;/p&gt;
&lt;pre id=&quot;code_1646832803347&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  // HomeFoodsDetailPresenter
  const dispatch = useDispatch();

  const onScroll = e =&amp;gt; {
    dispatch(updateY('HomeRecDetail', e.nativeEvent.contentOffset.y));
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 화면에서는 setParams에서 dispatch로만 바꿔줬다&lt;/p&gt;
&lt;pre id=&quot;code_1646833174076&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// components/Header/TabBar
const scrollY = useRef(new Animated.Value(0));
  const y = useSelector(state =&amp;gt; state.scrolls[index]);

  useEffect(() =&amp;gt; {
    scrollY.current.setValue(y);
  }, [y]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TabBar에서도 useSelector로 받아오도록 변경했다.&lt;/p&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;iframe src=&quot;https://giphy.com/embed/UEa8XSvTuoEFabGnMt&quot; width=&quot;266&quot; height=&quot;480&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&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;이걸 구현한건 2달 전이지만 지금에야 수정했다(....)&lt;/p&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;a href=&quot;https://github.com/react-navigation/react-navigation/issues/4786&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/react-navigation/react-navigation/issues/4786&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1646833869687&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Performance issues and jumpy animations on Android when using setParams to pass Animated Value from one component to another. &amp;middot;&quot; data-og-description=&quot;Please help me out, I've been stuck with this problem for the last 4-5 days. The problem: https://gfycat.com/CelebratedVapidBadger This is my MainTab1.js component which holds the ScrollView wi...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/react-navigation/react-navigation/issues/4786&quot; data-og-url=&quot;https://github.com/react-navigation/react-navigation/issues/4786&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b1q10m/hyNEtgbwZQ/BrQlvPh2ng3C0ZqlWdzpqK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/react-navigation/react-navigation/issues/4786&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/react-navigation/react-navigation/issues/4786&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b1q10m/hyNEtgbwZQ/BrQlvPh2ng3C0ZqlWdzpqK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Performance issues and jumpy animations on Android when using setParams to pass Animated Value from one component to another. &amp;middot;&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Please help me out, I've been stuck with this problem for the last 4-5 days. The problem: https://gfycat.com/CelebratedVapidBadger This is my MainTab1.js component which holds the ScrollView wi...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음.. 확실히 collpasible header를 구현하는데에는 적절하지 않았던것 같다.&lt;/p&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-filename=&quot;Slide 4_3 - 2.png&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;760&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cO4Rvj/btrvqbEZxl2/tta3ohxfITJLKX0w6XGU2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cO4Rvj/btrvqbEZxl2/tta3ohxfITJLKX0w6XGU2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cO4Rvj/btrvqbEZxl2/tta3ohxfITJLKX0w6XGU2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcO4Rvj%2FbtrvqbEZxl2%2Ftta3ohxfITJLKX0w6XGU2k%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;786&quot; height=&quot;760&quot; data-filename=&quot;Slide 4_3 - 2.png&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;760&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;span style=&quot;color: #000000;&quot;&gt;HomeFoodDetail 안에 Header, TabBar, Content 들어가도록 구조를 바꾸는게 제일 좋겠지만 각 컴포넌트들을 아릅답게 분리해놓았는데 다시 합치기 싫었다(=심술났다)&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이상으로 Mount 앱의 Collapsible Header 구현기였다&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;// 04.07 수정 //&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 원인은 local / global state의 문제 따위가 아니라 단순히 useNativeDriver를 사용하지 않아 발생하는 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649338737122&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function HomeTabs(navigation) {
  const scrollY = useRef(new Animated.Value(0));

  return (
    &amp;lt;HomeTab.Navigator
      initialRouteName={navigation.route.params.initialRoute}
      tabBar={props =&amp;gt; &amp;lt;TabBar scrollY={scrollY} {...props} /&amp;gt;}&amp;gt;
      &amp;lt;HomeTab.Screen
        name=&quot;HomeFoodDetail&quot;
        component={() =&amp;gt; &amp;lt;FoodDetail scrollY={scrollY} /&amp;gt;}
        options={{title: '음식'}}
      /&amp;gt;
      &amp;lt;HomeTab.Screen
        name=&quot;HomeRecDetail&quot;
        component={() =&amp;gt; &amp;lt;RecDetail scrollY={scrollY} /&amp;gt;}
        options={{title: '레크'}}
      /&amp;gt;
    &amp;lt;/HomeTab.Navigator&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HomeTab에서는 위와 같이 Prop Drilling으로 scrollY를 꽂아주고&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649338780871&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;onScroll={Animated.event([
  {
    nativeEvent: {
      contentOffset: {
        y: scrollY.current,
      },
    },
  },
])}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onScoll을 다음과 같이 변경해서 해결할 수 있었다&lt;/p&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>개발/프로젝트</category>
      <author>Jinhoda</author>
      <guid isPermaLink="true">https://mannered.tistory.com/9</guid>
      <comments>https://mannered.tistory.com/9#entry9comment</comments>
      <pubDate>Wed, 9 Mar 2022 22:53:31 +0900</pubDate>
    </item>
    <item>
      <title>React Navgation navigate with object params</title>
      <link>https://mannered.tistory.com/8</link>
      <description>&lt;pre id=&quot;code_1646718643098&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;navigation.navigate('FoodSetChangeCount', {state});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Navigation에서 navigate를 사용하면 다른 화면으로 이동할 수 있는데, 이때 params에 전달하고자 하는 정보를 넣어서 보낼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포스팅하고 있는 내용과는 별개로 React Navigation에서는 params로 object를 그대로 전달하는 방법을 지양해달라고 말하고 있다. 뒤에서 서술할 내용과도 연관이 있고,&amp;nbsp;&lt;a href=&quot;https://reactnavigation.org/docs/params/#what-should-be-in-params&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://reactnavigation.org/docs/params/#what-should-be-in-params&lt;/a&gt; 를 참고하면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1c1e21;&quot;&gt;However, this is an anti-pattern. Data such as user objects should be in your global store instead of the navigation state. Otherwise you have the same data duplicated in multiple places. This can leads to bugs such as the profile screen showing outdated data even if the user object has changed after navigation.&lt;/span&gt;&lt;span style=&quot;color: #1c1e21;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;It also becomes problematic to link to the screen via deep linking or on the Web, since:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;The URL is a representation of the screen, so it also needs to contain the params, i.e. full user object, which can make the URL very long and unreadable&lt;/li&gt;
&lt;li&gt;Since the user object is in the URL, it's possible to pass a random user object representing a user which doesn't exist, or has incorrect data in the profile&lt;/li&gt;
&lt;li&gt;If the user object isn't passed, or improperly formatted, this could result in crashes as the screen won't know how to handle it&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A better way is to pass only the ID of the user in params&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 한다. 다음부터는 애초에 이런 식으로 객체를 전달하는 것이 아닌 Redux에 넣어놓고 ID만을 전달하는 방법으로 구현하는 것이 더 좋을 것 같다. 하지만 프로젝트를 진행할 당시에는 이미 먼 길을 떠난 상태(...)였기 때문에 객체를 보내는 방법을 유지할 수밖에 없었고, 이 방식을 사용하면서 나타난 문제점을 해결한 과정을 써보도록 하겠다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646644715435&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// FoodSetPresenter
&amp;lt;ChangeCountButton
  onPress={() =&amp;gt; {
    navigation.navigate('FoodSetChangeCount', {state});
  }}&amp;gt;
  &amp;lt;ChangeCountText&amp;gt;수량변경&amp;lt;/ChangeCountText&amp;gt;
&amp;lt;/ChangeCountButton&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FoodSet 화면에서 FoodSetChangeCounter로 이동하면서 위와 같이 state를 넣어줘서 사용하려고 했다. 전달받는 화면에서는 아래와 같이 params를 불러올 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1646644821353&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// FoodSetChangeCounterContainer
const [state, setState] = useState({
    isLoaded: false,
    ...route.params.state,
  });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터도 잘 불러와져서 그냥 사용했지만 이후에 큰 문제가 발생했다(...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 FoodSet 화면에서의 state는 다음과 같다&lt;/p&gt;
&lt;pre id=&quot;code_1646644951004&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const [state, setState] = useState({
    memberCnt: 1,
    foodSet: {},
    items: [],
    id: route.params.id,
  });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;눈여겨 봐야할 부분은 음식 세트를 먹을 인원수를 나타내는 &lt;b&gt;memberCnt&lt;/b&gt;와, items 배열이 담고 있는 item 객체의 &lt;b&gt;count&lt;/b&gt; 변수이고, 둘 다 1로 초기화되어 있다. FoodSet 화면에서는 memberCnt만 수정할 수 있으며 FoodSetChangeCount 화면에서는 memberCnt와, item들의 count를 수정할 수 있다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-03-08 오후 2.36.47.png&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;1004&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctGucn/btrvtADDCWR/2jesjgo7GspdjNt7yeMCs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctGucn/btrvtADDCWR/2jesjgo7GspdjNt7yeMCs1/img.png&quot; data-alt=&quot;왼쪽 FoodSet에서는 총 인원수만, 오른쪽 FoodSetChangeCount에서는 총 인원수와 각 구성품의 개수를 변경할 수 있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctGucn/btrvtADDCWR/2jesjgo7GspdjNt7yeMCs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctGucn%2FbtrvtADDCWR%2F2jesjgo7GspdjNt7yeMCs1%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;1110&quot; height=&quot;1004&quot; data-filename=&quot;스크린샷 2022-03-08 오후 2.36.47.png&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;1004&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;왼쪽 FoodSet에서는 총 인원수만, 오른쪽 FoodSetChangeCount에서는 총 인원수와 각 구성품의 개수를 변경할 수 있다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FoodSet에서 인원수를 변경하고 FoodSetChangeCount으로 넘어가면 변경된 인원수가 잘 반영되었지만, 문제는 구성품 변경에서 나타났다. 기획에서 원했던 것은 &lt;b&gt;FoodSetChangeCount에서 구성품을 변경하더라도 다시 FoodSet으로 돌아왔을 때 구성품의 개수는 1로 초기화&lt;/b&gt;되어 있어야 하는 것이었다. 그러나 FoodSetChangeCount 로 navigate할 때 state를 같이 보내서 이동한 다음, 구성품을 변경하고 다시 FoodSet으로 돌아오면 구성품의 개수가 1이 아닌 &lt;b&gt;FoodSetChangeCount에서 변경했던 개수로 보여지는 문제가 발생했다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 의도했던 시나리오는 FoodSet[총 인원수: 1, 각 구성품 개수: 1] -&amp;gt; FoodSetChangeCount[총 인원수: 2, 각 구성품 개수: 3] -&amp;gt; FoodSet[총 인원수: 2, 각 구성품 개수: 1] 이었지만, &lt;span&gt;FoodSet[총 인원수: 1, 각 구성품 개수: 1] -&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;FoodSetChangeCount&lt;span&gt;[총 인원수: 2, 각 구성품 개수: 3] -&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;FoodSet[총 인원수: 2, 각 구성품 개수: 3]으로 동작하고 있었다.&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;해당 이슈를 전달 받았을 때 다행히 곧바로 깊은 복사, 얕은 복사가 떠올랐다. state 객체는 Object 자료형으로 javascript에서는 참조값이기 때문에 변수가 객체의 주소를 나타내기 때문에 복사했을 때 원본 객체의 주소 값을 가리키게 된다. 따라서&lt;/p&gt;
&lt;pre id=&quot;code_1646718650165&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;navigation.navigate('FoodSetChangeCount', {state});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 params를 전달하면 state의 값이 아닌 주소가 전달되어 FoodSetChangeCount에서 구성품의 개수를 변경했을 때 FoodSet에서도 변경된 것이다. javascript에서 객체를 깊은 복사하려면 재귀적으로 돌면서 새로운 객체를 생성하거나, JSON.Stringfy 등의 방법이 있지만 &lt;span style=&quot;color: #212529;&quot;&gt;lodash 라이브러리를 사용하면 더 쉽게 복사할 수 있다고 해서 사용해보았다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1646718908034&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;ChangeCountButton
  onPress={() =&amp;gt; {
    const _state = _.cloneDeep(state);
    navigation.navigate('FoodSetChangeCount', {_state});
  }}&amp;gt;
  &amp;lt;ChangeCountText&amp;gt;수량변경&amp;lt;/ChangeCountText&amp;gt;
&amp;lt;/ChangeCountButton&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 FoodSetChangeCount에서 변경해도 FoodSet에 변화가 없음을 확인했다. 앞으로는 글로벌 저장소에 넣어놓고 id를 전달해서 값을 주고 받는 방식으로 구현할 것이다.&lt;/p&gt;</description>
      <category>개발/React Native</category>
      <author>Jinhoda</author>
      <guid isPermaLink="true">https://mannered.tistory.com/8</guid>
      <comments>https://mannered.tistory.com/8#entry8comment</comments>
      <pubDate>Tue, 8 Mar 2022 14:58:19 +0900</pubDate>
    </item>
    <item>
      <title>Mount 프로젝트 회고</title>
      <link>https://mannered.tistory.com/7</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;index.png&quot; data-origin-width=&quot;1684&quot; data-origin-height=&quot;1190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjDIEu/btrvhMFnCUe/3fakF4B2wBCVUNF5xuxou0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjDIEu/btrvhMFnCUe/3fakF4B2wBCVUNF5xuxou0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjDIEu/btrvhMFnCUe/3fakF4B2wBCVUNF5xuxou0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjDIEu%2FbtrvhMFnCUe%2F3fakF4B2wBCVUNF5xuxou0%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;1684&quot; height=&quot;1190&quot; data-filename=&quot;index.png&quot; data-origin-width=&quot;1684&quot; data-origin-height=&quot;1190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4개월 간의 프로젝트가 끝났다. 2월 14일 최종발표가 끝나고도 릴리즈 버전에 문제가 생겨 출시를 못하고 있었는데 드디어 &lt;a href=&quot;https://play.google.com/store/apps/details?id=com.wit.mount&quot;&gt;플레이스토어&lt;/a&gt;에 등록 되었다. 출시 직후 백엔드 서버를 내려서 동작하지는 않는다(...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많을 것을 배웠고 아쉬움도 남는 프로젝트였다.&lt;/p&gt;
&lt;h1&gt;느낀점&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Github 이해도&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Develop / Feature / Hotfix 등의 브랜치로 나누어 작업하는 것이 도움이 되었다. Feature/navigation에서 작업하다가 HomePresenter에 아이콘 추가하는 등 난잡하게 일하던 방식을 바꿀 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Issue 기능을 적극적으로 활용하고, Pull Request에 Template 을 만들어 작업했다. 카톡이 아닌 Github의 Issue 기능을 사용해서 문제가 발생한 상황과 코드를 바로 연결지어 확인할 수 있었고, Template에 맞게 풀리퀘를 올려 작업자들이 어떤 기능을 추가/수정했는지, 어떤 로직이 변화되었는지, 다른 코드를 작성할 때 참고할 점을 확인할 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Typescript의 장점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TS로 진행한 프로젝트가 아니지만 &lt;b&gt;TS의 필요성, 유용성을 깨달았다&lt;/b&gt;. 타입 추론 기능을 제공한다는 사실은 알고 있었으나, Mount 프로젝트를 진행하기 전까지는 그다지 필요하다고 생각하지 않았다. 그러나 Mount 앱의 화면 개수는 31개로, 3명의 초짜 프론트엔드 개발자끼리 진행하기에는 규모가 꽤 컸다. 초반에는 감당할 수 있는 수준이었으나, 구현한 화면의 개수가 늘어나고 부모-자식 컴포넌트의 깊이가 커질 수록 각 컴포넌트끼리 어떤 데이터가 오가는지 알아내기 힘들어졌다 (기획서에 상품을 추가하고 확인하려면 홈 화면 -&amp;gt; 상품 클릭 -&amp;gt; 수량 변경 화면 -&amp;gt; 기획서 선택 -&amp;gt; 기획서 화면의 과정을 거쳐야 했다). 매번 작업할 때마다 console.log 찍고 주석에 적어놓고 작업하는 것은 전혀 효율적이지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 프로젝트에서 Typescript를 연습할 때 이전의 삽질의 경험 덕분인지 쉽게 적응할 수 있었고, 바로 Typescript의 장점을 깨달아버렸다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redux 기초 지식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 이론으로만 학습한 Redux 플로우(상태를 저장하는 Store에 변경하려는 정보를 담은 Action을 Dispatch를 통해 앱의 상태를 변경하는 Reducer 함수로 전달하는 일련의 과정)를 React-Redux를 통해 reducer와 action을 정의하고 각 컴포넌트에서 useSelector로 Store에 접근하거나 useDispatch로 상태를 변경하도록 구현해보았다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;진행과정&lt;/h1&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;folder structure : api, components, navigation, screen, store&lt;/li&gt;
&lt;li&gt;navigation : Auth, Main, Tab, Planner, ...&lt;/li&gt;
&lt;li&gt;Redux store : planners, search, users&lt;/li&gt;
&lt;/ul&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;Home(Main, FoodDetail, RecDetail, Tutorial), Search, Favorite, MyPage, Plan, PlanEditor&lt;/li&gt;
&lt;li&gt;상품 상세 화면(Detail)에서 기획서(Planner)로 상품 정보를 전달하는 로직 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/프로젝트</category>
      <author>Jinhoda</author>
      <guid isPermaLink="true">https://mannered.tistory.com/7</guid>
      <comments>https://mannered.tistory.com/7#entry7comment</comments>
      <pubDate>Mon, 7 Mar 2022 15:11:40 +0900</pubDate>
    </item>
  </channel>
</rss>