<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>my_code.log</title>
        <link>https://velog.io/</link>
        <description>조금씩 정리하자!!!</description>
        <lastBuildDate>Tue, 17 Jun 2025 12:03:55 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>my_code.log</title>
            <url>https://velog.velcdn.com/images/my_code/profile/73959b7f-ba85-4eb2-bf19-3ab011edc3bb/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. my_code.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/my_code" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[What To Eat] 메뉴 투표 기능 구현하기]]></title>
            <link>https://velog.io/@my_code/What-To-Eat-%EB%A9%94%EB%89%B4-%ED%88%AC%ED%91%9C-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@my_code/What-To-Eat-%EB%A9%94%EB%89%B4-%ED%88%AC%ED%91%9C-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 17 Jun 2025 12:03:55 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/my_code/post/7e80571a-aa65-42ff-89b0-11ca36c9f859/image.png" alt=""></p>
<blockquote>
<p>이 프로젝트는 이것저것 적용해서 테스트하기 위한 프로젝트입니다. 게시물 CRUD를 기반으로 식사 메뉴 투표하는 기능을 구현했습니다.</p>
</blockquote>
<p>이번에는 기존 게시물 시스템에 투표 기능을 추가하는 과정을 상세히 다루어보겠습니다. Express.js, TypeScript, Prisma를 사용하여 게시물 생성(투표 생성), 투표하기, 투표 취소, 투표 결과 조회 등의 기능을 구현했습니다.</p>
<hr>
<h2 id="데이터베이스-스키마-설계">데이터베이스 스키마 설계</h2>
<h4 id="최종-스키마-구조">최종 스키마 구조</h4>
<ul>
<li><strong>Post 모델의 투표 관련 필드</strong><ul>
<li><code>isPoll</code>: 해당 게시물이 투표 게시물인지 여부를 나타냅니다. false가 기본값으로, 일반 게시물과 투표 게시물을 구분합니다.</li>
<li><code>isPollActive</code>: 투표가 활성화 상태인지 여부를 나타냅니다. 관리자가 투표를 일시 중지할 수 있습니다.</li>
<li><code>pollExpiresAt</code>: 투표 만료 시간을 설정할 수 있습니다. null인 경우 만료 시간이 없음을 의미합니다.</li>
</ul>
</li>
<li><strong>Vote 모델</strong><ul>
<li><code>text</code>: 투표 항목의 텍스트를 저장합니다.</li>
<li><code>postId</code>: 어떤 게시물의 투표 항목인지 연결합니다.</li>
<li><code>userVotes</code>: 해당 투표 항목에 대한 모든 투표 기록을 참조합니다.</li>
</ul>
</li>
<li><strong>UserVote 모델 (중간 테이블)</strong><ul>
<li><code>@@unique([userId, voteId])</code>: 한 사용자가 같은 투표 항목에 중복 투표하는 것을 방지합니다.</li>
<li><code>@@index</code>: 조회 성능 향상을 위한 인덱스를 설정합니다.<pre><code class="language-sql">// backend/prisma/schema.prisma
</code></pre>
</li>
</ul>
</li>
</ul>
<p>model User {
  id                    String    @id @default(uuid())
  email                 String    @unique
  password              String
  nickname              String?
  socialId              String?   @unique
  refreshToken          String?   @unique
  refreshTokenExpiresAt DateTime?
  createdAt             DateTime  @default(now())
  updatedAt             DateTime  @updatedAt
  deletedAt             DateTime?</p>
<p>  posts     Post[]
  userVotes UserVote[]</p>
<p>  @@map(&quot;users&quot;)
}</p>
<p>model Post {
  id        String    @id @default(uuid())
  title     String // 게시물/투표 제목
  content   String    @db.Text // 게시물/투표 내용
  authorId  String
  author    User      @relation(fields: [authorId], references: [id], onDelete: Cascade)
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  deletedAt DateTime?</p>
<p>  // 투표 관련 필드
  isPoll        Boolean   @default(false) // 투표 여부
  isPollActive  Boolean   @default(true) // 투표 활성화 상태
  pollExpiresAt DateTime? // 투표 만료 시간</p>
<p>  votes Vote[] // 투표 항목들</p>
<p>  @@map(&quot;posts&quot;)
}</p>
<p>model Vote {
  id        String     @id @default(uuid())
  text      String // 투표 항목 텍스트
  postId    String
  post      Post       @relation(fields: [postId], references: [id], onDelete: Cascade)
  userVotes UserVote[] // 투표 기록들
  createdAt DateTime   @default(now())
  updatedAt DateTime   @updatedAt</p>
<p>  @@index([postId])
  @@map(&quot;votes&quot;)
}</p>
<p>model UserVote {
  id        String   @id @default(uuid())
  userId    String
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  voteId    String
  vote      Vote     @relation(fields: [voteId], references: [id], onDelete: Cascade)
  createdAt DateTime @default(now())</p>
<p>  @@unique([userId, voteId]) // 한 사용자는 한 투표 항목에 한 번만 투표 가능
  @@index([userId])
  @@index([voteId])
  @@map(&quot;user_votes&quot;)
}</p>
<pre><code>
&lt;br&gt;

---
## 타입 정의 및 인터페이스
#### DTO (Data Transfer Object) 패턴
- `CreatePostDto`: 게시물 생성 시 필요한 데이터를 정의합니다. 투표 관련 필드들은 선택사항으로 설정하여 일반 게시물도 생성할 수 있습니다.
- `UpdatePostDto`: 게시물 수정 시 사용되며, 모든 필드가 선택사항입니다. 부분 업데이트를 지원합니다.
- `VoteDto`: 투표하기/취소 시 필요한 투표 항목 ID를 정의합니다.
#### 응답 타입 설계
- `PostResponse`: 클라이언트에게 반환되는 게시물 정보를 정의합니다. 투표 관련 정보는 선택적으로 포함됩니다.
- `VoteResponse`: 각 투표 항목의 상세 정보와 통계를 포함합니다.
- `PostsResponse`: 페이지네이션이 적용된 게시물 목록 응답을 정의합니다.

```typescript
// backend/src/types/post.types.ts

import { Post, User, Vote, UserVote } from &#39;@prisma/client&#39;;

// 투표 항목 응답 타입
export interface VoteResponse {
  id: string;
  text: string;
  voteCount: number;    // 해당 항목의 투표 수
  percentage: number;   // 전체 대비 투표 비율
  userVoted: boolean;   // 현재 사용자가 투표했는지 여부
}

// 투표 기록 응답 타입
export interface UserVoteResponse {
  id: string;
  userId: string;
  voteId: string;
  createdAt: Date;
}

// 게시물 생성 DTO
export interface CreatePostDto {
  title: string;
  content: string;
  isPoll?: boolean;           // 투표 여부 (선택사항)
  isPollActive?: boolean;     // 투표 활성화 상태 (선택사항)
  pollExpiresAt?: Date;       // 투표 만료 시간 (선택사항)
  votes?: string[];           // 투표 항목 텍스트 배열 (선택사항)
}

// 게시물 수정 DTO
export interface UpdatePostDto {
  title?: string;
  content?: string;
  isPoll?: boolean;
  isPollActive?: boolean;
  pollExpiresAt?: Date | null;
  votes?: string[];           // 투표 항목 수정 시 기존 항목을 모두 교체
}

// 투표 DTO
export interface VoteDto {
  voteId: string;             // 투표할 항목의 ID
}

// 게시물 응답 타입
export interface PostResponse {
  id: string;
  title: string;
  content: string;
  author: {
    id: string;
    nickname: string;
  };
  createdAt: Date;
  updatedAt: Date;
  isPoll: boolean;
  isPollActive: boolean;
  pollExpiresAt: Date | null;
  votes?: VoteResponse[];     // 투표 항목 정보 (상세 조회 시에만 포함)
  totalVotes?: number;        // 전체 투표 수
  userVoted?: boolean;        // 현재 사용자 투표 여부
}

// 게시물 목록 응답 타입
export interface PostsResponse {
  posts: PostResponse[];
  total: number;              // 전체 게시물 수
  page: number;               // 현재 페이지
  limit: number;              // 페이지당 게시물 수
  totalPages: number;         // 전체 페이지 수
}</code></pre><br>

<hr>
<h2 id="서비스-레이어-구현">서비스 레이어 구현</h2>
<h4 id="postservice-클래스-구조">PostService 클래스 구조</h4>
<pre><code class="language-typescript">// backend/src/services/post.service.ts

export class PostService {
  private prisma: PrismaClient;

  constructor() {
    this.prisma = new PrismaClient();
  }

  // 게시물 생성
  async createPost(userId: string, dto: CreatePostDto): Promise&lt;PostResponse&gt; { ... }

  // 게시물 수정
  async updatePost(postId: string, userId: string, dto: UpdatePostDto): Promise&lt;PostResponse&gt; { ... }

  // 게시물 삭제
  async deletePost(postId: string, userId: string): Promise&lt;void&gt; { ... }

  // 게시물 목록 조회
  async getPosts(page: number = 1, limit: number = 10): Promise&lt;PostsResponse&gt; { ... }

  // 게시물 상세 조회
  async getPost(postId: string, userId: string | null): Promise&lt;PostResponse&gt; { ... }

  // 투표 게시물 검증 (공통 메서드)
  private async validateVotePost(postId: string, userId: string, voteId: string) { ... }

  // 투표하기
  async vote(postId: string, userId: string, dto: VoteDto): Promise&lt;PostResponse&gt; { ... }

  // 투표 취소
  async cancelVote(postId: string, userId: string, dto: VoteDto): Promise&lt;PostResponse&gt; { ... }

  // 응답 포맷팅 메서드들
  private formatPostResponse(post: PostWithDetails, userId: string | null): PostResponse { ... }
  private formatPostListResponse(post: PostWithDetails): PostResponse { ... }
}</code></pre>
<br>

<h4 id="게시물-생성-로직-상세-분석">게시물 생성 로직 상세 분석</h4>
<ul>
<li><strong>1단계</strong>: 투표 게시물인 경우, 투표 항목이 2개 이상 10개 이하인지 검증합니다.</li>
<li><strong>2단계</strong>: 투표 항목에 중복이 있는지 검사합니다.</li>
<li><strong>3단계</strong>: 게시물(Post)과, 투표 게시물이라면 투표 항목(Vote)까지 함께 생성합니다.</li>
<li><strong>4단계</strong>: author, votes, userVotes 등 관련 데이터를 포함하여 생성된 게시물을 조회합니다.</li>
<li><strong>5단계</strong>: 응답 포맷팅 메서드(formatPostResponse)를 통해 클라이언트에 반환할 형태로 가공합니다.</li>
</ul>
<pre><code class="language-typescript">// backend/src/services/post.service.ts

export class PostService {
  private prisma: PrismaClient;

  constructor() {
    this.prisma = new PrismaClient();
  }

  // 게시물 생성
  async createPost(userId: string, dto: CreatePostDto): Promise&lt;PostResponse&gt; {
    const { title, content, isPoll, isPollActive, pollExpiresAt, votes } = dto;

    // 투표 게시물인 경우 투표 항목 검증
    if (isPoll) {
      if (!votes || votes.length &lt; 2) {
        throw new HttpException(400, &#39;투표 항목은 최소 2개 이상 필요합니다.&#39;);
      }
      if (votes.length &gt; 10) {
        throw new HttpException(400, &#39;투표 항목은 최대 10개까지 가능합니다.&#39;);
      }
    }

    // 투표 게시물인 경우 투표 메뉴 중복 체크
    if (dto.isPoll &amp;&amp; dto.votes) {
      const uniqueVotes = new Set(dto.votes);
      if (uniqueVotes.size !== dto.votes.length) {
        throw new HttpException(400, &#39;투표 메뉴에 중복된 항목이 있습니다.&#39;);
      }
    }

    const post = await this.prisma.post.create({
      data: {
        title,
        content,
        authorId: userId,
        isPoll: isPoll || false,
        isPollActive: isPollActive ?? true,
        pollExpiresAt,
        votes: isPoll
          ? {
              create: votes!.map((text) =&gt; ({ text })),
            }
          : undefined,
      },
      include: {
        author: {
          select: {
            id: true,
            nickname: true,
          },
        },
        votes: {
          include: {
            userVotes: {
              include: {
                user: {
                  select: {
                    id: true,
                  },
                },
              },
            },
          },
        },
      },
    });

    return this.formatPostResponse(post, userId);
  }

  ...

}</code></pre>
<br>

<h4 id="투표-게시물-검증-로직">투표 게시물 검증 로직</h4>
<p>게시물 존재 여부, 투표 게시물 여부, 활성화 상태, 만료 여부, 투표 항목 존재 여부를 모두 검증합니다. 아래 메서드는 투표하기/투표 취소 등에서 공통적으로 사용되어 중복 코드를 줄입니다.</p>
<pre><code class="language-typescript">// backend/src/services/post.service.ts

...

// 투표 게시물 검증
  private async validateVotePost(postId: string, userId: string, voteId: string) {
    try {
      const post = await this.prisma.post.findUnique({
        where: { id: postId },
        include: {
          votes: {
            include: {
              userVotes: {
                include: {
                  user: {
                    select: {
                      id: true,
                    },
                  },
                },
              },
            },
          },
        },
      });

      if (!post) {
        throw new HttpException(404, &#39;게시물을 찾을 수 없습니다.&#39;);
      }

      if (!post.isPoll) {
        throw new HttpException(400, &#39;투표 게시물이 아닙니다.&#39;);
      }

      if (!post.isPollActive) {
        throw new HttpException(400, &#39;종료된 투표입니다.&#39;);
      }

      // 만료 시간 확인 로직 개선
      if (post.pollExpiresAt &amp;&amp; post.pollExpiresAt &lt; new Date()) {
        throw new HttpException(400, &#39;만료된 투표입니다.&#39;);
      }

      const vote = post.votes.find((v) =&gt; v.id === voteId);
      if (!vote) {
        throw new HttpException(404, &#39;투표 항목을 찾을 수 없습니다.&#39;);
      }

      return { post, vote };
    } catch (error) {
      console.error(&#39;투표 게시물 검증 중 오류 발생:&#39;, error);
      if (error instanceof HttpException) {
        throw error;
      }
      throw new HttpException(500, &#39;투표 게시물 검증 중 오류가 발생했습니다.&#39;);
    }
  }

...
</code></pre>
<br>

<h4 id="투표하기-기능">투표하기 기능</h4>
<ul>
<li><strong>투표 게시물 검증</strong>: 앞서 만든 <code>validateVotePost</code>로 게시물과 투표 항목의 유효성을 확인합니다.</li>
<li><strong>중복 투표 방지</strong>: 이미 해당 게시물에 투표한 기록이 있으면 예외를 발생시킵니다.</li>
<li><strong>투표 기록 생성</strong>: <code>UserVote</code> 테이블에 투표 기록을 추가합니다.</li>
<li><strong>최신 게시물 정보 반환</strong>: 투표 결과가 반영된 게시물 정보를 반환합니다.</li>
</ul>
<pre><code class="language-typescript">// backend/src/services/post.service.ts

async vote(postId: string, userId: string, dto: VoteDto): Promise&lt;PostResponse&gt; {
  const { voteId } = dto;

  // 투표 게시물 검증
  await this.validateVotePost(postId, userId, voteId);

  // 이미 투표했는지 확인
  const existingVote = await this.prisma.userVote.findFirst({
    where: {
      userId,
      vote: { postId }
    }
  });

  if (existingVote) {
    throw new HttpException(400, &#39;이미 투표했습니다.&#39;);
  }

  // 투표 생성
  await this.prisma.userVote.create({
    data: { userId, voteId }
  });

  // 업데이트된 게시물 조회 및 반환
  const updatedPost = await this.prisma.post.findUnique({
    where: { id: postId },
    include: {
      author: { select: { id: true, nickname: true } },
      votes: {
        include: {
          userVotes: {
            include: { user: { select: { id: true } } }
          }
        }
      }
    }
  });

  return this.formatPostResponse(updatedPost!, userId);
}</code></pre>
<br>

<h4 id="투표-취소-기능">투표 취소 기능</h4>
<ul>
<li><strong>투표 게시물 검증</strong>: <code>validateVotePost</code>로 게시물과 투표 항목의 유효성을 확인합니다.</li>
<li><strong>투표 기록 확인</strong>: 해당 사용자가 해당 항목에 투표한 기록이 있는지 확인합니다.</li>
<li><strong>투표 취소</strong>: <code>UserVote</code> 테이블에서 해당 기록을 삭제합니다.</li>
<li><strong>최신 게시물 정보 반환</strong>: 투표 취소가 반영된 게시물 정보를 반환합니다.</li>
</ul>
<pre><code class="language-typescript">// backend/src/services/post.service.ts

async cancelVote(postId: string, userId: string, dto: VoteDto): Promise&lt;PostResponse&gt; {
  const { voteId } = dto;

  // 투표 게시물 검증
  await this.validateVotePost(postId, userId, voteId);

  // 사용자의 투표 확인
  const userVote = await this.prisma.userVote.findUnique({
    where: {
      userId_voteId: { userId, voteId }
    }
  });

  if (!userVote) {
    throw new HttpException(400, &#39;해당 항목에 투표한 기록이 없습니다.&#39;);
  }

  // 투표 취소
  await this.prisma.userVote.delete({
    where: {
      userId_voteId: { userId, voteId }
    }
  });

  // 업데이트된 게시물 조회 및 반환
  const updatedPost = await this.prisma.post.findUnique({
    where: { id: postId },
    include: {
      author: { select: { id: true, nickname: true } },
      votes: {
        include: {
          userVotes: {
            include: { user: { select: { id: true } } }
          }
        }
      }
    }
  });

  return this.formatPostResponse(updatedPost!, userId);
}</code></pre>
<br>

<h4 id="응답-포맷팅">응답 포맷팅</h4>
<ul>
<li><code>totalVotes</code>: 게시물의 전체 투표 수를 계산합니다.</li>
<li><code>userVoted</code>: 현재 사용자가 이 게시물에 투표했는지 여부를 계산합니다.</li>
<li><code>votes</code>: 각 투표 항목별로 투표 수, 비율, 사용자의 투표 여부를 포함한 정보를 제공합니다.</li>
<li><strong>응답 최적화</strong>: 게시물 목록 조회 시에는 투표 정보(votes)를 제외하고, 상세 조회 시에만 포함합니다.</li>
</ul>
<pre><code class="language-typescript">// backend/src/services/post.service.ts

private formatPostResponse(post: PostWithDetails, userId: string | null): PostResponse {
  const totalVotes = post.votes.reduce((sum, vote) =&gt; sum + vote.userVotes.length, 0);
  const userVoted = userId
    ? post.votes.some((vote) =&gt; 
        vote.userVotes.some((userVote) =&gt; userVote.user.id === userId)
      )
    : false;

  return {
    id: post.id,
    title: post.title,
    content: post.content,
    author: {
      id: post.author.id,
      nickname: post.author.nickname || &#39;&#39;,
    },
    createdAt: post.createdAt,
    updatedAt: post.updatedAt,
    isPoll: post.isPoll,
    isPollActive: post.isPollActive,
    pollExpiresAt: post.pollExpiresAt,
    votes: post.votes.map((vote) =&gt; ({
      id: vote.id,
      text: vote.text,
      voteCount: vote.userVotes.length,
      percentage: totalVotes &gt; 0 ? (vote.userVotes.length / totalVotes) * 100 : 0,
      userVoted: userId ? 
        vote.userVotes.some((userVote) =&gt; userVote.user.id === userId) : false,
    })),
    totalVotes,
    userVoted,
  };
}</code></pre>
<br>

<h4 id="게시물-목록-조회-포멧팅-votes-데이터-제외">게시물 목록 조회 포멧팅 (votes 데이터 제외)</h4>
<ul>
<li><strong>목적</strong>: 게시물 목록 조회 시 사용되는 응답 포맷팅 함수입니다.</li>
<li><strong>기본 정보만 반환</strong>: 게시물의 id, 제목, 내용, 작성자, 생성/수정일, 투표 여부 및 상태, 만료 시간 등 <strong>핵심 정보만 반환</strong>합니다.</li>
<li><strong>투표 상세 정보 제외</strong>: 투표 항목(votes), 투표 수(totalVotes), 사용자 투표 여부(userVoted) 등 상세 정보는 포함하지 않습니다.</li>
<li><strong>장점</strong>: 목록 조회 시 불필요한 데이터 전송을 줄여 <strong>성능을 최적화</strong>하고, 클라이언트에서 빠르게 목록을 렌더링할 수 있습니다.</li>
</ul>
<pre><code class="language-typescript">// backend/src/services/post.service.ts

private formatPostListResponse(post: PostWithDetails): PostResponse {
  return {
    id: post.id,
    title: post.title,
    content: post.content,
    author: {
      id: post.author.id,
      nickname: post.author.nickname || &#39;&#39;,
    },
    createdAt: post.createdAt,
    updatedAt: post.updatedAt,
    isPoll: post.isPoll,
    isPollActive: post.isPollActive,
    pollExpiresAt: post.pollExpiresAt,
  };
}</code></pre>
<p>이처럼 formatPostListResponse는 게시물 목록 조회에 최적화된 간결한 응답을 제공하여, 상세 조회와 역할을 분리하고 전체 API의 효율성을 높입니다.</p>
<br>

<hr>
<h2 id="실행-결과">실행 결과</h2>
<ul>
<li>게시물 생성 (투표 포함)
<img src="https://velog.velcdn.com/images/my_code/post/cbee43a9-3547-4da0-8dde-329e176320c1/image.png" alt=""></li>
</ul>
<ul>
<li>게시물 상세 조회
<img src="https://velog.velcdn.com/images/my_code/post/267312fb-17e9-4056-9fa4-507ddfc27302/image.png" alt=""></li>
</ul>
<ul>
<li>게시물 수정
<img src="https://velog.velcdn.com/images/my_code/post/57771600-3f86-486b-b338-7ef362502f51/image.png" alt="">
<img src="https://velog.velcdn.com/images/my_code/post/2cce0d16-7ed9-4ade-8698-490b034020cd/image.png" alt=""></li>
</ul>
<ul>
<li>메뉴 투표
<img src="https://velog.velcdn.com/images/my_code/post/de449213-14be-4ff8-8177-c1373f46b265/image.png" alt=""></li>
</ul>
<ul>
<li>메뉴 투표 취소
<img src="https://velog.velcdn.com/images/my_code/post/76329c6b-bb61-4e9d-bde0-c10c30de3765/image.png" alt=""></li>
</ul>
<br>

<hr>
<h2 id="트러블-슈팅">트러블 슈팅</h2>
<h3 id="nm-관계-설계-문제">N:M 관계 설계 문제</h3>
<p><strong>문제:</strong></p>
<ul>
<li>Prisma에서 User와 Vote 간 N:M 관계 구현 시 복잡한 쿼리 작성 필요</li>
<li>투표 기록의 생성 시간 등 메타데이터 저장 불가</li>
<li>중복 투표 방지 로직 구현 복잡</li>
</ul>
<p><strong>원인:</strong></p>
<pre><code class="language-sql">// 문제가 있던 초기 설계
model User {
  votes Vote[] // 직접적인 N:M 관계
}

model Vote {
  users User[] // 직접적인 N:M 관계
}</code></pre>
<p><strong>해결:</strong></p>
<pre><code class="language-sql">// 중간 테이블을 사용한 개선된 설계
model User {
  userVotes UserVote[]
}

model Vote {
  userVotes UserVote[]
}

model UserVote {
  id        String   @id @default(uuid())
  userId    String
  voteId    String
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  vote      Vote     @relation(fields: [voteId], references: [id], onDelete: Cascade)
  createdAt DateTime @default(now())

  @@unique([userId, voteId]) // 중복 투표 방지
  @@index([userId])
  @@index([voteId])
  @@map(&quot;user_votes&quot;)
}</code></pre>
<br>

<h3 id="투표-항목-수정-시-데이터-무결성-문제">투표 항목 수정 시 데이터 무결성 문제</h3>
<p><strong>문제:</strong></p>
<ul>
<li>투표 항목 수정 시 기존 투표 기록과 연결이 끊어짐</li>
<li>투표 통계가 부정확해짐</li>
<li>데이터 무결성 위반</li>
</ul>
<p><strong>원인:</strong></p>
<pre><code class="language-typescript">// 문제가 있던 코드
async updatePost(postId: string, userId: string, dto: UpdatePostDto) {
  // 투표 항목을 직접 수정하면 기존 투표 기록과 불일치
  await this.prisma.vote.updateMany({
    where: { postId },
    data: { text: dto.votes } // 기존 투표 기록과 연결 끊어짐
  });
}</code></pre>
<p><strong>해결:</strong></p>
<pre><code class="language-typescript">// 삭제 후 재생성 방식으로 변경
async updatePost(postId: string, userId: string, dto: UpdatePostDto) {
  if (post.isPoll &amp;&amp; dto.votes) {
    const existingVotes = post.votes;
    const newVotes = dto.votes;

    // 삭제할 투표 항목 찾기
    const votesToDelete = existingVotes.filter(
      (existing) =&gt; !newVotes.includes(existing.text)
    );

    // 추가할 투표 항목 찾기
    const votesToAdd = newVotes.filter(
      (newVote) =&gt; !existingVotes.some((existing) =&gt; existing.text === newVote)
    );

    // 삭제할 투표 항목의 모든 투표 기록 삭제
    if (votesToDelete.length &gt; 0) {
      await this.prisma.userVote.deleteMany({
        where: {
          voteId: { in: votesToDelete.map((vote) =&gt; vote.id) }
        }
      });

      await this.prisma.vote.deleteMany({
        where: {
          id: { in: votesToDelete.map((vote) =&gt; vote.id) }
        }
      });
    }

    // 새로운 투표 항목 추가
    if (votesToAdd.length &gt; 0) {
      await this.prisma.vote.createMany({
        data: votesToAdd.map((text) =&gt; ({
          postId,
          text,
        }))
      });
    }
  }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[What To Eat] 게시물 기본 CRUD 구현하기]]></title>
            <link>https://velog.io/@my_code/What-To-Eat-%EA%B2%8C%EC%8B%9C%EB%AC%BC-%EA%B8%B0%EB%B3%B8-CRUD-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@my_code/What-To-Eat-%EA%B2%8C%EC%8B%9C%EB%AC%BC-%EA%B8%B0%EB%B3%B8-CRUD-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 09 Jun 2025 07:25:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/my_code/post/dc9056ce-51bb-479e-b4f4-b7a0470d8fa1/image.png" alt=""></p>
<blockquote>
<p>이 프로젝트는 이것저것 적용해서 테스트하기 위한 프로젝트입니다. 이번에는 투표 기능 구현에 들어가기 전에 기본적인 게시물 CRUD를 구현했습니다.</p>
</blockquote>
<p>식사 메뉴 투표 커뮤니티 프로젝트를 진행하면서, 투표 기능을 구현하기 전에 먼저 기본적인 게시물 관리 기능부터 차근차근 구현하기로 했습니다. 한 번에 다 구현하는 게 아니라 조금씩 살을 붙이면서 견고한 기반을 다지는 것이 빠르게 구현이 가능할 것 같다고 생각했습니다.</p>
<hr>
<h2 id="데이터베이스-스키마-설계">데이터베이스 스키마 설계</h2>
<ul>
<li><strong>1:N 관계</strong>: 한 사용자가 여러 게시물 작성 가능</li>
<li><strong>onDelete</strong>: Cascade: 사용자 삭제 시 게시물도 함께 삭제</li>
<li><strong>@updatedAt</strong>: 게시물 수정 시 자동으로 시간 업데이트</li>
<li><strong>@db.Text</strong>: 긴 내용을 위한 TEXT 타입 사용</li>
</ul>
<pre><code class="language-typescript">// backend/prisma/schema.prisma

model User {
  id                     String    @id @default(uuid())
  email                  String    @unique
  password               String
  nickname               String?
  socialId               String?   @unique
  refreshToken           String?   @unique
  refreshTokenExpiresAt  DateTime?
  createdAt              DateTime  @default(now())

  posts                  Post[]    // 추가된 관계

  @@map(&quot;users&quot;)
}

model Post {
  id          String   @id @default(uuid())
  title       String
  content     String   @db.Text
  authorId    String
  author      User     @relation(fields: [authorId], references: [id], onDelete: Cascade)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  deletedAt   DateTime?

  @@map(&quot;posts&quot;)
}</code></pre>
<br>

<hr>
<h2 id="typescript-타입-정의">TypeScript 타입 정의</h2>
<p>API 통신과 타입 안전성을 위해 필요한 인터페이스들을 정의했습니다.</p>
<ul>
<li><strong>DTO 패턴</strong>: 입력과 출력을 명확히 분리</li>
<li><strong>선택적 속성</strong>: UpdatePostDto는 부분 업데이트 지원</li>
<li><strong>페이지네이션</strong>: 대용량 데이터 처리를 위한 구조</li>
</ul>
<p>사실 DTO 패턴은 Nest.js에서 사용했었는데, Express에서도 살짝 사용해볼까해서 구현했습니다.</p>
<pre><code class="language-typescript">// backend/src/types/post.types.ts

export interface CreatePostDto {
  title: string;
  content: string;
}

export interface UpdatePostDto {
  title?: string;
  content?: string;
}

export interface PostResponse {
  id: string;
  title: string;
  content: string;
  authorId: string;
  author: {
    id: string;
    nickname: string | null;
    email: string;
  };
  createdAt: Date;
  updatedAt: Date;
  deletedAt: Date | null;
}

export interface PostListResponse {
  posts: PostResponse[];
  total: number;
  page: number;
  limit: number;
}</code></pre>
<br>

<hr>
<h2 id="서비스-계층-구현">서비스 계층 구현</h2>
<p>비즈니스 로직을 담당하는 PostService를 구현했습니다.</p>
<p><strong>1. 권한 체크</strong>: 작성자만 자신의 게시물 수정/삭제 가능
<strong>2. 검색 기능</strong>: 제목과 내용에서 OR 조건으로 검색
<strong>3. 페이지네이션</strong>: skip/take를 활용한 효율적인 데이터 조회
<strong>4. 성능 최적화</strong>: Promise.all로 병렬 처리
<strong>5. 데이터 선택</strong>: 민감한 정보 제외하고 필요한 데이터만 조회</p>
<pre><code class="language-typescript">// backend/src/services/post.service.ts

export class PostService {
  // 게시물 생성
  async createPost(authorId: string, data: CreatePostDto): Promise&lt;PostResponse&gt; {
    const { title, content } = data;

    const post = await prisma.post.create({
      data: {
        title,
        content,
        authorId,
      },
      include: {
        author: {
          select: {
            id: true,
            nickname: true,
            email: true,
          },
        },
      },
    });

    return post;
  }

  // 게시물 목록 조회 (페이지네이션 포함)
  async getPosts(query: GetPostsQuery): Promise&lt;PostListResponse&gt; {
    const { page = 1, limit = 10, search } = query;
    const skip = (page - 1) * limit;

    // 삭제되지 않았고, search에 해당하는 게시물만 조회
    const where = {
      deletedAt: null,
      ...(search &amp;&amp; {
        OR: [{ title: { contains: search } }, { content: { contains: search } }],
      }),
    };

    const [posts, total] = await Promise.all([
      prisma.post.findMany({
        where,
        include: {
          author: {
            select: {
              id: true,
              nickname: true,
              email: true,
            },
          },
        },
        orderBy: {
          createdAt: &#39;desc&#39;,
        },
        skip,
        take: limit,
      }),
      prisma.post.count({ where }),
    ]);

    return {
      posts,
      total,
      page,
      limit,
    };
  }

  // 게시물 단일 조회
  async getPostById(id: string): Promise&lt;PostResponse&gt; {
    const post = await prisma.post.findUnique({
      where: {
        id,
        deletedAt: null,
      },
      include: {
        author: {
          select: {
            id: true,
            nickname: true,
            email: true,
          },
        },
      },
    });

    if (!post) {
      throw new Error(&#39;게시물을 찾을 수 없습니다.&#39;);
    }

    return post;
  }

  // 게시물 수정
  async updatePost(id: string, authorId: string, data: UpdatePostDto): Promise&lt;PostResponse&gt; {
    // 게시물 존재 확인 및 작성자 검증 (삭제되지 않은 게시물만)
    const existingPost = await prisma.post.findFirst({
      where: {
        id,
        deletedAt: null,
      },
    });

    if (!existingPost) {
      throw new Error(&#39;게시물을 찾을 수 없습니다.&#39;);
    }

    if (existingPost.authorId !== authorId) {
      throw new Error(&#39;게시물을 수정할 권한이 없습니다.&#39;);
    }

    const updatedPost = await prisma.post.update({
      where: { id },
      data,
      include: {
        author: {
          select: {
            id: true,
            nickname: true,
            email: true,
          },
        },
      },
    });

    return updatedPost;
  }

  // 게시물 삭제
  async deletePost(id: string, authorId: string): Promise&lt;void&gt; {
    // 게시물 존재 확인 및 작성자 검증 (삭제되지 않은 게시물만)
    const existingPost = await prisma.post.findFirst({
      where: {
        id,
        deletedAt: null,
      },
    });

    if (!existingPost) {
      throw new Error(&#39;게시물을 찾을 수 없습니다.&#39;);
    }

    if (existingPost.authorId !== authorId) {
      throw new Error(&#39;게시물을 삭제할 권한이 없습니다.&#39;);
    }

    // Soft Delete
    await prisma.post.update({
      where: { id },
      data: {
        deletedAt: new Date(),
      },
    });
  }
}</code></pre>
<br>

<hr>
<h2 id="컨트롤러-계층-구현">컨트롤러 계층 구현</h2>
<p>HTTP 요청을 처리하는 PostController를 구현했습니다.</p>
<p><strong>1. 입력값 검증</strong>: 클라이언트 데이터의 유효성 검사
<strong>2. 에러 처리</strong>: 적절한 HTTP 상태 코드와 메시지 반환
<strong>3. 타입 안전성</strong>: TypeScript를 활용한 컴파일 타임 검증
<strong>4. 일관된 응답</strong>: ApiResponse 인터페이스 활용
<strong>5. 보안</strong>: JWT 토큰을 통한 사용자 인증</p>
<pre><code class="language-typescript">// backend/src/controllers/post.controller.ts

export class PostController {
  constructor(private postService: PostService) {}

  // 게시물 생성
  createPost = async (req: AuthenticatedRequest, res: Response, next: NextFunction): Promise&lt;void&gt; =&gt; {
    try {
      const { title, content }: CreatePostDto = req.body;
      const authorId = req.user.id;

      // 입력값 검증
      if (!title || !content) {
        const errorResponse: ErrorResponseDTO = {
          success: false,
          message: &#39;제목과 내용을 입력해주세요.&#39;,
        };
        res.status(400).json(errorResponse);
        return;
      }

      if (title.length &gt; 200) {
        const errorResponse: ErrorResponseDTO = {
          success: false,
          message: &#39;제목은 200자를 초과할 수 없습니다.&#39;,
        };
        res.status(400).json(errorResponse);
        return;
      }

      const post = await this.postService.createPost(authorId, { title, content });

      const successResponse: ApiResponse = {
        success: true,
        message: &#39;게시물이 생성되었습니다.&#39;,
        data: { post },
      };

      res.status(201).json(successResponse);
    } catch (error) {
      if (error instanceof Error) {
        const errorResponse: ErrorResponseDTO = {
          success: false,
          message: error.message,
        };
        res.status(400).json(errorResponse);
        return;
      }
      next(error);
    }
  };

  // 게시물 목록 조회
  getPosts = async (req: Request, res: Response, next: NextFunction): Promise&lt;void&gt; =&gt; {
    try {
      const { page, limit, search }: GetPostsQuery = req.query;

      const pageNum = page ? parseInt(String(page), 10) : 1;
      const limitNum = limit ? parseInt(String(limit), 10) : 10;

      // 페이지네이션 검증
      if (pageNum &lt; 1) {
        const errorResponse: ErrorResponseDTO = {
          success: false,
          message: &#39;페이지 번호는 1 이상이어야 합니다.&#39;,
        };
        res.status(400).json(errorResponse);
        return;
      }

      if (limitNum &lt; 1 || limitNum &gt; 100) {
        const errorResponse: ErrorResponseDTO = {
          success: false,
          message: &#39;limit은 1 이상 100 이하여야 합니다.&#39;,
        };
        res.status(400).json(errorResponse);
        return;
      }

      const result = await this.postService.getPosts({
        page: pageNum,
        limit: limitNum,
        search: typeof search === &#39;string&#39; ? search : undefined,
      });

      const successResponse: ApiResponse = {
        success: true,
        data: result,
      };

      res.json(successResponse);
    } catch (error) {
      // 에러 처리 로직...
    }
  };
}</code></pre>
<br>

<hr>
<h2 id="아키텍처-고민과-선택">아키텍처 고민과 선택</h2>
<h4 id="repository-패턴을-사용하지-않은-이유">Repository 패턴을 사용하지 않은 이유</h4>
<p>처음에는 Controller → Service → Repository 3계층 구조를 고려했지만, 다음과 같은 이유로 Controller → Service 구조를 선택했습니다.</p>
<ol>
<li>Prisma가 이미 Repository 역할: 타입 안전성과 쿼리 빌더 제공</li>
<li>프로젝트 규모: 현재는 단순한 CRUD 작업이 주를 이룸</li>
<li>개발 속도: 불필요한 보일러플레이트 코드 제거</li>
<li>유지보수성: 코드 복잡도 감소</li>
</ol>
<h4 id="언제-repository-패턴이-필요할까">언제 Repository 패턴이 필요할까?</h4>
<ul>
<li>복잡한 Raw SQL 쿼리가 많을 때</li>
<li>여러 데이터베이스 지원 필요</li>
<li>캐싱 로직 구현</li>
<li>매우 복잡한 비즈니스 로직</li>
</ul>
<br>

<hr>
<h2 id="실행-결과">실행 결과</h2>
<ul>
<li>게시물 생성 (Create)
<img src="https://velog.velcdn.com/images/my_code/post/f5e8e1e0-55b7-4386-aefc-7c29ef39fd92/image.png" alt=""></li>
</ul>
<ul>
<li>게시물 목록 조회 (Read)
<img src="https://velog.velcdn.com/images/my_code/post/3b3ee81b-828b-4d9a-a510-c465c55e5954/image.png" alt=""></li>
</ul>
<ul>
<li>게시물 상세 조회 (Read)
<img src="https://velog.velcdn.com/images/my_code/post/497c1720-cdf5-438c-9858-bc8a6d2a66eb/image.png" alt=""></li>
</ul>
<ul>
<li>게시물 수정 (Update)
<img src="https://velog.velcdn.com/images/my_code/post/682d3ae3-8a6a-424c-900f-fcb1bb873fcb/image.png" alt="">
<img src="https://velog.velcdn.com/images/my_code/post/d34687b7-b299-40bd-b493-be1d7cf5dedd/image.png" alt=""></li>
</ul>
<ul>
<li>게시물 삭제 (Delete)
<img src="https://velog.velcdn.com/images/my_code/post/8ce1b69f-6deb-48f4-83aa-794b359b03d8/image.png" alt="">
<img src="https://velog.velcdn.com/images/my_code/post/80adda45-4afd-4952-95c3-ccb7db73a2dc/image.png" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[What To Eat] JWT Refresh Token 적용하기]]></title>
            <link>https://velog.io/@my_code/What-To-Eat-JWT-Refresh-Token-%EC%A0%81%EC%9A%A9</link>
            <guid>https://velog.io/@my_code/What-To-Eat-JWT-Refresh-Token-%EC%A0%81%EC%9A%A9</guid>
            <pubDate>Mon, 09 Jun 2025 05:23:49 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/my_code/post/e4260623-b10b-4b30-9115-0784cdc379ca/image.png" alt=""></p>
<blockquote>
<p>이 프로젝트는 이것저것 적용해서 테스트하기 위한 프로젝트입니다. 이번에는 JWT Refresh Token을 적용하려고 합니다.</p>
</blockquote>
<p>이번에는 사용자 인증 시스템의 핵심인 Refresh Token을 적용하는 과정을 단계별로 자세히 설명해드리겠습니다. 단순한 JWT 인증에서 한 단계 더 나아가, 보안성과 사용자 경험을 모두 만족시키는 인증 시스템을 만들어보겠습니다.</p>
<hr>
<h2 id="refresh-token이란">Refresh Token이란?</h2>
<p>일반적인 JWT 토큰 시스템의 딜레마를 먼저 살펴보겠습니다.</p>
<h4 id="짧은-수명-토큰의-문제점">짧은 수명 토큰의 문제점</h4>
<ul>
<li>사용자가 자주 다시 로그인해야 함 (사용자 경험 악화)</li>
<li>서비스 이용 중 갑자기 로그아웃됨</li>
</ul>
<h4 id="긴-수명-토큰의-문제점">긴 수명 토큰의 문제점</h4>
<ul>
<li>토큰이 탈취되면 오랫동안 악용 가능 (보안 위험)</li>
<li>로그아웃해도 토큰이 여전히 유효함</li>
</ul>
<h4 id="refresh-token은-이-딜레마를-해결-가능">Refresh Token은 이 딜레마를 해결 가능</h4>
<ul>
<li>Access Token: 실제 API 요청에 사용하는 짧은 수명의 토큰 (30분)</li>
<li>Refresh Token: Access Token을 갱신하기 위한 긴 수명의 토큰 (7일)</li>
</ul>
<h4 id="은행-시스템으로-비유">은행 시스템으로 비유</h4>
<ul>
<li>Access Token = 일회용 OTP (즉시 사용, 짧은 유효기간)</li>
<li>Refresh Token = 실제 은행 카드 (OTP 발급용, 긴 유효기간)</li>
</ul>
<br>

<hr>
<h2 id="access-token-vs-refresh-token-상세-비교">Access Token vs Refresh Token 상세 비교</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>Access Token</th>
<th>Refresh Token</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>목적</strong></td>
<td>API 접근 권한</td>
<td>토큰 갱신</td>
<td>Access Token은 실제 데이터 접근, Refresh Token은 새 토큰 발급만</td>
</tr>
<tr>
<td><strong>수명</strong></td>
<td>짧음 (15분~1시간)</td>
<td>길음 (7~30일)</td>
<td>보안과 편의성의 균형점</td>
</tr>
<tr>
<td><strong>저장 위치</strong></td>
<td>메모리, localStorage</td>
<td>httpOnly 쿠키 권장</td>
<td>Refresh Token은 XSS 공격 방어 위해 쿠키 사용</td>
</tr>
<tr>
<td><strong>전송 빈도</strong></td>
<td>모든 API 요청</td>
<td>갱신 시에만</td>
<td>네트워크 노출 최소화</td>
</tr>
<tr>
<td><strong>검증 방식</strong></td>
<td>JWT 서명 검증만</td>
<td>JWT + DB 검증</td>
<td>Refresh Token은 서버에서 상태 관리</td>
</tr>
<tr>
<td><strong>무효화</strong></td>
<td>불가능 (만료까지 유효)</td>
<td>즉시 가능</td>
<td>로그아웃 시 DB에서 삭제</td>
</tr>
</tbody></table>
<br>

<hr>
<h2 id="장점과-단점">장점과 단점</h2>
<h4 id="장점">장점</h4>
<ol>
<li>보안성 향상:<ul>
<li>Access Token 탈취 시 최대 30분만 유효</li>
<li>Refresh Token은 DB에 해싱되어 저장함으로 이중 보안</li>
</ul>
</li>
<li>사용자 경험 개선:<ul>
<li>7일간 자동 로그인 연장</li>
<li>백그라운드에서 투명한 토큰 갱신</li>
</ul>
</li>
<li>세밀한 접근 제어:<ul>
<li>디바이스별 개별 로그아웃 가능</li>
<li>의심스러운 활동 시 특정 세션만 무효화</li>
</ul>
</li>
<li>확장성:<ul>
<li>마이크로서비스 환경에서 중앙 인증 서버 구축 가능</li>
<li>다양한 클라이언트 (웹, 모바일) 동시 지원</li>
</ul>
</li>
</ol>
<h4 id="단점">단점</h4>
<ol>
<li>구현 복잡성:<ul>
<li>토큰 상태 관리를 위한 DB 저장소 필요</li>
<li>프론트엔드에서 토큰 갱신 로직 구현 필요</li>
</ul>
</li>
<li>성능 오버헤드:<ul>
<li>Refresh Token 검증 시 DB 조회 필요 (Redis 활용 가능?)</li>
<li>토큰 갱신을 위한 추가 네트워크 요청</li>
</ul>
</li>
<li>동기화 복잡성:<ul>
<li>여러 탭/디바이스에서 토큰 상태 동기화 (Redis 활용 가능)</li>
<li>Race condition 방지 로직 필요 (lock 매커니즘 활용 가능)</li>
</ul>
</li>
</ol>
<br>

<hr>
<h2 id="전체-토큰-라이프사이클">전체 토큰 라이프사이클</h2>
<p><img src="https://velog.velcdn.com/images/my_code/post/70496b96-d63c-4da1-a3de-b373477db60e/image.png" alt=""></p>
<hr>
<h2 id="구현-과정">구현 과정</h2>
<h4 id="환경-변수-설정">환경 변수 설정</h4>
<p>Access Token과 Refresh Token은 반드시 다른 시크릿 키를 사용해야 합니다.</p>
<pre><code># .env 파일
JWT_ACCESS_SECRET=
JWT_REFRESH_SECRET=</code></pre><br>

<h4 id="데이터베이스-스키마-수정">데이터베이스 스키마 수정</h4>
<p>그런 경우는 많이 없지만, DB가 탈취되어도 원본 토큰을 알 수 없도록 해싱처리했습니다. 그리고 JWT 자체 만료 시간과 별도로 DB에서도 관리하기 위해 별도의 필드를 추가했습니다. 만약에 여러 탭에서 동시 로그인이 가능하게 만든다면 Refresh Token을 위한 별도의 테이블을 만들고 User와 1:N 관계를 맺을 수 있습니다.</p>
<pre><code class="language-sql">// prisma/schema.prisma

model User {
  id                     String    @id @default(uuid())
  email                  String    @unique
  password               String    
  nickname               String?   
  socialId               String?   @unique 
  refreshToken           String?   // bcrypt 해싱된 리프레시 토큰
  refreshTokenExpiresAt  DateTime? // 리프레시 토큰 만료 시간
  createdAt              DateTime  @default(now())
  updatedAt              DateTime  @updatedAt

  @@map(&quot;users&quot;)
}</code></pre>
<br>

<h4 id="jwt-서비스-구현---토큰-관리의-핵심">JWT 서비스 구현 - 토큰 관리의 핵심</h4>
<p>Access Token을 Refresh 용도로 사용하는 것을 방지하기 위해 타입별로 다른 시크릿 키 사용하도록 구현했습니다. 
그리고 Refresh Token의 원본 토큰은 클라이언트에게, 해시 처리된 것은 DB에 저장합니다.</p>
<pre><code class="language-typescript">// src/services/jwt.service.ts
import jwt from &#39;jsonwebtoken&#39;;
import { prisma } from &#39;../utils/prisma.util&#39;;
import bcrypt from &#39;bcrypt&#39;;

export class JwtService {
  private accessSecret: string;
  private refreshSecret: string;

  constructor() {
    this.accessSecret = process.env.JWT_ACCESS_SECRET || &#39;your-accessSecret-key&#39;;
    this.refreshSecret = process.env.JWT_REFRESH_SECRET || &#39;your-refreshSecret-key&#39;;
  }

  // 액세스 토큰 생성 (30분)
  generateAccessToken(userId: string): string {
    const payload = {
      id: userId,
      type: &#39;access&#39;,  // 토큰 타입 명시
      iat: Math.floor(Date.now() / 1000),
      exp: Math.floor(Date.now() / 1000) + 30 * 60, // 30분
    };

    return jwt.sign(payload, this.accessSecret);
  }

  // 리프레시 토큰 생성 및 DB 저장
  async generateRefreshToken(userId: string): Promise&lt;string&gt; {
    const payload = {
      id: userId,
      type: &#39;refresh&#39;,  // 토큰 타입 명시
      iat: Math.floor(Date.now() / 1000),
      exp: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60, // 7일
    };

    const refreshToken = jwt.sign(payload, this.refreshSecret);

    // bcrypt로 토큰 해싱
    const tokenHash = await bcrypt.hash(refreshToken, 10);

    const expiresAt = new Date();
    expiresAt.setDate(expiresAt.getDate() + 7);

    // DB에 해시형태의 리프레시 토큰 저장
    await prisma.user.update({
      where: { id: userId },
      data: {
        refreshToken: tokenHash,
        refreshTokenExpiresAt: expiresAt,
      },
    });

    return refreshToken; // 원본 토큰 반환 (클라이언트 전송용)
  }

  // 토큰 쌍 생성 - 로그인/갱신 시 사용
  async generateTokens(userId: string) {
    const accessToken = this.generateAccessToken(userId);
    const refreshToken = await this.generateRefreshToken(userId);

    return {
      accessToken,
      refreshToken,
    };
  }
}</code></pre>
<br>

<h4 id="로그인-구현-및-토큰-발급">로그인 구현 및 토큰 발급</h4>
<ul>
<li>Passport 인증: 이메일/비밀번호 검증을 Passport에 위임</li>
<li>토큰 생성 위임: Controller는 JwtService에 토큰 생성 요청</li>
<li>자동 DB 저장: JwtService 내부에서 Refresh Token 해싱 후 저장</li>
<li>보안 응답: 비밀번호 제외한 사용자 정보만 반환</li>
</ul>
<pre><code class="language-typescript">// src/controllers/auth.controller.ts
export class AuthController {
  constructor(
    private authService: AuthService,
    private jwtService: JwtService
  ) {}

  // Passport Local Strategy를 사용한 로그인
  signInWithPassport = async (req: Request, res: Response, next: NextFunction): Promise&lt;void&gt; =&gt; {
    passport.authenticate(&#39;local&#39;, async (error: any, user: any, info: any) =&gt; {
      if (error) {
        return next(error);
      }

      if (!user) {
        const errorResponse: ErrorResponseDTO = {
          success: false,
          message: info?.message || &#39;로그인에 실패했습니다.&#39;,
        };
        res.status(401).json(errorResponse);
        return;
      }

      try {
        // JWT 토큰 생성 (JwtService 통해서)
        const { accessToken, refreshToken } = await this.jwtService.generateTokens(user.id);

        const { password: _, ...userWithoutPassword } = user;

        const successResponse: ApiResponse&lt;AuthResponseDTO&gt; = {
          success: true,
          message: &#39;로그인이 완료되었습니다.&#39;,
          data: {
            user: userWithoutPassword,
            accessToken,
            refreshToken,
          },
        };
        res.json(successResponse);
      } catch (error) {
        next(error);
      }
    })(req, res, next);
  };
}</code></pre>
<p><img src="https://velog.velcdn.com/images/my_code/post/442a38ff-fa67-4b7b-b101-0b49b0049a17/image.png" alt=""></p>
<h4 id="refresh-token-검증-미들웨어">Refresh Token 검증 미들웨어</h4>
<p>JWT Service의 verifyRefreshToken() 메서드를 통해 JWT 서명 검증, 토큰 타입 확인, DB 확인, 토큰 해시 비교, 만료 시간 확인 과정을 거칩니다. </p>
<pre><code class="language-typescript">// src/middlewares/auth.middleware.ts

// Refresh Token 전용 미들웨어
export const authenticateRefreshToken = async (
  req: Request,
  res: Response,
  next: NextFunction
) =&gt; {
  try {
    const authHeader = req.headers.authorization;

    if (!authHeader?.startsWith(&#39;Bearer &#39;)) {
      return res.status(401).json({
        success: false,
        message: &#39;Refresh Token이 필요합니다.&#39;,
      });
    }

    const token = authHeader.substring(7);
    const jwtService = new JwtService();

    // 복잡한 검증 과정 (JWT + DB + 해시 비교)
    const decoded = await jwtService.verifyRefreshToken(token);

    (req as AuthenticatedRequest).user = { id: decoded.id };
    (req as AuthenticatedRequest).refreshToken = token;
    next();

  } catch (error) {
    return res.status(401).json({
      success: false,
      message: &#39;토큰 인증에 실패했습니다.&#39;,
    });
  }
};</code></pre>
<pre><code class="language-typescript">// 리프레시 토큰 검증 (다층 보안)
async verifyRefreshToken(refreshToken: string): Promise&lt;any&gt; {
  try {
    // 1. JWT 서명 검증
    const decoded = jwt.verify(refreshToken, this.refreshSecret) as any;

    // 2. 토큰 타입 확인
    if (decoded.type !== &#39;refresh&#39;) {
      throw new Error(&#39;유효하지 않은 토큰 타입입니다.&#39;);
    }

    // 3. DB에서 사용자와 해시된 토큰 조회
    const user = await prisma.user.findUnique({
      where: { id: decoded.id },
      select: {
        id: true,
        refreshToken: true,
        refreshTokenExpiresAt: true,
      },
    });

    if (!user || !user.refreshToken || !user.refreshTokenExpiresAt) {
      throw new Error(&#39;유효하지 않은 리프레시 토큰입니다.&#39;);
    }

    // 4. bcrypt로 토큰 해시 비교
    const isTokenValid = await bcrypt.compare(refreshToken, user.refreshToken);
    if (!isTokenValid) {
      throw new Error(&#39;유효하지 않은 리프레시 토큰입니다.&#39;);
    }

    // 5. 만료 시간 확인
    if (new Date() &gt; user.refreshTokenExpiresAt) {
      await this.revokeRefreshToken(user.id);
      throw new Error(&#39;만료된 리프레시 토큰입니다.&#39;);
    }

    return { id: user.id };
  } catch (error) {
    throw new Error(&#39;유효하지 않은 리프레시 토큰입니다.&#39;);
  }
}</code></pre>
<h4 id="api-요청-및-토큰-검증-흐름">API 요청 및 토큰 검증 흐름</h4>
<p><img src="https://velog.velcdn.com/images/my_code/post/1cb7cae3-e8f7-4982-bc0e-4496f22d1f3e/image.png" alt=""></p>
<br>

<h4 id="토큰-갱신-refresh-로직">토큰 갱신 (Refresh) 로직</h4>
<ul>
<li>미들웨어 분리: 복잡한 검증은 authenticateRefreshToken에서 처리</li>
<li>단순한 재생성: Controller에서는 새 토큰 생성만 담당</li>
<li>자동 덮어쓰기: generateTokens()에서 기존 토큰을 새 토큰으로 교체</li>
</ul>
<pre><code class="language-typescript">// 토큰 재발급
refreshToken = async (req: Request, res: Response, next: NextFunction): Promise&lt;void&gt; =&gt; {
  try {
    const user = req.user as any;

    // 새로운 토큰 쌍 생성 (간단한 재생성)
    const tokens = await this.jwtService.generateTokens(user.id);

    const successResponse: ApiResponse = {
      success: true,
      message: &#39;토큰이 갱신되었습니다.&#39;,
      data: {
        accessToken: tokens.accessToken,
        refreshToken: tokens.refreshToken,
      },
    };
    res.json(successResponse);
  } catch (error) {
    if (error instanceof Error) {
      const errorResponse: ErrorResponseDTO = {
        success: false,
        message: error.message,
      };
      res.status(401).json(errorResponse);
      return;
    }
    next(error);
  }
};</code></pre>
<br>

<hr>
<h2 id="실행-결과">실행 결과</h2>
<ul>
<li><strong>로그인 (passport)</strong>
<img src="https://velog.velcdn.com/images/my_code/post/7db9b99e-b2de-4dda-be5c-90d96ffa3c7b/image.png" alt=""></li>
</ul>
<ul>
<li><strong>로그아웃</strong>
<img src="https://velog.velcdn.com/images/my_code/post/6a7f1dbb-387f-430e-8bce-48b6cff9ada4/image.png" alt=""></li>
</ul>
<ul>
<li><strong>토큰 재발급</strong>
<img src="https://velog.velcdn.com/images/my_code/post/94202969-b7bf-41c4-ac8b-2061d6aa1d10/image.png" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[What To Eat] Passport 활용한 인증 구현하기]]></title>
            <link>https://velog.io/@my_code/What-To-Eat-Passport-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9D%B8%EC%A6%9D-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@my_code/What-To-Eat-Passport-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9D%B8%EC%A6%9D-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 31 May 2025 09:21:42 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 프로젝트는 이것저것 적용해서 테스트하기 위한 프로젝트입니다. 이번에는 Passport를 이용한 인증 시스템을 구현하려고 합니다. </p>
<p>아직은 간단한 내용이지만 조금씩 구현하면서 세부적인 내용을 채울 예정입니다. </p>
</blockquote>
<p>현대 웹 애플리케이션에서 사용자 인증은 핵심 기능 중 하나입니다. 이번 글에서는 Express, TypeScript, Passport.js를 활용하여 확장 가능하고 타입 안전한 인증 시스템을 구축하는 과정을 구현 하도록 하겠습니다. TypeScript을 사용했기에 타입 안정성을 생각해야 했습니다.</p>
<hr>
<h2 id="인증에-사용할-타입-정의">인증에 사용할 타입 정의</h2>
<p>TypeScript의 장점을 살리기 위해 먼저 타입 정의를 작성했습니다.</p>
<ul>
<li><code>AuthenticatedRequest</code>로 인증된 요청의 타입을 확장</li>
<li>DTO 패턴으로 입력/출력 데이터 구조 명확화</li>
<li>제네릭을 활용한 재사용 가능한 응답 타입</li>
</ul>
<pre><code class="language-typescript">// apps/backend/src/types/auth.types.ts

import { Request } from &#39;express&#39;;

// User Entity Type
export interface UserEntity {
  id: string;
  email: string;
  createdAt: Date;
}

// Request DTO
export interface SignUpRequestDTO {
  email: string;
  password: string;
}

// 인증된 사용자 정보가 포함된 Request
export interface AuthenticatedRequest extends Request {
  user: UserEntity;
}

// Response DTO
export interface ApiResponse&lt;T = any&gt; {
  success: boolean;
  message?: string;
  data?: T;
}
export interface SignUpResponseDTO {
  user: UserEntity;
}
export interface AuthResponseDTO {
  user: UserEntity;
  token: string;
}

// Error Response DTO
export interface ErrorResponseDTO {
  success: false;
  message: string;
  error?: string;
}</code></pre>
<br>

<hr>
<h2 id="passport-전략-설정">Passport 전략 설정</h2>
<p>Local Strategy와 JWT Strategy를 구성했습니다. 여기서 설정한 Passport에 대한 설정을 사용할 때는 항상 다른 라우터나 미들웨어보다 먼저 실행되어야 합니다.</p>
<p>Passport 전략들이 미리 등록되어야 <code>passport.authenticate()</code> 호출 시 정상 작동합니다.</p>
<pre><code class="language-typescript">// apps/backend/src/config/passport.config.ts

import passport from &#39;passport&#39;;
import { Strategy as LocalStrategy } from &#39;passport-local&#39;;
import { Strategy as JWTStrategy, ExtractJwt } from &#39;passport-jwt&#39;;
import bcrypt from &#39;bcrypt&#39;;
import { prisma } from &#39;../utils/prisma.util&#39;;

// Local Strategy (로그인에서 사용)
passport.use(
  new LocalStrategy(
    {
      usernameField: &#39;email&#39;,
      passwordField: &#39;password&#39;,
    },
    async (email: string, password: string, done) =&gt; {
      try {
        // 사용자 찾기
        const user = await prisma.user.findUnique({
          where: { email },
        });

        if (!user) {
          return done(null, false, { message: &#39;이메일 또는 비밀번호가 올바르지 않습니다.&#39; });
        }

        // 비밀번호 확인
        const isPasswordValid = await bcrypt.compare(password, user.password);
        if (!isPasswordValid) {
          return done(null, false, { message: &#39;이메일 또는 비밀번호가 올바르지 않습니다.&#39; });
        }

        return done(null, user);
      } catch (error) {
        return done(error);
      }
    }
  )
);

// JWT Strategy (토큰 인증에서 사용)
passport.use(
  new JWTStrategy(
    {
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET || &#39;your-secret-key&#39;,
    },
    async (payload, done) =&gt; {
      try {
        const user = await prisma.user.findUnique({
          where: { id: payload.id },
        });

        if (user) {
          return done(null, user);
        } else {
          return done(null, false);
        }
      } catch (error) {
        return done(error, false);
      }
    }
  )
);

export default passport;</code></pre>
<br>

<hr>
<h2 id="jwt-서비스-분리">JWT 서비스 분리</h2>
<p>처음에는 <code>AuthService</code>에 JWT 로직이 포함되어 있었는데, 다음과 같은 이유로 분리시켰습니다.</p>
<ul>
<li><code>AuthService</code>가 너무 많은 책임을 가짐</li>
<li>JWT 로직을 다른 서비스에서 재사용 가능</li>
<li>테스트 및 유지보수 용이성 향상</li>
</ul>
<p>사실 이렇게 작은 프로젝트에서는 굳이 분리할 필요는 없지만, 분리하는 방법도 구현해보고 싶었기에 찾아서 적용했습니다.</p>
<pre><code class="language-typescript">// apps/backend/src/services/jwt.service.ts

import jwt, { SignOptions } from &#39;jsonwebtoken&#39;;

export interface JwtPayload {
  id: string;
  iat?: number;
  exp?: number;
}

export class JwtService {
  private readonly secret: string;
  private readonly expiresIn: string;

  constructor() {
    this.secret = process.env.JWT_SECRET || &#39;your-secret-key&#39;;
    this.expiresIn = process.env.JWT_EXPIRES_IN || &#39;7d&#39;;
  }

  // JWT 토큰 생성
  generateToken(userId: string): string {
    const payload: JwtPayload = { id: userId };

    const options: SignOptions = {
      expiresIn: this.expiresIn as any,
    };

    return jwt.sign(payload, this.secret, options);
  }

  // JWT 토큰 검증
  verifyToken(token: string): JwtPayload {
    try {
      return jwt.verify(token, this.secret) as JwtPayload;
    } catch (error) {
      throw new Error(&#39;유효하지 않은 토큰입니다.&#39;);
    }
  }</code></pre>
<br>

<hr>
<h2 id="authservice-간소화">AuthService 간소화</h2>
<p>JWT 로직을 분리한 후 AuthService는 순수한 인증 비즈니스 로직만 담당합니다. 회원가입, 사용자 정보 조회만 담당하도록 수정했습니다.</p>
<pre><code class="language-typescript">// apps/backend/src/services/auth.service.ts

import bcrypt from &#39;bcrypt&#39;;
import { prisma } from &#39;../utils/prisma.util&#39;;
import { SignUpRequestDTO, UserEntity, SignUpResponseDTO } from &#39;../types/auth.types&#39;;

export class AuthService {
  private readonly saltRounds = 10;

  // 회원가입
  async signUp(data: SignUpRequestDTO): Promise&lt;SignUpResponseDTO&gt; {
    const { email, password } = data;

    // 이메일 중복 확인
    const existingUser = await prisma.user.findUnique({
      where: { email },
    });

    if (existingUser) {
      throw new Error(&#39;이미 존재하는 이메일입니다.&#39;);
    }

    // 비밀번호 해싱
    const hashedPassword = await bcrypt.hash(password, this.saltRounds);

    // 사용자 생성
    const user = await prisma.user.create({
      data: {
        email,
        password: hashedPassword,
      },
    });

    // 비밀번호 제외하고 반환
    const { password: _, ...userWithoutPassword } = user;
    return { user: userWithoutPassword };
  }

  // 사용자 정보 조회
  async getUserById(id: string): Promise&lt;UserEntity&gt; {
    const user = await prisma.user.findUnique({
      where: { id },
      select: {
        id: true,
        email: true,
        createdAt: true,
      },
    });

    if (!user) {
      throw new Error(&#39;사용자를 찾을 수 없습니다.&#39;);
    }

    return user;
  }
}</code></pre>
<br>

<hr>
<h2 id="인증-미들웨어-구현">인증 미들웨어 구현</h2>
<p>Passport JWT Strategy를 활용한 미들웨어를 구현했습니다. 사용자의 <strong>로그인 여부</strong>를 확인하기 위한 미들웨어입니다.</p>
<pre><code class="language-typescript">// apps/backend/src/middlewares/auth.middleware.ts

import { Request, Response, NextFunction } from &#39;express&#39;;
import passport from &#39;passport&#39;;

// Passport JWT 미들웨어 (사용자 인증 확인용 미들웨어)
export const authenticateJWT = (req: Request, res: Response, next: NextFunction) =&gt; {
  passport.authenticate(&#39;jwt&#39;, { session: false }, (error: any, user: any, info: any) =&gt; {
    if (error) {
      return res.status(500).json({
        success: false,
        message: &#39;인증 처리 중 오류가 발생했습니다.&#39;,
      });
    }

    if (!user) {
      return res.status(401).json({
        success: false,
        message: info?.message || &#39;인증이 필요합니다.&#39;,
      });
    }

    // req.user에 사용자 정보 설정
    req.user = user;
    next();
  })(req, res, next);
};</code></pre>
<br>

<hr>
<h2 id="의존성-주입-적용한-컨트롤러">의존성 주입 적용한 컨트롤러</h2>
<p>Nest.js에서 사용했던 <strong>의존성 주입 패턴</strong>을 한 번 사용해 보았습니다. 의존성 주입을 사용하면 테스트 시 mock 함수에 주입이 용이하고, 서비스 간 결합도를 감소시켜 코드 유연성을 증가시킬 수 있습니다.</p>
<p>Passport Local 전략인 <code>passport.authenticate(&#39;local&#39;, ...)</code>을 사용해서 로그인 기능을 구현했습니다.</p>
<pre><code class="language-typescript">// apps/backend/src/controllers/auth.controller.ts

import { Request, Response, NextFunction } from &#39;express&#39;;
import passport from &#39;passport&#39;;
import { AuthService } from &#39;../services/auth.service&#39;;
import {
  SignUpRequestDTO,
  AuthenticatedRequest,
  ApiResponse,
  ErrorResponseDTO,
  SignUpResponseDTO,
  AuthResponseDTO,
} from &#39;../types/auth.types&#39;;
import { JwtService } from &#39;../services/jwt.service&#39;;

export class AuthController {
  constructor(
    private authService: AuthService,
    private jwtService: JwtService
  ) {}

  // 회원가입
  signUp = async (req: Request, res: Response, next: NextFunction): Promise&lt;void&gt; =&gt; {
    try {
      const { email, password }: SignUpRequestDTO = req.body;

      // 입력값 검증
      if (!email || !password) {
        const errorResponse: ErrorResponseDTO = {
          success: false,
          message: &#39;이메일과 비밀번호를 입력해주세요.&#39;,
        };
        res.status(400).json(errorResponse);
        return;
      }

      // 이메일 형식 검증
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailRegex.test(email)) {
        const errorResponse: ErrorResponseDTO = {
          success: false,
          message: &#39;올바른 이메일 형식을 입력해주세요.&#39;,
        };
        res.status(400).json(errorResponse);
        return;
      }

      // 비밀번호 길이 검증
      if (password.length &lt; 4) {
        const errorResponse: ErrorResponseDTO = {
          success: false,
          message: &#39;비밀번호는 최소 4자 이상이어야 합니다.&#39;,
        };
        res.status(400).json(errorResponse);
        return;
      }

      const result = await this.authService.signUp({ email, password });

      const successResponse: ApiResponse&lt;SignUpResponseDTO&gt; = {
        success: true,
        message: &#39;회원가입이 완료되었습니다.&#39;,
        data: result,
      };

      res.status(201).json(successResponse);
    } catch (error) {
      if (error instanceof Error) {
        const errorResponse: ErrorResponseDTO = {
          success: false,
          message: error.message,
        };
        res.status(400).json(errorResponse);
        return;
      }
      next(error);
    }
  };

  // Passport Local Strategy를 사용한 로그인
  signInWithPassport = (req: Request, res: Response, next: NextFunction): void =&gt; {
    passport.authenticate(&#39;local&#39;, (error: any, user: any, info: any) =&gt; {
      if (error) {
        return next(error);
      }

      if (!user) {
        const errorResponse: ErrorResponseDTO = {
          success: false,
          message: info?.message || &#39;로그인에 실패했습니다.&#39;,
        };
        res.status(401).json(errorResponse);
        return;
      }

      // JWT 토큰 생성 (JwtService 통해서)
      const token = this.jwtService.generateToken(user.id);

      const { password: _, ...userWithoutPassword } = user;

      const successResponse: ApiResponse&lt;AuthResponseDTO&gt; = {
        success: true,
        message: &#39;로그인이 완료되었습니다.&#39;,
        data: {
          user: userWithoutPassword,
          token,
        },
      };
      res.json(successResponse);
    })(req, res, next);
  };

  // 사용자 정보 조회
  getProfile = async (
    req: AuthenticatedRequest,
    res: Response,
    next: NextFunction
  ): Promise&lt;void&gt; =&gt; {
    try {
      const userId = req.user.id;

      const user = await this.authService.getUserById(userId);

      const successResponse: ApiResponse = {
        success: true,
        data: { user },
      };
      res.json(successResponse);
    } catch (error) {
      if (error instanceof Error) {
        const errorResponse: ErrorResponseDTO = {
          success: false,
          message: error.message,
        };
        res.status(404).json(errorResponse);
        return;
      }
      next(error);
    }
  };
}</code></pre>
<br>

<hr>
<h2 id="라우터-구성과-타입-문제-해결">라우터 구성과 타입 문제 해결</h2>
<p>Express와 TypeScript의 타입 호환성 문제를 현실적으로 해결했습니다. 일단 프로젝트의 규모가 작기 때문에 <code>as any</code> 캐스팅으로 Express 호환성을 확보했습니다. </p>
<p><code>as any</code>가 필요한 이유는 <code>AuthController.getProfile</code> 메서드가 <code>AuthenticatedRequest</code> 타입을 받지만, Express Router는 모든 핸들러가 기본 <code>Request</code> 타입을 받는다고 가정합니다. 미들웨어 체인에서 <code>authenticateJWT</code>가 <code>req.user</code>를 추가하는 동적 변화를 TypeScript가 추적하지 못하기 때문입니다.</p>
<p>그래도 혹시나 <code>as any</code>가 너무 많이 사용된다면 <code>헬퍼 함수</code>나 <code>Request handler</code>를 추가로 구현할 계획입니다.</p>
<pre><code class="language-typescript">// apps/backend/src/routes/auth.routes.ts

import { Router } from &#39;express&#39;;
import { AuthController } from &#39;../controllers/auth.controller&#39;;
import { authenticateJWT } from &#39;../middlewares/auth.middleware&#39;;
import { AuthenticatedRequest } from &#39;../types/auth.types&#39;;
import { AuthService } from &#39;../services/auth.service&#39;;
import { JwtService } from &#39;../services/jwt.service&#39;;

const router = Router();
const authController = new AuthController(new AuthService(), new JwtService());

// 회원가입
router.post(&#39;/signup&#39;, authController.signUp);

// 로그인 (Passport Local Strategy 사용)
router.post(&#39;/signin&#39;, authController.signInWithPassport);

// 사용자 프로필 조회 (인증 필요)
router.get(&#39;/profile&#39;, authenticateJWT, authController.getProfile as any);

// 토큰 유효성 검증
router.get(&#39;/verify&#39;, authenticateJWT, (req, res) =&gt; {
  const authReq = req as AuthenticatedRequest;
  res.json({
    success: true,
    message: &#39;유효한 토큰입니다.&#39;,
    data: {
      user: authReq.user,
    },
  });
});

export default router;</code></pre>
<br>

<hr>
<h2 id="메인-서버-통합">메인 서버 통합</h2>
<p>모든 구성 요소를 연결하는 메인 서버를 설정했습니다.</p>
<pre><code class="language-typescript">// apps/backend/src/index.ts

import express, { NextFunction, Request, Response } from &#39;express&#39;;
import cors from &#39;cors&#39;;
import dotenv from &#39;dotenv&#39;;
import cookieParser from &#39;cookie-parser&#39;;

dotenv.config();

import { errorHandlerMiddleware } from &#39;./middlewares/error-handler.middleware&#39;;
import { prisma } from &#39;./utils/prisma.util&#39;;
import &#39;./config/passport.config&#39;; // Passport 설정 초기화
import authRoutes from &#39;./routes/auth.routes&#39;;

const app = express();
const SERVER_PORT = process.env.SERVER_PORT || 3000;

// 미들웨어
app.use(cors());
app.use(express.json());
app.use(cookieParser());

// 라우트 정의
app.get(&#39;/&#39;, (req: Request, res: Response) =&gt; {
  res.json({ message: &#39;API 서버가 실행 중입니다.&#39; });
});

// 인증 라우트
app.use(&#39;/api/auth&#39;, authRoutes);

console.log(&#39;DB 연결 테스트 시작...&#39;);
prisma.$queryRaw`SELECT 1`;

// 에러 처리 미들웨어 등록
app.use(errorHandlerMiddleware);

app.listen(SERVER_PORT, () =&gt; {
  console.log(`서버가 포트 ${SERVER_PORT}에서 실행 중입니다`);
});</code></pre>
<br>

<hr>
<h2 id="실행-결과">실행 결과</h2>
<ul>
<li><p><strong>회원가입</strong>
<img src="https://velog.velcdn.com/images/my_code/post/e276ba84-8b43-41de-9a44-da1626b4e680/image.png" alt=""></p>
</li>
<li><p><strong>로그인</strong>
<img src="https://velog.velcdn.com/images/my_code/post/2039b21d-23da-40ca-854b-f254646441fa/image.png" alt=""></p>
</li>
<li><p><strong>프로필 조회</strong>
<img src="https://velog.velcdn.com/images/my_code/post/1ed761f6-0e50-4ac7-b170-71b63ead0e78/image.png" alt=""></p>
</li>
</ul>
<br>

<hr>
<h2 id="트러블-슈팅">트러블 슈팅</h2>
<h3 id="jwt-토큰-생성-시-typescript-타입-에러">JWT 토큰 생성 시 TypeScript 타입 에러</h3>
<p><strong>문제:</strong>
JWT 토큰 생성 시 <code>expiresIn</code> 옵션에서 타입 에러가 발생했습니다.</p>
<pre><code class="language-typescript">generateToken(userId: string): string {
  return jwt.sign({ id: userId }, this.jwtSecret, { 
    expiresIn: this.jwtExpiresIn  // Type &#39;string&#39; is not assignable to type &#39;number&#39;
  });
}</code></pre>
<pre><code>No overload matches this call.
Type &#39;string&#39; is not assignable to type &#39;number | StringValue | undefined&#39;.</code></pre><p><strong>원인:</strong></p>
<ul>
<li><code>jsonwebtoken</code> 라이브러리의 TypeScript 타입 정의가 불완전함</li>
<li><code>expiresIn</code>은 실제로는 &#39;7d&#39;, &#39;1h&#39; 같은 문자열을 받지만, 타입 정의에서는 제한적으로 정의됨</li>
<li>환경변수에서 가져온 문자열이 라이브러리 타입과 정확히 일치하지 않음</li>
</ul>
<p><strong>해결:</strong>
타입 캐스팅을 사용하여 해결했습니다.</p>
<ul>
<li>실제 런타임에서는 완벽하게 작동함이 확인됨</li>
<li>라이브러리 자체의 타입 정의 문제이므로 우리 코드의 문제가 아님</li>
</ul>
<pre><code class="language-typescript">generateToken(userId: string): string {
  const payload: JwtPayload = { id: userId };

  const options: SignOptions = {
    expiresIn: this.expiresIn as any, // 타입 캐스팅으로 해결
  };

  return jwt.sign(payload, this.secret, options);
}</code></pre>
<br>

<h3 id="express-router에서-authenticatedrequest-타입-호환성-문제">Express Router에서 AuthenticatedRequest 타입 호환성 문제</h3>
<p><strong>문제:</strong>
Express Router에서 <code>AuthenticatedRequest</code> 타입을 사용하는 컨트롤러 메서드를 등록할 때 타입 에러가 발생했습니다.</p>
<pre><code class="language-typescript">router.get(&#39;/profile&#39;, authenticateJWT, authController.getProfile);</code></pre>
<pre><code>Argument of type &#39;(req: AuthenticatedRequest, res: Response, next: NextFunction) =&gt; Promise&lt;void&gt;&#39; 
is not assignable to parameter of type &#39;RequestHandler&#39;.
Type &#39;AuthenticatedRequest&#39; is not assignable to parameter of type &#39;Request&#39;.</code></pre><p><strong>원인:</strong></p>
<ul>
<li>Express Router의 타입 시스템 한계: Express에서는 모든 핸들러가 동일한 <code>Request</code> 타입을 받는다고 가정함</li>
<li>미들웨어 체인의 동적 특성: <code>authenticateJWT</code> 미들웨어가 <code>req.user</code>를 추가하지만, 컴파일 타임에는 이를 추적할 수 없음</li>
<li>컴파일 타임 vs 런타임 차이:<ul>
<li>컴파일 타임: TypeScript는 <code>req.user</code>가 없다고 판단</li>
<li>런타임: Passport가 실제로 <code>req.user</code>를 설정함</li>
</ul>
</li>
</ul>
<p><strong>해결:</strong>
타입 캐스팅을 사용하여 Express와의 호환성을 확보했습니다.</p>
<ul>
<li>명확하고 즉시 적용 가능</li>
<li>타입 캐스팅 위치가 명확하여 유지보수 용이</li>
<li>프로젝트 규모에 적합한 현실적 접근<pre><code class="language-typescript">router.get(&#39;/profile&#39;, authenticateJWT, authController.getProfile as any);
</code></pre>
</li>
</ul>
<p>// 인라인 핸들러에서는 명시적 캐스팅 사용
router.get(&#39;/verify&#39;, authenticateJWT, (req, res) =&gt; {
  const authReq = req as AuthenticatedRequest;
  res.json({
    success: true,
    data: { user: authReq.user },
  });
});</p>
<pre><code>
**추가로 찾은 해결 방법들:**
1. **Module Augmentation**: 전역 타입 확장
```typescript
declare global {
  namespace Express {
    interface Request {
      user?: UserEntity;
    }
  }
}</code></pre><ol start="2">
<li><strong>헬퍼 함수</strong>: 타입 변환 유틸리티<pre><code class="language-typescript">// 복잡도 대비 효과가 낮아 제외
const authenticatedRoute = (handler: (req: AuthenticatedRequest, res: Response) =&gt; void) =&gt; {
return (req: Request, res: Response, next: NextFunction) =&gt; {
 handler(req as AuthenticatedRequest, res);
};
};</code></pre>
</li>
</ol>
<br>

<h3 id="passport-설정-로딩-순서-문제">Passport 설정 로딩 순서 문제</h3>
<p><strong>문제:</strong>
Passport 설정이 제대로 로드되지 않아 인증이 작동하지 않았습니다.</p>
<pre><code>import authRoutes from &#39;./routes/auth.routes&#39;;
import &#39;./config/passport.config&#39;; // 늦게 로드됨</code></pre><p><strong>원인:</strong></p>
<ul>
<li><strong>모듈 로딩 순서</strong>: Passport 전략 설정이 라우트보다 늦게 로드됨</li>
<li><strong>passport.authenticate() 호출 시점</strong>: 전략이 등록되기 전에 사용하려고 함</li>
<li><strong>ES6 import의 호이스팅</strong>: import 순서가 실행 순서에 영향</li>
</ul>
<p><strong>해결:</strong>
Passport 설정을 다른 모듈들보다 먼저 로드하도록 순서를 조정했습니다.</p>
<ul>
<li>환경변수 로딩 → Passport 설정 → 라우트 순서 준수</li>
<li>의존성 있는 모듈들의 로딩 순서 중요성 인식</li>
</ul>
<pre><code class="language-typescript">dotenv.config();                    // 1. 환경변수 먼저
import &#39;./config/passport.config&#39;;  // 2. Passport 설정
import authRoutes from &#39;./routes/auth.routes&#39;; // 3. 라우트 마지막</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[What To Eat] MySQL + Prisma 데이터베이스 연결하기]]></title>
            <link>https://velog.io/@my_code/What-To-Eat-MySQL-Prisma-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@my_code/What-To-Eat-MySQL-Prisma-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 29 May 2025 14:26:31 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/my_code/post/63d8657a-88dc-45c6-880d-0151e41de98a/image.png" alt=""></p>
<blockquote>
<p>이번에는 백엔드 API 구현에 앞서 데이터베이스를 연결하려고 합니다.</p>
</blockquote>
<p>모던 웹 개발에서 백엔드 API 서버를 구축할 때 데이터베이스 연결과 에러 처리는 필수적인 요소입니다. 이번 글은 Express.js와 TypeScript를 기반으로 Prisma ORM을 사용하여 MySQL 데이터베이스에 연결하고, 에러 처리 미들웨어를 구현하는 과정을 설명하겠습니다.</p>
<hr>
<h2 id="mysql--prisma-데이터베이스-연결하기">MySQL + Prisma 데이터베이스 연결하기</h2>
<h3 id="프로젝트-기본-설정">프로젝트 기본 설정</h3>
<h4 id="필요한-패키지-설치">필요한 패키지 설치</h4>
<p><code>@prisma/client</code>: 런타임에서 데이터베이스와 상호작용하는 클라이언트
<code>prisma</code>: 개발 도구 (마이그레이션, 스키마 관리 등)</p>
<pre><code class="language-bash"># 백엔드 디렉토리로 이동
cd apps/backend

# Prisma 관련 패키지 설치
npm install @prisma/client
npm install -D prisma

# 기타 필요한 패키지
npm install express cors dotenv cookie-parser
npm install -D @types/express @types/cors @types/cookie-parser</code></pre>
<h4 id="prisma-초기화">Prisma 초기화</h4>
<p>아래 명령어를 통해 <code>prisma</code> 디렉토리와 <code>.env</code> 파일을 생성합니다. <code>prisma/schema.prisma</code> 파일에 데이터베이스 스키마를 정의하고, <code>.env</code> 파일에 데이터베이스 연결 정보를 저장합니다.</p>
<pre><code class="language-bash">npx prisma init</code></pre>
<hr>
<h3 id="환경-변수-설정">환경 변수 설정</h3>
<h4 id="env-파일-구성">.env 파일 구성</h4>
<ul>
<li><code>DATABASE_URL</code>: MySQL 데이터베이스 연결 문자열</li>
<li><code>SERVER_PORT</code>: 서버 포트 번호</li>
</ul>
<pre><code class="language-bash"># .env 파일
DATABASE_URL=&quot;mysql://username:password@localhost:3306/whattoeat&quot;
SERVER_PORT=3000</code></pre>
<hr>
<h3 id="prisma-스키마-정의">Prisma 스키마 정의</h3>
<h4 id="prismaschemaprisma-설정">prisma/schema.prisma 설정</h4>
<ul>
<li><code>generator</code>: Prisma 클라이언트 생성 설정</li>
<li><code>datasource</code>: 데이터베이스 연결 설정</li>
<li><code>model</code>: 데이터베이스 테이블 구조 정의 (Food 테이블 예시)</li>
</ul>
<pre><code class="language-typescript">// backend/prisma/schema.prisma

generator client {
  provider        = &quot;prisma-client-js&quot;
  previewFeatures = [&quot;omitApi&quot;]
}

datasource db {
  provider = &quot;mysql&quot;
  url      = env(&quot;DATABASE_URL&quot;)
}

model User {
  id        String   @id @default(uuid())
  email     String   @unique
  password  String
  createdAt DateTime @default(now())

  @@map(&quot;users&quot;)
}</code></pre>
<h4 id="데이터베이스에-적용">데이터베이스에 적용</h4>
<p>아래 명령어를 통해 스키마를 기반으로 실제 데이터베이스 테이블을 생성합니다.</p>
<pre><code class="language-bash">npx prisma db push</code></pre>
<hr>
<h3 id="prisma-클라이언트-설정">Prisma 클라이언트 설정</h3>
<pre><code class="language-typescript">// src/utils/prisma.util.ts

import { PrismaClient } from &#39;@prisma/client&#39;;

export const prisma = new PrismaClient({
  log: [&#39;query&#39;, &#39;info&#39;, &#39;warn&#39;, &#39;error&#39;],

  // 에러 메시지를 평문이 아닌, 개발자가 읽기 쉬운 형태로 출력
  errorFormat: &#39;pretty&#39;,
}); // PrismaClient 인스턴스를 생성합니다.

const connectDB = async () =&gt; {
  try {
    await prisma.$connect();
    console.log(&#39;DB 연결에 성공했습니다.&#39;);
  } catch (error) {
    console.error(&#39;DB 연결에 실패했습니다.&#39;, error);
  }
};

connectDB();</code></pre>
<hr>
<h3 id="에러-처리-미들웨어-구현">에러 처리 미들웨어 구현</h3>
<h4 id="커스텀-에러-클래스-생성">커스텀 에러 클래스 생성</h4>
<p>HTTP 상태 코드와 메시지를 포함하는 커스텀 에러 클래스입니다. 표준 Error 클래스를 확장하여 API 에러 처리에 특화된 기능을 제공합니다.</p>
<pre><code class="language-typescript">// src/utils/error-exception.util.ts

class HttpException extends Error {
  status: number;
  message: string;

  constructor(status: number, message: string) {
    super(message);
    this.status = status;
    this.message = message;
  }
}

export default HttpException;</code></pre>
<h4 id="에러-처리-미들웨어">에러 처리 미들웨어</h4>
<ul>
<li>Express의 에러 처리 미들웨어는 4개의 매개변수 <code>(err, req, res, next)</code>를 가져야 함</li>
<li>개발 환경에서만 스택 트레이스를 응답에 포함</li>
<li>모든 에러를 일관된 JSON 형태로 응답</li>
</ul>
<pre><code class="language-typescript">// src/middlewares/error-handler.middleware.ts

import { Request, Response, NextFunction } from &#39;express&#39;;
import HttpException from &#39;../utils/error-exception.util&#39;;

export const errorHandlerMiddleware = (
  err: HttpException,
  req: Request,
  res: Response,
  next: NextFunction
) =&gt; {
  const status = err.status || 500;
  const message = err.message || &#39;Internal Server Error&#39;;

  res.status(status).json({
    status,
    message,
  });
};</code></pre>
<hr>
<h3 id="express-서버-설정">Express 서버 설정</h3>
<ul>
<li><code>dotenv.config()</code>를 최상단에 배치하여 환경변수를 먼저 로드</li>
<li>미들웨어 등록 순서가 중요: 에러 처리 미들웨어는 반드시 마지막에 등록</li>
<li>DB 연결 상태를 확인을 위해 <code>prisma.$queryRaw</code> 사용 (추후 삭제)</li>
</ul>
<pre><code class="language-typescript">// src/index.ts

import express, { NextFunction, Request, Response } from &#39;express&#39;;
import cors from &#39;cors&#39;;
import dotenv from &#39;dotenv&#39;;
import cookieParser from &#39;cookie-parser&#39;;

// 환경변수를 다른 모듈보다 먼저 로드
dotenv.config();

import { errorHandlerMiddleware } from &#39;./middlewares/error-handler.middleware&#39;;
import { prisma } from &#39;./utils/prisma.util&#39;;

const app = express();
const SERVER_PORT = process.env.SERVER_PORT || 3000;

// 미들웨어
app.use(cors());
app.use(express.json());
app.use(cookieParser());

// 라우트 정의
app.get(&#39;/&#39;, (req: Request, res: Response) =&gt; {
  res.json({ message: &#39;API 서버가 실행 중입니다.&#39; });
});

console.log(&#39;DB 연결 테스트 시작...&#39;);
prisma.$queryRaw`SELECT 1`;

// 에러 처리 미들웨어 등록
app.use(errorHandlerMiddleware);

app.listen(SERVER_PORT, () =&gt; {
  console.log(`서버가 포트 ${SERVER_PORT}에서 실행 중입니다`);
});
</code></pre>
<hr>
<h3 id="프로젝트-실행">프로젝트 실행</h3>
<p>아래 명령을 통해 프로젝트 실행합니다.</p>
<pre><code class="language-bash">npm run dev</code></pre>
<p>아래와 같은 출력이 나오면 정상적으로 실행 및 DB 연결이 된겁니다.</p>
<pre><code>[nodemon] restarting due to changes...
[nodemon] starting `ts-node src/index.ts`
DB 연결 테스트 시작...
서버가 포트 3000에서 실행 중입니다
prisma:info Starting a mysql pool with 13 connections.
DB 연결에 성공했습니다.</code></pre><hr>
<h2 id="트러블-슈팅">트러블 슈팅</h2>
<h3 id="cookie-parser-타입-선언-문제">cookie-parser 타입 선언 문제</h3>
<p>*<em>문제: *</em></p>
<pre><code>Could not find a declaration file for module &#39;cookie-parser&#39;</code></pre><p>*<em>원인: *</em>
<code>cookie-parser</code> 패키지의 TypeScript 타입 정의가 없어서 발생하는 에러</p>
<p><strong>해결:</strong></p>
<pre><code>npm install --save-dev @types/cookie-parser</code></pre><br>

<h3 id="에러-핸들러-미들웨어-타입-문제">에러 핸들러 미들웨어 타입 문제</h3>
<p>*<em>문제: *</em></p>
<pre><code>No overload matches this call. Argument of type &#39;(err: any, req: Request, res: Response, next: NextFunction) =&gt; Response&#39; is not assignable to parameter of type &#39;PathParams&#39;</code></pre><p>*<em>원인: *</em>
Express는 에러 핸들러를 특별한 방식으로 인식하는데, 4개의 매개변수를 가진 함수가 에러 핸들러로 인식되지 않았습니다.</p>
<p><strong>해결:</strong>
커스텀 에러 인터페이스를 정의하여 타입 안전성 확보</p>
<pre><code class="language-typescript">// backend/src/middlewares/error-handler.middleware.ts

import { Request, Response, NextFunction } from &#39;express&#39;;
import HttpException from &#39;../utils/error-exception.util&#39;;

export const errorHandlerMiddleware = (
  err: HttpException,
  req: Request,
  res: Response,
  next: NextFunction
) =&gt; {
  // 에러 처리 로직
};</code></pre>
<br>

<h3 id="top-level-await-사용-문제">Top-level await 사용 문제</h3>
<p>*<em>문제: *</em></p>
<pre><code>Top-level &#39;await&#39; expressions are only allowed when the &#39;module&#39; option is set to &#39;es2022&#39;, &#39;esnext&#39;...</code></pre><p>*<em>원인: *</em>
비동기 함수로 래핑하여 사용 (TypeScript 설정 변경 없이 해결)</p>
<p><strong>해결:</strong></p>
<pre><code class="language-typescript">const connectDB = async () =&gt; {
  try {
    await prisma.$connect();
    console.log(&#39;DB 연결에 성공했습니다.&#39;);
  } catch (error) {
    console.error(&#39;DB 연결에 실패했습니다.&#39;, error);
  }
};

connectDB();</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[What To Eat] React + Express 모노레포 프로젝트 세팅하기]]></title>
            <link>https://velog.io/@my_code/What-To-Eat-React-Express-%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@my_code/What-To-Eat-React-Express-%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 29 May 2025 12:11:40 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/my_code/post/8473455b-697d-4592-9a1b-afb9bebd9cf1/image.png" alt=""></p>
<blockquote>
<p><strong>모노레포</strong>에 대한 궁금증이 생겨서 모노레포 형태의 프론트엔드+백엔드 프로젝트를 진행하려고 합니다.</p>
</blockquote>
<h2 id="프로젝트-개요">프로젝트 개요</h2>
<p>이번 프로젝트의 주제는 <strong>식사 메뉴 투표 커뮤니티</strong> 입니다. 일단 프로젝트 세팅에 대한 기술 스택은 다음과 같습니다.</p>
<pre><code>프론트엔드: React, TypeScript, Vite, Tailwind CSS
백엔드: Node.js, Express, TypeScript
모노레포 도구: Turborepo</code></pre><p><strong>모노레포(Monorepo)</strong> 는 여러 프로젝트를 하나의 저장소에서 관리하는 방법입니다. 이번 포스트는 Turborepo를 사용하여 프론트엔드(React)와 백엔드(Express)를 함께 관리하는 TypeScript 기반 모노레포 프로젝트를 세팅하는 과정을 설명합니다.</p>
<p>모노레포 방식은 코드 재사용성을 높이고, 종속성 관리를 단순화하며, 프로젝트 간 일관성을 유지하는 데 도움이 됩니다. 이를 통해 개발 생산성을 크게 향상시킬 수 있다고 합니다.</p>
<p><strong>Turborepo란?</strong> 공식 문서 설명에 따르면, JavaScript나 TypeScript 코드를 위해 최적화된 빌드 시스템이라고 한다. JavaScript와 TypeScript의 린트나 빌드, 테스트와 같은 코드베이스 작업은 시간이 꽤 소요되는 작업인데, Turborepo는 캐싱을 통해 로컬 설정을 진행하고 CI 속도를 높여준다.</p>
<hr>
<h2 id="초기-프로젝트-세팅">초기 프로젝트 세팅</h2>
<p><strong>Turborepo</strong> 를 사용해 기본 모노레포 구조를 생성합니다. 이 명령어는 기본적인 파일 구조와 설정 파일을 자동으로 생성해 줍니다.</p>
<p>이번에는 <code>apps</code>에 있는 기본 <code>docs</code>와 <code>web</code>은 지우고 <code>frontend</code>와 <code>backend</code>로 바꿔서 직접 설정하려고 합니다.</p>
<pre><code class="language-bash"># Turborepo 프로젝트 생성
npx create-turbo@latest</code></pre>
<br>

<p><code>루트 package.json</code>은 모노레포의 중앙 제어 센터 역할을 합니다. workspaces 설정을 통해 npm/yarn/pnpm이 하위 패키지들을 인식하고 관리할 수 있게 됩니다.</p>
<pre><code class="language-typescript">{
  &quot;name&quot;: &quot;what-to-eat&quot;,
  &quot;private&quot;: true,
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;turbo run dev&quot;,
    &quot;build&quot;: &quot;turbo run build&quot;,
    &quot;lint&quot;: &quot;turbo run lint&quot;,
    &quot;test&quot;: &quot;turbo run test&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;prettier&quot;: &quot;^3.5.3&quot;,
    &quot;turbo&quot;: &quot;^2.5.3&quot;,
    &quot;typescript&quot;: &quot;5.8.2&quot;
  },
  &quot;packageManager&quot;: &quot;pnpm@9.0.0&quot;,
  &quot;engines&quot;: {
    &quot;node&quot;: &quot;&gt;=18&quot;
  }
}</code></pre>
<br>

<p><code>turbo.json</code>은 Turborepo의 핵심 설정 파일로, 각 작업(dev, build 등)의 의존성과 캐싱 규칙을 정의합니다. 이를 통해 빌드 시간을 단축하고 효율적인 워크플로우를 구성할 수 있습니다.</p>
<pre><code class="language-typescript">{
  &quot;$schema&quot;: &quot;https://turborepo.com/schema.json&quot;,
  &quot;ui&quot;: &quot;tui&quot;,
  &quot;tasks&quot;: {
    &quot;build&quot;: {
      &quot;dependsOn&quot;: [&quot;^build&quot;],
      &quot;outputs&quot;: [&quot;dist/**&quot;, &quot;.next/**&quot;, &quot;build/**&quot;, &quot;apps/backend/dist/**&quot;]
    },
    &quot;dev&quot;: {
      &quot;cache&quot;: false,
      &quot;persistent&quot;: true
    },
    &quot;lint&quot;: {},
    &quot;test&quot;: {}
  }
}</code></pre>
<hr>
<h2 id="공유-패키지-설정">공유 패키지 설정</h2>
<p>공유 패키지는 모노레포의 핵심 장점 중 하나입니다. 여러 앱에서 공통으로 사용되는 코드, 설정, 컴포넌트를 분리하여 일관성과 재사용성을 높입니다.</p>
<p>아직 어떻게 활용해야 할지 감이 오지 않지만 조금씩 구현하면서 활용할 방법을 생각해야겠습니다.</p>
<p>모든 프로젝트에서 일관된 TypeScript 설정을 사용하기 위한 공유 패키지를 생성합니다. 이를 통해 프로젝트 간 타입 정의의 일관성을 유지할 수 있습니다.</p>
<p>아래 명령어를 통해 모든 프로젝트에서 일관된 TypeScript 설정을 사용하기 위한 공유 패키지를 생성합니다. 이를 통해 프로젝트 간 타입 정의의 일관성을 유지할 수 있습니다.</p>
<pre><code class="language-bash">mkdir -p packages/tsconfig
cd packages/tsconfig
npm init -y</code></pre>
<pre><code class="language-typescript">// packages/tsconfig/package.json

{
  &quot;name&quot;: &quot;tsconfig&quot;,
  &quot;version&quot;: &quot;0.0.0&quot;,
  &quot;private&quot;: true,
  &quot;files&quot;: [
    &quot;base.json&quot;,
    &quot;react-app.json&quot;,
    &quot;node-app.json&quot;
  ]
}</code></pre>
<br>

<p>모든 TypeScript 프로젝트의 기반이 되는 공통 설정으로, 기본적인 컴파일 옵션을 정의합니다.</p>
<pre><code class="language-typescript">// packages/tsconfig/base.json

{
  &quot;$schema&quot;: &quot;https://json.schemastore.org/tsconfig&quot;,
  &quot;display&quot;: &quot;base settings&quot;,
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;es2020&quot;,
    &quot;module&quot;: &quot;esnext&quot;,
    &quot;moduleResolution&quot;: &quot;node&quot;,
    &quot;esModuleInterop&quot;: true,
    &quot;forceConsistentCasingInFileNames&quot;: true,
    &quot;strict&quot;: true,
    &quot;skipLibCheck&quot;: true
  },
  &quot;exclude&quot;: [&quot;node_modules&quot;]
}</code></pre>
<p>React 애플리케이션에 특화된 TypeScript 설정으로, JSX 지원과 DOM 타입을 포함합니다.</p>
<pre><code class="language-typescript">// packages/tsconfig/react-app.json

{
  &quot;$schema&quot;: &quot;https://json.schemastore.org/tsconfig&quot;,
  &quot;display&quot;: &quot;React app&quot;,
  &quot;extends&quot;: &quot;./base.json&quot;,
  &quot;compilerOptions&quot;: {
    &quot;lib&quot;: [&quot;dom&quot;, &quot;dom.iterable&quot;, &quot;esnext&quot;],
    &quot;jsx&quot;: &quot;react-jsx&quot;,
    &quot;noEmit&quot;: true,
    &quot;resolveJsonModule&quot;: true,
    &quot;isolatedModules&quot;: true
  }
}</code></pre>
<p>Node.js 백엔드에 적합한 TypeScript 설정으로, CommonJS 모듈 시스템과 소스맵 생성을 지원합니다.</p>
<pre><code class="language-typescript">// packages/tsconfig/node-app.json:

{
  &quot;$schema&quot;: &quot;https://json.schemastore.org/tsconfig&quot;,
  &quot;display&quot;: &quot;Node app&quot;,
  &quot;extends&quot;: &quot;./base.json&quot;,
  &quot;compilerOptions&quot;: {
    &quot;module&quot;: &quot;commonjs&quot;,
    &quot;outDir&quot;: &quot;dist&quot;,
    &quot;sourceMap&quot;: true
  }
}</code></pre>
<br>

<p>아래 명령을 통해 코드 품질과 일관성을 유지하기 위한 공유 ESLint 설정을 생성합니다. 이를 통해 모든 프로젝트에서 동일한 코딩 규칙을 적용할 수 있습니다.</p>
<pre><code class="language-bash">mkdir -p packages/eslint-config
cd packages/eslint-config
npm init -y
npm install -D eslint eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/parser @typescript-eslint/eslint-plugin</code></pre>
<pre><code class="language-typescript">// packages/eslint-config/package.json

{
  &quot;name&quot;: &quot;@whattoeat/eslint-config&quot;,
  &quot;version&quot;: &quot;0.0.0&quot;,
  &quot;private&quot;: true,
  &quot;main&quot;: &quot;index.js&quot;,
  &quot;dependencies&quot;: {
    &quot;eslint-plugin-react&quot;: &quot;^7.32.2&quot;,
    &quot;eslint-plugin-react-hooks&quot;: &quot;^4.6.0&quot;,
    &quot;@typescript-eslint/eslint-plugin&quot;: &quot;^5.59.0&quot;,
    &quot;@typescript-eslint/parser&quot;: &quot;^5.59.0&quot;
  }
}</code></pre>
<p>React와 TypeScript에 적합한 ESLint 규칙을 정의합니다. 최신 React 문법(React 17+ JSX 변환)을 지원하도록 일부 규칙을 조정했습니다.</p>
<pre><code class="language-typescript">// packages/eslint-config/index.js:

module.exports = {
  extends: [&quot;plugin:@typescript-eslint/recommended&quot;, &quot;plugin:react/recommended&quot;, &quot;plugin:react-hooks/recommended&quot;],
  parser: &quot;@typescript-eslint/parser&quot;,
  settings: {
    react: {
      version: &quot;detect&quot;,
    },
  },
  rules: {
    &quot;react/react-in-jsx-scope&quot;: &quot;off&quot;,
  },
};</code></pre>
<hr>
<h2 id="프론트엔드-앱-설정">프론트엔드 앱 설정</h2>
<p>아래 명령어를 통해 Vite를 사용하는 React+TypeScript 프로젝트를 생성합니다. Vite는 빠른 개발 서버와 효율적인 빌드 도구를 제공하는 최신 프론트엔드 도구입니다.</p>
<pre><code class="language-bash">cd apps
npm create vite@latest frontend -- --template react-ts
cd frontend
npm install</code></pre>
<p>공유 TypeScript 설정을 상속받고, 경로 별칭(@)을 설정하여 import 경로를 간소화합니다.</p>
<pre><code class="language-typescript">// apps/frontend/tsconfig.json

{
  &quot;extends&quot;: &quot;../../packages/tsconfig/react-app.json&quot;,
  &quot;compilerOptions&quot;: {
    &quot;baseUrl&quot;: &quot;.&quot;,
    &quot;paths&quot;: {
      &quot;@/*&quot;: [&quot;./src/*&quot;]
    }
  },
  &quot;include&quot;: [&quot;src&quot;],
  &quot;references&quot;: [{ &quot;path&quot;: &quot;./tsconfig.node.json&quot; }]
}</code></pre>
<p>Vite 구성 파일에 대한 TypeScript 설정으로, 프로젝트 참조(Project References) 기능을 사용하기 위한 composite 설정이 포함됩니다.</p>
<pre><code class="language-typescript">// apps/frontend/tsconfig.node.json

{
  &quot;compilerOptions&quot;: {
    &quot;composite&quot;: true,
    &quot;tsBuildInfoFile&quot;: &quot;./node_modules/.tmp/tsconfig.node.tsbuildinfo&quot;,
    &quot;target&quot;: &quot;ES2022&quot;,
    &quot;lib&quot;: [&quot;ES2023&quot;],
    &quot;module&quot;: &quot;ESNext&quot;,
    &quot;skipLibCheck&quot;: true,

    /* Bundler mode */
    &quot;moduleResolution&quot;: &quot;bundler&quot;,
    &quot;allowImportingTsExtensions&quot;: false,
    &quot;verbatimModuleSyntax&quot;: true,
    &quot;moduleDetection&quot;: &quot;force&quot;,
    &quot;noEmit&quot;: false,

    /* Linting */
    &quot;strict&quot;: true,
    &quot;noUnusedLocals&quot;: true,
    &quot;noUnusedParameters&quot;: true,
    &quot;erasableSyntaxOnly&quot;: true,
    &quot;noFallthroughCasesInSwitch&quot;: true,
    &quot;noUncheckedSideEffectImports&quot;: true
  },
  &quot;include&quot;: [&quot;vite.config.ts&quot;]
}</code></pre>
<br>

<p><code>apps/frontend</code>에서 사용할 컴포넌트와 <code>Tailwind CSS</code>에 대한 설정은 각자 원하는 형태로 설정하시면 됩니다. 저는 일단 백엔드부터 구현 후 그에 맞게 프론트를 구현할 계획입니다.</p>
<hr>
<h2 id="백엔드-앱-설정">백엔드 앱 설정</h2>
<p><code>Express</code> 기반 백엔드를 위한 기본 패키지와 TypeScript 개발 환경을 설정합니다. <code>cors 미들웨어</code>는 프론트엔드와의 통신을 위해 필요합니다.</p>
<pre><code class="language-bash">cd apps
mkdir backend
cd backend
npm init -y
npm install express cors cookie-parser bcrpyt jsonwebtoken
npm install -D typescript ts-node @types/express @types/node @types/cors @type/cookie-parser nodemon dotenv</code></pre>
<br>

<p>공유 패키지에 있는 Node.js TypeScript 설정을 상속받고, 소스 코드 디렉토리를 지정합니다.</p>
<pre><code class="language-typescript">// apps/backend/tsconfig.json

{
  &quot;extends&quot;: &quot;../../packages/tsconfig/node-app.json&quot;,
  &quot;compilerOptions&quot;: {
    &quot;rootDir&quot;: &quot;src&quot;
  },
  &quot;include&quot;: [&quot;src/**/*&quot;]
}</code></pre>
<br>

<p>개발, 빌드, 실행을 위한 스크립트와 필요한 의존성을 정의합니다. nodemon은 개발 중 파일 변경을 감지하여 서버를 자동으로 재시작합니다. 아마도 <code>bcrpyt</code>, <code>jsonwebtoken</code> 타입 패키지 설치가 필요할 것 같긴한데, 사용할 때 추가로 설치하면 될 것 같습니다.</p>
<pre><code class="language-typescript">// apps/backend/package.json

{
  &quot;name&quot;: &quot;backend&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;description&quot;: &quot;&quot;,
  &quot;main&quot;: &quot;index.js&quot;,
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;nodemon --watch src --ext ts,js --exec ts-node src/index.ts&quot;,
    &quot;build&quot;: &quot;tsc&quot;,
    &quot;start&quot;: &quot;node dist/index.js&quot;,
    &quot;lint&quot;: &quot;eslint src --ext .ts&quot;
  },
  &quot;keywords&quot;: [],
  &quot;author&quot;: &quot;&quot;,
  &quot;license&quot;: &quot;ISC&quot;,
  &quot;type&quot;: &quot;commonjs&quot;,
  &quot;dependencies&quot;: {
    &quot;@prisma/client&quot;: &quot;^6.8.2&quot;,
    &quot;bcrypt&quot;: &quot;^6.0.0&quot;,
    &quot;cookie-parser&quot;: &quot;^1.4.7&quot;,
    &quot;cors&quot;: &quot;^2.8.5&quot;,
    &quot;express&quot;: &quot;^5.1.0&quot;,
    &quot;jsonwebtoken&quot;: &quot;^9.0.2&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;@types/cookie-parser&quot;: &quot;^1.4.8&quot;,
    &quot;@types/cors&quot;: &quot;^2.8.18&quot;,
    &quot;@types/express&quot;: &quot;^5.0.2&quot;,
    &quot;@types/node&quot;: &quot;^22.15.24&quot;,
    &quot;dotenv&quot;: &quot;^16.5.0&quot;,
    &quot;nodemon&quot;: &quot;^3.1.10&quot;,
    &quot;prisma&quot;: &quot;^6.8.2&quot;,
    &quot;ts-node&quot;: &quot;^10.9.2&quot;,
    &quot;typescript&quot;: &quot;^5.8.3&quot;
  }
}</code></pre>
<br>

<p>기본적인 Express 서버로, CORS 설정과 JSON 파싱 미들웨어를 적용하고 바로 다음에 진행할 데이터베이스 연결을 위해 <code>dotenv.config()</code> 설정도 합니다.</p>
<pre><code class="language-typescript">// apps/backend/src/index.ts

import express, { NextFunction, Request, Response } from &#39;express&#39;;
import cors from &#39;cors&#39;;
import dotenv from &#39;dotenv&#39;;
import cookieParser from &#39;cookie-parser&#39;;

dotenv.config();

const app = express();
const SERVER_PORT = process.env.SERVER_PORT || 3000;

// 미들웨어
app.use(cors());
app.use(express.json());
app.use(cookieParser());

// 라우트 정의
app.get(&#39;/&#39;, (req: Request, res: Response) =&gt; {
  res.json({ message: &#39;API 서버가 실행 중입니다.&#39; });
});

app.listen(SERVER_PORT, () =&gt; {
  console.log(`서버가 포트 ${SERVER_PORT}에서 실행 중입니다`);
});</code></pre>
<hr>
<h2 id="프로젝트-실행">프로젝트 실행</h2>
<p><code>Turborepo</code>를 사용하면 하나의 명령어로 모든 앱과 패키지를 동시에 개발하고 빌드할 수 있어 개발 효율성이 향상됩니다. 일단 <strong>개발 환경</strong>에서 실행하기 위해 루트 디렉토리에서 아래와 같은 명령을 사용합니다.</p>
<pre><code class="language-bash">npm run dev</code></pre>
<hr>
<h2 id="트러블-슈팅">트러블 슈팅</h2>
<h3 id="typescript-project-references-오류">TypeScript Project References 오류</h3>
<p><strong>문제:</strong></p>
<pre><code>error TS6306: Referenced project must have setting &quot;composite&quot;: true.
error TS6310: Referenced project may not disable emit.</code></pre><p><strong>원인:</strong>
TypeScript의 Project References 기능을 사용할 때, 참조되는 프로젝트(<code>tsconfig.node.json</code>)에서 특정 설정이 필요합니다. <code>composite: true</code>가 필요하고, <code>noEmit: true</code>는 사용할 수 없습니다.</p>
<p><strong>해결:</strong></p>
<pre><code class="language-typescript">{
  &quot;compilerOptions&quot;: {
    &quot;composite&quot;: true,  // 추가
    &quot;noEmit&quot;: false,    // true에서 false로 변경
    // ...
  }
}</code></pre>
<br>

<h3 id="es-모듈과-commonjs-혼용-문제">ES 모듈과 CommonJS 혼용 문제</h3>
<p><strong>문제:</strong></p>
<pre><code>ReferenceError: module is not defined in ES module scope
This file is being treated as an ES module because it has a &#39;.js&#39; file extension and package.json contains &quot;type&quot;: &quot;module&quot;.</code></pre><p><strong>원인:</strong>
프론트엔드 package.json에 <code>&quot;type&quot;: &quot;module&quot;</code> 설정이 있어 모든 .js 파일이 ES 모듈로 처리됩니다. 그러나 PostCSS와 Tailwind 설정 파일은 CommonJS 형식(<strong>module.exports</strong>)을 사용합니다.</p>
<p><strong>해결:</strong>
Node.js는 .cjs 확장자 파일을 항상 CommonJS로 인식하므로 문제가 해결됩니다.</p>
<pre><code>postcss.config.js → postcss.config.cjs
tailwind.config.js → tailwind.config.cjs</code></pre><br>

<h3 id="tailwind-css-v4-postcss-플러그인-변경">Tailwind CSS v4 PostCSS 플러그인 변경</h3>
<p><strong>문제:</strong></p>
<pre><code>[plugin:vite:css] [postcss] It looks like you&#39;re trying to use `tailwindcss` directly as a PostCSS plugin. The PostCSS plugin has moved to a separate package...</code></pre><p><strong>원인:</strong>
Tailwind CSS v4에서는 아키텍처 변경으로 PostCSS 플러그인이 별도 패키지로 분리되었습니다.</p>
<p><strong>해결:</strong></p>
<ol>
<li><p>패키지 설치:</p>
<pre><code class="language-bash">npm install @tailwindcss/postcss</code></pre>
</li>
<li><p>PostCSS 설정 수정:</p>
<pre><code class="language-typescript">// frontend/postcss.config.cjs
</code></pre>
</li>
</ol>
<p>module.exports = {
  plugins: {
    &#39;@tailwindcss/postcss&#39;: {},
    autoprefixer: {},
  },
};</p>
<pre><code>
&lt;br&gt;

### TypeScript 컴포넌트 확장자 문제
**문제:**</code></pre><p>error TS5097: An import path can only end with a &#39;.tsx&#39; extension when &#39;allowImportingTsExtensions&#39; is enabled.</p>
<pre><code>
**원인:**
TypeScript 설정에서 `allowImportingTsExtensions: false`로 설정된 경우 import 문에서 파일 확장자(.tsx)를 사용할 수 없습니다.

**해결:**
```typescript
// 변경 전
import App from &#39;./App.tsx&#39;

// 변경 후
import App from &#39;./App&#39;</code></pre><hr>
<h2 id="기타">기타</h2>
<ul>
<li>패키지 설치 시 현재 디렉토리를 잘 확인해야 합니다.<ul>
<li>프론트에서 사용하는 패키지는 <code>apps/frontend</code>에서 설치 명령어 사용</li>
</ul>
</li>
<li>공유 패키지에서 공유 ui는 선택사항합니다.<ul>
<li>저는 하나의 앱에서만 사용할 예정이기에 공유 ui는 사용하지 않습니다.</li>
<li>만약 사용자, 관리자, 홈페이지 등등으로 나눠지는 경우 활용하면 좋습니다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2024.10.25 TIL] FastAPI 고용량 파일 업로드 시 청크 사용하기]]></title>
            <link>https://velog.io/@my_code/2024.10.25-TIL-FastAPI-%EA%B3%A0%EC%9A%A9%EB%9F%89-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-%EC%8B%9C-%EC%B2%AD%ED%81%AC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@my_code/2024.10.25-TIL-FastAPI-%EA%B3%A0%EC%9A%A9%EB%9F%89-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-%EC%8B%9C-%EC%B2%AD%ED%81%AC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 25 Oct 2024 02:05:52 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/my_code/post/f84e38f5-2273-417b-84fb-79a45fbf4e43/image.png" alt=""></p>
<h1 id="💻-tiltoday-i-learned">💻 TIL(Today I Learned)</h1>
<h2 id="📌-today-i-done">📌 Today I Done</h2>
<h3 id="✏️-청크chucks란">✏️ 청크(Chucks)란?</h3>
<ul>
<li><p>청크는 대용량 파일을 작고 관리하기 쉬운 조각으로 나눈 것입니다.</p>
</li>
<li><p>일반적으로 1MB에서 5MB 사이의 크기로 나누지만, 네트워크 상태와 서버 설정에 따라 조절할 수 있습니다.</p>
</li>
<li><p>각 청크는 고유 식별자를 가지며, 이를 통해 서버는 청크의 순서와 완전성을 확인합니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-왜-청크-단위로-쪼개서-업로드하는가">✏️ 왜 청크 단위로 쪼개서 업로드하는가?</h3>
<ul>
<li><p>게이트웨이 제한 우회:</p>
<ul>
<li><p>대부분의 서버와 게이트웨이는 10MB 이상의 데이터 전송을 제한합니다.</p>
</li>
<li><p>청크 업로드를 사용하면 이 제한을 우회하여 수백 MB 또는 GB 단위의 대용량 파일도 전송할 수 있습니다.</p>
</li>
</ul>
</li>
<li><p>네트워크 중단에 대한 복원력:</p>
<ul>
<li><p>업로드 중 연결이 끊어져도 이미 전송된 청크는 서버에 유지됩니다.</p>
</li>
<li><p>연결이 복구되면 마지막으로 성공한 청크부터 업로드를 재개할 수 있습니다.</p>
</li>
</ul>
</li>
<li><p>리소스 최적화:</p>
<ul>
<li>서버와 클라이언트의 메모리 사용을 효율적으로 관리할 수 있습니다.</li>
</ul>
</li>
<li><p>병렬 처리 가능:</p>
<ul>
<li>여러 청크를 동시에 업로드하여 전체 속도를 향상시킬 수 있습니다.</li>
</ul>
</li>
</ul>
<br>

<h3 id="✏️-구현-과정">✏️ 구현 과정</h3>
<ul>
<li><p>프론트엔드:</p>
<ul>
<li><p>파일을 업로드 받습니다.</p>
</li>
<li><p>해당 파일을 지정된 크기(예: 5MB)의 청크로 나눕니다.</p>
</li>
<li><p>각 청크에 대해 FormData 객체를 생성하고 청크 데이터와 메타데이터를 추가합니다.</p>
</li>
<li><p>반복적으로 axios POST 요청을 보내 청크들을 하나씩 전달합니다.</p>
</li>
<li><p>이 때, 청크의 총 갯수와 현재 전송하고 있는 청크의 인덱스 번호를 전달합니다.</p>
</li>
</ul>
</li>
<li><p>백엔드:</p>
<ul>
<li><p>해당 엔드포인트로 클라이언트가 전달한 청크 데이터와 메타데이터를 받습니다.</p>
</li>
<li><p>받은 청크를 임시 저장소(디렉토리, 전역 변수 등)에 저장합니다.</p>
</li>
<li><p>청크를 받아서 S3에 임시로 저장합니다.</p>
</li>
<li><p>청크 데이터가 모두 모이게 되면 하나의 파일 데이터로 바꿔서 해당 파일에 대한 URL를 반환합니다.</p>
</li>
</ul>
</li>
</ul>
<br>

<h3 id="✏️-클라이언트-예시-코드">✏️ 클라이언트 예시 코드</h3>
<pre><code class="language-js">async function uploadFile(file) {
    const chunkSize = 5 * 1024 * 1024; // 5MB
    const totalChunks = Math.ceil(file.size / chunkSize);

    for (let chunkNumber = 1; chunkNumber &lt;= totalChunks; chunkNumber++) {
        const start = (chunkNumber - 1) * chunkSize;
        const end = Math.min(start + chunkSize, file.size);
        const chunk = file.slice(start, end);

        const formData = new FormData();
        formData.append(&#39;file&#39;, chunk, file.name);
        formData.append(&#39;filename&#39;, file.name);
        formData.append(&#39;chunk_number&#39;, chunkNumber);
        formData.append(&#39;total_chunks&#39;, totalChunks);

        try {
            const response = await axios.post(&#39;/upload-chunk/&#39;, formData, {
                headers: { &#39;Content-Type&#39;: &#39;multipart/form-data&#39; }
            });
            console.log(response.data.message);

            if (chunkNumber === totalChunks) {
                console.log(&#39;파일 업로드 완료. URL:&#39;, response.data.file_url);
                // 여기서 file_url을 사용하여 추가 작업을 수행할 수 있습니다.
            }
        } catch (error) {
            console.error(&#39;업로드 실패:&#39;, error);
            break; // 에러 발생 시 업로드 중단
        }
    }
}</code></pre>
<br>

<h3 id="✏️-boto3를-이용한-s3-청크-업로드-구현">✏️ boto3를 이용한 S3 청크 업로드 구현</h3>
<pre><code class="language-python">from fastapi import FastAPI, File, UploadFile, Form
from fastapi.responses import JSONResponse
import boto3
from botocore.exceptions import ClientError

app = FastAPI()

s3_client = boto3.client(&#39;s3&#39;,
    aws_access_key_id=&#39;YOUR_ACCESS_KEY&#39;,
    aws_secret_access_key=&#39;YOUR_SECRET_KEY&#39;,
    region_name=&#39;YOUR_REGION&#39;
)

BUCKET_NAME = &#39;YOUR_BUCKET_NAME&#39;
S3_URL_PREFIX = f&quot;https://{BUCKET_NAME}.s3.amazonaws.com/&quot;

multipart_uploads = {}

@app.post(&quot;/upload-chunk/&quot;)
async def upload_chunk(
    file: UploadFile = File(...),
    filename: str = Form(...),
    chunk_number: int = Form(...),
    total_chunks: int = Form(...)
):
    try:
        if filename not in multipart_uploads:
            response = s3_client.create_multipart_upload(Bucket=BUCKET_NAME, Key=filename)
            multipart_uploads[filename] = {
                &quot;UploadId&quot;: response[&quot;UploadId&quot;],
                &quot;Parts&quot;: []
            }

        upload_id = multipart_uploads[filename][&quot;UploadId&quot;]

        response = s3_client.upload_part(
            Bucket=BUCKET_NAME,
            Key=filename,
            PartNumber=chunk_number,
            UploadId=upload_id,
            Body=file.file
        )

        multipart_uploads[filename][&quot;Parts&quot;].append({
            &quot;PartNumber&quot;: chunk_number,
            &quot;ETag&quot;: response[&quot;ETag&quot;]
        })

        if chunk_number == total_chunks:
            complete_response = s3_client.complete_multipart_upload(
                Bucket=BUCKET_NAME,
                Key=filename,
                UploadId=upload_id,
                MultipartUpload={&quot;Parts&quot;: sorted(multipart_uploads[filename][&quot;Parts&quot;], key=lambda x: x[&quot;PartNumber&quot;])}
            )
            del multipart_uploads[filename]

            # S3 URL 생성
            file_url = f&quot;{S3_URL_PREFIX}{filename}&quot;

            return JSONResponse(content={
                &quot;message&quot;: &quot;파일 업로드 완료&quot;,
                &quot;file_url&quot;: file_url
            }, status_code=200)

        return JSONResponse(content={&quot;message&quot;: f&quot;청크 {chunk_number}/{total_chunks} 업로드 성공&quot;}, status_code=200)

    except ClientError as e:
        return JSONResponse(content={&quot;message&quot;: f&quot;업로드 실패: {str(e)}&quot;}, status_code=500)</code></pre>
<br>

<h3 id="✏️-참고자료">✏️ 참고자료</h3>
<ul>
<li><p><a href="https://arnabgupta.hashnode.dev/mastering-chunked-file-uploads-with-fastapi-and-nodejs-a-step-by-step-guide#heading-server-side-implementationfastapi">https://arnabgupta.hashnode.dev/mastering-chunked-file-uploads-with-fastapi-and-nodejs-a-step-by-step-guide#heading-server-side-implementationfastapi</a></p>
</li>
<li><p><a href="https://requests.readthedocs.io/en/latest/user/advanced/#streaming-uploads">https://requests.readthedocs.io/en/latest/user/advanced/#streaming-uploads</a></p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-tomorrows-goal">📌 Tomorrow&#39;s Goal</h2>
<h3 id="✏️-fastapi-공식문서-더-살펴보기">✏️ FastAPI 공식문서 더 살펴보기</h3>
<ul>
<li><p>의존성 주입(Dependency Injection) 심화 학습</p>
</li>
<li><p>FastAPI의 보안 기능 (인증, 권한 관리) 탐구</p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-todays-goal-i-done">📌 Today&#39;s Goal I Done</h2>
<h3 id="✔️-파일-데이터를-청크단위로-쪼개서-업로드">✔️ 파일 데이터를 청크단위로 쪼개서 업로드</h3>
<ul>
<li><p>대용량 파일 처리의 복잡성을 직접 경험하며 효율적인 업로드 방식의 중요성을 깨달았습니다.</p>
</li>
<li><p>청크 단위 업로드를 통해 네트워크 불안정성에 대응하는 방법을 학습했습니다. 이는 실제 서비스에서 매우 중요한 부분이라고 느꼈습니다.</p>
</li>
<li><p>S3와 FastAPI를 연동하는 과정에서 클라우드 서비스와 웹 프레임워크의 통합에 대한 이해도가 높아졌습니다.</p>
</li>
<li><p>단일 엔드포인트에서 여러 기능을 처리하는 방식을 구현하며, API 설계의 유연성에 대해 고민해볼 수 있었습니다.</p>
</li>
</ul>
<br>]]></description>
        </item>
        <item>
            <title><![CDATA[[2024.09.20 TIL] 웹훅(Webhook)과 API의 차이]]></title>
            <link>https://velog.io/@my_code/2024.09.20-TIL-%EC%9B%B9%ED%9B%85Webhook%EA%B3%BC-API%EC%9D%98-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@my_code/2024.09.20-TIL-%EC%9B%B9%ED%9B%85Webhook%EA%B3%BC-API%EC%9D%98-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Fri, 20 Sep 2024 08:02:01 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>본 내용은 내일배움캠프 커리어톤에서 활동한 내용을 기록한 글입니다.</p>
</blockquote>
<hr>
<h1 id="💻-tiltoday-i-learned">💻 TIL(Today I Learned)</h1>
<h2 id="📌-today-i-done">📌 Today I Done</h2>
<p><img src="https://velog.velcdn.com/images/my_code/post/6c46115a-49ae-4e12-976d-b2697b7d4123/image.png" alt=""></p>
<h3 id="✏️-api란">✏️ API란?</h3>
<ul>
<li><p><strong>API</strong>는 <strong>Application Programming Interface</strong>의 줄임말로 API는 응용 프로그램에서 사용할 수 있도록 운영체제나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스를 뜻함</p>
</li>
<li><p>간단하게 말하면, 다른 소프트웨어나 어플리케이션 간을 연결해주는 매개체이자 약속라고 할 수 있음</p>
</li>
<li><p><strong>서로 다른 시스템 간의 통신을 할 수 있게 해주는 중개자의 역할을 담당함</strong></p>
</li>
</ul>
<br>

<h3 id="✏️-웹훅webhook이란">✏️ 웹훅(Webhook)이란?</h3>
<ul>
<li><p>웹훅(Webhook)은 특정 이벤트가 발생했을 때 다른 시스템에 자동으로 HTTP POST 요청을 보내는 방법</p>
</li>
<li><p>이를 통해 실시간으로 데이터나 알림을 전달할 수 있음</p>
</li>
<li><p>예를 들어, 결제가 완료될 때 결제 서비스 정보를 서버로 전송하는 등의 다양한 시스템 간의 효율적인 실시간 통신을 가능하게 함</p>
</li>
<li><p>웹훅의 이점에 대해 atlassian에서 잘 정리해놓아서 가지고 왔음</p>
</li>
</ul>
<blockquote>
<p>Advantages of webhooks</p>
<p>Without webhooks, if you want to detect when events occur in Bitbucket Cloud, you need to poll the API. However, polling the API is inconvenient, inefficient, and error-prone. Consider how SMS messages work on mobile phones. You don&#39;t have to check your messages every 5 minutes to see if you have a text because your phone sends you a notification. In the same way, webhooks work like the notification so that the API does not have to check for the same activity every minute.</p>
</blockquote>
<ul>
<li><p>즉, 우리의 휴대폰에서 5분마다 메시지가 왔는지 확인하는 것은 매우 비효율적임</p>
</li>
<li><p>그래서 우리는 메시지가 올 때만 알람을 듣고 확인함</p>
</li>
<li><p>5분마다 폰을 켜서 메시지가 왔는지 확인하는 것을 <strong>API</strong></p>
</li>
<li><p>메시지가 왔을 때 알람을 주는 것을 <strong>Webhook</strong></p>
</li>
</ul>
<br>

<h3 id="✏️-참고자료">✏️ 참고자료</h3>
<ul>
<li><p><a href="https://daeguowl.tistory.com/33">https://daeguowl.tistory.com/33</a></p>
</li>
<li><p><a href="https://sbjjsurfing.tistory.com/94">https://sbjjsurfing.tistory.com/94</a></p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-tomorrows-goal">📌 Tomorrow&#39;s Goal</h2>
<h3 id="✏️-aws-elastic-beanstalk로-cicd-파이프라인-구축하기">✏️ AWS Elastic Beanstalk로 CI/CD 파이프라인 구축하기</h3>
<ul>
<li><p>내일은 AWS Elastic Beanstalk로 CI/CD 파이프라인 구축할 예정</p>
</li>
<li><p>오늘 간단한 조사를 마쳤으니 내일은 실제로 프로젝트에 적용해서 제대로 CI/CD 되는지 확인할 예정</p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-todays-goal-i-done">📌 Today&#39;s Goal I Done</h2>
<h3 id="✔️-기술-면접-예상-질문-답변-작성">✔️ 기술 면접 예상 질문 답변 작성</h3>
<ul>
<li><p>오늘은 면접 대비반에서 진행하는 기술 면접 예상 질문 답변 작성 미션을 진행함</p>
</li>
<li><p>예상 질문 중 웹훅(Webhook)에 대한 내용이 있어서 구글링을 통해서 내용을 찾음</p>
</li>
<li><p>처음 웹훅에 대한 내용을 봤을 때는 그냥 API와 비슷한 것 같아서 그 둘의 차이를 찾아봄</p>
</li>
<li><p>결과적으로 요청하는 데이터를 주는 것은 같지만 언제(When) 주냐에 따라서 웹훅인지 API인지 결정된다고 함</p>
</li>
</ul>
<br>]]></description>
        </item>
        <item>
            <title><![CDATA[[2024.09.19 TIL] AWS Elastic Beanstalk 란?]]></title>
            <link>https://velog.io/@my_code/2024.09.19-TIL-AWS-Elastic-Beanstalk-%EB%9E%80</link>
            <guid>https://velog.io/@my_code/2024.09.19-TIL-AWS-Elastic-Beanstalk-%EB%9E%80</guid>
            <pubDate>Thu, 19 Sep 2024 08:36:14 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>본 내용은 내일배움캠프 커리어톤에서 활동한 내용을 기록한 글입니다.</p>
</blockquote>
<hr>
<h1 id="💻-tiltoday-i-learned">💻 TIL(Today I Learned)</h1>
<h2 id="📌-today-i-done">📌 Today I Done</h2>
<h3 id="✏️-elastic-beanstalk-란">✏️ Elastic Beanstalk 란?</h3>
<p><img src="https://velog.velcdn.com/images/my_code/post/a000cf08-fdc0-40e3-b26d-a921abfafecd/image.png" alt=""></p>
<ul>
<li><p>&quot;AWS Elastic Beanstalk는 Java, .NET, PHP, Node.js, Python, Ruby, Go 및 Docker를 사용하여 개발된 웹 애플리케이션 및 서비스를 Apache, Nginx, Passenger 및 IIS와 같은 친숙한 서버에서 손쉽게 배포하고 확장할 수 있는 서비스입니다.&quot; - 출처 AWS</p>
</li>
<li><p>간단하게 말하면 AWS 클라우드에서 애플리케이션을 신속하게 배포하고 관리할 수 있는 서비스를 말함</p>
</li>
<li><p>추후 GitHub Actions와 AWS Elastic Beanstalk를 이용해서 구현할 예정</p>
</li>
</ul>
<br>

<h3 id="✏️-쉽게-배포할-수-있는-이유">✏️ 쉽게 배포할 수 있는 이유</h3>
<ul>
<li><p>Elastic Beanstalk의 &quot;쉽게&quot;라는 기준은 다음과 같은 자동화 기능 사용</p>
<ul>
<li><p>웹 서버 자동 설정: Apache나 Nginx와 같은 웹 서버를 자동으로 설정</p>
</li>
<li><p>로드 밸런싱: 설정에 따라 Elastic Load Balancer(ELB)를 자동으로 연결</p>
</li>
<li><p>오토 스케일링: 애플리케이션의 트래픽에 따라 인스턴스 수를 자동으로 조정</p>
</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/my_code/post/b879f7d1-6e7e-40e6-ae59-aa965051258e/image.png" alt=""></p>
<br>

<h3 id="✏️-elastic-beanstalk-워크플로">✏️ Elastic Beanstalk 워크플로</h3>
<p><img src="https://velog.velcdn.com/images/my_code/post/cfa46b61-9acb-49fe-9251-2b201fbda639/image.png" alt=""></p>
<ul>
<li><p>애플리케이션 생성 후, 애플리케이션의 버전으 바뀌게 되면 Elastic Beanstalk가 자동으로 환경을 실행</p>
</li>
<li><p>스스로 코드 실행에 필요한 AWS 리소스 생성 및 구성</p>
</li>
</ul>
<br>

<h3 id="✏️-참고자료">✏️ 참고자료</h3>
<ul>
<li><p><a href="https://bluayer.com/46">https://bluayer.com/46</a></p>
</li>
<li><p><a href="https://pypystory.tistory.com/36">https://pypystory.tistory.com/36</a></p>
</li>
<li><p><a href="https://velog.io/@hahaha/AWS-EBElastic-Beanstalk%EC%9D%B4%EB%9E%80">https://velog.io/@hahaha/AWS-EBElastic-Beanstalk%EC%9D%B4%EB%9E%80</a></p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-tomorrows-goal">📌 Tomorrow&#39;s Goal</h2>
<h3 id="✏️-aws-elastic-beanstalk로-cicd-파이프라인-구축하기">✏️ AWS Elastic Beanstalk로 CI/CD 파이프라인 구축하기</h3>
<ul>
<li><p>내일은 AWS Elastic Beanstalk로 CI/CD 파이프라인 구축할 예정</p>
</li>
<li><p>오늘 간단한 조사를 마쳤으니 내일은 실제로 프로젝트에 적용해서 제대로 CI/CD 되는지 확인할 예정</p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-todays-goal-i-done">📌 Today&#39;s Goal I Done</h2>
<h3 id="✔️-aws-elastic-beanstalk에-대해-조사">✔️ AWS Elastic Beanstalk에 대해 조사</h3>
<ul>
<li><p>오늘은 최종 프로젝트에 적용할 AWS Elastic Beanstalk에 대해서 간단하게 조사함</p>
</li>
<li><p>다행히 잘 정리된 내용을 찾아서 그 내용을 참고로 적용할 예정</p>
</li>
</ul>
<br>]]></description>
        </item>
        <item>
            <title><![CDATA[[2024.09.19 TIL] 코드카타 (다트 게임)]]></title>
            <link>https://velog.io/@my_code/2024.09.19-TIL-%EC%BD%94%EB%93%9C%EC%B9%B4%ED%83%80-%EB%8B%A4%ED%8A%B8-%EA%B2%8C%EC%9E%84</link>
            <guid>https://velog.io/@my_code/2024.09.19-TIL-%EC%BD%94%EB%93%9C%EC%B9%B4%ED%83%80-%EB%8B%A4%ED%8A%B8-%EA%B2%8C%EC%9E%84</guid>
            <pubDate>Thu, 19 Sep 2024 08:00:21 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>본 내용은 하루 활동한 내용을 기록하기 위한 게시물입니다.</p>
</blockquote>
<hr>
<h1 id="💻-tiltoday-i-learned">💻 TIL(Today I Learned)</h1>
<h2 id="📌-today-i-done">📌 Today I Done</h2>
<h3 id="✏️-다트-게임">✏️ 다트 게임</h3>
<h4 id="📝-문제">📝 문제</h4>
<p>카카오톡 게임별의 하반기 신규 서비스로 다트 게임을 출시하기로 했다. 다트 게임은 다트판에 다트를 세 차례 던져 그 점수의 합계로 실력을 겨루는 게임으로, 모두가 간단히 즐길 수 있다.</p>
<p>갓 입사한 무지는 코딩 실력을 인정받아 게임의 핵심 부분인 점수 계산 로직을 맡게 되었다. 다트 게임의 점수 계산 로직은 아래와 같다.</p>
<ol>
<li>다트 게임은 총 3번의 기회로 구성된다.</li>
<li>각 기회마다 얻을 수 있는 점수는 0점에서 10점까지이다.</li>
<li>점수와 함께 Single(S), Double(D), Triple(T) 영역이 존재하고 각 영역 당첨 시 점수에서 1제곱, 2제곱, 3제곱 (점수1 , 점수2 , 점수3 )으로 계산된다.</li>
<li>옵션으로 스타상(<em>) , 아차상(#)이 존재하며 스타상(</em>) 당첨 시 해당 점수와 바로 전에 얻은 점수를 각 2배로 만든다. 아차상(#) 당첨 시 해당 점수는 마이너스된다.</li>
<li>스타상(<em>)은 첫 번째 기회에서도 나올 수 있다. 이 경우 첫 번째 스타상(</em>)의 점수만 2배가 된다. (예제 4번 참고)</li>
<li>스타상(<em>)의 효과는 다른 스타상(</em>)의 효과와 중첩될 수 있다. 이 경우 중첩된 스타상(*) 점수는 4배가 된다. (예제 4번 참고)</li>
<li>스타상(*)의 효과는 아차상(#)의 효과와 중첩될 수 있다. 이 경우 중첩된 아차상(#)의 점수는 -2배가 된다. (예제 5번 참고)</li>
<li>Single(S), Double(D), Triple(T)은 점수마다 하나씩 존재한다.</li>
<li>스타상(*), 아차상(#)은 점수마다 둘 중 하나만 존재할 수 있으며, 존재하지 않을 수도 있다.</li>
</ol>
<p>0~10의 정수와 문자 S, D, T, *, #로 구성된 문자열이 입력될 시 총점수를 반환하는 함수를 작성하라.</p>
<h4 id="⌛️-제한사항">⌛️ 제한사항</h4>
<ul>
<li>X</li>
</ul>
<h4 id="📥-입출력-예시">📥 입출력 예시</h4>
<p><img src="https://velog.velcdn.com/images/my_code/post/50dcfb45-cecc-4d9c-9aa4-17022915d272/image.png" alt=""></p>
<br>

<h4 id="⭕️-제출-코드">⭕️ 제출 코드</h4>
<ul>
<li><p>어떤 방법을 사용할지 고민하다가 점수와 보너스, 옵션를 각각의 리스트로 만들어서 보너스와 옵션에 맞도록 점수 리스트를 만들어서 마지막에 합산하는 방법을 선택함</p>
</li>
<li><p>그래서 정규표현식을 통해서 점수, 보너스, 옵션을 분리해서 튜플 형태로 만들어서 사용함</p>
<pre><code class="language-python"></code></pre>
</li>
</ul>
<p>import re</p>
<p>def cal(num, opt):
    if opt == &#39;S&#39;:
        return num ** 1
    elif opt == &#39;D&#39;:
        return num ** 2
    elif opt == &#39;T&#39;:
        return num ** 3</p>
<p>def solution(dartResult):
    scores = []
    options = []</p>
<pre><code># 점수와 보너스, 옵션을 한 번에 추출
parts = re.findall(&#39;(\d+)([SDT])([*#]?)&#39;, dartResult)

for score, bonus, option in parts:
    scores.append(cal(int(score), bonus))
    options.append(option if option else &#39; &#39;)

result = 0
for i in range(len(options)):
    if options[i] == &#39;*&#39;:
        scores[i] *= 2
        if i &gt; 0:
            scores[i-1] *= 2
    elif options[i] == &#39;#&#39;:
        scores[i] *= -1

return sum(scores)</code></pre><pre><code>
&lt;br&gt;

#### 🔔 다른 사람 풀이
- 다른 사람 풀이인데, 정규표현식을 사용하지 않아서 코드가 상당히 길어짐

- 정규표현식 자체를 계속 외우고 있기 어렵기 때문에 아래와 같은 방법도 알아두는 게 좋을 것 같음

- 정규표현식의 사용 여부만 다르고 마지막에 점수를 합치는 방법은 같음
```python
def bonus(num, bonus_num):
    if bonus_num == &#39;S&#39;:
        return num ** 1
    elif bonus_num == &#39;D&#39;:
        return num ** 2
    else:
        return num ** 3


def solution(dartResult):
    answer = 0
    answer_list = [0]
    score = []

    for char in dartResult:
        if char.isdigit():
            if score:
                if len(score) == 2:
                    result1 = bonus(int(score[0]), score[1])
                    answer_list.append(result1)
                    score = []

                elif len(score) == 3:
                    result1 = bonus(int(score[0]), score[1])

                    if score[2] == &#39;*&#39;:
                        answer_list[-1] *= 2
                        answer_list.append(result1 * 2)
                    elif score[2] == &#39;#&#39;:
                        answer_list.append(result1 * (-1))
                    score = []

            if len(score) &gt; 0 and score[-1].isdigit():
                score[-1] = int(score[0])* 10 + int(char)
                score[-1] = str(score[-1])
            else:
                score.append(char)
        else:
            score.append(char)

    if score:
        if len(score) == 2:
            result1 = bonus(int(score[0]), score[1])
            answer_list.append(result1)

        elif len(score) == 3:
            result1 = bonus(int(score[0]), score[1])

            if score[2] == &#39;*&#39;:
                answer_list[-1] *= 2
                answer_list.append(result1 * 2)
            elif score[2] == &#39;#&#39;:
                answer_list.append(result1 * (-1))

    return sum(answer_list)</code></pre><br>

<hr>
<h2 id="📌-tomorrows-goal">📌 Tomorrow&#39;s Goal</h2>
<h3 id="✏️-이력서-피드백-반영">✏️ 이력서 피드백 반영</h3>
<ul>
<li><p>제출한 이력서에 대한 피드백을 받을 예정</p>
</li>
<li><p>길면 이틀 정도 소요된다고 해서 내일이면 받을 것 같음</p>
</li>
<li><p>피드백 받은 내용을 기반으로 이력서를 수정할 예정</p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-todays-goal-i-done">📌 Today&#39;s Goal I Done</h2>
<h3 id="✔️-코드카타-문제-풀기">✔️ 코드카타 문제 풀기</h3>
<ul>
<li><p>오늘은 2018년 카카오 블라인드 테스트에서 나온 문제를 품</p>
</li>
<li><p>레벨1이긴 하지만 생각보다 어려웠음</p>
</li>
<li><p>하지만 정답률이 높은 걸 보니 그리 어려운 문제에 속하지 않는 것 같음</p>
</li>
<li><p>그렇기에 너무 급하지 않게 꾸준히 문제를 풀면서 레벨을 올려야겠음</p>
</li>
</ul>
<br>]]></description>
        </item>
        <item>
            <title><![CDATA[[2024.09.11 TIL] 면접 준비, 커리어톤 기술 면접 예상 질문]]></title>
            <link>https://velog.io/@my_code/2024.09.11-TIL-%EB%A9%B4%EC%A0%91-%EC%A4%80%EB%B9%84-%EC%BB%A4%EB%A6%AC%EC%96%B4%ED%86%A4-%EA%B8%B0%EC%88%A0-%EB%A9%B4%EC%A0%91-%EC%98%88%EC%83%81-%EC%A7%88%EB%AC%B8</link>
            <guid>https://velog.io/@my_code/2024.09.11-TIL-%EB%A9%B4%EC%A0%91-%EC%A4%80%EB%B9%84-%EC%BB%A4%EB%A6%AC%EC%96%B4%ED%86%A4-%EA%B8%B0%EC%88%A0-%EB%A9%B4%EC%A0%91-%EC%98%88%EC%83%81-%EC%A7%88%EB%AC%B8</guid>
            <pubDate>Wed, 11 Sep 2024 07:59:50 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>본 내용은 내일배움캠프 커리어톤에서 활동한 내용을 기록한 글입니다.</p>
</blockquote>
<hr>
<h1 id="💻-tiltoday-i-learned">💻 TIL(Today I Learned)</h1>
<h2 id="📌-today-i-done">📌 Today I Done</h2>
<h3 id="✏️-class와-object에-대해-설명해주세요">✏️ Class와 Object에 대해 설명해주세요.</h3>
<ul>
<li><p>Class는 객체를 생성하기 위한 템플릿 또는 설계도로, 객체의 속성과 메서드를 정의합니다.</p>
</li>
<li><p>Object는 클래스의 인스턴스로 클래스라는 설계도를 보고 만들어진 것을 의미합니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-polymorphism-개념에-대해-설명하고-개인팀-프로젝트에-적용한-사례가-있다면-이야기해주세요">✏️ Polymorphism 개념에 대해 설명하고, 개인/팀 프로젝트에 적용한 사례가 있다면 이야기해주세요.</h3>
<ul>
<li><p>다형성은 클래스가 다양한 형태로 동작하는 것을 의미합니다.</p>
</li>
<li><p>즉, 동일한 메서드 명을 사용해도 그 메서드가 다르게 동작하게 만드는 특성입니다.</p>
</li>
<li><p>대표적으로 오버로딩, 오버라이딩이 있습니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-object-의-특성에-따라-사용할-수-있는-data-structure-에는-어떤-것이-있는지-설명해주세요">✏️ Object 의 특성에 따라 사용할 수 있는 Data Structure 에는 어떤 것이 있는지 설명해주세요.</h3>
<ul>
<li><p>객체는 키와 값 쌍의 집합으로 키의 이름을 통해서 값에 접근합니다.</p>
</li>
<li><p>예를 들면, 사용자 객체에 사용자 정보를 저장해서 데이터 관리가 가능합니다.</p>
</li>
<li><p>그 외에도 객체 데이터를 순서있는 형태로 다루기 위한 배열, 객체보다 더 다양한 타입의 키를 사용할 수 있는 맵 등이 있습니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-개인팀-프로젝트에-적용해-보았거나--사용할-줄-아는-rdb-에-대해-이야기해주세요">✏️ 개인/팀 프로젝트에 적용해 보았거나 / 사용할 줄 아는 RDB 에 대해 이야기해주세요.</h3>
<ul>
<li><p>최근에 진행한 팀 프로젝트에서 MySQL을 RDB로 사용했습니다.</p>
</li>
<li><p>이 때, Nest.js 프레임워크에서 TypeORM을 활용해서 데이터베이스와의 상호작용을 구현했습니다.</p>
</li>
<li><p>MySQL를 사용하여 복잡한 관계를 관리할 수 있으며, ACID 트랜젝션을 통해서 오류 및 충돌 방지가 가능하기 때문에 선택했습니다.</p>
</li>
<li><p>그리고 TypeScript와 MySQL과 호환성이 좋은 TypeORM를 사용했고, 쿼리 빌더를 통해 복잡한 DB 쿼리를 로우 쿼리와 유사한 형태로 쉽게 작성할 수 있었습니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-javakotlin-으로-구성된-프로젝트의-kotlinjava-컨버팅-작업이-가능한지-무엇을-염두고려-해야하는지-이야기해주세요">✏️ Java(Kotlin) 으로 구성된 프로젝트의 Kotlin(Java) 컨버팅 작업이 가능한지, 무엇을 염두/고려 해야하는지 이야기해주세요.</h3>
<ul>
<li><p>주로 Node.js를 사용했기 때문에 위의 질문보다는 JavaScript를 TypeScript로 컨버팅하는 것에 대해 이야기 해보겠습니다.</p>
</li>
<li><p>JS와 TS의 가장 큰 차이는 타입 정의의 유무입니다.</p>
</li>
<li><p>그 밖에 기본적인 문법이 동일하기 때문에 변환 작업이 충분히 가능합니다.</p>
</li>
<li><p>JS에서 TS로 변환하기 위해서는 기존의 JS코드에서 타입을 정의해줄 필요가 있고, .js 파일에서 .ts파일 확장자로 변경해야 합니다.</p>
</li>
<li><p>그리고 TypeScript를 사용하기 위해서는 tsconfig.json과 같은 TypeScript 설정 파일을 생성해서 필요한 설정을 작성해야 합니다.</p>
</li>
<li><p>이렇게 JS에서 TS로의 변환을 통해 코드 품질을 개선하고 유지보수성을 높일 수 있고, 코드 작성 단계에서 에러를 검출할 수 있기 때문에 안전하고 효율적인 코드를 작성할 수 있습니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-legacy-spring-과-최근-spring-혹은-spring-boot-의-차이점에-대해-아는대로-설명해주세요">✏️ Legacy Spring 과 최근 Spring 혹은 Spring Boot 의 차이점에 대해 아는대로 설명해주세요.</h3>
<ul>
<li><p>이 질문도 Node.js와 맞지 않아서 Express.js와 Nest.js의 차이점에 대해서 설명하겠습니다.</p>
</li>
<li><p>Express는 가장 널리 사용되는 Node.js 웹 프레임워크입니다.</p>
</li>
<li><p>Express는 주로 간단한 웹 서버 구축을 통해 빠르게 웹을 개발하기 위해서 사용됩니다.</p>
</li>
<li><p>Express의 경우 별도의 라이프 사이클이 없고 미들웨어로 개발자가 구현해야 하는 문제가 있습니다.</p>
</li>
<li><p>그리고 Express는 자바스크립트 기반이기에 타입에 의한 런타임 에러가 많이 발생합니다.</p>
</li>
</ul>
<ul>
<li><p>그래서 이에 대한 대안으로 저는 대표적으로 Nest.js가 있다고 생각합니다.</p>
</li>
<li><p>Nest.js는 타입스크립트를 기반으로 해서 타입에 대한 런타임 에러를 줄일 수 있습니다.</p>
</li>
<li><p>그리고 Nest.js에는 가드, 인터셉터, 파이프 등과 같은 라이프 사이클이 존재하기 때문에 개발자는 그 라이프 사이클에 맞춰서 개발을 진행하면 됩니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-rdb와-nosql-db의-차이점에-대해-이야기해주세요">✏️ RDB와 NoSQL DB의 차이점에 대해 이야기해주세요.</h3>
<ul>
<li><p>RDB는 데이터를 테이블 형식으로 저장하며, 고정된 스키마를 가집니다.</p>
</li>
<li><p>각 테이블 간의 관계를 정의하기 위해 외래 키를 사용합니다.</p>
</li>
<li><p>그리고 ACID 트랜잭션을 지원하여 데이터의 무결성을 보장합니다.</p>
</li>
<li><p>RDB의 예로 금융 시스템, ERP 시스템 등 데이터 무결성이 중요한 곳에서 사용합니다.</p>
</li>
</ul>
<ul>
<li><p>NoSQL DB는 다양한 형식으로 데이터를 저장할 수 있으며, 스키마가 유연하기 때문에 데이터 구조를 사정에 정의할 필요가 없습니다.</p>
</li>
<li><p>높은 가용성과 성능을 제공하지만 완벽한 일관성을 보장하진 않습니다.</p>
</li>
<li><p>NoSQL DB의 예로 소셜 미디어, IoT, 빅데이터 분석 등 대규모 데이터를 빠르게 처리하는 곳에서 사용합니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-restful-api의-설계-원칙을-설명해주세요">✏️ RESTful API의 설계 원칙을 설명해주세요.</h3>
<ul>
<li><p>REST API는 HTTP Method를 통해 자원에 대한 CRUD를 적용하고 주소를 정하는 방법을 의미합니다.</p>
</li>
<li><p>즉, RESTFul API는 REST API 주소 체계를 이용하는 시스템을 의미합니다.</p>
</li>
<li><p>RESTful API의 설계 원칙은 적절한 HTTP 메서드를 사용하고, 서버의 상태 유지가 없으며, 적할한 HTTP 상태 코드를 사용하는 등으로 구성되어 있습니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-서버-사이드-렌더링과-클라이언트-사이드-렌더링의-차이점을-설명해주세요">✏️ 서버 사이드 렌더링과 클라이언트 사이드 렌더링의 차이점을 설명해주세요.</h3>
<ul>
<li>서버 사이드 렌더링은 서버에서 웹페이지의 콘텐츠를 생성하여 클라이언트에 전달하는 방식입니다.</li>
<li>클라이언트 사이드 렌더링은 클라이언트에서 자바스크립트를 통해 콘텐츠를 동적으로 생성하는 방식입니다.</li>
<li>그리고 서버 사이드 렌더링은 서버에서 상태를 관리하는 반면, 클라이언트 사이드 렌더링은 클라이언트에서 상태를 관리하여 사용자 상호작용에 따라 동적으로 콘텐츠를 변경할 수 있습니다.</li>
</ul>
<br>

<h3 id="✏️-웹훅webhook이란-무엇인지-설명해주세요">✏️ 웹훅(Webhook)이란 무엇인지 설명해주세요.</h3>
<ul>
<li><p>웹훅은 특정 이벤트가 발생했을 때 다른 시스템에 자동으로 HTTP POST 요청을 보내는 방법입니다.</p>
</li>
<li><p>이를 통해 실시간으로 데이터나 알림을 전달할 수 있습니다.</p>
</li>
<li><p>예를 들어, 결제가 완료될 때 결제 서비스 정보를 서버로 전송하는 등의 다양한 시스템 간의 효율적인 실시간 통신을 가능하게 합니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-redis의-주요-사용-사례를-설명해-주세요">✏️ Redis의 주요 사용 사례를 설명해 주세요.</h3>
<ul>
<li><p>Redis는 고성능의 인메모리 데이터 구조 저장소입니다.</p>
</li>
<li><p>가장 대표적인 사용 사례는 캐싱 기능으로, 데이터베이스의 쿼리 결과나 API 응답을 캐싱하여 애플리케이션의 성능을 향상시킵니다.</p>
</li>
<li><p>데이터베이스의 대한 반복적인 요청을 줄이고, 빠른 읽기 속도를 제공합니다.</p>
</li>
<li><p>그리고 순위 및 점수 관리 용도로도 많이 사용됩니다.</p>
</li>
<li><p>Sorted Set 자료구조를 사용하여 콘텐츠 인기 순위 등을 관리할 수 있으며, 빠른 삽입 및 조회가 가능하여 효율적입니다.</p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-tomorrows-goal">📌 Tomorrow&#39;s Goal</h2>
<h3 id="✏️-기술-면접-예상-질문-답변-작성하기--면접-코칭-신청">✏️ 기술 면접 예상 질문 답변 작성하기 + 면접 코칭 신청</h3>
<ul>
<li><p>내일은 나머지 기술 면접 예상 질문 답변을 작성할 예정</p>
</li>
<li><p>그리고 첫 면접 코칭을 신청할 예정</p>
</li>
</ul>
<br>

<h3 id="✏️-현직자-커피챗-진행하기">✏️ 현직자 커피챗 진행하기</h3>
<ul>
<li><p>내일 저녁에 현직자 커피챗을 통해서 이것 저것 질문할 예정</p>
</li>
<li><p>사실 뭘 물어볼지 조금 막막한 상태라서 취업까지의 진행 과정을 물어볼 예정</p>
</li>
<li><p>일단 눈 앞에 있는 것부터 하나씩 해결하면서 진행해야 할 것 같음</p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-todays-goal-i-done">📌 Today&#39;s Goal I Done</h2>
<h3 id="✔️-기술-면접-예상-질문-작성하기">✔️ 기술 면접 예상 질문 작성하기</h3>
<ul>
<li><p>오늘은 기술 면접 예상 질문에 대한 답변을 작성함</p>
</li>
<li><p>질문 내용이 어렵지 않았지만 조금 더 구체적으로 대답할 필요가 있어 보이기 때문에 해당 내용들을 찾아서 답변을 작성함</p>
</li>
<li><p>그리고 5번 질문은 Node.js와 다르기 때문에 조금 비슷할 것 같은 질문으로 JS와 TS에 대한 내용을 작성함</p>
</li>
</ul>
<br>]]></description>
        </item>
        <item>
            <title><![CDATA[[2024.09.09 TIL] 면접 준비, 커리어톤 인성 면접 예상 질문]]></title>
            <link>https://velog.io/@my_code/2024.09.09-TIL-%EB%A9%B4%EC%A0%91-%EC%A4%80%EB%B9%84-%EC%9D%B8%EC%84%B1-%EB%A9%B4%EC%A0%91-%EC%98%88%EC%83%81-%EC%A7%88%EB%AC%B8-1</link>
            <guid>https://velog.io/@my_code/2024.09.09-TIL-%EB%A9%B4%EC%A0%91-%EC%A4%80%EB%B9%84-%EC%9D%B8%EC%84%B1-%EB%A9%B4%EC%A0%91-%EC%98%88%EC%83%81-%EC%A7%88%EB%AC%B8-1</guid>
            <pubDate>Mon, 09 Sep 2024 08:48:49 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>본 내용은 내일배움캠프 커리어톤에서 활동한 내용을 기록한 글입니다.</p>
</blockquote>
<hr>
<h1 id="💻-tiltoday-i-learned">💻 TIL(Today I Learned)</h1>
<h2 id="📌-today-i-done">📌 Today I Done</h2>
<h3 id="✏️-우리-회사에-지원한-동기를-말씀해주실-수-있을까요">✏️ 우리 회사에 지원한 동기를 말씀해주실 수 있을까요?</h3>
<ul>
<li><p>저는 귀사의 ~~서비스를 바탕으로 사용자에게 더 좋은 편의성을 제공할 수 있는 개발자가 되고 싶습니다.</p>
</li>
<li><p>그리고 저는 내일배움캠프라는 부트캠프를 통해 얻은 백엔드 지식과 협업 능력, 끈기를 귀사에서 더욱 성장시켜 팀에 긍정적인 영향을 미치고, 함께 성장하며 발전해 나가고 싶기 때문에 지원했습니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-백엔드-개발자가-되기로-한-이유에-대해-말씀해주실-수-있을까요">✏️ 백엔드 개발자가 되기로 한 이유에 대해 말씀해주실 수 있을까요?</h3>
<ul>
<li><p>저는 백엔드 개발자가 되기로 결심한 이유는 복잡한 시스템을 설계하고, 데이터 처리의 효율성을 극대화하는 데 매력을 느꼈기 때문입니다.</p>
</li>
<li><p>웹 애플리케이션의 기초를 다지는 백엔드 개발이 사용자 경험에 직접적인 영향을 미친다는 점에서 큰 보람을 느끼고 있습니다.</p>
</li>
<li><p>또한, 다양한 기술 스택을 활용해 문제를 해결하고, 지속적으로 발전할 수 있는 환경에서 일하는 것이 저에게 매우 흥미롭습니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-백엔드-개발자로서-본인만의-강점과-근거가-되는-경험에-대해-말씀해주세요">✏️ 백엔드 개발자로서 본인만의 강점과 근거가 되는 경험에 대해 말씀해주세요.</h3>
<ul>
<li><p>저의 백엔드 개발자로서의 강점은 문제 해결 능력과 효율성을 생각한다는 점입니다.</p>
</li>
<li><p>이를 바탕으로, &quot;Give me the ticket&quot; 프로젝트에서 AWS CloudFront를 도입하여 이미지 요청 속도를 70% 감소시키는 성과를 달성했습니다.</p>
</li>
<li><p>이 경험을 통해 성능 최적화의 중요성을 깊이 이해하게 되었고, 사용자 경험을 개선하는 데 기여할 수 있었습니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-첫-직장--다음-직장에서-어떤-걸-기대하고-있나요">✏️ 첫 직장 / 다음 직장에서 어떤 걸 기대하고 있나요?</h3>
<ul>
<li><p>첫 직장에서는 실제 업무를 통해 이론을 적용하고, 실무 경험을 쌓는 것을 기대하고 있습니다.</p>
</li>
<li><p>특히, 다양한 프로젝트에 참여하며 팀원들과 협업하고, 멘토링을 통해 빠르게 성장할 수 있는 환경을 원합니다.</p>
</li>
<li><p>또한, 새로운 기술을 배우고, 문제 해결 능력을 키우는 기회를 통해 전문성을 발전시키고 싶습니다.</p>
</li>
<li><p>다음 직장에서는 이전 경험을 바탕으로 더 깊이 있는 기술적 도전과 리더십 역할을 기대합니다.</p>
</li>
<li><p>팀의 목표 달성을 위해 기여하고, 후배 개발자들에게 지식을 공유하며 성장하는 문화를 만들어가는 데 기여하고 싶습니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-지원자-님을-동기부여하게-만드는-요인이-무엇인가요">✏️ 지원자 님을 동기부여하게 만드는 요인이 무엇인가요?</h3>
<ul>
<li>명확한 목표를 설정하고 이를 달성하기 위한 과정에서 성취감을 느끼는 것이 저에게 큰 동기부여가 됩니다.</li>
</ul>
<br>

<h3 id="✏️-회사가-지원자-님을-꼭-뽑아야-하는-요소가-있다면-말씀해주실-수-있나요">✏️ 회사가 지원자 님을 꼭 뽑아야 하는 요소가 있다면 말씀해주실 수 있나요?</h3>
<ul>
<li><p>저는 새로운 기술과 트렌드에 대한 높은 호기심과 학습 의지를 가지고 있습니다.</p>
</li>
<li><p>변화하는 기술 환경에 빠르게 적응하며, 팀에 필요한 기술을 지속적으로 습득해 나갈 것입니다.</p>
</li>
<li><p>그리고 새로운 도전과 변화에 대해 긍정적인 자세를 유지하며, 항상 최선을 다해 업무에 임하는 태도를 가지고 있습니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-5년-10년-후에-어떤-사람이-되고-싶나요">✏️ 5년, 10년 후에 어떤 사람이 되고 싶나요?</h3>
<ul>
<li><p>5년 후에는 숙련된 백엔드 개발자로 자리 잡고, 다양한 프로젝트를 통해 폭넓은 기술 경험을 쌓고 싶습니다.</p>
</li>
<li><p>팀의 핵심 멤버로서 기술적 문제를 해결하고, 후배 개발자들에게 멘토링을 하며 팀의 성장을 이끌어가는 역할을 맡고 싶습니다.</p>
</li>
<li><p>10년 후에는 기술 리더로 성장하여, 팀을 이끌고 전략적인 의사결정을 내리는 역할을 수행하고 싶습니다.</p>
</li>
<li><p>프로젝트 매니지먼트와 팀 관리 능력을 갖춘 리더가 되어, 혁신적인 솔루션을 개발하고 조직의 목표를 달성하는 데 기여할 것입니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-팀-프로젝트에서-스트레스를-받거나-갈등-상황이-있을-때-어떻게-대처하나요">✏️ 팀 프로젝트에서 스트레스를 받거나 갈등 상황이 있을 때 어떻게 대처하나요?</h3>
<ul>
<li><p>우선 냉정함을 유지하고 상황을 분석하려고 합니다.</p>
</li>
<li><p>감정적으로 반응하기 보다는 문제의 본질을 파악하는 데 집중합니다.</p>
</li>
<li><p>그리고 갈등 상황에서 받은 피드백을 바탕으로 스스로를 돌아보고 개선할 점을 찾으려고 할 것 같습니다.</p>
</li>
<li><p>나의 생각을 옳다고만 말하는 것보다 소통을 통해서 적극적으로 피드백을 수용할 줄 알아야 한다고 생각합니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-작업-일정이-촉박할-때-어떤-생각을-하는-편인가요">✏️ 작업 일정이 촉박할 때 어떤 생각을 하는 편인가요?</h3>
<ul>
<li><p>저는 가장 먼저 작업의 우선 순위를 정해서 가장 중요한 작업부터 처리해야 한다고 생각합니다.</p>
</li>
<li><p>이러한 상황에 대해서 팀원들과 소통해서 협력할 수 있는 방안을 찾아야 한다고 생각합니다.</p>
</li>
<li><p>또한, 시간을 효율적으로 관리하기 위해서 일정표를 만들어 계획을 재수립해야 할 것 같습니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-프로젝트--업무-진행-중-동료와-갈등이-있었던-경험이-있나요">✏️ 프로젝트 / 업무 진행 중 동료와 갈등이 있었던 경험이 있나요?</h3>
<ul>
<li><p>저는 이전 프젝트에서 팀원들과 동시성 처리 기술 선택에 대한 갈등이 있었습니다.</p>
</li>
<li><p>의견은 TypeORM의 비관적 락을 사용할지, Redis을 사용한 RedLock을 사용할지로 나눠졌습니다.</p>
</li>
<li><p>우선 팀원들의 의견을 경청하고, 그들이 선호하는 기술의 장단점을 이해하려 했습니다.</p>
</li>
<li><p>이후, 각 기술들의 대한 자료를 찾아보고, 장단점을 확인해 RedLock를 선택하는 의견으로 통합되었습니다.</p>
</li>
<li><p>결국, 팀원들과의 협의를 통해 최선의 선택을 할 수 있었고, 갈등을 효과적으로 해결할 수 있었습니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-가장-좋았던--힘들었던-협업-경험을-소개해주세요">✏️ 가장 좋았던 / 힘들었던 협업 경험을 소개해주세요.</h3>
<ul>
<li><p>가장 좋았던 협업 경험은 하루에 두 번 스크럼을 통해서 서로의 피드백을 주고 받는 과정이 가장 좋았습니다.</p>
</li>
<li><p>오전에는 오늘의 목표에 대한 보고를 진행해서 각자가 맡은 작업을 공유했습니다. 이를 통해 팀원들이 서로의 진행 상황을 이해하고, 필요한 자원이나 조언을 주고 받을 수 있었습니다.</p>
</li>
<li><p>저녁에는 그 날 목표에 대한 진행 상황을 보고하는 시간을 가졌습니다. 이를 통해 각자의 성과를 돌아보고, 어려운 점에 대한 논의도 할 수 있었습니다.</p>
</li>
<li><p>결과적으로 프로젝트의 효율성이 크게 향상되었고, 팀원들과 협업이 더욱 즐거운 경험이 되었습니다.</p>
</li>
<li><p>이 경험은 제가 팀워크와 소통의 중요성을 깊게 이해하는 데 큰 도움이 되었습니다.</p>
</li>
</ul>
<br>

<ul>
<li><p>가장 힘들었던 협업 경험은 공연 예매 및 중고 거래 프로젝트에서 예매, 환불 정책에 대한 기획을 진행할 때였습니다.</p>
</li>
<li><p>팀원들마다 각자 다양한 의견들이 있어서 의사결정이 지연되었습니다.</p>
</li>
<li><p>예를 들어, 사용자의 편의성과 회사의 이익, 환불보다 중고 거래를 사용하도록 만들어야 하기 때문에, 여러 번의 회의를 통해 합의점을 찾는 과정이 힘들었습니다.</p>
</li>
<li><p>특히, 프로젝트가 진행되는 동안 예매와 환불 정책에 대한 내용이 계속해서 변경되다 보니, 팀 안에서도 혼란이 발생했습니다.</p>
</li>
<li><p>이러한 문제를 해결하기 위해서 기준이 되는 밴치마킹 사이트를 정해서 서로의 의견 차이를 좁혔습니다.</p>
</li>
<li><p>그리고 정책 변경에 대한 문서화를 통해 모두가 동일한 정보를 공유할 수 있도록 했습니다.</p>
</li>
<li><p>이 경험을 통해 변화에 유연하게 대응하는 것의 중요성과 팀워크의 가치를 깊이 이해하게 되었습니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-소통이-어려운-상황에서-어떻게-대처하시나요">✏️ 소통이 어려운 상황에서 어떻게 대처하시나요?</h3>
<ul>
<li><p>소통이 어려운 상황이라면 저는 가장 먼저 상대방의 의견을 경청할 것 같습니다.</p>
</li>
<li><p>그들의 입장을 이해하려고 노력하고, 필요하다면 추가 질문을 통해 명확히 합니다.</p>
</li>
<li><p>복잡한 내용이라면, 시각 자료나 문서화된 내용을 통해 명확하게 전달하는 것도 좋은 방법이라고 생각합니다.</p>
</li>
<li><p>또한, 내가 전달한 내용이 제대로 이해되었는지 확인하고 필요하면 설명을 다시 해줍니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-타인과의-협업에서-가장-중요하게-생각하는-것이-무엇인가요">✏️ 타인과의 협업에서 가장 중요하게 생각하는 것이 무엇인가요?</h3>
<ul>
<li><p>저는 타인과의 협업에서 소통과 상대를 배려하는 자세가 가장 중요하다고 생각합니다.</p>
</li>
<li><p>효과적인 소통은 협업을 원활하게 만드는 핵심 요소입니다.</p>
</li>
<li><p>각자의 의견을 명확하게 전달하고 상대방의 의견을 경청함으로써 오해를 줄이고 공동의 목표를 함께 이해할 수 있습니다.</p>
</li>
<li><p>또한, 상대를 배려하는 자세도 매우 중요합니다.</p>
</li>
<li><p>각 팀원의 강점과 약점을 이해하고 필요한 경우 지원하는 것이 팀워크를 강화하는 데 큰 도움이 됩니다.</p>
</li>
<li><p>이런 두 가지 자세로 항상 협업에 임하고자 합니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-친구--동료는-지원자-님을-어떤-사람이라고-말하나요">✏️ 친구 / 동료는 지원자 님을 어떤 사람이라고 말하나요?</h3>
<ul>
<li><p>제 동료들은 저를 신뢰할 수 있는 사람이라고 말할 것 같습니다.</p>
</li>
<li><p>저는 항상 책임감 있게 일에 임하고, 팀원들이 필요할 때 도움을 주려고 노력합니다.</p>
</li>
<li><p>또한, 소통을 중요시하는 사람이라고도 생각합니다.</p>
</li>
<li><p>서로의 의견을 존중하고, 열린 마음으로 대화를 나누는 것을 중요하게 여깁니다.</p>
</li>
</ul>
<br>

<h3 id="✏️-동료와-서로-의견이-다른-경우-어떻게-조율하시나요">✏️ 동료와 서로 의견이 다른 경우 어떻게 조율하시나요?</h3>
<ul>
<li><p>저는 가장 먼저 상대방의 의견을 충분히 경청합니다.</p>
</li>
<li><p>동료의 관점에서 이해하려고 노력하며, 이를 통해 대화의 기초를 다집니다.</p>
</li>
<li><p>각자의 의견에 대한 근거와 데이터를 공유하여 객관적인 정보를 바탕으로 논의하며, 타협점을 탖으려고 노력합니다.</p>
</li>
<li><p>필요하다면 팀 회의를 소집하여 더 많은 의견을 수렴하고 다양한 관점을 고려하는 과정을 가질 것 같습니다.</p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-tomorrows-goal">📌 Tomorrow&#39;s Goal</h2>
<h3 id="✏️-직무-지식-복습하기">✏️ 직무 지식 복습하기</h3>
<ul>
<li><p>웹 서버에 대한 기술 질문들 복습하기</p>
</li>
<li><p>이전에 작성했던 기술 질문들에 대한 내용을 하나의 게시물에 정리하기</p>
</li>
<li><p>질문들에 대한 답변을 입으로 말하는 연습하기</p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-todays-goal-i-done">📌 Today&#39;s Goal I Done</h2>
<h3 id="✔️-인성-면접-예상-질문">✔️ 인성 면접 예상 질문</h3>
<ul>
<li><p>오늘은 면접 대비반 첫 날</p>
</li>
<li><p>커리어톤에 대한 간단한 OT를 진행하고 이력서나 면접을 준비하면서 가져야 할 자세 등에 대한 세션을 진행함</p>
</li>
<li><p>오늘은 면접 대비반 미션 중 현직자 커피챗 신청과 인성 면접 예상 질문에 대한 답변을 작성하는 시간을 가짐</p>
</li>
<li><p>다른 질문들은 인터넷을 참고하면서 작성하는 게 가능했음</p>
</li>
<li><p>하지만 지원 동기에 대한 답변은 좀처럼 결정되지 않았음</p>
</li>
<li><p>회사들마다 기술이나 비전이 있고 어떤 식으로 답변해야 할지 막막했기 때문에 현직자 커피챗을 신청하면서 질문 사항으로 넣어서 신청함</p>
</li>
<li><p>현직자 커피챗 이후에 조금 더 수정, 보완할 예정</p>
</li>
</ul>
<br>]]></description>
        </item>
        <item>
            <title><![CDATA[[2024.09.04 TIL] 이력서 작성, 커리어톤 자기소개 수정하기]]></title>
            <link>https://velog.io/@my_code/2024.09.04-TIL-%EC%9D%B4%EB%A0%A5%EC%84%9C-%EC%9E%91%EC%84%B1-%EC%9E%90%EA%B8%B0%EC%86%8C%EA%B0%9C-%EC%88%98%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@my_code/2024.09.04-TIL-%EC%9D%B4%EB%A0%A5%EC%84%9C-%EC%9E%91%EC%84%B1-%EC%9E%90%EA%B8%B0%EC%86%8C%EA%B0%9C-%EC%88%98%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 05 Sep 2024 08:47:01 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.</p>
</blockquote>
<hr>
<h1 id="💻-tiltoday-i-learned">💻 TIL(Today I Learned)</h1>
<h2 id="📌-today-i-done">📌 Today I Done</h2>
<h3 id="✏️-자기소개-피드백">✏️ 자기소개 피드백</h3>
<blockquote>
<p>[자기소개]</p>
<ul>
<li>이전에 자기소개 피드백으로 드린 사항이 거의 반영되지 않아 다시 첨부드립니다.</li>
</ul>
<p>node 기반 백엔드 개발자를 준비하시므로 자기소개에서 아래를 추가로 어필해주면 좋습니다.</p>
<ol>
<li>nest js 기반으로 OOP에 대한 이해도가 있음</li>
<li>SQL 기반 DB를 다룰 수 있음 (MySQL or Postgresql)</li>
</ol>
</blockquote>
<br>

<h3 id="✏️-자기소개-수정하기">✏️ 자기소개 수정하기</h3>
<ul>
<li><p>저는 성능 개선과 효율성을 중요시하는 신입 개발자 김정찬입니다.</p>
<ul>
<li>&quot;Give me the ticket&quot; 프로젝트에서 AWS CloudFront를 도입하여 이미지 요청 속도를 70% 감소시키는 성과를 달성했습니다.</li>
<li>NestJS를 활용하여 OOP 원칙을 적용하고, 의존성 주입으로 코드의 재사용성과 테스트 용이성을 높였습니다.</li>
<li>MySQL과 TypeORM을 사용해 엔티티와 컬럼을 정의하여 데이터베이스를 구현하고, 효율적인 데이터 관리와 성능 최적화를 경험했습니다.</li>
</ul>
</li>
</ul>
<br>

<hr>
<h2 id="📌-tomorrows-goal">📌 Tomorrow&#39;s Goal</h2>
<h3 id="✏️-면접-특강-시청하기">✏️ 면접 특강 시청하기</h3>
<ul>
<li><p>이력서 Pass를 빨리 받았기 때문에 면접 특강을 미리 시청할 예정</p>
</li>
<li><p>면접 특강 시청 전에 Pass와 동시에 피드백이 있었기 때문에 약간 수정할 예정</p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-todays-goal-i-done">📌 Today&#39;s Goal I Done</h2>
<h3 id="✔️-자기소개-수정하기">✔️ 자기소개 수정하기</h3>
<ul>
<li><p>이력서 Pass를 받긴 했지만 추가적인 피드백이 있었음</p>
</li>
<li><p>그래서 추가적인 피드백을 반영하고 다시 이력서 코칭을 신청함</p>
</li>
<li><p>이력서 코칭이 완료되기 전에 면접 특강을 미리 지급 받음</p>
</li>
<li><p>그래서 면접 특강을 바탕으로 내용을 정리할 예정</p>
</li>
</ul>
<br>]]></description>
        </item>
        <item>
            <title><![CDATA[[2024.09.03 TIL] 코드카타 (같은 숫자는 싫어, 예산)]]></title>
            <link>https://velog.io/@my_code/2024.09.03-TIL-%EC%BD%94%EB%93%9C%EC%B9%B4%ED%83%80-%EA%B0%99%EC%9D%80-%EC%88%AB%EC%9E%90%EB%8A%94-%EC%8B%AB%EC%96%B4-%EC%98%88%EC%82%B0</link>
            <guid>https://velog.io/@my_code/2024.09.03-TIL-%EC%BD%94%EB%93%9C%EC%B9%B4%ED%83%80-%EA%B0%99%EC%9D%80-%EC%88%AB%EC%9E%90%EB%8A%94-%EC%8B%AB%EC%96%B4-%EC%98%88%EC%82%B0</guid>
            <pubDate>Tue, 03 Sep 2024 09:01:40 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>본 내용은 하루 활동한 내용을 기록하기 위한 게시물입니다.</p>
</blockquote>
<hr>
<h1 id="💻-tiltoday-i-learned">💻 TIL(Today I Learned)</h1>
<h2 id="📌-today-i-done">📌 Today I Done</h2>
<h3 id="✏️-같은-숫자는-싫어">✏️ 같은 숫자는 싫어</h3>
<h4 id="📝-문제">📝 문제</h4>
<p>배열 arr가 주어집니다. 배열 arr의 각 원소는 숫자 0부터 9까지로 이루어져 있습니다. 이때, 배열 arr에서 연속적으로 나타나는 숫자는 하나만 남기고 전부 제거하려고 합니다. 단, 제거된 후 남은 수들을 반환할 때는 배열 arr의 원소들의 순서를 유지해야 합니다. 예를 들면,</p>
<ul>
<li>arr = [1, 1, 3, 3, 0, 1, 1] 이면 [1, 3, 0, 1] 을 return 합니다.</li>
<li>arr = [4, 4, 4, 3, 3] 이면 [4, 3] 을 return 합니다.</li>
</ul>
<p>배열 arr에서 연속적으로 나타나는 숫자는 제거하고 남은 수들을 return 하는 solution 함수를 완성해 주세요.</p>
<h4 id="⌛️-제한사항">⌛️ 제한사항</h4>
<p>배열 arr의 크기 : 1,000,000 이하의 자연수
배열 arr의 원소의 크기 : 0보다 크거나 같고 9보다 작거나 같은 정수</p>
<h4 id="📥-입출력-예시">📥 입출력 예시</h4>
<p><img src="https://velog.velcdn.com/images/my_code/post/61aa4d5d-c483-4405-a777-4d33b8f1b9f4/image.png" alt=""></p>
<br>

<h4 id="⭕️-제출-코드">⭕️ 제출 코드</h4>
<ul>
<li><p>반복문을 통해서 다음 인덱스의 요소와 비교 후 같지 않으면 result에 넣음</p>
</li>
<li><p>하지만 마지막 요소까지 반복문이 돌지 않기 때문에 마지막 요소는 따로 추가</p>
<pre><code class="language-python"></code></pre>
</li>
</ul>
<p>def solution(arr):
    result = []
    for i in range(len(arr)-1):
        if (arr[i] != arr[i+1]):
            result.append(arr[i])
    result.append(arr[-1])</p>
<pre><code>return result</code></pre><pre><code>
&lt;br&gt;

#### 🔔 다른 사람 풀이
- 아무래도 스택이나 큐를 사용하는 방식이 조금 더 적합해 보임
```python

def no_continuous(s):
    # 함수를 완성하세요
    result = []
    for c in s:
        if len(result) == 0 or result[-1] != c:
            result.append(c)

    return result
</code></pre><br>

<h3 id="✏️-예산">✏️ 예산</h3>
<h4 id="📝-문제-1">📝 문제</h4>
<p>S사에서는 각 부서에 필요한 물품을 지원해 주기 위해 부서별로 물품을 구매하는데 필요한 금액을 조사했습니다. 그러나, 전체 예산이 정해져 있기 때문에 모든 부서의 물품을 구매해 줄 수는 없습니다. 그래서 최대한 많은 부서의 물품을 구매해 줄 수 있도록 하려고 합니다.</p>
<p>물품을 구매해 줄 때는 각 부서가 신청한 금액만큼을 모두 지원해 줘야 합니다. 예를 들어 1,000원을 신청한 부서에는 정확히 1,000원을 지원해야 하며, 1,000원보다 적은 금액을 지원해 줄 수는 없습니다.</p>
<p>부서별로 신청한 금액이 들어있는 배열 d와 예산 budget이 매개변수로 주어질 때, 최대 몇 개의 부서에 물품을 지원할 수 있는지 return 하도록 solution 함수를 완성해주세요.</p>
<h4 id="⌛️-제한사항-1">⌛️ 제한사항</h4>
<ul>
<li>d는 부서별로 신청한 금액이 들어있는 배열이며, 길이(전체 부서의 개수)는 1 이상 100 이하입니다.</li>
<li>d의 각 원소는 부서별로 신청한 금액을 나타내며, 부서별 신청 금액은 1 이상 100,000 이하의 자연수입니다.</li>
<li>budget은 예산을 나타내며, 1 이상 10,000,000 이하의 자연수입니다.</li>
</ul>
<h4 id="📥-입출력-예시-1">📥 입출력 예시</h4>
<p><img src="https://velog.velcdn.com/images/my_code/post/d334e504-a7a2-491e-955b-7423fa09bb38/image.png" alt=""></p>
<h4 id="📥-입출력-예시-설명">📥 입출력 예시 설명</h4>
<p><strong>입출력 예 #1</strong>
각 부서에서 [1원, 3원, 2원, 5원, 4원]만큼의 금액을 신청했습니다. 만약에, 1원, 2원, 4원을 신청한 부서의 물품을 구매해주면 예산 9원에서 7원이 소비되어 2원이 남습니다. 항상 정확히 신청한 금액만큼 지원해 줘야 하므로 남은 2원으로 나머지 부서를 지원해 주지 않습니다. 위 방법 외에 3개 부서를 지원해 줄 방법들은 다음과 같습니다.</p>
<ul>
<li>1원, 2원, 3원을 신청한 부서의 물품을 구매해주려면 6원이 필요합니다.</li>
<li>1원, 2원, 5원을 신청한 부서의 물품을 구매해주려면 8원이 필요합니다.</li>
<li>1원, 3원, 4원을 신청한 부서의 물품을 구매해주려면 8원이 필요합니다.</li>
<li>1원, 3원, 5원을 신청한 부서의 물품을 구매해주려면 9원이 필요합니다.</li>
<li>3개 부서보다 더 많은 부서의 물품을 구매해 줄 수는 없으므로 최대 3개 부서의 물품을 구매해 줄 수 있습니다.</li>
</ul>
<p><strong>입출력 예 #2</strong>
모든 부서의 물품을 구매해주면 10원이 됩니다. 따라서 최대 4개 부서의 물품을 구매해 줄 수 있습니다.</p>
<br>

<h4 id="⭕️-제출-코드-1">⭕️ 제출 코드</h4>
<ul>
<li><p>최대한 많은 부서에 예산을 주는 것이 목적</p>
</li>
<li><p>그래서 예상 예산이 적은 부서부터 차례대로 총 예산에서 빼줌</p>
<pre><code class="language-python"></code></pre>
</li>
</ul>
<p>def solution(d, budget):
    count = 0;
    d.sort()
    for i in d:
        if i &lt;= budget:
            count += 1
            budget -= i
    return count</p>
<p>```</p>
<br>

<hr>
<h2 id="📌-tomorrows-goal">📌 Tomorrow&#39;s Goal</h2>
<h3 id="✏️-이력서-피드백-반영">✏️ 이력서 피드백 반영</h3>
<ul>
<li><p>제출한 이력서에 대한 피드백을 받을 예정</p>
</li>
<li><p>길면 이틀 정도 소요된다고 해서 내일이면 받을 것 같음</p>
</li>
<li><p>피드백 받은 내용을 기반으로 이력서를 수정할 예정</p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-todays-goal-i-done">📌 Today&#39;s Goal I Done</h2>
<h3 id="✔️-코드카타-문제-풀기">✔️ 코드카타 문제 풀기</h3>
<ul>
<li><p>코딩테스트를 파이썬으로 진행할 예정이기 때문에 예전에 공부했던 파이썬 문법들을 다시 공부함</p>
</li>
<li><p>그러고 몸풀기 문제로 프로그래머스 레벨1 문제들을 풀어봄</p>
</li>
<li><p>확실히 알고리즘 문제는 풀지 않으면 머리가 굳는 것 같음</p>
</li>
<li><p>레벨1은 정말 기초 문제인데도 바로바로 풀리지 않았음</p>
</li>
<li><p>일단 파이썬 기초 100제 문제들을 통해서 파이썬 문법들을 다시 공부할 필요가 있어 보임</p>
</li>
</ul>
<br>]]></description>
        </item>
        <item>
            <title><![CDATA[[2024.08.30 TIL] 이력서 작성, 자기소개 작성하기]]></title>
            <link>https://velog.io/@my_code/2024.08.30-TIL-%EC%9D%B4%EB%A0%A5%EC%84%9C-%EC%9E%91%EC%84%B1-%EC%9E%90%EA%B8%B0%EC%86%8C%EA%B0%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@my_code/2024.08.30-TIL-%EC%9D%B4%EB%A0%A5%EC%84%9C-%EC%9E%91%EC%84%B1-%EC%9E%90%EA%B8%B0%EC%86%8C%EA%B0%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 03 Sep 2024 08:30:24 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.</p>
</blockquote>
<hr>
<h1 id="💻-tiltoday-i-learned">💻 TIL(Today I Learned)</h1>
<h2 id="📌-today-i-done">📌 Today I Done</h2>
<h3 id="✏️-자기소개-작성하기">✏️ 자기소개 작성하기</h3>
<ul>
<li>자기 소개 문구 : 문제 해결에 대한 끈기와 팀워크를 중요시하는 신입 개발자 김정찬입니다.<ul>
<li>사례/근거 1 : 프로젝트 &quot;Give me the ticket&quot;에서 TypeScript와 Node.js를 활용하여 백엔드 시스템을 구축하며, 팀원들과의 원활한 소통을 통해 코드 컨벤션과 협업 룰을 설정하였습니다. 이를 통해 효율적인 개발 환경을 조성하고, 성공적으로 사용자 인증 및 인가 처리 기능을 구현했습니다.</li>
<li>사례/근거 2 : 주 3~5회의 TIL 작성을 통해 끈기와 꾸준함을 발휘하며, 블로그를 운영하여 배운 내용을 공유하고 있습니다. 이를 통해 지속적인 학습과 성장에 힘쓰고 있으며, 도전적인 문제를 해결하는 데 집중하고 있습니다.</li>
</ul>
</li>
</ul>
<br>

<ul>
<li>자기 소개 문구 : 성능 개선과 효율성을 중요시하는 신입 개발자 김정찬입니다.<ul>
<li>사례/근거 1 : &quot;Give me the ticket&quot; 프로젝트에서 AWS CloudFront를 도입하여 이미지 요청 속도를 70% 감소시키는 성과를 달성했습니다. 이를 통해 사용자 경험을 크게 향상시키고, 서비스의 응답성을 개선했습니다.</li>
<li>사례/근거 2 : CD workflow의 멈춤 현상을 해결하여 정상적인 배포 과정을 확보했습니다. 기존 라이브러리를 사용하는 게 아니라 SSH 명령어를 직접 사용하여 문제를 해결함으로써, 배포 지연을 방지하고 서비스 가용성을 높이는 데 기여했습니다.</li>
</ul>
</li>
</ul>
<br>

<h3 id="✏️-자기소개-피드백">✏️ 자기소개 피드백</h3>
<blockquote>
<p>node 기반 백엔드 개발자를 준비하시므로 자기소개에서 아래를 추가로 어필해주면 좋습니다.</p>
<ol>
<li>nest js 기반으로 OOP에 대한 이해도가 있음</li>
<li>SQL 기반 DB를 다룰 수 있음 (MySQL or Postgresql)</li>
<li>클라우드 인프라 리소스를 다루어본 경험이있음 (AWS EC2, CloudFront)</li>
</ol>
<p>블로그 운영이나 팀원들과 협업 사항보다는 이런 기술적인 경험들을 자기소개 부터 집중해서 어필하면 좋을 것 같네요</p>
</blockquote>
<hr>
<h2 id="📌-tomorrows-goal">📌 Tomorrow&#39;s Goal</h2>
<h3 id="✏️-인텔리픽에-이력서-제출">✏️ 인텔리픽에 이력서 제출</h3>
<ul>
<li><p>작성했던 내용을 기반으로 이력서 양식을 채울 예정</p>
</li>
<li><p>인텔리픽에서 제공하는 이력서 양식이 있기 때문에 거기에 맞춰서 제출할 예정</p>
</li>
<li><p>이후에는 피드백을 기반으로 수정할 예정</p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-todays-goal-i-done">📌 Today&#39;s Goal I Done</h2>
<h3 id="✔️-자기소개-작성하기">✔️ 자기소개 작성하기</h3>
<ul>
<li><p>오늘은 이력서에 넣을 짧은 자기 소개 문구를 작성함</p>
</li>
<li><p>기술 외적인 부분과 기술적인 부분에 대한 소개 문구를 각각 작성함</p>
</li>
<li><p>일단 끈기와 팀워크에 대한 내용부터 작성해서 제출할 예정</p>
</li>
<li><p>기술적인 부분으로 조금 더 어필을 하고 싶었지만 생각보다 기술적인 강점을 어떻게 어필할지 가장 어려웠음</p>
</li>
</ul>
<br>]]></description>
        </item>
        <item>
            <title><![CDATA[[2024.08.29 TIL] 내일배움캠프 95일차 (이력서 작성, 프로젝트 파트 완성하기)]]></title>
            <link>https://velog.io/@my_code/2024.08.29-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-95%EC%9D%BC%EC%B0%A8-%EC%9D%B4%EB%A0%A5%EC%84%9C-%EC%9E%91%EC%84%B1-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%8C%8C%ED%8A%B8-%EC%99%84%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@my_code/2024.08.29-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-95%EC%9D%BC%EC%B0%A8-%EC%9D%B4%EB%A0%A5%EC%84%9C-%EC%9E%91%EC%84%B1-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%8C%8C%ED%8A%B8-%EC%99%84%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 29 Aug 2024 01:22:08 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.</p>
</blockquote>
<hr>
<h1 id="💻-tiltoday-i-learned">💻 TIL(Today I Learned)</h1>
<h2 id="📌-today-i-done">📌 Today I Done</h2>
<h3 id="✏️-1-프로젝트-파트-완성하기">✏️ 1. 프로젝트 파트 완성하기</h3>
<ul>
<li><p>길게 쓰여진 문장 줄이기 (구문 다이어트)
<img src="https://velog.velcdn.com/images/my_code/post/bcb2dffb-d5dc-4efd-af36-2e4f61cdc735/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/6a066c95-6afd-45f3-a2d8-26b51a9527e2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/09fe62ac-e7cd-40c1-bd11-2f6336c88bb7/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/ec174a0e-5b2a-4d58-8972-8ff3d9f28add/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/b1ca312a-74b1-47f3-845e-5ec4a730c1c9/image.png" alt=""></p>
</li>
<li><p>내가 쓴 문장에 핵심 키워드가 있는지 점점</p>
</li>
<li><p>두괄식으로 제목을 정리
<img src="https://velog.velcdn.com/images/my_code/post/c607e1bc-5b66-4044-8adb-295d02b25b21/image.png" alt=""></p>
</li>
</ul>
<br>

<hr>
<h3 id="✏️-2-give-me-the-ticket-최종-프로젝트">✏️ 2. Give me the ticket (최종 프로젝트)</h3>
<ul>
<li><strong>프로젝트 소개</strong> : 공연 예매와 예매한 티켓의 중고 거래를 지원하는 서비스</li>
<li><strong>사용 스킬</strong><ul>
<li>Back-End : TypeScript, Node.js, Nest.js</li>
<li>Front-End : HTML, CSS, JavaScript, EJS</li>
<li>Database : MySQL, Redis, AWS S3</li>
<li>DevOps / Infra : GitHub Actions, AWS EC2, AWS ELB, AWS Route 53, AWS CloudFront</li>
</ul>
</li>
<li><strong>담당 역할</strong><ul>
<li>로컬 회원가입 및 로그인<ul>
<li>공연 예매 기능 때문에 해당 서비스에서 사용자를 구분해서 관리가 필요</li>
<li>AWS RDS를 이용한 MySQL를 통해 사용자가 입력한 정보를 저장</li>
<li><a href="https://velog.io/@my_code/2024.07.25-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-71%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83%ED%86%A0%ED%81%B0%EC%9E%AC%EB%B0%9C%EA%B8%89-%EA%B5%AC%ED%98%84#%EF%B8%8F-refreshtoken-%EA%B2%80%EC%A6%9D%EC%9D%84-%EA%B8%B0%EC%A1%B4%EC%9D%98-passportstrategy-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0">인증된 사용자인지 검증</a>하기 위해 토큰 별 Strategy를 구현</li>
</ul>
</li>
<li><a href="https://velog.io/@my_code/2024.07.30-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-74%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-API-%EA%B5%AC%ED%98%84#%EF%B8%8F-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-api-%EA%B5%AC%ED%98%84">카카오 소셜 로그인</a><ul>
<li>사용자에게 편리하고 안심되는 인증 방법 선택지를 주기 위해서 구현</li>
<li>개인정보를 카카오와 같은 안전한 큰 기업에 맡기고, 해당 서비스에서는 토큰만 발급해주는 방법을 사용</li>
<li>사용자가 카카오 계정을 통한 로그인이 가능함</li>
</ul>
</li>
<li><a href="https://velog.io/@my_code/2024.07.26-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-72%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%97%AD%ED%95%A0-%EA%B0%80%EB%93%9C-%EA%B5%AC%ED%98%84-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-API%EA%B5%AC%ED%98%84#%EF%B8%8F-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-api-%EA%B5%AC%ED%98%84">이미지 업로드</a><ul>
<li>해당 서비스에 사용자 프로필 이미지와 공연 이미지가 필요</li>
<li>데이터베이스에서 이미지 데이터를 다루기에는 무거움</li>
<li>AWS S3 서비스를 통해 이미지 데이터를 저장</li>
<li>S3로부터 받은 이미지 URL를 클라이언트에게 반환</li>
<li>데이터베이스 부하를 줄이며, 안정적으로 대량의 이미지를 효율적으로 처리 가능</li>
</ul>
</li>
</ul>
</li>
<li><strong>트러블 슈팅</strong><ul>
<li><a href="https://velog.io/@my_code/2024.08.05-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-78%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B3%B5%EC%97%B0-%EC%88%98%EC%A0%95-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B5%AC%ED%98%84-CICD-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0#%EF%B8%8F-cd-workflow-%EB%A9%88%EC%B6%A4-%ED%98%84%EC%83%81-%EB%A6%AC%EC%86%8C%EC%8A%A4-100-%EC%82%AC%EC%9A%A9">CD workflow의 멈춤 현상</a>을 해결하여 정상적인 배포 과정 확보<ul>
<li>CD workflow 멈춤 현상으로 인해 배포 지연과 서비스 가용성이 떨어졌음</li>
<li>EC2 SSH 접속을 도와주는 <code>appleboy/ssh-action</code>을 다운로드 하는 과정에서 타임아웃이 발생</li>
<li>그 과정에서 CPU 점유율이 100%로 치솟는 현상이 발생</li>
<li>해당 action을 사용하지 않고 직접 SSH 명령어를 통해서 명령을 실행하는 방법을 사용</li>
<li>멈춤 현상을 해결하고 정상적인 배포 과정이 이뤄짐</li>
</ul>
</li>
<li>URL 노출 방지 및 토큰 전달 개선을 통한 <a href="https://velog.io/@my_code/2024.08.14-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-85%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A0%88%EB%94%94%EC%8A%A4%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B3%A0%EB%8F%84%ED%99%94#%EF%B8%8F-%EB%A0%88%EB%94%94%EC%8A%A4%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B3%A0%EB%8F%84%ED%99%94">소셜 로그인 보안 강화</a><ul>
<li>카카오 로그인 백엔드 API를 호출하면 생성한 토큰을 URL에 넣어서 프론트엔드로 전달했음</li>
<li>이 방식은 토큰이 그대로 들어나기에 보안적인 취약함</li>
<li>별도의 랜덤 코드를 만들어서 해당 코드를 키로 사용하는 사용자 ID를 레디스에 저장함</li>
<li>토큰을 직접 노출된 형태가 아니라 Body에 담아서 전달 → 보안 측면에서 다소 향상</li>
</ul>
</li>
<li>CDN 적용을 통한 이미지 <a href="https://velog.io/@my_code/2024.08.15-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-86%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-CDN-%EC%86%8D%EB%8F%84-%EC%B0%A8%EC%9D%B4-%ED%85%8C%EC%8A%A4%ED%8A%B8#%EF%B8%8F-cdn-%EC%86%8D%EB%8F%84-%EC%B0%A8%EC%9D%B4-%ED%85%8C%EC%8A%A4%ED%8A%B8">요청 속도 70% 감소</a><ul>
<li>공연 이미지의 용량이 크면 매번 요청할 때마다 시간이 소요</li>
<li>AWS CloudFront를 사용해 이미지 데이터를 캐싱할 수 있는 여러 엣지 서버 적용</li>
<li>이미지 요청에 대한 응답 속도를 서울 리전 기준으로 70% 감소시킬 수 있었음</li>
</ul>
</li>
</ul>
</li>
</ul>
<br>

<hr>
<h2 id="📌-tomorrows-goal">📌 Tomorrow&#39;s Goal</h2>
<h3 id="✏️-자기소개-작성하기">✏️ 자기소개 작성하기</h3>
<ul>
<li><p>내일은 자기소개서를 작성할 예정</p>
</li>
<li><p>미리 내용을 간단히 봤지만, 기존에 작성했던 내용은 사용하지 못할 것 같음</p>
</li>
<li><p>그렇기에 이번 캠프를 통해 얻은 경험을 바탕으로 완전히 새롭게 작성할 예정</p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-todays-goal-i-done">📌 Today&#39;s Goal I Done</h2>
<h3 id="✔️-프로젝트-파트-완성하기">✔️ 프로젝트 파트 완성하기</h3>
<ul>
<li><p>오늘 프로젝트에 대한 나의 역할과 트러블 슈팅을 작성함</p>
</li>
<li><p>작성하고 보니 한 줄씩은 간결해 보이지만 개조식으로 이번에는 밑으로 내용이 길어짐</p>
</li>
<li><p>내용이 길더라도 일단은 작성하고 이력서 인텔리픽에서 점검받을 때 따로 피드백을 받도록 하자!</p>
</li>
</ul>
<br>]]></description>
        </item>
        <item>
            <title><![CDATA[[2024.08.28 TIL] 내일배움캠프 94일차 (이력서 작성, 이력서 기초 다지기)]]></title>
            <link>https://velog.io/@my_code/2024.08.28-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-94%EC%9D%BC%EC%B0%A8-%EC%9D%B4%EB%A0%A5%EC%84%9C-%EC%9E%91%EC%84%B1-%EC%9D%B4%EB%A0%A5%EC%84%9C-%EA%B8%B0%EC%B4%88-%EB%8B%A4%EC%A7%80%EA%B8%B0</link>
            <guid>https://velog.io/@my_code/2024.08.28-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-94%EC%9D%BC%EC%B0%A8-%EC%9D%B4%EB%A0%A5%EC%84%9C-%EC%9E%91%EC%84%B1-%EC%9D%B4%EB%A0%A5%EC%84%9C-%EA%B8%B0%EC%B4%88-%EB%8B%A4%EC%A7%80%EA%B8%B0</guid>
            <pubDate>Thu, 29 Aug 2024 01:08:21 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.</p>
</blockquote>
<hr>
<h1 id="💻-tiltoday-i-learned">💻 TIL(Today I Learned)</h1>
<h2 id="📌-today-i-done">📌 Today I Done</h2>
<h3 id="✏️-1-이력서-기초-다지기">✏️ 1. 이력서 기초 다지기</h3>
<ul>
<li>Step 01<ul>
<li>재료 준비하기 (= 나만의 개발 경험 수집)<ul>
<li>5분 기록 보드</li>
<li>회고 노트 등 활용</li>
</ul>
</li>
</ul>
</li>
<li>Step 02<ul>
<li>재료 분류하기 (= 개발 경험 분류하기)<ul>
<li>간단한 기능 구현</li>
<li>트러블 슈팅으로 구분
<img src="https://velog.velcdn.com/images/my_code/post/d0189781-4a8e-46e2-8a5a-6c17371df916/image.png" alt=""></li>
</ul>
</li>
</ul>
</li>
<li>Step 03<ul>
<li>재료 다듬기 (= <code>골든 서클</code> 적용하기)<ul>
<li>Why, How, What
<img src="https://velog.velcdn.com/images/my_code/post/3b00bbc1-6956-44c3-b655-f8d2f3db3615/image.png" alt=""></li>
</ul>
</li>
</ul>
</li>
</ul>
<br>

<hr>
<h3 id="✏️-2-give-me-the-ticket-최종-프로젝트">✏️ 2. Give me the ticket (최종 프로젝트)</h3>
<ul>
<li><p>간단한 구현이나, 기능을 수정했던 경험이 있다면 적어주세요.</p>
<ul>
<li><p>로컬 회원가입 및 로그인</p>
<ul>
<li>Why : 해당 서비스에서 사용자를 구분해서 관리가 필요했기 때문에</li>
<li>How : AWS RDS를 이용한 MySQL를 통해 사용자가 입력한 정보를 저장하고 인증된 사용자인지 검증을 통해</li>
<li>What : 사용자가 로컬로 회원가입 및 로그인이 가능하게 구현함</li>
<li><a href="https://velog.io/@my_code/2024.07.25-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-71%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83%ED%86%A0%ED%81%B0%EC%9E%AC%EB%B0%9C%EA%B8%89-%EA%B5%AC%ED%98%84">https://velog.io/@my_code/2024.07.25-TIL-내일배움캠프-71일차-최종-팀프로젝트-로그인회원가입로그아웃토큰재발급-구현</a></li>
</ul>
</li>
<li><p>카카오 소셜 로그인</p>
<ul>
<li>Why : 사용자에게 편리하고 안심되는 인증 방법 선택지를 주기 위해서</li>
<li>How : 개인정보를 카카오와 같은 안전한 큰 기업에 맡기고, 해당 서비스에서는 토큰만 발급해주는 방법을 통해</li>
<li>What : 사용자가 카카오 계정을 통한 로그인이 가능하게 구현함</li>
<li><a href="https://velog.io/@my_code/2024.07.30-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-74%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-API-%EA%B5%AC%ED%98%84">https://velog.io/@my_code/2024.07.30-TIL-내일배움캠프-74일차-최종-팀프로젝트-카카오-소셜-로그인-API-구현</a></li>
</ul>
</li>
<li><p>이미지 업로드</p>
<ul>
<li>Why : 해당 서비스에 사용자 프로필 이미지와 공연 이미지가 필요하고 데이터베이스에서 이미지 데이터를 다루기에는 무겁기 때문에</li>
<li>How : AWS S3 서비스를 통해 이미지 데이터를 저장하고, S3로부터 받은 이미지 URL를 클라이언트에게 반환해줌으로써</li>
<li>What : 데이터베이스 부하를 줄이며, 안정적으로 대량의 이미지를 효율적으로 처리할 수 있음</li>
<li><a href="https://velog.io/@my_code/2024.07.26-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-72%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%97%AD%ED%95%A0-%EA%B0%80%EB%93%9C-%EA%B5%AC%ED%98%84-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-API%EA%B5%AC%ED%98%84#%EF%B8%8F-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-api-%EA%B5%AC%ED%98%84">https://velog.io/@my_code/2024.07.26-TIL-내일배움캠프-72일차-최종-팀프로젝트-역할-가드-구현-이미지-업로드-API구현#️-이미지-업로드-api-구현</a></li>
</ul>
</li>
</ul>
</li>
<li><p>트러블슈팅이나, 어떤 기능 또는 성능을 개선했던 경험이 있다면 적어주세요.</p>
<ul>
<li><p>CD workflow 멈춤 현상</p>
<ul>
<li>Why_1 : CD workflow 멈춤 현상으로 인해 배포 지연과 서비스 가용성이 떨어져 이는 사용자에게도 불편함을 제공하기 때문에</li>
<li>Why_2 : EC2 SSH 접속을 도와주는 <code>appleboy/ssh-action</code>을 다운로드 하는 과정에서 타임아웃이 발생하고 그 과정에서 CPU 점유율이 100%로 치솟는 현상이 발생하는 게 멈춤 현상의 원인이기 때문에</li>
<li>How : 해당 action을 사용하지 않고 직접 SSH 명령어를 통해서 명령을 실행하는 방법을 통해</li>
<li>What : 멈춤 현상을 해결하고 정상적인 배포 과정이 이루어지도록 함</li>
<li><a href="https://velog.io/@my_code/2024.08.05-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-78%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B3%B5%EC%97%B0-%EC%88%98%EC%A0%95-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B5%AC%ED%98%84-CICD-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0#%EF%B8%8F-cd-workflow-%EB%A9%88%EC%B6%A4-%ED%98%84%EC%83%81-%EB%A6%AC%EC%86%8C%EC%8A%A4-100-%EC%82%AC%EC%9A%A9">https://velog.io/@my_code/2024.08.05-TIL-내일배움캠프-78일차-최종-팀프로젝트-공연-수정-프론트엔드-구현-CICD-에러-해결#️-cd-workflow-멈춤-현상-리소스-100-사용</a></li>
<li><a href="https://velog.io/@my_code/2024.08.06-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-79%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B9%8C%EB%93%9C-%ED%8C%8C%EC%9D%BC%EB%A7%8C-EC2-%EC%84%9C%EB%B2%84%EB%A1%9C-%EC%98%AE%EA%B2%A8%EC%84%9C-%EC%8B%A4%ED%96%89">https://velog.io/@my_code/2024.08.06-TIL-내일배움캠프-79일차-최종-팀프로젝트-빌드-파일만-EC2-서버로-옮겨서-실행</a></li>
</ul>
</li>
<li><p>소셜 로그인 보안 강화</p>
<ul>
<li>Why : 기존에는 카카오 로그인 백엔드 API를 호출하면 생성한 토큰을 URL에 넣어서 프론트엔드로 전달했는데, 이러한 방식은 토큰이 그대로 들어나기에 보안적인 취약점을 보완하기 위해</li>
<li>How : 별도의 랜덤 코드를 만들어서 해당 코드를 키로 사용하는 사용자 ID를 레디스에 저장하는 방법을 통해</li>
<li>What : 클라이언트가 필요로 하는 토큰을 직접 노출된 형태가 아니라 Body에 담아서 전달하기 때문에 보안 측면에서 다소 향상되었음</li>
<li><a href="https://velog.io/@my_code/2024.08.14-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-85%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A0%88%EB%94%94%EC%8A%A4%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B3%A0%EB%8F%84%ED%99%94">https://velog.io/@my_code/2024.08.14-TIL-내일배움캠프-85일차-최종-팀프로젝트-레디스를-이용한-소셜-로그인-고도화</a></li>
</ul>
</li>
<li><p>CDN 적용</p>
<ul>
<li>Why : 공연에 대한 이미지를 사용할 때, 이미지의 용량이 크면 매번 요청할 때마다 시간이 소요되기 때문에</li>
<li>How : AWS CloudFront를 사용해 이미지 데이터를 캐싱할 수 있는 여러 엣지 서버를 둠으로써</li>
<li>What : 이미지 요청에 대한 응답 속도를 서울 리전 기준으로 70% 감소시킬 수 있었음</li>
<li><a href="https://velog.io/@my_code/2024.08.15-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-86%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-CDN-%EC%86%8D%EB%8F%84-%EC%B0%A8%EC%9D%B4-%ED%85%8C%EC%8A%A4%ED%8A%B8">https://velog.io/@my_code/2024.08.15-TIL-내일배움캠프-86일차-최종-팀프로젝트-CDN-속도-차이-테스트</a></li>
</ul>
</li>
</ul>
</li>
</ul>
<br>

<hr>
<h2 id="📌-tomorrows-goal">📌 Tomorrow&#39;s Goal</h2>
<h3 id="✏️-프로젝트-파트-완성하기">✏️ 프로젝트 파트 완성하기</h3>
<ul>
<li><p>내일은 오늘 작성한 내용을 기반으로 프로젝트에 대한 설명을 작성하는 파트를 작성할 예정</p>
</li>
<li><p>이력서 기초부분에서 재료가 워낙 길기 때문에 줄일 필요가 있음</p>
</li>
<li><p>개조식으로 작성하되, 너무 긴 문장을 차라리 나눠서 설명하는 게 도움이 될 것 같음</p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-todays-goal-i-done">📌 Today&#39;s Goal I Done</h2>
<h3 id="✔️-이력서-기초-다지기">✔️ 이력서 기초 다지기</h3>
<ul>
<li><p>오늘은 최종 프로젝트에 대한 기능 구현, 트러블 슈팅 경험에 대해서 작성함</p>
</li>
<li><p>5분 기록에 많이 남겼다고 생각했지만 막상 재료를 골라볼려고 하니 쓸만한 게 없었음</p>
</li>
<li><p>그래서 일단 각각 3가지 정도씩만 골랐음</p>
</li>
<li><p>일단 커리어 매니저님께서도 양은 신경쓰지 말고 작성하라고 하셨기 때문에 최대한 간결하게 작성하려고 노력함</p>
</li>
<li><p>하지만 프로젝트에 대한 설명을 작성할 때는 조금 더 간결하게 작성할 필요가 있어 보임</p>
</li>
</ul>
<br>]]></description>
        </item>
        <item>
            <title><![CDATA[[2024.08.26 TIL] 내일배움캠프 92일차 (최종 팀프로젝트, 발표 예상 질문)]]></title>
            <link>https://velog.io/@my_code/2024.08.26-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-92%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%9C%ED%91%9C-%EC%98%88%EC%83%81-%EC%A7%88%EB%AC%B8</link>
            <guid>https://velog.io/@my_code/2024.08.26-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-92%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%9C%ED%91%9C-%EC%98%88%EC%83%81-%EC%A7%88%EB%AC%B8</guid>
            <pubDate>Mon, 26 Aug 2024 12:44:02 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.</p>
</blockquote>
<hr>
<h1 id="💻-tiltoday-i-learned">💻 TIL(Today I Learned)</h1>
<h2 id="📌-today-i-done">📌 Today I Done</h2>
<h3 id="✏️-브로셔-서비스-핵심-기능-마무리">✏️ 브로셔 서비스 핵심 기능 마무리</h3>
<ul>
<li><p><strong>CDN과 캐시 서버의 차이</strong></p>
<ul>
<li><p><strong>CDN</strong></p>
<ul>
<li>전 세계에 분산된 서버를 통해 콘텐츠를 사용자에게 빠르게 전달하는 데 중점을 둠</li>
<li>여러 지역에 위치한 엣지 서버로 구성되어 있음</li>
<li>사용자와 가장 가까운 서버에서 콘텐츠를 제공</li>
<li>EX) 웹사이트의 이미지</li>
</ul>
</li>
<li><p><strong>캐시 서버</strong></p>
<ul>
<li>특정 서버 또는 네트워크 내에서 데이터를 저장</li>
<li>특정 애플리케이션이나 데이터베이스와 연결되어, 해당 시스템 내에서 데이터를 저장하고 관리함</li>
<li>EX) 자주 요청되는 데이터나 결과를 저장</li>
</ul>
</li>
</ul>
</li>
</ul>
<br>

<ul>
<li><p><strong>추후 도전 계획에서 Elastic Beanstalk를 선택한 이유</strong></p>
<ul>
<li>지금은 EC2 인스턴스 하나를 통해서 배포하도록 CI/CD 설정이 되어 있음</li>
<li>물론, 확장된 인스턴스에 맞도록 CI/CD 설정을 바꿀 수 있지만 Auto Scaling까지 생각해야 하기 때문에 너무 복잡해짐</li>
<li>그래서 Elastic Beanstalk를 통해 배포함으로써 Auto Scaling에 의해 Scaling Out된 인스턴스까지 CI/CD 가능하게 만들고 싶음</li>
</ul>
</li>
<li><p><strong>Elastic Beanstalk, ECS, EKS의 차이</strong></p>
<ul>
<li><p><strong>Elastic Beanstalk (EB)</strong></p>
<ul>
<li>웹 애플리케이션 및 서비스의 배포와 관리 간소화</li>
<li>자동 인프라 프로비저닝, 로드 밸런싱, 스케일링 관리</li>
<li>사용자가 애플리케이션 코드에 집중할 수 있도록 도움</li>
<li>난이도 : 낮음<ul>
<li>사용자 친화적인 인터페이스로 복잡한 설정 없이 코드 업로드만으로 배포 가능</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Amazon ECS (Elastic Container Service)</strong></p>
<ul>
<li>컨테이너화된 애플리케이션을 관리하고 배포하는 서비스</li>
<li>자동 스케일링 및 로드 밸런싱 제공</li>
<li>AWS Fargate와 함께 서버리스 컨테이너 실행 가능</li>
<li>난이도 : 중간<ul>
<li>Docker 및 컨테이너 개념 이해 필요, 설정 및 관리에 대한 학습 곡선 존재</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Amazon EKS (Elastic Kubernetes Service)</strong></p>
<ul>
<li>Kubernetes 클러스터를 관리하고 운영하는 서비스</li>
<li>AWS에서 Kubernetes 클러스터를 쉽게 생성하고 운영할 수 있도록 지원</li>
<li>EKS는 ECS보다 Kubernetes 생태계의 다양한 도구와 통합할 수 있는 장점이 있음</li>
<li>난이도 : 높음<ul>
<li>Kubernetes의 복잡성 이해 필요, 클러스터 관리와 보안 등의 직접적 다루기</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<br>

<ul>
<li><p><strong>DB 락과 RedLock의 차이</strong></p>
<ul>
<li><p><strong>DB 락</strong></p>
<ul>
<li>대상 :  데이터베이스 내의 특정 데이터, 즉 테이블의 행(row)이나 전체 테이블, 또는 특정 페이지</li>
<li>데이터베이스 내에서의 동시성 제어를 위한 전통적인 방법</li>
<li>주로 단일 데이터베이스 인스턴스 내에서 사용</li>
</ul>
</li>
<li><p><strong>RedLock</strong></p>
<ul>
<li>대상 : Redis 서버에 저장된 특정 키(key)</li>
<li>분산 시스템에서 안전하게 락을 관리하기 위한 알고리즘</li>
<li>여러 서버에서의 자원 충돌을 방지하는 데 초점을 맞춤</li>
</ul>
</li>
</ul>
</li>
</ul>
<br>

<hr>
<h2 id="📌-tomorrows-goal">📌 Tomorrow&#39;s Goal</h2>
<h3 id="✏️-최종-발표회">✏️ 최종 발표회</h3>
<ul>
<li><p>내일은 드디어 최종 프로젝트를 마무리하는 발표날</p>
</li>
<li><p>내일 오전에 발표 피드백을 한 번 더 진행하고 최종 마무리할 예정</p>
</li>
<li><p>오후부터는 발표회와 부스 운영을 할 예정</p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-todays-goal-i-done">📌 Today&#39;s Goal I Done</h2>
<h3 id="✔️-발표-스크립트-작성">✔️ 발표 스크립트 작성</h3>
<ul>
<li><p>오늘은 발표 내용에 대한 피드백을 계속 진행함</p>
</li>
<li><p>특별히 다른 조의 분들과 모여서 발표 연습을 진행함</p>
</li>
<li><p>이 때, 헷갈리거나 잘 모르는 질문들을 오늘 TIL로 정리함</p>
</li>
<li><p>그리고 팀원들이 구현한 기능들에 대해서도 구체적인 코드까지는 아니더라도 어떻게 돌아가는지는 알아야 질문이 들어와도 대답할 수 있음</p>
</li>
</ul>
<br>]]></description>
        </item>
        <item>
            <title><![CDATA[[2024.08.22 TIL] 내일배움캠프 90일차 (최종 팀프로젝트, 브로셔 서비스 핵심 기능 작성)]]></title>
            <link>https://velog.io/@my_code/2024.08.22-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-90%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B8%8C%EB%A1%9C%EC%85%94-%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%95%B5%EC%8B%AC-%EA%B8%B0%EB%8A%A5-%EC%9E%91%EC%84%B1</link>
            <guid>https://velog.io/@my_code/2024.08.22-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-90%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B8%8C%EB%A1%9C%EC%85%94-%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%95%B5%EC%8B%AC-%EA%B8%B0%EB%8A%A5-%EC%9E%91%EC%84%B1</guid>
            <pubDate>Thu, 22 Aug 2024 14:03:10 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.</p>
</blockquote>
<hr>
<h1 id="💻-tiltoday-i-learned">💻 TIL(Today I Learned)</h1>
<h2 id="📌-today-i-done">📌 Today I Done</h2>
<h3 id="✏️-브로셔-서비스-핵심-기능-작성">✏️ 브로셔 서비스 핵심 기능 작성</h3>
<ul>
<li><p>내가 설정한 우리 서비스의 핵심 기능은 다음과 같음</p>
<ul>
<li>회원가입 및 로그인</li>
<li>공연 예매</li>
<li>마이페이지</li>
<li>중고 거래 등록</li>
<li>중고 거래 티켓 구매</li>
<li>포트원 결제 시스템</li>
<li>Elasticsearch 검색 기능</li>
<li>공연 필터링 기능</li>
<li>랭킹 기능 (인기 공연 조회)</li>
</ul>
</li>
<li><p>그래서 현재 공연 필터링과 랭킹 기능을 제외한 나머지는 작성을 완료함</p>
</li>
<li><p>블로그에 내용을 다 넣기에는 너무 많기 때문에 링크를 남김</p>
</li>
<li><p><a href="https://teamsparta.notion.site/Give-me-the-ticket-d6b13f43e5564718a2062bcbc01cd15a#5d2cfb04678a4c4798ddfe38d057ab07">https://teamsparta.notion.site/Give-me-the-ticket-d6b13f43e5564718a2062bcbc01cd15a#5d2cfb04678a4c4798ddfe38d057ab07</a></p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-tomorrows-goal">📌 Tomorrow&#39;s Goal</h2>
<h3 id="✏️-브로셔-서비스-핵심-기능-마무리">✏️ 브로셔 서비스 핵심 기능 마무리</h3>
<ul>
<li><p>내일은 브로셔 서비스 핵심 기능 작성을 마무리 하고 튜터님께 피드백을 받을 예정</p>
</li>
<li><p>일단 팀원분께서 받은 피드백은 썸네일을 gif 형식을 사용하라는 것이라서 수정함</p>
</li>
<li><p>지금 봤을 때는 생각보다 내용이 부실한 것 같지만 튜터님의 의견을 들어보고 수정할 예정</p>
</li>
<li><p>사실 어떤 내용을 더 넣어야 할지 감이 오지 않음</p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-todays-goal-i-done">📌 Today&#39;s Goal I Done</h2>
<h3 id="✔️-브로셔-서비스-핵심-기능-작성">✔️ 브로셔 서비스 핵심 기능 작성</h3>
<ul>
<li><p>오늘은 최종 프로젝트 브로셔에 서비스 핵심 기능 작성했음</p>
</li>
<li><p>어제 브로셔에 대한 파트를 분배해서 오늘 다 같이 작업에 들어감</p>
</li>
<li><p>서비스 핵심 기능을 테스트하면서 해당 기능의 과정을 캡쳐해서 브로셔 내용에 넣었음</p>
</li>
<li><p>그리고 그 사진에서의 내용이나 설명을 글로 2~3줄 남겼음</p>
</li>
<li><p>처음에는 간단하게 사진으로만 구성했는데, 튜터님께서 gif를 넣는 게 좋겠다고 하셔서 해당 기능의 과정을 gif로 녹화해서 썸네일에 넣었음</p>
</li>
<li><p>브로셔 자체가 느려지고 무겁지만, 우리 프로젝트를 모르는 사람이 봤을 때 굉장히 도움이 될 것 같음</p>
</li>
</ul>
<br>]]></description>
        </item>
        <item>
            <title><![CDATA[[2024.08.21 TIL] 내일배움캠프 89일차 (최종 팀프로젝트, Auto Scaling 테스트)]]></title>
            <link>https://velog.io/@my_code/2024.08.21-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-89%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Auto-Scaling-%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@my_code/2024.08.21-TIL-%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-89%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Auto-Scaling-%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Wed, 21 Aug 2024 14:38:33 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.</p>
</blockquote>
<hr>
<h1 id="💻-tiltoday-i-learned">💻 TIL(Today I Learned)</h1>
<h2 id="📌-today-i-done">📌 Today I Done</h2>
<h3 id="✏️-auto-scaling-테스트">✏️ Auto Scaling 테스트</h3>
<ul>
<li><p>지난번 스파이크 테스트는 짧은 시간 동안 많은 트래픽으로 서비스에 요청을 해서 서버가 버티는지 확인하는 테스트였음</p>
</li>
<li><p>이번에 진행하는 테스트는 인스턴스 1개로 시작해서 부하(트래픽)를 점점 늘렸을 때 Auto Scaling으로 생성된 인스턴스에 의해서 부하를 어떻게 수용하는지 확인하기 위한 테스트임</p>
</li>
<li><p>예상으로는 인스턴스 1개였을 때 부하가 너무 많으면 다음 인스턴스가 생성될 때까지 성능이 떨어지다가 인스턴스가 생성되면 성능이 다시 오르는 형태를 반복할 것 같음</p>
</li>
<li><p>그래서 기존에 대상 그룹으로 ALB에 연결된 4개의 인스턴스 중 1개를 제외하고 모두 대상 취소 시킴</p>
</li>
<li><p>인스턴스 하나만 연결됨</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/my_code/post/46954e86-1d2a-4a3d-bcbd-9e54b49accbf/image.png" alt=""></p>
<ul>
<li><p>그래서 트래픽이 많아질 경우 Auto Scaling을 통해서 인스턴스가 동적으로 생성되고 그로 인해 트래픽이 분산되어 요청 응답 시간이 줄어드는 것을 확인하는 테스트를 진행할 예정</p>
</li>
<li><p>1차 테스트</p>
<ul>
<li>조회 쓰레드 : 60000개</li>
<li>시간 : 600초</li>
<li>Timeout : X</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/my_code/post/21d53b79-6193-4d19-9d93-61a323e40694/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/b5e342d0-6d49-4cae-b199-7594f7d458cc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/10605f4c-4024-4142-bd77-c1e2f541474e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/43d9a514-f054-4a2b-9ab6-722da72ea468/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/0b2ac383-d4c0-472b-baaf-d32065b5c5c6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/e5370660-27ab-4f27-b7fb-a9b039967aa4/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/0dc8e2cd-b59d-4ee8-8484-ddee4e015932/image.png" alt=""></p>
<ul>
<li><p>다시 테스트하기 위해서 인스턴스를 1개만 남도록 만듦</p>
</li>
<li><p>2차 테스트</p>
<ul>
<li>조회 쓰레드 : 100000개</li>
<li>시간 : 600초</li>
<li>Timeout : X</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/my_code/post/b6950f66-28b7-47d1-b664-9650bf608717/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/78f52ea4-7fc5-4689-91e0-d1fd6c4ab3a1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/cf1e4271-45ef-45d1-9236-7765f0011cc1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/837d73b5-17e0-4af7-804f-db189964231b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/99f11071-c0db-442e-b022-d2e555cf18b9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/c40339ad-1370-4da9-ac8f-76ff74465fe7/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/85d3ddf8-fe99-4d80-87fd-c1b4943ca84c/image.png" alt=""></p>
<ul>
<li><p>2차 테스트에서도 인스턴스가 추가되었을 때는 정상적으로 응답 시간이 줄어들지만 인스턴스 4개로도 수용되지 않아 이후에는 응답 시간이 늘어나는 모습을 보여주고 있음</p>
</li>
<li><p>3차 테스트</p>
<ul>
<li>조회 쓰레드 : 80000개</li>
<li>시간 : 600초</li>
<li>Timeout : X</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/my_code/post/9ddcd8c7-8788-4958-852b-861d0292b1e9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/732bfc4b-bfc2-441d-9b99-8ead3b8463c1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/29e49ce4-0fb3-4d52-9176-bb39c520c408/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/e3778a94-6ab5-40d3-88b5-5f258aa6751a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/6cc23827-238a-4785-803a-c5669f387f4a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/a15488b1-633f-430f-873b-14d0f310e16f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/2dd24d3a-fb60-47bc-8edf-21510571c7d1/image.png" alt=""></p>
<ul>
<li><p>3차 테스트에서는 전체 쓰레드의 수를 2차보다 조금 줄였는데 인스턴스가 생성된 시간과 비교했을 때 시간이 줄어드는 모습을 찾기는 다소 어려워 보임</p>
</li>
<li><p>아직까진 1차, 2차 테스트가 응답 시간이 줄어드는 모습이 그나마 잘 보임</p>
</li>
<li><p>그리고 시간이 지나니 Auto Scaling에서 자동으로 인스턴스의 수를 최소로 맞춰서 인스턴스를 종료시킴</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/my_code/post/2a3a2c15-0c95-48e2-851e-15266448d8f7/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/my_code/post/ad9a5d3f-835b-4825-a6fd-1e557c3a0055/image.png" alt=""></p>
<ul>
<li>결과적으로 동적 크기 조정 정책을 통해서 요청 수가 많아지는 경우 자동으로 인스턴스를 생성해서 아래와 같이 응답 시간을 줄일 수 있음</li>
</ul>
<p><img src="https://velog.velcdn.com/images/my_code/post/daa395a9-2960-4305-ae7d-c2620e8da54c/image.png" alt=""></p>
<ul>
<li><p>이는 사용자에게  트래픽이 많을 때도 응답 시간을 줄여줌으로써 서비스 이용이 용이하게 할 수 있음</p>
</li>
<li><p>다만, 인스턴스가 생성되고 활성화 되기까지의 시간은 필요함</p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-tomorrows-goal">📌 Tomorrow&#39;s Goal</h2>
<h3 id="✏️-최종-프로젝트-브로셔-작성하기">✏️ 최종 프로젝트 브로셔 작성하기</h3>
<ul>
<li><p>내일은 최종 프로젝트에서 협력사에서 프로젝트 소개하기 위한 브로셔를 작성할 예정</p>
</li>
<li><p>내가 맡은 부분은 서비스 핵심 기능을 작성하는 역할임</p>
</li>
<li><p>일단 오늘은 어떤 기능들에 대해서 서술할지 기능들만 나열함</p>
</li>
<li><p>내일 실제 프론트엔드 모습의 사진과 설명을 통해서 작성할 예정</p>
</li>
</ul>
<br>

<hr>
<h2 id="📌-todays-goal-i-done">📌 Today&#39;s Goal I Done</h2>
<h3 id="✔️-auto-scaling-테스트">✔️ Auto Scaling 테스트</h3>
<ul>
<li><p>오늘은 Auto Scaling을 통해서 트래픽의 변화에 따른 인스턴스 확장 및 축소에 대해서 테스트함</p>
</li>
<li><p>Auto Scaling 그룹에서 동적 크기 조정 정책을 정하면 그 조건에 해당하는 경우 인스턴스가 확장됨</p>
</li>
<li><p>이번 테스트에서의 목적은 인스턴스가 확장되었을 때의 요청 시간의 감소가 이루어 졌는지를 보기 위한 테스트임</p>
</li>
<li><p>1차 테스트인 60000개의 요청에서 Auto Scaling에서 생성한 인스턴스가 생성되면서 응답 시간이 줄어 드는 것을 확인함</p>
</li>
<li><p>나머지 2, 3차 테스트에서도 인스턴스가 생성되면 응답 시간이 줄어들긴 하는데 중간 중간에 발생하는 에러로 응답 시간이 들쭉날쭉임</p>
</li>
<li><p>아마도 10만과 8만개의 트래픽은 인스턴스 4개로는 무리라서 저런 결과가 나온게 아닐까 함</p>
</li>
</ul>
<br>]]></description>
        </item>
    </channel>
</rss>