<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>성우의 라이브 개발쇼</title>
        <link>https://velog.io/</link>
        <description>풀스택 개발자가 되고싶은 개발자</description>
        <lastBuildDate>Mon, 04 Mar 2024 04:28:10 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>성우의 라이브 개발쇼</title>
            <url>https://velog.velcdn.com/images/explorer-cat/profile/5ae00fc5-74fd-4749-a305-5cb39bc2ae67/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 성우의 라이브 개발쇼. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/explorer-cat" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[ubuntu,linux 캐시메모리 지우기]]></title>
            <link>https://velog.io/@explorer-cat/ubuntulinux-%EC%BA%90%EC%8B%9C%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%A7%80%EC%9A%B0%EA%B8%B0</link>
            <guid>https://velog.io/@explorer-cat/ubuntulinux-%EC%BA%90%EC%8B%9C%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%A7%80%EC%9A%B0%EA%B8%B0</guid>
            <pubDate>Mon, 04 Mar 2024 04:28:10 GMT</pubDate>
            <description><![CDATA[<p>캐시메모리를 비워주는 방법은 간단합니다.</p>
<p>crontab -e
crontab을 열어줍니다.</p>
<p>특정한 시간에 또는 특정 시간 마다 어떤 작업을 자동으로 수행하게 해주고 싶을 때 사용하는 명령어가 cron입니다. cron은 특정한 시간에 특정한 작업을 수행하게 해주는 스케줄링 역할을 하는데 이 cron작업을 설정하는 파일을 crontab파일이라고 합니다.</p>
<p>매 시간마다 캐시메모리를 비워주려면</p>
<p>0 **** sync &amp;&amp; echo 3 &gt; /proc/sys/vm/drop_caches # 매 시간 캐시 비우기
지정한 시간에 캐시메모리를 비워주려면</p>
<p>40 3 *** sync &amp;&amp; echo 3 &gt; /proc/sys/vm/drop_caches # 매일 오전 3시 40분에 캐시 비우기
crontab 안에 작성해주면 됩니다.</p>
<p>crontab의 파일형식은 다음과 같습니다.</p>
<p>첫번째 : 분 0-59
두번째 : 시 0-23
세번째 : 일 0-31
네번째 : 월 1-12
다섯번째 : 요일 0-7 (0 또는 7=일요일, 1=월, 2=화,…)
여섯번째 : 명령어 실행할 명령을 한줄로 쓴다.
캐시메모리를 잘 관리하여 리눅스 느려짐을 예방합시다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] BottomSheet 코드]]></title>
            <link>https://velog.io/@explorer-cat/React-BottomSheet-%EC%BD%94%EB%93%9C</link>
            <guid>https://velog.io/@explorer-cat/React-BottomSheet-%EC%BD%94%EB%93%9C</guid>
            <pubDate>Fri, 22 Dec 2023 02:24:48 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-javascript">import &#39;./App.css&#39;;
import { BottomSheet } from &#39;react-spring-bottom-sheet&#39;
import { useState,useRef,useEffect } from &#39;react&#39;
import &#39;react-spring-bottom-sheet/dist/style.css&#39;
function App() {
  const [open, setOpen] = useState(true)
  const focusRef = useRef();

  useEffect(() =&gt; {
    // Setting focus is to aid keyboard and screen reader nav when activating this iframe
    focusRef.current.focus()
  }, [])


  return (
    &lt;&gt;
        &lt;button onClick={() =&gt; setOpen((open) =&gt; !open)} ref={focusRef}&gt;
          {open ? &#39;Close&#39; : &#39;Open&#39;}
        &lt;/button&gt;

        &lt;BottomSheet
          open={open}
          onDismiss={() =&gt; setOpen(false)}
          skipInitialTransition
          initialFocusRef={focusRef}
          expandOnContentDrag={open}
          snapPoints={({ maxHeight }) =&gt; [maxHeight * 0.6]}
        &gt;

         &lt;div&gt;
            &lt;p&gt;
              When &lt;div&gt;blocking&lt;/div&gt; is &lt;div&gt;false&lt;/div&gt; it&#39;s possible to
              use the Bottom Sheet as an height adjustable sidebar/panel.
            &lt;/p&gt;
            &lt;p&gt;
              You can combine this with &lt;div&gt;onDismissable&lt;/div&gt; to fine-tune
              the behavior you want.
            &lt;/p&gt;
          &lt;/div&gt;
        &lt;/BottomSheet&gt;
    &lt;/&gt;
  );
}

export default App;
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ReactNative] CodePush를 통한 업데이트 적용기]]></title>
            <link>https://velog.io/@explorer-cat/ReactNative-CodePush%EB%A5%BC-%ED%86%B5%ED%95%9C-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%A0%81%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@explorer-cat/ReactNative-CodePush%EB%A5%BC-%ED%86%B5%ED%95%9C-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%A0%81%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Mon, 06 Nov 2023 00:01:46 GMT</pubDate>
            <description><![CDATA[<h3 id="1-자동-모드">1. 자동 모드</h3>
<p>자동 모드 업데이트는 앱을 업데이트하는 가장 간단한 방법이며 최종 사용자에게 가장 침습적인 환경이 가장 적습니다.</p>
<pre><code>codePush.sync();</code></pre><p>업데이트를 사용할 수 있는 경우 다음에 앱을 다시 시작할 때 자동으로 다운로드되고 설치됩니다(최종 사용자 또는 OS에 의해 명시적으로). 그러나 개발자는 매개 변수를 사용하여 설치 동작을 수정할 수 있습니다.installMode</p>
<p>직접 실행: 업데이트가 실행 중인 애플리케이션에 즉시 적용됩니다. 애플리케이션은 새 콘텐츠로 즉시 다시 로드됩니다.
ON_NEXT_RESTART: 업데이트가 다운로드되었지만 즉시 설치되지는 않습니다. 새 콘텐츠는 다음에 애플리케이션이 시작될 때 사용할 수 있습니다.
ON_NEXT_RESUME: 업데이트가 다운로드되었지만 즉시 설치되지는 않습니다. 새 콘텐츠는 다음에 애플리케이션이 다시 시작되거나 다시 시작될 때 사용할 수 있으며, 어떤 이벤트가 먼저 발생하든 관계없이 사용할 수 있습니다.
예를 들어 업데이트를 즉시 다운로드하고 설치하기 위해 개발자는 다음과 같이 매개 변수를 installMode 사용할 수 있습니다.</p>
<h3 id="코드푸시-업데이트-완료-후-앱-강제-재실행시-rollback-이슈">코드푸시 업데이트 완료 후 앱 강제 재실행시 rollback 이슈</h3>
<pre><code>https://velog.io/@beanlove97/react-native-codepush-rollback-%EB%90%98%EB%8A%94-%EC%9D%B4%EC%8A%88</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] JPA+MyBatis 사용하여 외래키 지정, 부모 키 삭제시 자식키도 함께 삭제 시키기!]]></title>
            <link>https://velog.io/@explorer-cat/SpringBoot-JPAMyBatis-%EC%82%AC%EC%9A%A9%ED%95%98%EC%8B%9C-%EC%99%B8%EB%9E%98%ED%82%A4-%EC%98%88%EC%A0%9C%EC%BD%94%EB%93%9C</link>
            <guid>https://velog.io/@explorer-cat/SpringBoot-JPAMyBatis-%EC%82%AC%EC%9A%A9%ED%95%98%EC%8B%9C-%EC%99%B8%EB%9E%98%ED%82%A4-%EC%98%88%EC%A0%9C%EC%BD%94%EB%93%9C</guid>
            <pubDate>Sun, 15 Oct 2023 03:13:49 GMT</pubDate>
            <description><![CDATA[<h2 id="jpamybatis-흐름도">JPA+Mybatis 흐름도</h2>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/9dd545f0-3e6e-4a7c-9d81-74673d4c7fff/image.png" alt=""></p>
<hr>
<h2 id="외래키를-통한-데이터-동시-삭제">외래키를 통한 데이터 동시 삭제</h2>
<p>테이블 생성시 CONSTRAINT 로 외래키를 연결해주어야함
이미 자동으로 테이블이 생성되었거나, 생성한 상태라면 아래 쿼리문으로 수동으로 외래키 연결을 해주었습니다.</p>
<h3 id="첫번째-시도">첫번째 시도</h3>
<pre><code class="language-sql">ALTER TABLE review
ADD CONSTRAINT fk_store_idx
FOREIGN KEY (store_idx) REFERENCES store(idx);</code></pre>
<pre><code>java.sql.SQLException: Cannot delete or update a parent row: a foreign key constraint fails (`flambus`.`review_tag`, CONSTRAINT `fk_review_tag_idx` FOREIGN KEY (`review_idx`) REFERENCES `review` (`idx`))</code></pre><p>자식까지 함께 삭제할 수 없다고 하네요. 외래키를 걸때  &quot;CASCADE&quot; 옵션을 추가해야 된다고 합니다.</p>
<p>근데 이미 걸었던 외래키 조건을 어떻게 삭제해야하나.. 추가적으로 막 걸면 안될꺼같은데 조건이름도 막무가내로 해서 기억도 안나고 좀 당황스러웠습니다.</p>
<hr>
<h3 id="현재-테이블의-외래키-조건을-확인하기">현재 테이블의 외래키 조건을 확인하기</h3>
<pre><code class="language-sql">-- 제약조건을 확인하고싶다면
SHOW CREATE TABLE review_like;
</code></pre>
<p>해당 테이블의 정보를 확인 할 수 있어요!</p>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/9e46322e-3c0a-4324-97c5-4a78591b9641/image.png" alt=""></p>
<p>사진 하단에 보면 CONSTRAINT &#39;조건명&#39; 보이시나요?
내가 추가했던 외래키 조건명을 알았으니 이제 지워봅시다!</p>
<hr>
<h3 id="외래키-조건-삭제">외래키 조건 삭제</h3>
<pre><code class="language-sql">
-- 제약조건 삭제
ALTER TABLE review_like
DROP FOREIGN KEY fk_review_like_idx;
</code></pre>
<p>정상적으로 삭제가 됐는지 한번 더 확인해주세요</p>
<pre><code class="language-sql">-- 제약조건을 확인하고싶다면
SHOW CREATE TABLE review_like;
</code></pre>
<hr>
<h3 id="cascade-옵션을-추가해서-외래키-걸기">CASCADE 옵션을 추가해서 외래키 걸기</h3>
<pre><code class="language-sql">
-- review_like 테이블에 외래 키 제약 조건 추가
-- CASCADE 조건을 함께 걸어주면 삭제할때 자식까지 같이 삭제됨.
ALTER TABLE review_like
ADD CONSTRAINT fk_review_like_idx
FOREIGN KEY (review_idx)
REFERENCES review(idx)
ON DELETE CASCADE;
</code></pre>
<p>다시 로직에서 JPA 코드를 실행하게되면 review.idx 와 연결되어있는review_like.review_idx,review_tag.review_idx 가 함께 삭제된다!</p>
<pre><code class="language-java"> reviewRepository.deleteById(reviewIdx);</code></pre>
<p><a href="https://velog.velcdn.com/images/explorer-cat/post/5fa4e9c3-face-43e9-8bc2-37610a3bb9cf/image.png"></a></p>
<h4 id="참고-레퍼런스">참고 레퍼런스</h4>
<p><a href="https://jydlove.tistory.com/50">https://jydlove.tistory.com/50</a>
<a href="https://creamilk88.tistory.com/158">https://creamilk88.tistory.com/158</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React Native] IOS에서 http://  로 구성된 WebView 화면이 출력되지 않는 경우 해결방법!]]></title>
            <link>https://velog.io/@explorer-cat/ReactNative-IOS%EC%97%90%EC%84%9C-http-%EB%A1%9C-%EA%B5%AC%EC%84%B1%EB%90%9C-WebView-%ED%99%94%EB%A9%B4%EC%9D%B4-%EC%B6%9C%EB%A0%A5%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EA%B2%BD%EC%9A%B0-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@explorer-cat/ReactNative-IOS%EC%97%90%EC%84%9C-http-%EB%A1%9C-%EA%B5%AC%EC%84%B1%EB%90%9C-WebView-%ED%99%94%EB%A9%B4%EC%9D%B4-%EC%B6%9C%EB%A0%A5%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EA%B2%BD%EC%9A%B0-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Mon, 09 Oct 2023 07:15:32 GMT</pubDate>
            <description><![CDATA[<p>최근 회사 일이 앱 런칭일정으로 인해 사이드 프로젝트에 시간을 많이 투자하지 못하고있었습니다.</p>
<p>현재 진행중인 사이드 프로젝트 <strong>&quot;Flambus(플램버스)</strong>&quot;는 내 근처 맛집을 탐험하며 아케이드 형식 게임 방식으로 맛집을 탐방하며, 맛집 정보를 공유하는 플랫폼 입니다</p>
<p>대학생 디자이너 3명, 취준 프론트엔드 개발자 1명, 백엔드개발자 2명과 함께 진행하고 있습니다. 저는 백엔드 개발로 참여했지만, 회사에서는 ReactNative 앱을 개발하고 있기 때문에 프론트엔드 개발자인 &quot;섭우&quot;님에게 많이 배우기 위해 앱 개발도 조금씩 참여하기러 했습니다!</p>
<div style = "display:flex">
  <img width="20%" style = "margin-right:10px" src ="https://velog.velcdn.com/images/explorer-cat/post/7c24f980-324f-4ed6-ab6a-55c145cb308c/image.png">
</div>


<p>서비스의 아키텍쳐와 ERD 등 개발의 관련된 글은 정리해서 또 포스팅할 예정이에요</p>
<hr>
<p>현재 저희 프로젝트는 섭우님이 카카오맵 기반의 웹뷰로 맵을 구현하고 앱과 통신 브릿지를 통해 웹뷰와 앱이 통신시키는 방식으로 개발을 진행중입니다
몇일 전 섭우님에게 카톡을 통해 해당 이슈를 파악하게 되었어요.</p>
<div style = "display:flex">
  <img width = "50%" src = "https://velog.velcdn.com/images/explorer-cat/post/0bc3d9a6-1aa5-433d-aadb-924f507e8fee/image.png">
</div>

<p>(텍스트에서도 빡침이 느껴네요)</p>
<p>저도 너무 궁금해서 원인을 파악하고싶은 맘에 칼퇴근을 하고 호기롭게 에뮬레이터를 킨 순간
IOS에서만 웹뷰 화면에서 백지가 출력되었습니다.</p>
<p>물론 사파리, 혹은 PC크롬으로 웹서버 접근시 정상적으로 맵이 출력이 되는데 말이죠.</p>
  <img width = "50%" src ="https://velog.velcdn.com/images/explorer-cat/post/7563d6df-c244-4f7c-bd67-962940f0661a/image.png">




<h3 id="1차-시도">1차 시도</h3>
<p>일단 카카오 SNS 로그인을 구현할때도 https:// 인증 관련한 이슈가 많았기 때문에 로컬
<a href="http://localhost%EB%A1%9C">http://localhost로</a> 리다이렉팅 되는게 문제인거 같아서 &quot;mkcert&quot; 를 이용해 로컬 https 인증 도메인을 달면 해결되겠지 생각했습니다.</p>
<blockquote>
</blockquote>
<p>mkcert 로컬 환경에서도 https:// 보안 인증서를 사용할수있어요 !
<a href="https://github.com/FiloSottile/mkcert">https://github.com/FiloSottile/mkcert</a></p>
<p>https://도메인을 달고도 IOS 화면에서는 백지가 계속 출력되었습니다.</p>
<p>다른 플랫폼들에서는 정상동작 하기에 확실히 IOS 정책상 인증되지않은 도메인을 차단하는것이라고 생각했는데 안되서 당황했습니다.</p>
<h3 id="2차-시도">2차 시도</h3>
<p>2차 시도까지 오기에 1~2시간 가량의 삽질이 있었습니다. 
로컬이 아닌 실제 서버에도 웹서버를 올려도 봤지만 결과는 FAIL 그래서 &quot;근본적인 문제&quot;를 해결 할 수 있는 방법이 있는지 찾기 시작했습니다.</p>
<p>바로 IOS 자체에서 해당 정책을 풀 수 있는지 말이죠!</p>
<p>그래서 찾게된것이 바로 <strong>ATS (App Transport Security)</strong> 입니다.
2015년부터 IOS 9 버전부터 도입된 보안사항 입니다.
이 시큐리티 보안 정책이 인증되지않은 http 컨텐츠의 로드를 막고있었던 모양이네요.</p>
<h3 id="해결방법">해결방법</h3>
<p>Xcode를 열어주세요 (IDE에서 ios폴더에서 바로 info.plist 열어두되요!)</p>
<blockquote>
<p>info.plist 를 열고 &quot;App Transport Security Settings&quot; 카테고리에서
&quot;Allow Arbitray Loads in Web Content&quot; 
&quot;Allow Arbitray Loads&quot; 
2개를 추가하고 &quot;Yes&quot;로 해주시고 새로 빌드해주세요!</p>
</blockquote>
<img width="100%" src = "https://velog.velcdn.com/images/explorer-cat/post/b6b37d7c-9219-4610-9e5e-6fe2b50bc6f0/image.png">




<h3 id="결과">결과</h3>
<img width ="100%" src = "https://velog.velcdn.com/images/explorer-cat/post/17484f2a-3979-4865-b551-b790381cde75/image.png">

<p>정상적으로 웹뷰 화면이 출력이 되네요
물론 정식 배포할때는 웹서버에서 https 인증도메인이 달려서 나가겠지만, 개발할동안은 로컬에서도 편하게 사용할 수 있도록 풀어두는게 좋을것 같네요!</p>
<h4 id="레퍼런스">레퍼런스</h4>
<p><a href="https://developer.apple.com/news/?id=jxky8h89">https://developer.apple.com/news/?id=jxky8h89</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS EC2 AMI 2023.01 이후 마리아디비 설치방법]]></title>
            <link>https://velog.io/@explorer-cat/AWS-EC2-AMI-2023.01-%EC%9D%B4%ED%9B%84-%EB%A7%88%EB%A6%AC%EC%95%84%EB%94%94%EB%B9%84-%EC%84%A4%EC%B9%98%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@explorer-cat/AWS-EC2-AMI-2023.01-%EC%9D%B4%ED%9B%84-%EB%A7%88%EB%A6%AC%EC%95%84%EB%94%94%EB%B9%84-%EC%84%A4%EC%B9%98%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Wed, 27 Sep 2023 13:54:13 GMT</pubDate>
            <description><![CDATA[<p>구글링해서 나오는 설치방법들이 안되길래 찾아보니
최신 AMI EC2(23.01)로 세팅하면 설치 방법이 달라지는거 같다.</p>
<p>이것도 모르고 3시간을 삽질했다..
</p>
<h3 id="dnf-명령어-업데이트">dnf 명령어 업데이트</h3>
<pre><code>sudo dnf update</code></pre><h3 id="설치">설치</h3>
<pre><code>sudo dnf install mariadb105-server</code></pre><h3 id="마리아-디비-시작">마리아 디비 시작</h3>
<pre><code>sudo systemctl start mariadb</code></pre><h3 id="설치-확인">설치 확인</h3>
<pre><code>sudo systemctl status mariadb</code></pre><h3 id="시큐어-설치선택">시큐어 설치(선택)</h3>
<pre><code>sudo mysql_secure_installation</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[React Native] WebView를 사용하고있는 화면에서 앱이 종료되어 버리는 경우 대처방법]]></title>
            <link>https://velog.io/@explorer-cat/React-Native-WebView%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B3%A0%EC%9E%88%EB%8A%94-%ED%99%94%EB%A9%B4%EC%97%90%EC%84%9C-%EC%95%B1%EC%9D%B4-%EC%A2%85%EB%A3%8C%EB%90%98%EC%96%B4-%EB%B2%84%EB%A6%AC%EB%8A%94-%EA%B2%BD%EC%9A%B0-%EB%8C%80%EC%B2%98%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@explorer-cat/React-Native-WebView%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B3%A0%EC%9E%88%EB%8A%94-%ED%99%94%EB%A9%B4%EC%97%90%EC%84%9C-%EC%95%B1%EC%9D%B4-%EC%A2%85%EB%A3%8C%EB%90%98%EC%96%B4-%EB%B2%84%EB%A6%AC%EB%8A%94-%EA%B2%BD%EC%9A%B0-%EB%8C%80%EC%B2%98%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Fri, 08 Sep 2023 01:50:10 GMT</pubDate>
            <description><![CDATA[<img src="https://velog.velcdn.com/images/explorer-cat/post/6c8eda41-e224-4af6-8957-7daae935f30a/image.gif" width="20%" height="20%">


<h3 id="webview를-사용하고있는-화면에서-앱이-종료되어-버리는-경우-대처방법">WebView를 사용하고있는 화면에서 앱이 종료되어 버리는 경우 대처방법</h3>
<p>안드로이드에서만 특정 스크린을 사용중 스크롤을 아래로 내렸을 경우에 앱이 죽어버리는 현상이 발생습니다.
아무런 로그도 발생하지않고 앱이 죽어버려, 원인 파악이 힘들었는데 해당 화면에서는 일부 기능을 react-native-webview 를 사용한 webview로 기능을 제공해주고 있어 해당 라이브러리 커뮤니티에서 서칭중 비슷한 증상 이슈를 발견했습니다.</p>
<p>해당 이슈는 웹뷰의 브라우저와 안드로이드 버전간의 충돌로 인해 웹뷰를 지나서 스크롤되는 순간 앱이 crash 됩니다.
<del>Chrome 73이 출시되기 시작하면서 해당 문제가 발생하기 시작했다합니다.</del></p>
<hr>
<h3 id="커뮤니티에서-제공하는-해결-솔루션중-대표적인것-2가지를-가져왔습니다">커뮤니티에서 제공하는 해결 솔루션중 대표적인것 2가지를 가져왔습니다.</h3>
<p>해결방법 1. 사용자에게 직업 특정 크롬 버전 사용을 권장한다    </p>
<p>해결방법 2.</p>
<pre><code>        &lt;View style={{ flex: 1 }}&gt;
          &lt;WebView style={{ opacity: 0.99, minHeight: 1 }} ... /&gt;
        &lt;/View&gt;</code></pre><p>웹뷰에 불투명도를 적용하는 방법으로 문제를 해결했습니다.</p>
<p>아래는 해당 이슈의 대한 라이브러리 개발자의 댓글 입니다.</p>
<blockquote>
<p>좀 더 많은 맥락을 추가하자면; 지난 목요일 Chrome 73이 출시되기 시작하자 우리도 이 문제를 경험했습니다. 불투명도를 적용하기 전에 약 150,000번의 충돌이 발생했습니다. 0.99 해킹으로 대부분의 경우 문제가 해결되었습니다. 그러나 수정 사항이 적용된 경우 WebView 콘텐츠가 올바르게 렌더링되지 않는 문제(스크롤 작업이 비정상적으로 작동하고 탭이 실제 탭과 다른 위치에 등록됨)에 문제가 발생했습니다. ScrollView에 포함된 5000px 높이의 대형 웹뷰로 제한되는 것 같습니다. 특정 웹뷰의 경우 이제 불투명도: 0.99 수정 없이도 충돌을 &#39;수정&#39;하는 것처럼 보이는 모달에서 엽니다. 우리는 아직 이 문제를 조사하는 중이므로 그보다 더 구체적인 정보는 없지만 아마도 이 추가 정보는 같은 문제를 겪고 있는 누군가에게 도움이 될 것입니다.</p>
</blockquote>
<p>더많은 정보는 아래 링크를 참고하세요!</p>
<p><a href="https://github.com/react-native-webview/react-native-webview/issues/429">https://github.com/react-native-webview/react-native-webview/issues/429</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ReactNative] 국내 1위 네이버 캘린더 앱 클론 코딩을 해보자 (1)]]></title>
            <link>https://velog.io/@explorer-cat/ReactNative-%EA%B5%AD%EB%82%B4-1%EC%9C%84-%EB%84%A4%EC%9D%B4%EB%B2%84-%EC%BA%98%EB%A6%B0%EB%8D%94-%EC%95%B1-%ED%81%B4%EB%A1%A0-%EC%BD%94%EB%94%A9%EC%9D%84-%ED%95%B4%EB%B3%B4%EC%9E%90-1</link>
            <guid>https://velog.io/@explorer-cat/ReactNative-%EA%B5%AD%EB%82%B4-1%EC%9C%84-%EB%84%A4%EC%9D%B4%EB%B2%84-%EC%BA%98%EB%A6%B0%EB%8D%94-%EC%95%B1-%ED%81%B4%EB%A1%A0-%EC%BD%94%EB%94%A9%EC%9D%84-%ED%95%B4%EB%B3%B4%EC%9E%90-1</guid>
            <pubDate>Sat, 05 Aug 2023 04:23:31 GMT</pubDate>
            <description><![CDATA[<p>아래는 네이버 앱 메인화면 입니다.</p>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/6dc2668a-9ddc-4e5e-8b81-199518470bf3/image.gif" alt=""></p>
<blockquote>
<p>모든 언어의 공부는 클론코딩부터 시작이다. 대학생 시절 노마드 코더나 유튜브에 돌아다니는 클론코딩을 했던 경험이 많았습니다. ReactNative 를 첫 시작하는 초보개발자분들이 있다면 오픈소스 라이브러리의 사용법이나 ReactNative가 이런것이구나 하고 익히는데 캘린더만한 앱이 없을 것 같고, 저도 집에서 천천히 복습할겸 시작하게된 프로젝트입니다.</p>
</blockquote>
<blockquote>
<p>환경</p>
</blockquote>
<ul>
<li>react-native 0.72.3</li>
<li>react-native-calendars</li>
<li>react-redux</li>
</ul>
<h1 id="1-개발에-필요한-util-함수-개발">1. 개발에 필요한 Util 함수 개발</h1>
<p>calendarUtils.js</p>
<pre><code class="language-javascript">/**
 * @title Date 객체를 스트링 형태의 날짜로 변환해줍니다.
 * @param date
 * @returns {string}
 */
export function getFormattedDate(date) {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, &#39;0&#39;);
    const day = String(date.getDate()).padStart(2, &#39;0&#39;);

    return `${year}-${month}-${day}`;
}


/**
 * 스트링 형태의 날짜를 주면 분리해서 json으로 만들어줌.
 * @param dateString
 * @returns {{month: *, year: *, day: *}}
 */
export function parseDateString(dateString) {
    const [year, month, day] = dateString.split(&#39;-&#39;);

    return {
        year,
        month: String(parseInt(month)), // 단일 자리 월에 0을 붙이지 않도록 변환
        day: String(parseInt(day))      // 단일 자리 일에 0을 붙이지 않도록 변환
    };
}
</code></pre>
<h1 id="2-캘린더앱의-핵심-캘린더-구현하기">2. 캘린더앱의 핵심 캘린더 구현하기</h1>
<p>가장 많이 사용하는 오픈소스 라이브러리를 사용해서 최대한 네이버 캘린더와 비슷하게 한번 만들어보려 합니다.</p>
<blockquote>
<p>무려 8.6K의 깃허브 Stars!!
<a href="https://wix.github.io/react-native-calendars/docs/">https://wix.github.io/react-native-calendars/docs/</a></p>
</blockquote>
<hr>
<h2 id="2-1-메인-캘린더">2-1 메인 캘린더</h2>
<p>사용자가 선택한 날짜는 앱 자체에서 전역적으로 관리하면서 다른 기능에도 부가적으로 사용해야될것 같아 ReduxStore에서 관리하도록 처리했습니다.</p>
<p>MainCalendar.js</p>
<pre><code class="language-javascript">export default function MainCalendar() {
    const dispatch = useDispatch();
    const windowWidth = Dimensions.get(&#39;window&#39;).width;


    let currentMonth;
    return (
        &lt;&gt;
            &lt;CalendarHeader /&gt;
            &lt;CalendarList
                onDayPress={day =&gt; {
                    dispatch(selectedDate(day.dateString))
                }}
                customHeader={({current})=&gt;{
                    //현재 헤더의 년월 정보를 변수에 저장해놓고 Day컴포넌트르 만들때 사용할게요.
                    currentMonth = parseDateString(current);
                }}
                headerStyle={{height: 200}}
                dayComponent={(data) =&gt; {
                    return (
                        &lt;Day data={data} currentMonth ={currentMonth}/&gt;
                    )
                }}
                //월이 바뀔경우 바뀐 월의 1일을 선택합니다.
                onMonthChange={month =&gt; {
                    // console.log(&quot;바뀜&quot;)
                    const parseString = parseDateString(month.dateString);
                    dispatch(selectedDate(`${parseString.year}-${parseString.month &lt; 10 ? `0${parseString.month}` : `${parseString.month}`}-01`))
                }}
                hideExtraDays={false}
                horizontal={true}
                pagingEnabled={true}
                disabledByDefault={true}
                calendarWidth={windowWidth}

            /&gt;
        &lt;/&gt;
    );
}</code></pre>
<h2 id="2-2-각-일day영역-컴포넌트-만들기">2-2 각 일(day)영역 컴포넌트 만들기</h2>
<ol>
<li><p>handleOnPressDay() 함수는 각 컴포넌트별로 이벤트가 모두 생성되기때문에 UseCallBack을 통해 메모이제이션 해두고 캐싱하도록 처리해 컴포넌트 성능을 개선했습니다.</p>
</li>
<li><p>currentMonth는 캘린더가 첫 화면을 렌더링할때 렌더링하는 년,달 입니다.
해당 매게변수에 저장되있는 값으로 이전달인지 다음달인지 확인해서 컴포넌트 Text Color를 결정합니다. (아래 코드 부분을 보시면됩니다.)</p>
</li>
</ol>
<pre><code class="language-javascript">customHeader={({current})=&gt;{
     //현재 헤더의 년월 정보를 변수에 저장해놓고 Day컴포넌트르 만들때 사용할게요.
       //캘린더가 생성될때마다 생성되는 날짜 정보를 변수에 저장해서 사용함.
     currentMonth = parseDateString(current);
  }}</code></pre>
<p>MainCalendar.js</p>
<pre><code class="language-javascript">const Day = ({ data ,currentMonth}) =&gt; {
    const todayDate = getFormattedDate(new Date());

    const dayDate = {
        year: data.date.year.toString(),
        month: data.date.month.toString(),
        day: data.date.day.toString(),
    };
    const dispatch = useDispatch();
    const selected = useSelector(state =&gt; state.selectedDate);
    //현재 선택되어있는 날짜 flag
    const isSelected = selected === data.date.dateString;
    //현재 생성하는 Day 컴포넌트의 월과 헤더 타이틀에 표시되는 월이 동일한가? 동일하지 않으면 Gray 처리 해야함.
    const isSameMonth = currentMonth.month === dayDate.month;
    //현재 생성하는 Day컴포넌트가 오늘인가? 오늘이라면 검은색으로 표시해줘야함
    const isToday = todayDate === data.date.dateString;
    //컴포넌트 Text Color
    const textColor = isSameMonth ? (isToday ? &#39;white&#39; : &#39;black&#39;) : &#39;gray&#39;;

    //해당 함수는 각 컴포넌트마다 모두 똑같기 때문에 UseCallBack에 넣어놓고 사용하자.
    const handleOnPressDay = useCallback(({ date }) =&gt; {
        dispatch(selectedDate(date.dateString));
    }, [dispatch]);

    return (
        &lt;TouchableOpacity
            activeOpacity={1}
            style={[{ height: 40, paddingLeft: 8, paddingRight: 8 }]}
            onPress={() =&gt; handleOnPressDay(data)}&gt;
            &lt;View
                style={[
                    styles.day,
                    isSelected &amp;&amp; styles.selectedDay,
                    isToday &amp;&amp; styles.today,
                ]}&gt;
                &lt;Text
                    style={[
                        globalFonts.fontSemiBold,
                        {
                            fontSize: 13,
                            color: textColor,
                        },
                    ]}&gt;
                    {data.date.day}
                &lt;/Text&gt;
            &lt;/View&gt;
        &lt;/TouchableOpacity&gt;
    );
};</code></pre>
<h2 id="2-3-사용자가-선택한-날짜-store에-저장하기">2-3 사용자가 선택한 날짜 store에 저장하기</h2>
<p>action.js</p>
<pre><code class="language-javascript">// Action Types
export const SELECTED_DATE = &#39;SELECTED_DATE&#39;;
// Action Creators
export const selectedDate = (state) =&gt; {
    return {
        type: SELECTED_DATE,
        state : state
    };
};</code></pre>
<h2 id="2-4-캘린더-헤더-만들기">2-4 캘린더 헤더 만들기</h2>
<p>1.헤더에서도 해당 년월을 보여 줘야하는 요구사항이 있어 store에 저장되어있는 선택된 날짜를 가져와 관리합니다.</p>
<pre><code class="language-javascript">const CalendarHeader = React.memo(({}) =&gt; {
    const selected = useSelector(state =&gt; state.selectedDate);
    const parts = selected.split(&#39;-&#39;);
    const year = parts[0];
    const month = parseInt(parts[1]);

    return(
        &lt;View style = {[styles.calendarHeaderView,{height:70}]}&gt;
            &lt;View style = {{flex:1}}&gt;
                &lt;Text style = {[globalFonts.fontBold,{fontSize:20}]}&gt;{`${year}. ${month}`}&lt;/Text&gt;
            &lt;/View&gt;
            &lt;View style = {{flex:1,flexDirection:&#39;row&#39;,alignItems:&#39;flex-end&#39;,marginBottom:4}}&gt;
                    &lt;Text style = {[styles.dayOfWeek,{color:&#39;red&#39;,marginRight:1}]}&gt;일&lt;/Text&gt;
                    &lt;Text style = {[styles.dayOfWeek]}&gt;월&lt;/Text&gt;
                    &lt;Text style = {[styles.dayOfWeek]}&gt;화&lt;/Text&gt;
                    &lt;Text style = {[styles.dayOfWeek]}&gt;수&lt;/Text&gt;
                    &lt;Text style = {[styles.dayOfWeek]}&gt;목&lt;/Text&gt;
                    &lt;Text style = {[styles.dayOfWeek]}&gt;금&lt;/Text&gt;
                    &lt;Text style = {[styles.dayOfWeek,{color:&#39;blue&#39;}]}&gt;토&lt;/Text&gt;
            &lt;/View&gt;
        &lt;/View&gt;
    )
})</code></pre>
<h2 id="2-5-전체-코드">2-5 전체 코드</h2>
<pre><code class="language-javascript">import {StyleSheet, Text, TouchableOpacity, View} from &#39;react-native&#39;;
import React, {useState, useMemo, useEffect, useCallback} from &#39;react&#39;;
import {Dimensions} from &#39;react-native&#39;;
import {CalendarList} from &quot;react-native-calendars&quot;;
import {globalFonts} from &quot;../styles/globalStyles&quot;;
import {getFormattedDate, parseDateString} from &#39;../utils/calendarUtils&#39;
import {selectedDate, selectedPrevDate} from &quot;../modules/actions/actions&quot;;
import {Provider, useDispatch, useSelector} from &#39;react-redux&#39;


const todayDate = getFormattedDate(new Date());
export default function MainCalendar() {
    const dispatch = useDispatch();
    const windowWidth = Dimensions.get(&#39;window&#39;).width;
    let currentMonth;

    return (
        &lt;&gt;
            &lt;CalendarHeader /&gt;
            &lt;CalendarList
                onDayPress={day =&gt; {
                    dispatch(selectedDate(day.dateString))
                }}
                customHeader={({current})=&gt;{
                    //현재 헤더의 년월 정보를 변수에 저장해놓고 Day컴포넌트르 만들때 사용할게요.
                    currentMonth = parseDateString(current);
                }}
                dayComponent={(data) =&gt; {
                    return (
                        &lt;View style = {{height:40}}&gt;
                            &lt;Day data={data} currentMonth ={currentMonth}/&gt;
                        &lt;/View&gt;
                    )
                }}
                //월이 바뀔경우 바뀐 월의 1일을 선택합니다.
                onMonthChange={month =&gt; {
                    //오늘 날짜가 포함된 달이 아닌경우에만 1을 선택함
                    if (month.dateString !== todayDate) {
                        const parseString = parseDateString(month.dateString);
                        dispatch(selectedDate(`${parseString.year}-${parseString.month &lt; 10 ? `0${parseString.month}` : `${parseString.month}`}-01`))

                    }
                }}
                hideExtraDays={false}
                horizontal={true}
                pagingEnabled={true}
                // disabledByDefault={true}
                calendarWidth={windowWidth}

            /&gt;
        &lt;/&gt;
    );
}




const Day = ({ data ,currentMonth}) =&gt; {


    const dayDate = {
        year: data.date.year.toString(),
        month: data.date.month.toString(),
        day: data.date.day.toString(),
    };
    const dispatch = useDispatch();
    const selected = useSelector(state =&gt; state.selectedDate);
    //현재 선택되어있는 날짜 flag
    const isSelected = selected === data.date.dateString;
    //현재 생성하는 Day 컴포넌트의 월과 헤더 타이틀에 표시되는 월이 동일한가? 동일하지 않으면 Gray 처리 해야함.
    const isSameMonth = currentMonth.month === dayDate.month;
    //현재 생성하는 Day컴포넌트가 오늘인가? 오늘이라면 검은색으로 표시해줘야함
    const isToday = todayDate === data.date.dateString;
    //컴포넌트 Text Color
    const textColor = isSameMonth ? (isToday ? &#39;white&#39; : &#39;black&#39;) : &#39;gray&#39;;

    //해당 함수는 각 컴포넌트마다 모두 똑같기 때문에 UseCallBack에 넣어놓고 사용하자.
    const handleOnPressDay = useCallback(({ date }) =&gt; {
        dispatch(selectedDate(date.dateString));
    }, [dispatch]);

    return (
        &lt;TouchableOpacity
            activeOpacity={1}
            style={[{flex:1, minWidth:50,alignItems:&#39;center&#39;}]}
            onPress={() =&gt; handleOnPressDay(data)}&gt;
            &lt;View
                style={[
                    styles.day,
                    isSelected &amp;&amp; styles.selectedDay,
                    isToday &amp;&amp; styles.today,
                ]}&gt;
                &lt;Text
                    style={[
                        globalFonts.fontSemiBold,
                        {
                            fontSize: 13,
                            color: textColor,
                            textAlign: &#39;center&#39;,

                        },
                    ]}&gt;
                    {data.date.day}
                &lt;/Text&gt;
            &lt;/View&gt;
        &lt;/TouchableOpacity&gt;
    );
};



const CalendarHeader = React.memo(({}) =&gt; {
    const selected = useSelector(state =&gt; state.selectedDate);
    const parts = selected.split(&#39;-&#39;);
    const year = parts[0];
    const month = parseInt(parts[1]);

    return(
        &lt;View style = {[styles.calendarHeaderView,{height:70}]}&gt;
            &lt;View style = {{flex:1}}&gt;
                &lt;Text style = {[globalFonts.fontBold,{fontSize:20}]}&gt;{`${year}. ${month}`}&lt;/Text&gt;
            &lt;/View&gt;
            &lt;View style = {{flex:1,flexDirection:&#39;row&#39;,alignItems:&#39;flex-end&#39;,marginBottom:4}}&gt;
                    &lt;Text style = {[styles.dayOfWeek,{color:&#39;red&#39;,marginRight:1}]}&gt;일&lt;/Text&gt;
                    &lt;Text style = {[styles.dayOfWeek]}&gt;월&lt;/Text&gt;
                    &lt;Text style = {[styles.dayOfWeek]}&gt;화&lt;/Text&gt;
                    &lt;Text style = {[styles.dayOfWeek]}&gt;수&lt;/Text&gt;
                    &lt;Text style = {[styles.dayOfWeek]}&gt;목&lt;/Text&gt;
                    &lt;Text style = {[styles.dayOfWeek]}&gt;금&lt;/Text&gt;
                    &lt;Text style = {[styles.dayOfWeek,{color:&#39;blue&#39;}]}&gt;토&lt;/Text&gt;
            &lt;/View&gt;
        &lt;/View&gt;
    )
})


const styles = StyleSheet.create({
    calendarHeaderView: {
        paddingLeft: 16,
        paddingRight: 16,
        paddingTop: 12,
        // paddingBottom: 12,
        flexDirection:&#39;column&#39;,
        // flex:1,
    },
    dayOfWeek : {
        flex:1,
        minWidth:50,
        fontSize:12,
        textAlign: &#39;center&#39;,
    },
    day: {
        alignItems: &#39;center&#39;,
        justifyContent: &#39;center&#39;,
        width: 22,
        height: 22,
    },
    selectedDay: {
        borderRadius: 15,
        backgroundColor: &#39;#C9C9C9&#39;
    },
    today: {
        borderRadius: 15,
        backgroundColor: &#39;black&#39;
    }

});



</code></pre>
<h2 id="2-5-완성된-화면">2-5 완성된 화면</h2>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/b110bd7e-ea00-45fe-8921-dbc9e7e00b19/image.gif" alt=""></p>
<h2 id="실제-네이버-앱">실제 네이버 앱</h2>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/af3123ea-12af-4e88-adad-7e0565546105/image.jpeg" alt=""></p>
<hr>
<p>좀 비슷한가요? 시간이 된다면 하단 상세 일정 정보와 메뉴 네비게이터도 만들어서 올려보겠습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ReactNative] Android 빌드중 v2.14.1 and RN 0.71.0 (Android) Could not get unknown property 'hermesEnabled' for project ':app' of type org.gradle.api.Project. 오류 발생시 해결방법]]></title>
            <link>https://velog.io/@explorer-cat/ReactNative-Android-%EB%B9%8C%EB%93%9C%EC%A4%91-v2.14.1-and-RN-0.71.0-Android-Could-not-get-unknown-property-hermesEnabled-for-project-app-of-type-org.gradle.api.Project.-%EC%98%A4%EB%A5%98-%EB%B0%9C%EC%83%9D%EC%8B%9C-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@explorer-cat/ReactNative-Android-%EB%B9%8C%EB%93%9C%EC%A4%91-v2.14.1-and-RN-0.71.0-Android-Could-not-get-unknown-property-hermesEnabled-for-project-app-of-type-org.gradle.api.Project.-%EC%98%A4%EB%A5%98-%EB%B0%9C%EC%83%9D%EC%8B%9C-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Mon, 31 Jul 2023 04:32:47 GMT</pubDate>
            <description><![CDATA[<p>react-native-reanimated 에 올라온 이슈
<a href="https://github.com/software-mansion/react-native-reanimated/issues/3959">https://github.com/software-mansion/react-native-reanimated/issues/3959</a></p>
<hr>
<h3 id="빌드시-아래와같이-뜨시는분들은-모두-해결이-가능한-방법인듯">빌드시 아래와같이 뜨시는분들은 모두 해결이 가능한 방법인듯</h3>
<pre><code>Where: Build file &#39;D:\Projects\Vow\iye_react_native_user_app\node_modules\react-native-reanimated\android\build.gradle&#39; line: 152

What went wrong: A problem occurred evaluating project &#39;:react-native-reanimated&#39;.</code></pre><pre><code>After upgrading to 2.14.4 - Could not get unknown property &#39;hermesEnabled&#39; for project &#39;:app&#39; of type org.gradle.api.Project</code></pre><pre><code>* where: build file &#39;/users/mypc/directory/node_modules/react-native-reanimated/android/build.gradle&#39; line: 246</code></pre><p>참고로 나는 3번째에 출력되는 로그가 발생했었다.</p>
<hr>
<h3 id="해결방법">해결방법</h3>
<p>이슈 내용은 reanimated 패키지와 리액트 0.70.0 이상 버전에서 나타나는 이슈로 확인된다.</p>
<p>해결방법은 node_modules에 설치된 react-native-reanimated/android 폴더에 있는 build.gradle 에서 아래와같이 수정하면된다.</p>
<p>아래 명령어로 node_module의 관리자권한을 부여해서 node_module를 수정했음.</p>
<pre><code class="language-javascript"> chomod -R 777 node_module/react-native-reanimated/android</code></pre>
<pre><code class="language-javascript">
// Check if Hermes is enabled in app setup
    def appProject = rootProject.allprojects.find { it.plugins.hasPlugin(&#39;com.android.application&#39;) }
    if ((REACT_NATIVE_MINOR_VERSION &gt;= 71 &amp;&amp; appProject?.hermesEnabled?.toBoolean()) || appProject?.ext?.react?.enableHermes?.toBoolean()) {
        return &quot;hermes&quot;
    }</code></pre>
<p>아래코드로 변경해주면 정상적으로 빌드된다.</p>
<pre><code class="language-javascript">
// Check if Hermes is enabled in app setup
    def appProject = rootProject.allprojects.find { it.plugins.hasPlugin(&#39;com.android.application&#39;) }
       if ((REACT_NATIVE_MINOR_VERSION &gt;= 71) || appProject?.ext?.react?.enableHermes?.toBoolean()) {
        return &quot;hermes&quot;
    }</code></pre>
<h3 id="fix-pull-request-링크">Fix pull request 링크</h3>
<p><a href="https://github.com/codewithmecoder/react-native-reanimated/commit/37ff277a9a2dc2c1c3c967a42ddc7089ca952933">https://github.com/codewithmecoder/react-native-reanimated/commit/37ff277a9a2dc2c1c3c967a42ddc7089ca952933</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot][AWS S3] 이미지,파일 업로드 모듈 개발기]]></title>
            <link>https://velog.io/@explorer-cat/SpringBoot-AWS-S3%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%B4-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81-%EB%A7%8C%EB%93%A4%EA%B8%B01</link>
            <guid>https://velog.io/@explorer-cat/SpringBoot-AWS-S3%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%B4-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81-%EB%A7%8C%EB%93%A4%EA%B8%B01</guid>
            <pubDate>Sun, 30 Jul 2023 13:27:06 GMT</pubDate>
            <description><![CDATA[<h2 id="💡-업로드-모듈을-개발하는-이유">💡 업로드 모듈을 개발하는 이유</h2>
<p><del>내 머리속에 있는 지우개 어서오고 오랜만</del>
<img src="https://velog.velcdn.com/images/explorer-cat/post/f3235579-b7d5-43ab-b838-c795409e8b5c/image.png" alt=""></p>
<p>최근 사이드 프로젝트를 소셜 네트워크(SNS) 기반 맛집 소개앱(플램버스)를 진행하고 있다.
현재 디자인 및 기획을 디테일 하고 있는 과정에서 미리 공통적으로 사용할만한 주말마다 짬짬히 비즈니스 로직을 개발하고 있었다.</p>
<p>이미 완성은 했었지만 나중에 다시 보면 분명 왜 이렇게 했는지 기억을 다 지워버릴것이기 때문에 코드를 정리하면서 회고하려고 합니다</p>
<p>업로드 모듈은 리뷰 작성, 피드 작성..등등 많은 곳에서 사용될 예정이므로 결합도와 확장성에 신경을 써서 개발하려고 노력했습니다.<del>(지만 좋은 코드인지는 잘 모르겠음)</del></p>
<p>혹시 비슷한 로직을 구현하고 계신분중에 성격급하신분들은 그냥 아래 코드를 참고하시면됩니다.
<del>시간이 금이다!</del>
<a href="https://github.com/explorer-cat/flambus-v1.0-springboot">https://github.com/explorer-cat/flambus-v1.0-springboot</a></p>
<h2 id="why-s3를-선택했는지">WHY S3를 선택했는지?</h2>
<blockquote>
<p>Amazon Simple Storage Service(Amazon S3)는 업계 최고의 확장성, 데이터 가용성, 보안 및 성능을 제공하는 객체 스토리지 서비스입니다. 모든 규모와 업종의 고객은 Amazon S3를 사용하여 데이터 레이크, 웹 사이트, 모바일 애플리케이션, 백업 및 복원, 아카이브, 엔터프라이즈 애플리케이션, IoT 디바이스, 빅 데이터 분석 등 다양한 사용 사례에서 원하는 양의 데이터를 저장하고 보호할 수 있습니다. Amazon S3는 특정 비즈니스, 조직 및 규정 준수 요구 사항에 맞게 데이터에 대한 액세스를 최적화, 구조화 및 구성할 수 있는 관리 기능을 제공합니다.</p>
</blockquote>
<p>애초 설계할때부터 서버에 직접적으로 업로드 하는 방식은 생각하지 않았다.
일단 AWS 프리티어를 사용중이기도 했고, 내가 알기론 EC2 서버에 네트워크 업로드,다운로드 용량에 따라서 추가 요금이 발생한다. <del>(예전에 무지성 git clone 하다가 요금 폭탄을 맞은적이 있음)</del></p>
<p>S3는 요금이 아주 착하다. 또한 SDK를 통한 여러 써드파티 기능들도 지원을 하고 있다.
실제 사용하고있는 Ubuntu 서버에서 S3로 바로 접근도 가능함!</p>
<h2 id="spring-boot-aws-s3-연결">SPRING BOOT AWS S3 연결</h2>
<blockquote>
<p>세팅법은 구글에 검색하면 많이 나와있으니, 아무거나 참고하면된다.</p>
</blockquote>
<h2 id="업로드-서비스-구조">업로드 서비스 구조</h2>
<h4 id="uploadcontrollerjava">UploadController.java</h4>
<p>해당 컨트롤러는 업로드 서비스 테스트를 위해 작성했습니다.</p>
<pre><code class="language-java">@PutMapping(value=&quot;/upload&quot;,consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResultDTO&lt;Map&lt;String,Object&gt;&gt; saveImage(
            @RequestParam(value=&quot;image&quot;) List&lt;MultipartFile&gt; image,
            @RequestParam String userId) throws IOException {
        Map&lt;String,Object&gt; sampleArray = new HashMap&lt;&gt;();
        uploadService.upload(image,userId ,AttachmentType.REVIEW,2344);
        return ResultDTO.of(ApiResponseCode.CREATED.getCode(),ApiResponseCode.CREATED.getMessage(), sampleArray);
    }</code></pre>
<h4 id="uploadservicejava">UploadService.java</h4>
<pre><code class="language-java">    /**
     * @param multipartFile     업로드된 파일 multipartFile 객체
     * @param userId            사용자 userId
     * @param attachmentType    업로드 타입(&quot;REVIEW,FEED&quot;)
     * @return
     * @throws IOException
     * @title 파일 업로드
     */
    @Transactional
    public List&lt;Map&lt;String, Object&gt;&gt; upload(List&lt;MultipartFile&gt; multipartFile, String userId, AttachmentType attachmentType, long mappedId) throws IOException {

        List&lt;UploadImage&gt; saveImageDataList = new ArrayList&lt;&gt;();

        if(multipartFile.size() &lt;= 0) {
            new IllegalArgumentException(&quot;업로드될 파일이 없습니다.&quot;);
        }

        //MultipartFile로 받은 객체를 File 객체로 변환
        for (MultipartFile file : multipartFile) {
            //업로드 시도한 파일 제한 용량 검증
            validateFileSize(file);
            //MultipartFile -&gt; File 객체로 convert
            File convertFile = convert(file).orElseThrow(() -&gt; new IllegalArgumentException(&quot;MultipartFile -&gt; File 전환 실패&quot;));
            //s3에 적재될 유니크한 파일 이름
            String saveFileName = generateUniqueFileName(file.getName());
            //실제 파일 이름.
            String orginFileName = file.getOriginalFilename();
            //업로드될 버킷 PATH
            String bucketPath = attachmentType.getType() + &quot;/&quot; + userId + &quot;/&quot; + mappedId + &quot;/&quot; + saveFileName;//적재할 경로 세팅
            //업로드 된 이미지 URL
            String url = putS3(convertFile, bucketPath); //s3에 적재
            // 로컬에 생성된 File 삭제 (MultipartFile -&gt; File 전환 하며 로컬에 파일 생성됨)
            removeNewFile(convertFile);
            //S3 버킷에 적재된 이미지 파일 정보를 게시글정보와 함께 맵핑해서 디비에 저장함.
            saveImageDataList.add(UploadImage.builder()
                    .fileName(orginFileName)
                    .uniqueFileName(saveFileName)
                    .imageUrl(url)
                    .fileSize(file.getSize())
                    .attachmentType(attachmentType.getType())
                    .mappedId(mappedId)
                    .created(LocalDateTime.now())
                    .updated(LocalDateTime.now())
                    .build());
        }

        return saveDB(saveImageDataList, attachmentType, mappedId);
    }


/**
* @title S3 업로드 모듈
* @param uploadFile
* @param fileName
* @return
*/
private String putS3(File uploadFile, String fileName) {
amazonS3Client.putObject(
      new PutObjectRequest(bucket, fileName, uploadFile)
              .withCannedAcl(CannedAccessControlList.PublicRead)    // PublicRead 권한으로 업로드 됨
);
return amazonS3Client.getUrl(bucket, fileName).toString();
}</code></pre>
<hr>
<p>validateFileSize() 함수는 업로드 전 조건을 검사하는 검증 함수입니다.</p>
<pre><code class="language-java">    /**
     * @title 파일의 최대 용량을 초과한 경우 예외 처리
     * @param file
     */
    private void validateFileSize(MultipartFile file) {
        String contentType = file.getContentType();
        long fileSize = file.getSize();

        FileType fileType = FileType.fromContentType(contentType);

        if (fileType != null) {
            switch (fileType) {
                case PNG:
                case JPEG:
                    if (fileSize &gt; fileType.getMaxSize()) {
                        System.out.println(fileType.getContentType() + &quot; : 업로드 제한된 용량 이상입니다.&quot;);
                        // ZIP 파일의 최대 용량을 초과한 경우 예외 처리
                        // throw new YourException(&quot;ZIP 파일 용량 초과&quot;); // 예외 처리 방식은 상황에 맞게 정의
                    }
                    break;
                default:
                    break;
            }
        }
    }
</code></pre>
<hr>
<p>MultipartFile로 받은 데이터를 File객체로 변환해줍니다.
이 과정에서 정상적인 업로드 객체가 아니라면 예외처리 되도록 했습니다.</p>
<pre><code class="language-java">//MultipartFile -&gt; File 객체로 convert
File convertFile = convert(file).orElseThrow(() -&gt; new IllegalArgumentException(&quot;MultipartFile -&gt; File 전환 실패&quot;));</code></pre>
<pre><code class="language-java">/**
 * @title multipart 파일 객체를 일반 File 객체로 변환
 * @param file
 * @author 최성우
 * @return
 */
private Optional&lt;File&gt; convert(MultipartFile file) {
    try {
        File convertFile = new File(file.getOriginalFilename());

        if (convertFile.createNewFile()) {
            try (FileOutputStream fos = new FileOutputStream(convertFile)) {
                fos.write(file.getBytes());
            }
            return Optional.of(convertFile);
        }
        return Optional.empty();
    } catch (Exception e) {
        System.out.println(&quot;e : &quot; + e);
    }
    return Optional.empty();
}</code></pre>
<hr>
<p>다음은 실제로 File 객체로 변환된 데이터를 S3 적재하는 로직입니다.</p>
<pre><code class="language-java">//s3에 적재될 유니크한 파일 이름
String saveFileName = generateUniqueFileName(file.getName());
//실제 파일 이름.
String orginFileName = file.getOriginalFilename();
//업로드될 버킷 PATH
String bucketPath = attachmentType.getType() + &quot;/&quot; + userId + &quot;/&quot; + mappedId + &quot;/&quot; + saveFileName;//적재할 경로 세팅
</code></pre>
<p>어려운 로직은 아니였지만 고민을 오래했던것 같습니다.![]
업로드에서 끝나는것이 아닌 사용자들의 이미 업로드한 피드,리뷰에 대해수 수정,삭제에 대한 예외도 고려했어야 했습니다.</p>
<hr>
<p><strong>고려했어야할점</strong></p>
<blockquote>
<ol>
<li>만약 사용자가 동일한 이름의 파일을 여러개 올린다면 서버에서는 어떻게 구분할까?</li>
<li>만약 사용자가 리뷰를 수정할때 업로드한 이미지를 삭제한다면?</li>
<li>다른 사용자들이 올린 이미지 이름과 용량마저 다 똑같은 파일이 업로드된다면?</li>
</ol>
</blockquote>
<p>고민끝에 버킷에 업로드되는 객체의 파일이름을 UUID를 통해 유니크한 이름으로 변경하고 적재하는 방법을 선택했고 적재 경로를 _&quot;업로드타입/유저아이디/리뷰게시글아이디/유니크파일명&quot; _으로 결정을 하게되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/ee9b334f-cedf-4d86-96ef-b811ae400087/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/68d89e9d-db24-4c3e-9a2a-2acb9dbc568b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/89a66b98-cd12-46c7-92b4-c415a99a2564/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/cb6cef8b-d2bd-404a-906e-e4ad9c16574c/image.png" alt=""></p>
<p>그리고 동시에 데이터베이스에는 해당 리뷰아이디와 파일의 실제 이름, 버킷에 적재된 이름 등을 맵핑시켜 클라이언트 요청에도 문제가 없도록 처리했습니다.</p>
<p>사용자는 서버로 리뷰 아이디만 요청하면 업로드된 URL 주소를 알 수 있습니다.
<a href="https://flambus-bucket-korea.s3.ap-northeast-2.amazonaws.com/review/22/2344/1f72e22b-2c16-426b-a0ce-facbba58d2a1">https://flambus-bucket-korea.s3.ap-northeast-2.amazonaws.com/review/22/2344/1f72e22b-2c16-426b-a0ce-facbba58d2a1</a></p>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/50530390-be62-40c0-8217-f542c0cea188/image.png" alt=""></p>
<p>SaveDB 함수</p>
<p>&#39;이미 해당 리뷰 또는 피드에 업로드된 이미지가 있는 경우, 모두 삭제&#39;를 진행하는 이유는
사용자가 리뷰나 피드를 수정했을때 새로운 이미지를 추가했는지 삭제했는지 구분하기 번거롭기때문에 수정될경우 해당 폴더 자체를 지우고 새로 업로드 하는 로직으로 구현했습니다.</p>
<p>구현속도나 따로 처리해야될 부분은 많이 줄었으나, 단점은 피드나 리뷰의 글만 수정해도 첨부파일이 있다면 해당 이미지는 삭제되고 서버쪽에 재업로드된다는 단점이 있겠습니다.
<del>개선해야지..</del></p>
<pre><code class="language-java">    /**
     * @title S3에 적재된 파일과 맵핑 정보를 데이터베이스에 저장합니다.
     * @param saveImageData 맵핑된 이미지 정보.
     * @param attachmentType
     * @param mappedId
     * @return List&lt;Map&lt;String, Object&gt;&gt;
     */
    private List&lt;Map&lt;String, Object&gt;&gt; saveDB(List&lt;UploadImage&gt; saveImageData, AttachmentType attachmentType, long mappedId) {
        List&lt;UploadImage&gt; existingImages = uploadRepository.findByAttachmentTypeAndMappedId(attachmentType.getType(), mappedId);

        // 이미 해당 리뷰 또는 피드에 업로드된 이미지가 있는 경우, 모두 삭제
        if (!existingImages.isEmpty()) {
            uploadRepository.deleteAll(existingImages);
        }

        // 새로운 이미지들을 저장
        List&lt;UploadImage&gt; savedImages = uploadRepository.saveAll(saveImageData);

        // 저장된 이미지들의 정보를 결과 리스트에 추가
        List&lt;Map&lt;String, Object&gt;&gt; results = new ArrayList&lt;&gt;();

        for (UploadImage savedImage : savedImages) {
            Map&lt;String, Object&gt; image = new HashMap&lt;&gt;();
            image.put(&quot;fileName&quot;, savedImage.getFileName());
            image.put(&quot;imageUrl&quot;, savedImage.getImageUrl());
            results.add(image);
        }

        return results;
    }</code></pre>
<p>uuid를 활용해 유니크 파일명을 생성하는 함수</p>
<pre><code class="language-java">    /**
     * @title 파일이름이 겹치지 않기 위한 유니크한 파일 이름을 만들어주는 함수.
     * @param originalFileName 업로드하는 파일의 원본 이름
     * @return uuid
     */
    private String generateUniqueFileName(String originalFileName) {
        String extension = &quot;&quot;;
        int lastDotIndex = originalFileName.lastIndexOf(&quot;.&quot;);
        if (lastDotIndex &gt;= 0) {
            extension = originalFileName.substring(lastDotIndex);
        }

        // UUID를 사용하여 랜덤값을 생성하고, 확장자와 합쳐서 고유한 파일 이름을 생성
        String uniqueID = UUID.randomUUID().toString();
        return uniqueID + extension;
    }</code></pre>
<hr>
<h2 id="파일의-타입과-업로드-타입등을-enum으로-관리해보자">파일의 타입과 업로드 타입등을 Enum으로 관리해보자</h2>
<p>관리를 용이하게 하기위해 2가지의 Enum Class를 생성해서 관리했습니다.</p>
<p>AttachmentType 은 사용자가 업로드하는 이미지가 <strong>&quot;리뷰&quot; **인지</strong> &quot;피드&quot;** 인지 아니면 추후 추가될 어떤 것 인지에 따라 DB에 AttachmentType별로 다르게 적재합니다.</p>
<h4 id="attachmenttypejava">AttachmentType.java</h4>
<pre><code class="language-java">
public enum AttachmentType {
    REVIEW(&quot;review&quot;),
    FEED(&quot;feed&quot;);
    // 추후에 추가될 다른 업로드 타입들

    private final String type;

    AttachmentType(String type) {
        this.type = type;
    }

    public String getType() {
        return type;
    }

    public static AttachmentType fromString(String text) {
        for (AttachmentType uploadType : AttachmentType.values()) {
            if (uploadType.type.equalsIgnoreCase(text)) {
                return uploadType;
            }
        }
        throw new IllegalArgumentException(&quot;Invalid uploadType: &quot; + text);
    }
}
</code></pre>
<h4 id="filetypejava">FileType.java</h4>
<p>FileType.java 는 사용자가 업로드하는 파일객체의 확장자의 따라 용량과 타입을 반환합니다.
또한 파일 타입을 체크하는 요소들이 업로드,다운로드 간 여러곳에서 사용되고 있었습니다.</p>
<pre><code class="language-java">public enum FileType {
    ZIP(&quot;application/zip&quot;, 100 * 1024 * 1024), // 100MB
    PNG(&quot;image/png&quot;, 10 * 1024 * 1024), // 10MB
    JPEG(&quot;image/jpeg&quot;, 20 * 1024 * 1024), // 20MB
    PDF(&quot;application/pdf&quot;, 50 * 1024 * 1024); // 50MB

    private final String contentType;
    private final long maxSize;

    FileType(String contentType, long maxSize) {
        this.contentType = contentType;
        this.maxSize = maxSize;
    }

    public String getContentType() {
        return contentType;
    }

    public long getMaxSize() {
        return maxSize;
    }

    public static FileType fromContentType(String contentType) {
        for (FileType type : FileType.values()) {
            if (type.getContentType().equals(contentType)) {
                return type;
            }
        }
        return null; // 해당 contentType이 없으면 null 반환 or 예외 처리
    }
}
</code></pre>
<p>두가지를 분리한 이유는 지속적으로 추가,삭제,변경이 될 수 있는 부분인거같아서 관리를 용이하게 하기 위해 Enum을 사용했습니다.</p>
<p>참고 문서는 동욱님이 배민시절 올리신 우테크 블로그 글을 참고했습니다.
<a href="https://techblog.woowahan.com/2527/">https://techblog.woowahan.com/2527/</a></p>
<hr>
<h2 id="테스트-요청">테스트 요청</h2>
<p>테스트를 위해 두가지 컨트롤러를 미리 작성했습니다.
첫번째는 디비에 맵핑된 ID로 이미지를 요청
두번째는 업로드 타입과 타입의 ID로 요청(4번 리뷰의 데이터를 불러오겠음 등)</p>
<pre><code class="language-java">@GetMapping(&quot;/image/{id}&quot;)
public ResultDTO&lt;UploadImage&gt; getImageById(@PathVariable Long id) {
    try{
        Optional&lt;UploadImage&gt; image = uploadService.getImageById(id);
        if (!image.isPresent()) {
            return ResultDTO.of(ApiResponseCode.SUCCESS.getCode(), ApiResponseCode.SUCCESS.getMessage(), null);
        } else {
            return ResultDTO.of(ApiResponseCode.SUCCESS.getCode(), ApiResponseCode.SUCCESS.getMessage(), image.get());
        }
    } catch(NullPointerException error) {
        Optional&lt;UploadImage&gt; image = uploadService.getImageById(id);
        return ResultDTO.of(200,&quot;success&quot;,image.get());
    }
}

@GetMapping(&quot;/image/{attachment}/{mappedId}&quot;)
public ResultDTO&lt;List&lt;UploadImage&gt;&gt; getImageByAttachmentType(
        @PathVariable String attachment,
        @PathVariable long mappedId) {
    List&lt;UploadImage&gt; results = uploadService.getImageByAttachmentType(AttachmentType.fromString(attachment),mappedId);
    return ResultDTO.of(ApiResponseCode.SUCCESS.getCode(), ApiResponseCode.SUCCESS.getMessage(), results);
}</code></pre>
<h4 id="uploadservicejava-1">UploadService.java</h4>
<p>해당 요청의 대한 로직은 JPA로 간단하게 구현했습니다.</p>
<pre><code class="language-java">/**
 * @title AttachmentType에 맞는 이미지 데이터를 반환합니다.
 * @param attachmentType {&quot;reivew&quot;,&quot;feed&quot;}
 * @param mappedId {&quot;reviewId&quot;,&quot;feedId&quot;}
 * @return [data]
 */
public List&lt;UploadImage&gt; getImageByAttachmentType(AttachmentType attachmentType, long mappedId) {
    List&lt;UploadImage&gt; byAttachmentTypeAndMappedId = uploadRepository.findByAttachmentTypeAndMappedId(attachmentType.getType(), mappedId);
    return byAttachmentTypeAndMappedId;
}

/**
 * @title Image pk를 통한 이미지 데이터 반환
 * @param id : db pk
 * @return
 */
public Optional&lt;UploadImage&gt; getImageById(Long id) {
    return uploadRepository.findById(id);
}
</code></pre>
<hr>
<h3 id="결과">결과</h3>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/ccd6626b-0d05-4af3-9247-a9053059f138/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/8ecbf488-4746-4996-9430-00409c1056e7/image.png" alt=""></p>
<hr>
<p>리뷰나 피드관련 API가 개발되야지 사용을 해볼 수 있겠지만, 현재까지는 만족스러운것 같습니다.</p>
<p>하지만 개발하면서 보안이나 구조적으로 문제가 없을까 하는 고민은 계속 했던것 같습니다.
실제 실무에서 30GB 이상의 파일을 업로드도 가능했고, 실시간 업로드 상황도 알아야했기때문에  버킷과 서버간의 중간에서 유효성을 검증해주는 소켓 파일서버가 있었지만, 이번에 사용하는 업로드는 간단한 이미지 정도로 따로 중계 소켓파일서버를 개발하지는 않았습니다.</p>
<p>서비스 로직이 수정되거나 개발중에 아키텍쳐가 바뀌면 왜 바뀌었는지를 비롯해서 어떤 부분이 문제 였는지 또 남겨보면 좋은 경험이 될 것 같습니다.</p>
<p>일부 핵심 코드만 올렸지만 사용한 전체 코드를 확인하여 사용하실 분들은 아래 링크를 참고해주세요
<a href="https://github.com/explorer-cat/flambus-v1.0-springboot">https://github.com/explorer-cat/flambus-v1.0-springboot</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React-Native] UseMemo를 사용해 렌더링을 개선해보기]]></title>
            <link>https://velog.io/@explorer-cat/React-Native-UseMemoUseCallBack%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@explorer-cat/React-Native-UseMemoUseCallBack%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Sat, 29 Jul 2023 13:47:57 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/explorer-cat/post/377e09bf-1a38-4d58-a212-fe554d652a66/image.png" alt=""></p>
<h2 id="📒-usememousecallback-왜-써야하는가"><strong>📒 UseMemo,UseCallback 왜 써야하는가?</strong></h2>
<p> &quot;useMemo와 useCallback이 왜 필요한가?&quot;를 먼저 생각해보겠습니다.
 리액트 내장 훅인 이 두 함수가 우리에게 편리함을 제공해준다면, 분명히 필요성이 제시되었기 때문에 누군가 개발했다는 것을 의미를 합니다. 가장 핵심적인 키워드는 &#39;렌더링 최적화&#39;입니다. </p>
<h2 id="📒-메모이제이션memoization이란"><strong>📒 메모이제이션(Memoization)이란?</strong></h2>
<p>  메모이제이션은 일종의 <em>캐싱과 같습니다. 특정 연산을 반복해야 할 때 사용되는 기법으로, 반복되는 결과를 메모리에 저장하여 중복되는 연산 없이 빠른 실행을 가능하게 해 줍니다. 
(</em>캐시(cache) : 데이터나 값을 미리 복사해놓는 임시 장소를 의미합니다. 캐시는 접근 시간에 비해 원래 데이터에 접근하는 시간이 오래 걸리는 경우나 연산을 반복해야 하는 시간을 절약하고 싶을 때 사용됩니다. 데이터를 미리 복사해놓으면 계산이나 접근 시간 없이 더 빠른 속도로 데이터 접근이 가능합니다.) </p>
<p>다만 메모이제이션 기법은, 결국 메모리에 특정한 값을 저장하는 것이기 때문에, 정말 필요한 경우가 아닌데 남용하는 경우에는 오히려 성능을 저하시킬 수 있으니, 정말 필요한 경우에만 사용해야합니다.</p>
<p>그러므로 어떠한 학습적인 용도가 있는게 아닌이상, 눈에띄는 성능저하가 발생하는 경우에만 사용하도록 합시다.</p>
<p>사용해야할 경우를 간단하게 정리하자면, 성능저하가 눈에띌정도의 값비싼 함수의 결과값을 저장해야하는 경우, 여러곳에서 재사용이 필요한 경우, 종속성배열의 값이 자주 변경되지 않는경우.</p>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/558e948b-14cf-460e-a665-d3ae770ecbac/image.png" alt=""></p>
<h4 id="usememo">useMemo</h4>
<pre><code class="language-javascript">useMemo(() =&gt; { /* 로직 */ }, []);</code></pre>
<h4 id="usecallback">useCallback</h4>
<pre><code class="language-javascript">const [sum, setSum] = useState(0);

const addNum = useCallback(() =&gt; {
  setSum(num + 1);
}, [num]);</code></pre>
<hr>
<h3 id="정확히-이해하기-간단한-앱을-작성-해-보았다">정확히 이해하기 간단한 앱을 작성 해 보았다.</h3>
<p>아래 로직 같은 경우 API 요청 -&gt; 요청 후 특정 마켓에 있는 조건에 맞는 상장 코인만 필터해서 화면에 렌더해주는 함수이다.</p>
<pre><code class="language-javascript">//업비트 API를 요청하고 전체 상장된 코인중 KRW PAIR 상장된 코인을 VIEW로 뿌려준다.
    const [number, setNumber] = useState(0);
    const upbitListing = useSelector(state =&gt; state.upbitListingCoin);


    const getUpbitAllPairs = () =&gt; {
        let result = []
        console.log(&quot;전체 상장된 원화 코인을 요청했습니다.&quot;)
        for (const key in upbitListing.KRW_MARKET) {
            result.push(&lt;View key = {upbitListing.KRW_MARKET[key].korean_name.toString()}&gt;&lt;Text&gt;{upbitListing.KRW_MARKET[key].korean_name}&lt;/Text&gt;&lt;/View&gt;)
        }
        return result;
    }



    return (
        &lt;Container&gt;
            &lt;TouchableOpacity
                onPress={() =&gt; {
                    setNumber(number+1);
                }}
                style = {{height:50,backgroundColor:&#39;red&#39;,alignItems:&#39;center&#39;,justifyContent:&#39;center&#39;}}&gt;
                &lt;Text&gt;
                    number = {number}
                &lt;/Text&gt;&lt;
                /TouchableOpacity&gt;

            &lt;ScrollView&gt;
                &lt;View&gt;
                    {getUpbitAllPairs()}
                &lt;/View&gt;
            &lt;/ScrollView&gt;
        &lt;/Container&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/da72994a-bceb-471f-a1b3-1b5117c2a55e/image.png" alt=""></p>
<p>같은 컴포넌트에서 number State가 변경될때마다 컴포넌트가 리렌더링 되면서 getUpbitAllPairs 함수가 항상 호출되고 콘솔 로그가 찍히는것을 확인 할 수 있었다. 같은 컴포넌트에 있는 State가 바뀔때마다 무조건적으로 해당 함수가 호출되어 코인 리스트를 불러오는것은 비효율적이라는 생각이 들었다.</p>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/900ebf51-b3b0-4b40-80c6-945d6e71ee0a/image.png" alt=""></p>
<p>위와 같이 컴포넌트 내에서 어떤 State가 변하든 또는 부모 컴포넌트에서 Props가 새로 내려오던, 무조건적으로 함수가 요청된다. 만약 해당 함수가 단순 API 요청 함수가 아니고 API요청 후 특정 무거운 비즈니스 로직까지 거친다면, 비효율적일것임.</p>
<blockquote>
<p>로직 코드 개선 후 </p>
</blockquote>
<pre><code class="language-javascript">    const [number, setNumber] = useState(0);
    const upbitListing = useSelector(state =&gt; state.upbitListingCoin);
    const [filterData, setFilterData] = useState([]);

    const getUpbitAllPairsUseMemo = useMemo(() =&gt; {
        let result = []
        for (const key in upbitListing.KRW_MARKET) {
                result.push(&lt;View key={upbitListing.KRW_MARKET[key].korean_name.toString()}&gt;&lt;Text&gt;{upbitListing.KRW_MARKET[key].korean_name}&lt;/Text&gt;&lt;/View&gt;)
        }
        return result;
    }, [filterData])



    return (
        &lt;Container&gt;
            &lt;TouchableOpacity
                onPress={() =&gt; {
                    setNumber(number+1);
                }}
                style = {{height:50,backgroundColor:&#39;red&#39;,alignItems:&#39;center&#39;,justifyContent:&#39;center&#39;}}&gt;
                &lt;Text&gt;
                    number = {number}
                &lt;/Text&gt;&lt;
                /TouchableOpacity&gt;

            &lt;ScrollView&gt;
                &lt;View&gt;
                    {getUpbitAllPairsUseMemo}
                &lt;/View&gt;
            &lt;/ScrollView&gt;
        &lt;/Container&gt;
    )</code></pre>
<p>number State가 변해도 getUpbitAllPairsUseMemo()는 useMemo에 캐싱되어있기 때문에 새로 렌더링 하지않고 캐시 메모리에서 꺼내서 사용하게 된다.</p>
<p>-</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React Native] reanimated,gesture-handler 를 활용해 애니메이션과 UX경험 올리기]]></title>
            <link>https://velog.io/@explorer-cat/react-native-reanimated</link>
            <guid>https://velog.io/@explorer-cat/react-native-reanimated</guid>
            <pubDate>Wed, 26 Jul 2023 13:40:49 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/explorer-cat/post/dafafc66-f124-4a80-96e1-019ffff08b48/image.png" alt=""></p>
<h3 id="💡-reanimated를-왜-사용하게-되었는가">💡 Reanimated를 왜 사용하게 되었는가?</h3>
<p>회사 업무 중 실제 구현이 필요했던 요구사항은 아래 네이버캘린더의 기능처럼 하단 View영역을 제스쳐로 드래그해 올려, 내용을 크게 볼 수 있는 Drawer View 형태 였다.
<img src="https://velog.velcdn.com/images/explorer-cat/post/93d2551b-3acd-4c98-ab3a-75c79e1a48f6/image.gif" alt=""></p>
<p>처음에는 간단한 라이브러리로 구현 할 수 있을꺼라고 쉽게 생각하여, 오픈 라이브러리를 사용해보았다.</p>
<p>아래는 사용해본 라이브러리 순서대로 느낀점을 적어보았다.</p>
<h2 id="--react-native-draggable-drawer">- react-native-draggable-drawer</h2>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/5d1873aa-222e-4ae5-9e13-60210ce8b936/image.gif" alt=""></p>
<p>좋았다. 원하는 UX를 구현 할 수 있었고, 사용방법도 쉬웠다. 
하지만 마지막 업데이트와 배포일이 8년전인 것과 안드로이드에서의 화면때문에 조금 선택하기 망설여졌다. 👊</p>
<h2 id="각종-drawer-네이게이션-라이브러리들을-사용해보았다">각종 Drawer 네이게이션 라이브러리들을 사용해보았다.</h2>
<p>... 하지만 대부분 Drawer는 View가 보이지 않는 영역에 있다가 특정 버튼이나 제스쳐를 통해 보이는 기능만 지원을 했고, 우리 앱의 요구사항에는 맞지 않아 보였다.</p>
<p>💦 오픈 소스 라이브러리 10개 정도 모두 설치해서 프로토타입을 만들어 보았지만, 뭔가 3% 부족해 보였고 네이버 캘린더처럼 똑같이 만들고 싶다는 욕심도 있었기 때문에 선택이 그 3% 포기하고 라이브러리를 선택하는건 쉽지 않았다.</p>
<h4 id="💡-많은-서칭을-하던중-모든-drawer-라이브러리들이-아래와-같은-의존성-패키지를-설치해야지만-사용이-가능하다는-것을-알게-되었다">💡 많은 서칭을 하던중 모든 Drawer 라이브러리들이 아래와 같은 의존성 패키지를 설치해야지만 사용이 가능하다는 것을 알게 되었다!!</h4>
<blockquote>
<p>react-native-reanimated &amp;&amp; react-native-gesture-handler</p>
</blockquote>
<h3 id="reanimated란">Reanimated란?</h3>
<p>Reanimated는 부드러운 애니메이션을 구현하기 위해서는 초당 60프레임의 화면전환이 필요하다. 그리고 이를 위해서는 16밀리초 내에 프레임이 렌더링되어야 한다고 한다. 리액트 네이티브에서 기본적으로 제공되는 gesture와 Animated API를 사용하면 애니메이션의 계산을 UI Thread와 JS Thread의 커뮤니케이션에 의존해야 합니다. 그리고 두 쓰레드 간의 통신은 비동기로 이루어지기 때문에 Response가 16 밀리초 이내에 오는 것을 보장할 수 없다. 특히 모바일의 성능이 떨어지는 경우에는 시간이 더 지연될 수도 있다.</p>
<h3 id="아래-사진-참조-thread-간의-통신은-bridge를-통해-비동기적으로-메시지를-주고-받습니다">(아래 사진 참조) Thread 간의 통신은 Bridge를 통해 비동기적으로 메시지를 주고 받습니다</h3>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/1dfcefb4-3265-4f69-a3c5-e4e441159dbf/image.webp" alt=""></p>
<blockquote>
<p>그렇게 직접 Reanimated를 사용해 만들어보기로 결정했다. 직접 만들면 유지보수나, 추후 확장성 측면에서도 유리 할 것이라고 생각했고, 분명 추가적인 고도화를 거치면서 추가적인 요구사항이 있을 것이라고 생각했다. (가보자고~)</p>
</blockquote>
<h3 id="1-애니매이션-값-만들기">1. 애니매이션 값 만들기</h3>
<p>컴포넌트의 움직임은 translate 값에 의존하게 됩니다. useSharedValue 를 통해 선언해줍니다. 이 값은 이벤트 함수 내부에서 이벤트의 값에 따라 계속 업데이트 되며 style 값에 바인딩 되어 업데이트된 값이 실제 style props에 반영될 수 있도록 합니다.</p>
<pre><code class="language-javascript">const animation = useSharedValue(200); // Default height when closed</code></pre>
<h3 id="2--이벤트-함수-만들기">2.  이벤트 함수 만들기</h3>
<p>이벤트가 일어나면 수행될 이벤트 함수를 만들어줍니다. reanimated에서 import한useAnimatedGestureHandler 를 사용합니다. 제네릭으로 필요한 타입을 주입해줄 수 있습니다. onStart는 제스처가 시작될 때, onActive 는 제스처가 진행되는 동안, onEnd 는 제스처가 종료되었을 때 호출됩니다. 각 프로퍼티에 할당된 콜백은 모두 event 객체와 context 객체를 받아올 수 있습니다.</p>
<p>ps. 예민하게 반응해서 좌표 계산과 세팅하는데 어려움이 좀 있었습니다. 
<del>솔직히 아직 정확히 이해하고 사용한다고 보기는 힘든 상황인거 같음. 느낌정도만..</del></p>
<pre><code class="language-javascript">    const onGestureEvent = useAnimatedGestureHandler({
        onStart: (_, ctx) =&gt; {
            ctx.startHeight = animation.value;
        },
        onActive: (event, ctx) =&gt; {
            let newHeight = ctx.startHeight - event.translationY;
            if (newHeight &gt; maxHeight) {
                newHeight = maxHeight;
            }
            if (newHeight &lt; minHeight) {
                newHeight = minHeight;
            }
            animation.value = newHeight;
        },
        onEnd: (event) =&gt; {
            if (event.translationY &lt; gestureHeight || animation.value &gt; screenHeight - openGestureY) {
                animation.value = withTiming(maxHeight, { duration: 300 });
                runOnJS(setOpen)(true);
            } else {
                runOnJS(setOpen)(false);
                if (animation.value !== maxHeight &amp;&amp; animation.value !== openGestureY) {
                    animation.value = withTiming(minHeight, { duration: 300 });
                }
            }
        },
    });</code></pre>
<h3 id="3--애니메이션-스타일">3.  애니메이션 스타일</h3>
<p>reanimated 에서는 useAnimatedStyle 이라는 훅을 통해 애니메이션 스타일을 관리합니다. 이 훅을 통해 sharedValue 와 View properties 간의 관계가 만들어집니다. 훅을 통해 만들어진 animStyle 은 매번 sharedValue 값이 업데이트 될 때마다 업데이트됩니다. 만약 특정 조건에서 업데이트 되도록 하고 싶다면 useEffect 와 유사하게 두번째 인자로 [dependencies] 를 추가해줄 수도 있습니다. 이 훅을 통해 만들어진 값은 반드시 Reanimated 에서 import 한 Animated 컴포넌트의 style 프로퍼티에 할당해주어야 합니다.</p>
<pre><code class="language-javascript">   const boxStyle = useAnimatedStyle(() =&gt; {
        return {
            height: animation.value,
        };
    });

</code></pre>
<h3 id="4-애니메이션-값-할당">4. 애니메이션 값 할당</h3>
<pre><code class="language-javascript">    return (
        &lt;&gt;
            &lt;View style={{ flex: 1}}&gt;
                &lt;Calendar
                    onDayPress={day =&gt; {
                        setSelected(day.dateString);
                    }}
                    markedDates={{
                        [selected]: {selected: true, disableTouchEvent: true, selectedDotColor: &#39;orange&#39;}
                    }}
                /&gt;
            &lt;/View&gt;
            &lt;View style={{ position: &#39;absolute&#39;, zIndex: 99, bottom: 0, backgroundColor: &#39;white&#39; ,borderTopWidth:1,borderColor:&#39;#C9C9C9&#39;}}&gt;
                &lt;PanGestureHandler onGestureEvent={onGestureEvent}&gt;
                    &lt;Animated.View style={[boxStyle, styles.mySummaryModal, { width: screenWidth}]}&gt;
                        &lt;View style = {{padding:12,alignItems:&#39;center&#39;}}&gt;
                            &lt;View style = {{width:50,height:5,backgroundColor:&#39;#C9C9C9&#39;,borderRadius:8}}&gt;

                            &lt;/View&gt;
                        &lt;/View&gt;
                        &lt;View style={{ padding: 20 }}&gt;
                            &lt;Text&gt;&lt;/Text&gt;
                        &lt;/View&gt;
                    &lt;/Animated.View&gt;
                &lt;/PanGestureHandler&gt;
            &lt;/View&gt;
        &lt;/&gt;
    );
};</code></pre>
<h1 id="👏-완성된-화면과-전체-소스코드">👏 완성된 화면과 전체 소스코드!</h1>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/d14fcbee-c821-4631-8369-61e311ccf99a/image.gif" alt=""></p>
<p>아래는 완성된 코드 입니다.
일부 Gesture 좌표 같은 부분은 다듬을 필요가 있겠지만 요구사항대로 잘 만들어진거 같네요 🔥</p>
<pre><code class="language-javascript">
const MyCalendar = () =&gt; {
    const [selected, setSelected] = useState(&#39;&#39;);

    const screenHeight = Dimensions.get(&#39;screen&#39;).height;
    const screenWidth = Dimensions.get(&#39;screen&#39;).width;
    const [open, setOpen] = useState(false);
    //플러스 버튼 클릭시 열리는 백그라운드 뷰
    const animation = useSharedValue(200); // Default height when closed
    const openGestureY = 200;
    const minHeight = 400; // Set the maximum height the view can expand to
    const maxHeight = 700; // Set the maximum height the view can expand to
    const gestureHeight = -100; //얼만큼 손을 올려야지 올라갈건지


    useEffect(() =&gt; {
        if (open) {
            animation.value = withSpring(maxHeight, { damping: 100 });
        } else {
            animation.value = withSpring(minHeight, { damping: 100});
        }
    }, [open]);

    const onGestureEvent = useAnimatedGestureHandler({
        onStart: (_, ctx) =&gt; {
            ctx.startHeight = animation.value;
        },
        onActive: (event, ctx) =&gt; {
            let newHeight = ctx.startHeight - event.translationY;
            if (newHeight &gt; maxHeight) {
                newHeight = maxHeight;
            }
            if (newHeight &lt; minHeight) {
                newHeight = minHeight;
            }
            animation.value = newHeight;
        },
        onEnd: (event) =&gt; {
            if (event.translationY &lt; gestureHeight || animation.value &gt; screenHeight - openGestureY) {
                animation.value = withTiming(maxHeight, { duration: 300 });
                runOnJS(setOpen)(true);
            } else {
                runOnJS(setOpen)(false);
                if (animation.value !== maxHeight &amp;&amp; animation.value !== openGestureY) {
                    animation.value = withTiming(minHeight, { duration: 300 });
                }
            }
        },
    });

    const boxStyle = useAnimatedStyle(() =&gt; {
        return {
            height: animation.value,
        };
    });



    return (
        &lt;&gt;
            &lt;View style={{ flex: 1}}&gt;
                &lt;Calendar
                    onDayPress={day =&gt; {
                        setSelected(day.dateString);
                    }}
                    markedDates={{
                        [selected]: {selected: true, disableTouchEvent: true, selectedDotColor: &#39;orange&#39;}
                    }}
                /&gt;
            &lt;/View&gt;
            &lt;View style={{ position: &#39;absolute&#39;, zIndex: 99, bottom: 0, backgroundColor: &#39;white&#39; ,borderTopWidth:1,borderColor:&#39;#C9C9C9&#39;}}&gt;
                &lt;PanGestureHandler onGestureEvent={onGestureEvent}&gt;
                    &lt;Animated.View style={[boxStyle, styles.mySummaryModal, { width: screenWidth}]}&gt;
                        &lt;View style = {{padding:12,alignItems:&#39;center&#39;}}&gt;
                            &lt;View style = {{width:50,height:5,backgroundColor:&#39;#C9C9C9&#39;,borderRadius:8}}&gt;

                            &lt;/View&gt;
                        &lt;/View&gt;
                        &lt;View style={{ padding: 20 }}&gt;
                            &lt;Text&gt;&lt;/Text&gt;
                        &lt;/View&gt;
                    &lt;/Animated.View&gt;
                &lt;/PanGestureHandler&gt;
            &lt;/View&gt;
        &lt;/&gt;
    );
};

</code></pre>
<p>참고 사이트</p>
<blockquote>
<p><a href="https://github.com/software-mansion/react-native-reanimated">https://github.com/software-mansion/react-native-reanimated</a>
<a href="https://docs.swmansion.com/react-native-reanimated/">https://docs.swmansion.com/react-native-reanimated/</a>
<a href="https://medium.com/crossplatformkorea/%EC%9B%90%EB%A6%AC%EC%99%80-%EC%98%88%EC%A0%9C%EB%A5%BC-%ED%86%B5%ED%95%B4-react-native-reanimated-v2-%EC%9E%85%EB%AC%B8%ED%95%98%EA%B8%B0-336e832f6ed6">https://medium.com/crossplatformkorea/%EC%9B%90%EB%A6%AC%EC%99%80-%EC%98%88%EC%A0%9C%EB%A5%BC-%ED%86%B5%ED%95%B4-react-native-reanimated-v2-%EC%9E%85%EB%AC%B8%ED%95%98%EA%B8%B0-336e832f6ed6</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS EC2] SpringBoot 프로젝트 환경 세팅 및 배포 & 오류 해결방법 ]]></title>
            <link>https://velog.io/@explorer-cat/AWS-EC2-SpringBootMariaDBJPA-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%B0%ED%8F%AC%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@explorer-cat/AWS-EC2-SpringBootMariaDBJPA-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%B0%ED%8F%AC%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Tue, 07 Mar 2023 15:05:17 GMT</pubDate>
            <description><![CDATA[<h3 id="🗣-배포-환경">🗣 배포 환경</h3>
<p>AWS Amazon Linux2 t2-micro
MariaDB 10.x
Java jdk 11
Spring Boot 2.7.6 Gradle</p>
<hr>
<h3 id="📓-git-install--repository-clone">📓 Git Install &amp; Repository Clone</h3>
<pre><code class="language-bash">#깃 설치
yum install git </code></pre>
<pre><code class="language-bash">#깃 버전 확인
git --version</code></pre>
<hr>
<h3 id="📀-mariadb-설치-및-세팅">📀 MariaDB 설치 및 세팅</h3>
<pre><code class="language-bash">yum install mariadb-server #MariaDB 설치</code></pre>
<pre><code class="language-bash">systemctl start mariadb #시작
systemctl status mariadb #상태 확인</code></pre>
<p> 설치된 데이터베이스 비밀번호 세팅</p>
<pre><code class="language-bash">mysql -u root -p</code></pre>
<p>설치 후 첫 접속시 비밀번호는 없는 상태라 enter 입력시 접근이 된다.</p>
<pre><code class="language-bash">MariaDB [mysql]&gt; update user set password=password(&#39;새로운 비밀번호&#39;) where user=&#39;root&#39;;
MariaDB [mysql]&gt; flush privileges;
MariaDB [mysql]&gt; select host, user, password from user;</code></pre>
<p>비밀번호 세팅이 완료되었다.</p>
<hr>
<h3 id="🔥-jdk-설치와-환경변수-해당-절차가-중요함">🔥 JDK 설치와 환경변수 (해당 절차가 중요함!!)</h3>
<p>1.자바 설치
is this ok가 나오면 y를 입력해서 설치 진행</p>
<pre><code class="language-bash">yum install java-1.8.0-openjdk
java -version #java 버전 확인</code></pre>
<p>2.javac 설치</p>
<pre><code class="language-bash">yum install java-1.8.0-openjdk-devel.x86_64
javac -version #버전 확인</code></pre>
<p>3.환경변수 설정
which를 통해 java의 경로를 가져온다.</p>
<pre><code class="language-bash">which java
ouput =&gt; /usr/bin/java</code></pre>
<pre><code>readlink -f /usr/bin/java
output =&gt; /usr/lib/jvm/java-1.8.0-openjdk</code></pre><p>4./etc/profile 파일 편집</p>
<pre><code class="language-bash">vi /etc/profile</code></pre>
<p>5.환경변수 입력
shift + g를 입력하여 마지막 라인으로 이동해서 아래 환경변수 입력하고 저장</p>
<pre><code class="language-txt">export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk
export PATH=$PATH:$JAVA_HOME/bin
export CLASSPATH=$JAVA_HOME/jre/lib:$JAVA_HOME/lib/tools.jar</code></pre>
<ol start="6">
<li>환경변수 확인<pre><code class="language-bash">echo $JAVA_HOME
output =&gt; /usr/lib/jvm/java-1.8.0-openjdk</code></pre>
<h3 id="👊-프로젝트-세팅하고-빌드하기">👊 프로젝트 세팅하고 빌드하기</h3>
<blockquote>
<p>프로젝트 빌드를 위해서는 서버 메모리가 확보가 되어있어야 하는것 같다. 0.5GB 서버로 빌드했는데.. 계속 빌드 FAILED 되서 로그 까보니 빌드중 메모리부족으로 인한 실패였다...</p>
</blockquote>
</li>
</ol>
<p>1.application.properties를 세팅된 데이터베이스 정보로 만들어준다.</p>
<p>2.jar 파일 빌드</p>
<pre><code class="language-bash">./gradlew build</code></pre>
<p>3.libs 폴더에 있는 jar 파일 실행</p>
<pre><code class="language-bash">nohup java -jar [파일명].jar 2&gt;&amp;1 &amp;</code></pre>
<p>java -jar은 .jar를 실행시키는 명령어 입니다. 하지만 이렇게 .jar를 수행시키면 터미널 접속이 끊길 때 애플리케이션도 함께 종료됩니다.
nohup(no hang up) 명령어를 사용하면 세션 연결이 끊겨도 애플리케이션을 계속 수행시킵니다.
2&gt;&amp;1은 표준 에러와 표준 출력을 한 곳에서 로그를 남긴다는 의미입니다.
nohup 사용시 기본적으로 nohup.out에 로그가 쌓이게 됩니다. 하지만 표준 출력과 표준 에러를 각기 다른 파일에 남기고 싶다면 1&gt;[파일명] 2&gt;[파일명]으로 두 출력을 구분하여 남길 수 있습니다. 이때 1은 표준 출력, 2는 표준 에러를 의미합니다.
즉 2&gt;&amp;1은 표준 에러를 표준 출력과 같은 곳에 남긴다는 의미가 됩니다.</p>
<ul>
<li>실행중인 nohup 프로세스 종료 방법<pre><code class="language-bash">ps -ef #실행중인 프로세스 확인
kill -9 [pid] #프로세스 종료</code></pre>
</li>
</ul>
<p>POSTMAN을 날려보자</p>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/d11f0ab5-f39e-4b31-b20d-959dd0cdc406/image.png" alt=""></p>
<hr>
<h3 id="shell-파일로-간편하게-배포하기">SHELL 파일로 간편하게 배포하기</h3>
<p>작성중.</p>
<h2 id="참고-레퍼런스">참고 레퍼런스</h2>
<p>mariaDB 세팅:  <a href="https://ilimes.github.io/mariadb/post28">https://ilimes.github.io/mariadb/post28</a>
ec2서버에 마리아디비 설치
<a href="https://realsalmon.tistory.com/4">https://realsalmon.tistory.com/4</a>
aws ec2 깃설치하기
<a href="https://chucoding.tistory.com/23">https://chucoding.tistory.com/23</a>
[SpringBoot] 게시판 (5) - AWS EC2에 배포하기 (feat. AWS RDS)🐵
<a href="https://victorydntmd.tistory.com/338">https://victorydntmd.tistory.com/338</a>
mariadb 실행 및 비밀번호 설정
<a href="https://realsalmon.tistory.com/4">https://realsalmon.tistory.com/4</a>
gradle spring boot 배포하기
<a href="https://velog.io/@shawnhansh/AWS-EC2%EC%97%90-SpringBoot-gradle-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8jar-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0">https://velog.io/@shawnhansh/AWS-EC2%EC%97%90-SpringBoot-gradle-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8jar-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MariaDB JOIN 쿼리 /JPA JOIN Mapping 해보기]]></title>
            <link>https://velog.io/@explorer-cat/%EA%B4%80%EA%B3%84%ED%98%95-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-JOIN</link>
            <guid>https://velog.io/@explorer-cat/%EA%B4%80%EA%B3%84%ED%98%95-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-JOIN</guid>
            <pubDate>Sat, 25 Feb 2023 07:00:11 GMT</pubDate>
            <description><![CDATA[<h3 id="단순-테이블간-join-하기">단순 테이블간 JOIN 하기</h3>
<p>메인 카테고리와 서브카테고리를 조인 한다 조건은 main_category_id와 sub_category_id 같은것 끼리</p>
<pre><code>SELECT * FROM main_category LEFT JOIN sub_category ON main_category.main_category_id = sub_category.main_category_id;</code></pre><h3 id="테이블-join-시-원하는-컬럼만-보기">테이블 JOIN 시 원하는 컬럼만 보기</h3>
<p>메인 카테고리와 서브카테고리를 조인 한다 조건은 main_category_id와 sub_category_id 같은것 끼리</p>
<pre><code>SELECT main_category.main_category_id,main_category.name,sub_category.sub_category_id,sub_category.name FROM main_category LEFT JOIN sub_category ON main_category.main_category_id = sub_category.main_category_id</code></pre><h3 id="테이블-join-후-컬럼이름을-바꿔서-출력">테이블 JOIN 후 컬럼이름을 바꿔서 출력</h3>
<p>컬럼 이름 오른쪽에서 AS {컬럼이름} 을 통해 변경해서 출력한다.</p>
<pre><code>SELECT main_category.main_category_id,main_category.name AS mainCategoryName ,sub_category.sub_category_id,sub_category.name As subCategoryName FROM main_category LEFT JOIN sub_category ON main_category.main_category_id = sub_category.main_category_id</code></pre><h3 id="jpa에서-테이블끼리-관계-지정">JPA에서 테이블끼리 관계 지정</h3>
<p>MainCategory 와 SubCategory는 OneToMany 1:N 관계의 테이블이다.</p>
<pre><code class="language-java">    @ManyToOne
    @JoinColumn(name = &quot;main_category_ID&quot;)
    private MainCategory mainCategory;</code></pre>
<p>SubCategoryTable의 main_category_ID는 MainCategoryTable의 ID(pk)와 관계되어있으므로
위와 같이 어노테이션을 지정해준다.</p>
<pre><code class="language-java">    @Override
    public List&lt;SubCategoryResponseDTO&gt; getAllSubCategory(long mainCategoryId) {
        List&lt;SubCategoryResponseDTO&gt; result = new ArrayList&lt;&gt;();

        List&lt;SubCategory&gt; categories = subCategoryRepository.findByMainCategoryId(mainCategoryId);

        for (SubCategory v : categories) {
            SubCategoryResponseDTO dto = new SubCategoryResponseDTO();
            result.add(dto.fromEntity(v));
        }
        if (result.isEmpty()) {
            throw new IllegalStateException(&quot;Empty&quot;);
        } else {
            return result;
        }
    }</code></pre>
<p>path 로 받은 mainCategoryId를 찾아서 반환을 하면</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 2309 문제 풀이]]></title>
            <link>https://velog.io/@explorer-cat/%EB%B0%B1%EC%A4%80-2309-%EB%AC%B8%EC%A0%9C-%ED%92%80%EC%9D%B4</link>
            <guid>https://velog.io/@explorer-cat/%EB%B0%B1%EC%A4%80-2309-%EB%AC%B8%EC%A0%9C-%ED%92%80%EC%9D%B4</guid>
            <pubDate>Fri, 17 Feb 2023 11:24:45 GMT</pubDate>
            <description><![CDATA[<p>제출 했던 정답</p>
<pre><code class="language-python">input = [int(input()) for _ in range(9)]

result = 0;

for a in input:
    result = result + a;

flag = False

for b in range(0,9):
        for c in range(0,9):
            if c != b:
                if result - (input[b] + input[c]) == 100:
                    a1 = input[b]
                    a2 = input[c]
                    input.remove(a1)
                    input.remove(a2)
                    flag = True;
                    break;
        if flag == True:
            break;

input = sorted(input);
for d in input:
    print(d)</code></pre>
<p>개선된 코드 </p>
<pre><code class="language-python">import itertools
input = [int(input()) for _ in range(9)]


for i in itertools.combinations(input,7):
    if(sum(i) == 100):
        for num in sorted(i):
            print(num)
        break;
</code></pre>
<p>itertools.combinations 는 순열을 구할 수 있음.</p>
<p>순열이란 서로다른 N개에서 r개를 선택할때 순서를 고려하여 중복없이 뽑을 수 있음.
리스트 [1,3,5,6]</p>
<p>[(1,3)(1,5)(1,6),(3,5),(3,6)...]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] DTO를 꼭 사용해야하는가]]></title>
            <link>https://velog.io/@explorer-cat/Spring-DTO%EB%A5%BC-%EA%BC%AD-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC%ED%95%98%EB%8A%94%EA%B0%80</link>
            <guid>https://velog.io/@explorer-cat/Spring-DTO%EB%A5%BC-%EA%BC%AD-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC%ED%95%98%EB%8A%94%EA%B0%80</guid>
            <pubDate>Mon, 06 Feb 2023 17:46:50 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/explorer-cat/post/4cd6aac7-68b9-4fe1-a7bc-4829337b5289/image.png" alt=""></p>
<h3 id="dto-란-무엇인가">DTO 란 무엇인가?</h3>
<p>DTO란 Data Transfer Object의 약자로, 계층 간 데이터 전송을 위해 도메인 모델 대신 사용되는 객체이다. 이때, 계층이란 Presentation(View, Controller), Business(Service), Persistence(DAO, Repository) 등을 의미한다.</p>
<h3 id="dto-왜-써야되는건데">DTO 왜 써야되는건데?</h3>
<p>아래는 현재 스프링에 입문 2주차 토이프로젝트 코드이다.</p>
<p><em>Entity</em></p>
<pre><code class="language-java">@Data
@Entity
public class BoardVo {

    @Id
    @GeneratedValue
    private int id;
    private int categoryId;
    private String title;
    private String content;
    private boolean approval;
    private String creator;
    private LocalDateTime regDt;
}</code></pre>
<p><em>Service</em></p>
<pre><code class="language-java">@Override
public ResponseEntity&lt;List&lt;BoardVo&gt;&gt; getCategoryPostList(int categoryId) {
        try {
            return new ResponseEntity&lt;&gt;(boardRepository.findByCategoryIdAndApproval(categoryId,true), HttpStatus.OK);
        } catch (RuntimeException e) {
            return ResponseEntity.internalServerError().build();
        } catch (Exception e) {
            return ResponseEntity.internalServerError().build();
        }
    }
}
</code></pre>
<p>현재 작성된 Board에서 작성된 카테고리와 게시글정보를 불러오는 메서드이다. </p>
<p>이렇게 사용하면서 사실 클라이언트쪽에서 기능동작에는 아무 문제가 없었기 때문에 &#39;DTO를 왜쓰는거지 그냥 이대로 리턴해주면 편하고 좋은데?&#39; 라는 생각을 가지고 있었다.</p>
<p>그래서 DTO에 대해서 좀 공부를 하기 시작했다.</p>
<p>&#39;맞다&#39; 사실 도메인 객체를 지금처럼 View로 그대로 전달해도 된다. 하지만 메인 객체를 View에 직접 전달할 경우 도메인의 비즈니스 기능이 노출될 수 있으며 Model과 View 사이에 의존성이 생긴다고한다.</p>
<h3 id="그놈의-의존성-작은-프로젝트에서-감이-잘안온다">그놈의 의존성.. 작은 프로젝트에서 감이 잘안온다..</h3>
<p><u>뷰와의 의존성 문제</u>
예를 들어 프론트엔드에서 백엔드쪽으로 받고자하는 데이터는 자주 바뀐다. 이때 Entity 정보를 모두 넘기고있었다면 상당히 귀찮은 요소가 될 수 있다.</p>
<p><u>비즈니스 기능의 노출</u>
메인 객체를 View에 직접 전달할 경우 도메인의 비즈니스 기능이 노출될 수 있는 부분에서 깊은 영감을 깨달았다.</p>
<p>만약 위에서 내코드가 Board 객체가 아닌 User 객체 였다면? Entity에 있는 사용자 모든정보가 반환된다.(password 까지) 모든 정보를 반환을 원하지않다면 비즈니스 로직에 해당 정보들을 제거한 하는 로직을 거친 후 반환해야하는데 DTO 만들어놓으면 그냥 맵핑시켜서 넘기면된다. </p>
<h4 id="도메인-model을-캡슐화하고-ui-화면에서-사용하는-데이터만-선택적으로-보낼-수-있다">도메인 Model을 캡슐화하고, UI 화면에서 사용하는 데이터만 선택적으로 보낼 수 있다.</h4>
<p>정답이 있는건 아니지만 상황에 따라 DTO를 사용해 변환 해서 쓰고 필요한 방법을 선택해서 사용하는게 맞는거같다.</p>
<p>Resonpnse의 대한 DTO 뿐만 아닌 Request요청의 대한 DTO도 만들어서 쓰시는분들이 있는거같다.</p>
<h4 id="실습-그럼-dto-를-만들어보자">[실습] 그럼 DTO 를 만들어보자</h4>
<p><em>Controller</em></p>
<pre><code class="language-java">    @GetMapping(&quot;/api/v1/board&quot;)
    //@RequestParam(value = &quot;category&quot;,required = false, defaultValue = &quot;0&quot;) category가 없어도 들어오게
    public ResponseEntity&lt;List&lt;BoardResponseDto&gt;&gt; getCategoryPostInfo(
            @RequestParam(value = &quot;category&quot;, required = false , defaultValue = &quot;0&quot;) int categoryId,
            @RequestParam(value = &quot;postId&quot;, required = false, defaultValue = &quot;0&quot;) int postId) {
        /**
         * case 1 : 카테고리와 게시글번호가 둘다 없는 경우 요청자체가 잘못됨.
         * case 2 : 카테고리 아이디만 요청했을 경우 -&gt; 해당 카테고리 모든 게시글 정보 반환
         * case 3 : 카테고리 아이디와 게시글 아이디를 요청했을 경우 -&gt; 해당 카테고리에 있는 게시글 정보만 반환
         */

        if (categoryId == 0) { //카테고리와 게시글번호가 둘다 없는 경우
            System.out.println(&quot;category prams required&quot;);
        } else if(categoryId != 0 &amp;&amp; postId == 0) { //카테고리 아이디만 왔을 경우 해당 카테고리 모든 게시글 정보 반환
            return boardService.getCategoryPostList(categoryId); //getByAll -&gt; List&lt;Board&gt;
        } else if(categoryId != 0 &amp;&amp; postId != 0) {
            return boardService.getPost(postId); //getById -&gt; Optinal&lt;Board&gt;
        }
        return null;
    }</code></pre>
<p><em>Serivce</em></p>
<pre><code class="language-java">    @Override
    public ResponseEntity&lt;List&lt;BoardResponseDto&gt;&gt; getPost(int id) {
        List&lt;BoardResponseDto&gt; boardList = new ArrayList&lt;BoardResponseDto&gt;();
        BoardVo board = boardRepository.findById(id).get();
        boardList.add(new BoardResponseDto(board));
        return new ResponseEntity&lt;&gt;(boardList, HttpStatus.OK);
    }</code></pre>
<pre><code class="language-java">    @Override
    public ResponseEntity&lt;List&lt;BoardResponseDto&gt;&gt; getCategoryPostList(int categoryId) {
        try {
            return new ResponseEntity&lt;&gt;(boardRepository.findByCategoryIdAndApproval(categoryId,true), HttpStatus.OK);
        } catch (RuntimeException e) {
            return ResponseEntity.internalServerError().build();
        } catch (Exception e) {
            return ResponseEntity.internalServerError().build();
        }
    }</code></pre>
<p>현재 View에서는 단건 객체라도 리스트 안에 넣어서 넘겨주는 구조이기 때문에 find해온 borad entity객체를 boardList 라는 빈 List 안에 넣어준다. 이때 DTO
생성자를 호출해서 DTO로 맵핑 시켜서 리스트에 넣고 리턴 시킨다</p>
<pre><code class="language-java">        BoardVo board = boardRepository.findById(id).get();
        boardList.add(new BoardResponseDto(board));</code></pre>
<hr>
<p>*<em>결과는 ? *</em>
<img src="https://velog.velcdn.com/images/explorer-cat/post/1e889f43-47f3-4fd9-96dd-26ff69236b89/image.png" alt=""></p>
<p>아주 이뿌게 잘 온다.</p>
<p>실제 앱 화면에서도 한번 보자</p>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/94f0e510-5f42-4dae-af65-9c55811d419e/image.gif" alt=""></p>
<p>다음은 View에서 사용할 Response 공통 DTO를 한번 만들어보는걸로 복습해야겠다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring legacy MariaDB + MyBatis 세팅(시도) 해보기 (1)]]></title>
            <link>https://velog.io/@explorer-cat/Spring-legacy-MariaDB-MyBatis-%EC%84%B8%ED%8C%85%EC%8B%9C%EB%8F%84-%ED%95%B4%EB%B3%B4%EA%B8%B0-1</link>
            <guid>https://velog.io/@explorer-cat/Spring-legacy-MariaDB-MyBatis-%EC%84%B8%ED%8C%85%EC%8B%9C%EB%8F%84-%ED%95%B4%EB%B3%B4%EA%B8%B0-1</guid>
            <pubDate>Sun, 01 Jan 2023 16:48:07 GMT</pubDate>
            <description><![CDATA[<p>이 게시글은 apple m1 mac 기준으로 작성되었습니다.</p>
<h3 id="1-mariadb-설치">1. MariaDB 설치</h3>
<pre><code>homebrew로 mariaDB 설치

$brew install mariadb</code></pre><pre><code>mariaDB service 시작

$brew services start mariadb</code></pre><pre><code>mariaDB service 정지

$brew services stop mariadb</code></pre><pre><code>mariaDB service 상태 확인

$brew services list</code></pre><h3 id="2-mariadb-password-setting">2. MariaDB Password Setting</h3>
<p>mariaDB root 계정으로 접속</p>
<pre><code>$ mysql -u root -p</code></pre><p>아래 명령어로 Password를 설정합니다.</p>
<pre><code>MariaDB [mysql]&gt; set password for root@&#39;localhost&#39; = PASSWORD(&#39;root&#39;);
Query OK, 0 rows affected (0.008 sec)</code></pre><p>다시 mariaDB 루트 계정으로 접속을 시도하고 설정한 비밀번호를 입력해봅니다.</p>
<pre><code> mysql -uroot -p  </code></pre><p><img src="https://velog.velcdn.com/images/explorer-cat/post/21c88fb2-9942-42b2-9c52-b22ebdc41719/image.png" alt=""></p>
<p>여기까지 진행됐다면 성공인듯 하다..</p>
<h3 id="3-데이터베이스를-생성해보자">3. 데이터베이스를 생성해보자.</h3>
<pre><code>현재 데이터베이스 조회

MariaDB [(none)]&gt; show databases;

사용할 테스트 데이터베이스 생성

MariaDB [(none)]&gt; create database springDB;
Query OK, 1 row affected (0.001 sec)
</code></pre><h3 id="3-데이터베이스-관리툴-세팅">3. 데이터베이스 관리툴 세팅</h3>
<p>예전에 사용하던 HeidiSQL을 사용하려고하니, Mac 에서는 SequelPro 라는 관리툴을 많이 사용한다해서 설치해보았다.</p>
<p>download : <a href="https://www.sequelpro.com/">https://www.sequelpro.com/</a></p>
<h3 id="4-spring-의존성-세팅">4. Spring 의존성 세팅</h3>
<p>위치 : pom.xml
추가 해야할 것
mybatis : mybatis 라이브러리
mybatis-spring : mybatis와 spring 연동
spirng-tx: spring에서 database 처리와 transaction 처리 라이브러리
spring-jdbc: spring에서 database 처리 라이브러리
spring-test
dpcp : jdbc의 Datasource 사용을 위해
spring-web : listener 추가를 위해
spring-webmvc :</p>
<p>참고 레퍼런스
<a href="https://velog.io/@0_sujeong/spring-mybatis-mariaDB-%EC%84%B8%ED%8C%85">https://velog.io/@0_sujeong/spring-mybatis-mariaDB-%EC%84%B8%ED%8C%85</a></p>
<p><a href="https://linked2ev.github.io/database/2021/04/15/MariaDB-3.-MariaDB-%EC%84%A4%EC%B9%98-for-Mac/">https://linked2ev.github.io/database/2021/04/15/MariaDB-3.-MariaDB-%EC%84%A4%EC%B9%98-for-Mac/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[intellij Spring Regacy 시작하기(프로젝트 생성  'Hello Spring')]]></title>
            <link>https://velog.io/@explorer-cat/intellij-Spring-Regacy-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1-Hello-Spring</link>
            <guid>https://velog.io/@explorer-cat/intellij-Spring-Regacy-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1-Hello-Spring</guid>
            <pubDate>Fri, 30 Dec 2022 17:05:52 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Spring을 한번도 다뤄보지 않은 채로 환경설정을 한 것이라 정상적인 절차가 아닐 수도 있습니다.
아래 버전 및 세팅 환경을 작성하였으니 참고해주세요. :)</p>
</blockquote>
<blockquote>
<p>세팅 환경</p>
</blockquote>
<ul>
<li>IntelliJ 2021.3.3   </li>
<li>Java 1.8  </li>
<li>m1 chip</li>
<li>apache-tomcat-8.5.84  </li>
</ul>
<hr>
<ol>
<li>Java 환경 변수 
<a href="https://youngwonhan-family.tistory.com/entry/MacOS-JAVAHOME-%ED%8C%A8%EC%8A%A4-%EC%B6%94%EA%B0%80-zsh-bash-%EA%B5%AC%EB%B6%84">https://youngwonhan-family.tistory.com/entry/MacOS-JAVAHOME-%ED%8C%A8%EC%8A%A4-%EC%B6%94%EA%B0%80-zsh-bash-%EA%B5%AC%EB%B6%84</a></li>
<li>tomcat 다운로드 (8버전이 안정적일거같음..)
<a href="https://tomcat.apache.org/tomcat-8.5-doc/index.html">https://tomcat.apache.org/tomcat-8.5-doc/index.html</a></li>
</ol>
<hr>
<h3 id="new-project-생성">New Project 생성</h3>
<ul>
<li>New Project 생성창에서 Java Enterprise를 선택하고 프로젝트 명을 세팅해준다.</li>
<li>Project Template은 Web application 을 선택한다.</li>
<li>Application server &quot;New..&quot; 버튼을 클릭해서 다운 받아 압축을 푼 tomcat 폴더를 지정해준다.</li>
<li>최하단 Project SDK 자바 버전을 설정해준다 !</li>
</ul>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/20d8fca7-f796-44c0-8f4e-74ed320e87f0/image.png" alt=""></p>
<h3 id="servlet-이-기본체크-되어있는지-확인하고-finish">Servlet 이 기본체크 되어있는지 확인하고 Finish</h3>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/7eba563b-fa59-4657-bfb3-96111a94cf30/image.png" alt=""></p>
<h3 id="tomcat-세팅하기">Tomcat 세팅하기</h3>
<ul>
<li>우측 상단 Run/Debug Configurations 팝업창을 켜준다</li>
<li>팝업창 왼쪽 상단 + 버튼을 클릭해서 Tomcat Server -&gt; Local을 클릭한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/96ac9ce3-230f-4cd5-9958-57137db87fe5/image.png" alt=""></p>
<ul>
<li>Tomcat Name 설정 후 Application server 에서 Browser를 클릭해서 압축을 풀어놓은 tomcat 폴더를 매핑 해준다.<ul>
<li>자동으로 되어있다면 안해줘도 되는거 같다.</li>
</ul>
</li>
<li>Fix 버튼 클릭 후 OK 클릭 후 저장하고 run 버튼 클릭</li>
</ul>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/03dce2c1-5339-4099-9d37-b00de57abbcc/image.png" alt=""></p>
<p>브라우저가 잘뜬다면 여기까지는 오케이다.</p>
<h3 id="spring-framework-세팅하기">Spring Framework 세팅하기</h3>
<ul>
<li>프로젝트 폴더를 우클릭 해서 &#39;Add Framework Support...&#39;를 클릭한다.</li>
<li>Spring 메뉴 안에 &#39;Spring MVC(..)&#39;를 체크하고 Ok를 눌러준다.<ul>
<li>초반에 Spring 프레임워크 자체가 안떠서 고생했는데 ultimate 2021.3.3 버전을 다시 설치하니 생겨있었다...</li>
</ul>
</li>
<li>Ok 를 클릭하고 세팅이 완료되었다면 왼쪽 탐색기에 src 디렉토리가 생겼다면 문제없이 완료된거다.
<img src="https://velog.velcdn.com/images/explorer-cat/post/784504be-d322-44a4-b81a-92a24d68f7c1/image.png" alt=""></li>
</ul>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/10b7c253-0642-4eaa-8b22-c0a3c8e44517/image.png" alt=""></p>
<h3 id="pomxml-에-해당-dependency-추가">pom.xml 에 해당 dependency 추가</h3>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.springframework&lt;/groupId&gt;
    &lt;artifactId&gt;spring-context&lt;/artifactId&gt;
    &lt;version&gt;5.3.16&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.springframework&lt;/groupId&gt;
    &lt;artifactId&gt;spring-webmvc&lt;/artifactId&gt;
    &lt;version&gt;5.3.16&lt;/version&gt;
&lt;/dependency&gt;</code></pre>
<ul>
<li>load maven project 눌러서 빨간 표시 제거</li>
</ul>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/14a22f11-aa30-447f-8b0d-7b2cda83b6ea/image.png" alt=""></p>
<h3 id="webapp-폴더-web-inf-webxml-파일을-수정해준다">webapp 폴더 WEB-INF web.xml 파일을 수정해준다.</h3>
<pre><code class="language-xml">        &lt;url-pattern&gt;/&lt;/url-pattern&gt;</code></pre>
<h3 id="library-세팅">library 세팅?</h3>
<p>-오른쪽에 있는 Abailable Elements 들에 있는 Spring 라이브러리들을 왼쪽으로 옮겨주고 OK</p>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/d1d60cc4-9f74-4b12-b57d-3686d0135e32/image.png" alt=""></p>
<h3 id="controlloer-생성">Controlloer 생성</h3>
<ul>
<li>src/main/java/controller 폴더를 생성하고 HomeController 클래스를 생성한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/970d0c1a-0585-493c-9a41-439cd6b8e85c/image.png" alt=""></p>
<p>-해당 코드를 클래스에 넣어준다.</p>
<pre><code class="language-class">package controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class HomeController {
    @RequestMapping(&quot;/&quot;)
    public String index(Model model) {
        return &quot;index&quot;;
    }
}</code></pre>
<p>-webapp / WEB-INF/dispacher-servelet.xml 파일을 열고 아래 코드를 넣어준다.</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
       xmlns:context=&quot;http://www.springframework.org/schema/context&quot;
       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd&quot;&gt;

    &lt;context:component-scan base-package=&quot;controller&quot; /&gt;
&lt;/beans&gt;</code></pre>
<p>Run/Debug Configurations Server탭 URL과 Deployment 탭 하단의 Application context를 &#39;/&#39;로 바꿔줘야한다.</p>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/32b7852a-00c3-4798-accc-5025902de4ec/image.png" alt=""></p>
<p>그리고 우측 상단에 RUN 버튼을 클릭해서 잘돌아가는지 확인해보자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[spring 환경설정(1) bad interpreter: /bin/sh^M: no such file or directory 문제 해결법]]></title>
            <link>https://velog.io/@explorer-cat/spring-%ED%99%98%EA%B2%BD%EC%84%A4%EC%A0%951-bad-interpreter-binshM-no-such-file-or-directory-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%EB%B2%95</link>
            <guid>https://velog.io/@explorer-cat/spring-%ED%99%98%EA%B2%BD%EC%84%A4%EC%A0%951-bad-interpreter-binshM-no-such-file-or-directory-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%EB%B2%95</guid>
            <pubDate>Wed, 28 Dec 2022 14:22:18 GMT</pubDate>
            <description><![CDATA[<p>로컬에 maven을 설치하지 않아도 사용할수 있도록 하기 위해 mvnw 파일을 실행시켜 설치한다.</p>
<pre><code>./mvnw package </code></pre><p>를 실행하니 아래와 같은 에러가 발생했다.</p>
<pre><code>zsh: permission denied: ./mvnw</code></pre><p>permission denied 많이 보던 녀석이다.  권한 문제인듯 하다. 
단순하게 해당 파일에 쓰기 권한을 부여해보았다.</p>
<pre><code>//권한 부여
1. chmod -x {filename} 
2. chmod 757 {filename}

둘중에 하나를 사용하면된다.</code></pre><p>권한을 부여를 하니.. 아래와 같은 이슈가 또 발생했다.</p>
<pre><code>zsh: ./mvnw: bad interpreter: /bin/sh^M: no such file or directory</code></pre><p>대충 읽어보니 mvnw 안에 뭔가 있어서는 안될 나쁜것 들이 적혀있다는것 같은데...
구글링을 해보니 아마 해당 프로젝트를 빌드해서 배포한 사람의 환경이 윈도우 인듯 하다.</p>
<p>파일 내용중 라인끝에 있는 개행문자(?)가 달라서 발생하는 문제이다.
vi 로 라인끝에 개행문자가 있는지 확인해보자.</p>
<p><img src="https://velog.velcdn.com/images/explorer-cat/post/4c0a81b4-d6d9-4316-b1cd-2a772a94d68b/image.png" alt=""></p>
<p>아무리 찾아도 없다. 도대체 끝에 뭘 지우라는건가?</p>
<p>아래 명령어로 다시 파일을 열어보자</p>
<pre><code>vi -b {filename}</code></pre><p><img src="https://velog.velcdn.com/images/explorer-cat/post/079ae8d0-6a12-48c9-9aa9-675fd42ca490/image.png" alt=""></p>
<p>우리 눈에 보이지 않던 문제를 일으키던 ^M 이 우리 눈에 나타났다.
이제 해당 문자들을 지워야하는데 한번에 지워 버릴 수 있는 명령어를 입력해보자.</p>
<p>vi -b {} 명령어로 해당 파일을 켜둔상태로 명령어를 입력해준다.</p>
<pre><code>:%s/^M 
이때 중요한건 ^M 은 ctrl+v+m 을 쳐서 나오는 파란색 글씨로 보여야한다.
키보드로 하나하나 치는거 아닙니다.

:wq //저장![]
</code></pre><p>이렇게 하면 해당 파일이 잘 실행된다.</p>
<p>참고 블로그 
<a href="https://jolly-sally.tistory.com/31">https://jolly-sally.tistory.com/31</a>
<a href="https://www.slipp.net/questions/468">https://www.slipp.net/questions/468</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[m1 리액트 네이티브 삽질 메모..]]></title>
            <link>https://velog.io/@explorer-cat/m1-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C-%EC%82%BD%EC%A7%88-%EB%A9%94%EB%AA%A8</link>
            <guid>https://velog.io/@explorer-cat/m1-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C-%EC%82%BD%EC%A7%88-%EB%A9%94%EB%AA%A8</guid>
            <pubDate>Thu, 20 Oct 2022 13:34:47 GMT</pubDate>
            <description><![CDATA[<p>에러 해결🔑 
error Failed to build iOS project. We ran &quot;xcodebuild&quot; command but it exited with error code 65.</p>
<p><a href="https://positiveko-til.vercel.app/til/react-native/error65.html#_1-xcode%E1%84%8B%E1%85%A6%E1%84%89%E1%85%A5-derived-data-%E1%84%89%E1%85%A1%E1%86%A8%E1%84%8C%E1%85%A6%E1%84%92%E1%85%A1%E1%84%80%E1%85%B5">https://positiveko-til.vercel.app/til/react-native/error65.html#_1-xcode%E1%84%8B%E1%85%A6%E1%84%89%E1%85%A5-derived-data-%E1%84%89%E1%85%A1%E1%86%A8%E1%84%8C%E1%85%A6%E1%84%92%E1%85%A1%E1%84%80%E1%85%B5</a></p>
]]></description>
        </item>
    </channel>
</rss>