<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>luna_runa.log</title>
        <link>https://velog.io/</link>
        <description>백엔드 개발자</description>
        <lastBuildDate>Tue, 13 Aug 2024 12:24:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>luna_runa.log</title>
            <url>https://velog.velcdn.com/images/luna_runa/profile/090a336f-0dac-4c1d-babd-c67d7afb6e87/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. luna_runa.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/luna_runa" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[TypeORM] JOIN + stringify(JSON_EXTRACT)된 항목에 대한 orderBy]]></title>
            <link>https://velog.io/@luna_runa/TypeORM-JOIN-stringify%EB%90%9C-%ED%95%AD%EB%AA%A9%EC%97%90-%EB%8C%80%ED%95%9C-orderBy</link>
            <guid>https://velog.io/@luna_runa/TypeORM-JOIN-stringify%EB%90%9C-%ED%95%AD%EB%AA%A9%EC%97%90-%EB%8C%80%ED%95%9C-orderBy</guid>
            <pubDate>Tue, 13 Aug 2024 12:24:03 GMT</pubDate>
            <description><![CDATA[<p>TypeORM을 사용하다 보면 QueryBuilder에 skip&amp;take가 있고, offset&amp;limit이 있다는 것을 알 수 있다.
<a href="https://donis-note.medium.com/typeorm-pagination%EC%97%90-%EA%B4%80-%ED%95%9C-%EC%A0%95%EB%A6%AC-3a92d106a373">이에 대해 상세히 설명해주는 글</a>이 많으니 여기선 이것에 대해 깊게 다루진 않겠지만, 보다싶이 JOIN에 대한 skip&amp;take에선 이미 성능 문제가 있다.</p>
<ul>
<li>(상황에 따라) 쓸모없는 DISTINCT 사용</li>
<li>쿼리가 1+1로 나감</li>
</ul>
<p>근데 여기서 추가적으로, 다른 테이블의 JSON stringify된 데이터를 기준으로 orderBy를 정렬하려고 할 때도 이슈가 있다.</p>
<blockquote>
<p>아래부턴 실제 사내에서 쓰이던 구조와 비슷하게 임의로 수정한 코드이기 때문에 휴먼에러가 존재할 수 있습니다.
또한 TypeORM 0.3.17버전을 기준으로 작성되었으며 버전에 따라 다른 결과를 보일 수 있습니다.</p>
</blockquote>
<p>유저-포스트의 1:N(혹은 1:1) 관계가 있으며, 포스트의 meta_data라는 열에 여러 정보를 json으로 관리하고 있다. 이 중 subject라는 필드를 기준으로 정렬하려고 한다.</p>
<pre><code class="language-ts">await this.createQueryBuilder(&#39;user&#39;)
      .leftJoinAndSelect(&#39;user.post&#39;, &#39;post&#39;)
      .select([
        &#39;user.id&#39;,
        `JSON_EXTRACT(post.meta_data, &#39;$.subject&#39;) AS post_subject`,
      ])
      .where(`${searchType} like :searchValue`, { searchValue: `%${searchValue}%` })
      .skip(skipCount)
      .take(showCount)
      .orderBy(&#39;post_subject&#39;, &#39;DESC&#39;)
      .getManyAndCount()</code></pre>
<pre><code class="language-sql"># 실제로 발생하는 쿼리
SELECT DISTINCT `distinctAlias`.`user_id` AS `ids_user_id`
FROM (
  SELECT `user`.`id` AS `user_id`, JSON_EXTRACT(`post`.`meta_data`, &#39;$.subject&#39;) AS post_subject
  FROM `user` `user` LEFT JOIN `post` `post` ON `post`.`id`=`user`.`post_id`
  WHERE `user`.`name` like ?
) `distinctAlias`
ORDER BY post_subject DESC, `user_id` ASC LIMIT 10

# 실행 결과
[ERROR] Expression #1 of ORDER BY clause is not in SELECT list, references column &#39;distinctAlias.post_subject&#39; which is not in SELECT list; this is incompatible with DISTINCT</code></pre>
<p>성능 문제는 둘째치고 실행조차 되지 않게 된다.
DISTINCT와 ORDER BY를 사용할 때 ORDER BY에서 참조하는 모든 걸럼은 SELECT 리스트에 포함되어야 한다고 한다.
실제로 존재하는 열에 대한 orderBy를 사용할 땐 SELECT DISTINCT에 추가되는 모습을 볼 수 있다.</p>
<pre><code class="language-ts">await this.createQueryBuilder(&#39;user&#39;)
      .leftJoinAndSelect(&#39;user.post&#39;, &#39;post&#39;)
      .select([
        &#39;user.id&#39;,
        `JSON_EXTRACT(post.meta_data, &#39;$.subject&#39;) AS post_subject`,
          &#39;user.create_date&#39;
      ])
      .where(`${searchType} like :searchValue`, { searchValue: `%${searchValue}%` })
      .skip(skipCount)
      .take(showCount)
      .orderBy(&#39;user.create_date&#39;, &#39;DESC&#39;)
      .getManyAndCount()</code></pre>
<pre><code class="language-sql"># 실제로 발생하는 쿼리
SELECT DISTINCT `distinctAlias`.`user_id` AS `ids_user_id`, `distinctAlias`.`user_create_date`
FROM (
  SELECT `user`.`id` AS `user_id`, JSON_EXTRACT(`post`.`meta_data`, &#39;$.subject&#39;) AS post_subject, `user`.`create_date` AS `user_create_date`
  FROM `user` `user` LEFT JOIN `post` `post` ON `post`.`id`=`user`.`post_id`
  WHERE `user`.`name` like ?
) `distinctAlias`
ORDER BY `distinctAlias`.`user_create_date` DESC, `user_id` ASC LIMIT 10

# 실행 결과
# ... id IN을 실행하는 select 한번, COUNT를 위한 select 한번</code></pre>
<p>이를 보아 알 수 있는 점은 JSON_EXTRACT 등 가상의 컬럼을 이용한 OrderBy를 사용하고 싶어도 TypeORM에서 자동으로 DISTINCT의 SELECT에 넣어주지 못하기 때문에 불가능하다는 것이다.</p>
<p>그럼 이를 어떻게 해결해야할까?
하나의 방법으로는 위에 공유된 글처럼 innerJoin 서브쿼리를 이용하면 해결이 될 것 같다. (실제로도 이 방법이 옳을 것 같다)
하지만 필자는 해당 방법을 사용하지 않고 offset&amp;limit을 사용했다.
이유는 굉장히 단순한데 실제 사내에서 발생한 조인 테이블의 관계가 One-To-One이었기 때문이다.
<a href="https://github.com/typeorm/typeorm/issues/4998#issuecomment-1193028576">Github의 관련 이슈 댓글</a>에서도 언급되었듯이 One-To-One 관계에서는 DISTINCT가 전혀 필요하지 않으며, 마찬가지로 offset&amp;limit을 사용했을 때 발생하는 중복 열 이슈도 발생되지 않는다.</p>
<p>라고 결론을 내렸고 일단 현재로서는 아무 문제도 발생하지 않았으나 예상하지 못한 부분에서 문제가 발생할지도 모른다. JSON_EXTRACT를 사용하는 비슷한 케이스에서의 OrderBy에 대해 고민하고 있을 때 참고해볼 수 있도록 작성하게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[일상] 첫 대형 프로젝트 마무리 회고록]]></title>
            <link>https://velog.io/@luna_runa/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A7%88%EB%AC%B4%EB%A6%AC-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@luna_runa/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A7%88%EB%AC%B4%EB%A6%AC-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Wed, 22 Feb 2023 06:22:10 GMT</pubDate>
            <description><![CDATA[<p>첫 회사에서 처음으로 맡아본 프로젝트가 마무리됐다.
항상 풀스택을 목표로 했었지만 진짜 실무에서 풀스택으로 밑바닥 설계부터 끝까지 맡는다는건 역시 생각보단 고난했다.
반년이 조금 넘은 지금 회고록을 작성해보기로 했다.</p>
<hr>
<h2 id="0-개요">0. 개요</h2>
<p>사내에서 기존에 사용하던 웹 관련 프론트, 백엔드의 기술스택을 마이그레이션하게 됐다.
상세하게 프론트는 <strong>Angular에서 React로</strong>, 백엔드는 <strong>Flask에서 NestJS로</strong> 옮기게 됐다.
이런 선택을 하게된 이유는 아래와 같다.</p>
<ul>
<li>기존 레거시 프로젝트(Angular, Flask)는 외주 업체에서 전달받았지만 아무 문서도 없었고 이쪽 회사에 맞춰서 개발이 된 상태도 아니었기 때문에 불필요한 코드도 굉장히 많았으며 아키텍처도 존재하지 않고 하나의 함수에서 라우팅, 비즈니스 로직, DB 제어, 응답을 모두 수행하는 등 <strong>유지보수가 굉장히 힘든 상태</strong>의 코드였다.</li>
<li>소수의 인원(1~2명)으로 웹 전반의 유지보수 + 요구사항 개발을 진행해야했지만 기술스택이 TypeScript(Angular) / Python(Flask)로 분리되어 있어 <strong>개발비용 측면에서 불리</strong>한 상황이었다.</li>
<li>모바일 프론트의 경우 React Native로 개발되고 있었으며 웹 프론트도 React로 변경하면 비슷한 프레임워크(라이브러리) 환경이기 때문에 좀 더 이점을 취할 수 있을 것이라 판단되었다.</li>
<li>같은 이유로 백엔드도 NestJS로 변경하면 <strong>웹 프론트 + 앱 프론트 + 백엔드 셋 다 TypeScript로 모든 서비스를 하나의 언어스택</strong>으로 구성할 수 있었다.</li>
</ul>
<p>결정적으로 회사에서 서비스를 확장하려고 하는 단계였으며 DB, 서버, 프론트에 대규모 수정사항이 생길 구조적 변경 요구사항이 생기게 됐다.
현재 상태를 유지하면서 추가 개발을 진행하는 것은 장기적으로 고려해봤을때 결국은 기술부채를 계속해서 늘리기만 할 뿐이라 판단했으며 이에 마이그레이션 작업을 제안하여 진행하게 됐다.</p>
<h2 id="1-백엔드-서버">1. 백엔드 서버</h2>
<p>앞서 말했듯이 레거시 서버는 라우팅, 비즈니스 로직, DB 제어, 응답 등 모든 로직이 하나의 함수에서만 이루어져 있었으며 당연하게도 재사용성은 미비했고 중복 로직이 다수 존재했다.
API 문서 또한 존재하지 않았기 때문에 처음에 분석하고 재설계하는 과정에서 굉장히 많이 고생했던 기억이 아직도 난다.
DB 구조에서도 무분별한 인덱스 설정과 사용되지 않는 컬럼, 잘못 사용되는 컬럼 등 많은 문제점이 존재하는 것을 확인할 수 있었다.</p>
<p>같은 TypeScript로 구성할 수 있는 Express를 선택할 수도 있었겠지만 이미 위의 문제점들을 겪어본 입장에서 장기적으로도 아키텍처가 지켜질 수 있도록 프레임워크 차원에서 지원받고 싶어 NestJS를 선택했으며 어떻게 만들어야 유지보수, 추가 기능 개발에 용이할 수 있을지 찾아보았고 클린 아키텍처라는 것을 공부해 도입해보기로 결정했다.</p>
<h3 id="1-1-클린-아키텍처">1-1. 클린 아키텍처</h3>
<p><img src="https://velog.velcdn.com/images/luna_runa/post/5d4bffae-bfd5-4111-a717-9514aca8f0c2/image.png" alt=""></p>
<p>각 레이어를 뚜렷하게 정의하여 나누고 분리된 의존성을 토대로 따로따로 개발하며 이를 이용한 이점으로 테스트 코드를 작성해보고.. 처음엔 모든 것이 좋은 느낌이었다. 하지만 얼마 안가 여러 고민들이 깊게 자리하게 됐으며 일주일 내내 구조만 계속 변경했던 시기도 있었다.</p>
<ul>
<li>도메인(엔티티)레이어를 잘못 이해하고 사용하고 있었다. 그저 멤버 변수만 존재하는, 실질적으론 타입과 다름없는 도메인들만 생겨났다.<ul>
<li>사실 전체 서비스가 DB에서 단순 CRUD 작업만 진행하는 것이 70% 이상이었으며 비즈니스 도메인이 거의 없다는 점도 한몫 했었다. 어찌보면 이 아키텍처를 선택한 것부터 잘못됐던 걸까? 싶었다.</li>
<li>그렇다보니 &#39;진짜 비즈니스 로직&#39; 이 있어도 유스케이스 레이어에서 작성하게 되는 일이 잦아졌고 이게 정말 도메인을 지키는, 도메인을 중심으로 사용하는 아키텍처가 맞나..? 싶어졌다.</li>
</ul>
</li>
</ul>
<blockquote>
<p>클린 아키텍처로 구성했다는 많은 깃 레포, 블로그 자료 등을 참조해보며 생각해본 결과 이런 의문들이 계속 생겨나는 근본적인 이유는 &#39;이미 DB가 완성되어 있었으며 사실상 DB 스키마에 의존하여 단순한 CRUD만 진행하는 서비스&#39;였기 때문에 더더욱 그런쪽으로 밖에 생각하지 못하게 됐던 것 같았다.
이를 교정하기 위해 &#39;만들면서 배우는 헥사고날 아키텍처 설계와 구현&#39; 이라는 책을 많이 참고했었으며 덕분에 클린 아키텍처를 온전하게 사용하는 법을 터득했다.</p>
</blockquote>
<ul>
<li>DTO를 어떻게 구성하고 어디서 선언한 뒤 어떻게 넘겨줘야 할지 굉장히 심하게 고민했었다.<ul>
<li>처음엔 인터페이스로만 선언한 뒤 객체 리터럴로 반환, 전달을 진행했었지만 런타임 시점에서 사라지는 타입으로 DTO를 전달하고 사용하는 것이 옳은걸까..? 타입에 대한 유효성 검증을 아예 진행하지 않는 것이 과연 올바른걸까...? 라는 고민이 생겨났다.</li>
<li>위 고민 끝에 클래스 형식으로 DTO를 대부분 바꾸던 중 클래스로 만들어 사용하는 것도 깔끔하고 납득되는 해답이 아니었기 때문에 더 혼란스러웠다. 정의상 DTO는 아무 로직도, 함수도 포함하지 않아야 된다는데 그렇다면 더더욱 클래스일 이유가 없는게 아닐까? 멤버변수만 있는 클래스가 인터페이스(타입)과 도대체 뭐가 다른걸까?</li>
</ul>
</li>
</ul>
<blockquote>
<p>결국 해당 프로젝트 안에서 DTO를 어떻게 만들고 사용할지에 대한 컨벤션을 정했으며 명확한 기준을 나눠서 인터페이스와 클래스를 적절히 섞어서 사용하게 되었다.
통일성 관점에서도 별로 좋진 않은 것 같고 클래스에 static 메서드(안쪽 레이어의 DTO로 변환하는 함수)가 포함되어 있었기 때문에 DTO의 정의랑도 잘 맞지 않는 거 같았지만 DTO 관련 논쟁은 우리 뿐만이 아니라 다른 사람들도 많이 겪는 문제인 것으로 보아 확실하게 구분지어 정의할 순 없는 영역이라 생각했고 여러 시행착오 끝에 도달한 그나마 납득 가능한 정의들이었다.
해당 방식은 <a href="https://github.com/pvarentsov/typescript-clean-architecture">pvarentsov/typescript-clean-architecture</a> 레포지터리에서 참조했다.</p>
</blockquote>
<p>클린 아키텍처를 효과적으로 사용하기 위해선 고려해봐야 되는 점이 생각보다도 굉장히 많았으며 이런 개념이 생겨난 이유가 많이 와닿았다. 역시 최우선적으로 지켜야 될것은 &#39;의존성의 흐름, 방향과 제어&#39; 라고 생각해 그것을 중점으로 뒀으며 결과적으로 나름 납득 가능한 선에서의 아키텍처를 정의해냈고 이로 인해 유닛 테스트를 작성하는데에 많은 이점을 가져올 수 있었다.</p>
<h3 id="1-2-typeorm">1-2. TypeORM..........</h3>
<p>보통 NestJS로 프로젝트를 구성할 때 TypeORM을 많이 사용하며 공식문서에서도 이를 기반으로 설명하고 있다.
실제로 프로젝트를 구성했을 단계에서 여러 ORM을 비교해봤을 때 TypeORM이 여러면에서 적합하다고 판단되어 도입했지만 정말. 정말... 여러 문제가 많았다........</p>
<ul>
<li>타입스크립트를 사용하는데도 타입에 안전하지 않다.<ul>
<li>select 옵션으로 특정 컬럼들을 지정하는 순간 TypeORM 엔티티와 결과 값의 타입은 완전히 달라지게 된다. undefined.....</li>
<li>쿼리빌더를 사용할 때 해당 엔티티에 대한 타입을 자동완성으로 지원해주지 않는다. 사실상 생 쿼리와 다름없는 매직 스트링을 입력하게 되며 이 경우 ORM을 사용하는 이점이 소실되며 오히려 생 쿼리를 쿼리빌더 문법으로 사용하기 위해 찾아내는 작업을 해야해서 일을 두번씩 하는 느낌이었다. (특히 andWhere 안에서 orWhere를 사용하는 복잡한 조건에서 &#39;Brackets&#39; 라는 객체를 생성해 사용해야 했는데 이 경험이 굉장히 좋지 않았다.)</li>
</ul>
</li>
<li>굉장히 많은 버그가 존재했다. 정말 많이 존재했다.<ul>
<li>거의 항상 이슈에서 검색해봤던 거 같다. 제일 기억에 남는 것은 join이 들어가는 where 쿼리에서 inner join이 기본 값으로 적용되는 점이었다. 해당 문제는 12월 4일 0.3.11버전으로 업데이트되면서 해결됐지만 이런 비슷한 문제들을 굉장히 많이 겪어 지쳐갔다.</li>
<li>클린(레이어드) 아키텍처에서 사용할 때 트랜잭션 처리에 관해서 곤란했다.</li>
</ul>
</li>
<li>여러 모듈이 엮여 있는 트랜잭션 작업을 수행하기 위해선 유스케이스 레이어에서 트랜잭션을 알아야 했으며 이를 알게되는 순간 유스케이스 레이어가 인프라 레이어의 세부사항을 알게되는 문제점을 갖고 있다.</li>
<li>이를 해결하기 위한 typeorm-transactional-cls-hooked 등 라이브러리가 존재하긴 했으나 어떤 선택지도 적절하진 않았다. (사실 이 문제는 TypeORM만의 문제점은 아닌 것 같긴 하다.)</li>
</ul>
<p>프로젝트의 중반부터는 정말 하루에도 몇번씩이나 TypeORM... 또 TypeORM 너야..... 라는 생각을 달고 살았던 것 같다. 이쯤부터 Prisma를 잠깐씩 공부해보고 있었는데 물론 프리즈마도 깊게 파고 들어가면서 프로젝트를 진행해보면 문제점이 생기긴 하겠지만 그래도... 정말 TypeORM에 비해서는 천사처럼 느껴질 정도로 사용 경험이 굉장히 좋았다.</p>
<blockquote>
<p>개인적으로 TypeORM은 결국 스프링 JPA의 엄청난 하위호환일 뿐이라는 느낌을 굉장히 많이 들게했으며 이걸 사용할 바에는 차라리 스프링으로, JPA를 사용해서 구현하는게 낫겠다 싶었다..
그에 비해 Prisma는 확실하게 독자적인 방식을 채택하며 좀 더 TypeScript, NodeJS스럽다 라는 느낌을 많이 받을 수 있어 만족스러웠다.
비록 NestJS도 TypeORM도 스프링 환경을 참조해서 만들었다곤 해도 결국 독자적인 방향을 추구하지 않는다면 &#39;그거 쓸바엔 그냥 스프링 쓰면 되는 거 아냐? 어차피 스프링에 비해서 기능도 굉장히 빈약하고 불안정하던데&#39; 에 그치지 않을까 라고 생각한다.</p>
</blockquote>
<h2 id="2-인프라데브옵스">2. 인프라/데브옵스</h2>
<p>입사 후 3~4주차. 아직도 그때의 괴로운 기억이 선명하게 남아있었다.
레거시 프로젝트의 배포 프로세스를 로컬에서 테스트해보고 실제 배포를 진행했던 날이었는데 정말 끔찍한 경험이었다.</p>
<ul>
<li>배포에 필요한 모든 프로세스를 개발자가 &#39;<strong>매번 수동으로</strong>&#39; 직접 구성해야 했다.<ul>
<li>로컬에서 빌드를 진행하고, 새 버전의 이미지를 생성해 ECR에 업로드하고, ECS에 새로운 Task를 개정하고, Service를 업데이트 하는데 인스턴스의 가용 자원, 고정되어있는 포트 등의 문제도 발생했다.</li>
<li>하나의 ECS 클러스터, 하나의 서비스, 심지어 하나의 태스크에 모든 컨테이너(서버, 프론트, traefik, Worker, 심지어 레디스까지!)에 대한 정의가 구성되어 있었으며 당연하게도 하나의 수정사항이 생기면 엮여있던 <strong>모든 서비스들이 다운</strong>됐다. 실제로.....</li>
</ul>
</li>
</ul>
<p>덕분에 정말 이것만은 개선을 꼭 해내야겠다 라고 마음을 먹었었으며 결과는 아래와 같다.</p>
<h3 id="배포-프로세스">배포 프로세스</h3>
<p><img src="https://velog.velcdn.com/images/luna_runa/post/fa4f08df-84fa-4078-9a1a-841824e36fbb/image.png" alt=""></p>
<ul>
<li>그림에는 없지만 PR이 올라오면 Github Action에서 빌드, 테스트, 린트 등 CI 파이프라인을 거치도록 구축했다.</li>
<li>CodePipeline을 활용해서 ECS Blue/Green 무중단 배포 CD 파이프라인을 구축했다.<ul>
<li>당연히 이전과는 다르게 클러스터와 서비스, 태스크를 나눠 각각의 수정사항이 다른 서비스에 영향을 미치지 않도록 구성했다.</li>
</ul>
</li>
</ul>
<blockquote>
<p>AWS의 여러 기능들을 처음 써보게 되었으며 보름 가까이 코드와는 멀어져 공부만 했던 것 같다.
인프라, 데브옵스 관련 정보글들은 찾아보면 항상 추상적으로 설명만 하고 구체적인 설정 예제가 없어 굉장히 막막하기도 했고 제대로 구축하고 있는게 맞을까 잘못되면 어떡하지 하는 걱정에 겁이나서 하루종일 손도 못댔던 기억이 난다.
더군다나 AWS 콘솔 환경이 신버전으로 업그레이드하고 있는 중이었으며 구버전과 신버전을 번갈아가면서 특정 기능이 존재하는지 찾아봐야 했다......
그래도 결국 공식문서에는 모든 답이 있었고(그렇게 친절하진 않았지만) 결국은 완전히 구축을 해내 실제 배포가 진행되고 수정사항이 생겼을 때도 main 브랜치에 push가 되는 순간 파이프라인이 돌고 n분 뒤에 새 버전으로 배포가 성공적으로 진행되는 것을 확인했을 때의 성취감은 정말...
직접 구축하고 나니  그 수많은 정보글들이 왜 추상적으로만 알려줬는지 이해가 됐었다.</p>
</blockquote>
<h3 id="전체-서비스-아키텍처-간략">전체 서비스 아키텍처 (간략)</h3>
<p><img src="https://velog.velcdn.com/images/luna_runa/post/2c21ca3f-8b6d-4416-a4d1-5d088cbe98aa/image.png" alt="">
배포 환경에서의 아키텍처도 일부 수정된 부분이 있었는데 원래 레거시 구조에선 컨테이너로 띄워져있던 traefik이 로드밸런싱을 담당하고 있었으며 라우팅은 가비아를 사용했었다.
여기서 컨테이너들이 고정된 포트를 사용했기 때문에 당연히 오토스케일링이나 무중단 배포는 사실상 불가능한 구조였다.</p>
<p>이를 해결하기 위해 AWS의 여러 기능들을 검토해서 적극적으로 사용해 개선했으며 현재 서비스는 언제든 확장이 편리하게 가능한 상태로 개선해냈다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeORM] NestJS + TypeORM + Postgres에서 DB 마이그레이션 작업 사용하기 (+ 에러 정리)]]></title>
            <link>https://velog.io/@luna_runa/TypeORM-NestJS-TypeORM-Postgres%EC%97%90%EC%84%9C-DB-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9E%91%EC%97%85-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-%EC%97%90%EB%9F%AC-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@luna_runa/TypeORM-NestJS-TypeORM-Postgres%EC%97%90%EC%84%9C-DB-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9E%91%EC%97%85-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-%EC%97%90%EB%9F%AC-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 27 Oct 2022 03:40:02 GMT</pubDate>
            <description><![CDATA[<p>NestJS에서 TypeORM으로 PostgreSQL을 사용할 때 DB 스키마 변경, 등에서 중요한 마이그레이션 기능을 사용하기 위한 절차이다.</p>
<h1 id="setup">setup</h1>
<pre><code>yarn add typeorm@0.2.45 @nestjs/typeorm pg</code></pre><p>의존성 패키지들을 설치한 뒤 package.json을 설정해준다.</p>
<pre><code class="language-json">// package.json
  &quot;scripts&quot;: {
    // 이 부분의 --config 중요!!
    &quot;typeorm&quot;: &quot;node --require ts-node/register ./node_modules/typeorm/cli.js --config src/ormconfig.ts&quot;,
    &quot;make:migrations&quot;: &quot;yarn typeorm migration:generate -n&quot;,
    &quot;make:migrate&quot;: &quot;yarn typeorm migration:run&quot;,
    &quot;make:rollback&quot;: &quot;yarn typeorm migration:revert&quot;
  }</code></pre>
<p>TypeOrm 모듈에 설정 파일을 전달하기 위한 방법으로 ormconfig.ts를 넣어주는 방법을 사용한다.</p>
<pre><code class="language-.env"># .env
DB_HOST=
DB_PORT=
DB_USER=
DB_PASSWORD=
DB_NAME=</code></pre>
<pre><code class="language-ts">// src/ormconfig.ts
import { TypeOrmModuleOptions } from &#39;@nestjs/typeorm&#39;
import * as dotenv from &#39;dotenv&#39;

dotenv.config()

const ormconfig: TypeOrmModuleOptions = {
  type: &#39;postgres&#39;,
  host: process.env.DB_HOST,
  port: process.env.DB_PORT,
  username: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  logging: true,
  entities: [__dirname + &#39;/**/*.entity{.ts,.js}&#39;],

  // TypeOrm 자동 동기화, migration 작업을 위해 중지
  synchronize: false,

  // dist/migrations에 있는 파일들을 실행
  migrations: [__dirname + &#39;/migrations/**/*{.ts,.js}&#39;],
  // migrate run 자동 실행
  // migrationsRun: true,
  cli: {
    // entitiesDir: &#39;src/entities&#39;,
    // src/migrations에 있는 파일들을 dist/migrations에 생성
    migrationsDir: &#39;src/migrations&#39;,
  },
}

export default ormconfig
</code></pre>
<pre><code class="language-ts">// src/app.module.ts
import ormconfig from &#39;./ormconfig&#39;

@Module({
  imports: {
    ...
    TypeOrmModule.forRoot(ormconfig),
    ...
  ],
  ...</code></pre>
<hr>
<h1 id="run">run</h1>
<pre><code>yarn make:migrations (이름)
yarn make:migrate
yarn make:rollback</code></pre><p>yarn make:migrations {이름} 실행 시</p>
<ol>
<li>DB 상태와 현재 Entity파일의 차이를 찾아낸다.</li>
<li>src/migrations 폴더에 ....-{이름}.ts를 생성한다.</li>
<li>dist/migrations 폴더에 ....-{이름}.js로 컨버팅된다.</li>
</ol>
<p>yarn make:migrate / rollback은 각각 up, down (마이그레이션 실행, 되돌리기) 역할을 수행한다.</p>
<hr>
<h1 id="known-error">known error</h1>
<h2 id="typeormmoduleoptions-형식에-할당할-수-없습니다">&#39;TypeOrmModuleOptions&#39; 형식에 할당할 수 없습니다.</h2>
<blockquote>
<p>&#39;{ type: &quot;postgres&quot;; host: string; port: number; username: string; password: string; database: string; logging: true; entities: string[]; synchronize: true; migrations: string[]; cli: { migrationsDir: string; }; }&#39; 형식은 &#39;TypeOrmModuleOptions&#39; 형식에 할당할 수 없습니다.
  개체 리터럴은 알려진 속성만 지정할 수 있으며 &#39;{ retryAttempts?: number | undefined; retryDelay?: number | undefined; toRetry?: ((err: any) =&gt; boolean) | undefined; autoLoadEntities?: boolean | undefined; keepConnectionAlive?: boolean | undefined; verboseRetryLog?: boolean | undefined; } &amp; Partial&lt;...&gt;&#39; 형식에 &#39;cli&#39;이(가) 없습니다.ts(2322)</p>
</blockquote>
<p>이 에러가 뜨는 이유는 TypeORM 신버전(0.3.x)에서 TypeOrmModuleOptions에 type으로 PostgreSQL을 설정했을때 cli 속성이 사라졌기 때문이다. 레퍼런스를 찾아본 결과 해당 옵션 없이도 해결할 수 있는 방법이 있긴 한 거 같지만 굳이 최신버전이어야 될 이유도 없고 굉장히 번거로웠기 때문에 다운그레이드로 해결했다.</p>
<pre><code class="language-json">// package.json
  &quot;dependencies&quot;: {
    ...
    &quot;typeorm&quot;: &quot;^0.2.45&quot;,
    ...
  }</code></pre>
<hr>
<h2 id="missingdrivererror-wrong-driver-undefined-given-supported-drivers-are-aurora-data-api-">MissingDriverError: Wrong driver: &quot;undefined&quot; given. Supported drivers are: &quot;aurora-data-api&quot;, &quot;...&quot;.</h2>
<blockquote>
<p>[Nest] 23260  - 2022. 10. 27. 오전 11:15:58   ERROR [ExceptionHandler] Wrong driver: &quot;undefined&quot; given. Supported drivers are: &quot;aurora-data-api&quot;, &quot;aurora-data-api-pg&quot;, &quot;better-sqlite3&quot;, &quot;capacitor&quot;, &quot;cockroachdb&quot;, &quot;cordova&quot;, &quot;expo&quot;, &quot;mariadb&quot;, &quot;mongodb&quot;, &quot;mssql&quot;, &quot;mysql&quot;, &quot;nativescript&quot;, &quot;oracle&quot;, &quot;postgres&quot;, &quot;react-native&quot;, &quot;sap&quot;, &quot;sqlite&quot;, &quot;sqljs&quot;.
MissingDriverError: Wrong driver: &quot;undefined&quot; given. Supported drivers are: &quot;aurora-data-api&quot;, &quot;aurora-data-api-pg&quot;, &quot;better-sqlite3&quot;, &quot;capacitor&quot;, &quot;cockroachdb&quot;, &quot;cordova&quot;, &quot;expo&quot;, &quot;mariadb&quot;, &quot;mongodb&quot;, &quot;mssql&quot;, &quot;mysql&quot;, &quot;nativescript&quot;, &quot;oracle&quot;, &quot;postgres&quot;, &quot;react-native&quot;, &quot;sap&quot;, &quot;sqlite&quot;, &quot;sqljs&quot;.</p>
</blockquote>
<p>nestjs에서 typeorm은 TypeOrmModule.forRoot()에 아무 인수도 제공하지 않고도 ormconfig.json을 자동으로 인식할 수 있지만 타입 관련해서 제대로 잡지 못해서 나타나는 에러였다.
원인은 아마 실행되는 순서일 것이라 생각되어 ormconfig을 ts파일로 만들어 타입을 부여해주는 것으로 해결했다.</p>
<pre><code class="language-ts">// src/ormconfig.ts
import { TypeOrmModuleOptions } from &#39;@nestjs/typeorm&#39;

const ormconfig: TypeOrmModuleOptions = {
  type: &#39;postgres&#39;,
  ...
}

export default ormconfig</code></pre>
<hr>
<h2 id="typeormerror-no-connection-options-were-found-in-any-orm-configuration-files">TypeORMError: No connection options were found in any orm configuration files.</h2>
<p>이 에러는 typeorm 명령어를 사용할 때 config로 아무 파일도 전해지지 않았기 때문이다.
위의 에러와 마찬가지로 typeorm은 ormconfig.json은 자동으로 인식하지만 위의 과정으로 인해 ormconfig.ts로 변경했기 때문에 찾지 못해 발생한 에러였다.
명령어의 뒤에 --config 옵션으로 ormconfig.ts파일을 지정해준다.</p>
<pre><code class="language-json">// package.json
  &quot;scripts&quot;: {
    ...
    &quot;typeorm&quot;: &quot;node --require ts-node/register ./node_modules/typeorm/cli.js --config src/ormconfig.ts&quot;,
    ...
  }</code></pre>
<hr>
<h2 id="no-migrations-are-pending">No migrations are pending</h2>
<blockquote>
<p>query: SELECT * FROM current_schema()
query: SHOW server_version;
query: SELECT * FROM &quot;information_schema&quot;.&quot;tables&quot; WHERE &quot;table_schema&quot; = &#39;public&#39; AND &quot;table_name&quot; = &#39;migrations&#39;
query: SELECT * FROM &quot;information_schema&quot;.&quot;tables&quot; WHERE &quot;table_schema&quot; = &#39;public&#39; AND &quot;table_name&quot; = &#39;typeorm_metadata&#39;
query: SELECT * FROM &quot;migrations&quot; &quot;migrations&quot; ORDER BY &quot;id&quot; DESC
No migrations are pending
✨  Done in 1.67s.</p>
</blockquote>
<p>typeorm migration:generate 명령도 제대로 동작했고 migrations폴더에 마이그레이션에 관한 코드도 제대로 생성됐는데 migrations 파일을 찾을 수 없다는 에러가 발생했다.
ts파일을 js로 컨버팅하고 dist로 옮겨졌을 때 위치를 제대로 잡아주지 못해서 나타나는 에러였다.
ormconfig에서 migrations랑 migrationsDir에 적절한 위치를 잡아주면 해결된다.</p>
<pre><code class="language-ts">// src/ormconfig.ts
const ormconfig: TypeOrmModuleOptions = {
  ...
  // migration:run 명령어에서 사용하는 위치
  // 컨버팅 이후의 __dirname: dist, dist/migrations에 있는 파일들을 실행
  migrations: [__dirname + &#39;/migrations/**/*{.ts,.js}&#39;],
  cli: {
    // migration:generate 명령어에서 사용하는 위치
    // 실제 프로젝트에서 migrations 폴더를 어디에 만들 것인지 지정한다.
    // src/migrations에 생성된 파일들은 tsc를 거쳐 dist/migrations에 생성된다.
    migrationsDir: &#39;src/migrations&#39;,
  },</code></pre>
<p>migration 파일들은 src/migrations에 생성할 것이기 때문에 migrationsDir은 이렇게 지정한다.
이후 tsc를 거쳐 컨버팅된 파일들은 dist/migrations에 생성된다. (src -&gt; dist)
ormconfig 파일도 마찬가지로 컨버팅되어 dist로 전달되고 dist에서 실행되며 이 시점의 경로를 migrations에 잡아줘야 해결된다.
&#39;src/migrations&#39;나 &#39;/migrations/&#39;이면 에러가 발생하며 __dirname을 사용해 제대로 dist 위치를 잡아주는 것으로 해결했다.</p>
<hr>
<h2 id="error-eacces-permission-denied-scandir-libraryapplication-supportappleassetcachedata">Error: EACCES: permission denied, scandir &#39;/Library/Application Support/Apple/AssetCache/Data&#39;</h2>
<blockquote>
<p>[Nest] 24076  - 2022. 10. 27. 오전 11:47:53   ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)...
Error: EACCES: permission denied, scandir &#39;/Library/Application Support/Apple/AssetCache/Data&#39;</p>
</blockquote>
<p>이 에러 메시지 자체는 아마도 Mac에서만 발생할 것으로 예상되지만 발생 조건으론 위와 같이 경로를 잘못 지정했을 때 발생한다.
해당 에러가 발생했을 때의 상황은 ormconfig 파일을 따로 service로 만들어 주입받게해 TypeOrmModule로 집어넣는 방식을 선택했을 때였다.</p>
<pre><code class="language-ts">// src/config/database/postgres-config.service./ts
@Injectable()
export class PostgresConfigService implements TypeOrmOptionsFactory {
  constructor(private configService: ConfigService) {}

  createTypeOrmOptions(): TypeOrmModuleOptions {
    return {
      entities: [&#39;/**/*.entity{.ts,.js}&#39;],</code></pre>
<p>에러 메시지로는 권한이 없다고 하는데 에러의 원인은 entities필드의 경로가 잘못되었을 때 발생했으며 경로를 제대로 잡아주면 해결된다.</p>
<pre><code>entities: [__dirname + &#39;../../../**/*.entity{.ts,.js}&#39;],</code></pre><p>하지만 어차피 이 방법으론 typeorm 명령어에서 config를 넘겨주지 못했기 때문에 다른 방법을 선택하기로 했다.</p>
<hr>
<p>이 과정중 굉장히 많은 에러가 발생했고 각 에러 하나하나를 해결하는데 꽤 많은 시간을 소모했으며 여기저기 검색해봐도 프로젝트마다 다른 ormconfig, package.json의 설정을 사용해 해결하기 어려웠다.
결국 해결한 결과도 ormconfig.ts가 typeorm에서 읽히게 하기 위해서 nest에 의존하지 않아야 했기 때문에 configService를 사용하지 못했고 dotenv.config()를 사용했다는 점이 찜찜하긴 했지만 ormconfig에 대한 내용을 중복해서 작성하기도 싫어 이쯤에서 타협하기로 했다.
이후에도 새 프로젝트를 처음 설정할 때 발생할 문제였기 때문에 기록으로 남긴다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[R2] S3 -> R2 마이그레이션 with S3 API]]></title>
            <link>https://velog.io/@luna_runa/R2-S3-R2-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-with-S3-API</link>
            <guid>https://velog.io/@luna_runa/R2-S3-R2-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-with-S3-API</guid>
            <pubDate>Thu, 20 Oct 2022 03:15:17 GMT</pubDate>
            <description><![CDATA[<p>회사에서 S3에 에셋을 담아두고 있었는데 요금 관련해서 R2가 더 저렴하다고 판단되어 이동하게 됐다.
S3에 이미 파일이 n백개 있었으며 지정되어 있는 헤더 등을 온전히 옮기기 위해 R2에서 제공해주는 S3 API를 사용하기로 했다.</p>
<p>R2에서의 S3 API는 현재 구현중이며 아직 지원하지 않는 부분이 굉장히 많았는데 특히 이쪽 서비스에서 필요한 업로드 진행도를 뽑아내는데에 고생했다. 특히 aws-sdk-js-v3을 사용했을 때 제대로 동작하지 않는 부분이 많았으며 R2에서는 v2를 사용하기로 했다.</p>
<hr>
<p>시나리오는 S3 버킷 내의 모든 파일들의 키를 뽑고, 키로 파일을 찾아와 R2에 업로드한다. 
우선 의존 라이브러리를 설치한다.</p>
<blockquote>
<p>yarn add aws-sdk @aws-sdk/client-s3</p>
</blockquote>
<p>S3 관련은 v3(@aws-sdk/client-s3)을 사용하고 R2에 업로드 하는 것은 v2(aws-sdk)를 사용할 것이다.</p>
<blockquote>
<p>! 아래 코드는 글을 작성했을 당시(돌아가는 로직) 기준이며 최신 코드는 Github을 참조해주세요.
 <a href="https://github.com/Luna-Runa/s3-to-r2">https://github.com/Luna-Runa/s3-to-r2</a></p>
</blockquote>
<h1 id="s3">S3</h1>
<pre><code class="language-ts">// ./s3Client.ts
import { S3Client } from &#39;@aws-sdk/client-s3&#39;;

export const s3Client = new S3Client({
  region: &#39;ap-northeast-2&#39;, // your region
  endpoint: `https://s3.ap-northeast-2.amazonaws.com`, // your endpoint
  credentials: {
    accessKeyId: {accessKeyId},
    secretAccessKey: {secretAccessKey},
  },
});

export const S3_BUCKET = {bucketName} as const;</code></pre>
<pre><code class="language-ts">// ./index.ts
import { ListObjectsCommand } from &#39;@aws-sdk/client-s3&#39;;
import { s3Client, S3_BUCKET } from &#39;./s3Client.js&#39;;

export const getKeysByBucket = async () =&gt; {
  try {
    const files = await s3Client.send(
      new ListObjectsCommand({ Bucket: S3_BUCKET }),
    );

    const keys = files.Contents.map((item) =&gt; item.Key);

    return keys; // [&#39;key1&#39;, &#39;key2&#39;, &#39;key3&#39;]
  } catch (err) {
    console.log(&#39;Error&#39;, err);
  }
};

export const getFileByKey = async (getBucketParams: GetObjectCommandInput) =&gt; {
  try {
    const data = await s3Client.send(new GetObjectCommand(getBucketParams));

    return data;
  } catch (err) {
    console.log(&#39;getFileByKey Error : &#39;, err);
  }
};
</code></pre>
<p>getKeysByBucket으로 버킷 내의 모든 키를 불러온 뒤 getFileByKey에 하나씩 넣어주면 된다.</p>
<h1 id="r2">R2</h1>
<pre><code class="language-ts">// ./r2Client.ts
import { S3Client } from &#39;@aws-sdk/client-s3&#39;;
import S3 from &#39;aws-sdk/clients/s3.js&#39;;

export const r2ClientV2 = new S3({
  endpoint: `https://{accountId}.r2.cloudflarestorage.com`,
  accessKeyId: {accessKeyId},
  secretAccessKey: {secretAccessKey},
  signatureVersion: &#39;v4&#39;,
});

export const r2ClientV3 = new S3Client({
  region: &#39;auto&#39;,
  endpoint: `https://{accountId}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: {accessKeyId},
    secretAccessKey: {secretAccessKey},
  },
});

export const R2_BUCKET = {bucketName} as const;
</code></pre>
<pre><code class="language-ts">// ./index.ts
import { r2ClientV2, r2ClientV3, R2_BUCKET } from &#39;./r2Client.js&#39;;

// R2의 버킷 CORS는 대쉬보드에서 설정이 불가능하기 때문에 여기서 설정해줘야된다.
export const putBucketCors = async () =&gt; {
  try {
    const data = await r2ClientV3.send(
      new PutBucketCorsCommand({
        Bucket: R2_BUCKET,
        CORSConfiguration: {
          CORSRules: [
            {
              AllowedHeaders: [&#39;*&#39;],
              AllowedMethods: [&#39;HEAD&#39;, &#39;GET&#39;, &#39;PUT&#39;, &#39;POST&#39;, &#39;DELETE&#39;],
              AllowedOrigins: [&#39;*&#39;],
              ExposeHeaders: [&#39;ETag&#39;],
            },
          ],
        },
      }),
    );
  } catch (err) {
    console.log(&#39;putBucketCors Error : &#39;, err);
  }
};

await putBucketCors();

let i = 1;

export const uploadFile = async (uploadBucketParams: PutObjectRequest) =&gt; {
  try {
    const fileName =
      uploadBucketParams.ContentDisposition.split(&#39;filename=&#39;)[1];

    const upload = r2ClientV2.upload(uploadBucketParams, (err, data) =&gt; {
      console.log(`${i} / ${keys.length} Complete!`);
      fs.appendFileSync(
        &#39;log.txt&#39;,
        `${fileName.padEnd(50, &#39; &#39;)} ${i++} / ${keys.length} Complete!\n`,
      );
    });

    // file upload progress, progress.total is not working
    upload.on(&#39;httpUploadProgress&#39;, (progress) =&gt; {
      console.log(
        `${fileName.padEnd(50, &#39; &#39;)} progress : ${
          (progress.loaded / uploadBucketParams.ContentLength) * 100
        }`,
      );
    });
  } catch (err) {
    console.log(&#39;uploadFile Error : &#39;, err);
  }
};</code></pre>
<p>먼저 Bucket의 CORS 설정을 해주기 위한 putBucketCors를 실행시킨다.
S3는 대쉬보드에서 만들면서 설정 가능하지만 R2에서는 API를 이용해서 설정해야한다.
여기선 굳이 v2를 사용할 이유는 없으니 v3를 사용했다.</p>
<p>uploadFile 함수로 PutObjectRequest를 받아 실제 업로드를 처리한다.</p>
<p>이쪽 시나리오에서는 버킷에 저장하는 파일의 키는 uuid로 저장하고 ContentDisposition에 filename= 뒤에 원본 파일 이름이 들어있었기 때문에 fileName을 저렇게 설정했다.</p>
<p>업로드 진행도를 확인하기 위해 AWS.S3.upload (v2)를 사용했다. 
upload 함수의 콜백으로 어떤 파일이 완료되었고 n개중 i번째인지를 로그 파일로 추출한다.
만약 파일 이름을 버킷에 들어가는 파일 키 그 자체로 지정하고 있다면 fileName 대신에 이 함수의 콜백 인자에서 data.Key를 이용해 추출하면 된다.</p>
<p>upload.on을 이용해 progress를 추출한다.
는 progress.loaded는 다행히 정상적으로 제공해주지만 progress.total은 undefined로 넘어오기 때문에 param으로 줬던 ContentLength를 사용한다.</p>
<p>이제 필요한 함수는 모두 구현했다.</p>
<h1 id="전체-코드">전체 코드</h1>
<pre><code class="language-ts">// ./index.ts
import {
  GetObjectCommand,
  GetObjectCommandInput,
  ListObjectsCommand,
  PutBucketCorsCommand,
} from &#39;@aws-sdk/client-s3&#39;;
import { s3Client, S3_BUCKET } from &#39;./s3Client.js&#39;;
import { PutObjectRequest } from &#39;aws-sdk/clients/s3.js&#39;;
import { r2ClientV2, r2ClientV3, R2_BUCKET } from &#39;./r2Client.js&#39;;
import fs from &#39;fs&#39;;

// run this once
// export const putBucketCors = async () =&gt; {
//   try {
//     const data = await r2ClientV3.send(
//       new PutBucketCorsCommand({
//         Bucket: R2_BUCKET,
//         CORSConfiguration: {
//           CORSRules: [
//             {
//               AllowedHeaders: [&#39;*&#39;],
//               AllowedMethods: [&#39;HEAD&#39;, &#39;GET&#39;, &#39;PUT&#39;, &#39;POST&#39;, &#39;DELETE&#39;],
//               AllowedOrigins: [&#39;*&#39;],
//               ExposeHeaders: [&#39;ETag&#39;],
//             },
//           ],
//         },
//       }),
//     );
//   } catch (err) {
//     console.log(&#39;Error&#39;, err);
//   }
// };

// putBucketCors();

export const getKeysByBucket = async () =&gt; {
  try {
    const files = await s3Client.send(
      new ListObjectsCommand({ Bucket: S3_BUCKET }),
    );

    const keys = files.Contents.map((item) =&gt; item.Key);

    return keys;
  } catch (err) {
    console.log(&#39;getKeysByBucket Error : &#39;, err);
  }
};

export const getFileByKey = async (getBucketParams: GetObjectCommandInput) =&gt; {
  try {
    const data = await s3Client.send(new GetObjectCommand(getBucketParams));

    return data;
  } catch (err) {
    console.log(&#39;getFileByKey Error : &#39;, err);
  }
};

export const uploadFile = async (uploadBucketParams: PutObjectRequest) =&gt; {
  try {
    const fileName =
      uploadBucketParams.ContentDisposition.split(&#39;filename=&#39;)[1];

    const upload = r2ClientV2.upload(uploadBucketParams, (err, data) =&gt; {
      console.log(`${i} / ${keys.length} Complete!`);
      fs.appendFileSync(
        &#39;log.txt&#39;,
        `${fileName.padEnd(50, &#39; &#39;)} ${i++} / ${keys.length} Complete!\n`,
      );
    });

    // file upload progress, progress.total is not working
    upload.on(&#39;httpUploadProgress&#39;, (progress) =&gt; {
      console.log(
        `${fileName.padEnd(50, &#39; &#39;)} progress : ${
          (progress.loaded / uploadBucketParams.ContentLength) * 100
        }`,
      );
    });
  } catch (err) {
    console.log(&#39;uploadFile Error : &#39;, err);
  }
};

const keys = await getKeysByBucket();

let i = 1;

keys.forEach(async (Key) =&gt; {
  const getBucketParams = {
    Bucket: S3_BUCKET,
    Key,
  };

  const { ContentLength, ContentDisposition, Body, ContentType } =
    await getFileByKey(getBucketParams);

  const uploadBucketParams: PutObjectRequest = {
    Bucket: R2_BUCKET,
    Key,
    ContentLength,
    ContentDisposition,
    Body,
    ContentType,
  };

  uploadFile(uploadBucketParams);
});
</code></pre>
<h1 id="알려진-에러">알려진 에러</h1>
<blockquote>
<p> A client error (RequestTimeTooSkewed) occurred when calling the ListObjects operation: The difference between the request time and the current time is too large.</p>
</blockquote>
<p>50개의 파일을 병렬로 업로드하던 중 이 에러가 발생했으며 원인은 적혀있듯이 요청을 보낸 시간과 현재 시간의 차이가 15분(900000ms)보다 커지면 발생한다.
50개의 업로드 요청을 한번에 병렬로 보냈지만 인터넷 문제, 최대 업로드 처리가 가능한 파일 수 문제 등이 원인일 것이라 예상했으며 병렬처리로 보낼 파일의 갯수를 적당히 조절하는 것으로 해결했다.</p>
<hr>
<blockquote>
<p>MalformedXML: The XML you provided was not well formed or did not validate against our published schema.</p>
</blockquote>
<p>사실 업로드 자체는 v3의 putObjectCommand를 사용하면 말끔히 할 수 있지만 이쪽 시나리오에서는 업로드 진행도를 클라이언트에게 보내야했고 R2에서 v3의 upload는 요청이 제대로 이루어지지 않아 400코드를 뱉으며 업로드에 실패했다.
R2에선 아직 S3 API를 전부 완벽히 구현해내지 못했으며 특히 v3의 upload는 작동을 아예 안했고 v2의 upload에선 upload.on에서 주는 progress.total이 작동하지 않았다.
추후엔 사라질 문제일 것이라고 생각되지만 현시점의 해결 방법을 기록하기 위해 이 글을 작성했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[PostgreSQL] psql + pgadmin Docker-Compose.yaml]]></title>
            <link>https://velog.io/@luna_runa/PostgreSQL-psql-pgadmin-Docker-Compose.yaml</link>
            <guid>https://velog.io/@luna_runa/PostgreSQL-psql-pgadmin-Docker-Compose.yaml</guid>
            <pubDate>Tue, 11 Oct 2022 12:01:28 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-yaml">version: &#39;3.8&#39;
services:
  postgres:
    container_name: postgres
    image: postgres
    restart: always
    environment:
      POSTGRES_USER: 
      POSTGRES_PASSWORD: 
      POSTGRES_DB: 
    ports:
      - &#39;5432:5432&#39;

  pgadmin:
    container_name: pgadmin
    image: dpage/pgadmin4
    restart: always
    environment:
      PGADMIN_DEFAULT_EMAIL: 
      PGADMIN_DEFAULT_PASSWORD: 
    ports:
      - &#39;8088:80&#39;
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[NestJS] NestJS에서 WebSocket 통신(@nestjs/websockets)에 대해]]></title>
            <link>https://velog.io/@luna_runa/NestJS-Socket-%ED%86%B5%EC%8B%A0nestjswebsockets%EC%97%90-%EB%8C%80%ED%95%B4</link>
            <guid>https://velog.io/@luna_runa/NestJS-Socket-%ED%86%B5%EC%8B%A0nestjswebsockets%EC%97%90-%EB%8C%80%ED%95%B4</guid>
            <pubDate>Mon, 10 Oct 2022 22:31:13 GMT</pubDate>
            <description><![CDATA[<p>사이드 프로젝트에서 채팅방 기능을 위해 Socket을 사용한다.
우선 NestJS에선 소켓을 쓰기위해 대표적으로 ws와 Socket.io를 사용해서 구현할 수 있다.
이중 레퍼런스도 많고 한층 더 추상화? 되었다고 생각되는 Socket.io를 사용하기로 했다.</p>
<pre><code class="language-ts">import { Logger } from &#39;@nestjs/common&#39;
import { MessageBody, SubscribeMessage, WebSocketGateway, WebSocketServer, WsResponse } from &#39;@nestjs/websockets&#39;
import { Server } from &#39;socket.io&#39;

interface loginMessage {
  user: string
}

interface roomMessage extends loginMessage {
  message: string
}

@WebSocketGateway(8080, { transports: [&#39;websocket&#39;] })
export class EventGateway {
  @WebSocketServer()
  server: Server
  logger = new Logger()

  @SubscribeMessage(&#39;ClientToServer&#39;)
  handleClientToServer(@MessageBody() { user }: loginMessage) {
    this.logger.log(`Login: ${user}`)
    this.server.emit(&#39;ServerToClient&#39;, `로그인 환영: ${user}`)
  }

  @SubscribeMessage(&#39;Room1&#39;)
  handleRoom1(@MessageBody() { user, message }: roomMessage): WsResponse {
    this.logger.log(`Client: ${user} Message: ${message}`)
    this.server.emit(`${user}&#39;s AlertHandle`, &#39;new!&#39;)
    // this.server.emit(&#39;Room1&#39;, `${user}: ${message}`) // Room1을 구독하고 있는 전체에게 emit (broadcast)
    // return `${user}: ${message}` // Room1으로 요청을 보낸 특정 클라이언트에게만 Room1으로 emit
    return { event: &#39;Room1All&#39;, data: `${user}: ${message}` } // event로 emit
  }
}</code></pre>
<p>서버측에서 클라이언트로 다시 메시지를 전송하는 방법으로 3가지가 있다.</p>
<h3 id="1-thisserveremitevent-data">1. this.server.emit(&#39;event&#39;, data)</h3>
<p>Socket.io에서 기본적으로 제공해주는 방식으로 event로 emit을 날려 event를 Listen(on) 하고있는 모든 클라이언트에게 메시지를 보낸다.
<img src="https://velog.velcdn.com/images/luna_runa/post/1c045424-7eea-439e-adaf-671d1c127714/image.png" alt="">
<img src="https://velog.velcdn.com/images/luna_runa/post/f3668a19-6b71-411f-9c05-7cadafa422be/image.png" alt="">
정상적으로 해당 방에 존재하는(on 하고 있는) 모든 사람에게 메시지를 전송했다.</p>
<h3 id="2-return-data">2. return data</h3>
<p>NestJS에서 Gateway로 만든 함수에선 return을 이용해서도 메시지를 전송할 수 있었다.
여기서 return으로 처리가 가능하려면 Client측에서도 콜백함수를 열어줘야한다.</p>
<pre><code>// client code
                                        //이쪽으로 return이 넘어오는거!!
socket.emit(&#39;events&#39;, { name: &#39;Nest&#39; }, (data) =&gt; console.log(data));
</code></pre><p><img src="https://velog.velcdn.com/images/luna_runa/post/f8ce6a56-7ef6-42e6-a575-0842f4bd47d6/image.png" alt="">
<img src="https://velog.velcdn.com/images/luna_runa/post/799a2270-76ca-4306-b0bb-0f469d9d5fb4/image.png" alt="">
그저 클라이언트 측에서 보낸 emit에 대한 응답을 전송하며 이 응답은 클라이언트에서 사용한 emit의 콜백 함수로 return된다. 따로 특정 이벤트에 emit을 날리진 않았다.</p>
<h3 id="3-return--event-data--multiple-responses">3. return { event, data } (Multiple Responses?)</h3>
<p>뭔가 return으로도 특정 이벤트에 대한 응답을 보낼 수 있도록 Nestjs쪽에서 제공해주지 않을까? 하고 찾아다니다 공식 문서에 있는 Multiple Responses를 찾게 된다.
<img src="https://velog.velcdn.com/images/luna_runa/post/3918f09b-ee4d-4c21-8f7b-d3e3d34bfcfc/image.png" alt=""></p>
<p>특정 event를 따로 지정할 수 있다는 점으로 2번과는 다르긴 하다.
하지만 결국 이 방법도 모든 클라이언트에게 emit을 보내진 않았고 요청을 보낸 클라이언트에게만 emit이 전송되는 것을 확인했다.
공식 문서의 설명을 봤을땐 뭔가를 Multiple하게 보낸다는 것 같은데... 달라지는 점은 event를 재정의해서 보낼 수 있다는 점 밖에 찾지 못했다.
<img src="https://velog.velcdn.com/images/luna_runa/post/30535d86-cc97-421a-9c61-4d9223539877/image.png" alt="">
<img src="https://velog.velcdn.com/images/luna_runa/post/ac066126-425c-4aa7-b557-726c1f7d4de5/image.png" alt="">
전혀 All이 아닌 모습... 분명 Multiple Responses라고 적은 것에는 이유가 있을 것 같은데 뭔지 찾지 못했다..</p>
<hr>
<p>결론은 return을 쓰는게 아니라 server.emit(boradcast)을 사용해서 Socket.io 객체의 메소드를 사용하는 방법으로 채팅방에 있는 모두에게 메시지를 전송시키도록 구현하게 될 것 같다.</p>
<p>return을 사용하는 방식은 event를 다시 적지 않아도 되고 간편하고 return type도 확인할 수 있었지만 요청을 보내왔던 클라이언트에게만 emit을 보내기 때문에 1:1 통신 시나리오에서만 사용할 수 있을 것 같다.
분명 다른 방법이 있을 것만 같지만 찾지 못했다... Multiple Response라고 명명한 의미를 잘 찾지 못해 아쉬웠다. 콜백에 대한 응답 + 특정 이벤트에 대한 응답을 보낼 수 있는 것을 두고 Multiple이라고 칭한 것일까..?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[일상] 입사 3달차 회고록]]></title>
            <link>https://velog.io/@luna_runa/%EC%9D%BC%EC%83%81-%EC%9E%85%EC%82%AC-3%EB%8B%AC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@luna_runa/%EC%9D%BC%EC%83%81-%EC%9E%85%EC%82%AC-3%EB%8B%AC%EC%B0%A8-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Thu, 06 Oct 2022 13:06:33 GMT</pubDate>
            <description><![CDATA[<p>취직을 하게 된지 약 3달정도 지났다.</p>
<p>어쩌다보니 입사 초기부터 사내에 있던 Angular/Flask 웹 사이트를 React/Nestjs로 마이그레이션 하는 작업을 맡게되었다.
이유는 프론트/백 언어 통합(TS)으로 한가지 언어만으로도 둘 다 관리할 수 있도록 접근성 향상과 비용 절감도 있었지만 역시 제일 큰 이유론 레거시 코드가 정말 정말 정말 엉망이었다... 의존성도 다 꼬여있으며 뷰 로직과 핵심 비즈니스 로직, 라우팅, DB와 통신/조작 등 수많은 로직을 하나의 함수에서 모조리 처리하고 있었다....</p>
<p>나름 코딩은 어렸을 때부터 했다고 생각했지만 실무에 들어오고보니 4달 전까지도 웹개발 프레임워크/라이브러리를 &quot;Hello World!&quot; 수준밖에 하지 못했던 것 같다.</p>
<p>지금은 React로 재사용성을 고려하며 컴포넌트를 만드는 법을 알게 되었고 지겹도록 배웠던 OOP가 왜 존재하게 되었고 이걸 지키는게 왜 중요한지 뼈저리게 느끼게 되었다.</p>
<p>내가 입사하면서부터 시작하게 된 사내 스터디로 클린 코드/아키텍처를 공부하고 반면교사로 레거시 코드를 보며 왜 다른 사람들이 보기 쉽게 코딩을 해야하는지 깨달고 직접 체험하면서 &#39;여태 나는 반도 못지켰구나. 내가 짠 코드들은 유지보수가 불가능했구나.&#39; 라는 사실을 새삼 깨달았다.
나름대로 가독성 좋게, 다른 사람도 이해할 수 있게 짰다고 생각했지만 현실은 너무나도 달랐던 것이었다.
그리고 다양한 아키텍처, 디자인 패턴을 공부하며 제대로 지키지 못했을 때 오는 문제점들을 스스로 겪으면서 반성도 많이 하게됐다..
그저 구현만 잘 하면 되지 라고 생각했었는데 실무는 너무나도 달랐고 진짜 개발자는 뭔지, 좋은 설계는 어떤건지 많이 고민하고 배워가고 있는 것 같다.</p>
<p>3개월만에 굉장히 많은 경험들을 하게 되었고 작업을 위해 알고 있었어야 할, 앞으로도 더 잘 알아야 할 지식들이 굉장히 많아졌다.</p>
<p>2주 전에 겨우 React 마이그레이션 작업이 1차적으로 완료되었고 이번주는 레거시 백 코드를 읽으며 어떤 기능들을 가져갈지 정리하며 DB 설계를 들어갔다.
백엔드에선 정말 제일 밑바닥 설계부터 들어가야되니 기대 반 걱정 반이다.
클린 아키텍처를 기반으로 한 설계를 진행할 것이며 가능한 테스트도 작성해보고 CD/CI도 구현해 볼 것 같다. 기간 내로 다 할 수 있을지 모르겠지만 정말 다양한 부분들을 실무로 접해볼 수 있을 것 같아 기대가 크다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript] 제네릭 타입 메모]]></title>
            <link>https://velog.io/@luna_runa/TypeScript-%EC%A0%9C%EB%84%A4%EB%A6%AD-%ED%83%80%EC%9E%85-%EB%A9%94%EB%AA%A8</link>
            <guid>https://velog.io/@luna_runa/TypeScript-%EC%A0%9C%EB%84%A4%EB%A6%AD-%ED%83%80%EC%9E%85-%EB%A9%94%EB%AA%A8</guid>
            <pubDate>Tue, 13 Sep 2022 01:24:25 GMT</pubDate>
            <description><![CDATA[<p>T extends CommonTable : T는 CommonTable의 확장.
  =&gt; T는 CommonTable <strong>이거나(or)</strong>, CommonTable의 <strong>확장</strong>일 수 있다.</p>
<blockquote>
<p>&#39;SubmitHandler<T>&#39; 형식의 인수는 &#39;SubmitHandler<CommonTable>&#39; 형식의 매개 변수에 할당될 수 없습니다.
  &#39;CommonTable&#39; 형식은 &#39;T&#39; 형식에 할당할 수 없습니다.
    &#39;CommonTable&#39;은(는) &#39;T&#39; 형식의 제약 조건에 할당할 수 있지만, &#39;T&#39;은(는) &#39;CommonTable&#39; 제약 조건의 다른 하위 형식으로 인스턴스화할 수 있습니다. ts(2345)</p>
</blockquote>
<p>여기서 T는 extends CommonTable을 했기 때문에 들어갈 수 있다고 생각할 수도 있다.
  CommonTable과 완전히 일치한다면 들어갈 수 있겠지만 확장인 경우 CommonTable에 속하지 않아 들어갈 수 없다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[react-hook-form] MUI에서 react-hook-form & Redux-ToolKit을 이용해 데이터 관리하기]]></title>
            <link>https://velog.io/@luna_runa/react-hook-form-MUI%EC%97%90%EC%84%9C-react-hook-form-Redux-ToolKit%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@luna_runa/react-hook-form-MUI%EC%97%90%EC%84%9C-react-hook-form-Redux-ToolKit%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 24 Aug 2022 11:13:05 GMT</pubDate>
            <description><![CDATA[<p>여러 외부 라이브러리를 사용하다보니 어느부분에 어떻게 사용해야 하는지 헷갈려서 정리하기로 했다.
예시로 들 프로그램은 MUI로 구성된 폼을 react-hook-form을(이하 RFH) 사용해서 유저가 입력한 데이터를 Redux(ToolKit)로 저장하는 간단하지만 여러 외부 라이브러리가 섞여 복잡해진 로직이다.</p>
<h3 id="mui--rtk">MUI &amp; RTK</h3>
<p>기본 뼈대가 되는 UI작업을 MUI을 사용해서 하기로 했다.</p>
<pre><code class="language-tsx"> &lt;Dialog open={open} onClose={onClose}&gt;
     ...
                  &lt;TextField label=&quot;이름&quot; value={name} /&gt;
                  &lt;FormControlLabel
                    label=&quot;접근 제어&quot;
                    control={&lt;Checkbox checked={!!accessControl} onChange={handleCheck} /&gt;}
                  /&gt;

              &lt;Button sx={{ width: 100 }} color=&quot;primary&quot; variant=&quot;contained&quot;&gt;
                {Strings.contents.TEXT_CONTENTS_REGISTRY_BUTTON}
              &lt;/Button&gt;
     ...
 &lt;/Dialog&gt;</code></pre>
<p>여기서 실제 데이터인 name과 accessControl은 Redux store를 사용해 관리하고 있다. (useSelector)
handleCheck로 accessControl을 관리한다. (useDispatch)</p>
<p>이제 이 폼에서 버튼을 누르면 TextField와 Checkbox에 있는 값을 불러와 원래 값에 dispatch를 해야하는데 이를 처리하는 것을 RHF을 사용해 하려한다.</p>
<h3 id="mui--rtk--rhf">MUI &amp; RTK + RHF</h3>
<p>RHF는 기본적으로 직접 제어되지 않는 요소에 사용되지만 직접 제어를 하는 외부 UI 라이브러리와 같이 사용하기 위해선 <a href="https://react-hook-form.com/api/usecontroller/controller">Controller</a>를 사용하면 된다.
라고 써있지만 실제로 찾아보면 이걸 도대체 어떻게 써야하는지 감이 잘 안오고 어떻게 써보더라도 값이 제대로 넘겨지거나 수정되지 않는 현상을 볼 수 있다.</p>
<pre><code class="language-ts">  const dispatch = useDispatch&lt;AppDispatch&gt;()
  const categoryDetail = useSelector((state: RootState) =&gt; state.categoryDetail.data)
  const categoriesViewList = useSelector((state: RootState) =&gt; state.categoryView.data)
  const [open, setOpen] = useState(false)
  const { control, handleSubmit, reset } = useForm&lt;Tables&gt;()

  useEffect(() =&gt; {
    if (categoryDetail.id !== UNSET) {
      setOpen(true)
    }
  }, [categoryDetail, categoriesViewList])

  //중요!!!
  useEffect(() =&gt; {
    reset(categoryDetail)
  }, [categoryDetail])

  const onClose = () =&gt; {
    setOpen(false)
    dispatch(setCategory(initialState.data))
  }

  const onSubmit: SubmitHandler&lt;Tables&gt; = data =&gt; {
    ...
    //Controller의 name=&quot;&quot;부분
    const { name, accessControl } = data
    ...
  }</code></pre>
<p>여기서 useEffect()에 있는 reset(data) 부분에 (RTK에서 받아온)실제 데이터를 넣어줘야 RHF에서도 해당 데이터를 사용할 수 있다.
RHF 내부에서 사용하는 상태에 Redux 값을 복사해주는 핵심 부분이며 Redux의 값이 변할때마다 다시 초기화해줘야 하니 의존성에도 잊지말고 넣어주자.
사실 이부분을 넣어주지 않아서 계속 리덕스 스토어에 있는 값과 RHF에 있는 값이 따로놀아서 n시간동안 문제였다...</p>
<ol>
<li>useForm 안에 따로 저장소가 있으며 useForm() 안에 인수로 기본값 처리 가능.</li>
<li>reset(데이터)로 RHF의 저장소에 직접 저장 가능.</li>
<li>render의 콜백에서 받아온 field.value 안에 해당 데이터가 존재하며 접근 가능.</li>
<li>render 안에서 field.onChange를 사용하면 field.value의 값 변경 이벤트를 지정할 수 있으며 기본적으로 제공되는 이벤트가 존재(이건 MUI쪽에서 받아오는 거 같은데 정확하진 않다.)   </li>
</ol>
<pre><code class="language-tsx">&lt;Dialog open={open} onClose={onClose}&gt;
      ...
                  &lt;Controller
                    name=&quot;name&quot;
                    control={control}
                    defaultValue={name}
                    render={({ field }) =&gt; &lt;TextField {...field} /&gt;}
                  /&gt;
                  &lt;Controller
                    name=&quot;accessControl&quot;
                    control={control}
                    render={() =&gt; (
                      &lt;FormControlLabel
                        control={&lt;Checkbox checked={!!accessControl} onChange={handleCheck} /&gt;}
                        label=&quot;접근 제어&quot;
                      /&gt;
                    )}
                  /&gt;

              &lt;Button
                type=&quot;submit&quot;
                sx={{ width: 100 }}
                color=&quot;primary&quot;
                variant=&quot;contained&quot;
                onClick={handleSubmit(onSubmit)}
              &gt;
               ...
&lt;/Dialog&gt;</code></pre>
<pre><code class="language-tsx">&lt;Controller
  name=&quot;name&quot;
  control={control}
  defaultValue={name}
  render={({ field }) =&gt; &lt;TextField {...field} /&gt;}
  /&gt;</code></pre>
<p>위와 같이 Controller에서 RHF의 onSubmit 함수에서 인수로 받아와 처리할 변수 이름을 name=에 집어넣고 control=에 useForm에서 받아온 control을 넣어준다.
<br></p>
<pre><code class="language-tsx">render={({ field }) =&gt; &lt;TextField {...field} /&gt;}</code></pre>
<p>TextField의 경우 dispatch를 사용하지 않으니 체인지 이벤트에 관해서 기본적으로 제공해주는 onChange 이벤트(handleCheck)로 값을 수정하면 된다.
<br></p>
<pre><code class="language-tsx">render={() =&gt; (
  &lt;FormControlLabel
    control={&lt;Checkbox checked={!!accessControl} onChange={handleCheck} /&gt;}
    label=&quot;접근 제어&quot;
    /&gt;
)}</code></pre>
<p>하지만 Checkbox의 경우 눌렀을때 값이 변하는 이벤트를 dispatch로 보내줘야 했기 때문에 onChange에 dispatch 이벤트를 넣어줘야 한다.</p>
<hr>
<p>MUI에서도 기본적인 컨트롤을 제공해주고 RHF에서도 따로 컨트롤이 있었으며 Redux에 저장되어있는 값으로 기본값을 넘겨줘야 했기 때문에 여러 라이브러리의 공식문서를 다 참고해야 했다.
사실 크게 어려운부분도 아니었던 거 같은데 라이브러리가 어떻게 동작하는지, 어떻게 사용해야하는지 몰라서 시간을 굉장히 잡아먹었던 거 같다. 아직도 완전히 알진 못하는 거 같고 역시 외부라이브러리를 사용하는데엔 장점과 단점이 있는 거 같다.</p>
<hr>
<p>나중에 다시 보니 정말 개판이다... 리덕스에서 받아온 값은 useForm의 초기에만 기본값으로 사용하기 위해 넘겨주면 되며 이후 변경사항은 submit을 성공적으로 수행하면 서버에 데이터가 실제로 변했을 것이기 때문에 다시 서버에서 업데이트를 받으면 됐을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ESLint] no-unused-prop-types 에러]]></title>
            <link>https://velog.io/@luna_runa/ESLint-no-unused-prop-types-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@luna_runa/ESLint-no-unused-prop-types-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Thu, 18 Aug 2022 06:06:30 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>&#39;onPageChange&#39; PropType is defined but prop is never used
eslint(react/no-unused-prop-types)</p>
</blockquote>
<pre><code class="language-typescript">export interface TableFooterProps {
  tableList: CategoryView[]
  page: number
  rowsPerPage: number
  //이하 세개의 함수에서 에러
  onPageChange: (page: number) =&gt; void
  onRowsPerPageChange: (page: number) =&gt; void
  setEmptyRows: (page: number) =&gt; void
}

export default function TableListFooter(props: TableFooterProps) {
  const { tableList, page, rowsPerPage } = props
  const {
    TablePaginationActionsComponent,
    handleChangePage,
    handleChangeRowsPerPage,
  } = useTableFooter({ ...props, count: tableList.length })

  return (
    ...
  )
}</code></pre>
<p>React-TS 컴포넌트에서 프롭스로 받아오는 함수를 사용하지 않으면 해당 에러가 발생한다.
이 코드에선 프롭스로 넘겨받은 함수를 또다시 다른 함수에 프롭스로 넘겨주는데 구조 분해 할당을 사용해서 넘겨줬더니 현재 함수에선 한번도 사용하지 않았다고 불평하는 것이었다.
이를 해결하기 위한 방법으론 여러가지가 있었다.</p>
<h3 id="props를-직접-하나하나-할당해주기">props를 직접 하나하나 할당해주기</h3>
<pre><code class="language-ts">export default function TableListFooter({
  tableList,
  page,
  rowsPerPage,
  onPageChange,
  onRowsPerPageChange,
  setEmptyRows,
}: TableFooterProps) {
  const {
    TablePaginationActionsComponent,
    handleChangePage,
    handleChangeRowsPerPage,
  } = useTableFooter(
    tableList,
    page,
    rowsPerPage,
    onPageChange,
    onRowsPerPageChange,
    setEmptyRows,
  )</code></pre>
<p>이렇게 하나하나 직접 명시해주면 어찌됐든 사용은 했기때문에 위의 에러도 사라지고 올바르게 동작할 수 있다. 하지만 코드가 매우 길어지고 더러워진다.
사실 원래 코드의 상태는 이와 같았으며 이런 현상을 막기 위해서 props를 일일이 푸는 대신에 묶어서 일부분만 사용하는 방법을 찾는 중이었고 그렇기때문에 이 방법으로 돌아가고 싶진 않았다.</p>
<h3 id="eslint-설정에서-끄기">eslint 설정에서 끄기</h3>
<pre><code class="language-json">...
&quot;rules&quot;: {
  ...
  &quot;react/no-unused-prop-types&quot;: [&quot;off&quot;],
  ...
}
...</code></pre>
<p>제일 간단한 해결방법이지만 잠재적 문제점이 발생할 수도 있다고 생각했기 때문에 내키지 않았다.</p>
<h3 id="✅-사용하지-않는-함수를-할당만-하고-넘기기">✅ 사용하지 않는 함수를 할당만 하고 넘기기</h3>
<pre><code class="language-ts">export default function TableListFooter(props: TableFooterProps) {
  const { tableList, page, rowsPerPage, ..._ } = props
  const {
    TablePaginationActionsComponent,
    handleChangePage,
    handleChangeRowsPerPage,
  } = useTableFooter(props)</code></pre>
<p>현재는 이 방법을 선택했다. Go 등의 언어에선 사용하지 않는 변수를 _로 처리하기도 하고 이 방법 외엔 가독성이나 안정성면에서 깔끔하지 못하다고 생각했다.
(지역변수) _가 선언되었지만 사용하지 않았다는 워닝이 발생하긴 하지만 이 선에서 타협하기로 했다.</p>
<hr>
<p>구현만 하는 것보다 코드를 얼마나 보기 좋게 만드냐를 고민하는 시간이 훨씬 더 많아졌으며 이런 일을 줄이기 위해 처음부터 설계나 규칙을 얼마나 잘 맞춰야 하는지 깨달았다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[EC2] zsh 설치 후 npm 명령어]]></title>
            <link>https://velog.io/@luna_runa/EC2-zsh-%EC%84%A4%EC%B9%98-%ED%9B%84-npm-%EB%AA%85%EB%A0%B9%EC%96%B4</link>
            <guid>https://velog.io/@luna_runa/EC2-zsh-%EC%84%A4%EC%B9%98-%ED%9B%84-npm-%EB%AA%85%EB%A0%B9%EC%96%B4</guid>
            <pubDate>Thu, 04 Aug 2022 02:35:51 GMT</pubDate>
            <description><![CDATA[<p>EC2에서 기본 터미널인 bash를 쓰다가 맥에서 쓰던 터미널이 그리워져서 zsh, oh my zsh 설치 후 세팅을 완료했다.
하지만 bash에선 되던 npm 명령어가 zsh에선 먹히지 않는 것을 알게되고 환경변수 세팅이 덜됐음을 확인했다.</p>
<p>vi ~/.zshrc
~/.bashrc에서 값을 가져와 bashrc -&gt; zshrc 해줬다.</p>
<pre><code>if [ -f /etc/zshrc ]; then
        . /etc/zshrc
fi

export NVM_DIR=&quot;$HOME/.nvm&quot;
[ -s &quot;$NVM_DIR/nvm.sh&quot; ] &amp;&amp; \. &quot;$NVM_DIR/nvm.sh&quot;  # This loads nvm
[ -s &quot;$NVM_DIR/bash_completion&quot; ] &amp;&amp; \. &quot;$NVM_DIR/bash_completion&quot;  # This loads nvm bash_completion</code></pre><p>source ~/.zshrc 후 정상적으로 npm이 작동되는 것을 확인했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] 실제 서비스 AWS에 배포하기 (메모장)]]></title>
            <link>https://velog.io/@luna_runa/Traefik-Traefik-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-with-docker-compose</link>
            <guid>https://velog.io/@luna_runa/Traefik-Traefik-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-with-docker-compose</guid>
            <pubDate>Mon, 01 Aug 2022 03:06:15 GMT</pubDate>
            <description><![CDATA[<p>연습 겸 실제 서비스를 AWS에 배포해봤으며 그 과정을 메모 겸 간략하게 정리했다.</p>
<h1 id="로컬">로컬</h1>
<p>큰 순서 틀</p>
<ul>
<li><p>db 올리기 (DB 접속하기 : psql -U 유저이름 -d DB이름)</p>
</li>
<li><p>init 올리고 컨테이너 들어가서 Flask CLI로 DB 설정해주기 (Readme.md)</p>
<ul>
<li>flask manager add (유저이름) (패스워드) = 로그인페이지 admin</li>
</ul>
</li>
<li><p>프론트 백 Traefik 설정해주고 올리기</p>
<ul>
<li>docker-compose.yaml 수정<pre><code class="language-yaml">labels:
    - traefik.frontend.rule=Host:요청을 받을 url (CNAME)
    - traefik.port=요청 url에 대한 포트
    - traefik.enable=true
    - traefik.frontend.entryPoints=http (여기선 http)
    #- traefik.backend=~~ 없어도 동작. 이름 명명</code></pre>
</li>
<li>traefik.toml 수정<pre><code class="language-toml">logLevel = &quot;INFO&quot;
[api]
  entryPoint = &quot;dashboard&quot;
  dashboard = true
  debug = true
</code></pre>
</li>
</ul>
<p>[entryPoints]</p>
<pre><code>[entryPoints.http]
    address = &quot;:80&quot;
[entryPoint.dashboard]
    address = &quot;:8080&quot;</code></pre><p>[docker]</p>
<pre><code>endpoint = &quot;unix:///var/run/docker.sock&quot;
exposedByDefault = true
usebindportip = true
domain = 메인 도메인 (A)</code></pre><pre><code></code></pre></li>
</ul>
<hr>
<h1 id="aws">AWS</h1>
<ul>
<li><p>db 올리기 -&gt; RDS 설정</p>
<ul>
<li><p>dev.toml</p>
<pre><code>...
[database]
connector = &#39;postgresql&#39;
host = &#39;&#39; #RDS 호스트
port = 5432
user = &#39;&#39; #RDS에서 적은거
password = &#39;&#39; #RDS에서 적은거
name = &#39;&#39;
...</code></pre></li>
<li><p>Postgres DB 리커버리</p>
<p>recovery.sh</p>
<pre><code>#!/bin/bash
</code></pre></li>
</ul>
<p>cd dbcommand</p>
<p>USER=
PASSWORD=
HOST_URL=
DB=
DB_HOST=postgresql://${USER}:${PASSWORD}@${HOST_URL}/${DB}
BACKUP_FILE=~.sql
psql -f ${BACKUP_FILE} ${DB_HOST}</p>
<p>cd ..
```</p>
</li>
<li><p>init 올리고 컨테이너 들어가서 Flask CLI로 DB 설정해주기 (Readme.md)</p>
<ul>
<li>flask db upgrade, flask manager add &#39;&#39; &#39;&#39; 까지만 (위에 리커버리에서 나머지 수행)</li>
</ul>
</li>
<li><p>S3 &amp; cloudfront 설정</p>
<ul>
<li>environment에 APIHost/cloudfront 설정, angular.json에 추가</li>
<li>dev.toml<pre><code>...
[asset]
host = &#39;&#39; #cloudfront
...</code></pre></li>
</ul>
</li>
<li><p>프론트 백 Traefik 설정해주고 이미지 빌드하기 -&gt; ECR 생성하고 ECS에 올리기</p>
<ul>
<li>프론트 prod 빌드 (./front-build build-prod)<pre><code>#!/bin/bash
</code></pre></li>
</ul>
<p>cd cms-front</p>
<p>if [ &quot;${1}&quot; == &quot;build-prod&quot; ]
then</p>
<pre><code>ng build -c ...-prod;</code></pre><p>elif [ &quot;${1}&quot; == &quot;build-dev&quot; ]
then</p>
<pre><code>ng build -c ...-develop;</code></pre><p>elif [ &quot;${1}&quot; == &quot;serve&quot; ]
then</p>
<pre><code>ng serve -c ...-develop;</code></pre><p>fi</p>
<p>cd ..</p>
<pre><code>- ECR 생성
  - AWS CLI 설치 : https://docs.aws.amazon.com/ko_kr/cli/latest/userguide/getting-started-install.html
  - AWS CLI configure 설정하기
  - .env 수정하기</code></pre><p>  ECR_HOST=
  REPOSITORY_PREFIX=
  FRONT=front:1.0.1
  BACK=back:1.0.0
  REGION=ap-northeast-2</p>
<pre><code>- docker-compose.yaml 수정
  - traefik, depends_on 설정 제거 (ECS에서 추가)
  - redis, worker 등 로컬에서 빌드가 필요하지 않은 부분 제거 (ECS에서 추가)
  ``` yaml
  version: &#39;3.2&#39;

  volumes:
    media-persist: {}
    redis-persist: {}

  services:
    cms-back:
      image: ${ECR_HOST}/${REPOSITORY_PREFIX}${BACK}
      build: ./cms-back
      container_name: cms-back
      volumes:
        - media-persist:/tmp
        - ./config:/etc/app:ro 
      ports:
        - &quot;5000:5000&quot;
      environment:
        - FLASK_APP=application/bootstrap.py
        - FLASK_ENV=development
        - CONFIG_FILE=/etc/app/service.toml

    cms-front:
      image: ${ECR_HOST}/${REPOSITORY_PREFIX}${FRONT}
      build: ./cms-front
      container_name: cms-front
      ports:
        - &quot;8001:80&quot;</code></pre><ul>
<li><p>ECR에 이미지 푸시 ECRPush.sh (호스트에서 AWS CLI을 사용해 docker-compose push)</p>
<pre><code>  #!/bin/bash
source .env;

if [ &quot;${1}&quot; == &quot;init&quot; ]
then 
    docker-compose -f docker-compose-init.yaml up --build

elif [ &quot;${1}&quot; == &quot;push&quot; ]
then
    docker-compose --env-file=.env build; 
    aws ecr get-login-password --region &quot;$REGION&quot; | docker login --username AWS --password-stdin &quot;$ECR_HOST&quot;
    docker-compose push
fi</code></pre></li>
<li><p>ECS로 이미지 실행하기</p>
<ul>
<li>ECS 클러스터 만들기</li>
<li>ECS 컨테이너 인스턴스 생성</li>
<li>가비아 DNS 설정 (A, CNAME:cms, manager)</li>
<li>컨테이너 인스턴스에서 DB(RDS) 연결 확인 (EC2에 로그인해서 RDS에 접근이 되는지)</li>
<li>CONFIG FILE 전달 (fileReceive.sh -&gt; scp 명령어)<pre><code>#!/bin/bash
</code></pre></li>
</ul>
<p>cd config</p>
<p>SERVICE_CONFIG=service.toml
TRAEFIK_CONFIG=traefik.toml
GUNICORN_CONFIG=gunicorn-config.py
CELERY_CONFIG=celeryconfig.py
EC2_HOST=~~</p>
<p>scp -i <del>.pem ${GUNICORN_CONFIG} ${SERVICE_CONFIG} ${TRAEFIK_CONFIG} ${CELERY_CONFIG} ${EC2_HOST}:</del></p>
<p>cd ..</p>
<pre><code>- CONFIG FILE 경로 수정 (pdf 참고)
- 작업 생성, ECS 서비스 생성, 작업 실행 (이전 성공한 서비스 참고해서 설정하기)
</code></pre></li>
</ul>
</li>
</ul>
<hr>
<h2 id="이슈-트래커">이슈 트래커</h2>
<h3 id="no-space-left-on-device-에러">No space left on device 에러</h3>
<p>도커 시스템 캐시가 꽉 차서 실행이 안되는 에러였다.</p>
<pre><code>docker system prune #해결
docker volume prune

#To see all volumes
docker volume ls

#To show docker disk usage
docker system df</code></pre><p>참고 : <a href="https://blockchainstudy.tistory.com/59">https://blockchainstudy.tistory.com/59</a></p>
<h3 id="https-관련-접속-불가">https 관련 접속 불가</h3>
<p>traefik에서 자동으로 생성해주는 ssl이 가끔 깨진다고 한다.
/opt/letsencrypt/acme.json 확인</p>
<h3 id="백엔드-꺼지는-503-worker--에러">백엔드 꺼지는 503, Worker ~ 에러</h3>
<blockquote>
<p>werkzeug.exceptions.ServiceUnavailable: 503 Service Unavailable: The server is temporarily unable to service your request due to maintenance downtime or capacity problems. Please try again later.</p>
</blockquote>
<blockquote>
<p> {/usr/src/app/application/controllers/registry.py:46} ERROR - /registry: base data not found. run flask init device</p>
</blockquote>
<p>docker-compose-init.yaml 작업을 안했을 때 발생하는 에러였다. Readme에 적혀있는 작업을 꼭 하자...</p>
<hr>
<h2 id="메모장">메모장</h2>
<h4 id="업데이트-했을-때-확인해야-하는-것">업데이트 했을 때 확인해야 하는 것:</h4>
<p>front 빌드 (front-build build-prod)
.env 버전 태그 바꾸기
이미지 푸시 (. command.sh push)
작업에서 이미지 버전 태그 수정한 새 작업 등록
서비스 업데이트
기존 작업 중지</p>
<h4 id="sh파일-실행파일로-변환하기">.sh파일 실행파일로 변환하기</h4>
<p>mv a.sh a (이름 바꾸기)
chmod 755 a
실행: ./a</p>
<hr>
<p>갑자기 한번에 Traefik, ECS, ECR, RDS, S3, Cloudfront 등등 많은 기술들을 접하게 됐지만 하나하나 따라가보니 신기하고 재밌었다. 하지만 베이스 지식은 튼튼히 해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker-compose] PostgreSQL, pgAdmin 설치 + Flask]]></title>
            <link>https://velog.io/@luna_runa/Docker-compose-PostgreSQL-pgAdmin-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@luna_runa/Docker-compose-PostgreSQL-pgAdmin-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Wed, 27 Jul 2022 09:45:00 GMT</pubDate>
            <description><![CDATA[<p>docker-compose-db.yaml</p>
<pre><code class="language-yaml">version: &quot;3.8&quot;
services:
  postgres:
    container_name: postgres
    image: &quot;postgres:14.3&quot;
    volumes:
      - postgres:/var/lib/postgres
    ports:
      - 5432:5432
    environment:
      - POSTGRES_USER=postgres #psql -U 유저
      - POSTGRES_PASSWORD=test #pgAdmin password
      - TZ=Asia/Seoul

  pgadmin:
    container_name: pgadmin
    image: dpage/pgadmin4
    ports:
      - 8088:80
    environment:
      - PGADMIN_DEFAULT_EMAIL=test@test.com #address:8088 인덱스페이지 로그인
      - PGADMIN_DEFAULT_PASSWORD=test #address:8088 인덱스페이지 로그인
      - TZ=Asia/Seoul
    depends_on:
      - postgres

volumes:
  postgres:</code></pre>
<p>docker.env</p>
<pre><code class="language-yaml">FLASK_APP=server.py
FLASK_ENV=development
FLASK_DEBUG=True
FLASK_HOST=localhost
FLASK_PORT=4000

SECRET_KEY=secret

PG_HOST=postgres #DB 주소. 여기선 도커에서 생기는 postgres를 지정 (위에서 postgres: 부분)
PG_PORT=5432
PG_DB=postgres
PG_USER=postgres
PG_PASSWORD=test</code></pre>
<p>docker-compose-flask.yaml</p>
<pre><code class="language-yaml">version: &quot;3.8&quot;
services:
  flask:
    container_name: flask
    env_file:
      - ./docker.env
    build:
      context: ./src
    ports:
      - 4000:4000
    volumes:
      - ./:/app
</code></pre>
<p>/src/Dockerfile</p>
<pre><code class="language-shell">FROM python:3.10.5

WORKDIR /app

COPY requirements.txt requirements.txt

RUN pip install -r requirements.txt

COPY . .

WORKDIR /app/src
#위 과정중 도커 내부에도 똑같이 /src 폴더가 생김

CMD [&quot;python&quot;, &quot;-m&quot;, &quot;flask&quot;, &quot;run&quot;, &quot;--host=0.0.0.0&quot;, &quot;--port=4000&quot;]
#이 앱은 도커 내부의 localhost에서 실행되기 때문에 컨테이너 외부에서 서비스를 사용하기 위해</code></pre>
<hr>
<p>한시간 정도를 날려먹고나서야 확실하게 이해하고 넘어가야 한다는 것을 다시금 깨달았다..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker-compose] Makefile -> Docker-compose 전환기]]></title>
            <link>https://velog.io/@luna_runa/Docker-compose-Makefile-Docker-compose-%EC%A0%84%ED%99%98%EA%B8%B0</link>
            <guid>https://velog.io/@luna_runa/Docker-compose-Makefile-Docker-compose-%EC%A0%84%ED%99%98%EA%B8%B0</guid>
            <pubDate>Tue, 26 Jul 2022 09:28:09 GMT</pubDate>
            <description><![CDATA[<p><a href="https://github.com/aler9/rtsp-simple-server">rtps-simple-server</a>라는 라이브러리를 사용하게 되었는데 개발 환경 문제로 프로젝트가 제대로 설치되지 않았다.
이 프로젝트에서는 Makefile을 이용해 도커 파일을 실행시키는 것으로 보였는데 최근 docker-compose를 배운 김에 연습겸 Makefile을 해석해 compose 파일로 빼내기로 했다.</p>
<p>rtps-simple-server의 Makefile :</p>
<pre><code class="language-bash">define DOCKERFILE_RUN
FROM $(BASE_IMAGE)
RUN apk add --no-cache ffmpeg
WORKDIR /s
COPY go.mod go.sum ./
RUN go mod download
COPY . ./
RUN go build -o /out .
WORKDIR /
ARG CONFIG_RUN
RUN echo &quot;$$CONFIG_RUN&quot; &gt; rtsp-simple-server.yml
endef
export DOCKERFILE_RUN

define CONFIG_RUN
#rtspAddress: :8555
#rtpAddress: :8002
#rtcpAddress: :8003
#metrics: yes
#pprof: yes

paths:
  all:
    runOnDemand: ffmpeg -re -stream_loop -1 -i ./public/WS.mp4 -c copy -f rtsp rtsp://localhost:8554/mystream

#  proxied:
#    source: rtsp://192.168.2.198:554/stream
#    sourceProtocol: tcp
#    sourceOnDemand: yes
#    runOnDemand: ffmpeg -i rtsp://192.168.2.198:554/stream -c copy -f rtsp rtsp://localhost:$$RTSP_PORT/proxied2

#  original:
#    runOnReady: ffmpeg -i rtsp://localhost:554/original -b:a 64k -c:v libx264 -preset ultrafast -b:v 500k -max_muxing_queue_size 1024 -f rtsp rtsp://localhost:8554/compressed

endef
export CONFIG_RUN

run:
    echo &quot;$$DOCKERFILE_RUN&quot; | docker build -q . -f - -t temp \
    --build-arg CONFIG_RUN=&quot;$$CONFIG_RUN&quot;
    docker run --rm -it \
    --network=host \
    -v /home/ec2-user/rtsp-simple-server/public:/public \
    temp \
    sh -c &quot;/out&quot;</code></pre>
<p>DOCKERFILE_RUN 에서 Dockerfile 추출 :
RUN echo &quot;$$CONFIG_RUN&quot; &gt; rtsp-simple-server.yml을 쉽게 하기위해
CONFIG_RUN 부분을 따로 rtsp-simple-server-temp.yml로 분리한다</p>
<pre><code>FROM golang:1.18-alpine3.15

RUN apk add --no-cache ffmpeg

WORKDIR /s

COPY go.mod go.sum ./

RUN go mod download

COPY . ./

RUN go build -o /out .

WORKDIR /

COPY ./rtsp-simple-server-temp.yml ./rtsp-simple-server.yml

CMD [&quot;sh&quot;, &quot;-c&quot;, &quot;/out&quot;]</code></pre><p>docker-compose.yaml</p>
<pre><code>version: &quot;3.8&quot;
services:
  rtsp-simple-server:
    build: .
    volumes:
      - ./public:/public
    ports:
      - &quot;8554:8554&quot;</code></pre><p>public 폴더에 재생시킬 동영상 넣기.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker, EC2, RTSP] rtsp-simple-server 관련 에러]]></title>
            <link>https://velog.io/@luna_runa/Docker-EC2-RTSP-rtsp-simple-server-%EA%B4%80%EB%A0%A8-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@luna_runa/Docker-EC2-RTSP-rtsp-simple-server-%EA%B4%80%EB%A0%A8-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Tue, 26 Jul 2022 05:12:39 GMT</pubDate>
            <description><![CDATA[<p>EC2에서 rtsp-simple-server를 실행하려 했으나 이미 빌드된 바이너리 파일은 실행되지 않았기 때문에 git clone <a href="https://github.com/aler9/rtsp-simple-server.git">https://github.com/aler9/rtsp-simple-server.git</a> 으로 파일을 받아와 Makefile을 사용해 docker에 띄웠다.
.yml 파일에 runOnDemand를 이용해 클라이언트가 접속했을때마다 ffmpeg 스트리밍을 진행할 생각이었다.</p>
<pre><code class="language-yml">//rtsp-simple-server.yml
...
path:
  all:
    runOnDemand: ffmpeg -re -stream_loop -1 -i ./public/WS.mp4 -c copy -f rtsp rtsp://localhost:8554/mystream
...
run:
  ...
  -v /home/ec2-user/rtsp-simple-server/public:/public \
  ...</code></pre>
<p>하지만 완성적으로 실행하기까지 많은 시행착오를 겪었으며 이를 정리해보려한다.</p>
<h3 id="runondemand에서-localhost에-ffmpeg-스트리밍을-진행하지-못하는-문제">runOnDemand에서 localhost에 ffmpeg 스트리밍을 진행하지 못하는 문제</h3>
<p>이 문제는 git clone을 하기 전에 rtsp-simple-server 공식 문서(README)에 적혀있는대로 docker run --rm -it --network=host aler9/rtsp-simple-server 로 도커허브에 있는 이미지를 사용했을 때 나타났다.
원인은 yml파일을 -v 옵션으로 복사했을 때 localhost를 변환된 주소로 짚어주지 않고 로컬(EC2)주소를 사용했기 때문이라고 생각했다.
git clone을 사용해 코드를 불러와 Makefile로 직접 도커 이미지를 생성하는 과정에 yml파일을 설정해줌으로 해결했다.
ps. Makefile에서 yml을 직접 echo를 이용해 집어넣어 주는 방식인 거 같다.</p>
<h3 id="연결은-되지만-ffmpeg이-즉시-꺼지는-문제">연결은 되지만 ffmpeg이 즉시 꺼지는 문제</h3>
<p>이 문제는 ffmpeg 스트리밍 실행시 400에러와 함께 rtsp서버에 올라가지도 않는 현상과 같이 일어났는데 아마 rtsp-simple-server 내부의 어떤 파일이 깨지거나 했을 문제였을거라 생각한다. 다시 git clone을 진행해 해결했다.</p>
<h3 id="make-run-빌드중-계속-실패하는-문제">make run 빌드중 계속 실패하는 문제</h3>
<p>Makefile로 인해 docker 처리를 하던 도중 에러가 발생했고 그 에러를 해결했음에도 make run을 입력하면 같은 에러로 계속 실패했었다. 이는 이미 빌드 중에 docker image나 container, volume이 생성되었고 이들을 캐시했으므로 발생하는 오류였다. 해결하기 위해 캐싱된 이미지와 컨테이너, 볼륨을 강제로 삭제했다.
(사실 도커 기본 지식이 있었다면 쉽게 해결했을 일이었다.)</p>
<hr>
<p>적고나니 생각보다 적은 문제들이 자잘하게 일어난 것처럼 보이지만 실제론 에러가 에러를 낳고 한 문제들마다 많은 시간을 소모했다. 앞으로 똑같은 문제에 다시 당하고 싶지 않아 글을 남긴다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] 명령어 메모장]]></title>
            <link>https://velog.io/@luna_runa/Docker-%EB%AA%85%EB%A0%B9%EC%96%B4-%EB%A9%94%EB%AA%A8%EC%9E%A5</link>
            <guid>https://velog.io/@luna_runa/Docker-%EB%AA%85%EB%A0%B9%EC%96%B4-%EB%A9%94%EB%AA%A8%EC%9E%A5</guid>
            <pubDate>Tue, 26 Jul 2022 04:46:07 GMT</pubDate>
            <description><![CDATA[<p>모든 실행 중인 컨테이너 삭제
docker kill $(docker ps -q)</p>
<p>모든 중지된 컨테이너 삭제
docker rm $(docker ps -a -q)</p>
<p>모든 이미지 삭제하기
docker rmi $(docker images -q)</p>
<p>모든 매핑되지 않은 볼륨 삭제하기
docker volume rm $(docker volume ls -f dangling=true -q)</p>
<p>컨테이너 로그 확인 (최근 10개)
docker logs --tail 10 (컨테이너 이름)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ffmpeg] 뻘짓 막기용 메모]]></title>
            <link>https://velog.io/@luna_runa/ffmpeg-%EB%BB%98%EC%A7%93-%EB%A7%89%EA%B8%B0%EC%9A%A9-%EB%A9%94%EB%AA%A8</link>
            <guid>https://velog.io/@luna_runa/ffmpeg-%EB%BB%98%EC%A7%93-%EB%A7%89%EA%B8%B0%EC%9A%A9-%EB%A9%94%EB%AA%A8</guid>
            <pubDate>Mon, 25 Jul 2022 06:26:47 GMT</pubDate>
            <description><![CDATA[<p>요약:
ffmeg -i 파일이름
으로 파일 인코더 정보 확인하기!!!!!</p>
<hr>
<p>rtsp 서버를 열어서 android에서 접속해보는  과제를 진행중이었는데 rtsp UDP로 접속한 영상이 별로 큰 사이즈도 아닌데 계속 끊기면서 패킷 누락, Audio Sync 에러가 발생했다.
이 문제가 발생한 원인으로 여러가지를 생각해보며 테스트해봤다.</p>
<ul>
<li>MAC M1이라서 발생하는 문제?
아마 이 이유가 없지는 않았겠지만 핵심적인 문제점은 아니었다. 실제로 윈도우에서 돌린 서버와 M1에서 돌린 서버를 비교했을 때 심각한 차이를 확인하진 못했다.</li>
<li>라우터의 문제?
사내 라우터를 거쳐가며 생기는 문제일 수도 있다고 생각했었으나 이것 또한 치명적인 문제점은 아니었다. 끊기는 파일은 여전히 끊겼다.</li>
<li>영상파일 자체의 문제?
결국 이 문제임이 밝혀졌으며 인터넷에서 아무거나 막 주워온 영상이라서 인코더 정보도 들어있지 않았으며 해당 파일에서만 이 문제가 심각하게 발생하는 것을 확인했다...</li>
</ul>
<p>이 외에 인터넷 or M1의 문제라고 생각되어서 AWS EC2에 rtsp 서버를 띄워서 테스트도 해봤으나 문제의 그 파일로 띄웠을 때 아예 영상이 20초에 한번쯤밖에 받지 못하는 절망적인 결과도 확인했었다......</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS Linux2] 인스턴스 기본 설정, 설치]]></title>
            <link>https://velog.io/@luna_runa/AWS-Linux2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EA%B8%B0%EB%B3%B8-%EC%84%A4%EC%A0%95-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@luna_runa/AWS-Linux2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EA%B8%B0%EB%B3%B8-%EC%84%A4%EC%A0%95-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Fri, 22 Jul 2022 05:02:39 GMT</pubDate>
            <description><![CDATA[<p>sudo yum update
sudo yum install git</p>
<p>curl -o- <a href="https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh">https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh</a> | bash
. ~/.nvm/nvm.sh
nvm install 14.18.1 (노드 버전)</p>
<p>sudo yum install docker
sudo curl -L <a href="https://github.com/docker/compose/releases/latest/download/docker-compose-$">https://github.com/docker/compose/releases/latest/download/docker-compose-$</a>(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
sudo chmod 666 /var/run/docker.sock (도커 컴포즈 권한문제)</p>
<p>or</p>
<p>sudo amazon-linux-extras install docker</p>
<p>sudo systemctl start docker</p>
<p>sudo 없이 도커 명령어
sudo usermod -aG docker ec2-user</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Linux] MAC -> EC2 scp shell script ]]></title>
            <link>https://velog.io/@luna_runa/Linux-MAC-EC2-scp-shell-script</link>
            <guid>https://velog.io/@luna_runa/Linux-MAC-EC2-scp-shell-script</guid>
            <pubDate>Wed, 20 Jul 2022 05:18:03 GMT</pubDate>
            <description><![CDATA[<pre><code>#!/bin/bash
DIREC= . build
EC2_HOST=
scp -i (.pem) -r ${DIREC} ${EC2_HOST}</code></pre><pre><code>source (name).sh</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] pyenv, virtualenv 세팅]]></title>
            <link>https://velog.io/@luna_runa/Python-pyenv-virtualenv-%EC%84%B8%ED%8C%85</link>
            <guid>https://velog.io/@luna_runa/Python-pyenv-virtualenv-%EC%84%B8%ED%8C%85</guid>
            <pubDate>Wed, 13 Jul 2022 02:32:19 GMT</pubDate>
            <description><![CDATA[<p>homebrew로 pyenv를 설치했다는 가정.</p>
<p>pyenv install -list = pyenv로 설치할 수 있는 목록 표시
pyenv versions = 설치되어있는 모든 버전, 가상환경 목록과 현재 선택되어있는 상태 *표시
pyenv virtualenv (이름) = 가상환경 생성
pyenv shell (이름) = 버전(가상환경) 선택</p>
]]></description>
        </item>
    </channel>
</rss>