<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>joooii.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 03 Feb 2026 10:06:31 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>joooii.log</title>
            <url>https://velog.velcdn.com/images/jooo_ii/profile/fd5aa61a-d760-4ce3-9ce1-3a76d9988aec/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. joooii.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jooo_ii" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[회고] 유레카 부트캠프 프론트엔드 과정: 미니 프로젝트 핏로그 - 프론트엔드]]></title>
            <link>https://velog.io/@jooo_ii/%ED%9A%8C%EA%B3%A0-%EC%9C%A0%EB%A0%88%EC%B9%B4-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B3%BC%EC%A0%95-%EB%AF%B8%EB%8B%88-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%95%8F%EB%A1%9C%EA%B7%B8-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C</link>
            <guid>https://velog.io/@jooo_ii/%ED%9A%8C%EA%B3%A0-%EC%9C%A0%EB%A0%88%EC%B9%B4-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B3%BC%EC%A0%95-%EB%AF%B8%EB%8B%88-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%95%8F%EB%A1%9C%EA%B7%B8-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C</guid>
            <pubDate>Tue, 03 Feb 2026 10:06:31 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jooo_ii/post/5ab0f5ea-da51-4ee8-ba35-d9f16e729f70/image.png" alt=""></p>
<p>지난번에 미니프로젝트 핏로그의 백엔드 부분이 끝나고, 최종적으로 프론트엔드 작업을 시작하게 되었다.</p>
<h2 id="💼-fe-github">💼 FE Github</h2>
<p><a href="https://github.com/FitLog-ureca/FE">https://github.com/FitLog-ureca/FE</a></p>
<br/>

<h2 id="📌-주제">📌 주제</h2>
<p>하루의 운동 목표를 투두리스트 형태로 계획하고 완료 여부를 기록하며, 운동 기록을 잔디형 시각화로 제공해 성취감을 높이는 <strong><em>운동 관리 웹 프로젝트</em></strong></p>
<br/>

<h2 id="⚙️-프론트엔드-기술스택">⚙️ 프론트엔드 기술스택</h2>
<ul>
<li>React <code>^19.2.1</code></li>
<li>Next.js <code>^16.0.7</code></li>
<li>TypeScript <code>^5</code></li>
<li>Tailwind css <code>^4</code></li>
<li>Redux Toolkit <code>^2.11.1</code></li>
<li>Tanstack Query <code>^5.90.12</code></li>
</ul>
<br/>

<h2 id="🛠️-개발환경">🛠️ 개발환경</h2>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/57327d7d-8be9-4871-a2f4-fadc283ee2b6/image.png" alt=""></p>
<br/>


<h2 id="🛠️-시스템-아키텍처">🛠️ 시스템 아키텍처</h2>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/ab89a27e-8c01-4d9f-85aa-9707d1445fb4/image.png" alt=""></p>
<br/>

<h2 id="🏃🏻-유저-플로우차트">🏃🏻 유저 플로우차트</h2>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/8b78d613-1ede-49b4-923c-cb6d698bbf24/image.png" alt="">
로그인 -&gt; 메인화면
<img src="https://velog.velcdn.com/images/jooo_ii/post/edfeb114-1998-4512-b224-e797520787ea/image.png" alt="">
세부화면 -&gt; 메인화면</p>
 <br/>

<h2 id="🌆-피그마---ui-구성">🌆 피그마 - UI 구성</h2>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/c8a23bb0-6a28-4351-836f-716060505e66/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/ef1ac2e1-0913-4734-92f8-358fde591169/image.png" alt=""></p>
<br/>




<h2 id="🗂️-프로젝트-구조">🗂️ 프로젝트 구조</h2>
<pre><code>src
├─ actions         // 서버액션
├─ api            // API 
├─ app
│  ├─ api        // Next API (운동 시작 여부 판단을 위한)
│  │  └─ todos
│  ├─ login
│  ├─ signup
│  └─ todos
├─ assets        // 이미지 파일
├─ components        // 컴포넌트
│  ├─ auth
│  ├─ main
│  ├─ main-left
│  ├─ main-right
│  ├─ navbar
│  ├─ todos
│  └─ ui
├─ hooks        // 커스텀 훅
├─ lib            // 라이브러리 (tanstack)
│  ├─ interceptor
│  └─ tanstack
├─ store        // 상태관리 (redux toolkit)        
│  └─ redux    
│     └─ features
└─ types        // 타입</code></pre><br/>

<h2 id="👾-내가-맡은-개발-파트">👾 내가 맡은 개발 파트</h2>
<h3 id="▪︎-input-actionbutton-checkbox-공용-컴포넌트-개발"><strong>▪︎ Input, ActionButton, CheckBox 공용 컴포넌트 개발</strong></h3>
<p>이전 프로젝트를 진행할 때는 공용 컴포넌트 개발 없이 그때그때 필요한 컴포넌트를 만들어서 나중에 하나의 컴포넌트로 분리해 버리는 방식으로 개발을 했었다.</p>
<p>사실 이전에 해왔던 것이 컴포넌트 재사용이라 생각했었으나, 다른 팀이 한 것을 보니 내가 했던 작업은 사실상 컴포넌트 분리에 가까웠던 것 같았다.</p>
<p>그래서 이번 프로젝트를 진행할 때는 피그마 디자인과 동일하게 자주 사용하는 input, button 같은 공용 컴포넌트는 <code>clsx</code> 유틸리티 라이브러리를 사용하여 다른 컴포넌트에서 동일하지만 css 가 약간 다른 컴포넌트에서 사용할 때 조건적으로 css를 적용할 수 있도록 하여 재사용성을 향상시켰다.</p>
<table>
<thead>
<tr>
<th>Input</th>
<th>Button</th>
<th>Checkbox</th>
</tr>
</thead>
<tbody><tr>
<td><div align="center"><img src="https://velog.velcdn.com/images/jooo_ii/post/dfe5dded-7523-49e3-a924-c6d6ffa303a5/image.png" width="300" /></div></td>
<td><div align="center"><img src="https://velog.velcdn.com/images/jooo_ii/post/2d14c820-fb9f-4497-977c-cc5e9bdf03bc/image.png" width="300" /></div></td>
<td><div align="center"><img src="https://velog.velcdn.com/images/jooo_ii/post/e7419c4e-03d9-479c-b1ff-f26fa1a860d7/image.png" width="30" /></div></td>
</tr>
</tbody></table>
<br/>

<h3 id="▪︎-로그인-회원가입-access-token-refresh-token">▪︎ 로그인, 회원가입 (Access Token, Refresh Token)</h3>
<p>백엔드에서 JWT 토큰을 사용하여 로그인 과정을 구현하는 로직을 구현했었기 때문에, 프론트엔드에서도 로그인 기능을 맡아서 작업하였다. 지난 백엔드 시간에는 refresh token을 따로 구현하지 않고, access token만으로 구현했었으나, 이번에 refresh token 백엔드 작업을 수행하여서 프론트엔드에서 토큰 저장하는 작업이 필요했다.
이때 고민했던 것이 token의 저장 위치였다.</p>
<p>결론적으로는 Accss Token은 JS 메모리에, Refresh Token은 HttpOnly Cookies에 저장하여 관리하였다.
이에 따라 interceptor와 proxy (=middleware)를 사용하여 로그인 인증이 필요한 도메인에서는 로그인 인증이 되어야 접근할 수 있도록 구현하였다.</p>
<table>
<thead>
<tr>
<th>로그인 화면</th>
<th>회원가입 화면</th>
</tr>
</thead>
<tbody><tr>
<td><div align="center"><img src="https://velog.velcdn.com/images/jooo_ii/post/67f8ae12-a9ff-4663-8164-266c11c6a742/image.png" width="400" /></div></td>
<td><div align="center"><img src="https://velog.velcdn.com/images/jooo_ii/post/8becf9f7-e496-4e8e-bda8-83c9076f1164/image.png" width="400" /></div></td>
</tr>
</tbody></table>
<br/>

<table>
<thead>
<tr>
<th>Refresh Token 쿠키 저장</th>
</tr>
</thead>
<tbody><tr>
<td><div align="center"><img src="https://velog.velcdn.com/images/jooo_ii/post/92f9381a-b5ff-4818-afa2-2f129edd8953/image.png" width="850" /></div></td>
</tr>
</tbody></table>
<br/>

<h3 id="▪︎-세부화면-운동-목표-완료-체크-휴식-관련-타이머-기능">▪︎ 세부화면 (운동 목표 완료 체크, 휴식 관련 타이머 기능)</h3>
<p><strong>1) 운동 목표 완료 체크</strong>
해당 부분은 투두리스트 체크 기능과 동일하게 작업하였다. 이때 Redux Toolkit을 사용하여 투두 상태를 관리하였다. </p>
<table>
<thead>
<tr>
<th>체크 기능</th>
<th>투두리스트 체크 상태 관리</th>
</tr>
</thead>
<tbody><tr>
<td><div align="center"><img src="https://velog.velcdn.com/images/jooo_ii/post/28c0fb91-135c-499f-a957-4facabd8311e/image.png" width="400" /></div></td>
<td><div align="center"><img src="https://velog.velcdn.com/images/jooo_ii/post/25989c72-6c22-403d-9594-0a21b6f27fd0/image.png" width="400" /></div></td>
</tr>
</tbody></table>
<br/>



<p>*<em>2) 휴식 체크 시에 타이머 기능 *</em>
운동 완료 체크 여부에 따라 개별 세트의 휴식 버튼 활성화 여부가 결정되는 기능이며, 휴식 버튼을 누르면 타이머가 활성화되는 기능이다.
이는 개별 세트 완료할 때마다 일정 시간 휴식을 취한 후, 다음 세트를 들어가기 때문에 구현한 기능이다.</p>
<p>이때, 체크한 해당 세트의 휴식을 어떻게 기억하고, 시간을 어떻게 기록해야 할지 엄청난 고민에 빠졌었다.</p>
<p>도저히 감을 못 잡아서 claude에게 물어본 결과, Redux로 타이머 상태(활성화 여부, 남은 시간, todoId)를 관리하고, 마지막으로 체크한 todoId를 sessionStorage에 저장하여 새로고침 후에도 어떤 운동의 휴식이었는지 복원할 수 있도록 구현하는 방법이 있었다.</p>
<p>이를 기반으로 아래와 같이 <code>timerSlice.ts</code>를 작성하였다.</p>
<pre><code class="language-js">import { Timer } from &quot;@/types/timer&quot;;
import { createSlice, PayloadAction } from &quot;@reduxjs/toolkit&quot;;

// sessionStorage에서 todoId 복원
const getInitialTodoId = (): number | null =&gt; {
  if (typeof window === &quot;undefined&quot;) return null;
  const saved = sessionStorage.getItem(&quot;lastTodoId&quot;);
  return saved ? parseInt(saved, 10) : null;
};

const initialState: Timer = {
  isActive: false,
  duration: 0,
  todoId: getInitialTodoId(),
};

const timerSlice = createSlice({
  name: &quot;timer&quot;,
  initialState,
  reducers: {
    // 휴식 시작
    startRest(
      state,
      action: PayloadAction&lt;{ duration: number; todoId: number }&gt;
    ) {
      state.isActive = true;
      state.duration = action.payload.duration;
      state.todoId = action.payload.todoId;

      // sessionStorage에 마지막으로 선택된 todoId 저장
      if (typeof window !== &quot;undefined&quot;) {
        sessionStorage.setItem(&quot;lastTodoId&quot;, action.payload.todoId.toString());
      }
    },
    // 휴식 스탑
    stopRest(state) {
      state.isActive = false;
      state.duration = 0;
    },
    // 타이머 초기화
    clearTimer(state) {
      state.isActive = false;
      state.duration = 0;
      state.todoId = null;
      // sessionStorage에서 제거
      if (typeof window !== &quot;undefined&quot;) {
        sessionStorage.removeItem(&quot;lastTodoId&quot;);
      }
    },
  },
});

export const { startRest, stopRest, clearTimer } = timerSlice.actions;
export default timerSlice.reducer;</code></pre>
<table>
<thead>
<tr>
<th>타이머 비활성화</th>
<th>타이머 활성화 (휴식 버튼 클릭)</th>
<th>휴식 시간 기록</th>
</tr>
</thead>
<tbody><tr>
<td><div align="center"><img src="https://velog.velcdn.com/images/jooo_ii/post/f8a6eec8-915b-4e16-b8b1-7402c8dc5162/image.png" width="330" /></div></td>
<td><div align="center"><img src="https://velog.velcdn.com/images/jooo_ii/post/466c6b9d-77c2-410d-8f24-24a96977f434/image.png" width="330" /></div></td>
<td><div align="center"><img src="https://velog.velcdn.com/images/jooo_ii/post/22842bad-0ceb-48db-a35d-0ddb4aab15af/image.png" width="330" /></div></td>
</tr>
</tbody></table>
<br/>


<table>
<thead>
<tr>
<th>마지막으로 체크한 todoId 저장</th>
</tr>
</thead>
<tbody><tr>
<td><div align="center"><img src="https://velog.velcdn.com/images/jooo_ii/post/d0bd8cda-5de5-471a-82cc-8b7ebe67a68f/image.png" width="600" /></div></td>
</tr>
</tbody></table>
<br/>

<h3 id="▪︎-프로필--수정-기능">▪︎ 프로필  수정 기능</h3>
<p>이름, 생년월일, 자기소개, 프로필 이미지를 수정할 수 있는 기능을 개발하였다.
프로필 이미지 관련해서는 Base64 형태로 DB에 직접 저장하는 방식으로 구현하였다.
처음 기획했을 때는 AWS S3로 이미지를 관리하고자 하였는데, 비용 대비 효율이 낮다고 판단하여 Base64 형태로 DB에 직접 저장하는 방식이 현실적인 선택이라 판단했다.</p>
<p>DB에 저장할 때도 그대로 저장하는 것이 아니라, <strong>이미지의 최대 크기를 300 * 300px로 제한</strong>하였고, <strong>JPEG 압축을 적용</strong>하여 <strong><em>2MB -&gt; 약 30~70KB 수준으로 감소</em></strong>시켜 관리하였다.</p>
<p>이에 관련해서는 아래 기록해 두었다.
<a href="https://joooii.tistory.com/26">Spring Boot에서 이미지 파일 관리하기</a></p>
<br/>

<table>
<thead>
<tr>
<th>프로필 수정</th>
<th>프로필 조회</th>
</tr>
</thead>
<tbody><tr>
<td><div align="center"><img src="https://velog.velcdn.com/images/jooo_ii/post/f8ca56ca-6775-414c-b9b1-daa1800db804/image.png" width="300" /></div></td>
<td><div align="center"><img src="https://velog.velcdn.com/images/jooo_ii/post/9ac7dd55-df5a-432a-92d7-10da6dad3cc3/image.png" width="300" /></div></td>
</tr>
</tbody></table>
<br/>

<h3 id="▪︎-운동-시작-여부에-따른-세부화면-접근-제어">▪︎ 운동 시작 여부에 따른 세부화면 접근 제어</h3>
<p>기존엔 운동 시작 여부에 상관없이 세부화면 (/todos)에 접근할 수 있었다. 이를 위해 API를 구현하기에는 오버 스펙일 것 같다는 생각이 들어서 Next의 API 기능 (<code>next/server</code>)을 사용하여 &#39;운동 시작&#39; 버튼을 누르면 쿠키에 <code>canEnterTodos = true</code>를 저장하여 true일 때만 접근이 가능하도록 하였다. 또한 &#39;운동 완료&#39; 버튼을 누르면 쿠키에서 canEnterTodos가 삭제되어 세부화면에 접근하지 못하고, 접근 시 메인화면으로 강제 리다이렉트 되도록 구현하였다.</p>
<pre><code class="language-js">// app/api/todos/start/route.ts 
// 운동 시작시

import { NextResponse } from &quot;next/server&quot;;

export async function POST() {
  const res = NextResponse.json({ ok: true });
  res.cookies.set(&quot;canEnterTodos&quot;, &quot;true&quot;, {
    httpOnly: true,
    path: &quot;/&quot;,
    sameSite: &quot;lax&quot;,
  });
  return res;
}</code></pre>
<pre><code class="language-js">// app/api/todos/end/route.ts
// 운동 완료시

import { NextResponse } from &quot;next/server&quot;;

export async function POST() {
  const res = NextResponse.json({ ok: true });
  res.cookies.set(&quot;canEnterTodos&quot;, &quot;&quot;, { maxAge: 0, path: &quot;/&quot; });
  return res;
}
</code></pre>
<table>
<thead>
<tr>
<th>canEnterTodos 저장</th>
</tr>
</thead>
<tbody><tr>
<td><div align="center"><img src="https://velog.velcdn.com/images/jooo_ii/post/18993faa-59c3-4dee-af0b-1e868252979c/image.png" width="700" /></div></td>
</tr>
</tbody></table>
<br/>

<table>
<thead>
<tr>
<th>start API</th>
<th>end API</th>
</tr>
</thead>
<tbody><tr>
<td><div align="center"><img src="https://velog.velcdn.com/images/jooo_ii/post/751454c8-a233-423e-aa31-fe0d31e7c235/image.png" width="500" /></div></td>
<td><div align="center"><img src="https://velog.velcdn.com/images/jooo_ii/post/4528ea6f-f69d-460a-878a-bae31c58d46d/image.png" width="500" /></div></td>
</tr>
</tbody></table>
<h2 id="📹-시연영상">📹 시연영상</h2>
<p><a href="https://velog.velcdn.com/images/jooo_ii/post/898f97a6-9c3c-4bca-b513-134fd5cda9b2/image.mov">https://velog.velcdn.com/images/jooo_ii/post/898f97a6-9c3c-4bca-b513-134fd5cda9b2/image.mov</a></p>
<h2 id="💫-회고">💫 회고</h2>
<h3 id="🌝-좋았던-점">🌝 좋았던 점</h3>
<p><strong>▪︎ 처음으로 팀장을 맡았던 프로젝트</strong>
그동안 팀장을 하기에는 리더십이 부족하고, 실력도 부족하다고 생각했어서 쉽게 도전해보지 않았던 직책이다. 그렇기에 팀장을 잘할 수 있을까?라는 고민이 컸다. 
그래서 일정 관리부터 파트 분배까지 팀장의 역할을 최대한 수행하고자 노력하였다. 확실히 팀원으로 참여했을 때와 팀장으로 참여했을 때의 무게감은 차원이 달랐던 것 같다. 그래도 팀원이 잘 도와줘서 프로젝트를 깔끔하게 마무리할 수 있었다.</p>
<p><strong>▪︎ 백엔드의 이해</strong>
프론트엔드 반에서 진행한 프로젝트였기에 백엔드까지 전부 다 구현해야 했다. 백엔드는 예전 express 제외하고는.. spring boot는 처음이었기 때문에 폴더 구조부터 API 구현 방법, 로그인 처리 등 1부터 시작해서 쉽지 않았다. 그렇기에 강의와 책을 통해 추가 학습을 진행하면서 백엔드에 대한 어느 정도의 감이 생겼다고 생각한다. (남이 볼 땐 모르겠지만)
사실 프론트엔드만 하게 되면 백엔드 코드가 어떻게 구성되는지 알 방법이 없다. 이번 기회에 폴더 구조와 코드를 보는 방법을 익힐 수 있어서 좋았다. </p>
<h3 id="🌚-아쉬웠던-점">🌚 아쉬웠던 점</h3>
<p><strong>▪︎ AI 도구의 높은 의존성</strong>
이번 프로젝트를 하면서 사실상 백엔드는 AI가 전부 다 구현했다고 .. 해도 할 말이 없다. AI를 적당히 활용하면 득이 되는 것을 알고 있지만, 생각보다 활용을 너무 많이 해서 약간의 실이 된 것 같아 아쉬웠다. 이 의존성을 좀 줄여놔야 득이 되는 데 그게 쉽지가 않다..</p>
<p><strong>▪︎ 한정된 기술 스택</strong>
해당 부트캠프에서 제시한 기술 스택을 사용해야만 했던 부분이 조금 아쉽다. 예를 들면 Redux나 MyBatis를 사용했어야만 해서 약간의 기술 스택 선택에 있어 자유롭지 못했던 것이 조금 아쉽다. Zustand를 써보고 싶었는데, 이건 종합 프로젝트에 가서 사용해 봐야겠다. 후회는 없다.</p>
<hr>
<h2 id="마치며-">마치며 ..</h2>
<p>10월 달부터 진행했던 미니 프로젝트가 드디어 성황리에 막을 내렸다. 생각보다 프론트엔드 작업을 하면서 예상치 못한 문제들이 많아서 백엔드 수정을 많이 하게 되었다. 이 과정에서 역시 완벽한 설계란 없구나라는 생각이 들었다.
이번 프로젝트에서 Redux Toolkit과 Tanstack Query를 반드시 사용했어야 해서 이번 기회에 정환님 인프런 강의를 들으면서 초고속으로 사용 방법을 익혔다. 어찌저찌 사용해서 구현을 하긴 했으나, 다음 종합 프로젝트에서는 조금 더 완벽하게 사용해보고 싶다.
그래도 이번 프로젝트를 하면서 백엔드를 좀 더 잘해보고 싶다는 생각이 들었고, 인프런에 나와있는 영한님 무료 강의를 들어보고 종프때는 AI 툴에 적당히 의존하고 싶다 ..</p>
<p>이번에 공식문서를 많이 보려고 노력하였고, 이를 기반으로 proxy(middleware)의 사용 방법을 익히니 이해가 더욱 잘 되었고, 공식문서의 중요성을 또 한 번 느꼈다</p>
<p>연말 전에 깔끔하게 마무리할 수 있어서 좋다 !
메리 크리스마스이옵니다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고] 유레카 부트캠프 프론트엔드 과정: 미니 프로젝트 핏로그 - 백엔드]]></title>
            <link>https://velog.io/@jooo_ii/%ED%9A%8C%EA%B3%A0-%EC%9C%A0%EB%A0%88%EC%B9%B4-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B3%BC%EC%A0%95-%EB%AF%B8%EB%8B%88-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%95%8F%EB%A1%9C%EA%B7%B8-%EB%B0%B1%EC%97%94%EB%93%9C</link>
            <guid>https://velog.io/@jooo_ii/%ED%9A%8C%EA%B3%A0-%EC%9C%A0%EB%A0%88%EC%B9%B4-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B3%BC%EC%A0%95-%EB%AF%B8%EB%8B%88-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%95%8F%EB%A1%9C%EA%B7%B8-%EB%B0%B1%EC%97%94%EB%93%9C</guid>
            <pubDate>Sat, 31 Jan 2026 18:15:09 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jooo_ii/post/d1b27692-7c43-454a-becf-ef10a7495d33/image.png" alt=""></p>
<p>유레카에서 첫 미니 프로젝트를 시작했다. (두근두근)</p>
<h2 id="📌-주제-선정">📌 주제 선정</h2>
<p>프론트엔드의 꽃인 To Do List를 적용하기 위해서 고민을 하던 중 운동 목표를 세우고 이에 따라 운동을 수행하면 캘린더에 깃허브 잔디처럼 심어놓는 식으로 하자는 팀원의 좋은 아이디어를 채택해서 해당 주제로 프로젝트를 진행하게 되었다!!</p>
 <br/>

<h2 id="🌟-미니-프로젝트-최종-주제-🌟">🌟 미니 프로젝트 최종 주제 🌟</h2>
<p>하루의 운동량을 기록하고 시각적으로 성취감을 느끼며 꾸준한 동기부여를 통해 건강한 삶을 실천하는 운동 관리 웹앱 프로젝트이다.
협업은 Notion, Github을 사용해서 진행하였다.
<img src="https://velog.velcdn.com/images/jooo_ii/post/c0a1a02b-9299-42a1-8dc7-31db4e2ece81/image.png" alt=""></p>
<p><a href="https://github.com/FitLog-ureca/BE">FitLog - BE (Github)</a></p>
 <br/>

<h2 id="⚙️-백엔드-기술스택">⚙️ 백엔드 기술스택</h2>
<pre><code>- Java 17
- Spring Boot 3.4.10
- Gradle
- MyBatis 3.0.3
- MySQL
- Swagger UI 2.7.0</code></pre><p>처음에 Swagger Open Api의 버전을 2.0.2로 했으나, GlobalExceptionHandler를 사용하면서 SpringDoc과 SpringBoot의 버전이 불일치해서 Swagger의 버전을 업데이트했다.</p>
<br/>

<h2 id="💵-db-설계">💵 DB 설계</h2>
<h3 id="최종-erd">최종 ERD</h3>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/40f3da64-0f2b-47b1-8657-5d59706934bd/image.png" alt=""></p>
<p>현재는 기획했던 기능이 크게 없기 때문에 테이블은 3개로 이루어져 있다.</p>
<p><strong>users</strong>
: 회원 정보 관련</p>
<p><strong>exercises</strong>
: 운동 종목 관련</p>
<p><strong>todos</strong>
: 운동 목표 및 기록 관련</p>
<p>기능은 크게 Auth, Todos, Exercises, Profile로 구성되어 있습니다.</p>
<br/>

<h2 id="🧾-명세">🧾 명세</h2>
<p>명세는 구글링해서 제일 보편화되어있는 템플릿을 채택해 적용했다.
사실 명세를 4일동안 수정하려니 정말 어렵고 생각할 게 너무 많아서 더 오래 걸린 감이 있다.
명세가 중요하다 라는 말이 괜히 있는 게 아니었다..
<img src="https://velog.velcdn.com/images/jooo_ii/post/78349edd-deba-495c-b508-2ba4f8292321/image.png" alt=""></p>
 <br/>


<h2 id="📮-기능-및-api">📮 기능 및 API</h2>
<h3 id="auth">Auth</h3>
<ul>
<li><p><strong>로그인</strong> (<code>POST /auth/login</code>)</p>
<ul>
<li>JWT를 사용하여 Access Token (유효기간: 1시간)을 쿠키에 저장하는 방식</li>
</ul>
</li>
<li><p><strong>로그아웃</strong> (<code>POST /auth/logout</code>)</p>
<ul>
<li>로그아웃 시 쿠키에서 Access Token 삭제</li>
</ul>
</li>
<li><p><strong>회원가입</strong> (<code>POST /auth/signup</code>)</p>
<ul>
<li>DB에 회원 정보 저장</li>
</ul>
</li>
<li><p><strong>Refresh Token 발급</strong> (<code>POST /auth/refresh</code>)</p>
<ul>
<li>Access Token 만료 시 Refresh Token API를 호출하여 Access Token 재발급</li>
<li>Refresh Token 유효기간: 7일</li>
</ul>
</li>
</ul>
<h3 id="todos">Todos</h3>
<ul>
<li><p><strong>캘린더 월간 운동 요약</strong> (<code>GET /todos/summary</code>)</p>
<ul>
<li>완료한 운동 상태에 따라 캘린더에 색상을 표현하기 위한 월간 운동 조회</li>
</ul>
</li>
<li><p><strong>운동 항목 생성</strong> (<code>POST /todos</code>)</p>
<ul>
<li>세트번호가 1인 세트 항목 생성</li>
</ul>
</li>
<li><p><strong>운동 세트 항목 생성</strong> (<code>POST /todos/{todoId}/sets</code>)</p>
<ul>
<li>세트 항목 생성 (2, 3, .., N 세트)</li>
</ul>
</li>
<li><p><strong>운동 목표 수정</strong> (<code>PATCH /todos/record/{todoId}</code>)</p>
<ul>
<li>세트 기록 수정 (reps + weight)</li>
</ul>
</li>
<li><p><strong>운동 완료 체크</strong> (<code>PATCH /todos/complete/{id}</code>)</p>
<ul>
<li>개별 세트 완료 시 완료 체크 여부 (isCompleted)</li>
</ul>
</li>
<li><p><strong>휴식시간 기록</strong> (<code>PATCH /todos/rest/{todoId}</code>)</p>
<ul>
<li>휴식 시작/종료 시간을 기반으로 휴식시간 기록</li>
<li>로컬 스토리지에 시간을 기록해서 휴식 시간을 기록하는 방식</li>
</ul>
</li>
<li><p><strong>휴식시간 초기화</strong> (<code>DELETE /todos/rest/reset/{todoId}</code>)</p>
<ul>
<li>휴식시간 초기화 (휴식 시작 시간, 종료 시간 제거)</li>
</ul>
</li>
<li><p><strong>하루 운동 완료</strong> (<code>PATCH /todos/done/{date}</code>)</p>
<ul>
<li>하루 운동을 완료했는지에 대한 여부 (isDone) 변경</li>
</ul>
</li>
<li><p><strong>운동 목표 삭제</strong> (<code>DELETE /todos/{id}</code>)</p>
<ul>
<li>저장된 운동 목표 삭제</li>
</ul>
</li>
<li><p><strong>운동 세트 항목 삭제</strong> (<code>DELETE /todos/{todoId}</code>)</p>
<ul>
<li>설정한 목표 세트를 삭제</li>
</ul>
</li>
<li><p><strong>운동 항목 삭제</strong> (<code>DELETE /todos/workouts/{workoutId}</code>)</p>
<ul>
<li>설정한 목표 운동 항목을 삭제</li>
</ul>
</li>
</ul>
<h3 id="exercises">Exercises</h3>
<ul>
<li><p><strong>운동 기록 조회</strong> (<code>GET /exercises</code>)</p>
<ul>
<li>특정 날짜의 운동 기록 및 총 칼로리 조회</li>
</ul>
</li>
<li><p><strong>운동 종목 리스트 조회</strong> (<code>GET /exercises/search</code>)</p>
<ul>
<li>운동명 검색 및 페이징 목록 조회</li>
</ul>
</li>
</ul>
<h3 id="profile">Profile</h3>
<ul>
<li><p><strong>프로필 조회</strong> (<code>GET /profile/me</code>)</p>
<ul>
<li>이름, 나이, 프로필 이미지 조회</li>
</ul>
</li>
<li><p><strong>프로필 수정</strong> (<code>PUT /profile/update</code>)</p>
<ul>
<li>이름, 생년월일, 프로필 이미지 수정</li>
<li>생년월일 수정 시 나이 자동 계산</li>
</ul>
</li>
</ul>
 <br/>

<h2 id="🗂️-프로젝트-구조">🗂️ 프로젝트 구조</h2>
<pre><code>com
└── ureca
    └── fitlog
        ├── auth          // 🔐 인증 및 회원 관리
        │   ├── controller
        │   ├── dto
        │   ├── jwt
        │   ├── mapper
        │   └── service
        │
        ├── todos         // ✅ 운동 목표(To-Do) 관리 기능
        │   ├── controller
        │   ├── dto
        │   │   ├── request
        │   │   └── response
        │   ├── mapper
        │   └── service
        │
        ├── exercise      // 💪 운동 종목 및 기록 관리
        │   ├── controller
        │   ├── dto
        │   ├── mapper
        │   └── service
        │
        ├── profile       // 👤 사용자 프로필 관리
        │   ├── controller
        │   ├── dto
        │   ├── mapper
        │   └── service
        │
        ├── common        // ⚙️ 공통 모듈 (예외, 유틸 등)
        │   ├── exception
        │   └── util
        │
        └── config        // 🧩 전역 환경 설정</code></pre> <br/>

<h2 id="📚-트러블슈팅">📚 트러블슈팅</h2>
<h3 id="1-access-token-쿠키에-저장--refresh-token">1. Access Token 쿠키에 저장 + Refresh Token</h3>
<p>기존에는 Access Token을 header에 저장하는 방식으로 구현을 했었다. </p>
<p>진행하고 있던 프로젝트에서는 header에 저장했던 것 같아서 그렇게 구현을 했는데, API 요청 테스트를 할 때마다 Bearar Token에 담으려고 하니 그것도 일이었다.
그리고 좀 그렇긴 하지만 Swagger에서 Bearer Token을 저장해서 하는 방법을 몰랐다. ㅎㅎ </p>
<p>그래서 다른 팀의 팀원이 쿠키에 저장하면 그런 과정 필요없다는 것을 말해줘서 그때 아 쿠키에 저장해야겠다 라고 방향을 돌렸다.</p>
<pre><code class="language-java">/** 로그인 (JWT 발급 포함) */
@PostMapping(&quot;/login&quot;)
@Operation(
        summary = &quot;로그인&quot;
)
public ResponseEntity&lt;LoginResponseDTO&gt; login(@RequestBody LoginRequestDTO request, HttpServletResponse response) {
    try {
        LoginResponseDTO loginResult = authService.login(request);
        // JWT 토큰 생성
        String token = jwtTokenProvider.createToken(loginResult.getLoginId());
        int cookieMaxAge = (int) (jwtTokenProvider.getValidityInMilliseconds());
        Cookie cookie = new Cookie(&quot;accessToken&quot;, token);
        cookie.setHttpOnly(true);  // JS 접근 불가
        cookie.setSecure(true);    // HTTPS 전용
        cookie.setPath(&quot;/&quot;);       // 모든 경로에 유효
        cookie.setMaxAge(cookieMaxAge); // 1일
        response.addCookie(cookie);


        // Builder로 새 객체 생성
        LoginResponseDTO responseBody = LoginResponseDTO.builder()
                .message(&quot;로그인에 성공했습니다.&quot;)
                .loginId(loginResult.getLoginId())
                .name(loginResult.getName())
                .token(token)
                .build();

        return ResponseEntity.ok(responseBody);
    } catch (IllegalArgumentException e) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(LoginResponseDTO.builder()
                        .message(&quot;아이디 또는 비밀번호가 올바르지 않습니다.&quot;)
                        .build());
    }  catch (Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(LoginResponseDTO.builder()
                        .message(&quot;로그인 처리 중 오류가 발생했습니다.&quot;)
                        .build());
    }
}</code></pre>
<p>쿠키에 저장까지는 어찌저찌 했는데, Refresh Token 재발급하는 과정에서 크케 난관을 맞아서 일단 Refresh Token 재발급 구현은 뒤로 미뤄뒀다 ..</p>
<p>왜냐면 Access Token과 Refresh Token 저장 위치가 너무 고민이 됐다.</p>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/ae2f14f4-2d0f-42d2-8ee4-c7db7829a918/image.png" alt="token 저장 위치 비교 표"></p>
<p>일단 조금 더 조합을 고민해봐야겠다.
<strong>➡️ 최종적으로 Access Token을 JS 메모리에 저장하고, Refresh Token을 HttpOnly Cookies에 저장하여 관리하는 방법으로 구현하였다.</strong></p>
<h3 id="2-swagger-기본-응답값이-additionalprop-로-나옴">2. Swagger 기본 응답값이 additionalProp 로 나옴</h3>
<pre><code class="language-js">{
  &quot;additionalProp1&quot;: &quot;string&quot;,
  &quot;additionalProp2&quot;: &quot;string&quot;,
  &quot;additionalProp3&quot;: &quot;string&quot;
}</code></pre>
<blockquote>
<p><strong>❓ 원인</strong>
해당 문제의 원인은 Swagger가 Java 객체를 스캔할 때 응답 타입을 명시적으로 알 수 없거나, 제네릭/Map 형태로 감싸져 있을 경우 자동으로 추상화된 응답 스키마를 생성하기 때문이었다.
우리 코드에서는 TodoController에서 Map을 특히나 많이 사용하고 있었어서 저렇게 기본 응답 스키마가 생성되었던 것이다.</p>
</blockquote>
<blockquote>
<p><strong>❗️ 해결</strong>
이를 해결하기 위해서</p>
</blockquote>
<p> 1) Map 대신 DTO 타입을 명확히 하기 위해서 응답값 갯수가 많은 경우에는 DTO를 따로 빼서 지정하였다. 
 2) 응답값 갯수가 적은 경우에는 @ApiResponse을 사용해서 기본 응답값을 지정했다.</p>
<p>물론 이게 좋은 방법 같지는 않아서 고민이 됐지만, 시간상 위 2개의 방식으로 이 문제를 해결했다.
추후 리팩토링하면서 공통 응답 스키마를 지정하는 파일을 생성해서 재사용할 수 있게 해야될 것 같다.</p>
<h3 id="3-swagger-500-오류---controlleradvicebeaninitobject-버전-충돌">3. Swagger 500 오류 - ControllerAdviceBean.<init>(Object) 버전 충돌</h3>
<blockquote>
<p><strong>❓ 원인</strong>
전역 예외 처리용(GlobalExceptionHandler)을 추가한 직후 Swagger UI 접속할 때 500에러가 발생했다.</p>
</blockquote>
<p>이는 GlobalExceptionHandler에서 @RestControllerAdvice을 사용하면서 생긴 에러로 Spring Boot (v.3.4.x)와 Springdoc (OpenAPI, v.2.0.2)의 버전 충돌로 인해 발생한 것이었다.</p>
<blockquote>
<p><strong>❗️ 해결</strong>
찾아보니 Spring Boot의 버전을 3.3.x로 다운그레이드하거나 Swagger를 2.7.0 snapshot 버전을 사용하는 방식을 사용하라고 했다.
이에 우리는 Swagger의 버전을 업그레이드하는 것이 오류를 최소화하는 방법이라 생각되어 해당 방식을 채택하여 오류를 해결하였다.</p>
</blockquote>
<h4 id="-ngrok">+ Ngrok</h4>
<p>Ngrok은 로컬 개발 환경에서 인터넷을 통해 웹 애플리케이션에 안전하게 접근할 수 있도록 하는 도구이다.
보안 연결을 통해 인터넷에서 로컬 서버를 실행할 수 있으며, 웹 애플리케이션을 외부에 노출시키지 않고도 테스트할 수 있다.</p>
<p>이때 403에러가 계속 났는데, 이는 SecurityConfig 설정 때문이었으며, 아래처럼 anyRequest()를 permitAll()해서 해결할 수 있었다.</p>
<pre><code class="language-java">@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            .csrf(AbstractHttpConfigurer::disable)
            .cors(cors -&gt; {})
            .sessionManagement(session -&gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -&gt; auth
                    .anyRequest().permitAll()
            )</code></pre>
<p>또한 SwaggerConfig에서 아래처럼 @OpenAPIDefinition에 ngrok 서버를 추가해서 403에러를 해결했다.</p>
<pre><code class="language-java">@OpenAPIDefinition(
        servers = {
                // 🔹 Swagger UI가 ngrok을 통해 열릴 때, 이 URL로 요청을 보냄
                @Server(url = &quot;ngrok서버&quot;, description = &quot;Ngrok Tunnel Server&quot;),
                @Server(url = &quot;http://localhost:8080&quot;, description = &quot;Localhost Server&quot;)
        }
)</code></pre>
<hr>
<h1 id="회고">회고</h1>
<p>이렇게 백엔드 주간이 끝났다.
백엔드를 오래 배우지 않아 따로 공부하면서 바로 프로젝트를 진행하느라 속도를 붙이기까지 오래 걸려서 너무 아쉬웠다.
그래도 이론 배울 때보다는 확실히 실전으로 프로젝트를 진행하며 백엔드 공부를 하니 정보 습득력이 더 향상되었다.
제일 큰 아쉬운 점은 GPT를 너무 애용했다는 것.. 이긴 한데 이 부분은 백엔드 강의를 조금 더 들어보려고 한다.</p>
<p>그래도 API 명세를 직접 고민하고, 구조를 생각해볼 수 있던 시간이라서 이번 미니프로젝트 백엔드를 통해 얻어가는 게 참 많았다고 생각한다.
이제 또 프론트엔드를 하면서 아마 백엔드 수정이 필수일 것이다. 수업을 나가면서도 지속적으로 코드를 들여다보고, 코드를 어떻게 짜는 것이 좋을지 지속적으로 고민해봐야할 것 같다.</p>
<p>아 정처기 공부 언제하냐</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[33~35일차] 미니 프로젝트 - 백엔드]]></title>
            <link>https://velog.io/@jooo_ii/3335%EC%9D%BC%EC%B0%A8-%EB%AF%B8%EB%8B%88-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%B1%EC%97%94%EB%93%9C</link>
            <guid>https://velog.io/@jooo_ii/3335%EC%9D%BC%EC%B0%A8-%EB%AF%B8%EB%8B%88-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%B1%EC%97%94%EB%93%9C</guid>
            <pubDate>Sat, 31 Jan 2026 18:03:21 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jooo_ii/post/152260ed-071c-4b28-85cd-227d94ece036/image.png" alt=""></p>
<h2 id="1-프로젝트-빌드-도구-비교-maven-vs-gradle">1. 프로젝트 빌드 도구 비교 (Maven vs Gradle)</h2>
<h3 id="maven이란">Maven이란?</h3>
<p>2004년에 출시된 Java 기반 프로젝트 빌드 도구이다. 주로 프로젝트 빌드(컴파일, 패키징, 테스트, 배포 등)를 자동화하는 데 사용된다.</p>
<p>Maven은 XML 파일(pom.xml)을 통해 프로젝트 구조와 설정을 관리한다.</p>
<p>Maven은 일관된 구조로 인해 사용하기 쉽고, 다양한 외부 라이브러리와의 통합이 간편하다는 장점이 있으나, XML는 유연성이 부족하고 속도가 느리다는 단점이 있다.</p>
<p><strong>예시 코드)</strong></p>
<pre><code class="language-java">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
    xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt;
    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;
    &lt;parent&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;
        &lt;version&gt;3.5.6&lt;/version&gt;
        &lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt;
    &lt;/parent&gt;
    &lt;groupId&gt;com.uplus&lt;/groupId&gt;
    &lt;artifactId&gt;SpringBootTest&lt;/artifactId&gt;
    &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;
    &lt;name&gt;SpringBootTest&lt;/name&gt;
    &lt;description&gt;Demo project for Spring Boot&lt;/description&gt;
    &lt;url/&gt;
    &lt;licenses&gt;
        &lt;license/&gt;
    &lt;/licenses&gt;
    &lt;developers&gt;
        &lt;developer/&gt;
    &lt;/developers&gt;
    &lt;scm&gt;
        &lt;connection/&gt;
        &lt;developerConnection/&gt;
        &lt;tag/&gt;
        &lt;url/&gt;
    &lt;/scm&gt;
    &lt;properties&gt;
        &lt;java.version&gt;17&lt;/java.version&gt;
    &lt;/properties&gt;
    &lt;dependencies&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.mybatis.spring.boot&lt;/groupId&gt;
            &lt;artifactId&gt;mybatis-spring-boot-starter&lt;/artifactId&gt;
            &lt;version&gt;3.0.5&lt;/version&gt;
        &lt;/dependency&gt;

        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-devtools&lt;/artifactId&gt;
            &lt;scope&gt;runtime&lt;/scope&gt;
            &lt;optional&gt;true&lt;/optional&gt;
        &lt;/dependency&gt;
    &lt;/dependencies&gt;

    &lt;build&gt;
        &lt;plugins&gt;
            &lt;plugin&gt;
                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
            &lt;/plugin&gt;
        &lt;/plugins&gt;
    &lt;/build&gt;

&lt;/project&gt;</code></pre>
<h3 id="gradle이란">Gradle이란?</h3>
<p>Maven의 단점을 보완하기 위해 2009년에 출시된 Java 기반 프로젝트 빌드 도구이다. Groovy나 Kotlin 기반의 DSL(Domain-Specific Language)을 사용해 빌드 스크립트를 작성한다.</p>
<p>이 방식은 &#39;build.gradle&#39; 이라는 빌드 파일을 생성하기 때문에 가독성이 좋고, XML 보다 훨씬 더 유연하다. 이러한 특징으로 인해 Gradle의 주요 장점 중 하나는 빌드 성능이 좋고 (병렬 빌드 기본 지원 등) 빌드 시간을 크게 줄일 수 있다는 장점이 있다. 그러나 스크립트 문법에 익숙해야 해서 약간의 난이도가 있다는 단점이 있다.</p>
<p>또한 Maven과의 호환성도 좋아서 기존 Maven 프로젝트를 Gradle로 마이그레이션하는 것이 가능하다.</p>
<p><strong>예시 코드)</strong></p>
<pre><code class="language-java">plugins {
    id &#39;java&#39;
    id &#39;org.springframework.boot&#39; version &#39;3.4.10&#39;
    id &#39;io.spring.dependency-management&#39; version &#39;1.1.7&#39;
}

group = &#39;com.ureca&#39;
version = &#39;0.0.1-SNAPSHOT&#39;
description = &#39;Mini Project for Spring Boot&#39;

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;
    compileOnly &#39;org.projectlombok:lombok&#39;
    developmentOnly &#39;org.springframework.boot:spring-boot-devtools&#39;
    annotationProcessor &#39;org.projectlombok:lombok&#39;
    testImplementation &#39;org.springframework.boot:spring-boot-starter-test&#39;
    testRuntimeOnly &#39;org.junit.platform:junit-platform-launcher&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;
    implementation &#39;org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3&#39;
    runtimeOnly &#39;com.mysql:mysql-connector-j&#39;
    implementation &#39;org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2&#39;
}

tasks.named(&#39;test&#39;) {
    useJUnitPlatform()
}
</code></pre>
<p>이처럼 Gradle이 Maven 보다 학습 난이도가 있다는 단점이 있으나, 이를 제외하고는 Gradle을 사용하는 것이 좋다고 판단되어 이번 프로젝트 빌드 도구는 Gradle을 채택하였다.</p>
<hr>
<h2 id="2-api-명세-작성법">2. API 명세 작성법</h2>
<p>API 명세 작성법을 알기 전에 API가 뭔지 알아보자.</p>
<h3 id="api란">API란?</h3>
<p><strong>API(Application Programming Interface)</strong>는 정의 및 프로토콜 집합을 사용하여 두 소프트웨어 구성 요소가 서로 통신할 수 있게 하는 매커니즘이다.</p>
<p>API를 통해 백엔드와 프론트엔드가 통신하기 때문에 초기에 API 명세를 잘 수립하는 것이 상당히 중요하다.</p>
<h3 id="api-명세-작성법">API 명세 작성법</h3>
<h4 id="1-개요-overview">1) 개요 (Overview)</h4>
<ul>
<li>프로젝트의 큰 그림부터 설명한다.</li>
<li>이는 Swagger / Postman 문서의 최상단에 오는 부분이다.
<img src="https://velog.velcdn.com/images/jooo_ii/post/f98076e4-a291-4726-bdec-1ccc15a1e0c4/image.png" alt=""></li>
</ul>
<h4 id="2-엔드포인트-구조-설계">2) 엔드포인트 구조 설계</h4>
<ul>
<li>리소스(Resource) 단위로 URL을 설계한다.
<img src="https://velog.velcdn.com/images/jooo_ii/post/b7765b74-f5ad-4177-b4bd-9a96da7d417d/image.png" alt=""></li>
</ul>
<h4 id="3-api-별-상세-명세-작성">3) API 별 상세 명세 작성</h4>
<ul>
<li>실제 엔드포인트 단위로 세부 정보를 기입한다.
<img src="https://velog.velcdn.com/images/jooo_ii/post/26a1009d-d62f-4df9-8542-e94f91d7a565/image.png" alt=""></li>
</ul>
<p>위 순서를 토대로 작성한 API 명세는 다음과 같다.
<img src="https://velog.velcdn.com/images/jooo_ii/post/ae47a080-bf99-455f-8d57-abd0bf46af0e/image.png" alt=""></p>
<p>API 명세 작성 내용은 팀마다 다르기 때문에 어떤 것이 정답이라고 할 수 없다. 따라서 본인 팀에 잘 맞는 API 명세를 따라가는 것이 맞다.</p>
<hr>
<h2 id="3-erd-설계-및-db-구조-수립">3. ERD 설계 및 DB 구조 수립</h2>
<p>대망의 ERD 및 DB 설계 단계이다. 실제로도 오래 걸리고 고민도 오래했던 단계였다. </p>
<h3 id="erd란">ERD란?</h3>
<p><strong>ERD</strong>는 E-R 다이어그램을 말하며, 개체 속성과 개체 간 관계를 그림으로 표현한 것이다.</p>
<p>우리 팀은 ERD 설계는 DB Diagram.io 툴을 사용했고, DB는 MySQL을 사용했다.</p>
<p><a href="https://dbdiagram.io/">DB Diagram.io</a></p>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/a3d37003-317c-46f2-bf6e-fcfa811d89cb/image.png" alt=""></p>
<p>예전에는 ERDCloud를 썼었으나, dbdiagram.io의 UI가 더 깔끔해보여서 dbdiagram.io을 채택했다.</p>
<p>실제로도 DBML을 작성만 하면 ERD를 짜주는데 UI가 깔끔해서 보기 편했다.</p>
<hr>
<h2 id="4-백엔드-폴더-구조">4. 백엔드 폴더 구조</h2>
<h3 id="controller">Controller</h3>
<ul>
<li>클라이언트 요청(Request)을 받아 응답(Response)을 반환하는 <strong>API 진입점</strong>이다.</li>
<li>해당 폴더에서 API를 정의한다.</li>
</ul>
<h3 id="dto">DTO</h3>
<ul>
<li><strong>데이터 전송 객체</strong>이다.<ul>
<li>Request와 Response에 따라 파일을 구분해서 정의한다.</li>
</ul>
</li>
<li>계층 간 데이터만 옮긴다.</li>
</ul>
<h3 id="service">Service</h3>
<ul>
<li><strong>실제 비즈니스 로직(핵심 기능)</strong>을 수행한다.</li>
<li>controller에서 전달받은 요청 데이터를 검증하고, 필요한 데이터를 Mapper(or Repository)를 통해 가져와 처리한다.</li>
<li>주요 도메인별 로직을 담당한다.</li>
</ul>
<h3 id="repository-혹은-mapper">Repository (혹은 Mapper)</h3>
<ul>
<li><strong>DB 접근 담당</strong>이다. (데이터 CRUD 수행)</li>
<li>SQL을 실행하고, 그 결과를 DTO나 Entity 형태로 반환한다.</li>
<li>우리 미니프로젝트에서는 Mybatis를 사용하기 때문에 Mapper를 사용한다.</li>
</ul>
<h3 id="domain">Domain</h3>
<ul>
<li><p>프로젝트의 <strong>주요 기능 단위(업무 영역)</strong> 를 기준으로 코드를 모아둔 폴더이다.</p>
</li>
<li><p>예를 들어 users, todos, goals, calendars 등이 각각 하나의 도메인이 된다.</p>
</li>
<li><p>각 도메인 폴더 내부에는 해당 기능에 필요한 controller, service, mapper, dto, entity 등이 함께 포함된다.</p>
</li>
<li><p>도메인 단위로 관리하면 기능별로 코드가 구분되어 유지보수성과 확장성이 높아진다.</p>
</li>
</ul>
<h3 id="config">Config</h3>
<ul>
<li><p><strong>프로젝트 전역 설정 폴더</strong>이다.</p>
</li>
<li><p>Swagger, CORS, DB, 시큐리티 설정 등을 위치한다</p>
</li>
</ul>
<h3 id="applicationyml">application.yml</h3>
<ul>
<li>DB 연결, 포트, Swagger 설정 등 환경설정 파일이다.</li>
</ul>
<p>이를 바탕으로 우리 프로젝트의 폴더 구조는 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/9882bc3f-5f15-4074-82e8-cd6621fdc9ab/image.png" alt=""></p>
<hr>
<h2 id="5-swagger-적용">5. Swagger 적용</h2>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/f961de32-c348-48ff-b088-6859906ca650/image.png" alt=""></p>
<h3 id="swagger란">Swagger란?</h3>
<p>Swagger는 RestAPI를 자동으로 문서화해주며, 개발자가 편리하게 API를 호출하고 테스트 할 수 있는 도구이다.</p>
<h3 id="swagger-적용-순서">Swagger 적용 순서</h3>
<h4 id="1-buildgradle에-의존성을-추가한다">1) build.gradle에 의존성을 추가한다.</h4>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/1aea71ba-efcd-4df3-b348-cca9311c9b40/image.png" alt=""></p>
<pre><code class="language-java">implementation &#39;org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2&#39;}</code></pre>
<h4 id="2-swaggerconfig-을-생성한다">2) SwaggerConfig 을 생성한다.</h4>
<pre><code class="language-java">package com.ureca.fitlog.config;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.Contact;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {

    @Bean
    public OpenAPI fitlogOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title(&quot;FitLog API 문서&quot;)
                        .description(&quot;FitLog API 명세서&quot;)
                        .version(&quot;v1.0.0&quot;)
                        .contact(new Contact()
                                .name(&quot;URECA Team&quot;)
                                .email(&quot;ureca@fitlog.com&quot;)));
    }
}</code></pre>
<h4 id="3-httplocalhost8080swagger-uiindexhtml-에서-swagger가-잘-등록됐는지-확인한다">3) <a href="http://localhost:8080/swagger-ui/index.html#">http://localhost:8080/swagger-ui/index.html#</a> 에서 Swagger가 잘 등록됐는지 확인한다.</h4>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/d03039ed-f5cf-4c19-a12a-62c3bd176eea/image.png" alt=""></p>
<p>우리 팀의 Swagger</p>
<h4 id="4-열심히-api를-개발한다-ㅎㅎ">4. 열심히 API를 개발한다. ㅎㅎ</h4>
<hr>
<p>위처럼 현재까지의 &#39;프로젝트 첫 세팅 ~ API 개발&#39; 단계를 정리해보았다.</p>
<p>그간 프로젝트에서 프론트엔드는 많이 해봤으나 백엔드쪽은 아예 시도조차 안하고 있었다.</p>
<p>그렇기에 이번에 하는 미니 프로젝트에서 ERD 설계나 DB 설계, API 구현 등을 하려고 하니 너무 어려웠다. 이 과정에서 백엔드 개발자들은 대체 뭘까.. 라는 생각이 정말 많이 들었다.</p>
<p>그래도 또 DB 설계까지는 하고 나니 아직까지는 API를 만드는 과정이 신기해서 자주 들여다보게 되는 것 같다.</p>
<p>사실 아직까지는 GPT의 도움을 받아 API 개발 코드를 짜고 있으나, 지금은 그 코드를 보면서 이해하는 것이 정말 중요한 단계라 생각한다.</p>
<p>지금 기초를 탄탄하게 잡아놔야 나중에 AI에 휘둘리지 않으니까 열심히 해놔야겠다는 생각이 들었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[29~30일차] Spring, RESTful, CRUD 설계, MyBatis]]></title>
            <link>https://velog.io/@jooo_ii/2930%EC%9D%BC%EC%B0%A8-Spring-RESTful-CRUD-%EC%84%A4%EA%B3%84-MyBatis</link>
            <guid>https://velog.io/@jooo_ii/2930%EC%9D%BC%EC%B0%A8-Spring-RESTful-CRUD-%EC%84%A4%EA%B3%84-MyBatis</guid>
            <pubDate>Sat, 31 Jan 2026 17:44:52 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jooo_ii/post/aaffb8f1-c6e7-4604-bd4d-2474b18d880a/image.png" alt=""></p>
<h1 id="29일차">29일차</h1>
<h2 id="spring-정의">Spring 정의</h2>
<p>엔터프라이즈용 Java 애플리케이션 개발을 편하게 할 수 있게 해주는 오픈소스 경량급 애플리케이션 프레임워크이다.</p>
<h2 id="spring-boot">Spring Boot</h2>
<p>웹 애플리케이션을 쉽고 빠르게 만들 수 있도록 도와주는 자바의 웹 프레임워크이다.</p>
<p>스프링 프레임워크에 톰캣(Tomcat)이라는 서버를 내장하고 여러 편의 기능을 추가한 것이다.
<img src="https://velog.velcdn.com/images/jooo_ii/post/398daca2-49c2-4be0-b938-cb878b0906c6/image.png" alt=""></p>
<h2 id="스프링의-대표적-특징-4가지">스프링의 대표적 특징 4가지</h2>
<h3 id="제어-역전-ioc-inversion-of-control">제어 역전 (IoC, Inversion of Control)</h3>
<p>다른 객체를 직접 생성하거나 제어하는 것이 아니라, 외부에서 관리하는 객체를 가져와서 사용하는 것이다. 이는 객체의 생명주기 및 의존성 관리를 담당하는 IoC 컨테이너를 제공한다.</p>
<pre><code class="language-java">public class A {
  private B b; // 코드에서 객체를 생성하지 않음. 어디선간 받아온 객체를 b에 할당
 }</code></pre>
<br/>


<h3 id="의존성-주입-di-dependency-injection">의존성 주입 (DI, Dependency Injection)</h3>
<p>제어 역전을 구현하기 위해 사용하는 방법이며, 어떤 클래스가 다른 클래스에 의존한다는 뜻이다.
스프링은 의존성 주입을 통해 객체 간 관계를 설정하며, 애플리케이션의 결합도를 낮추고 유연성과 테스트 용이성을 향상시킨다.</p>
<p>이때 사용되는 개념이 빈 (Bean)인데, <code>@Autowired</code>라는 anntation으로 스프링 컨테이너에 있는 빈을 주입하는 역할을 한다.</p>
<pre><code class="language-java">public class A {
  @Autowired // A에서 B를 주입받음
  B b;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/8b4aeed6-9def-41a3-963f-17f4beefa7a2/image.png" alt=""></p>
 <br/>

<h3 id="aop-지원-관점-지향-프로그래밍-aspect-oriented-programming">AOP 지원 (관점 지향 프로그래밍, Aspect Oriented Programming)</h3>
<p>프로그래밍에 대한 관심을 핵심 관점, 부가 관점으로 나누어서 관심 기준으로 모듈화하는 것이다.</p>
 <br/>


<h3 id="이식-가능한-서비스-추상화-psa-portable-service-abstraction">이식 가능한 서비스 추상화 (PSA, Portable Service Abstraction)</h3>
<p>스프링에서 제공하는 다양한 기술들을 추상화해 개발자가 쉽게 사용하는 인터페이스를 의미한다. 
간단히 말하면, 어느 기술을 사용하던 일관된 방식으로 처리하도록 하는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/30536fb6-553f-4b62-ae99-2821460130fc/image.png" alt=""></p>
<p>이렇게 스프링부트의 기본적인 특징을 알아봤으니, Annotation에 대하여 알아보자.</p>
<h2 id="annotation">Annotation</h2>
<h3 id="정의">정의</h3>
<p>자바 소스 코드에 추가하여 사용할 수 있는 메타데이터의 일종이다. 보통 @ 기호를 앞에 붙여 사용한다.</p>
<p>즉, @를 이용하여 소스코드가 컴파일되거나 실행될 때 컴파일러 및 다른 프로그램에게 필요한 정보를 전달해주는 문법 요소이다.</p>
<h3 id="종류">종류</h3>
<p><strong>1) Bean 등록</strong></p>
<p><strong>1-1) 등록 방식</strong></p>
<ul>
<li><strong>자동등록</strong>
<code>@Component</code> : 일반적인 스프링 빈
<code>@Repository</code> : 비즈니스 로직 담당 클래스
<code>@Service</code> : DB 접근 클래스
<code>@Controller</code> : 웹 요청 처리 클래스
 -&gt; 클래스 선언부 위에 작성한다.
 -&gt; 해당 클래스의 객체를 bean으로 등록한다.</li>
</ul>
<ul>
<li><strong>수동등록</strong>
<code>@Bean</code> : 메서드 선언부에 작성
※ <code>@Configuration</code> : 스프링 설정 클래스 표시용. 최상단에 작성한다.</li>
</ul>
<br/> 

<p><strong>1-2) @Scope : Bean의 life cycle 설정</strong></p>
<p><strong>종류</strong></p>
<ul>
<li>singleton<ul>
<li>default로 설정된다.</li>
<li>컨테이너에 단 하나의 객체만 생성해서 제공한다. =&gt; 동일한 객체 사용</li>
</ul>
</li>
</ul>
<ul>
<li>prototype<ul>
<li>요청 시마다 매번 객체를 생성해서 제공한다.</li>
</ul>
</li>
</ul>
<ul>
<li>request<ul>
<li>웹에서 새로운 request일 때마다 객체를 생성해서 제공한다.</li>
</ul>
</li>
</ul>
<ul>
<li>session<ul>
<li>웹에서 새로운 session일 때마다 객체를 생성해서 제공한다.</li>
</ul>
</li>
</ul>
<br/>

<p><strong>2) DI 관련</strong>
<code>@Autowired</code> : 속성, 생성자, setter 메서드, 일반 메서드 위에 선언. Spring에서 지원 (객체끼리 순환참조 발생 가능성을 줄이기 위해 Autowried로 주입받는 것을 더 선호한다.)</p>
<p><code>@Resource(name=&quot;bean 이름&quot;)</code> : 속성, setter 메서드 위에 선언.</p>
<p><code>@Inject</code> : 속성, 생성자, setter 메서드, 일반 메서드 위에 선언. JAVA에서 지원</p>
<p><code>@Qualifier(&quot;bean 이름&quot;)</code> : @Autowired로 주입할 때, 동일한 타입의 객체가 여러 개일 때 name으로 구별한다.</p>
 <br/>


<p><strong>3) AOP 관련</strong>
<code>@Aspect</code>, <code>@Pointcut</code>, 어드바이서(<code>@Before</code>, <code>@After</code>, <code>@AfterReturning</code>, <code>@AfterThrowing</code>, <code>@Around</code>), <code>@Transactional</code></p>
 <br/>



<p><strong>4) Web</strong></p>
<p><code>@RequestMapping</code> : url mapping (모든 요청 방식을 처리해준다)</p>
<ul>
<li>@GetMapping, @PostMapping, @PutMapping, @DeleteMapping</li>
</ul>
<p><code>@RequestParam</code> : String, Primitive, Map에 대한 요청 데이터 지정</p>
<p><code>@ModelAttribute</code> : DTO에 대한 요청 데이터 지정</p>
<p><code>@ExceptionHandler</code> : 에러 처리하는 함수 선언</p>
<p><code>@ControllerAdvice</code> : 에러 처리하는 class 선언</p>
<p><code>@PathVariable</code> : 비동기 통신으로 전달된 get, delete 방식의 요청 데이터를 url에서 추출해 올 때 지정</p>
<p><code>@RequestBody</code> : 비동기 통신으로 전달된 put, post 방식의 요청 데이터 추출</p>
<p><code>@ResponseBody</code> : 응답을 특정 url로 이동하지 않고 직접 출력</p>
<p><code>@RestController</code> : Restful API를 위한 Controller 선언</p>
 <br/>

<p><strong>5) Spring Boot</strong>
<code>@SpringBootApplication</code> : spring boot를 실행</p>
<p><code>@Component(basePackages=&#39;&#39;)</code> : base package 설정</p>
<p><code>@MapperScan(basePackages = { &quot;com.ssafy.**.model.dao&quot; })</code> : mybatis Mapper에 대한 basePackages 설정</p>
 <br/>


<p><strong>6. Controller 계열</strong>
<code>@RestController</code></p>
<ul>
<li>Restful service를 위한 controller이다.</li>
<li>모든 메서드의 응답을 @ReponseBody를 붙여주는 효과가 있다.</li>
</ul>
<p><code>@RequestMapping</code></p>
<ul>
<li>Restful에서 서비스할 자원(Domain)명을 붙인다. 실질적인 API 요청 url이라 보면 된다.</li>
<li>형식: @RequestMapping(&quot;/요청명&quot;)</li>
</ul>
<p><code>@CrossOrigin</code></p>
<ul>
<li>CORS 요청에 대한 승인이다.</li>
<li>origins = {&quot;*&quot;} : 요청하는 모든 url, 메서드를 승인한다.
 -&gt; 보안에 취약하므로 잘 사용하지 않는다.</li>
</ul>
<hr>
<h2 id="restful">RESTful</h2>
<h3 id="정의-1">정의</h3>
<p>REST (Representational State Transfer) 아키텍처 스타일을 따르는 API이다. </p>
<p>웹의 기본 원칙 (HTTP 프로토콜, URI, 메서드 등)을 활용하여 시스템 간 데이터를 주고받는 방식을 정의한 설계 방식이며, 즉 인터넷 상에서 자원(Resource)을 주고받기 위한 간단하고 표준화된 방법이다.</p>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/9f65b6a6-263a-4e87-8ea6-06487d465720/image.png" alt=""></p>
<h3 id="구성요소">구성요소</h3>
<h4 id="1-자원-resource">1. 자원 (Resource)</h4>
<ul>
<li>REST에서는 모든 데이터를 자원으로 취급하며, URI (Uniform Resource Identifier)로 식별한다.</li>
</ul>
<h4 id="2-http-메서드">2. HTTP 메서드</h4>
<ul>
<li>자원에 대해 수행할 작업을 정의한다.</li>
<li>GET (조회), POST (생성), PUT (수정), DELETE (삭제)</li>
</ul>
<h4 id="3-상태-코드">3. 상태 코드</h4>
<ul>
<li>요청의 결과를 나타낸다. (200, 201, 400, 404, 500 등)</li>
</ul>
<h4 id="4-헤더-header">4. 헤더 (Header)</h4>
<ul>
<li>요청/응답에 대한 메타데이터를 포함한다. (<code>Content-Type: application/json</code> 등)</li>
</ul>
<h4 id="5-페이로드-payload">5. 페이로드 (Payload)</h4>
<ul>
<li>요청/응답에 포함된 실제 데이터이다.</li>
<li>주로 JSON 또는 XML 형식으로 전송된다.</li>
</ul>
<p>이외의 핵심원칙, 장단점, 모범 설계 사례는 아래 블로그에서 추가적인 정보가 있으니 보면 좋을 것 같다.</p>
<p><a href="https://kdong0712.tistory.com/39">RESTful API란?</a></p>
<hr>
<h2 id="crud-설계">CRUD 설계</h2>
<p><strong>C</strong>reate, <strong>R</strong>ead, <strong>U</strong>pdate, <strong>D</strong>elete의 앞글자를 딴 단어로, 대부분의 애플리케이션에서 기본적인 데이터 관리 작업을 정의한다.</p>
<br/>


<h2 id="ajax를-통한-요청-data-전송-방식">AJAX를 통한 요청 Data 전송 방식</h2>
<h3 id="form">Form</h3>
<ul>
<li>File<ul>
<li>POST</li>
</ul>
</li>
<li>QueryString : @RequestParam, @ModelAttribute를 인자로 해서 받을 수 있다. (url?xxx=..) <ul>
<li>무조건 GET</li>
</ul>
</li>
</ul>
<h3 id="path">Path</h3>
<ul>
<li>@PathVariable (url/data/123)<ul>
<li>GET, DELETE</li>
</ul>
</li>
</ul>
<h3 id="json">JSON</h3>
<ul>
<li>@RequestBody<ul>
<li>POST, PUT</li>
</ul>
</li>
</ul>
<br/>


<h2 id="api-생성-예시-코드">API 생성 예시 코드</h2>
<h3 id="get">GET</h3>
<p>(<a href="http://localhost:8080/ureca/test?id=1&amp;name=ureca&amp;email=ureca@uplus.com">http://localhost:8080/ureca/test?id=1&amp;name=ureca&amp;email=ureca@uplus.com</a>)</p>
<ul>
<li>1개씩 받을 때는 <strong>@RequestParam</strong> 사용</li>
</ul>
<pre><code class="language-java">@GetMapping
public ResponseEntity&lt;String&gt; hello(@RequestParam(value=&quot;id&quot;) String id,
                                @RequestParam(value=&quot;name&quot;) String name,
                                @RequestParam(value=&quot;email&quot;) String email){
    logger.debug(&quot;id:{}&quot;, id );
    logger.debug(&quot;name:{}&quot;, name );
    logger.debug(&quot;email:{}&quot;, email );
    ResponseEntity&lt;String&gt; response = new ResponseEntity&lt;&gt;(&quot;Success&quot;, HttpStatus.OK);
    return response;
}</code></pre>
<ul>
<li><p>여러개 받을 때는 <strong>@RequestParam + Map</strong> 사용</p>
<pre><code class="language-java">@GetMapping
public ResponseEntity&lt;String&gt; hello(@RequestParam Map&lt;String, String&gt; map){
  logger.debug(&quot;id:{}&quot;, map.get(&quot;id&quot;) );
  logger.debug(&quot;name:{}&quot;, map.get(&quot;name&quot;) );
  logger.debug(&quot;email:{}&quot;, map.get(&quot;email&quot;) );
  ResponseEntity&lt;String&gt; response = new ResponseEntity&lt;&gt;(&quot;Success&quot;, HttpStatus.OK);
  return response;
}</code></pre>
</li>
<li><p><strong>@ModelAttribute</strong> (기본적으로 JSON이다)</p>
<pre><code class="language-java">@GetMapping
public ResponseEntity&lt;Member&gt; hello(@ModelAttribute Member member){
  logger.debug(&quot;member:{})&quot;, member);
  logger.debug(&quot;id:{}&quot;, member.getId() );
  logger.debug(&quot;name:{}&quot;, member.getName());
  logger.debug(&quot;email:{}&quot;, member.getEmail() );
  ResponseEntity&lt;Member&gt; response = new ResponseEntity&lt;&gt;(member, HttpStatus.OK);
  return response;
}</code></pre>
</li>
</ul>
<h4 id="get-처리-방식">GET 처리 방식</h4>
<ul>
<li>Point Query (P.K) : 1개 요청 -&gt; @PathVariable</li>
<li>Range Query : 여러개 요청 -&gt; @QueryString</li>
</ul>
<h3 id="post">POST</h3>
<pre><code class="language-java">@PostMapping
public ResponseEntity&lt;String&gt; regist(@RequestBody Book book) {
    logger.debug(&quot;regist-books:{}&quot;, book);
    service.insert(book);
    return new ResponseEntity&lt;String&gt;(SUCCESS, HttpStatus.CREATED);
}</code></pre>
<h3 id="put">PUT</h3>
<pre><code class="language-java">@PutMapping
public ResponseEntity&lt;String&gt; update(@RequestBody Book book) {
    logger.debug(&quot;update-book:{}&quot;, book);
    service.update(book);
    return new ResponseEntity&lt;String&gt;(SUCCESS, HttpStatus.OK);
}
</code></pre>
<h3 id="delete">DELETE</h3>
<pre><code class="language-java">@DeleteMapping(&quot;/{isbn}&quot;)
public ResponseEntity&lt;String&gt; remove(@PathVariable(&quot;isbn&quot;) String isbn) { 
    logger.debug(&quot;remove-isbn:{}&quot;, isbn);
    service.remove(isbn);
    return new ResponseEntity&lt;String&gt;(SUCCESS, HttpStatus.OK);
}</code></pre>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/1245d30b-9e68-4580-8548-d5c2ae3273b5/image.png" alt=""></p>
<h4 id="🙌🏻-하나의-class로-모든-오류를-처리하기">🙌🏻 하나의 class로 모든 오류를 처리하기</h4>
<p><code>@ControllerAdvice</code>를 통해서 하나의 class에서 모든 에러를 관리할 수 있다.</p>
<pre><code class="language-java">package com.uplus.ureca;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

/**
 * @ControllerAdvice
 * 프로젝트에서 발생하는 모든 오류를 처리하는 기능
 * */
/**
    * error 메시지 처리 시 한글인 경우 깨지므로 한글 처리를 해야한다.
    * Content-type : application/json;charset-UTF-8
    * @ExceptionHandler
    * 해당 컨트롤러에서 발생하는 오류를 처리하는 Annotaion
**/

@ControllerAdvice
public class ExceptionControllerAdvice {    
    private Logger logger = LoggerFactory.getLogger(getClass());

    @ExceptionHandler
     public ResponseEntity&lt;String&gt; handler(Exception e){
         logger.error(&quot;TestController   msg:{}&quot;, e.getMessage());
         e.printStackTrace();
         String msg = &quot;처리 중 오류 발생&quot;;
         if (e instanceof UrecaException) {
             msg = e.getMessage();
        }


         //에러 메세지가 한글인 경우 깨지므로 한글 처리를 위한 응답 헤더 설정 
         HttpHeaders header = new HttpHeaders();
         header.add(&quot;Content-type&quot;, &quot;application/json;charset-UTF-8&quot;);

         return new ResponseEntity&lt;String&gt;(msg, header, HttpStatus.INTERNAL_SERVER_ERROR);
     }
}</code></pre>
<hr>
<h1 id="30일차">30일차</h1>
<h2 id="mybatis-마이바티스">Mybatis (마이바티스)</h2>
<h3 id="정의-2">정의</h3>
<p>개발자들이 SQL 쿼리를 쉽게 작성하고, 관리할 수 있도록 도와주는 <strong>퍼시스턴스 프레임워크</strong>이다.</p>
<p>주로 JPA를 사용하지만, 해당 부트캠프에서는 백엔드 기초만 나가기 때문에 빠르게 할 수 있는 마이바티스로 진도를 나갔다.</p>
<p>SQL Map XML 파일을 통해서 작성하고, 강력한 매핑 기능을 제공하는 장점이 있어서 하나의 파일에서 SQL 작성이 가능하다.</p>
<h3 id="select-예시-구문">Select 예시 구문</h3>
<pre><code class="language-js">&lt;select id=&quot;selectPerson&quot; parameterType=&quot;int&quot; resultType=&quot;hashmap&quot;&gt;
  SELECT * FROM PERSON WHERE ID = #{id}
&lt;/select&gt;</code></pre>
<ul>
<li>id : 메서드 이름</li>
<li>parameterType : 메서드 인자 타입</li>
<li>resultType : 쿼리를 수행한 결과(컬럼 데이타)를 담을 클래스 타입 (void일 경우 생략)</li>
</ul>
<p><img src="blob:https://velog.io/b2c6480a-36ce-4fdf-9faa-c5bfbd55b917" alt="업로드중.."></p>
<h3 id="insert-예시-구문">insert 예시 구문</h3>
<pre><code class="language-js">&lt;insert id=&quot;insert&quot; parameterType=&quot;Book&quot;&gt;
    insert into book (isbn, title, author, price, describ, img)
    values (#{isbn}, #{title}, #{author}, #{price}, #{describ}, #{img})
&lt;/insert&gt;</code></pre>
<h3 id="update-예시-구문">update 예시 구문</h3>
<pre><code class="language-js">&lt;update id=&quot;update&quot; parameterType=&quot;Member&quot;&gt;
    update members
    &lt;set&gt;
        &lt;if test=&quot;name != null&quot;&gt;name = #{name}, &lt;/if&gt;
        &lt;if test=&quot;password != null&quot;&gt;password = #{password}, &lt;/if&gt;
        &lt;if test=&quot;email != null&quot;&gt;email = #{email}, &lt;/if&gt;
        &lt;if test=&quot;address != null&quot;&gt;address = #{address}&lt;/if&gt;
    &lt;/set&gt;
    where id = #{id}
&lt;/update&gt;</code></pre>
<h3 id="delete-예시-구문">delete 예시 구문</h3>
<pre><code>&lt;delete id=&quot;remove&quot; parameterType=&quot;string&quot;&gt;
    delete from book where isbn = #{isbn} 
&lt;/delete&gt;</code></pre><hr>
<h1 id="회고">회고</h1>
<p>이렇게 7일 간의 백엔드 기초 수업이 끝났다. 기간이 짧고 추석 연휴가 있다보니 흐름이 중간에 끊긴 것 같아 아쉬움이 많이 남는 것 같다. 
백엔드는 제공받은 책으로라도 따로 공부를 해서 미니 프로젝트에 참여해야 할 것 같다.
이번 추석 연휴가 길어서 몸과 정신이 느슨해진 것 같아서 빠르게 정신 잡아야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 자바스크립트(Node.js) 알고리즘 문제 입력 처리 정리]]></title>
            <link>https://velog.io/@jooo_ii/%EB%B0%B1%EC%A4%80-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8Node.js-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%AC%B8%EC%A0%9C-%EC%9E%85%EB%A0%A5-%EC%B2%98%EB%A6%AC-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jooo_ii/%EB%B0%B1%EC%A4%80-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8Node.js-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%AC%B8%EC%A0%9C-%EC%9E%85%EB%A0%A5-%EC%B2%98%EB%A6%AC-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sat, 31 Jan 2026 11:48:09 GMT</pubDate>
            <description><![CDATA[<p>매번 문제 풀 때마다 헷갈리던 자바스크립트 입력 처리 방식을 정리해보고자 한다.</p>
<h3 id="1-한-줄-입력-숫자-1개-입력">1. 한 줄 입력 (숫자 1개 입력)</h3>
<pre><code class="language-js">const fs = require(&quot;fs&quot;);
const input = fs.readFileSync(0, &quot;utf-8&quot;).trim();

const n = Number(input);</code></pre>
<h3 id="2-한-줄-입력-공백으로-구분된-숫자-여러-개">2. 한 줄 입력 (공백으로 구분된 숫자 여러 개)</h3>
<pre><code class="language-js">const fs = require(&quot;fs&quot;);
const input = fs.readFileSync(0, &quot;utf-8&quot;).trim().split(&quot; &quot;);

const [n, m] = input.map(Number);</code></pre>
<h3 id="3-여러-줄-입력-첫-줄은-숫자-이후-배열">3. 여러 줄 입력 (첫 줄은 숫자, 이후 배열)</h3>
<pre><code class="language-js">// 입력값
5
1 2 3 4 5
const fs = require(&quot;fs&quot;);
const input = fs.readFileSync(0, &quot;utf-8&quot;).trim().split(&quot;\n&quot;);

const n = Number(input[0]);     // 첫 줄 
const arr = input[1].split(&quot; &quot;).map(Number);    // 두 번째 줄 -&gt; 배열
</code></pre>
<h3 id="4-여러-줄-입력-각-줄이-숫자-하나">4. 여러 줄 입력 (각 줄이 숫자 하나)</h3>
<pre><code class="language-js">// 입력값
5
1
2
3
4
5
const fs = require(&quot;fs&quot;);
const input = fs.readFileSync(0, &quot;utf-8&quot;).trim().split(&quot;\n&quot;).map(Number);

const n = input[0];
const arr = input.slice(1)  // 나머지 줄
</code></pre>
<h3 id="5-여러-줄-입력-2차원-배열">5. 여러 줄 입력 (2차원 배열)</h3>
<pre><code class="language-js">// 입력값
3
1 2 3
4 5 6
7 8 9
const fs = require(&quot;fs&quot;);
const input = fs.readFileSync(0, &quot;utf-8&quot;).trim().split(&quot;\n&quot;);

const n = Number(input[0]);
const arr = input.slice(1).map(line =&gt; line.split(&quot; &quot;).map(Number));

// 결과값
3
[[1,2,3], [4,5,6], [7,8,9]]</code></pre>
<h3 id="6-문자열-여러-줄">6. 문자열 여러 줄</h3>
<pre><code class="language-js">// 입력값
3
apple
banana
cherry
const fs = require(&quot;fs&quot;);
const input = fs.readFileSync(0, &quot;utf-8&quot;).trim().split(&quot;\n&quot;);

const n = Number(input[0]);
const words = input.slice(1);  // 문자열 그대로
</code></pre>
 <br/>

<hr>
<h2 id="총-정리">총 정리</h2>
<ul>
<li><code>fs.readFileSync(0, &quot;utf-8&quot;)</code> : 입력 전체 읽기</li>
<li><code>.trim()</code> : 맨 앞뒤 공백 제거</li>
<li><code>.split(&quot;\n&quot;)</code> : 줄 단위로 나누기</li>
<li><code>.split(&quot; &quot;)</code> : 공백 단위로 나누기</li>
<li><code>.map(Number)</code> : 숫자로 변환</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[16~20일차] 자료구조]]></title>
            <link>https://velog.io/@jooo_ii/1620%EC%9D%BC%EC%B0%A8-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@jooo_ii/1620%EC%9D%BC%EC%B0%A8-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Sat, 31 Jan 2026 11:43:47 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jooo_ii/post/454328a0-1bb2-44b0-9d76-c114141ce4ac/image.png" alt=""></p>
<h1 id="16일차">16일차</h1>
<h2 id="자료구조">자료구조</h2>
<h3 id="list">List</h3>
<p>List는 Java에서 제공하는 Collection 요소 중 하나로, 순서가 있는 데이터의 집합을 다루기 위한 인터페이스이다.</p>
<p>보통 코딩테스트 기준으로 리스트는 ArrayList를 의미한다.</p>
<pre><code class="language-java">ArrayList&lt;Integer&gt; list = new ArrayList&lt;&gt;();</code></pre>
<p><strong>배열</strong>과 <strong>ArrayList</strong>의 가장 큰 차이는 아래와 같다.</p>
<ul>
<li><p>배열 : 크기가 고정되어 있어서 데이터를 삽입하거나 삭제할 수 없다.</p>
</li>
<li><p>ArrayList : 크기가 가변적이며, 새 데이터의 삽입 or 기존 데이터 삭제가 가능하다.</p>
</li>
</ul>
<p>ArrayList는 크기가 가변적이며 데이터의 가공이 가능하다는 장점이 있지만, 새 데이터를 맨 뒤에 추가할 때는 시간복잡도가 O(1)이며, 기존 데이터 삭제 및 데이터 중간 삽입 시 시간복잡도가 O(N)까지 커질 수 있다. </p>
<p>그럼에도 배열은 시간복잡도가 O(1) 이기 때문에 자주 사용한다.</p>
<hr>
<p>여기서 잠깐, 시간복잡도의 개념을 잠깐 알아보자</p>
<p><strong>시간복잡도</strong> 란?</p>
<h3 id="개념">개념</h3>
<p>알고리즘의 성능을 나타내는 지표로, 입력 크기에 대한 연산 횟수의 상한을 의미한다. 낮으면 낮을수록 좋다.</p>
<h3 id="측정-방법">측정 방법</h3>
<p>알고리즘이 시작한 순간부터 결과값이 나올 때까지의 연산 횟수를 구한다.
만약 결과값을 구하기 위해 총 N번의 수행이 필요하다면, O(N)이 되는 것이다.
이에 반해 수행을 딱 1번만 필요하다면, O(1)이 되는 것이다.</p>
<p>예를 들어, 별 찍기 문제를 보자.</p>
<p>별 찍기 문제는 별을 점진적으로 찍어내서 N까지 찍어내는 것이다.</p>
<p>그럼 연산 횟수가 1, 2, 3, 4, ..., N-1, N 까지 가기 때문에 ( 1 + 2 + 3 + .. + N-1 + N ) / 2 = N(N-1) / 2 이다.</p>
<p>시간복잡도는 최고차항에서 계수를 빼고 계산하기 때문에 별 찍기 문제의 시간 복잡도는 O(N^2)이 된다.</p>
<p>또, 주로 특정 결과값을 도출하기 위해 특정값을 반씩 줄이는 동작을 한다면 시간복잡도는 O(logN)이다.</p>
<table>
  <tr>
    <td align="center">
      <img
        src="https://velog.velcdn.com/images/jooo_ii/post/b98b61a5-7c81-430f-bf73-f4dbccd741f8/image.png"
        width="350"
      />
      <br />
      <sub><b>그림 1.</b> 시간 복잡도(Big-O) 비교</sub>
    </td>
    <td align="center">
      <img
        src="https://velog.velcdn.com/images/jooo_ii/post/642abe05-c009-452c-8c34-ea87c2761e56/image.png"
        width="350"
      />
      <br />
      <sub><b>그림 2.</b> Big-O 표기법 성장 곡선</sub>
    </td>
  </tr>
</table>

<p style="font-size: 12px; color: gray; text-align: right;">
  출처:
  <br />
  1. <a href="https://wikidocs.net/232020" target="_blank">https://wikidocs.net/232020</a>
  <br />
  2. <a href="https://velog.io/@zxcvbnm5288/Nov.-10-2020-Big-O-natation%EB%B9%85%EC%98%A4-%ED%91%9C%EA%B8%B0%EB%B2%95" target="_blank">
    https://velog.io/@zxcvbnm5288/Nov.-10-2020-Big-O-natation
  </a>
</p>



<hr>
<p>List의 index는 0 ~ size()-1 이며, 중간 삽입할 수 있는 index는 0 ~ size() 까지이다.</p>
<p>ArrayLIst의 주된 메소드는 다음과 같다.</p>
<ul>
<li><p>추가 : <code>add()</code></p>
</li>
<li><p>접근 : <code>get()</code></p>
</li>
<li><p>삭제 : <code>remove()</code></p>
</li>
<li><p>유용한 메소드 :</p>
<ul>
<li><p><strong>배열</strong> : 전체 개수 - <code>length</code>, 모든 데이터 정렬 - <code>sort</code>, 모든 데이터를 string으로 반환 - <code>toString()</code></p>
</li>
<li><p><strong>ArrayList</strong> : 전체 개수 - <code>size()</code>, 빈 값 여부 반환 - <code>isEmpty()</code>, 모든 데이터 정렬 - <code>sort()</code></p>
</li>
</ul>
</li>
</ul>
 <br/>

<h3 id="set">Set</h3>
<p>Set은 특정한 값들을 저장하는 추상자료형이다.</p>
<p>Set은 데이터를 저장하는 순서가 없고, 중복된 값을 저장할 수 없다. 이는 list와는 완전히 반대되는 성격을 띄고 있다.</p>
<p>Set 자체는 인터페이스로 제공되며, HashSet, TreeSet, LinkedHashSet이 있다.</p>
<ul>
<li><p>HashSet : Set의 대표 클래스이며, 중복된 값을 저장할 수 없고, 저장 순서도 없다.</p>
</li>
<li><p>TreeSet : 데이터가 오름차순으로 정렬되어 저장된다.</p>
</li>
<li><p>LinkedHashSet : 데이터가 입력된 순서대로 저장된다.</p>
</li>
</ul>
<pre><code class="language-java">HashSet&lt;String&gt; set1 = new HashSet&lt;&gt;();</code></pre>
<br/>


<h3 id="map">Map</h3>
<p>Map은 유일한 key 값으로 value를 관리하는 자료구조이다. key 값은 유니크해야 하며, 검색이 가장 빠른 자료구조이다.</p>
<p>주요 구현 클래스로는 HashMap, TreeMap, LinkedHashMap 등이 있다. </p>
<p>Map은 인터페이스이므로 직접 인스턴스를 생성할 수 없기에 new HashMap과 같이 구체적인 구현 클래스의 인스턴스를 생성해야 한다.</p>
<pre><code class="language-java">// Map
Map&lt;String, Object&gt; map = new HashMap&lt;String, Object&gt;();

// HashMap - 좀 더 구체적이다
HashMap&lt;String, Object&gt; hashMap = new HashMap&lt;String, Object&gt;();
</code></pre>
<h4 id="데이터-검색-방법">데이터 검색 방법</h4>
<p>key 값으로 일치하는 key가 있는지 확인하고, 일치하는 키를 찾으면 key-value 값을 출력한다.</p>
<hr>
<h1 id="17일차">17일차</h1>
<h2 id="자료-구조의-종류">자료 구조의 종류</h2>
<h3 id="선형-자료구조">선형 자료구조</h3>
<p>이전 데이터와 다음 데이터가 1:1로 구성된 자료구조이다.</p>
<p>종류 : 배열, Stack, Queue, LinkedList</p>
<h3 id="비선형-자료구조">비선형 자료구조</h3>
<p>이전 데이터와 다음 데이터가 1:N으로 N:1, N:M으로 구성된 자료구조이다.</p>
<p>종류 : Tree, Graph</p>
<hr>
<h2 id="그래프-graph">그래프 (Graph)</h2>
<h3 id="개념-1">개념</h3>
<p>그래프는 정점(Node, Vertex)과 간선(Edge)를 이용한 비선형 데이터 구조이다.
간선은 두 정점을 연결하는 선으로 두 정점이 연결되어 있다를 의미한다. 
탐색 방법에는 BFS, DFS가 있다.
<img src="https://velog.velcdn.com/images/jooo_ii/post/41d261e6-4a0d-41d7-92bc-e9cbf70ba04b/image.png" alt=""></p>
<h4 id="종류">종류</h4>
<ul>
<li>무향 (양방향) 그래프 : 방향 X</li>
<li>유향 (단방향) 그래프 : 방향 O</li>
</ul>
<p>※ 양방향이고, Cycle이 있는 경우 방문체크가 필수이다. (visited boolean 배열 사용)</p>
<h4 id="가중치-비용">가중치 (비용)</h4>
<ul>
<li><p>간선에 가중치가 있는 경우 : A -&gt; B로 이동하는 거리값, 비용을 구한다.</p>
</li>
<li><p>간선에 가중치가 없는 경우 : 연결되어 있다는 표시로 1, true로 표현 / 연결되어 있지 않다는 표시로 0, false로 표현한다.</p>
</li>
</ul>
<hr>
<h2 id="너비-우선-탐색-bfs-breadth-first-search">너비 우선 탐색 (BFS, Breadth First Search)</h2>
<h3 id="개념-2">개념</h3>
<p>시작 노드를 기준으로 거리가 가장 가까운 노드를 우선 방문하는 방식이다.</p>
<h3 id="순서">순서</h3>
<ol>
<li><p>루트 노드부터 시작해서 자식 노드를 차례대로 방문한다 ( = 가까운 노드를 방문한다)</p>
</li>
<li><p>Queue 자료구조를 이용해서 해당 노드의 인접된 노드를 처리한다 </p>
</li>
<li><p>visited 배열에 방문체크를 한다</p>
</li>
<li><p>모든 노드들을 방문한다</p>
</li>
<li><p>더 이상 방문할 노드가 없으면 depth 를 내려간다</p>
</li>
<li><p>해당 depth의 인접한 모든 노드들을 우선 방문한다</p>
</li>
</ol>
<h3 id="시간복잡도">시간복잡도</h3>
<ul>
<li><p>인접 행렬로 구현 : O(N^2)</p>
</li>
<li><p>인접 리스트로 구현 : O(N+E)</p>
</li>
</ul>
<hr>
<h2 id="깊이-우선-탐색-dfs-depth-first-search">깊이 우선 탐색 (DFS, Depth First Search)</h2>
<h3 id="개념-3">개념</h3>
<p>현재 탐색 중인 노드를 기준으로 연결된 노드를 즉시 방문하는 방식이다. 주로 재귀함수를 사용해서 해결한다.</p>
<h3 id="순서-1">순서</h3>
<ol>
<li><p>루트 노드부터 시작해서 자식 노드를 모두 탐색한다</p>
</li>
<li><p>Stack 자료구조를 이용해서 해당 노드를 저장한다</p>
</li>
<li><p>더 이상 방문할 노드가 없으면 다시 돌아와서 자식 노드를 방문한다</p>
</li>
</ol>
<h3 id="시간복잡도-1">시간복잡도</h3>
<ul>
<li>O(N+E)
<img src="https://velog.velcdn.com/images/jooo_ii/post/492905f8-dda1-49bd-94c5-6b4f00efa019/image.png" alt=""><p style="font-size: 12px; color: gray; text-align: right;">
출처:
<br />
1. <a href="https://wikidocs.net/232020" target="_blank">https://velog.io/@vagabondms/DFS-vs-BFS</a>
</p>

</li>
</ul>
<p>둘 다 비슷하지만, <strong>DFS</strong>는 <strong>모든 경우의 수</strong>를 구하는 문제일 때, <strong>BFS</strong>는 <strong>최단 경로</strong>를 찾는 문제일 때 사용하면 된다.</p>
<h4 id="최단거리-최소비용">최단거리, 최소비용</h4>
<ul>
<li><p>가중치가 없을 경우 : BFS의 시간복잡도는 O(N)이다.</p>
</li>
<li><p>가중치가 있는 경우 : DFS = BFS 이며, 시간복잡도는 O(3^N-2*M-2)이다.</p>
</li>
</ul>
<hr>
<h1 id="19일차">19일차</h1>
<h2 id="서로소">서로소</h2>
<h3 id="개념-4">개념</h3>
<p>서로소 집합(겹치지 않는 집합)끼리 나눠진 원소를 처리하기 위한 자료구조이다. 유니온-파인드 알고리즘을 사용하여 문제를 해결한다.</p>
<h2 id="유니온-파인드-알고리즘">유니온-파인드 알고리즘</h2>
<p>이 알고리즘은 루트 노드를 find로 탐색하고, union으로 합치는 알고리즘이다.</p>
<h2 id="find-연산-찾기-연산">find 연산 (찾기 연산)</h2>
<p>특정 노드의 루트 노드가 무엇인지 탐색하는 방법이다. 보통 특정 노드가 같은 집합에 있는지 확인할 때 사용한다. 이때 연산은 재귀함수로 구현한다.</p>
<h3 id="순서-2">순서</h3>
<ol>
<li><p>현재 노드의 부모 노드를 확인한다</p>
</li>
<li><p>부모 노드를 확인하다가 부모 노드 = 루트 노드이면 find 연산을 종료한다</p>
</li>
</ol>
<p>이 연산의 최악의 시간복잡도는 O(N)이 될 수 있다. 이를 개선하기 위해 경로 압축을 활용해서 해결할 수 있다.</p>
<p>경로 압축이란 집합을 구성하는 트리를 평평하게 만들어서 트리의 depth를 줄이는 방법이다. </p>
<h2 id="union-연산-합치기-연산">union 연산 (합치기 연산)</h2>
<p>두 집합을 하나로 합치는 연산이며, 이는 두 집합의 루트 노드를 같게 하는 방법이다. </p>
<h3 id="순서-3">순서</h3>
<ol>
<li><p>주어진 두 노드의 루트 노드를 찾기 연산(find)를 통해 찾는다</p>
</li>
<li><p>루트 노드를 찾으면 두 루트 노드를 같에 한다 (이때 어떤 노드로 합치든 상관없다)</p>
</li>
</ol>
<pre><code class="language-java">public class DisjointSet {
    static int N;
    static int[] parents;

    /*
     * 모든 원소들에 대한 초기 값을 설정 하는 함수
     * - 자기 자신을 root로 설정 한다. 
     */
    public static void make() {
        for (int i = 0; i &lt; N; i++) {
            parents[i] = i;
        }
    }

    /*
     * 인자로 전달 받은 원소(v)의 root를 찾는 기능
     */
    public static int find(int v) {
        // root를 찾은 경우 (나 자신 return)
        if(parents[v] == v) return v;    // 내가 root라는 소리

        // v가 root가 아니므로 v의 부모로 가본다.
        return find(parents[v]);
    }


    /*
     * 인자로 받은 두 원소의 집합이 다른 경우 합치는 기능
     */
    public static boolean union(int a, int b) {
        // 두 원소의 root를 찾아서
        int aRoot = find(a);
        int bRoot = find(b);

        // 두 원소의 root가 동일하면  =&gt; 같은 집합이므로 union 하면 안됨
        if (aRoot == bRoot) return false;

        // 두 원소의 root가 다르면      =&gt; union
        parents[aRoot] = bRoot;
        return true;
    }

    public static void main(String[] args) {
        N= 6;
        parents = new int [N];
        make();
    }
}
</code></pre>
<p>참고로 find(), union() 메서드의 시간복잡도는 O(logN)이다. 유니온-파인드 알고리즘의 시간복잡도는 O(NlogN)으로 계산해도 된다고 한다.</p>
<hr>
<h1 id="회고">회고</h1>
<p>이번주에는 그래프 위주로 진도를 나갔다. 개념은 접해봤지만, 실제 문제로 푸려고 하니 어렵게 느껴졌다. 그런데 그래프도 재귀함수를 사용해서 풀기 때문에 더욱 재귀함수를 이해하고 있었어야 했다.</p>
<p>재귀도 개념은 쉽지만 구현이 끝도 없이 어려운 것 같다.</p>
<p>정말 많이 풀어보는 방법밖에 없을 것 같다.</p>
<p>수요일에는 LG 유플러스에서 주관하는 유플텍플에 다녀왔다.</p>
<p>실제로 현직자분들과 대화를 나눌 수 있어서 더 뜻 깊은 시간이었다. </p>
<p>나는 이런 곳에 가면 질문할 거리들이 생각이 안나서 옆에서 그냥 듣고 있던 적이 많았는데, 다른 사람들은 폭풍 질문을 하는 것을 보고 자극을 받았고, 나도 무엇이라도 얻고 가야겠다는 생각에 실제로 궁금했던 tailwindCSS를 현업에서도 사용하는지 여쭤보았다.</p>
<p>실제로 이번에 tailwindCSS와 lucide-react를 사용하는 방향으로 리팩토링을 진행했다고 하였다.</p>
<p>현재 프로젝트에서도 tailwindCSS와 lucide-react를 사용해서 구현했는데, 더 적극적으로 사용해도 좋을 것 같다. </p>
<p>특히 lucide-react은 뭔가 실제로 사용하는 지 궁금했다. 왜냐면 icon을 호출해서 사용하는 방식이라서 편하긴 했지만, 실제로 사용되는지 의문이 들었는데 이번 기회에 해소할 수 있어서 좋았다.</p>
<p>그리고 다른 사람들의 질의응답도 들어보니, 성능 개선은 선택이 아닌 필수라는 생각이 들었다. </p>
<p>아옹 더 열심히 해야겠다는 생각이 들었다. 그래도 유플텍플에 가서 많은 자극을 얻고 왔다!</p>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/55d838af-9976-40a9-bb6d-6be51f08d996/image.png" alt=""></p>
<p>제일 좋았던 건 아무래도 경품 당첨? ㅎ_ㅎ</p>
<p>유용하게 잘 쓰겠습니다</p>
<p>유플러스 감사합니다 저를 받아주세요 (제발)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[13~15일차] 알고리즘]]></title>
            <link>https://velog.io/@jooo_ii/1315%EC%9D%BC%EC%B0%A8-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</link>
            <guid>https://velog.io/@jooo_ii/1315%EC%9D%BC%EC%B0%A8-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</guid>
            <pubDate>Sat, 31 Jan 2026 11:19:20 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jooo_ii/post/b67b6525-bfbb-4475-91d3-92525c8de649/image.png" alt=""></p>
<p>지금은 알고리즘 위주라서 알고리즘을 풀기 위한 개념 이해 + 문제 풀이로 구성되어 있다.</p>
<h1 id="13일차">13일차</h1>
<h2 id="순열">순열</h2>
<h3 id="for-중첩">for 중첩</h3>
<ul>
<li><p>for를 중첩해서 순열을 구현하면 nPr에서 n은 랜덤이 가능하지만, r은 고정될 수 밖에 없다.</p>
</li>
<li><p>시간복잡도 : 중복순열(nTTr) -&gt; n^r</p>
</li>
</ul>
<table>
  <tr>
    <td align="center"><b>순열</b></td>
    <td align="center"><b>중복순열</b></td>
  </tr>
  <tr>
    <td>
      <img src="https://velog.velcdn.com/images/jooo_ii/post/415b3231-c011-4558-96da-5561b8f226ed/image.png" width="350"/>
    </td>
    <td>
      <img src="https://velog.velcdn.com/images/jooo_ii/post/4ee62cc9-98b6-42bc-9004-c41fd83e7034/image.png" width="350"/>
    </td>
  </tr>
</table>

<br/>

<h3 id="재귀로-푸는-방법">재귀로 푸는 방법</h3>
<ul>
<li><p>초기화: P(0) or P(r-1) 상관이 없다 (bottom -&gt; up인지 top -&gt; down인지를 결정하는 것이기 때문)</p>
</li>
<li><p>기저(중단) 조건</p>
<ul>
<li><p>0 ~ r-1 까지 (0 &lt; r)</p>
</li>
<li><p>r~1 ~ 0 까지 (r-1 &gt; -1)</p>
</li>
</ul>
</li>
</ul>
<ul>
<li>시간 복잡도가 오래 걸릴 때는 공간 복잡도로 해결할 수 있다.</li>
</ul>
<p><code>boolean[] visited</code></p>
<p>visited 배열을 생성해서 기본값으로 false (사용 x, 중복 x)를 둔 뒤, true (사용 O, 중복 O)가 되면 visited 배열에 저장한다.</p>
<p>이렇게 되면 시간복잡도가 r^2에서 1로 바뀐다. (-&gt; boolean 배열이 true / false 중 어느 것인지 확인만 하면 되니까)</p>
<p>=&gt; 시간복잡도는 n^(r-중복제거) 가 된다</p>
<p>따라서 순열의 시간복잡도 한계치는 아래와 같다.</p>
<pre><code class="language-html">순열 시간복잡도 한계치

- 일반재귀 : 9P9
- swap, n, p : 10P10
- DP : 11P11 이상</code></pre>
<ul>
<li>시간복잡도 총 정리
<img src="https://velog.velcdn.com/images/jooo_ii/post/b3315ce3-86f5-40fe-9265-dbb7e4592e51/image.png" alt=""></li>
</ul>
<hr>
<h2 id="순열과-관련된-문제">순열과 관련된 문제</h2>
<h3 id="swea-6808-규영이와-인영이">SWEA 6808 규영이와 인영이</h3>
<pre><code class="language-java">1~18까지의 합 최대 총합 171

⇒ 86+86정도이기 때문에 무승부는 사실상 없음

- 두 카드의 총합은 (18*19)/2 = 171이므로 무승부는 나오지 않음. 이기거나 지는 상황만 체크한다.
- 입력
    - 규영이의 카드가 입력으로 들어오고, 순서는 고정
    - 인영이의 카드를 규영이가 선택하지 않는 카드로 선택한다.
        - 18개 카드 중 규영이 카드 빼고 선택해야 함
            - 규영이 카드를 입력 받으면 boolean 배열에 규영이 카드 기록 (카드번호가 index)
            - boolean의 false인 index를 인영이의 카드로 사용하기
- 순열 만들기
    - 순열을 다 만든 후에 반복문을 통해 전체 카드에 대한 승패를 결정
</code></pre>
<hr>
<h2 id="비트마스크">비트마스크</h2>
<ul>
<li><p>정수의 이진수 표현을 자료구조로 활용하여, 각 비트의 상태를 통해 집합이나 여러 상태를 효율적으로 표현하고 처리하는 기법</p>
</li>
<li><p>비트연산자 : <code>&amp;</code>, <code>|</code>, <code>shift</code>, <code>XOR</code></p>
</li>
<li><p>0 양수 / 1 음수 로 이루어져 있다.</p>
</li>
</ul>
<br/>

<h3 id="비트마스크를-구하는-방법-2-3번-중요">비트마스크를 구하는 방법 (2, 3번 중요!!)</h3>
<pre><code class="language-java">1. 공집합과 꽉 찬 집합 구하기
     * A = 0;            //32개의 원소가 모두 0이므로 공집합
     * n = 10;             //10개인 원소 (1111111111)
     * A = (1&lt;&lt;n)-1;        //꽉 찬 집합

    0000000001 &lt;&lt; 10 ==&gt;10000000000 
    10000000000 -1 = 1111111111  

2. 특정 위치에 1이 있는지 check로 &amp; 사용 
     *  &amp;, and  : 연산하려는 두 비트가 모두 1일때 1이고 나머지는 0
                 특정 위치에 1이 있는지 체크 용도로 사용,  data &amp; 0 =&gt; 0으로 초기화   

    int a1 = 0b1000;
    int b1 = 0b0010;
    int c1 = 0b1110;

3. 원소 추가 : k번째 위치에 원소를 추가(1로 마스킹)하기 
    * A |= (1&lt;&lt;k)
    * k번째는 뒤에서 부터 세기 (0번째 부터~)

    c1 |= (1&lt;&lt;k);
    System.out.println(Integer.toBinaryString(c1));</code></pre>
<hr>
<h2 id="swap-순열">SWAP 순열</h2>
<ul>
<li><p>원소의 위치를 바꿔가면서 새로운 순열을 만들어주는 것이다</p>
</li>
<li><p>시간을 많이 단축할 수 있다 (n!)
```java</p>
</li>
</ul>
<ol>
<li>depth ~ R-1 까지 swap을 반복한다 (for문)</li>
<li>depth 전 index는 고정이다 (확정)
2-1. 기존의 원소 위치만 교체하므로 중복 검사를 할 필요가 없다.</li>
</ol>
<p>⇒ depth=R까지 반복</p>
<pre><code>
### 특징

- 배열의 첫 원소의 위치부터 순서대로 하나씩 바꾸며 모든 값을 한번씩 swap 한다.

- depth를 기준 인덱스로 하여 depth 보다 인덱스가 작은 값들은 그대로 고정하고 depth 이상의 인덱스들만 swap을 진행한다.

- 순열을 따로 구하지 않아도 된다 -&gt; 원본 배열을 그대로 사용한다

- nPn, nPr 모두 됨. 단 nPr시 전체 배열에서 앞에 r개만 사용

- 단점: 순열이 사전순(오름차순)으로 나오지 않는다. 수행속도 9P9 8ms 안전 10P10 34ms 안전



_**만약 R = 3인 경우)**_
- R ≥ 3 일 때는 마지막은 결국 자기 자신 교체이기 때문에 depth = R까지 갈 필요 없음 (R-1까지만 가면 된다)
- R ≤ 2 일 때는 depth = R까진 가야 됨


```java
public static void main(String[] args) {
    input = new int[] {1,2,3};
    N = input.length;
    R = input.length;

    long start = System.currentTimeMillis();        
    permutation(0); // 0번째 원소부터 뽑기
    long end = System.currentTimeMillis();
    System.out.printf(&quot;순열 수: %d   반복횟수:%d    수행시간: %dms %n &quot;, tc, count, end-start);

     }
    // swap 함수
    private static void swap (int i, int j) {
        int temp = input[i];
        input[i] = input[j];
        input[j] = temp;
    }

    /**
     * @param depth : 순열 배열의 index
     **/
    private static void permutation(int depth) {

    if (depth==R){
        tc++;
        return;
    }

    // depth 이전 인덱스는 고정, depth부터 swap 하기 
    for (int i = depth; i &lt; N; i++) { // i = depth이고, i = 0이 아니기 때문에, depth부터 하기 때문에 시간이 빠름!!!!!
        count++;    // 반복 횟수 세기

        // 두 수를 swap해서 새로운 순열을 만든다.
        swap(i, depth);
        permutation(depth+1);

//         다음 순열을 만들기 위해서 원복
        swap(i, depth);    // 이렇게 해도 바뀜
//        swap(depth, i);
    }
}</code></pre><hr>
<h2 id="조합">조합</h2>
<ul>
<li><p>서로 다른 N개의 원소에서 R개를 중복없이, 순서를 고려하지 않고 선택하는 것 (ex. 로또 뽑기)</p>
</li>
<li><p>특징: 첫번째 인덱스 숫자 &lt; 두번째 인덱스 숫자 &lt; … &lt; n번째 인덱스 숫자</p>
<pre><code>      -&gt; 항상 첫 &lt; 둘 이렇게 숫자가 더 커짐
      =&gt; 중복 없이, 순서를 고려하지 않기 때문에 조합의 특징이 뒤에 있는 원소의 index는 항상 앞에 있는 원소의 index 보다 커야된다.</code></pre></li>
</ul>
<pre><code class="language-java">package combination;

import java.util.Arrays;


public class CombinationTest1 {
    static int tc; // test case
    static int n, r;
    static int[] numbers;
    static int[] input;

    public static void main(String[] args) {
        input = new int[] {1,2,3};
        n = input.length;
        r = 2;
        numbers = new int[r];

        combi(0,0);
    }

    /**
     * @param depth        조합을 뽑아놓은 배열의 index
     * @param start        조합을 시작할 index
     * */
    private static void combi(int depth, int start) {
        if (depth == r) { // 기저조건
            tc++;
            return;
        }
        for (int i = start; i &lt; n; i++) { // 반복조건
            numbers[depth] = input[i];
            combi(depth+1, i+1);

        }
    }

}</code></pre>
<h4 id="-조합-관련-문제">+ 조합 관련 문제</h4>
<ul>
<li>백준 2309 일곱난쟁이 (해결)</li>
</ul>
<pre><code class="language-java">package 코드공유.BOJ;

import java.util.Arrays;
import java.util.Scanner;

public class Main_2309_일곱_난쟁이_김주희 {
    static int n, r;
    static int[] numbers;
    static int[] input;
    static int total;

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        input = new int[9];
        n = input.length;
        r = 7;
        numbers = new int[r];


        for (int i=0; i &lt; n; i++) {
            input[i] = sc.nextInt();
        }    
        combi(0,0);
    }

    private static void combi(int depth, int start) {
        if (depth == r) {
            if (total == 100) {
                Arrays.sort(numbers);

                for (int num : numbers) {
                    System.out.println(num);
                }
                System.exit(0);
            }
            return;
        }


        for (int i=start; i &lt; n; i++) {
            numbers[depth] = input[i];
            total += input[i];
            combi(depth+1, i+1);
            total -= input[i];
        }

    }
}
</code></pre>
<hr>
<h1 id="14일차">14일차</h1>
<p>문제 푸는 데 집중해서 기록해둔게 없다... 아마 조합 문제를 엄청 고민했던 것 같다 ㅜ </p>
<p>공포의 수영장 ..</p>
<p>이후 프론트 대면반 단체 회식이 있어서 대면반 사람들과 친해지게 된 계기가 되어서 너무 좋았고 알찼던 시간이었다.</p>
<p>프로젝트 때문에 맘 편히 즐기진 못했지만 .. ㅜㅡㅜ</p>
<hr>
<h1 id="15일차">15일차</h1>
<p>알고리즘 고수가 되면 풀어 볼 문제</p>
<ol>
<li><p>백준 1062 가르침 (비트마스킹) </p>
</li>
<li><p>프로그래머스 42820 후보키</p>
</li>
</ol>
<br/>

<h2 id="자료구조">자료구조</h2>
<h3 id="선형">선형</h3>
<ul>
<li>데이터와 다음 데이터가 연결된 것 (1개씩 맵핑된다)</li>
</ul>
<h3 id="스택">스택</h3>
<ul>
<li><p>먼저 들어간 값이 가장 나중에 나오는 구조 (LIFO)</p>
</li>
<li><p>주로 괄호 검사기 문제에 주로 활용된다.</p>
</li>
</ul>
<h3 id="큐">큐</h3>
<ul>
<li><p>먼저 들어간 값이 가장 먼저 나오는 구조 (FIFO)</p>
</li>
<li><p>요세푸스 문제가 큐 문제이다.</p>
</li>
</ul>
<hr>
<h2 id="관련-문제">관련 문제</h2>
<h3 id="백준-10799-쇠막대기-stack">백준 10799 쇠막대기 (stack)</h3>
<ul>
<li><p>괄호 짝이 맞으면 레이저를 쏴서 현재 레이저를 쐈을 때 앞에 짝이 안 지어진 &#39;(&#39;의 갯수를 세는 문제이다.</p>
</li>
<li><p>현재 괄호 바로 전 괄호의 모양에 따라서 pop을 할 지, total에 값이 쌓이게 될 지 결정된다.</p>
</li>
<li><p>문제를 해석하는 데 정말 오래 걸렸다.. 그래도 해석만 하면 어느 정도 풀리는 문제였다.</p>
</li>
</ul>
<pre><code class="language-java">package 코드공유.BOJ;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Stack;

public class Main_10799_쇠막대기 {

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader (new InputStreamReader(System.in));
        String line = br.readLine();
        int total = 0;

        Stack&lt;Character&gt; stack = new Stack&lt;&gt;();

        for (int i = 0; i &lt; line.length(); i++) {
            char c = line.charAt(i);
            if (c == &#39;(&#39;) {
                stack.push(c);
            } else if (c == &#39;)&#39;) {
                stack.pop();

                if (line.charAt(i - 1) == &#39;(&#39;) {
                total += stack.size();
                } else {
                    total++;
                }
            } 
        }    
        System.out.println(total);
    }
}</code></pre>
<br/>

<h3 id="백준-1158-요세푸스-문제-queue">백준 1158 요세푸스 문제 (queue)</h3>
<ul>
<li><p>문제 자체는 쉬웠지만, 풀기 어려웠던 문제</p>
</li>
<li><p>K번째 해당하는 숫자를 poll() 하고 그 값을 offer()를 통해 queue 뒤로 삽입한 뒤, poll()한 값을 result 리스트에 담아서 결과를 출력하는 문제이다.</p>
</li>
<li><p>출력 방식 자체도 독특했다.</p>
</li>
</ul>
<pre><code class="language-java">package 코드공유.BOJ;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Scanner;

public class Main_1158_요세푸스_문제 {
    int N;
    int K;

    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        int N = sc.nextInt();
        int K = sc.nextInt();

        Queue&lt;Integer&gt; q1 = new LinkedList&lt;&gt;();

        for (int i = 1; i &lt;= N; i++) {
            q1.offer(i);
        }

        List&lt;Integer&gt; result = new ArrayList&lt;&gt;();

        while(!q1.isEmpty()) {
            for (int j = 0; j &lt; K-1; j++) {
                int var = q1.poll();
                q1.offer(var);
            }
            result.add(q1.poll());
        }

        System.out.print(&quot;&lt;&quot;);
        for (int i = 0; i &lt; result.size(); i++) {
            System.out.print(result.get(i));
            if (i != result.size() - 1) {
                System.out.print(&quot;, &quot;);
            }
        }
        System.out.print(&quot;&gt;&quot;);

    }
}
</code></pre>
<hr>
<h1 id="회고">회고</h1>
<p>드디어 순조부가 끝났나?? 싶긴 한데.. 문제들이 너무 어렵고 헷갈려서 하염없이 문제만 쳐다봤던 날이 많아서 아쉬운 것 같다.</p>
<p>프로젝트가 생각보다 늦게 끝나고 너무 빠듯해서 주말에도 알고리즘 공부할 시간이 없었어서 너무 아쉬웠다 ㅜㅜ</p>
<p>얼른 백엔드 파트 전에는 끝내서 백엔드 때는 진도에 맞춰서 잘 따라갈 수 있도록 해야겠다.</p>
<p>하~~ 지긋지긋한 프로젝트 빨리 끝내고 싶다 이번주 내로 끝내버려야지 .......... 아마도....</p>
<p>사실 이번 프로젝트하면서 백엔드 공부의 중요성을 약간 깨닫게 되어서 빨리 알고리즘 끝내고 백엔드 배워 보고 싶긴 하다</p>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/0469cc8d-031f-4dad-b6e4-3a070559813b/image.png" alt=""></p>
<p>그리고 이번 주 개꿀인 점 : 하루 휴강  🥳🥳🥳🥳🥳</p>
<p>그리고 너무 기대된다 !!!!! 새로운 지식 많이 얻을 것 같다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Sentry] Sentry를 도입해서 에러 추적하기]]></title>
            <link>https://velog.io/@jooo_ii/Sentry-Sentry%EB%A5%BC-%EB%8F%84%EC%9E%85%ED%95%B4%EC%84%9C-%EC%97%90%EB%9F%AC-%EC%B6%94%EC%A0%81%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jooo_ii/Sentry-Sentry%EB%A5%BC-%EB%8F%84%EC%9E%85%ED%95%B4%EC%84%9C-%EC%97%90%EB%9F%AC-%EC%B6%94%EC%A0%81%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 31 Jan 2026 11:12:34 GMT</pubDate>
            <description><![CDATA[<p>현재 진행하고 있는 동아리 홈페이지 제작 중에 Sentry를 미리 도입해서 바로 에러를 추적할 수 있게끔 하기 위해서 Sentry를 도입했습니다. </p>
<h1 id="sentry란">Sentry란?</h1>
<p>애플리케이션에서 오류가 발생하면 알려주는 강력한 에러 트래킹 서비스입니다.</p>
<h2 id="sentry-적용-방법">Sentry 적용 방법</h2>
<ol>
<li>sentry.io에 회원가입을 합니다.</li>
</ol>
<ol start="2">
<li>회원가입 후 &#39;Project&#39;를 생성해서 플랫폼을 선택합니다.</li>
</ol>
<ol start="3">
<li>npm으로 프로젝트에 설치합니다.</li>
</ol>
<p><code>npm install @sentry/nextjs --save</code></p>
<p><code>npx @sentry/wizard@latest -i nextjs</code>
(wizard는 설정할 사항을 y/n로 선택해서 설정할 수 있게 해줍니다.)</p>
<p>wizard 설치 시 아래처럼 나오게 됩니다. 각자 상황에 맞추어 선택하면 됩니다!
<img src="https://velog.velcdn.com/images/jooo_ii/post/fce07972-9b23-4721-99fa-2080c771980f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/b21c0ccf-6a1e-4ccb-91af-103ac914aba3/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/62c0645a-044e-49c6-bfda-68cfbb8a9972/image.png" alt=""></p>
<ol start="4">
<li>sentry에서 발급받은 <code>NEXT_PUBLIC_SENTRY_DNS</code>를 <code>.env.local</code>에 저장해둡니다. </li>
</ol>
<p>(배포 시에는 DNS와 <code>SENTRY_AUTH_TOKEN</code>도 환경변수로 추가해야 합니다.)</p>
<ol start="5">
<li>제공해준 page.tsx로 연결을 확인합니다.</li>
</ol>
<p>아래처럼 나오는 것을 확인할 수 있습니다!
<img src="https://velog.velcdn.com/images/jooo_ii/post/9db04e90-2520-4c1b-90d5-27042e21ca3f/image.png" alt=""></p>
<hr>
<h2 id="파일-설명">파일 설명</h2>
<h3 id="sentryserverconfig">sentry.server.config</h3>
<ul>
<li>서버 사이드에서만 동작하는 Sentry 설정을 적는 곳</li>
<li>App Router의 서버컴포넌트 / API route에서만 적용됨.</li>
<li>예: API 라우트, 서버 컴포넌트, 백엔드 오류 추적</li>
</ul>
<h3 id="sentryedgeconfig">sentry.edge.config</h3>
<ul>
<li>Next.js의 Middleware나 Edge Runtime에서 동작하는 코드용 설정</li>
<li>middleware.ts / edge runtime API에서만 적용됨.</li>
<li>예: Edge Functions, Middleware 오류 추적</li>
</ul>
<h3 id="instrumentation-client">instrumentation-client</h3>
<ul>
<li>클라이언트 사이드(브라우저)에서 동작하는 Sentry 설정을 넣는 파일</li>
<li>클라이언트 초기화 시점에 실행됨</li>
<li>이전 <code>sentry.client.config</code> 파일과 동일 (Next.js 13+ App Router 환경에서 클라이언트 초기화 로직을 이 파일로 통일함)</li>
</ul>
<hr>
<h2 id="logger-level-소개">logger level 소개</h2>
<h3 id="trace">trace</h3>
<ul>
<li>애플리케이션의 실행 흐름과 디버깅 정보 상세히 기록 (디버깅용)</li>
</ul>
<h3 id="debug">debug</h3>
<ul>
<li>개발 단계에서 상세한 정보 기록 (디버깅용)</li>
</ul>
<h3 id="info">info</h3>
<ul>
<li>정보성 메시지 기록</li>
<li>애플리케이션의 주요 이벤트나 실행 상태에 대한 정보 전달</li>
</ul>
<h3 id="warn">warn</h3>
<ul>
<li>경고성 메시지 기록</li>
<li>예상치 못한 문제나 잠재적 오류 상황을 알리는 메시지 (작동은 정상적으로 함)</li>
</ul>
<h3 id="error">error</h3>
<ul>
<li>오류 메시지 기록</li>
<li>심각한 문제, 예외 상황, 애플리케이션 작동에 영향을 미칠 수 있음</li>
</ul>
<h3 id="fatal">fatal</h3>
<ul>
<li>가장 심각한 오류 메시지 기록</li>
<li>애플리케이션 동작을 중단시킬 수 있는 치명적인 오류</li>
</ul>
<p>공식문서 예시)</p>
<pre><code class="language-java">Sentry.logger.trace(&quot;Starting database connection&quot;, { database: &quot;users&quot; });
Sentry.logger.debug(&quot;Cache miss for user&quot;, { userId: 123 });
Sentry.logger.info(&quot;Updated profile&quot;, { profileId: 345 });
Sentry.logger.warn(&quot;Rate limit reached for endpoint&quot;, {
  endpoint: &quot;/api/results/&quot;,
  isEnterprise: false,
});
Sentry.logger.error(&quot;Failed to process payment&quot;, {
  orderId: &quot;order_123&quot;,
  amount: 99.99,
});
Sentry.logger.fatal(&quot;Database connection pool exhausted&quot;, {
  database: &quot;users&quot;,
  activeConnections: 100,
});</code></pre>
<p>실사용 예시) API 작업 시</p>
<pre><code class="language-javascript">onClick={async () =&gt; {
    await Sentry.startSpan(
      {
        name: &quot;Example Frontend/Backend Span&quot;,
        op: &quot;test&quot;,
      },
      async () =&gt; {
        const res = await fetch(&quot;/api/sentry-example-api&quot;);
        if (!res.ok) {
          setHasSentError(true);
          // API 실패를 로그로 기록
          Sentry.logger.fatal(&quot;API call failed from frontend&quot;, {
            status: res.status,
          });
        }
      }
    );

    const error = new SentryExampleFrontendError(
      &quot;This error is raised on the frontend of the example page.&quot;
    );

    // error 수준 로그
    Sentry.logger.error(&quot;Frontend error occurred&quot;, { error });
    throw error; // 해당 코드는 런타임시 잡아서 자동으로 Sentry에 전송
}}</code></pre>
<hr>
<h2 id="logger-vs-captureexception-차이">logger vs captureException 차이</h2>
<h3 id="sentryloggerlevel">Sentry.logger.&lt;level&gt;()</h3>
<ul>
<li>debug/info/warn은 기본 설정에서는 서버로 전송되지 않고, <strong>로컬 SDK</strong> 로깅으로만 남음.</li>
<li>단순히 로그 수준에 맞게 <strong>메시지를 기록</strong> (error/fatal만 Sentry로 전송됨).</li>
</ul>
<h3 id="sentrycaptureexceptionerror--sentrycapturemessagemessage">Sentry.captureException(error) / Sentry.captureMessage(message)</h3>
<ul>
<li>운영 환경에서 반드시 추적하고 싶은 예외는 이걸 써야 함.</li>
<li>무조건 이벤트로 전송해서 Sentry 대시보드에 남김.</li>
</ul>
<p>➡️ 즉, <strong>logger는 관찰/분류용, captureException은 꼭 전송하고 싶은 에러 보고용</strong>입니다.</p>
<hr>
<p>디스코드나 slack으로 연동도 가능하다고 해서 추후엔 연동해보면 좋을 것 같네용</p>
<hr>
<h3 id="참고자료">참고자료</h3>
<p><a href="https://tech.kakaopay.com/post/frontend-sentry-monitoring/#-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A1%9C%EA%B7%B8%EC%97%90-%EB%8C%80%ED%95%9C-%EB%8B%A4%EC%96%91%ED%95%9C-%EC%A0%95%EB%B3%B4-%EC%A0%9C%EA%B3%B5">Sentry로 우아하게 프론트엔드 에러 추적하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[12일차] 재귀함수 알고리즘 문제]]></title>
            <link>https://velog.io/@jooo_ii/12%EC%9D%BC%EC%B0%A8-%EC%9E%AC%EA%B7%80%ED%95%A8%EC%88%98-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@jooo_ii/12%EC%9D%BC%EC%B0%A8-%EC%9E%AC%EA%B7%80%ED%95%A8%EC%88%98-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Sat, 31 Jan 2026 11:02:43 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jooo_ii/post/c26c625a-4561-4138-bbc0-f2ee3c7e2c3e/image.png" alt=""></p>
<h1 id="1-오늘-공부한-내용-📝">[1] 오늘 공부한 내용 📝</h1>
<h2 id="재귀함수-알고리즘-문제">재귀함수 알고리즘 문제</h2>
<h3 id="재귀-함수가-뭔가요">재귀 함수가 뭔가요?</h3>
<ul>
<li>백준 17478</li>
<li>링크: <a href="https://www.acmicpc.net/problem/17478">https://www.acmicpc.net/problem/17478</a></li>
<li>나의 코드<pre><code class="language-java">import java.util.Scanner;
</code></pre>
</li>
</ul>
<p>public class Main {
    static int N;</p>
<pre><code>public static void ans(int i) {

    String u = &quot;____&quot;.repeat(i);

    if (i &lt; N) {
        System.out.printf(&quot;%s\&quot;재귀함수가 뭔가요?\&quot;\n&quot;, u);

        System.out.printf(&quot;%s\&quot;잘 들어보게. 옛날옛날 한 산 꼭대기에 이세상 모든 지식을 통달한 선인이 있었어.\n&quot;, u);
        System.out.printf(&quot;%s마을 사람들은 모두 그 선인에게 수많은 질문을 했고, 모두 지혜롭게 대답해 주었지.\n&quot;, u);
        System.out.printf(&quot;%s그의 답은 대부분 옳았다고 하네. 그런데 어느 날, 그 선인에게 한 선비가 찾아와서 물었어.\&quot;\n&quot;, u);
        ans(i + 1);
        System.out.printf(&quot;%s라고 답변하였지.\n&quot;, u);
    }

    if (i == N) {
        System.out.printf(&quot;%s\&quot;재귀함수가 뭔가요?\&quot;\n&quot;, u);
        System.out.printf(&quot;%s\&quot;재귀함수는 자기 자신을 호출하는 함수라네\&quot;\n&quot;, u);
        System.out.printf(&quot;%s라고 답변하였지.\n&quot;, u);
    }
}

public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    N = sc.nextInt();
    System.out.println(&quot;어느 한 컴퓨터공학과 학생이 유명한 교수님을 찾아가 물었다.&quot;);
    ans(0);
}</code></pre><p>}</p>
<pre><code>
- 풀이과정
재귀함수를 사용해서 문장을 출력하는 문제이다.
이 문제의 킥은 &#39;____&#39;를 i번 반복하는 것이므로 이 부분에 주의해야 한다.
맨 처음에 작성했던 코드는 아래와 같다.

```java
import java.util.Scanner;

public class Main {
    static int N;

    public static void ans(int i) {
        String u = &quot;____&quot;.repeat(N);
        // ... (중략)
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        N = sc.nextInt();
        System.out.println(&quot;어느 한 컴퓨터공학과 학생이 유명한 교수님을 찾아가 물었다.&quot;);
        ans(N);
    }
}
</code></pre><p>이 코드의 문제는 repeat을 N번 반복하고, main()에서 ans(N)을 호출하기 때문에 언더바(_)만 N번 반복하고, 시작을 N에서 출발해버리는 엉뚱한 상황이 되었다. (뭔 N을 그리 좋아하는지..)
문제 해결책이 전혀 떠오르지 않아서 GPT의 도움을 받았다.</p>
<p>이 문제는 어찌저찌 해결하긴 했다. 다른 분들 풀이도 찾아봐야겠다.</p>
<hr>
<h3 id="하노이의-탑">하노이의 탑</h3>
<ul>
<li>백준 1914</li>
<li>링크: <a href="https://www.acmicpc.net/problem/1914">https://www.acmicpc.net/problem/1914</a></li>
<li>코드는 아직 없다 (어려워서 추가 공부해야한다..)</li>
</ul>
<hr>
<h2 id="재귀함수-관련-추가-학습-내용">재귀함수 관련 추가 학습 내용</h2>
<p>재귀함수는 print 위치가 중요하다고 어디선가 들었었는데, 왜 중요한지 이해를 못 하고 있어서 강의를 찾아봤다.</p>
<pre><code class="language-java">public static void 재귀함수 (int i) {
    if (n == 0) return;
    else {
        System.out.println(i + &quot; &quot;); // 출력: 3 2 1 (1)
        재귀함수(n - 1);
        System.out.println(i + &quot; &quot;); // 출력: 1 2 3 (2)
    }
}</code></pre>
<p>(1)처럼 print가 재귀함수 내 재귀함수 앞에서 사용되면 내림차순으로 출력되고,
(2)처럼 print가 재귀함수 내 재귀함수 뒤에서 사용되면 오름차순으로 출력된다.</p>
<p>이는 재귀함수를 만나는 순간 함수의 첫번째 줄로 올라가는데, 이때 재귀함수 앞에 print가 있으면 print 다음 재귀함수 호출이므로 n .. 3 2 1 내림차순으로 출력되는 것이다.</p>
<p>근데 재귀함수 뒤에 print가 있으면 print가 나오기 전에 재귀함수가 즉시 호출되므로 stack에 값이 쌓이게 된다
(ex. [1 2 3] → 1 pop → 2 pop → 3 pop ⇒ 1 2 3 출력).
그래서 뒤에 print가 있으면 1 2 3 .. n 으로 오름차순으로 출력되는 것이다.</p>
<p><em>print가 재귀함수 앞에 있을 때 출력값</em>
<img src="https://velog.velcdn.com/images/jooo_ii/post/4872c794-448a-4ff0-87b8-67c64324b879/image.png" alt=""></p>
<p><em>print가 재귀함수 뒤에 있을 때 출력값</em>
<img src="https://velog.velcdn.com/images/jooo_ii/post/f6e84836-f553-4f68-928f-c47914a53989/image.png" alt=""></p>
<h2 id="순열-permutation">순열 (Permutation)</h2>
<p>정의: n개의 원소 중 r개를 뽑는 것 (순서 중요)
알고리즘 문제는 재귀함수로 푼다.
8P8 까지는 시간복잡도가 괜찮은데, 9P9가 되면 시간이 오래 걸려서 공간복잡도로 푸는 것 같았다 (이 부분을 통으로 놓쳐서 내 추측)</p>
<hr>
<h1 id="2-어려웠던-내용-😇">[2] 어려웠던 내용 😇</h1>
<p>일단 하노이의 탑이 원리가 이해되려다가 실제로 옮겨보면 이해가 안 돼서 미치겠다. 이 부분은 강의를 찾아봐야 할 것 같다.
그리고 순열 부분도 코드는 이해가 됐는데, 이건 문제를 실제로 풀어봐야 할 것 같다.
GPT에게 순열 관련 문제를 뽑아내라고 했으니 일단 적어두고, 일단 브론즈부터 풀어야지</p>
<ul>
<li>브론즈</li>
</ul>
<ol>
<li>백준 10872 (Bronze 3) – 팩토리얼</li>
<li>백준 10870 (Bronze 2) – 피보나치 수</li>
<li>백준 2309 (Bronze 1) – 일곱 난쟁이</li>
<li>백준 2750 (Bronze 2) – 수 정렬하기</li>
</ol>
<ul>
<li>실버, 골드</li>
</ul>
<ol>
<li>백준 15649 (실버 3) - N과 M (1)</li>
<li>백준 10974 (브루트포스, 실버 수준) - 모든 순열</li>
<li>백준 1722 (골드 5) - 순열의 순서</li>
</ol>
<hr>
<h1 id="3-궁금한-내용--부족한-내용-️">[3] 궁금한 내용 / 부족한 내용 ⁉️</h1>
<p>진심 전부 다 부족하고 궁금하다..</p>
<hr>
<h1 id="4-오늘의-총평-📮">[4] 오늘의 총평 📮</h1>
<p>아 너무 어렵다 진짜!!!!!! 특히 알고리즘을 자바로 풀자니 문법도 몰라서 미칠 것 같다 ㅜㅜ 알고리즘 잘 푸는 사람들은 어떻게 공부하길래 그런 알고리즘들이 떠오르는 건지 너무 궁금하다
알고리즘 공부 방법을 빨리 찾고 공부해야겠다 ..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[11일차] 객체지향]]></title>
            <link>https://velog.io/@jooo_ii/11%EC%9D%BC%EC%B0%A8-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5</link>
            <guid>https://velog.io/@jooo_ii/11%EC%9D%BC%EC%B0%A8-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5</guid>
            <pubDate>Sat, 31 Jan 2026 10:55:19 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jooo_ii/post/87c80345-e4e1-4bfa-817c-0090e3a2b203/image.png" alt=""></p>
<h1 id="1-오늘-공부한-내용-📝">[1] 오늘 공부한 내용 📝</h1>
<h2 id="객체지향">객체지향</h2>
<h3 id="캡슐화">캡슐화</h3>
<ul>
<li>private, default(생략), protected를 이용해서 캡슐로 쌓는 </li>
<li>public한 메서드(getter, setter)를 제공해야 한다</li>
<li>목적
1) 보호
2) 은닉 ( -&gt; decouple 효과 및 유지보수성을 높임)</li>
</ul>
<h3 id="️-getter-setter란">⁉️ Getter, Setter란?</h3>
<p><strong>Getter</strong></p>
<ul>
<li>private를 외부로 꺼내는 메서드</li>
<li>매개변수는 없고, 리턴값만 메서드로 정의</li>
<li><code>getXXX()</code> 형식</li>
</ul>
<p><strong>Setter</strong></p>
<ul>
<li>private에 값을 넣는 메서드</li>
<li>리턴값은 없고, 매개변수만 메서드로 정의</li>
</ul>
<pre><code class="language-java">class Student {
    // private 멤버 변수 (외부에서 직접 접근 불가)
    private String name;
    private int age;

    // Getter (매개변수 없음, return 값 있음)
    public String getName() {
        return name;   // name을 리턴
    }

    public int getAge() {
        return age;    // age를 리턴
    }

    // Setter (매개변수 있음, return 값 없음)
    public void setName(String name) {
        this.name = name;   // 전달받은 값을 멤버 변수에 저장
    }

    public void setAge(int age) {
        this.age = age;     // 전달받은 값을 멤버 변수에 저장
    }
}

public class Main {
    public static void main(String[] args) {
        Student s = new Student();

        // Setter 사용 (값 넣기)
        s.setName(&quot;홍길동&quot;);
        s.setAge(23);

        // Getter 사용 (값 꺼내기)
        System.out.println(&quot;이름: &quot; + s.getName());
        System.out.println(&quot;나이: &quot; + s.getAge());
    }</code></pre>
<p>(참고)
-&gt; 객체지향 원칙 중 &#39;정보은닉&#39;이 있는데, private로 숨겨놓은 필드를 getter로 조회하고, setter로 수정할 수 있다면, 정보은닉 원칙을 해칠 수 있기 때문에 지양하는 것이 좋다고 한다!
-&gt; 추후 백엔드 공부할 때 자세히 공부하면 좋을 것 같다.</p>
<hr>
<h3 id="상속">상속</h3>
<ul>
<li>상속 대상: 속성, 메서드</li>
<li>생성자는 상속에서 제외 대상이다. (생성자 이름 = 클래스 이름인데, 상속 시 부모클래스이름과 생성자 이름이 같아져버리기 때문에 에러가 발생. 대신 필요시에 호출을 통해서 재사용이 가능하다.)</li>
<li>생성 -&gt; 재사용</li>
<li>상속 -&gt; 재사용 + 수정</li>
<li>재사용하기 위해 상속을 하는 것이면 생성. 재사용하기 위해 상속을 받는 것은 성능 저하를 일으킨다.</li>
<li>수정을 하기 위해서 상속을 받는 것이 기본 생각이다.</li>
<li>이때 클래스는 단일 상속을 지원하는데, 인터페이스(interface)는 다중 상속도 지원한다.</li>
<li>상속 받은 속성이 private일 경우, 접근 불가하기 때문에 Setter / Getter를 통해 접근한다.</li>
</ul>
<hr>
<h3 id="오버라이드-override-재정의">오버라이드 (Override) (재정의)</h3>
<p>규칙 : 메서드 이름, 인자, 리턴 타입이 동일해야 한다.
효과 : 메서드 이름, 인자, 리턴 타입이 기존 코드와 동일하므로 코드를 따로 수정하지 않고, 변경된 내용을 반영할 수 있다.</p>
<p><strong>🌟 super()</strong></p>
<ul>
<li>Override에 의해서 무시된 상속 받은 멤버(메서드, 속성)을 호출할 때 super()를 통해 접근 가능하다.</li>
<li><code>super.메서드이름</code></li>
<li><code>super.속성</code></li>
</ul>
<hr>
<h3 id="추상-클래스-abstract">추상 클래스 (Abstract)</h3>
<p>Abstract는 final, static과 같이 쓸 수 없다.</p>
<ul>
<li>abstract : 반드시 상속을 하겠다 / 동적이다</li>
<li>final : 상속을 하지 않겠다</li>
<li>static : 정적이다
=&gt; 서로 이해상충되기 때문에 같이 쓸 수 없다 (final과 static은 이해상충되지 않기 때문에 같이 사용 가능)</li>
</ul>
<p>부모에서 abstract로 선언했을 때, 자식은 무조건 이에 관한 내용이 구현되어 있어야 한다.</p>
<hr>
<h3 id="interface">interface</h3>
<ol>
<li>다중상속</li>
<li>상수와 추상메서드로만 구성되어 있다.</li>
</ol>
<h3 id="추상메서드">추상메서드</h3>
<ul>
<li>메서드의 선언부만 작성하고, 구현 X</li>
</ul>
<hr>
<h3 id="static">static</h3>
<p>static을 붙이면 메모리에 미리 올라가기 때문에 객체 생성없이 클래스명으로 접근이 가능하다.
종류: <code>Arrays.binarySearch()</code>, <code>Arrays.toString()</code>, <code>Arrays.sort()</code>, <code>Math.random()</code>, <code>Math.ceil()</code> 등</p>
<hr>
<h3 id="sort">sort()</h3>
<p>int 타입 : <code>Integer.compare()</code>
문자열 타입 : <code>비교대상.compareTo(내 대상)</code></p>
<p>Comparator : 해당 클래스가 Comparable을 구현하지 않았거나, 다른 방식으로 정렬할 때 Comparator를 두 번째 인자로 전달한다.
형태: <code>Arrays.sort(기준 데이터, 람다식)</code></p>
<hr>
<h2 id="알고리즘">알고리즘</h2>
<h3 id="재귀함수">재귀함수</h3>
<p>1) 정의 : 함수 안에 자신의 함수를 다시 호출하는 함수
2) for문과의 차이</p>
<ol>
<li>depth를 내 마음대로 설정할 수 있다.</li>
<li>모든 반복문은 재귀로 쓸 수 있지만, 모든 재귀함수는 반복문으로 쓸 수 없다.
3) 작성 형태</li>
<li>반복할 코드</li>
<li>초기값 설정</li>
<li>중단 조건 (기저조건)</li>
<li>반복으로 가기 위한 유도 파트 (&lt;- 3번 조건으로 가기 위한 값 (증/감))
4) 반복 변수 필수</li>
</ol>
<p><strong>🌟 재귀함수와 for문의 차이를 코드로 표시</strong></p>
<pre><code class="language-java">for (int i = 0; i &lt; 10; i++) {
    // 반복할 코드
}

for (초기값; 중단 조건; 반복을 중단하기 위한 유도값) { }
// 반복문은 자체적으로 반복을 유도한다.</code></pre>
<pre><code class="language-java">  public static void a(int i) {
    // 중단 조건: 기저 조건
    if (i == 10) return; 

    a(i+1) // 반복하기 위한 유도값
}</code></pre>
<p><strong>🌟 재귀함수 작성 방법</strong></p>
<p>1) 기저 조건 (반복을 중단하기 위한 조건)
2) 특정 조건일 때 반복(재귀) 유도</p>
<p><strong>🌟 Bottom-Up, Top-Down 형태</strong></p>
<p>1) Bottom-Up : 오름차순 정렬
2) Top-Down : 내림차순 정렬</p>
<hr>
<h1 id="2-어려웠던-내용-😇">[2] 어려웠던 내용 😇</h1>
<ul>
<li>객체지향의 모든 것이 어려웠다..</li>
<li>재귀함수를 구현할 때, sum과 factorial을 구현하는 시간이 있었는데, 누적합(곱)을 total에 담는 방법을 몰라 GPT의 도움을 살짝 빌렸다. 이 부분이 조금 아쉬웠으나, 해결책은 static int total = 0를 전역함수로 설정해두고, main 함수에서 total=0으로 매번 초기화하는 방법을 사용했다. (한 파일에서 4종류의 재귀함수를 구현하고 있었기 때문에)</li>
<li>특히, 누적곱은 total = 0이 아닌, total = 1에서 시작해야 한다는 점도 기억해두자.</li>
</ul>
<pre><code class="language-java">public class FactorialTest {
    static int N = 5;
    static int total = 1; // 누적곱 &lt;- 0으로 하면 누적곱 = 0 

    // 기저조건 Bottom -&gt; up (오름차순)
    public static void print1(int i) {
        if (i &gt; N) return;
        total *= i;
        print1(i+1);
    }
    public static void main(String[] args) {
        total = 1;
        print1(1);
    }
 }</code></pre>
<hr>
<h1 id="3-궁금한-내용--부족한-내용-️">[3] 궁금한 내용 / 부족한 내용 ⁉️</h1>
<p>객체지향 부분은 백엔드를 배워야 내가 뭐가 궁금하고, 부족할 지 알 수 있을 것 같다.</p>
<hr>
<h1 id="4-느낀-점-📮">[4] 느낀 점 📮</h1>
<p>6시에 일어나기 성공했다 ㅜ
객체지향은 학교 강의 재수강 + 부트캠프 강의를 들어도 매번 새로운 것 같다.. 자바랑 하염없이 멀어지는 중
그리고 알고리즘을 실제로 풀어봐야 자바 문법에 익숙해질 것 같다
TIL 작성하는 법은 아래 링크를 참고했다. 오늘 공부한 내용 전체는 노션을 통해 정리하고, TIL에서는 유독 어려웠던 파트를 정리하는 편이 깔끔할 것 같다
더 열심히..........</p>
<p><a href="https://velog.io/@pyotato/TIL2023.04.15TIL%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%8D%A8%EC%95%BC%EB%90%A0%EA%B9%8C">TIL 작성 방법</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] HTML, CSS, JavaScript, 알고리즘]]></title>
            <link>https://velog.io/@jooo_ii/TIL-HTML-CSS-JavaScript-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</link>
            <guid>https://velog.io/@jooo_ii/TIL-HTML-CSS-JavaScript-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</guid>
            <pubDate>Sat, 31 Jan 2026 10:42:42 GMT</pubDate>
            <description><![CDATA[<h3 id="✏️-회고">✏️ 회고</h3>
<p>HTML, CSS, JavaScript
HTML, CSS, JavaScript를 8일 내로 배워야 했기 때문에 진도가 매우 빨랐다.
HTML, CSS는 어느정도 따라갔지만, JavaScript 부분에선 겨우 따라갔다.</p>
<p>부트캠프 전까지는 말하는 감자 정도 됐다고 생각했지만, GPT에 의존하지 않고 하려니 말도 못하는 감자가 됐다는 생각만 들었다...............
이제까지 했던 게 무용지물이 된 기분이 들긴 했지만, 의지를 더 불 태울 수 있었던 계기가 되었다 !!!!!</p>
<p>수업 끝나고 프로젝트까지 하려니까 복습을 제대로 하지 못했는데, 얼른 프로젝트를 마무리하고 나머지 공부 열심히 해야겠다 ....</p>
<p>매일 TIL 적는 습관도 만들어야겠다...</p>
<hr>
<h3 id="java-알고리즘">JAVA 알고리즘</h3>
<p>덜컥 올 것이 와버린 알고리즘 ..</p>
<p>eclipse는 객체지향프로그래밍 하면서 지겹게 했는데 자바는 참 어려운 것 같다.. </p>
<p>깊이 공부하지 않았던 분야여서 더 그랬던 것 같다</p>
<p>어떻게 공부를 해야 할 지 감을 잡아야 될 것 같다</p>
<p>첫 날에 강사님과 상담한 대로 스터디를 빨리 만들어서 공부하는 습관을 들여야겠다는 생각이 들었다</p>
<p>언제 하지..</p>
<hr>
<h3 id="🤓-다짐">🤓 다짐</h3>
<ol>
<li><p>수업 끝난 뒤 코에 바람 좀 집어넣고 1시간 ~ 1시간 30분 정도 복습하는 시간 가지면서 TIL 올리기</p>
</li>
<li><p>주말에 주간회고 작성하기 (5일간 했던 것, 앞으로 다짐 등)</p>
</li>
<li><p>알고리즘 스터디 만들거나 참여하기 💨</p>
</li>
<li><p>6시 30분 기상을 목표로 .. 😴</p>
</li>
<li><p>제에발 지각하지 말자 ..</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고] 유레카 부트캠프 3기 프론트엔드 신청 및 후기]]></title>
            <link>https://velog.io/@jooo_ii/%ED%9A%8C%EA%B3%A0-%EC%9C%A0%EB%A0%88%EC%B9%B4-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-3%EA%B8%B0-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%8B%A0%EC%B2%AD-%EB%B0%8F-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@jooo_ii/%ED%9A%8C%EA%B3%A0-%EC%9C%A0%EB%A0%88%EC%B9%B4-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-3%EA%B8%B0-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%8B%A0%EC%B2%AD-%EB%B0%8F-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sat, 31 Jan 2026 10:40:38 GMT</pubDate>
            <description><![CDATA[<p>LG U+에서 주관하는 유레카 부트캠프!</p>
<p>수료만 해도 LG U+ 서류/코테 면제(1년 이내 1회만 가능)라기에 좋은 기회라서 바로 보자마자 신청했습니다.</p>
<h3 id="서류">서류</h3>
<p>자기소개서 문항은 아래와 같습니다. 저는 비전공자이기 때문에 해당 문항이었습니다. 전공자 문항은 조금 달랐으니 참고하시길 바랍니다!</p>
<blockquote>
<ol>
<li>LG 유플러스 유레카 SW교육에 지원하신 동기와 과정 수료 후, 이루고 싶은 취업 계획을 작성해 주십시오. (최소 1자, 최대 500자 입력가능)
[비전공생] 2-1. SW에 관심을 가지게 된 계기 혹은 취업 준비를 하며 겪었던 경험에 대해 작성하고, LG 유플러스 유레카 SW교육 과정을 통해 향후 어떤 개발자로 성장하고 싶은지 작성해 주십시오. (최소 1자, 최대 500자 입력가능)</li>
</ol>
</blockquote>
<p>비전공자 프론트엔드로 신청했고, 마감 다음날 서류 합격 메일이 왔어요</p>
<br/>

<h3 id="적성-진단">적성 진단</h3>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/b4649a72-3e97-4363-8872-dbab49c83b10/image.png" alt=""></p>
<p>29일 합격 발표 후, 8월 1일에 바로 SW적성진단 응시가 있었습니다.</p>
<p>비전공자로 지원했기 때문에 적성진단을 응시했고, 이는 &#39;잡다&#39;라는 사이트에서 연습할 수 있으니, 연습하고 응시하시는 것을 추천드립니다!</p>
<p>2시간 걸린다고 했는데, 저는 40분 내로 끝냈어요</p>
<p>알고보니 총 2시간 안에만 보면 되는 거여서 저는 빠르게 끝낸 기억이 있습니다</p>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/1d4899c5-77ff-4335-be12-830af22c2811/image.png" alt=""></p>
<p>8/5 오전 9시에 바로 합격 문자가 왔습니다</p>
<p>8/7에 바로 면접이라 일정이 타이트했기 때문에 부랴부랴 비행기표 예매 했습니다 ㅜ</p>
<p>다행히 알바 시간 전에 부산 돌아올 수 있어서 정말 다행이었습니다.</p>
<br/>

<h3 id="면접">면접</h3>
<p>면접 준비는 그냥 구글링해보고, 다른 사람들이 작성해둔 인성 질문 정도만 준비해갔습니다!</p>
<p>면접 준비할 시간도 별로 없었기 때문에 1분 자기소개만 주구장창 외웠던 기억이 나네요</p>
<p>(물론 다 말하지 못했습니다 ㅎ)</p>
<p>면접 장소는 &#39;토즈 강남 컨퍼러스홀&#39;에서 진행됐습니다</p>
<p>2기 장소와는 다른 것 같았어요 </p>
<p>들어가자마자 유출 금지 서약서 작성하고, 다과도 준비해주셨습니다 (너무 맛있었어요 알바 가서 맛있게 먹었습니다)</p>
<p> <img src="https://velog.velcdn.com/images/jooo_ii/post/807998f4-e65f-4eea-bb23-8153b8da546f/image.png" alt=""></p>
<p>면접복은 캐주얼이라 했는데, 저는 셔츠가 없었어서.. 그냥 단색 반팔에 슬랙스 입고 갔는데, 다른 분들은 정말 면접복처럼 단정하게 입고오셨더라구요. 참고하세요 !</p>
<p>5:2 면접이었고, 30분 정도 했습니다.</p>
<p>그리고 면접 타임이 정말 많았어요</p>
<p>18일 전후로 합격 발표가 될 것이라고 공지 주셨어요</p>
<br/>


<h3 id="결과">결과</h3>
<p><img src="https://velog.velcdn.com/images/jooo_ii/post/fc2794e7-8ee8-4228-b848-b1099fdb4744/image.png" alt=""></p>
<p>18일까지 기도하면서 빌고 있었는데, 11일인 오늘 합격 메일이 갑자기 왔어요!</p>
<p>문자+메일 둘 다 와서 카페에 있었는데 손 정말 많이 떨었습니다 ㅜㅜ</p>
<p>서울로 상경하는 거라 준비할 게 생각보다 많을 것 같네요</p>
<p>지금 알바, 자취방 등등 ..</p>
<p>다음에 유레카 신청하시는 분들도 이 글이 도움이 됐으면 좋겠습니다!</p>
<p>정말 열심히 하겠습니당당 화이팅 <del>~</del>^,,^</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CSS] 모달 열릴 때 뒷 배경 스크롤 방지]]></title>
            <link>https://velog.io/@jooo_ii/CSS-%EB%AA%A8%EB%8B%AC-%EC%97%B4%EB%A6%B4-%EB%95%8C-%EB%92%B7-%EB%B0%B0%EA%B2%BD-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EB%B0%A9%EC%A7%80</link>
            <guid>https://velog.io/@jooo_ii/CSS-%EB%AA%A8%EB%8B%AC-%EC%97%B4%EB%A6%B4-%EB%95%8C-%EB%92%B7-%EB%B0%B0%EA%B2%BD-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EB%B0%A9%EC%A7%80</guid>
            <pubDate>Sat, 31 Jan 2026 10:35:18 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-javascript">useEffect(() =&gt; {
  if (isOpenModal) {
      document.body.style.overflow = &quot;hidden&quot;;
  } else {
      document.body.style.overflow = &quot;auto&quot;;
  }
}, [isOpenModal]);
</code></pre>
<p>React는 상관 없지만, Nest.js 환경에서는 SSR이 있기 때문에 document나 window가 서버에 존재하지 않아서 렌더링 시점만 조심하면 된다.</p>
<p>-&gt; 위 코드로 써도 상관은 없다고 한다. useEffect는 브라우저에서만 실행되기 때문에 서버사이드에서 document를 참조하는 일은 없기 때문.</p>
<p>그래도 타입 안정성을 위해서 <code>typeof document !== &quot;undefined&quot;</code>를 추가해도 된다.</p>
<pre><code class="language-javascript">useEffect(() =&gt; {
  if (typeof document !== &quot;undefined&quot;) {
      document.body.style.overflow = isOpenModal ? &quot;hidden&quot; : &quot;auto&quot;;
  }
}, [isOpenModal]);</code></pre>
]]></description>
        </item>
    </channel>
</rss>