<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>_seoy9.log :)</title>
        <link>https://velog.io/</link>
        <description>시대를 따라가는 개발자가 아닌, 시대를 이끄는 개발자</description>
        <lastBuildDate>Fri, 29 May 2026 14:36:31 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>_seoy9.log :)</title>
            <url>https://velog.velcdn.com/images/int_1sy/profile/fbe981ab-545c-4839-870c-8c7d3a5d0634/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. _seoy9.log :). All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/int_1sy" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[#44 #arx를 활용한 데이터 처리 ]]></title>
            <link>https://velog.io/@int_1sy/44-arx%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@int_1sy/44-arx%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Fri, 29 May 2026 14:36:31 GMT</pubDate>
            <description><![CDATA[<h1 id="arx를-활용한-데이터-익명화-실습-및-개선">ARX를 활용한 데이터 익명화 실습 및 개선</h1>
<h2 id="수업-시간에-한-데이터">수업 시간에 한 데이터</h2>
<p>수업에서는 ARX Anonymization Tool을 사용하여 Adult 데이터셋을 익명화하는 실습을 진행했다.
사용한 quasi-identifier 속성은 <strong>sex, age, race, marital-status, education, native-country</strong>이며, k-anonymity 기반의 익명화를 수행했다.</p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/e5da7552-56db-4867-8782-c1385b69258b/image.png" alt="수업 데이터 1"></p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/cccd330e-0e5b-47a6-9e3f-d8116c76180a/image.png" alt="수업 데이터 2"></p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/b804e493-f6fe-4b1a-899d-964a569317c0/image.png" alt="수업 데이터 3"></p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/619d5d08-5813-4de5-a90f-8cf101f26967/image.png" alt="수업 데이터 4"></p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/8ef09490-08ea-4e9d-a190-a4b84ee10d66/image.png" alt="수업 데이터 5"></p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/b22f0375-d250-4354-87c0-ac28120a664b/image.png" alt="수업 데이터 6"></p>
<p>수업에서 적용한 최적 변환은 <code>[0, 5, 1, 1, 1, 3]</code>이었으며,<br>익명화 후 <strong>Score: 35.61%</strong>, 평균 재식별 위험도는 <strong>0.159%</strong> 로 크게 감소했다.</p>
<hr>
<h2 id="아쉬운-점">아쉬운 점</h2>
<p>수업에서 진행한 익명화 결과를 분석하면서 몇 가지 아쉬운 점을 발견했다.</p>
<p><strong>1. education 계층 구조의 비효율성</strong></p>
<ul>
<li>기존 그룹핑이 <code>8 Set + 3 Set + 4 Set</code>으로 구성되어 있어 교육 수준의 의미적 유사성이 반영되지 않았다.</li>
<li>특히 데이터에서 빈도가 높은 <strong>HS-grad</strong>가 다른 값들과 함께 묶여, 불필요한 정보 손실이 발생했다.</li>
</ul>
<p><strong>2. native-country 계층 구조의 비효율성</strong></p>
<ul>
<li>41개 국가가 지역적 의미 없이 <code>10개 + 3개 + 12개</code>로 단순 묶음 처리되어 있었다.</li>
<li>대륙/지역 기반의 의미 있는 그룹핑이 이루어지지 않아 유틸리티 손실이 컸다.</li>
</ul>
<hr>
<h2 id="개선-방안">개선 방안</h2>
<h3 id="education-계층-구조-재설계">education 계층 구조 재설계</h3>
<p>빈도가 높은 <strong>HS-grad를 단독 그룹</strong>으로 분리하고, 나머지 값들을 교육 수준에 따라 의미적으로 재그룹핑했다.</p>
<table>
<thead>
<tr>
<th>Level 1 그룹</th>
<th>포함 값</th>
<th>개수</th>
</tr>
</thead>
<tbody><tr>
<td>미취학/초등</td>
<td>Preschool, 1st-4th, 5th-6th</td>
<td>3개</td>
</tr>
<tr>
<td>중등</td>
<td>7th-8th, 9th, 10th, 11th, 12th</td>
<td>5개</td>
</tr>
<tr>
<td>고등학교 졸업</td>
<td><strong>HS-grad</strong></td>
<td><strong>1개 (단독)</strong></td>
</tr>
<tr>
<td>대학 과정</td>
<td>Some-college, Assoc-voc, Assoc-acdm</td>
<td>3개</td>
</tr>
<tr>
<td>고등교육</td>
<td>Bachelors, Masters, Doctorate, Prof-school</td>
<td>4개</td>
</tr>
</tbody></table>
<blockquote>
<p>HS-grad를 단독으로 분리함으로써 해당 그룹의 일반화 수준을 낮추고 정보 손실을 최소화했다.</p>
</blockquote>
<h3 id="native-country-계층-구조-재설계">native-country 계층 구조 재설계</h3>
<p>41개 국가를 <strong>대륙/지역 기반</strong>으로 의미 있게 그룹핑했다.</p>
<table>
<thead>
<tr>
<th>Level 1 그룹</th>
<th>포함 국가</th>
<th>개수</th>
</tr>
</thead>
<tbody><tr>
<td>북미/카리브</td>
<td>United-States, Canada, Mexico, Puerto-Rico, Cuba, Jamaica, Dominican-Republic, Guatemala, El-Salvador, Haiti, Honduras, Nicaragua, Outlying-US(Guam), Trinadad&amp;Tobago</td>
<td>14개</td>
</tr>
<tr>
<td>남미</td>
<td>Columbia, Ecuador, Peru</td>
<td>3개</td>
</tr>
<tr>
<td>유럽</td>
<td>England, France, Germany, Greece, Holand-Netherlands, Hungary, Ireland, Italy, Poland, Portugal, Scotland, Yugoslavia</td>
<td>12개</td>
</tr>
<tr>
<td>아시아</td>
<td>Hong, India, Iran, Japan, Laos, Philippines, Taiwan, Thailand, Vietnam, Cambodia, China, South</td>
<td>12개</td>
</tr>
</tbody></table>
<hr>
<h2 id="개선-결과">개선 결과</h2>
<p>계층 구조 재설계 후 ARX에서 다시 익명화를 적용한 결과는 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/229fd17f-25ed-4662-86b9-6b332f58376f/image.png" alt="개선 결과 1"></p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/c9f853f0-cd48-4a6f-ac84-a410a16ec9bb/image.png" alt="개선 결과 2"></p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/b09ab56d-e992-4c28-9197-51d65a162b1b/image.png" alt="개선 결과 3"></p>
<p>계층 구조를 데이터의 특성과 의미적 유사성에 맞게 재설계하면, <strong>같은 익명성 수준을 유지하면서도 정보 손실(Score)을 줄일 수 있다</strong>는 것을 확인했다.</p>
<p>특히 빈도가 높은 값을 단독 그룹으로 분리하는 전략은 유틸리티 보존에 효과적이며, 국가처럼 범주가 많은 속성은 지역적/개념적 기준으로 그룹핑하는 것이 중요하다는 것을 배웠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[#43 #플러터 코드는 왜 이렇게 생겼을까]]></title>
            <link>https://velog.io/@int_1sy/43-%ED%94%8C%EB%9F%AC%ED%84%B0-%EC%BD%94%EB%93%9C%EB%8A%94-%EC%99%9C-%EC%9D%B4%EB%A0%87%EA%B2%8C-%EC%83%9D%EA%B2%BC%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@int_1sy/43-%ED%94%8C%EB%9F%AC%ED%84%B0-%EC%BD%94%EB%93%9C%EB%8A%94-%EC%99%9C-%EC%9D%B4%EB%A0%87%EA%B2%8C-%EC%83%9D%EA%B2%BC%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Fri, 29 May 2026 07:22:11 GMT</pubDate>
            <description><![CDATA[<h2 id="플러터의-코드는-왜-이렇게-생겼을까">플러터의 코드는 왜 이렇게 생겼을까?</h2>
<p>처음 플러터를 접해보면 너무나 많은 들여쓰기와 괄호들을 보고 왜 이렇게 어렵게 생긴거야?
아니 저기엔 쉼표가 있는데 여기는 왜 없어? 왜 여기 있어야해? 라고 생각해보신 적 있으실 수도 있는데요, 그 이유는 플러터는 모든 요소를 Widget으로 보기 때문입니다. 이에 대한 이야기를 이제 더 깊게 정리해보겠습니다.</p>
<h2 id="flutter가-다루는-것">Flutter가 다루는 것</h2>
<ol>
<li><strong>무엇을</strong> 화면에 보여줄 것인가</li>
<li>사용자가 <strong>어떤 행동</strong>을 했을 때 무엇을 할 것인가</li>
<li>데이터가 바뀌었을 때 화면을 <strong>어떻게 다시 그릴</strong> 것인가</li>
</ol>
<p>Flutter는 앱 화면을 만드는 도구다.<br>버튼, 입력값, 서버에서 가져온 데이터 등등…<br><strong>데이터 변화가 화면의 변화로 이어지는 구조</strong>를 코드로 제어한다.</p>
<hr>
<h2 id="모든-것은-widget이다">모든 것은 Widget이다</h2>
<p>Flutter에서는 제목도, 설명 문장도, 버튼도 전부 <strong>Widget</strong>이다.</p>
<table>
<thead>
<tr>
<th>위젯</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>StatelessWidget</code></td>
<td>상태가 없는 위젯. 한번 그려지면 내부 데이터가 바뀌지 않는 화면</td>
</tr>
<tr>
<td><code>StatefulWidget</code></td>
<td>상태가 있는 위젯. 데이터가 바뀌면 화면도 다시 그려야 할 때 사용</td>
</tr>
</tbody></table>
<h3 id="scaffold-화면의-기본-뼈대">Scaffold: 화면의 기본 뼈대</h3>
<p><code>Scaffold</code>는 상단 바, 본문 영역, 하단 버튼, 플로팅 버튼, 메뉴 등 화면의 기본 구조를 잡아주는 위젯이다.</p>
<hr>
<h2 id="상태state란-무엇인가">상태(State)란 무엇인가?</h2>
<blockquote>
<p><strong>state = 화면에 영향을 주는 데이터</strong><br><strong>상태 관리 = 데이터 관리</strong></p>
</blockquote>
<p>아래처럼 숫자가 바뀌어야 하는 데이터가 바로 state다.</p>
<pre><code class="language-dart">int count = 0;

// 상태를 변경하고 → build에게 다시 그리라고 알려줌
setState(() {
  count++;
});

Text(&quot;$count&quot;)  // 바뀐 숫자가 화면에 표시됨</code></pre>
<p>Flutter는 <code>build()</code>를 다시 실행시켜서 화면을 새로 그린다.<br>상태를 변경하고 → 상태에 맞춰서 UI를 바꾸는 것, 이미 우리는 해오고 있었다.</p>
<hr>
<h2 id="데이터를-가져오는-두-가지-방식">데이터를 가져오는 두 가지 방식</h2>
<h3 id="1-한-번만-가져오는-데이터-future">1. 한 번만 가져오는 데이터 (Future)</h3>
<p>서버에 요청 → 응답 받고 끝.</p>
<h3 id="2-계속-지켜보는-데이터--stream">2. 계속 지켜보는 데이터 — Stream</h3>
<p>데이터가 한 번 오고 끝이 아니라, <strong>시간이 지나면서 계속 흘러들어오는 데이터 통로</strong>다.</p>
<pre><code>앱 ↔ 서버 ↔ DB</code></pre><hr>
<h2 id="캐싱caching이란">캐싱(Caching)이란?</h2>
<p>서버와 사용자 사이에는 <strong>시간 차</strong>가 존재한다.<br>매번 서버에 요청하면 느릴 수 있기 때문에 <strong>캐싱</strong>을 사용한다.</p>
<blockquote>
<p><strong>캐싱 = 데이터를 기억해두는 것</strong><br><code>cache</code> 변수에는 서버의 GET 요청으로 가져온 정보들을 저장해 더 빠르게 화면에 보여준다.</p>
</blockquote>
<hr>
<h2 id="flutter-문법-괄호와-기호-완전-정리">Flutter 문법: 괄호와 기호 완전 정리</h2>
<h3 id="--중괄호--코드-실행-범위"><code>{ }</code> 중괄호 — 코드 실행 범위</h3>
<p>코드가 <strong>실행되는 범위(블록)</strong> 를 나타낼 때 사용한다.</p>
<pre><code class="language-dart">// 함수의 실행 내용
void sayHello() {
  print(&#39;hello&#39;);
}

// 버튼을 눌렀을 때 실행할 코드
onPressed: () {
  print(&#39;눌림!&#39;);
}

// 조건문 / 반복문 안에서 실행할 코드
if (count &gt; 0) {
  print(&#39;양수&#39;);
}

// setState 안에서 상태를 바꾸는 코드
setState(() {
  count++;
});</code></pre>
<hr>
<h3 id="--대괄호--여러-개를-목록으로-넣을-때"><code>[ ]</code> 대괄호 — 여러 개를 목록으로 넣을 때</h3>
<p>여러 위젯이나 값을 <strong>리스트(목록)</strong> 로 넣을 때 사용한다.</p>
<pre><code class="language-dart">Column(
  children: [        // 여러 자식 위젯을 목록으로
    Text(&#39;첫 번째&#39;),
    Text(&#39;두 번째&#39;),
    ElevatedButton(...),
  ],
)

AppBar(
  actions: [         // 우측 아이콘들을 목록으로
    IconButton(...),
    IconButton(...),
  ],
)</code></pre>
<hr>
<h3 id="--소괄호--위젯-생성--함수-호출"><code>( )</code> 소괄호 — 위젯 생성 &amp; 함수 호출</h3>
<p>위젯을 <strong>만들거나</strong> 함수를 <strong>호출할 때</strong> 사용한다.</p>
<pre><code class="language-dart">Text(&#39;안녕하세요&#39;)   // Text 위젯 생성
print(&#39;출력&#39;)        // 함수 호출
setState(() { })     // setState 함수 호출</code></pre>
<hr>
<h3 id="-콜론--파라미터-이름에-값을-전달할-때"><code>:</code> 콜론 — 파라미터 이름에 값을 전달할 때</h3>
<p>Flutter에서 위젯에 값을 넣을 때 <code>파라미터명: 값</code> 형태로 쓴다. (named parameter)</p>
<pre><code class="language-dart">Text(
  &#39;안녕하세요&#39;,
  style: TextStyle(fontSize: 20),   // style: 값
  textAlign: TextAlign.center,      // textAlign: 값
)

ElevatedButton(
  onPressed: () { },    // onPressed: 실행할 함수
  child: Text(&#39;클릭&#39;),  // child: 자식 위젯
)</code></pre>
<hr>
<h3 id="-쉼표--각각의-위젯값을-구분"><code>,</code> 쉼표 — 각각의 위젯(값)을 구분</h3>
<p>위젯이나 파라미터를 <strong>구분</strong>할 때 사용한다.<br>마지막 항목 뒤에도 쉼표를 붙이는 게 Flutter 스타일이다. (trailing comma → 자동 포맷팅에 도움)</p>
<pre><code class="language-dart">Column(
  children: [
    Text(&#39;하나&#39;),    // 쉼표로 구분
    Text(&#39;둘&#39;),
    Text(&#39;셋&#39;),      // 마지막에도 쉼표 OK
  ],
)</code></pre>
<hr>
<h3 id="-세미콜론--문장의-끝"><code>;</code> 세미콜론 — 문장의 끝</h3>
<p>하나의 실행문이 끝났음을 표시한다.</p>
<pre><code class="language-dart">int count = 0;   // 변수 선언 끝
count++;         // 실행문 끝
print(&#39;hi&#39;);     // 실행문 끝</code></pre>
<blockquote>
<p>주의: <code>{ }</code> 블록 뒤에는 <code>;</code>을 붙이지 않는다.</p>
</blockquote>
<hr>
<h2 id="📋-한눈에-정리">📋 한눈에 정리</h2>
<table>
<thead>
<tr>
<th>기호</th>
<th>용도</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>{ }</code></td>
<td>코드 실행 범위</td>
<td>함수 본문, 조건문, setState</td>
</tr>
<tr>
<td><code>[ ]</code></td>
<td>목록(리스트)</td>
<td>children: [], actions: []</td>
</tr>
<tr>
<td><code>( )</code></td>
<td>위젯 생성 / 함수 호출</td>
<td>Text(), setState()</td>
</tr>
<tr>
<td><code>:</code></td>
<td>파라미터에 값 전달</td>
<td>child: Text(), style: ...</td>
</tr>
<tr>
<td><code>,</code></td>
<td>항목 구분</td>
<td>위젯, 파라미터 사이</td>
</tr>
<tr>
<td><code>;</code></td>
<td>문장 끝</td>
<td>변수 선언, 실행문 끝</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[#42 #Future, Stream, 상태 관리, 캐시 이해하기]]></title>
            <link>https://velog.io/@int_1sy/42-Future-Stream-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-%EC%BA%90%EC%8B%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-xey728lw</link>
            <guid>https://velog.io/@int_1sy/42-Future-Stream-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-%EC%BA%90%EC%8B%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-xey728lw</guid>
            <pubDate>Thu, 28 May 2026 10:18:52 GMT</pubDate>
            <description><![CDATA[<h1 id="flutter에서-데이터를-다루는-두-가지-방식--future-stream-상태-관리-캐시-이해하기">Flutter에서 데이터를 다루는 두 가지 방식 — Future, Stream, 상태 관리, 캐시 이해하기</h1>
<hr>
<h2 id="1-오늘의-목표">1. 오늘의 목표</h2>
<p>Flutter에서 데이터를 다루는 두 가지 방식(Future와 Stream)의 차이를 이해하고, 상태(State)와 캐시(Cache)가 각각 어떤 역할을 하는지 정리한다.</p>
<hr>
<h2 id="2-핵심-개념">2. 핵심 개념</h2>
<h3 id="future--한-번만-가져오는-데이터">Future — 한 번만 가져오는 데이터</h3>
<p><code>Future</code>는 Flutter(Dart)에서 비동기로 데이터를 <strong>한 번</strong> 요청하고 결과를 받으면 끝나는 방식이다.</p>
<p>이 개념은 앱을 처음 켤 때 서버에서 데이터를 불러오거나, 버튼을 눌렀을 때 특정 정보를 딱 한 번 조회할 때 필요하다.</p>
<h3 id="stream--계속-지켜보는-데이터">Stream — 계속 지켜보는 데이터</h3>
<p><code>Stream</code>은 데이터가 한 번 오고 끝이 아니라, <strong>시간이 지나면서 계속 흘러들어오는 데이터 통로</strong>다.</p>
<p>이 개념은 DB나 서버에 변화가 생길 때마다 자동으로 새 데이터를 받아야 할 때 필요하다.</p>
<p>예를 들어 일정 앱에서 일정을 추가하거나 삭제하면 화면이 자동으로 갱신되어야 하는 경우에 쓴다.</p>
<h3 id="state--화면에-영향을-주는-데이터">State — 화면에 영향을 주는 데이터</h3>
<p><code>State</code>는 값이 바뀌면 화면을 다시 그려야 하는 데이터다.</p>
<p>이 개념은 카운터, 선택된 날짜, 입력 중인 텍스트처럼 UI에 직접 반영되어야 하는 값을 관리할 때 필요하다.</p>
<p>Flutter에서는 <code>setState()</code>를 호출해야 상태가 바뀐 것을 감지하고 <code>build()</code>를 다시 실행한다.</p>
<h3 id="cache--가져온-데이터를-임시로-기억하는-것">Cache — 가져온 데이터를 임시로 기억하는 것</h3>
<p><code>Cache</code>는 서버나 DB에서 가져온 데이터를 변수에 임시로 저장해두는 것이다.</p>
<p>이 개념은 매번 서버에 새로 요청하지 않고도 저장된 값을 바로 사용해서 응답 속도를 높여야 할 때 필요하다.</p>
<p>앱과 서버 사이에는 네트워크 지연이 있기 때문에, 자주 바뀌지 않는 데이터는 캐싱해두면 훨씬 자연스러운 UX를 만들 수 있다.</p>
<hr>
<h2 id="3-코드-예시">3. 코드 예시</h2>
<h3 id="future-방식--한-번만-조회">Future 방식 — 한 번만 조회</h3>
<pre><code class="language-dart">Future&lt;List&lt;Schedule&gt;&gt; getSchedules(DateTime date) {
  return (select(schedules)
    ..where((tbl) =&gt; tbl.date.equals(date)))
    .get();
}</code></pre>
<h3 id="stream-방식--실시간-감시">Stream 방식 — 실시간 감시</h3>
<pre><code class="language-dart">Stream&lt;List&lt;Schedule&gt;&gt; watchSchedules(DateTime date) {
  return (select(schedules)
    ..where((tbl) =&gt; tbl.date.equals(date)))
    .watch();
}</code></pre>
<h3 id="state--setstate로-화면-갱신">State — setState로 화면 갱신</h3>
<pre><code class="language-dart">int count = 0;

setState(() {
  count++;
});

Text(&#39;$count&#39;) // count가 바뀌면 자동으로 다시 그려짐</code></pre>
<h3 id="cache--서버-데이터를-변수에-저장">Cache — 서버 데이터를 변수에 저장</h3>
<pre><code class="language-dart">List&lt;Schedule&gt;? cache; // 서버의 GET 요청으로 가져온 일정 정보를 저장

Future&lt;void&gt; loadSchedules() async {
  cache = await getSchedules(selectedDate); // 한 번 저장해두면 재사용 가능
}</code></pre>
<h3 id="streambuilder로-실시간-ui-연결">StreamBuilder로 실시간 UI 연결</h3>
<pre><code class="language-dart">StreamBuilder&lt;List&lt;Schedule&gt;&gt;(
  stream: GetIt.I&lt;LocalDatabase&gt;().watchSchedules(selectedDate),
  builder: (context, snapshot) {
    return TodayBanner(
      selectedDate: selectedDate,
      count: snapshot.data?.length ?? 0,
    );
  },
),</code></pre>
<hr>
<h2 id="4-코드-설명">4. 코드 설명</h2>
<h3 id="future-vs-stream-핵심-차이">Future vs Stream 핵심 차이</h3>
<table>
<thead>
<tr>
<th>코드</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>.get()</code></td>
<td>데이터를 <strong>한 번만</strong> 가져오고 끝낸다</td>
</tr>
<tr>
<td><code>.watch()</code></td>
<td>데이터를 <strong>계속 감시</strong>하며 변화가 생길 때마다 새 값을 흘려보낸다</td>
</tr>
<tr>
<td><code>Future&lt;List&lt;Schedule&gt;&gt;</code></td>
<td>반환 타입이 Future이면 비동기 단발성 요청임을 의미한다</td>
</tr>
<tr>
<td><code>Stream&lt;List&lt;Schedule&gt;&gt;</code></td>
<td>반환 타입이 Stream이면 지속적으로 데이터가 흘러오는 통로임을 의미한다</td>
</tr>
</tbody></table>
<h3 id="state-관련-코드">State 관련 코드</h3>
<table>
<thead>
<tr>
<th>코드</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>int count = 0</code></td>
<td>화면에 표시할 상태(데이터)를 선언한다</td>
</tr>
<tr>
<td><code>setState(() { count++; })</code></td>
<td>상태를 변경하고 Flutter에게 &quot;build를 다시 실행해라&quot;라고 알린다</td>
</tr>
<tr>
<td><code>Text(&#39;$count&#39;)</code></td>
<td>현재 count 값을 화면에 표시하며, 상태가 바뀌면 자동으로 갱신된다</td>
</tr>
</tbody></table>
<h3 id="cache-관련-코드">Cache 관련 코드</h3>
<table>
<thead>
<tr>
<th>코드</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>List&lt;Schedule&gt;? cache</code></td>
<td>서버에서 받아온 일정 목록을 임시로 기억할 변수를 선언한다. <code>?</code>는 값이 없을 수도 있음(nullable)을 의미한다</td>
</tr>
<tr>
<td><code>cache = await getSchedules(...)</code></td>
<td>서버/DB에서 데이터를 가져와서 cache에 저장한다. 이후에는 서버 요청 없이 cache를 바로 사용할 수 있다</td>
</tr>
</tbody></table>
<h3 id="streambuilder-관련-코드">StreamBuilder 관련 코드</h3>
<table>
<thead>
<tr>
<th>코드</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>StreamBuilder&lt;List&lt;Schedule&gt;&gt;(...)</code></td>
<td>Stream에서 오는 데이터를 감지해서 UI를 자동으로 다시 그리는 위젯이다</td>
</tr>
<tr>
<td><code>stream: GetIt.I&lt;LocalDatabase&gt;().watchSchedules(selectedDate)</code></td>
<td><code>stream</code> 속성은 감시할 데이터 통로를 설정한다</td>
</tr>
<tr>
<td><code>snapshot.data?.length ?? 0</code></td>
<td>스트림에서 받은 일정 개수를 반환하고, 데이터가 아직 없으면 0을 기본값으로 사용한다</td>
</tr>
<tr>
<td><code>?.</code></td>
<td>값이 null일 수 있을 때 안전하게 접근하기 위한 Dart의 null-safe 문법이다</td>
</tr>
<tr>
<td><code>??</code></td>
<td>왼쪽 값이 null이면 오른쪽 값을 대신 사용하도록 설정한다</td>
</tr>
</tbody></table>
<hr>
<h2 id="5-전체-코드-maincalender--homescreen">5. 전체 코드 (MainCalender + HomeScreen)</h2>
<h3 id="main_calenderdart">main_calender.dart</h3>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:table_calendar/table_calendar.dart&#39;;
import &#39;package:calendar_scheduler/const/colors.dart&#39;;

class MainCalender extends StatelessWidget {

  final OnDaySelected onDaySelected; // 날짜 선택 시 실행
  final DateTime selectedDate;       // 선택된 날짜

  MainCalender({
    required this.onDaySelected,
    required this.selectedDate,
  });

  @override
  Widget build(BuildContext context) {
    return TableCalendar(
      locale: &#39;ko_kr&#39;,
      onDaySelected: onDaySelected,
      selectedDayPredicate: (data) =&gt;
          data.year == selectedDate.year &amp;&amp;
          data.month == selectedDate.month &amp;&amp;
          data.day == selectedDate.day,

      firstDay: DateTime(1800, 1, 1),
      lastDay: DateTime(3000, 1, 1),
      focusedDay: DateTime.now(),

      headerStyle: HeaderStyle(
        titleCentered: true,
        formatButtonVisible: false,
        titleTextStyle: TextStyle(
          fontWeight: FontWeight.w700,
          fontSize: 16.0,
        ),
      ),
      calendarStyle: CalendarStyle(
        isTodayHighlighted: false,
        defaultDecoration: BoxDecoration(
          borderRadius: BorderRadius.circular(6.0),
          color: LIGHT_GREY_COLOR,
        ),
        weekendDecoration: BoxDecoration(
          borderRadius: BorderRadius.circular(6.0),
          color: LIGHT_GREY_COLOR,
        ),
        selectedDecoration: BoxDecoration(
          borderRadius: BorderRadius.circular(6.0),
          border: Border.all(
            color: PRIMARY_COLOR,
            width: 1.0,
          ),
        ),
        defaultTextStyle: TextStyle(
          fontWeight: FontWeight.w600,
          color: DARK_GREY_COLOR,
        ),
        weekendTextStyle: TextStyle(
          fontWeight: FontWeight.w600,
          color: DARK_GREY_COLOR,
        ),
        selectedTextStyle: TextStyle(
          fontWeight: FontWeight.w600,
          color: PRIMARY_COLOR,
        ),
      ),
    );
  }
}</code></pre>
<h3 id="home_screendart">home_screen.dart</h3>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:calendar_scheduler/component/main_calender.dart&#39;;
import &#39;package:calendar_scheduler/component/schedule_card.dart&#39;;
import &#39;package:calendar_scheduler/component/today_banner.dart&#39;;
import &#39;package:calendar_scheduler/component/schedule_bottom_sheet.dart&#39;;
import &#39;package:calendar_scheduler/const/colors.dart&#39;;
import &#39;package:get_it/get_it.dart&#39;;
import &#39;package:calendar_scheduler/database/drift_database.dart&#39;;

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  State&lt;HomeScreen&gt; createState() =&gt; _HomeScreenState();
}

class _HomeScreenState extends State&lt;HomeScreen&gt; {
  DateTime selectedDate = DateTime.utc(
    DateTime.now().year,
    DateTime.now().month,
    DateTime.now().day,
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        backgroundColor: PRIMARY_COLOR,
        onPressed: () {
          showModalBottomSheet(
            context: context,
            isDismissible: true,
            builder: (_) =&gt; ScheduleBottomSheet(selectedDate: selectedDate),
            isScrollControlled: true,
          );
        },
        child: Icon(Icons.add),
      ),
      body: SafeArea(
        child: Column(
          children: [
            MainCalender(
              selectedDate: selectedDate,
              onDaySelected: onDaySelected,
            ),
            SizedBox(height: 8),
            StreamBuilder&lt;List&lt;Schedule&gt;&gt;(
              stream: GetIt.I&lt;LocalDatabase&gt;().watchSchedules(selectedDate),
              builder: (context, snapshot) {
                return TodayBanner(
                  selectedDate: selectedDate,
                  count: snapshot.data?.length ?? 0,
                );
              },
            ),
            SizedBox(height: 8),
            Expanded(
              child: StreamBuilder&lt;List&lt;Schedule&gt;&gt;(
                stream: GetIt.I&lt;LocalDatabase&gt;().watchSchedules(selectedDate),
                builder: (context, snapshot) {
                  if (!snapshot.hasData) return Container();

                  return ListView.builder(
                    itemCount: snapshot.data!.length,
                    itemBuilder: (context, index) {
                      final schedule = snapshot.data![index];

                      return Dismissible(
                        key: ObjectKey(schedule.id),
                        direction: DismissDirection.startToEnd,
                        onDismissed: (DismissDirection direction) {
                          GetIt.I&lt;LocalDatabase&gt;().removeSchedule(schedule.id);
                        },
                        child: Padding(
                          padding: const EdgeInsets.only(
                              bottom: 8, left: 8, right: 8),
                          child: ScheduleCard(
                            startTime: schedule.startTime,
                            endTime: schedule.endTime,
                            content: schedule.content,
                          ),
                        ),
                      );
                    },
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }

  void onDaySelected(DateTime selectedDate, DateTime focusedDate) {
    setState(() {
      this.selectedDate = selectedDate;
    });
  }
}</code></pre>
<hr>
<h2 id="6-전체-코드-설명-표">6. 전체 코드 설명 (표)</h2>
<h3 id="maincalender-위젯">MainCalender 위젯</h3>
<table>
<thead>
<tr>
<th>속성 / 코드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>OnDaySelected onDaySelected</code></td>
<td>날짜를 탭했을 때 실행할 함수를 외부에서 주입받는다</td>
</tr>
<tr>
<td><code>DateTime selectedDate</code></td>
<td>현재 선택된 날짜를 외부에서 주입받아 달력에 표시한다</td>
</tr>
<tr>
<td><code>locale: &#39;ko_kr&#39;</code></td>
<td>달력을 한국어로 표시한다</td>
</tr>
<tr>
<td><code>selectedDayPredicate: (data) =&gt; ...</code></td>
<td>연/월/일이 모두 일치하는 날짜를 &quot;선택된 날&quot;로 판단하는 조건을 설정한다</td>
</tr>
<tr>
<td><code>firstDay</code> / <code>lastDay</code></td>
<td>달력에서 이동 가능한 날짜의 범위를 설정한다</td>
</tr>
<tr>
<td><code>focusedDay: DateTime.now()</code></td>
<td>앱을 처음 켰을 때 화면에 보여질 기준 날짜를 오늘로 설정한다</td>
</tr>
<tr>
<td><code>titleCentered: true</code></td>
<td>달력 헤더의 월/년 제목을 가운데 정렬한다</td>
</tr>
<tr>
<td><code>formatButtonVisible: false</code></td>
<td>달력 형식을 바꾸는 버튼을 숨긴다</td>
</tr>
<tr>
<td><code>isTodayHighlighted: false</code></td>
<td>오늘 날짜에 자동으로 표시되는 강조 효과를 끈다</td>
</tr>
<tr>
<td><code>defaultDecoration</code></td>
<td>일반 날짜의 배경 스타일(둥근 모서리, 연한 회색)을 설정한다</td>
</tr>
<tr>
<td><code>selectedDecoration</code></td>
<td>선택된 날짜의 배경 스타일(테두리만 PRIMARY_COLOR로)을 설정한다</td>
</tr>
<tr>
<td><code>selectedTextStyle</code></td>
<td>선택된 날짜의 텍스트 색상을 PRIMARY_COLOR로 설정한다</td>
</tr>
</tbody></table>
<h3 id="homescreen-위젯">HomeScreen 위젯</h3>
<table>
<thead>
<tr>
<th>속성 / 코드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>selectedDate</code></td>
<td>현재 선택된 날짜를 저장하는 상태(State) 변수다</td>
</tr>
<tr>
<td><code>FloatingActionButton</code></td>
<td>우측 하단에 일정 추가 버튼을 표시한다</td>
</tr>
<tr>
<td><code>showModalBottomSheet(...)</code></td>
<td>버튼 클릭 시 화면 하단에서 입력 시트를 올려준다</td>
</tr>
<tr>
<td><code>isScrollControlled: true</code></td>
<td>키보드가 올라올 때 바텀 시트 높이가 함께 올라가도록 허용한다</td>
</tr>
<tr>
<td><code>StreamBuilder&lt;List&lt;Schedule&gt;&gt;</code></td>
<td>Stream 데이터를 감지해서 데이터가 바뀔 때마다 자동으로 UI를 다시 그린다</td>
</tr>
<tr>
<td><code>GetIt.I&lt;LocalDatabase&gt;().watchSchedules(selectedDate)</code></td>
<td>GetIt으로 DB 인스턴스를 가져와 해당 날짜의 일정을 실시간으로 감시한다</td>
</tr>
<tr>
<td><code>snapshot.data?.length ?? 0</code></td>
<td>일정 목록의 개수를 안전하게 가져오고, 데이터가 없으면 0을 반환한다</td>
</tr>
<tr>
<td><code>Dismissible</code></td>
<td>카드를 스와이프해서 삭제할 수 있게 해주는 위젯이다</td>
</tr>
<tr>
<td><code>key: ObjectKey(schedule.id)</code></td>
<td>Flutter가 각 카드를 고유하게 식별하기 위한 키를 부여한다</td>
</tr>
<tr>
<td><code>direction: DismissDirection.startToEnd</code></td>
<td>왼쪽→오른쪽 방향으로 밀었을 때만 삭제가 동작하도록 설정한다</td>
</tr>
<tr>
<td><code>onDismissed</code></td>
<td>카드가 완전히 밀려 사라진 직후 DB에서 해당 일정을 삭제한다</td>
</tr>
<tr>
<td><code>onDaySelected</code></td>
<td>달력에서 날짜를 탭하면 <code>setState()</code>로 selectedDate를 갱신해 화면을 다시 그린다</td>
</tr>
</tbody></table>
<hr>
<h2 id="7-실행-결과">7. 실행 결과</h2>
<ul>
<li>일정 카드를 왼쪽에서 오른쪽으로 밀면 카드가 사라지고, DB에서도 해당 일정이 삭제된다.</li>
<li>일정이 삭제되자마자 <code>TodayBanner</code>의 일정 개수가 자동으로 줄어든다.</li>
<li>날짜를 바꿔도 해당 날짜의 일정 개수가 배너에 실시간으로 반영된다.</li>
</ul>
<hr>
<h2 id="8-내가-바꿔본-점">8. 내가 바꿔본 점</h2>
<ul>
<li><code>direction</code>을 <code>DismissDirection.startToEnd</code>에서 <code>DismissDirection.endToStart</code>로 바꿨더니, 오른쪽→왼쪽으로 밀었을 때 삭제가 동작했다.</li>
<li><code>count: 0</code>으로 고정했을 때와 <code>snapshot.data?.length ?? 0</code>으로 연결했을 때를 비교했더니, 고정값은 일정을 추가해도 숫자가 변하지 않았고, Stream으로 연결하니 바로 반영되었다.</li>
</ul>
<hr>
<h2 id="9-에러--해결-방법">9. 에러 &amp; 해결 방법</h2>
<p>에러가 없었다.</p>
<hr>
<h2 id="10-오늘-알게-된-점">10. 오늘 알게 된 점</h2>
<ul>
<li>Flutter에서 데이터를 다루는 방식이 <code>Future</code>(한 번)와 <code>Stream</code>(계속 감시) 두 가지로 나뉜다는 것을 정리하니, 언제 어떤 걸 써야 하는지 기준이 생겼다. <strong>실시간으로 바뀌어야 하는 데이터는 Stream, 한 번만 가져오면 되는 데이터는 Future</strong>를 쓰면 된다.</li>
<li>상태 관리에서 &quot;상태 = 데이터&quot;라는 것이 확실히 와닿았다. <code>setState()</code>를 호출해야 Flutter가 <code>build()</code>를 다시 실행한다는 흐름을 이해했다.</li>
<li>캐싱은 단순히 변수에 저장하는 것이지만, 서버와 사용자 사이의 지연을 줄여주는 중요한 개념이라는 것을 알게 됐다. 자주 바뀌지 않는 데이터는 캐싱해두면 UX가 훨씬 자연스러워진다.</li>
</ul>
<hr>
<h2 id="11-아직-헷갈리는-점">11. 아직 헷갈리는 점</h2>
<ul>
<li>캐시 무효화: 캐시에 저장된 데이터가 서버에서 바뀌었을 때 어떻게 갱신해야 하는지 (언제 캐시를 날리고 다시 요청해야 하는지) 기준이 아직 헷갈린다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[#41 #리액트 프로젝트 실행시켜보기]]></title>
            <link>https://velog.io/@int_1sy/41-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%A4%ED%96%89%EC%8B%9C%EC%BC%9C%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@int_1sy/41-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%A4%ED%96%89%EC%8B%9C%EC%BC%9C%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 23 May 2026 12:28:28 GMT</pubDate>
            <description><![CDATA[<h2 id="react란-무엇인가">React란 무엇인가?</h2>
<p><strong>프론트엔드</strong>란 사용자가 직접 눈으로 보는 화면을 말합니다.</p>
<blockquote>
<p>버튼, 텍스트, 댓글창, 메뉴, 로그인 화면, 좋아요 버튼, 게시글 목록, 프로필 카드 ...</p>
</blockquote>
<h3 id="react의-핵심-철학">React의 핵심 철학</h3>
<blockquote>
<p><strong>화면을 작은 조각(컴포넌트)으로 나눠서 만든다</strong></p>
</blockquote>
<p>레고 블록처럼 각각의 조각을 만들어두고, 필요한 곳에 끼워 맞추는 방식입니다.<br>덕분에 코드를 재사용하기 쉽고, 수정할 때 해당 조각만 건드리면 됩니다.</p>
<hr>
<h2 id="프로젝트-시작--vite로-react-세팅">프로젝트 시작 — Vite로 React 세팅</h2>
<p>React 프로젝트는 <strong>Vite</strong>를 사용해서 시작합니다.</p>
<pre><code class="language-bash">npm create vite@latest my-app 
cd my-app
npm install
npm run dev</code></pre>
<h3 id="주요-파일-구조">주요 파일 구조</h3>
<table>
<thead>
<tr>
<th>파일</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>main.jsx</code></td>
<td>React를 시작하는 진입점 파일</td>
</tr>
<tr>
<td><code>App.jsx</code></td>
<td>화면에 보이는 대부분의 내용이 시작되는 곳</td>
</tr>
<tr>
<td><code>src/</code></td>
<td>source의 줄임말, 실제 작업하는 소스 파일들이 위치</td>
</tr>
</tbody></table>
<hr>
<h3 id="jsx-작성-시-꼭-기억할-규칙">JSX 작성 시 꼭 기억할 규칙</h3>
<ol>
<li><p><strong>return 내부의 요소는 반드시 하나의 덩어리로 묶여야 한다</strong><br>→ 여러 태그를 나란히 쓰고 싶다면 <code>&lt;&gt;...&lt;/&gt;</code> (Fragment)로 감싸기</p>
</li>
<li><p><strong>클래스 지정은 <code>class</code>가 아니라 <code>className</code></strong><br>→ <code>class</code>는 JavaScript의 예약어라서 JSX에서는 <code>className</code>을 사용</p>
</li>
<li><p><strong>중괄호 <code>{}</code>로 JavaScript 표현식 삽입</strong><br>→ <code>{user.name}</code>, <code>{count + 1}</code> 처럼 변수나 연산을 넣을 수 있음</p>
</li>
</ol>
<hr>
<h2 id="컴포넌트--화면을-조각으로-나누기">컴포넌트 — 화면을 조각으로 나누기</h2>
<p>React에서 <strong>컴포넌트</strong>는 화면의 한 조각을 만드는 함수입니다.</p>
<pre><code class="language-jsx">// ProfileCard라는 컴포넌트를 만들고
function ProfileCard() {
  return &lt;p&gt;저는 정원입니다.&lt;/p&gt;;
}

// App에서 레고처럼 끼워서 사용
function App() {
  return (
    &lt;div&gt;
      &lt;h1&gt;React 스터디 1일차&lt;/h1&gt;
      &lt;ProfileCard /&gt;  {/* 컴포넌트 사용! */}
    &lt;/div&gt;
  );
}</code></pre>
<hr>
<h2 id="state--변하는-값을-react가-관리한다">State — 변하는 값을 React가 관리한다</h2>
<h3 id="왜-일반-변수로는-안-될까">왜 일반 변수로는 안 될까?</h3>
<pre><code class="language-jsx">// 이렇게 하면 버튼을 눌러도 화면이 안 바뀜
let count = 0;

function increase() {
  count = count + 1; // 값은 바뀌지만 React는 모른다!
}</code></pre>
<p>일반 변수(<code>let</code>)는 값이 바뀌어도 React가 감지하지 못해서 <strong>화면이 다시 그려지지 않습니다.</strong></p>
<h3 id="usestate--react에게-변수-값이-바뀌면-자동으로-화면도-업데이트-해줘">useState — React에게 &quot;변수 값이 바뀌면 자동으로 화면도 업데이트 해줘&quot;</h3>
<pre><code class="language-jsx">import { useState } from &quot;react&quot;;

const [count, setCount] = useState(0);
//     ↑ 현재 값   ↑ 값을 바꾸는 함수   ↑ 초기값</code></pre>
<p><code>useState</code>를 사용하면 값이 바뀔 때마다 <strong>React가 자동으로 화면을 다시 그려줍니다.</strong></p>
<h3 id="state가-필요한-대표적인-예시">State가 필요한 대표적인 예시</h3>
<ul>
<li>좋아요 수</li>
<li>장바구니 수량</li>
<li>다크 모드 ON/OFF</li>
<li>새 댓글 추가</li>
<li>로그인 여부 (로그인하면 마이페이지 보이기)</li>
</ul>
<hr>
<h2 id="실습-1--카운터-만들기">실습 1 — 카운터 만들기</h2>
<pre><code class="language-jsx">import { useState } from &quot;react&quot;;

function App() {
  const [count, setCount] = useState(0);

  function increase() {
    setCount(count + 1);
  }
  function decrease() {
    setCount(count - 1);
  }
  function reset() {
    setCount(0);
  }

  return (
    &lt;&gt;
      &lt;h1&gt;카운터&lt;/h1&gt;
      &lt;p&gt;현재 숫자 : {count}&lt;/p&gt;
      &lt;button onClick={increase}&gt;+1 증가&lt;/button&gt;
      &lt;button onClick={decrease}&gt;-1 감소&lt;/button&gt;
      &lt;button onClick={reset}&gt;초기화&lt;/button&gt;
    &lt;/&gt;
  );
}</code></pre>
<p>버튼을 클릭하면 <code>setCount</code>가 호출되고, React가 새로운 <code>count</code> 값으로 화면을 다시 렌더링합니다.</p>
<hr>
<h2 id="실습-2-숙제--usestate-응용-3가지">실습 2 (숙제) — useState 응용 3가지</h2>
<h3 id="1-감정-카운터">1. 감정 카운터</h3>
<pre><code class="language-jsx">const [happy, setHappy] = useState(0);
const [sad, setSad] = useState(0);

&lt;button onClick={() =&gt; setHappy(happy + 1)}&gt;기쁨 😊 : {happy}&lt;/button&gt;
&lt;button onClick={() =&gt; setSad(sad + 1)}&gt;슬픔 😭 : {sad}&lt;/button&gt;</code></pre>
<h3 id="2-응원하기-버튼">2. 응원하기 버튼</h3>
<pre><code class="language-jsx">const [cheer, setCheer] = useState(0);

&lt;p&gt;지금까지 {cheer}번 응원받았습니다!&lt;/p&gt;
&lt;button onClick={() =&gt; setCheer(cheer + 1)}&gt;파이팅&lt;/button&gt;</code></pre>
<h3 id="3-프레임워크-투표앱">3. 프레임워크 투표앱</h3>
<pre><code class="language-jsx">const [reactVote, setReactVote] = useState(0);
const [vueVote, setVueVote] = useState(0);
const [svelteVote, setSvelteVote] = useState(0);

&lt;button onClick={() =&gt; setReactVote(reactVote + 1)}&gt;React&lt;/button&gt;
&lt;button onClick={() =&gt; setVueVote(vueVote + 1)}&gt;Vue&lt;/button&gt;
&lt;button onClick={() =&gt; setSvelteVote(svelteVote + 1)}&gt;Svelte&lt;/button&gt;</code></pre>
<p>State가 여러 개일 때는 <strong>각각 <code>useState</code>를 따로 선언</strong>합니다.<br>서로 독립적으로 관리되기 때문에 한 값이 바뀌어도 다른 값에는 영향을 주지 않습니다.
<img src="https://velog.velcdn.com/images/int_1sy/post/73d4f52d-33df-4ebd-8932-5d2120a4abac/image.png" alt="">
<img src="https://velog.velcdn.com/images/int_1sy/post/a3c982e8-d185-444d-aa2d-c97115ca7326/image.png" alt=""></p>
<hr>
<h2 id="핵심-정리">핵심 정리</h2>
<table>
<thead>
<tr>
<th>개념</th>
<th>한 줄 요약</th>
</tr>
</thead>
<tbody><tr>
<td><strong>컴포넌트</strong></td>
<td>화면을 이루는 작은 조각 (함수 형태)</td>
</tr>
<tr>
<td><strong>JSX</strong></td>
<td>JS 안에 HTML처럼 쓰는 React 전용 문법</td>
</tr>
<tr>
<td><strong><code>useState</code></strong></td>
<td>변하는 값을 React가 감지하고 화면을 다시 그리게 해주는 기능</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[#40 #Node - 다른 파일에서 함수 가져와서 사용하기]]></title>
            <link>https://velog.io/@int_1sy/40-Node-%EB%8B%A4%EB%A5%B8-%ED%8C%8C%EC%9D%BC%EC%97%90%EC%84%9C-%ED%95%A8%EC%88%98-%EA%B0%80%EC%A0%B8%EC%99%80%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@int_1sy/40-Node-%EB%8B%A4%EB%A5%B8-%ED%8C%8C%EC%9D%BC%EC%97%90%EC%84%9C-%ED%95%A8%EC%88%98-%EA%B0%80%EC%A0%B8%EC%99%80%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 23 May 2026 06:50:18 GMT</pubDate>
            <description><![CDATA[<h2 id="왜-코드를-여러-파일로-나눠야-할까">왜 코드를 여러 파일로 나눠야 할까?</h2>
<p>처음 코딩을 배울 때는 하나의 파일에 모든 코드를 작성하게 됩니다.<br>하지만 프로젝트가 커질수록 <strong>한 파일에 코드를 길게 적으면 이런 문제들이 생깁니다.</strong></p>
<table>
<thead>
<tr>
<th>문제</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>가독성 저하</td>
<td>어디에 어떤 코드가 있는지 찾기 어렵다</td>
</tr>
<tr>
<td>디버깅 어려움</td>
<td>오류가 났을 때 원인을 찾기 어렵다</td>
</tr>
<tr>
<td>사이드 이펙트</td>
<td>기능을 수정하다가 다른 기능까지 망가질 수 있다</td>
</tr>
<tr>
<td>협업 불편</td>
<td>여러 사람이 함께 작업하기 어렵다</td>
</tr>
<tr>
<td>재사용 불가</td>
<td>코드를 다른 곳에서 재사용하기 어렵다</td>
</tr>
</tbody></table>
<p>이 문제를 해결하는 것이 바로 <strong>모듈 시스템</strong>입니다.</p>
<hr>
<h2 id="nodejs-모듈-시스템의-핵심-2가지">Node.js 모듈 시스템의 핵심 2가지</h2>
<h3 id="moduleexports--이-기능을-다른-파일에서도-쓸-수-있게-내보낼게"><code>module.exports</code> — &quot;이 기능을 다른 파일에서도 쓸 수 있게 내보낼게&quot;</h3>
<pre><code class="language-js">// math.js

function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

function multiply(a, b) {
  return a * b;
}

function div(a, b) {
  return a / b;
}

function power(a, b) {
  return a ** b;
}

function sqare(a) {
  return a * a;
}

module.exports = {
  add,
  subtract,
  multiply,
  div,
  power,
  sqare,
};</code></pre>
<blockquote>
<p> <code>{ add: add }</code> 처럼 키와 값이 같을 때는 <code>{ add }</code> 로 줄여 쓸 수 있습니다!  
자바스크립트에서 앞뒤가 똑같으면 하나만 써도 됩니다.</p>
</blockquote>
<hr>
<h3 id="require--저-파일에-있는-기능을-가져와서-사용할게"><code>require()</code> — &quot;저 파일에 있는 기능을 가져와서 사용할게&quot;</h3>
<pre><code class="language-js">// app.js

const { add, subtract, multiply, div, power, sqare } = require(&quot;./math&quot;);</code></pre>
<p><code>require(&quot;./math&quot;)</code>로 <code>math.js</code>에서 내보낸 객체 전체를 가져온 다음,<br><strong>구조 분해 할당</strong>으로 필요한 함수만 꺼내서 사용합니다.</p>
<hr>
<h2 id="processargv--터미널에서-입력값-받기"><code>process.argv</code> — 터미널에서 입력값 받기</h2>
<p>Node.js에서 터미널로 직접 인수를 전달하려면 <code>process.argv</code>를 사용합니다.</p>
<pre><code>node app.js add 10 5</code></pre><p><code>process.argv</code>는 배열이고, 각 인덱스의 의미는 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>인덱스</th>
<th>내용</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>[0]</code></td>
<td>Node 실행 경로</td>
<td>컴퓨터 내의 node 실행 파일이 위치한 절대 경로</td>
</tr>
<tr>
<td><code>[1]</code></td>
<td>스크립트 파일 경로</td>
<td>현재 실행 중인 자바스크립트 파일의 절대 경로</td>
</tr>
<tr>
<td><code>[2]</code></td>
<td>첫 번째 추가 인수</td>
<td>사용자가 직접 입력한 첫 번째 데이터 (파라미터)</td>
</tr>
<tr>
<td><code>[3]</code></td>
<td>두 번째 추가 인수</td>
<td>사용자가 입력한 두 번째 데이터</td>
</tr>
</tbody></table>
<hr>
<h2 id="실습--터미널-계산기-만들기">실습 — 터미널 계산기 만들기</h2>
<p><code>math.js</code>의 함수들을 <code>app.js</code>에서 불러와, 터미널 입력값으로 연산을 수행하는 계산기를 만들어봤습니다.</p>
<pre><code class="language-js">// app.js

const { add, subtract, multiply, div, power, sqare } = require(&quot;./math&quot;);

const operator = process.argv[2];      // 연산자
const num1 = Number(process.argv[3]);  // 첫 번째 숫자
const num2 = Number(process.argv[4]);  // 두 번째 숫자

if (operator === &quot;add&quot;) {
  console.log(add(num1, num2));
} else if (operator === &quot;subtract&quot;) {
  console.log(subtract(num1, num2));
} else if (operator === &quot;multiply&quot;) {
  console.log(multiply(num1, num2));
} else if (operator === &quot;div&quot;) {
  console.log(div(num1, num2));
} else if (operator === &quot;power&quot;) {
  console.log(power(num1, num2));
} else if (operator === &quot;sqare&quot;) {
  console.log(sqare(num1));
} else {
  console.log(&quot;지원하지 않는 연산자입니다&quot;);
}</code></pre>
<h3 id="실행-예시">실행 예시</h3>
<pre><code class="language-bash">node app.js add 10 5       # 15
node app.js subtract 10 5  # 5
node app.js multiply 10 5  # 50
node app.js div 10 5       # 2
node app.js power 2 8      # 256
node app.js sqare 9        # 81</code></pre>
<blockquote>
<p><code>process.argv</code>로 받아오는 값은 <strong>문자열(string)</strong> 이기 때문에,<br>숫자 연산을 하려면 반드시 <code>Number()</code>로 변환해줘야 합니다!</p>
</blockquote>
<hr>
<h2 id="정리">정리</h2>
<pre><code>math.js  ──[module.exports]──▶  app.js
         ◀──[require()]──────</code></pre><ul>
<li><strong><code>module.exports</code></strong> : 기능을 파일 밖으로 내보낸다</li>
<li><strong><code>require()</code></strong> : 다른 파일의 기능을 내 파일로 가져온다</li>
<li><strong><code>process.argv</code></strong> : 터미널 실행 시 입력한 값을 배열로 받아온다</li>
</ul>
<p>모듈로 파일을 분리하면 코드가 훨씬 <strong>읽기 쉽고, 수정하기 쉽고, 재사용하기 쉬워집니다.</strong>  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[#39 #이해를 위한 벨로그 작성하는 방법]]></title>
            <link>https://velog.io/@int_1sy/%EC%9D%B4%ED%95%B4%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%B2%A8%EB%A1%9C%EA%B7%B8-%EC%9E%91%EC%84%B1%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@int_1sy/%EC%9D%B4%ED%95%B4%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%B2%A8%EB%A1%9C%EA%B7%B8-%EC%9E%91%EC%84%B1%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Thu, 14 May 2026 22:32:05 GMT</pubDate>
            <description><![CDATA[<h1 id="공부한-걸-그냥-올리지-말고-이렇게-정리해보자">공부한 걸 그냥 올리지 말고, 이렇게 정리해보자</h1>
<blockquote>
<p>그냥 하는 공부가 아니라, <strong>이해를 위한 벨로그</strong>를 쓰는 방법을 정리했다.
코드를 해석하는 수준을 넘어서, 내가 설명할 수 있을 때까지 이해했는지 확인하는 글쓰기 방법이다.</p>
</blockquote>
<hr>
<h2 id="왜-벨로그를-써야-하나">왜 벨로그를 써야 하나?</h2>
<p>공부하고 그냥 넘어가면 3일 뒤에 기억이 안 난다.
그런데 글로 쓰려고 하면 <strong>내가 뭘 모르는지</strong>가 바로 드러난다.</p>
<blockquote>
<p>설명할 수 없으면 아직 이해한 게 아니다.</p>
</blockquote>
<hr>
<h2 id="나쁜-제목-vs-좋은-제목">나쁜 제목 vs 좋은 제목</h2>
<p>포스팅 제목부터 구체적으로 쓰는 게 중요하다.
제목이 구체적이면, 글 내용도 자연스럽게 구체적이 된다.</p>
<table>
<thead>
<tr>
<th>나쁜 제목</th>
<th>좋은 제목</th>
</tr>
</thead>
<tbody><tr>
<td>Flutter 공부함</td>
<td>Flutter 프로젝트 생성 후 main.dart 구조 이해하기</td>
</tr>
<tr>
<td>Dart 배움</td>
<td>StatelessWidget과 build 메서드 이해하기</td>
</tr>
<tr>
<td>앱 만들기 함</td>
<td>Scaffold, Center, Text 위젯으로 첫 화면 만들기</td>
</tr>
</tbody></table>
<hr>
<h2 id="포스팅-구조--9단계-템플릿">포스팅 구조 — 9단계 템플릿</h2>
<h3 id="1단계-오늘의-목표">1단계. 오늘의 목표</h3>
<p>오늘 뭘 배웠는지 <strong>한 문장</strong>으로 정리한다.</p>
<pre><code>오늘은 Flutter에서 화면을 구성하는 기본 단위인 Widget을 배웠다.</code></pre><hr>
<h3 id="2단계-핵심-개념">2단계. 핵심 개념</h3>
<p>개념을 설명할 때는 아래 3가지 질문에 답하는 방식으로 쓴다.</p>
<ul>
<li><strong>이 개념이 뭐야?</strong></li>
<li><strong>왜 필요해?</strong></li>
<li><strong>언제 써?</strong></li>
</ul>
<p><strong>예시 — 개념 설명 문장 패턴</strong></p>
<pre><code>______는 Flutter에서 _____할 때 사용하는 위젯이다.
______는 Dart에서 _____을 표현하기 위해 사용하는 문법이다.
이 개념은 ________ 할 때 필요하다.</code></pre><pre><code>실제 예시)
Column은 Flutter에서 여러 위젯을 세로로 배치할 때 사용하는 위젯이다.
이 개념은 텍스트, 버튼 등을 위아래로 나열할 때 필요하다.</code></pre><hr>
<h3 id="3단계-코드-예시">3단계. 코드 예시</h3>
<p>핵심 코드만 올린다. 전체 파일을 통째로 올리면 읽기 어렵다.</p>
<pre><code class="language-dart">// Container 위젯 기본 예시
Container(
  width: 100,
  height: 100,
  color: Colors.blue,
)</code></pre>
<blockquote>
<p>Container는 화면에서 하나의 영역을 만들 때 사용하는 위젯이다.
크기, 색상, 여백, 테두리를 설정할 수 있어서 디자인의 기본 박스처럼 쓰인다.</p>
</blockquote>
<hr>
<h3 id="4단계-코드-설명">4단계. 코드 설명</h3>
<p>코드 한 줄씩 이게 무슨 의미인지 설명한다.</p>
<p><strong>코드 설명 문장 패턴</strong></p>
<pre><code>이 코드는 _______를 의미한다.
_____속성은 _______을 설정한다.
____안에는 _______이 들어간다.</code></pre><p><strong>예시 — main() 코드 설명</strong></p>
<pre><code class="language-dart">void main() {
  runApp(MyApp());
}</code></pre>
<ul>
<li><code>main()</code> 함수는 Dart 프로그램이 시작될 때 가장 먼저 실행되는 함수다.</li>
<li><code>runApp()</code>은 Flutter 앱을 실행하는 함수다.</li>
<li><code>MyApp()</code>은 내가 만든 앱 화면의 시작점이다.</li>
</ul>
<blockquote>
<p>즉 이 코드는 &quot;MyApp이라는 위젯을 앱으로 실행해라&quot;라는 의미다.</p>
</blockquote>
<hr>
<h3 id="5단계-실행-결과">5단계. 실행 결과</h3>
<p>화면이나 콘솔이 어떻게 나왔는지 설명한다.
가능하면 스크린샷을 같이 올리면 좋다.</p>
<p><strong>실행 결과 문장 패턴</strong></p>
<pre><code>실행 결과 __________가 화면에 표시되었다.
값을 ______에서 _______로 바꿨더니 ________이 변경되었다.</code></pre><hr>
<h3 id="6단계-내가-직접-바꿔본-것">6단계. 내가 직접 바꿔본 것</h3>
<p>공부에서 가장 중요한 단계다.
그냥 읽는 게 아니라 <strong>직접 값을 바꿔보고 어떻게 달라지는지</strong> 확인한다.</p>
<p><strong>문장 패턴</strong></p>
<pre><code>처음에는 ____________이었는데, _______을 바꿔서 ________이 달라졌다.</code></pre><p><strong>예시</strong></p>
<pre><code class="language-dart">Text(
  &#39;Hello Flutter&#39;,
  style: TextStyle(
    fontSize: 24,       // 24 → 40으로 바꿔봄
    color: Colors.blue, // blue → red로 바꿔봄
  ),
)</code></pre>
<ul>
<li><code>fontSize</code>를 24에서 40으로 바꿨더니 글자가 더 크게 보였다.</li>
<li><code>Colors.blue</code>를 <code>Colors.red</code>로 바꿨더니 글자 색상이 빨간색으로 바뀌었다.</li>
</ul>
<hr>
<h3 id="7단계-에러와-해결-방법">7단계. 에러와 해결 방법</h3>
<blockquote>
<p>에러를 정리하는 것이 실력 향상에 가장 빠른 지름길이다.</p>
</blockquote>
<p>에러가 무서운 게 아니라, <strong>기록하지 않는 게 더 아깝다.</strong></p>
<p><strong>에러 정리 패턴</strong></p>
<pre><code>[에러 메시지]
- 원인: 
- 해결 방법:
- 알게 된 점:</code></pre><p><strong>예시</strong></p>
<pre><code>에러: Expected to find &#39;,&#39;

원인: 위젯 안에서 속성을 작성할 때 쉼표를 빠뜨렸다.
해결 방법: Text() 위젯 뒤에 쉼표를 추가했다.
알게 된 점: Flutter는 위젯이 중첩 구조이기 때문에 괄호와 쉼표를 꼼꼼히 확인해야 한다.</code></pre><p><strong>처음에 원인을 잘못 짚었을 때 패턴</strong></p>
<pre><code>처음에는 ____________때문이라고 생각했다.
확인해보니 ________가 문제였다.
_______을 수정해서 문제를 해결했다.
다음에는 _______________를 주의해야겠다.</code></pre><hr>
<h3 id="8단계-오늘-알게-된-점">8단계. 오늘 알게 된 점</h3>
<p>이 섹션은 <strong>내 말로</strong> 쓰는 게 핵심이다.
검색해서 나온 설명을 그대로 붙여넣으면 의미 없다.</p>
<p><strong>회고 문장 패턴</strong></p>
<pre><code>오늘 가장 중요하게 이해한 내용은 _____다.
처음에는 ________라고 생각했는데, 알고 보니 ________였다.</code></pre><hr>
<h3 id="9단계-아직-헷갈리는-점">9단계. 아직 헷갈리는 점</h3>
<p>모르는 걸 솔직하게 남긴다.
이 내용이 <strong>다음 포스팅의 1단계 목표</strong>로 자연스럽게 이어진다.</p>
<p><strong>회고 문장 패턴</strong></p>
<pre><code>아직 ________부분은 헷갈린다.
다음에는 _________을 더 연습해보고 싶다.</code></pre><hr>
<h2 id="전체-구조-요약">전체 구조 요약</h2>
<pre><code>1. 오늘의 목표    → 한 문장으로
2. 핵심 개념      → 이게 뭐야? 왜 필요해? 언제 써?
3. 코드 예시      → 핵심 코드만
4. 코드 설명      → 한 줄씩, 내 말로
5. 실행 결과      → 어떻게 나왔는지
6. 직접 바꿔본 것 → 값을 바꿔보고 어떻게 달라졌는지
7. 에러와 해결    → 에러 메시지, 원인, 해결, 교훈
8. 오늘 알게 된 점 → 내 말로
9. 아직 헷갈리는 점 → 다음 포스팅의 시작점</code></pre><hr>
<h2 id="마지막으로">마지막으로</h2>
<p>글을 잘 쓰려고 하지 않아도 된다.
<strong>내가 나중에 읽었을 때 이해되는 글</strong>이면 충분하다.</p>
<p>처음에는 문장이 어색해도 괜찮다.
쓰다 보면 코드를 보는 눈도, 글 쓰는 실력도 같이 늘어난다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[#38 #Flutter Drift 로컬 데이터베이스 연동]]></title>
            <link>https://velog.io/@int_1sy/38-Flutter-Drift-%EB%A1%9C%EC%BB%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%97%B0%EB%8F%99</link>
            <guid>https://velog.io/@int_1sy/38-Flutter-Drift-%EB%A1%9C%EC%BB%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%97%B0%EB%8F%99</guid>
            <pubDate>Mon, 11 May 2026 14:37:09 GMT</pubDate>
            <description><![CDATA[<h1 id="flutter-drift로-로컬-db-만들기--일정-앱에-sqlite-연결하기">Flutter Drift로 로컬 DB 만들기 — 일정 앱에 SQLite 연결하기</h1>
<h2 id="1-오늘의-목표">1. 오늘의 목표</h2>
<p>Flutter에서 Drift 패키지를 사용해 SQLite 데이터베이스를 연결하고, 일정(Schedule) 데이터를 저장·조회·삭제하는 구조를 설계했다.</p>
<hr>
<h2 id="2-핵심-개념">2. 핵심 개념</h2>
<h3 id="drift의-의미">Drift의 의미</h3>
<p>Drift는 Flutter에서 SQLite 데이터베이스를 Dart 코드로 다룰 수 있게 해주는 ORM(Object-Relational Mapping) 패키지다.</p>
<h3 id="필요성">필요성</h3>
<p>SQLite를 직접 쓰면 쿼리를 문자열로 직접 써야 한다. 오타가 나도 컴파일 타임에 잡히지 않는다. Drift를 쓰면 쿼리를 Dart 코드로 작성할 수 있어서 자동완성도 되고, 에러도 미리 잡힌다.</p>
<h3 id="사용할-때">사용할 때</h3>
<p>앱을 껐다 켜도 데이터가 남아있어야 할 때 — 일정, 메모, 즐겨찾기 같은 로컬 저장 기능을 만들 때 사용한다.</p>
<hr>
<h2 id="3-핵심-코드-파일별">3. 핵심 코드 (파일별)</h2>
<h3 id="테이블-스키마--scheduledart">테이블 스키마 — <code>schedule.dart</code></h3>
<pre><code class="language-dart">class Schedules extends Table {
  IntColumn get id =&gt; integer().autoIncrement()();
  TextColumn get content =&gt; text()();
  DateTimeColumn get date =&gt; dateTime()();
  IntColumn get startTime =&gt; integer()();
  IntColumn get endTime =&gt; integer()();
}</code></pre>
<h3 id="데이터베이스-클래스--drift_databasedart">데이터베이스 클래스 — <code>drift_database.dart</code></h3>
<pre><code class="language-dart">@DriftDatabase(tables: [Schedules])
class LocalDatabase extends _$LocalDatabase {
  LocalDatabase() : super(_openConnection());

  Stream&lt;List&lt;Schedule&gt;&gt; watchSchedules(DateTime date) =&gt;
      (select(schedules)..where((tbl) =&gt; tbl.date.equals(date))).watch();

  Future&lt;int&gt; createSchedule(SchedulesCompanion data) =&gt;
      into(schedules).insert(data);

  Future&lt;int&gt; removeSchedule(int id) =&gt;
      (delete(schedules)..where((tbl) =&gt; tbl.id.equals(id))).go();

  @override
  int get schemaVersion =&gt; 1;
}</code></pre>
<h3 id="db-파일-연결--_openconnection">DB 파일 연결 — <code>_openConnection()</code></h3>
<pre><code class="language-dart">LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    final dbFolder = await getApplicationCacheDirectory();
    final file = File(p.join(dbFolder.path, &#39;db.sqlite&#39;));
    return NativeDatabase(file);
  });
}</code></pre>
<hr>
<h2 id="4-코드-설명-한-줄씩-내-말로">4. 코드 설명 (한 줄씩, 내 말로)</h2>
<h3 id="테이블-스키마--scheduledart-1">테이블 스키마 — <code>schedule.dart</code></h3>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>extends Table</code></td>
<td>테이블 선언</td>
<td>Drift에게 이 Dart 클래스가 SQLite 테이블임을 알림</td>
</tr>
<tr>
<td><code>integer().autoIncrement()()</code></td>
<td>자동 증가 기본키</td>
<td>INSERT 시 id가 1씩 자동 증가. SQL의 PRIMARY KEY AUTO_INCREMENT와 동일</td>
</tr>
<tr>
<td><code>dateTime()()</code></td>
<td>날짜 컬럼</td>
<td>Dart의 DateTime 타입을 저장. SQLite 내부에는 Unix timestamp로 저장되고, Drift가 꺼낼 때 DateTime으로 변환</td>
</tr>
<tr>
<td><code>text()()</code></td>
<td>문자열 컬럼</td>
<td>일정 내용(content)을 저장하는 텍스트 컬럼</td>
</tr>
<tr>
<td><code>integer()()</code></td>
<td>정수 컬럼</td>
<td>startTime / endTime 저장. 시 단위 정수로 표현</td>
</tr>
</tbody></table>
<hr>
<h3 id="crud-메서드--drift_databasedart">CRUD 메서드 — <code>drift_database.dart</code></h3>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>select(schedules)..where(...)</code></td>
<td>조건부 SELECT</td>
<td><code>..</code> cascade 연산자로 같은 객체에 메서드를 이어서 호출. 중간 변수 없이 코드가 짧아짐</td>
</tr>
<tr>
<td><code>.watch()</code></td>
<td>실시간 Stream 구독</td>
<td>DB 변경 시 자동으로 UI 갱신. <code>StreamBuilder</code>와 함께 사용</td>
</tr>
<tr>
<td><code>.get()</code></td>
<td>1회성 조회</td>
<td>쿼리를 한 번만 실행하고 Future 반환. 데이터 변경을 감지하지 않음</td>
</tr>
<tr>
<td><code>SchedulesCompanion</code></td>
<td>INSERT 래퍼 클래스</td>
<td>Drift가 자동 생성. Schedule 모델과 달리 일부 필드만 넣어도 됨</td>
</tr>
<tr>
<td><code>into(schedules).insert(data)</code></td>
<td>INSERT</td>
<td>Companion 객체를 받아 DB에 새 행 삽입</td>
</tr>
<tr>
<td><code>(delete(schedules)..where(...)).go()</code></td>
<td>DELETE</td>
<td>where로 조건 지정 후 <code>.go()</code>로 실행</td>
</tr>
</tbody></table>
<hr>
<h3 id="db-파일-연결--_openconnection-1">DB 파일 연결 — <code>_openConnection()</code></h3>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>LazyDatabase</code></td>
<td>지연 연결</td>
<td>실제로 DB를 사용하는 시점까지 연결을 미룸. 앱 초기 로딩 속도에 유리</td>
</tr>
<tr>
<td><code>getApplicationCacheDirectory()</code></td>
<td>앱 전용 캐시 경로 조회</td>
<td><code>path_provider</code> 패키지가 iOS / Android / macOS 각 OS에 맞는 경로를 자동으로 반환</td>
</tr>
<tr>
<td><code>p.join(dbFolder.path, &#39;db.sqlite&#39;)</code></td>
<td>OS 독립적 경로 생성</td>
<td><code>path</code> 패키지. <code>/</code> vs <code>\</code> 같은 OS별 구분자 차이를 자동 처리</td>
</tr>
<tr>
<td><code>NativeDatabase(file)</code></td>
<td>SQLite 파일 열기</td>
<td>지정 경로에 db.sqlite 파일을 생성하거나 열어 실제 연결 수립</td>
</tr>
</tbody></table>
<hr>
<h2 id="5-실행-결과">5. 실행 결과</h2>
<p>달력에서 날짜를 선택하면 해당 날짜의 일정 목록이 아래에 표시된다. 일정을 추가하면 리스트에 즉시 반영되고, 삭제하면 바로 사라진다. <code>watchSchedules()</code>가 <code>Stream</code>을 반환하기 때문에, <code>StreamBuilder</code>와 연결하면 따로 <code>setState()</code>를 호출하지 않아도 UI가 자동 갱신된다.</p>
<hr>
<h2 id="6-직접-바꿔본-것">6. 직접 바꿔본 것</h2>
<p>처음에는 <code>.watch()</code> 대신 <code>.get()</code>을 써봤다. <code>get()</code>은 <code>Future</code>를 반환하기 때문에 일정을 추가해도 화면이 바뀌지 않았다. 다시 새로고침(화면 이동 후 돌아오기)을 해야만 목록이 갱신되었다.</p>
<p><code>.watch()</code>로 바꾸니까 일정을 추가/삭제하는 순간 화면이 자동으로 갱신되었다. <strong><code>get()</code>은 스냅샷, <code>watch()</code>는 실시간 구독</strong>이라고 이해하면 딱 맞다.</p>
<hr>
<h2 id="7-오늘-알게-된-점">7. 오늘 알게 된 점</h2>
<p>가장 중요하게 이해한 건 <strong>Drift의 코드 생성 구조</strong>다.</p>
<p>내가 <code>schedule.dart</code>에서 테이블을 정의하면, <code>build_runner</code>가 <code>drift_database.g.dart</code>를 자동으로 만들어준다. 내가 쓰는 <code>LocalDatabase</code>는 그 생성된 파일의 <code>_$LocalDatabase</code>를 상속한다. 처음엔 <code>_$LocalDatabase</code>가 어디서 나온 건지 몰라서 당황했는데, <code>.g.dart</code> 파일을 보니 거기 있었다.</p>
<p>처음에는 &quot;DB에서 데이터를 가져오면 그냥 Future로 받으면 되는 거 아닌가?&quot; 라고 생각했는데, 알고 보니 일정 앱처럼 데이터가 자주 바뀌는 경우엔 <code>Stream</code>으로 구독하는 게 훨씬 편했다. 데이터가 바뀔 때마다 UI를 직접 갱신하는 코드를 쓸 필요가 없기 때문이다.</p>
<hr>
<h2 id="8-아직-헷갈리는-점">8. 아직 헷갈리는 점</h2>
<p><code>schemaVersion</code>을 올릴 때 마이그레이션을 어떻게 작성하는지 아직 모른다. 지금은 버전이 1이라 괜찮지만, 나중에 테이블 컬럼을 추가하거나 변경하면 기존 데이터가 어떻게 되는지 궁금하다.</p>
<p>다음엔 <code>MigrationStrategy</code>를 써서 스키마를 업데이트하는 방법을 공부해봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[#37 #Node가 뭘까?]]></title>
            <link>https://velog.io/@int_1sy/37-Node%EA%B0%80-%EB%AD%98%EA%B9%8C</link>
            <guid>https://velog.io/@int_1sy/37-Node%EA%B0%80-%EB%AD%98%EA%B9%8C</guid>
            <pubDate>Sat, 25 Apr 2026 06:17:20 GMT</pubDate>
            <description><![CDATA[<h2 id="1-nodejs란-무엇인가">1. Node.js란 무엇인가</h2>
<p>Node.js는 자바스크립트를 브라우저 밖에서도 실행할 수 있게 해주는 <strong>런타임 환경</strong>이다.
즉, 라이브러리나 프레임워크가 아니라 <strong>자바스크립트 실행 환경</strong>이다.</p>
<p>과거(2008년 이전)에는 자바스크립트가 오직 브라우저 안에서만 실행되었다.
그 이유는 브라우저 안에 자바스크립트를 실행하는 엔진이 포함되어 있었기 때문이다.
그래서 브라우저가 없으면 자바스크립트 파일은 단순한 텍스트 파일에 불과했다.</p>
<p>2009년, 라이언 달(Ryan Dahl)이 구글 크롬의 엔진을 가져와
브라우저 없이도 자바스크립트를 실행할 수 있게 만들었고, Node가 만들어지게 되었다.</p>
<hr>
<h2 id="2-왜-nodejs를-배우는가">2. 왜 Node.js를 배우는가</h2>
<p>실제 개발 환경에서는 서버를 사용하는 경우가 많다.
Node.js를 배우면 서버가 어떻게 동작하는지 이해할 수 있고,
최근 많이 사용되는 서버리스 방식도 더 쉽게 이해할 수 있다.</p>
<hr>
<h2 id="3-repl이란-무엇인가">3. REPL이란 무엇인가</h2>
<p>Node.js에는 REPL이라는 기능이 있다.</p>
<p>REPL은 다음의 과정을 반복한다.</p>
<ul>
<li>Read: 입력을 읽는다</li>
<li>Eval: 코드를 실행한다</li>
<li>Print: 결과를 출력한다</li>
<li>Loop: 다시 입력을 기다린다
<img src="https://velog.velcdn.com/images/int_1sy/post/e9df2450-411a-40e7-ac47-255fd36a7a63/image.png" alt=""><img src="https://velog.velcdn.com/images/int_1sy/post/9b5a9f1f-e809-44a5-a021-d5edc46ff401/image.png" alt="">
이 구조는 브라우저의 콘솔창과 유사하며,
Node 환경에서 자바스크립트를 바로 실행할 수 있게 해준다.</li>
</ul>
<hr>
<h2 id="4-npm이란-무엇인가">4. npm이란 무엇인가</h2>
<p>npm은 Node Package Manager의 약자로, Node 생태계의 앱스토어라고 말 할 수 있다.
전세계 개발자들이 만든 JS 코드 묶음(=패키지)을 누구나 다운 받아 쓸 수 있게 하는 시스템이다</p>
<p>예시:</p>
<ul>
<li>npm install: 패키지 설치</li>
<li>npm run dev: scripts에 정의된 명령 실행</li>
</ul>
<hr>
<h2 id="5-packagejson-파일">5. package.json 파일</h2>
<p>package.json은 프로젝트의 설정 정보를 담고 있는 파일이다.</p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;nodejs-day1&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;type&quot;: &quot;module&quot;,
  &quot;main&quot;: &quot;index.js&quot;,
  &quot;scripts&quot;: {
    &quot;test&quot;: &quot;echo \&quot;Error: no test specified\&quot; &amp;&amp; exit 1&quot;
  }
}</code></pre>
<p>주요 항목:</p>
<ul>
<li>name: 프로젝트 이름</li>
<li>version: 프로젝트 버전</li>
<li>type: module → ES Module(import/export) 사용 가능</li>
<li>main: 프로젝트의 시작 파일</li>
<li>scripts: npm run으로 실행할 명령어 정의</li>
</ul>
<hr>
<h2 id="6-모듈module이란">6. 모듈(Module)이란</h2>
<p>모듈은 기능 단위로 나누어진 자바스크립트 파일이다.
다른 파일에서 가져와 사용할 수 있어 코드 재사용이 가능하다.
ㄴ <code>import</code></p>
<hr>
<h2 id="7-모듈-방식의-변화">7. 모듈 방식의 변화</h2>
<p>과거에는 CommonJS 방식이 사용되었다.</p>
<pre><code class="language-js">const math = require(&#39;./math&#39;);</code></pre>
<p>이후 ES Module이 등장하면서 다음과 같이 사용할 수 있게 되었다.</p>
<pre><code class="language-js">import math from &#39;./math.js&#39;;</code></pre>
<hr>
<h2 id="8-코드-정리">8. 코드 정리</h2>
<h3 id="1-datejs">1. date.js</h3>
<pre><code class="language-js">import dayjs from &quot;dayjs&quot;;

const now = dayjs();
console.log(now);
console.log(now.format(&quot;YYYY-MM-DD&quot;));
console.log(now.format(&quot;YYYY년 MM월 DD일&quot;));
console.log(now.format(&quot;YYYY-MM-DD HH:mm:ss&quot;));

const nextWeek = now.add(7, &quot;day&quot;);
console.log(&quot;일주일 후:&quot;, nextWeek.format(&quot;YYYY년 MM월 DD일&quot;));</code></pre>
<h4 id="설명">설명</h4>
<p><code>dayjs</code> 라이브러리를 사용하여 현재 날짜와 시간을 다루는 코드이다.</p>
<ul>
<li><p><code>dayjs()</code> : 현재 날짜와 시간을 가져온다.</p>
</li>
<li><p><code>format()</code> : 날짜를 원하는 형식의 문자열로 변환한다.</p>
<ul>
<li><code>&quot;YYYY-MM-DD&quot;</code> → 2026-04-25 형태</li>
<li><code>&quot;YYYY년 MM월 DD일&quot;</code> → 한국어 날짜 형식</li>
<li><code>&quot;YYYY-MM-DD HH:mm:ss&quot;</code> → 날짜 + 시간</li>
</ul>
</li>
<li><p><code>add(7, &quot;day&quot;)</code> : 현재 날짜에서 7일을 더해 일주일 후 날짜를 계산한다.</p>
</li>
</ul>
<hr>
<h3 id="2-greetingjs">2. greeting.js</h3>
<pre><code class="language-js">export default function (name) {
  return `안녕하세요, ${name}!`;
}</code></pre>
<h4 id="설명-1">설명</h4>
<p>사용자의 이름을 받아 인사말을 반환하는 함수이다.</p>
<ul>
<li><code>export default</code> : 이 파일의 기본 내보내기 값</li>
<li><code>name</code> : 매개변수로 전달받은 이름</li>
<li>템플릿 리터럴(<code>`</code>)을 사용하여 문자열을 생성</li>
</ul>
<p>다른 파일에서 import할 때 이름을 자유롭게 지정할 수 있다.</p>
<hr>
<h3 id="3-mathjs">3. math.js</h3>
<pre><code class="language-js">export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

export const PI = 3.14159;</code></pre>
<h4 id="설명-2">설명</h4>
<p>기본적인 수학 연산을 수행하는 함수들을 모아둔 모듈이다.</p>
<ul>
<li><code>add(a, b)</code> : 두 수를 더하는 함수</li>
<li><code>multiply(a, b)</code> : 두 수를 곱하는 함수</li>
<li><code>PI</code> : 원주율 상수</li>
</ul>
<p>각 요소를 <code>export</code>하여 다른 파일에서 사용할 수 있도록 한다.</p>
<hr>
<h3 id="4-mainjs">4. main.js</h3>
<pre><code class="language-js">import { add, multiply, PI } from &quot;./math.js&quot;;
import greet from &quot;./greeting.js&quot;;

console.log(add(3, 5));
console.log(multiply(4, 7));
console.log(`원주율은 ${PI}입니다.`);
console.log(greet(&quot;서영&quot;));</code></pre>
<h4 id="설명-3">설명</h4>
<p>여러 모듈을 불러와 실행하는 메인 파일이다.</p>
<ul>
<li><code>import {}</code> : 이름을 지정하여 가져오는 방식 (named export)</li>
<li><code>import greet</code> : default export를 가져오는 방식</li>
<li><code>console.log()</code> : 실행 결과를 출력</li>
</ul>
<p>각 모듈에서 만든 기능을 실제로 사용하는 역할을 한다.
<img src="https://velog.velcdn.com/images/int_1sy/post/83918128-75dc-49bf-805f-86163063d4a1/image.png" alt=""></p>
<hr>
<h3 id="5-packagejson">5. package.json</h3>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;nodejs-day1&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;type&quot;: &quot;module&quot;,
  &quot;main&quot;: &quot;index.js&quot;,
  &quot;scripts&quot;: {
    &quot;start&quot;: &quot;node main.js&quot;,
    &quot;dev&quot;: &quot;node --watch main.js&quot;
  },
  &quot;dependencies&quot;: {
    &quot;dayjs&quot;: &quot;^1.11.20&quot;
  }
}</code></pre>
<h4 id="설명-4">설명</h4>
<p>Node.js 프로젝트의 설정 정보를 담고 있는 파일이다.</p>
<ul>
<li><p><code>type: &quot;module&quot;</code>
→ ES Module(import/export) 문법 사용 가능</p>
</li>
<li><p><code>scripts</code></p>
<ul>
<li><code>npm run start</code> : main.js 실행</li>
<li><code>npm run dev</code> : 파일 변경 시 자동 재실행</li>
</ul>
</li>
<li><p><code>dependencies</code>
→ 프로젝트에서 사용하는 외부 라이브러리 목록 (<code>dayjs</code>)</p>
</li>
</ul>
<hr>
<h3 id="6-package-lockjson">6. package-lock.json</h3>
<h4 id="설명-5">설명</h4>
<p>설치된 패키지들의 정확한 버전 정보를 기록하는 파일이다.</p>
<ul>
<li><code>npm install</code> 시 자동 생성</li>
<li>동일한 환경에서 동일한 버전이 설치되도록 보장</li>
<li>직접 수정하지 않고 npm이 관리함</li>
</ul>
<hr>
<h2 id="9-정리--오늘의-학습-목표">9. 정리 + 오늘의 학습 목표</h2>
<blockquote>
<p> Node.js가 프레임워크가 아닌 런타임임을 자기 말로 설명할 수 있다</p>
</blockquote>
<p>Node.js는 기능을 제공하는 도구(프레임워크)가 아니라
자바스크립트를 실행해주는 환경이다.</p>
<p>브라우저 없이도 JS 파일을 실행할 수 있게 해주며,
코드를 “돌리는 역할”을 한다는 점에서
라이브러리나 프레임워크와 다르다.</p>
<blockquote>
<p>서버리스 방식과 전통 서버 방식의 차이를 구분할 수 있다</p>
</blockquote>
<p>Node.js는 기능을 제공하는 도구(프레임워크)가 아니라
자바스크립트를 실행해주는 환경이다.</p>
<p>브라우저 없이도 JS 파일을 실행할 수 있게 해주며,
코드를 “돌리는 역할”을 한다는 점에서
라이브러리나 프레임워크와 다르다.</p>
<blockquote>
<p>Node REPL 에서 자바 스크립트를 직접 실행할 수 있다.</p>
</blockquote>
<p>Node REPL은 코드를 입력하면
즉시 실행하고 결과를 바로 보여주는 환경이다.</p>
<p>브라우저 콘솔처럼 동작하며,
간단한 테스트나 실습을 빠르게 할 때 유용하다.</p>
<h2 id="10-마무리">10. 마무리</h2>
<p>오늘은 Node의 탄생부터 기초를 조금 배워봤습니다!
더 열심히 공부해서 서버에 대해 높은 이해도를 가질 수 있도록 하겠습니다
그럼 우리 모두 <strong>즐코!</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[#36 #Flutter 캘린더 일정 관리 UI 상세 구현 (BottomSheet & CustomTextField)]]></title>
            <link>https://velog.io/@int_1sy/36-Flutter-%EC%BA%98%EB%A6%B0%EB%8D%94-%EC%9D%BC%EC%A0%95-%EA%B4%80%EB%A6%AC-UI-%EC%83%81%EC%84%B8-%EA%B5%AC%ED%98%84-BottomSheet-CustomTextField</link>
            <guid>https://velog.io/@int_1sy/36-Flutter-%EC%BA%98%EB%A6%B0%EB%8D%94-%EC%9D%BC%EC%A0%95-%EA%B4%80%EB%A6%AC-UI-%EC%83%81%EC%84%B8-%EA%B5%AC%ED%98%84-BottomSheet-CustomTextField</guid>
            <pubDate>Wed, 22 Apr 2026 12:59:03 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘의-수업-주제">오늘의 수업 주제</h2>
<blockquote>
<p>지난 시간에 구현한 캘린더 UI에 이어, 실제 일정을 표시하는 <strong>ScheduleCard</strong>, 오늘 날짜를 알려주는 <strong>TodayBanner</strong>, 그리고 일정을 입력받기 위한 <strong>BottomSheet</strong>와 <strong>CustomTextField</strong>를 구현하고 화면에 배치하는 방법</p>
</blockquote>
<hr>
<h2 id="코드-분석">코드 분석</h2>
<h3 id="componentcustom_text_fielddart">component/custom_text_field.dart</h3>
<p>사용자로부터 시간이나 일정 내용을 입력받는 공통 위젯</p>
<pre><code class="language-dart">import &#39;package:calendar_scheduler/const/colors.dart&#39;;
import &#39;package:flutter/material.dart&#39;;
import &#39;package:flutter/services.dart&#39;;

class CustomTextFeild extends StatelessWidget {
  final String label;
  final bool isTime;
  const CustomTextFeild({
    required this.label,
    required this.isTime,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          label,
          style: TextStyle(
            color: PRIMARY_COLOR,
            fontWeight: FontWeight.w600,
          ),
        ),
        Expanded(
          flex: isTime ? 0 : 1,
          child: TextFormField(
            cursorColor: Colors.grey,
            maxLines: isTime ? 1 : null,
            expands: !isTime,
            keyboardType: isTime
                ? TextInputType.number
                : TextInputType.multiline,
            inputFormatters: isTime
                ? [FilteringTextInputFormatter.digitsOnly]
                : [],
            decoration: InputDecoration(
              border: InputBorder.none,
              filled: true,
              fillColor: Colors.grey[300],
              suffixText: isTime ? &#39;시&#39; : null,
            ),
          ),
        ),
      ],
    );
  }
}</code></pre>
<table>
<thead>
<tr>
<th align="left">코드 역할</th>
<th align="left">비고</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>final bool isTime</strong></td>
<td align="left">시간 입력용(숫자)인지 내용 입력용(멀티라인)인지 구분</td>
</tr>
<tr>
<td align="left"><strong>maxLines: isTime ? 1 : null</strong></td>
<td align="left">시간은 한 줄만, 내용은 줄 제한 없이(null) 입력 가능</td>
</tr>
<tr>
<td align="left"><strong>expands: !isTime</strong></td>
<td align="left">내용 입력창일 때 부모의 남은 공간을 모두 채움</td>
</tr>
<tr>
<td align="left"><strong>inputFormatters</strong></td>
<td align="left"><code>digitsOnly</code>를 사용해 시간 입력 시 숫자만 허용</td>
</tr>
<tr>
<td align="left"><strong>suffixText: &#39;시&#39;</strong></td>
<td align="left">시간 입력창 우측에 &#39;시&#39; 단위를 표시</td>
</tr>
</tbody></table>
<hr>
<h3 id="componentschedule_carddart">component/schedule_card.dart</h3>
<p>등록된 일정의 시간과 내용을 시각적으로 보여주는 카드 위젯</p>
<pre><code class="language-dart">import &#39;package:calendar_scheduler/const/colors.dart&#39;;
import &#39;package:flutter/material.dart&#39;;

class ScheduleCard extends StatelessWidget {
  final int startTime;
  final int endTime;
  final String content;

  const ScheduleCard({
    required this.startTime,
    required this.endTime,
    required this.content,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        border: Border.all(width: 1.0, color: PRIMARY_COLOR),
        borderRadius: BorderRadius.circular(8.0),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: IntrinsicHeight(
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              _Time(startTime: startTime, endTime: endTime),
              SizedBox(width: 16.0),
              _Content(content: content),
              SizedBox(width: 16.0),
            ],
          ),
        ),
      ),
    );
  }
}

class _Time extends StatelessWidget {
  final int startTime;
  final int endTime;

  const _Time({required this.startTime, required this.endTime, Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final textStyle = TextStyle(fontWeight: FontWeight.w600, color: PRIMARY_COLOR, fontSize: 16);
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(&#39;${startTime.toString().padLeft(2, &#39;0&#39;)}:00&#39;, style: textStyle),
        Text(&#39;${endTime.toString().padLeft(2, &#39;0&#39;)}:00&#39;, style: textStyle.copyWith(fontSize: 10.0)),
      ],
    );
  }
}

class _Content extends StatelessWidget {
  final String content;
  const _Content({required this.content, Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Expanded(child: Text(content));
  }
}</code></pre>
<table>
<thead>
<tr>
<th align="left">코드 역할</th>
<th align="left">비고</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>IntrinsicHeight</strong></td>
<td align="left">자식 위젯들(시간/내용) 중 가장 높은 위젯에 맞춰 높이를 동기화</td>
</tr>
<tr>
<td align="left"><strong>padLeft(2, &#39;0&#39;)</strong></td>
<td align="left">시간이 1자리(예: 9)일 때 앞을 0으로 채워 &#39;09&#39;로 표시</td>
</tr>
<tr>
<td align="left"><strong>_Time / _Content</strong></td>
<td align="left">위젯을 분리하여 코드 가독성과 재사용성을 높임 (Private 위젯)</td>
</tr>
</tbody></table>
<hr>
<h3 id="componenttoday_bannerdart--schedule_bottom_sheetdart">component/today_banner.dart &amp; schedule_bottom_sheet.dart</h3>
<p>상단 날짜 표시 배너와 하단 입력창 부분</p>
<pre><code class="language-dart">// today_banner.dart 일부
return Container(
  color: PRIMARY_COLOR,
  child: Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
      Text(&#39;${selectDate.year}년 ${selectDate.month}월 ${selectDate.day}일&#39;),
      Text(&#39;$count개&#39;),
    ],
  ),
);

// schedule_bottom_sheet.dart 일부
showModalBottomSheet(
  context: context,
  isDismissible: true,
  builder: (_) =&gt; ScheduleBottomSheet(),
);</code></pre>
<table>
<thead>
<tr>
<th align="left">코드 역할</th>
<th align="left">비고</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>MainAxisAlignment.spaceBetween</strong></td>
<td align="left">날짜(좌)와 개수(우)를 양 끝으로 배치</td>
</tr>
<tr>
<td align="left"><strong>isDismissible: true</strong></td>
<td align="left">배경 탭 시 바텀 시트가 닫히도록 설정</td>
</tr>
<tr>
<td align="left"><strong>MediaQuery... / 2</strong></td>
<td align="left">화면 높이의 절반만큼만 시트가 올라오도록 설정</td>
</tr>
</tbody></table>
<hr>
<h3 id="screenhome_screendart">screen/home_screen.dart</h3>
<p>위의 컴포넌트들을 모두 모아 하나의 화면으로 조립</p>
<pre><code class="language-dart">class _HomeScreenState extends State&lt;HomeScreen&gt; {
  DateTime selectedDate = DateTime.utc(
    DateTime.now().year, DateTime.now().month, DateTime.now().day,
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        backgroundColor: PRIMARY_COLOR,
        onPressed: () {
          showModalBottomSheet(
            context: context,
            isDismissible: true,
            builder: (_) =&gt; ScheduleBottomSheet(),
          );
        },
      ),
      body: SafeArea(
        child: Column(
          children: [
            MainCalender(selectedDate: selectedDate, onDaySelected: onDaySelected),
            SizedBox(height: 8),
            TodayBanner(selectDate: selectedDate, count: 0),
            ScheduleCard(startTime: 12, endTime: 14, content: &#39;프로그램 공부&#39;),
          ],
        ),
      ),
    );
  }
}</code></pre>
<p><strong>이 파일이 하는 일:</strong> 캘린더, 배너, 일정 카드를 세로(<code>Column</code>)로 배치하고, FAB를 통해 일정을 추가할 수 있는 진입점을 제공합니다.</p>
<hr>
<h2 id="새롭게-알게-된-것">새롭게 알게 된 것</h2>
<ul>
<li><strong>IntrinsicHeight</strong>: 자식 위젯들의 높이를 가장 큰 자식에 맞춰주는 위젯으로, 디자인의 일관성을 맞추기 매우 편리하다.</li>
<li><strong>padLeft()</strong>: 문자열의 길이를 고정하고 빈 곳을 특정 문자로 채우는 함수를 통해 시간 포맷팅을 쉽게 구현할 수 있었다.</li>
<li><strong>InputFormatters</strong>: 텍스트 필드에서 특정 타입(숫자 등)만 입력받게 강제하는 법을 배웠다.</li>
</ul>
<h2 id="헷갈리는-부분">헷갈리는 부분</h2>
<ul>
<li><strong>Expanded와 Column</strong>: <code>CustomTextField</code> 내에서 <code>Expanded</code>를 사용할 때 부모 위젯의 제약 조건에 따라 에러가 날 수 있어 높이 설정에 유의해야 한다.</li>
<li><strong>State 공유</strong>: 바텀 시트에서 입력한 값을 어떻게 다시 <code>HomeScreen</code>의 리스트에 추가할지에 대한 로직 구현이 더 연습이 필요할 것 같다.</li>
</ul>
<h2 id="실습">실습</h2>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/02635fbe-eb75-4387-9bf6-db77a2835e89/image.png" alt=""><img src="https://velog.velcdn.com/images/int_1sy/post/140cb2a7-08a2-48b6-9153-c60b7b926c29/image.png" alt=""></p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[#34 #Flutter 기초 총정리]]></title>
            <link>https://velog.io/@int_1sy/34-Flutter-%EA%B8%B0%EC%B4%88-%EC%B4%9D%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@int_1sy/34-Flutter-%EA%B8%B0%EC%B4%88-%EC%B4%9D%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sun, 19 Apr 2026 15:05:23 GMT</pubDate>
            <description><![CDATA[<h2 id="1-flutter란">1. Flutter란?</h2>
<p>Flutter는 <strong>Google이 만든 크로스 플랫폼 앱 개발 프레임워크</strong>이다. 일반적으로 앱을 개발하려면 Android는 Kotlin, iOS는 Swift를 따로 써야 한다. 하지만 Flutter를 사용하면 <strong>Dart</strong> 언어 하나로 Android, iOS, 웹, 데스크톱 앱을 동시에 만들 수 있다.</p>
<p>Flutter의 가장 큰 장점 중 하나는 <strong>Hot Reload</strong>다. 코드를 수정하고 저장하면 앱이 처음부터 재시작되는 게 아니라 바뀐 부분만 즉시 화면에 반영된다. 덕분에 UI를 조금씩 다듬으면서 빠르게 작업하기에 탁월하다.</p>
<blockquote>
<p><strong>한 줄 요약:</strong> Dart 하나로 Android + iOS + Web + Desktop, Hot Reload로 빠른 UI 개발.</p>
</blockquote>
<hr>
<h2 id="2-프로젝트-폴더-구조">2. 프로젝트 폴더 구조</h2>
<h3 id="①-lib-폴더">① lib 폴더</h3>
<p>우리가 작성하는 모든 Dart 코드는 이 폴더 안에 들어간다. <code>main.dart</code> 파일이 앱의 시작점으로, 앱을 실행하면 가장 먼저 이 파일이 실행된다.</p>
<p>화면이 여러 개라면 <code>lib/screen/</code> 같은 하위 폴더를 만들어 화면별로 파일을 나눠 관리하는 것이 일반적인 패턴이다.</p>
<pre><code>lib/
  ├── screen/
  │   └── home_screen.dart
  └── main.dart</code></pre><h3 id="②-pubspecyaml-파일">② pubspec.yaml 파일</h3>
<p>프로젝트 전체의 <strong>설정 파일</strong>이다. 외부 패키지(라이브러리)를 등록하거나, 이미지 같은 에셋(assets)을 등록할 때 사용한다.</p>
<pre><code class="language-yaml">dependencies:
  flutter:
    sdk: flutter
  webview_flutter: ^4.4.2    # 외부 패키지 등록

flutter:
  assets:
    - asset/img/             # 이미지 폴더 등록</code></pre>
<blockquote>
<p>⚠️ <strong>주의:</strong> pubspec.yaml을 수정하면 반드시 <code>Pub get</code>을 실행해야 변경 사항이 적용된다.</p>
</blockquote>
<hr>
<h2 id="3-위젯widget">3. 위젯(Widget)</h2>
<p>Flutter에서 화면에 보이는 모든 것 — 글자, 버튼, 이미지, 여백 — 은 전부 <strong>위젯(Widget)</strong> 이다. 작은 위젯들을 조립해서 화면을 만드는 방식이다.</p>
<p>위젯은 트리(tree) 구조로 중첩된다. 위젯 안에 위젯이 들어가고, 그 안에 또 위젯이 들어가는 식이다.</p>
<pre><code class="language-dart">Scaffold(
  appBar: AppBar(
    title: Text(&#39;다연쌤 최고&#39;),
  ),
  body: WebViewWidget(
    controller: webViewController,
  ),
)</code></pre>
<p>위젯은 <strong>상태(state)의 유무</strong>를 기준으로 두 종류로 나뉜다.</p>
<h3 id="①-statelesswidget-상태-없는-위젯">① StatelessWidget (상태 없는 위젯)</h3>
<p>한 번 그려지면 변하지 않는 위젯이다. 데이터가 바뀔 일이 없는 정적인 화면에 사용한다. <code>build()</code> 메서드 하나만 존재하며, 이 메서드가 화면을 그리는 역할을 담당한다.</p>
<pre><code class="language-dart">class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(...);
  }
}</code></pre>
<h3 id="②-statefulwidget-상태-있는-위젯">② StatefulWidget (상태 있는 위젯)</h3>
<p>사용자의 조작이나 시간에 따라 화면이 바뀌어야 할 때 사용한다. Flutter가 내부적으로 위젯을 효율적으로 관리하기 위해 <strong>클래스 2개</strong>로 나뉜다.</p>
<ul>
<li><code>HomeScreen</code> — 껍데기(위젯 정의)</li>
<li><code>_HomeScreenState</code> — 실제 내용과 상태 관리</li>
</ul>
<pre><code class="language-dart">class HomeScreen extends StatefulWidget {
  @override
  State&lt;HomeScreen&gt; createState() =&gt; _HomeScreenState();
}

class _HomeScreenState extends State&lt;HomeScreen&gt; {
  DateTime firstDay = DateTime.now(); // 상태값

  @override
  Widget build(BuildContext context) {
    return Scaffold(...);
  }
}</code></pre>
<p>상태를 변경할 때는 반드시 <code>setState()</code> 안에서 값을 바꿔야 Flutter가 변경을 감지하고 화면을 다시 그린다.</p>
<h3 id="statelesswidget-vs-statefulwidget-비교">StatelessWidget vs StatefulWidget 비교</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>StatelessWidget</th>
<th>StatefulWidget</th>
</tr>
</thead>
<tbody><tr>
<td>화면 변화</td>
<td>없음 (정적)</td>
<td>있음 (동적)</td>
</tr>
<tr>
<td>setState()</td>
<td>불필요</td>
<td>필수</td>
</tr>
<tr>
<td>클래스 수</td>
<td>1개</td>
<td>2개</td>
</tr>
<tr>
<td>사용 예</td>
<td>소개 화면, 고정 UI</td>
<td>카운터, 입력 폼, D-day</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-앱이-실행되는-흐름">4. 앱이 실행되는 흐름</h2>
<p>Flutter 앱은 항상 아래 순서로 시작된다.</p>
<pre><code class="language-dart">void main() {              // ① 앱의 시작점
  runApp(                  // ② 위젯을 화면에 띄우는 함수
    MaterialApp(           // ③ 앱 전체를 감싸는 최상위 위젯
      home: HomeScreen(),  // ④ 첫 화면 지정
    ),
  );
}</code></pre>
<ul>
<li><strong><code>main()</code></strong> — Dart 프로그램의 시작점. Java의 <code>public static void main</code>과 동일한 역할.</li>
<li><strong><code>runApp()</code></strong> — 괄호 안에 넣은 위젯을 화면에 표시.</li>
<li><strong><code>MaterialApp</code></strong> — 앱 전체를 감싸는 최상위 위젯. Google Material Design 적용. theme, home 등을 설정.</li>
<li><strong><code>Scaffold</code></strong> — 각 화면의 뼈대. appBar(상단바), body(본문), floatingActionButton(하단 버튼) 제공.</li>
</ul>
<p>전체 구조를 트리로 나타내면 다음과 같다.</p>
<pre><code>main()
  └── runApp()
        └── MaterialApp   // 앱 전체 설정
              └── HomeScreen   // 첫 화면
                    └── Scaffold   // 화면 뼈대</code></pre><hr>
<h2 id="5-레이아웃-위젯--화면-배치하기">5. 레이아웃 위젯 — 화면 배치하기</h2>
<h3 id="①-column--세로-배치">① Column — 세로 배치</h3>
<p><code>children</code> 배열에 위젯을 넣으면 위에서 아래로 순서대로 배치된다.</p>
<pre><code class="language-dart">Column(
  children: [
    Text(&#39;U&amp;I&#39;),
    Text(&#39;우리 처음 만난 날&#39;),
    IconButton(...),
    Text(&#39;D+365&#39;),
  ],
)</code></pre>
<h3 id="②-row--가로-배치">② Row — 가로 배치</h3>
<p><code>children</code> 배열에 위젯을 넣으면 왼쪽에서 오른쪽으로 나란히 배치된다.</p>
<pre><code class="language-dart">Row(
  children: [
    Icon(Icons.star),
    Text(&#39;별점&#39;),
    Text(&#39;4.5&#39;),
  ],
)</code></pre>
<h3 id="③-정렬-옵션-mainaxis--crossaxis">③ 정렬 옵션 (mainAxis / crossAxis)</h3>
<p>Column과 Row에는 두 가지 정렬 축이 존재한다.</p>
<ul>
<li><strong>mainAxis(주축)</strong> — 위젯이 나열되는 방향 (Column: 세로, Row: 가로)</li>
<li><strong>crossAxis(교차축)</strong> — 주축의 반대 방향</li>
</ul>
<pre><code class="language-dart">Column(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  crossAxisAlignment: CrossAxisAlignment.stretch,
  children: [...],
)</code></pre>
<p>자주 쓰는 <code>mainAxisAlignment</code> 값 정리:</p>
<table>
<thead>
<tr>
<th>값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>start</code></td>
<td>위(또는 왼쪽)에 몰아서 배치</td>
</tr>
<tr>
<td><code>center</code></td>
<td>가운데에 모아서 배치</td>
</tr>
<tr>
<td><code>spaceBetween</code></td>
<td>첫·끝 위젯은 양 끝에 붙이고, 나머지 간격 균등 분배</td>
</tr>
<tr>
<td><code>spaceEvenly</code></td>
<td>모든 간격(양 끝 포함)을 균등하게</td>
</tr>
<tr>
<td><code>spaceAround</code></td>
<td>양 끝 간격은 절반, 위젯 사이 간격은 균등</td>
</tr>
</tbody></table>
<hr>
<h2 id="6-자주-쓰는-기본-위젯">6. 자주 쓰는 기본 위젯</h2>
<h3 id="①-sizedbox--빈-공간-만들기">① SizedBox — 빈 공간 만들기</h3>
<p>Flutter에서는 CSS의 <code>margin</code>이나 <code>padding</code> 대신 <code>SizedBox</code>를 끼워 넣어 간격을 만드는 방식을 많이 사용한다.</p>
<pre><code class="language-dart">// 세로 16px 여백
const SizedBox(height: 16),

// 가로 16px 여백
const SizedBox(width: 16),</code></pre>
<h3 id="②-safearea--안전-영역-확보">② SafeArea — 안전 영역 확보</h3>
<p>요즘 스마트폰에는 상단에 노치(카메라 영역)가 있고, 하단에 홈 인디케이터가 있다. <code>SafeArea</code>로 감싸면 그 영역을 자동으로 피해서 콘텐츠를 배치할 수 있다.</p>
<pre><code class="language-dart">SafeArea(
  top: true,      // 위쪽 노치 영역 피하기
  bottom: false,  // 아래쪽은 피하지 않기
  child: Column(...),
)</code></pre>
<h3 id="③-center--가운데-정렬">③ Center — 가운데 정렬</h3>
<p>자식 위젯을 부모 영역의 정가운데에 배치한다.</p>
<pre><code class="language-dart">Center(
  child: Image.asset(&#39;asset/img/j.jpg&#39;),
)</code></pre>
<hr>
<h2 id="7-이미지-넣기">7. 이미지 넣기</h2>
<p>Flutter에서 로컬 이미지를 사용하려면 총 3단계를 거쳐야 한다.</p>
<p><strong>① 폴더 만들기</strong> — 프로젝트 루트에 <code>asset/img/</code> 폴더를 만들고 이미지 파일을 넣는다.</p>
<p><strong>② pubspec.yaml 등록</strong> — 아래와 같이 경로를 등록한 뒤 반드시 <code>Pub get</code>을 실행한다.</p>
<pre><code class="language-yaml">flutter:
  assets:
    - asset/img/   # 폴더 안 모든 파일 사용 가능</code></pre>
<p><strong>③ 코드에서 사용</strong> — <code>Image.asset()</code>으로 경로를 지정하면 된다.</p>
<pre><code class="language-dart">Image.asset(&#39;asset/img/whtjdcks.jpg&#39;)</code></pre>
<blockquote>
<p>⚠️ <code>pubspec.yaml</code> 수정 후 <strong>Pub get</strong>을 빠뜨리면 이미지가 로드되지 않는다.</p>
</blockquote>
<hr>
<h2 id="8-스타일-한-곳에서-관리하기-theme">8. 스타일 한 곳에서 관리하기 (Theme)</h2>
<p>텍스트 스타일을 각 <code>Text</code> 위젯마다 직접 적으면, 나중에 폰트 크기를 바꿀 때 전부 찾아서 수정해야 한다. <code>MaterialApp</code>의 <code>theme</code>에 한 번만 정의해두면 <code>main.dart</code> 한 곳만 수정해도 앱 전체에 일괄 반영된다.</p>
<pre><code class="language-dart">MaterialApp(
  theme: ThemeData(
    textTheme: TextTheme(
      displayLarge: TextStyle(
        color: Colors.white,
        fontSize: 80,
        fontWeight: FontWeight.w700,
      ),
      displayMedium: TextStyle(color: Colors.white, fontSize: 50),
      bodyLarge: TextStyle(color: Colors.white, fontSize: 30),
      bodyMedium: TextStyle(color: Colors.white, fontSize: 20),
    ),
  ),
  home: HomeScreen(),
)</code></pre>
<p>각 화면에서는 <code>Theme.of(context)</code>로 정의된 스타일을 가져와 사용한다.</p>
<pre><code class="language-dart">final textTheme = Theme.of(context).textTheme;

Text(&#39;U&amp;I&#39;, style: textTheme.displayLarge),
Text(&#39;우리 처음 만난 날&#39;, style: textTheme.bodyLarge),</code></pre>
<blockquote>
<p>💡 <code>context</code>는 &quot;내가 위젯 트리에서 어디에 있는지&quot;를 알려주는 정보다. 이를 통해 상위에 설정된 Theme 정보를 가져올 수 있다.</p>
</blockquote>
<hr>
<h2 id="9-위젯을-클래스로-분리하기">9. 위젯을 클래스로 분리하기</h2>
<p>한 파일에 모든 코드를 넣으면 길어질수록 읽기가 어려워진다. 화면의 각 부분을 의미 단위로 잘라 별도의 위젯 클래스로 만드는 것이 Flutter의 일반적인 패턴이다.</p>
<pre><code class="language-dart">class _DDay extends StatelessWidget {
  final GestureTapCallback onHeartPressed;
  final DateTime firstDay;

  _DDay({required this.onHeartPressed, required this.firstDay});

  @override
  Widget build(BuildContext context) { ... }
}</code></pre>
<p>생성자에서 값을 받아 <strong>부모 위젯 → 자식 위젯</strong>으로 데이터를 전달하는 방식이다. <code>required</code> 키워드를 붙이면 해당 값이 반드시 전달되도록 강제할 수 있다.</p>
<blockquote>
<p><strong>정리:</strong> 화면을 <code>_DDay</code>, <code>_CoupleImage</code> 같은 작은 위젯으로 쪼개면 코드 가독성이 높아지고, 각 위젯을 독립적으로 수정할 수 있어 유지보수에 유리하다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[#35 # table_calendar 플러그인을 활용하여 Flutter 캘린더 UI를 구현]]></title>
            <link>https://velog.io/@int_1sy/35-tablecalendar-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-Flutter-%EC%BA%98%EB%A6%B0%EB%8D%94-UI%EB%A5%BC-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@int_1sy/35-tablecalendar-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-Flutter-%EC%BA%98%EB%A6%B0%EB%8D%94-UI%EB%A5%BC-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sun, 19 Apr 2026 14:45:02 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘의-수업-주제">오늘의 수업 주제</h2>
<blockquote>
<h3 id="table_calendar-플러그인을-활용하여-flutter-캘린더-ui를-구현하고-날짜-선택·포커스·스타일링-기능을-적용하는-방법"><code>table_calendar</code> 플러그인을 활용하여 Flutter 캘린더 UI를 구현하고, 날짜 선택·포커스·스타일링 기능을 적용하는 방법</h3>
</blockquote>
<p>에 대해 알아보겠습니다</p>
<h2 id="코드-분석">코드 분석</h2>
<p>main.dart</p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:calendar_scheduler/screen/home_screen.dart&#39;;

void main() {
  runApp(
    MaterialApp(
      home: HomeScreen(),
    )
  );
}</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td><code>void main()</code></td>
<td>앱의 진입점(Entry Point)</td>
<td>Dart 프로그램은 반드시 <code>main()</code>에서 시작</td>
</tr>
<tr>
<td><code>runApp()</code></td>
<td>Flutter 앱 실행 시작</td>
<td>전달받은 위젯을 루트 위젯으로 등록</td>
</tr>
<tr>
<td><code>MaterialApp()</code></td>
<td>Material 디자인 기반 앱 설정 위젯</td>
<td>테마, 라우팅, 네비게이션 등 앱 전반 설정 담당</td>
</tr>
<tr>
<td><code>home: HomeScreen()</code></td>
<td>앱 시작 시 처음 보여줄 화면 지정</td>
<td>앱을 켜면 제일 먼저 <code>HomeScreen</code>이 렌더링됨</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>이 파일이 하는 일:</strong> 앱의 시작점으로, <code>HomeScreen</code>을 첫 화면으로 하는 Flutter 앱을 실행한다.</p>
</blockquote>
<p>const/colors.dart</p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
const PRIMARY_COLOR = Color(0xFFEC5FC9);
final LIGHT_GREY_COLOR = Colors.grey[200]!;
final DARK_GREY_COLOR = Colors.grey[600]!;
final TEXT_FIELD_FILL_COLOR = Colors.grey[300]!;</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td><code>import &#39;package:flutter/material.dart&#39;</code></td>
<td>Flutter의 Material 디자인 라이브러리 불러오기</td>
<td><code>Color</code>, <code>Colors</code> 클래스를 쓰기 위해 필요</td>
</tr>
<tr>
<td><code>const PRIMARY_COLOR</code></td>
<td>컴파일 타임에 고정되는 상수 선언</td>
<td>변경 불가, 앱 전체에서 동일한 값 보장</td>
</tr>
<tr>
<td><code>Color(0xFFEC5FC9)</code></td>
<td>16진수 코드로 색상 직접 지정</td>
<td></td>
</tr>
<tr>
<td><code>final LIGHT_GREY_COLOR</code></td>
<td>런타임에 한 번만 할당되는 변수</td>
<td><code>Colors.grey[200]</code>은 런타임에 결정되므로 <code>const</code> 불가</td>
</tr>
<tr>
<td><code>Colors.grey[200]</code></td>
<td>Material 색상 팔레트에서 회색 200번 선택</td>
<td>숫자가 낮을수록 밝은 회색, 높을수록 어두운 회색</td>
</tr>
<tr>
<td><code>!</code> (null assertion)</td>
<td>null이 아님을 강제로 보장</td>
<td><code>Colors.grey[200]</code>의 반환 타입이 <code>Color?</code>라 <code>!</code> 없으면 컴파일 에러</td>
</tr>
<tr>
<td><code>TEXT_FIELD_FILL_COLOR</code></td>
<td>텍스트 입력 필드 배경색</td>
<td>이번 코드에서 선언만 되고 아직 사용되지 않음</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>이 파일이 하는 일:</strong> 앱 전체에서 반복 사용되는 색상값을 한 곳에 모아 관리한다. 나중에 색상을 바꿀 때 이 파일 하나만 수정하면 앱 전체에 반영된다.</p>
</blockquote>
<p>screen/home_screen.dart</p>
<pre><code class="language-java">import &#39;package:flutter/material.dart&#39;;
import &#39;package:calendar_scheduler/component/main_calender.dart&#39;;

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  State&lt;HomeScreen&gt; createState() =&gt; _HomeScreenState();
}

class _HomeScreenState extends State&lt;HomeScreen&gt; {
  DateTime selectedDate = DateTime.utc(
    DateTime.now().year,
    DateTime.now().month,
    DateTime.now().day,
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Column(
          children: [
            MainCalender(
              selectedDate: selectedDate,
              onDaySelected: onDaySelected,
            ),
          ],
        ),
      ),
    );
  }

  void onDaySelected(DateTime selectedDate, DateTime focusedDate) {
    setState(() {
      this.selectedDate = selectedDate;
    });
  }
}</code></pre>
<p><strong>① 클래스 선언부</strong></p>
<pre><code class="language-dart">class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  State&lt;HomeScreen&gt; createState() =&gt; _HomeScreenState();
}</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td><code>extends StatefulWidget</code></td>
<td>상태를 가질 수 있는 위젯으로 선언</td>
<td>날짜 선택처럼 화면이 바뀌어야 할 때 사용</td>
</tr>
<tr>
<td><code>const HomeScreen({Key? key})</code></td>
<td>위젯 생성자 선언</td>
<td><code>Key?</code>는 Flutter가 위젯 트리에서 위젯을 구분할 때 사용하는 식별자</td>
</tr>
<tr>
<td><code>: super(key: key)</code></td>
<td>부모 클래스(<code>StatefulWidget</code>) 생성자에 key 전달</td>
<td>상속 구조에서 부모 초기화를 위해 필수</td>
</tr>
<tr>
<td><code>@override</code></td>
<td>부모 클래스의 메서드를 재정의함을 명시</td>
<td>없어도 동작하지만 명시하는 게 좋은 습관</td>
</tr>
<tr>
<td><code>createState()</code></td>
<td>이 위젯과 연결될 State 객체 생성</td>
<td><code>StatefulWidget</code>은 반드시 <code>createState()</code>를 구현해야 함</td>
</tr>
<tr>
<td><code>_HomeScreenState</code></td>
<td>앞에 <code>_</code>가 붙으면 이 파일 내에서만 접근 가능</td>
<td>Dart에서 <code>_</code>는 private을 의미</td>
</tr>
</tbody></table>
<hr>
<p><strong>② State 클래스 — 상태 선언</strong></p>
<pre><code class="language-dart">class _HomeScreenState extends State&lt;HomeScreen&gt; {
  DateTime selectedDate = DateTime.utc(
    DateTime.now().year,
    DateTime.now().month,
    DateTime.now().day,
  );</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td><code>extends State&lt;HomeScreen&gt;</code></td>
<td><code>HomeScreen</code>의 상태를 관리하는 클래스</td>
<td><code>&lt;HomeScreen&gt;</code>으로 어떤 위젯의 State인지 타입 지정</td>
</tr>
<tr>
<td><code>DateTime selectedDate</code></td>
<td>현재 선택된 날짜를 저장하는 상태 변수</td>
<td>이 값이 바뀌면 <code>setState()</code>로 화면을 다시 그림</td>
</tr>
<tr>
<td><code>DateTime.utc(...)</code></td>
<td>UTC 기준 날짜 객체 생성</td>
<td><code>DateTime.now()</code>와 달리 시간대 영향을 받지 않음</td>
</tr>
<tr>
<td><code>DateTime.now().year</code></td>
<td>현재 연도 추출</td>
<td>초기값을 오늘 날짜로 세팅하기 위해 연·월·일을 각각 분리</td>
</tr>
</tbody></table>
<blockquote>
<p> <strong>왜 <code>DateTime.now()</code> 대신 <code>DateTime.utc()</code>를 쓸까?</strong>
<code>table_calendar</code>는 날짜 비교 시 UTC 기준을 사용하기 때문에, <code>DateTime.now()</code>(로컬 시간)와 비교하면 시간대 차이로 날짜가 틀릴 수 있음</p>
</blockquote>
<hr>
<p><strong>③ State 클래스 — build 메서드</strong></p>
<pre><code class="language-dart">@override
Widget build(BuildContext context) {
  return Scaffold(
    body: SafeArea(
      child: Column(
        children: [
          MainCalender(
            selectedDate: selectedDate,
            onDaySelected: onDaySelected,
          ),
        ],
      ),
    ),
  );
}</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td><code>Widget build(BuildContext context)</code></td>
<td>화면에 그릴 위젯 트리를 반환</td>
<td>상태가 바뀔 때마다 이 메서드가 다시 호출됨</td>
</tr>
<tr>
<td><code>Scaffold</code></td>
<td>앱 화면의 기본 뼈대 제공</td>
<td><code>AppBar</code>, <code>body</code>, <code>FloatingActionButton</code> 등의 슬롯을 가짐</td>
</tr>
<tr>
<td><code>SafeArea</code></td>
<td>노치·상태바 등 시스템 UI를 피해 안전 영역에만 렌더링</td>
<td>아이폰 노치나 안드로이드 상태바에 가려지는 것 방지</td>
</tr>
<tr>
<td><code>Column</code></td>
<td>자식 위젯들을 세로로 배치</td>
<td>나중에 캘린더 아래 일정 목록 등을 추가할 수 있는 구조</td>
</tr>
<tr>
<td><code>selectedDate: selectedDate</code></td>
<td>현재 선택된 날짜를 <code>MainCalender</code>에 전달</td>
<td>부모 → 자식으로 데이터를 내려주는 <strong>Props 패턴</strong></td>
</tr>
<tr>
<td><code>onDaySelected: onDaySelected</code></td>
<td>날짜 선택 이벤트 핸들러를 <code>MainCalender</code>에 전달</td>
<td>자식에서 발생한 이벤트를 부모가 처리하는 <strong>콜백 패턴</strong></td>
</tr>
</tbody></table>
<hr>
<p><strong>④ State 클래스 — onDaySelected 메서드</strong></p>
<pre><code class="language-dart">void onDaySelected(DateTime selectedDate, DateTime focusedDate) {
  setState(() {
    this.selectedDate = selectedDate;
  });
}</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td><code>void onDaySelected(...)</code></td>
<td>날짜가 선택됐을 때 실행할 함수</td>
<td><code>MainCalender</code>의 <code>onDaySelected</code> 콜백으로 전달됨</td>
</tr>
<tr>
<td><code>DateTime selectedDate</code> (파라미터)</td>
<td>사용자가 실제로 탭한 날짜</td>
<td></td>
</tr>
<tr>
<td><code>DateTime focusedDate</code> (파라미터)</td>
<td>현재 포커스된 날짜 (화면에 보이는 달 기준)</td>
<td>이 코드에서는 받기만 하고 사용하지 않음</td>
</tr>
<tr>
<td><code>setState(() { ... })</code></td>
<td>상태 변경 후 화면 리빌드 요청</td>
<td><code>setState</code> 없이 값만 바꾸면 화면이 갱신되지 않음</td>
</tr>
<tr>
<td><code>this.selectedDate</code></td>
<td>클래스 필드의 <code>selectedDate</code>를 명시적으로 지칭</td>
<td>파라미터명과 필드명이 같아서 <code>this.</code>로 구분</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>이 파일이 하는 일:</strong> 선택된 날짜를 상태로 관리하고, 날짜가 바뀔 때마다 캘린더를 다시 그리는 화면 전체의 <strong>상태 관리 허브</strong> 역할을 한다.</p>
</blockquote>
<p>component/main_calender.dart</p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:table_calendar/table_calendar.dart&#39;;
import &#39;package:calendar_scheduler/const/colors.dart&#39;;
class MainCalender extends StatelessWidget {

  final OnDaySelected onDaySelected; //날짜 선택 시 실행
  final DateTime selectedDate; //선택된 날짜

  MainCalender({
    required this.onDaySelected,
    required this.selectedDate,
});
  @override
  Widget build(BuildContext context) {
    return TableCalendar(

      onDaySelected: onDaySelected,
      selectedDayPredicate: (data) =&gt;
      data.year == selectedDate.year &amp;&amp;
      data.month == selectedDate.month &amp;&amp;
      data.day == selectedDate.day ,

      firstDay: DateTime(1800,1,1), //첫째날
      lastDay: DateTime(3000,1,1), //마지막날
      focusedDay: DateTime.now(), //화면에 보여지는 날

      headerStyle: HeaderStyle(
        titleCentered: true,
        formatButtonVisible: false,
        titleTextStyle: TextStyle(
          fontWeight: FontWeight.w700,
          fontSize: 16.0
        )
      ),
      calendarStyle: CalendarStyle(
        isTodayHighlighted: false,
        defaultDecoration: BoxDecoration(
          borderRadius: BorderRadius.circular(6.0),
          color: LIGHT_GREY_COLOR,
        ),
        weekendDecoration: BoxDecoration(
          borderRadius: BorderRadius.circular(6.0),
          color: LIGHT_GREY_COLOR,
        ),
        selectedDecoration: BoxDecoration(
          borderRadius: BorderRadius.circular(6.0),
          border: Border.all(
            color: PRIMARY_COLOR,
            width: 1.0,
          )
        ),
        defaultTextStyle: TextStyle(
          fontWeight: FontWeight.w600,
          color: DARK_GREY_COLOR,
        ),
          weekendTextStyle: TextStyle(
            fontWeight: FontWeight.w600,
            color: DARK_GREY_COLOR,
          ),
          selectedTextStyle: TextStyle(
            fontWeight: FontWeight.w600,
            color: PRIMARY_COLOR,
          ),

      ),
    );
  }
}</code></pre>
<p><strong>① 클래스 선언 및 생성자</strong></p>
<pre><code class="language-dart">class MainCalender extends StatelessWidget {
  final OnDaySelected onDaySelected;
  final DateTime selectedDate;

  MainCalender({
    required this.onDaySelected,
    required this.selectedDate,
  });</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td><code>extends StatelessWidget</code></td>
<td>자체 상태가 없는 위젯으로 선언</td>
<td>상태는 부모인 <code>HomeScreen</code>이 관리</td>
</tr>
<tr>
<td><code>final OnDaySelected onDaySelected</code></td>
<td>날짜 선택 콜백 함수를 외부에서 주입받음</td>
<td><code>OnDaySelected</code>는 <code>table_calendar</code>가 제공하는 타입</td>
</tr>
<tr>
<td><code>final DateTime selectedDate</code></td>
<td>현재 선택된 날짜를 외부에서 주입받음</td>
<td><code>final</code>이라 한 번 할당되면 변경 불가</td>
</tr>
<tr>
<td><code>required this.onDaySelected</code></td>
<td>생성자 호출 시 반드시 전달해야 하는 파라미터</td>
<td><code>required</code> 없으면 null 가능성이 생겨 안전하지 않음</td>
</tr>
</tbody></table>
<hr>
<p><strong>② TableCalendar 기본 설정</strong></p>
<pre><code class="language-dart">TableCalendar(
  firstDay: DateTime(1800,1,1),
  lastDay: DateTime(3000,1,1),
  focusedDay: DateTime.now(),
  onDaySelected: onDaySelected,
  selectedDayPredicate: (data) =&gt;
    data.year == selectedDate.year &amp;&amp;
    data.month == selectedDate.month &amp;&amp;
    data.day == selectedDate.day,</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td><code>firstDay: DateTime(1800,1,1)</code></td>
<td>달력에서 이동 가능한 가장 첫 날짜</td>
<td>이 날짜 이전으로 넘어갈 수 없음</td>
</tr>
<tr>
<td><code>lastDay: DateTime(3000,1,1)</code></td>
<td>달력에서 이동 가능한 가장 마지막 날짜</td>
<td>사실상 제한 없이 설정한 것</td>
</tr>
<tr>
<td><code>focusedDay: DateTime.now()</code></td>
<td>앱 실행 시 처음으로 보여줄 달</td>
<td>오늘이 속한 달이 기본으로 표시됨</td>
</tr>
<tr>
<td><code>onDaySelected: onDaySelected</code></td>
<td>날짜 탭 시 실행할 함수 연결</td>
<td>부모에서 전달받은 콜백을 그대로 연결</td>
</tr>
<tr>
<td><code>selectedDayPredicate: (data) =&gt; ...</code></td>
<td>각 날짜 셀이 &quot;선택됨&quot; 상태인지 판별하는 함수</td>
<td>달력의 모든 날짜에 대해 이 함수가 실행됨</td>
</tr>
<tr>
<td><code>data.year == selectedDate.year &amp;&amp; ...</code></td>
<td>연·월·일을 모두 비교</td>
<td><code>data == selectedDate</code>로 쓰면 시간(시·분·초)까지 비교해서 불일치 발생 가능</td>
</tr>
</tbody></table>
<hr>
<p><strong>③ HeaderStyle — 달력 상단 헤더</strong></p>
<pre><code class="language-dart">headerStyle: HeaderStyle(
  titleCentered: true,
  formatButtonVisible: false,
  titleTextStyle: TextStyle(
    fontWeight: FontWeight.w700,
    fontSize: 16.0,
  ),
),</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td><code>titleCentered: true</code></td>
<td>&quot;2024년 1월&quot;과 같은 헤더 타이틀을 가운데 정렬</td>
<td>기본값은 좌측 정렬</td>
</tr>
<tr>
<td><code>formatButtonVisible: false</code></td>
<td>&quot;2 weeks&quot; 같은 포맷 변경 버튼 숨김</td>
<td>기본값이 <code>true</code>라 명시적으로 꺼야 함</td>
</tr>
<tr>
<td><code>fontWeight: FontWeight.w700</code></td>
<td>글자 두께를 굵게 설정</td>
<td><code>w700</code> = Bold. 숫자가 클수록 굵음 (w100~w900)</td>
</tr>
<tr>
<td><code>fontSize: 16.0</code></td>
<td>글자 크기 16포인트로 설정</td>
<td><code>double</code> 타입이라 소수점 표기</td>
</tr>
</tbody></table>
<hr>
<p><strong>④ CalendarStyle — 날짜 셀 스타일</strong></p>
<pre><code class="language-dart">calendarStyle: CalendarStyle(
  isTodayHighlighted: false,

  defaultDecoration: BoxDecoration(
    borderRadius: BorderRadius.circular(6.0),
    color: LIGHT_GREY_COLOR,
  ),
  weekendDecoration: BoxDecoration(
    borderRadius: BorderRadius.circular(6.0),
    color: LIGHT_GREY_COLOR,
  ),
  selectedDecoration: BoxDecoration(
    borderRadius: BorderRadius.circular(6.0),
    border: Border.all(
      color: PRIMARY_COLOR,
      width: 1.0,
    ),
  ),

  defaultTextStyle: TextStyle(
    fontWeight: FontWeight.w600,
    color: DARK_GREY_COLOR,
  ),
  weekendTextStyle: TextStyle(
    fontWeight: FontWeight.w600,
    color: DARK_GREY_COLOR,
  ),
  selectedTextStyle: TextStyle(
    fontWeight: FontWeight.w600,
    color: PRIMARY_COLOR,
  ),
),</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td><code>isTodayHighlighted: false</code></td>
<td>오늘 날짜 자동 강조 표시 끄기</td>
<td>기본값 <code>true</code>라 끄지 않으면 오늘 날짜에 파란 원이 생김</td>
</tr>
<tr>
<td><code>defaultDecoration</code></td>
<td>평일 날짜 셀의 배경 스타일</td>
<td><code>BoxDecoration</code>으로 배경색, 테두리, 모서리 등을 지정</td>
</tr>
<tr>
<td><code>weekendDecoration</code></td>
<td>주말(토·일) 날짜 셀의 배경 스타일</td>
<td>이 코드에선 평일과 동일하게 설정</td>
</tr>
<tr>
<td><code>selectedDecoration</code></td>
<td>선택된 날짜 셀의 배경 스타일</td>
<td>배경색 없이 <code>PRIMARY_COLOR</code> 테두리만 표시</td>
</tr>
<tr>
<td><code>BorderRadius.circular(6.0)</code></td>
<td>셀 모서리를 6px 반경으로 둥글게</td>
<td>숫자가 클수록 더 둥글어짐</td>
</tr>
<tr>
<td><code>Border.all(color: ..., width: 1.0)</code></td>
<td>셀 전체 테두리를 1px로 그림</td>
<td><code>selectedDecoration</code>에만 적용해 선택된 날짜를 표시</td>
</tr>
<tr>
<td><code>defaultTextStyle</code></td>
<td>평일 날짜 숫자의 글자 스타일</td>
<td></td>
</tr>
<tr>
<td><code>weekendTextStyle</code></td>
<td>주말 날짜 숫자의 글자 스타일</td>
<td>기본적으로 빨간색이 되는데, 여기선 평일과 동일하게 설정</td>
</tr>
<tr>
<td><code>selectedTextStyle</code></td>
<td>선택된 날짜 숫자의 글자 스타일</td>
<td><code>PRIMARY_COLOR</code>(분홍색)로 강조</td>
</tr>
<tr>
<td><code>FontWeight.w600</code></td>
<td>글자 두께 Semi-Bold</td>
<td><code>w700(Bold)</code>보다 살짝 얇음</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>이 파일이 하는 일:</strong> <code>table_calendar</code> 패키지를 받아서 날짜 범위, 선택 로직, 헤더·셀 스타일을 모두 적용한 커스텀 캘린더 컴포넌트를 만들어 화면에 렌더링한다.</p>
</blockquote>
<p>pubspec.yaml</p>
<pre><code class="language-dart">
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.8
  table_calendar: 3.1.2
  intl: 0.19.0
  drift: 2.21.0
  drift_flutter: 0.1.0
  path_provider: 2.1.5
  path: ^1.9.0
  get_it: 8.0.2
  dio: 5.7.0
  provider: 6.1.2
  uuid: 4.5.1

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.

dev_dependencies:
  flutter_test:
    sdk: flutter

  # 개발할때만 사용되고, 앱을 실행할 때는 필요없는 플러그인을 여기다가 설치
  flutter_lints: ^6.0.0
  drift_dev: 2.21.0
  build_runner: 2.4.13
</code></pre>
<blockquote>
<p><strong>dependencies vs dev_dependencies</strong> 핵심 차이!
<code>dependencies</code>는 앱을 실행하는 사용자 기기에도 포함되고, <code>dev_dependencies</code>는 개발자 PC에서만 사용되고 배포 앱에는 들어가지 않음, 그래서 <code>drift_dev</code>나 <code>build_runner</code>처럼 코드 생성 용도의 패키지는 꼭 `dev_dependencie에 넣어야 앱 용량을 낭비하지 않아야함!</p>
</blockquote>
<h2 id="새롭게-알게-된-것">새롭게 알게 된 것</h2>
<ul>
<li>Flutter에서 플러그인을 설치할 때 <code>pubspec.yaml</code> 파일에 작성해야 한다.</li>
<li><code>dependencies</code>는 앱 실행 시에도 포함되어 실제 기능 구현에 필요한 패키지를 작성하는 공간이다.</li>
<li><code>dev_dependencies</code>는 개발 과정에서만 필요한 패키지를 추가하는 공간으로, 앱 실행 시에는 포함되지 않는다.</li>
<li>캘린더처럼 날짜를 다룰 때는 <code>DateTime.now()</code>대신 <code>DateTime.utc()</code>를 사용하여 시간 정보로 인한 오류를 방지할 수 있다는 것을 알게 되었다.</li>
</ul>
<h2 id="헷갈리는-부분">헷갈리는 부분</h2>
<ul>
<li>DateTime.utc()를 사용하는 이유는 이해했지만, 실제 프로젝트에서 언제 now()를 쓰고 언제 utc()를 써야 하는지 상황에 따라 구분하는 것이 아직 헷갈린다.</li>
<li><code>focusedDay</code>와 <code>selectedDate</code>의 역할 차이가 명확하게 구분되지 않는다.</li>
<li><code>selectedDayPredicate</code>가 내부적으로 어떻게 동작하는지, 모든 날짜마다 실행되는 구조가 성능에 영향을 주지 않는지 궁금하다.</li>
</ul>
<h2 id="실습">실습</h2>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/5b0c3156-1749-44bc-b31a-aa7439641a52/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[#33 #supabase를 통해 방명록 웹사이트 개발하기(2)]]></title>
            <link>https://velog.io/@int_1sy/33-supabase%EB%A5%BC-%ED%86%B5%ED%95%B4-%EB%B0%A9%EB%AA%85%EB%A1%9D-%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B02</link>
            <guid>https://velog.io/@int_1sy/33-supabase%EB%A5%BC-%ED%86%B5%ED%95%B4-%EB%B0%A9%EB%AA%85%EB%A1%9D-%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B02</guid>
            <pubDate>Thu, 26 Mar 2026 04:54:30 GMT</pubDate>
            <description><![CDATA[<p>이제 본격적으로 방명록 기능(메시지 작성 및 조회)을 구현해 보겠습니다. 프론트엔드 환경에서 데이터베이스와 어떻게 통신하는지, 그 흐름을 이해하는 것이 이번 포스팅의 핵심입니다.</p>
<hr>
<h2 id="📁-프로젝트-구조">📁 프로젝트 구조</h2>
<p>먼저 전체적인 프로젝트 디렉토리 구조는 다음과 같습니다.</p>
<pre><code class="language-bash">guestbook/
├── src/
│   ├── main.js
│   ├── messages.js
│   ├── supabase.js
│   ├── style.css
│   └── assets/
├── public/
├── index.html
├── .env
├── .gitignore
├── package.json
└── package-lock.json</code></pre>
<hr>
<h2 id="1-기본-뼈대-만들기-indexhtml">1. 기본 뼈대 만들기: <code>index.html</code></h2>
<p>사용자가 이름과 내용을 입력할 수 있는 폼과, 입력된 메시지가 출력될 리스트 영역을 만들어 줍니다.</p>
<pre><code class="language-html">&lt;!doctype html&gt;
&lt;html lang=&quot;ko&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &lt;title&gt;방명록&lt;/title&gt;
  &lt;/head&gt;

  &lt;body&gt;
    &lt;h1&gt;메세지 목록&lt;/h1&gt;

    &lt;div id=&quot;form&quot;&gt;
      &lt;input type=&quot;text&quot; id=&quot;nickname&quot; placeholder=&quot;이름&quot; /&gt;
      &lt;input type=&quot;text&quot; id=&quot;content&quot; placeholder=&quot;내용&quot; /&gt;
      &lt;button id=&quot;add-btn&quot;&gt;등록&lt;/button&gt;
    &lt;/div&gt;

    &lt;ul id=&quot;message-list&quot;&gt;&lt;/ul&gt;

    &lt;script type=&quot;module&quot; src=&quot;src/messages.js&quot;&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<blockquote>
<ul>
<li><strong>입력값</strong>: <code>nickname</code>, <code>content</code> 두 가지 데이터를 받습니다.</li>
<li><strong>이벤트</strong>: 버튼 클릭 시 자바스크립트에서 이벤트를 가로채어 처리합니다.</li>
<li><strong>동적 렌더링</strong>: 메시지 목록은 <code>&lt;ul id=&quot;message-list&quot;&gt;</code> 하위에 동적으로 추가됩니다.</li>
</ul>
</blockquote>
<hr>
<h2 id="2-db-연결-설정-supabasejs">2. DB 연결 설정: <code>supabase.js</code></h2>
<p>데이터를 불러오고 저장하기 위해 Supabase 클라이언트를 초기화합니다.</p>
<pre><code class="language-javascript">import { createClient } from &quot;@supabase/supabase-js&quot;;

// Supabase와 연결하는 객체 생성
export const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,     // 프로젝트 URL
  import.meta.env.VITE_SUPABASE_ANON_KEY // 공개 API 키
);</code></pre>
<blockquote>
<p><strong>환경 변수(env) 사용 시 주의점</strong></p>
<ul>
<li><strong><code>createClient</code></strong>: Supabase와의 통신을 담당하는 연결 객체를 생성합니다.</li>
<li><strong><code>import.meta.env</code></strong>: Vite 환경에서 환경 변수를 가져오는 전용 문법입니다. (기존 Node.js 방식인 <code>process.env</code>를 브라우저에서 사용하면 에러가 발생합니다!)</li>
</ul>
</blockquote>
<hr>
<h2 id="비동기-처리async--await">비동기 처리(<code>async</code> / <code>await</code>)</h2>
<p>데이터베이스와 통신하는 코드를 작성하기 전, 반드시 짚고 넘어가야 할 개념이 있습니다. 바로 <strong>비동기 처리</strong>입니다.</p>
<h3 id="비동기란-무엇인가요">비동기란 무엇인가요?</h3>
<p>일반적인 동기(Synchronous) 코드는 위에서 아래로 한 줄씩 순서대로 실행됩니다. 하지만 데이터베이스에 데이터를 요청하는 작업은 네트워크 상황에 따라 시간이 걸립니다. 결과를 기다리는 동안 웹페이지 전체가 멈춰버리면 안 되기 때문에, <strong>기다리는 동안 다른 작업을 할 수 있도록 비동기(Asynchronous) 방식</strong>으로 처리합니다.</p>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="left">특징</th>
<th align="left">비유</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>동기 (Sync)</strong></td>
<td align="left">앞 작업이 끝나야 다음 작업 시작</td>
<td align="left">커피 주문 후, 내 커피가 나올 때까지 계산대 앞에서 멍하니 기다리기</td>
</tr>
<tr>
<td align="left"><strong>비동기 (Async)</strong></td>
<td align="left">시간이 걸리는 작업은 던져두고 다음 코드 실행</td>
<td align="left">진동벨을 받고 자리에 가서 다른 일(핸드폰 등)을 하다가, 울리면 커피 받으러 가기</td>
</tr>
</tbody></table>
<h3 id="async--await-기본-구조"><code>async</code> / <code>await</code> 기본 구조</h3>
<pre><code class="language-javascript">async function 함수이름() {
  const 결과 = await 시간이_걸리는_작업();
}</code></pre>
<ul>
<li><strong><code>async</code></strong>: &quot;이 함수는 내부에 비동기 작업을 포함하고 있습니다&quot;라고 선언하는 역할입니다.</li>
<li><strong><code>await</code></strong>: &quot;이 작업이 끝날 때까지 여기서 잠시 기다리겠습니다&quot;라는 의미입니다. (반드시 <code>async</code> 함수 안에서만 쓸 수 있습니다.)</li>
</ul>
<hr>
<h2 id="3-messagesjs">3. <code>messages.js</code></h2>
<p>이제 배운 비동기 개념을 활용하여, 메시지를 불러오고(Read) 새 메시지를 등록하는(Create) 기능을 완성해 보겠습니다.</p>
<pre><code class="language-javascript">import { supabase } from &quot;./supabase.js&quot;;

// 1. DOM 요소 가져오기
const list = document.querySelector(&quot;#message-list&quot;);
const nicknameInput = document.querySelector(&quot;#nickname&quot;);
const contentInput = document.querySelector(&quot;#content&quot;);
const addBtn = document.querySelector(&quot;#add-btn&quot;);

// 2. 메시지 목록 불러오기 (Read)
async function loadMessages() {
  const { data, error } = await supabase
    .from(&quot;messages&quot;) // messages 테이블에서
    .select(&quot;*&quot;)      // 모든 데이터 조회
    .order(&quot;created_at&quot;, { ascending: false }); // 최신순(내림차순) 정렬

  // 에러 예외 처리
  if (error) {
    console.error(&quot;데이터 로드 실패:&quot;, error);
    return;
  }

  // 배열 데이터를 HTML 문자열로 변환하여 화면에 그리기
  list.innerHTML = data
    .map((msg) =&gt; `
      &lt;li data-id=&quot;${msg.id}&quot;&gt;
        &lt;strong&gt;${msg.nickname}&lt;/strong&gt; : ${msg.content}
        &lt;button class=&quot;edit-btn&quot;&gt;수정&lt;/button&gt;
        &lt;button class=&quot;delete-btn&quot;&gt;삭제&lt;/button&gt;
      &lt;/li&gt;
    `)
    .join(&quot;&quot;); // 배열을 하나의 문자열로 합침
}

// 3. 등록 버튼 클릭 이벤트 (Create)
addBtn.addEventListener(&quot;click&quot;, async () =&gt; {
  const nickname = nicknameInput.value.trim();
  const content = contentInput.value.trim();

  // 입력값 빈 칸 검증
  if (!nickname || !content) {
    alert(&quot;이름과 내용을 모두 입력하세요.&quot;);
    return;
  }

  // DB에 새 데이터 추가
  const { error } = await supabase
    .from(&quot;messages&quot;)
    .insert({ nickname, content }); // 객체 형태로 전달

  if (error) {
    console.error(&quot;데이터 저장 실패:&quot;, error);
    return;
  }

  // 입력창 초기화
  nicknameInput.value = &quot;&quot;;
  contentInput.value = &quot;&quot;;

  // 목록 다시 불러와서 화면 갱신
  loadMessages();
});

// 4. 페이지가 처음 열릴 때 목록 로드 실행
loadMessages();</code></pre>
<blockquote>
<p><strong>동작 흐름 요약</strong></p>
<ol>
<li>페이지 로드 시 <code>loadMessages()</code> 최초 실행</li>
<li>DB에서 최신 데이터 가져와서 화면에 출력(<code>innerHTML</code>)</li>
<li>사용자가 입력 후 &#39;등록&#39; 버튼 클릭 시 <code>insert</code>로 DB에 추가</li>
<li><code>loadMessages()</code>를 다시 호출하여 업데이트된 내용을 화면에 반영</li>
</ol>
</blockquote>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[#32 #supabase로 방명록 웹사이트 만들기(1)]]></title>
            <link>https://velog.io/@int_1sy/32-supabase%EB%A1%9C-%EB%B0%A9%EB%AA%85%EB%A1%9D-%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B01</link>
            <guid>https://velog.io/@int_1sy/32-supabase%EB%A1%9C-%EB%B0%A9%EB%AA%85%EB%A1%9D-%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B01</guid>
            <pubDate>Wed, 25 Mar 2026 16:54:02 GMT</pubDate>
            <description><![CDATA[<h3 id="개발환경-설정--supabase-준비하기">개발환경 설정 &amp; Supabase 준비하기</h3>
<p>안녕하세요.
오늘은 <strong>SUPABASE를 활용한 방명록 웹사이트 만들기 1편</strong>으로,
개발을 시작하기 위한 환경 설정과 Supabase에서 미리 준비해야 할 내용을 정리해보겠습니다.</p>
<hr>
<h2 id="1-vite로-바닐라-js-개발환경-구축하기">1. Vite로 바닐라 JS 개발환경 구축하기</h2>
<h3 id="vite란">Vite란?</h3>
<p><strong>는 웹 개발을 빠르게 시작할 수 있도록 도와주는 빌드 도구입니다.</strong></p>
<blockquote>
<p>빌드란?
사람이 작성한 소스 코드를 컴퓨터가 실행할 수 있는 형태로 변환하는 과정</p>
</blockquote>
<h3 id="vite의-장점">Vite의 장점</h3>
<ul>
<li>빠른 개발 서버 실행 속도</li>
<li>HMR(Hot Module Replacement) 지원
→ 코드 수정 시 새로고침 없이 즉시 반영</li>
</ul>
<hr>
<h2 id="2-nodejs-설치">2. Node.js 설치</h2>
<p>Vite를 사용하기 위해서는 <strong>가 필요합니다.</strong></p>
<p>설치 후 아래 명령어로 정상 설치 여부를 확인할 수 있습니다.</p>
<pre><code class="language-bash">node -v</code></pre>
<p>버전이 출력되지 않는다면 설치가 제대로 되지 않은 상태입니다.</p>
<hr>
<h2 id="3-vite-프로젝트-생성">3. Vite 프로젝트 생성</h2>
<p>터미널에서 아래 명령어를 입력합니다.</p>
<pre><code class="language-bash">npm create vite@latest</code></pre>
<p>이후 다음과 같이 설정합니다.</p>
<ul>
<li>Project name: <code>guestbook</code></li>
<li>framework: Vanilla</li>
<li>variant: Javascript</li>
</ul>
<p>프로젝트 생성이 완료되면 <code>guestbook</code> 폴더만 따로 열어 개발을 진행합니다.</p>
<hr>
<h2 id="4-supabase-설정">4. Supabase 설정</h2>
<h3 id="4-1-계정-생성-및-프로젝트-생성">4-1. 계정 생성 및 프로젝트 생성</h3>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/7cff5fe8-76a9-43eb-982e-e67e9fddffeb/image.png" alt=""></p>
<p><a href="https://supabase.com/">Supabase 공식페이지</a>
→ 접속 후 Google 로그인 → 오른쪽 상단 &quot;Dashboard&quot; 클릭</p>
<hr>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/79f8dde3-a9c1-4a67-98df-bf63748fb005/image.png" alt=""></p>
<p>Organization이 있다면 선택하고, 없다면 New Organization 생성</p>
<hr>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/3139f413-9e9a-45f0-b12e-59386e38eb58/image.png" alt=""></p>
<p>Organization 내부에서 새로운 프로젝트 생성</p>
<hr>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/a9137f29-d80b-444e-961a-4f7e6f693ee0/image.png" alt=""></p>
<p>프로젝트 생성 완료
→ 이후 URL과 KEY를 확인할 수 있음</p>
<hr>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/7bd3babc-8c57-4b47-953e-4cc1181192eb/image.png" alt=""></p>
<p>왼쪽 메뉴에서 Project Overview / Table Editor / SQL Editor 확인 가능
→ Overview는 전체 정보를 한눈에 볼 수 있음</p>
<hr>
<h3 id="4-2-권한-설정-중요">4-2. 권한 설정 (중요)</h3>
<p>개발 전에 SQL Editor에서 권한 설정을 진행합니다.</p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/ff375c75-e53a-46e5-a614-8d9378e54aba/image.png" alt=""></p>
<pre><code class="language-sql">create policy &quot;Allow insert for everyone&quot;
-- 이 정책의 이름 (Supabase 대시보드에서 식별용)

on messages
-- messages 테이블에 적용

for insert
-- INSERT 작업에만 적용 (SELECT, UPDATE, DELETE는 별개 정책 필요)

to public
-- 로그인 안 한 사람 포함 모든 사용자에게 허용

with check (true);
-- 삽입 조건 검사 — true면 조건 없이 무조건 허용</code></pre>
<ul>
<li>로그인하지 않은 사용자도 데이터 입력 가능</li>
<li>방명록 기능 구현을 위한 기본 설정</li>
</ul>
<hr>
<h3 id="4-3-테이블-생성">4-3. 테이블 생성</h3>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/376e65a2-2d72-4084-972b-f7c3a60216db/image.png" alt=""></p>
<p>Table Editor → Create a Table 클릭</p>
<hr>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/37571220-13d7-44ca-9302-7b20979c4ef7/image.png" alt=""></p>
<p>다음과 같이 테이블 생성</p>
<ul>
<li><p>테이블명: <code>messages</code></p>
</li>
<li><p>컬럼:</p>
<ul>
<li><code>id</code> (Primary Key)</li>
<li><code>created_at</code> (timestamp, 기본값: now())</li>
<li><code>content</code> (텍스트)</li>
</ul>
</li>
</ul>
<blockquote>
<p>RLS가 활성화된 경우
정책(policy)이 없으면 데이터 조회가 되지 않으므로 주의</p>
</blockquote>
<hr>
<h2 id="5-supabase-연동하기">5. Supabase 연동하기</h2>
<h3 id="5-1-라이브러리-설치">5-1. 라이브러리 설치</h3>
<pre><code class="language-bash">npm install @supabase/supabase-js</code></pre>
<hr>
<h3 id="5-2-supabasejs-파일-생성">5-2. supabase.js 파일 생성</h3>
<pre><code class="language-javascript">import { createClient } from &quot;@supabase/supabase-js&quot;;
// supabase 클라이언트 생성 함수 가져오기

export const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,      // .env 파일의 Supabase 프로젝트 URL
  import.meta.env.VITE_SUPABASE_ANON_KEY, // .env 파일의 공개 API 키 
  // process.env 를 쓰면 브라우저에서 ReferenceError 발생 !! &lt; 이거 땜에 고생 좀 함
);
// 생성된 클라이언트를 export해서 다른 파일에서 import { supabase } 로 재사용</code></pre>
<hr>
<h3 id="5-3-환경-변수-설정-env">5-3. 환경 변수 설정 (.env)</h3>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/8da83e1b-9a93-484e-80a3-7f78142f3796/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/0803ff76-07fa-4591-8665-59e7a1b5881d/image.png" alt=""></p>
<pre><code class="language-env">VITE_SUPABASE_URL=본인_URL
VITE_SUPABASE_ANON_KEY=본인_KEY</code></pre>
<p>Supabase Dashboard에서 URL과 KEY를 복사하여 입력하면 됩니다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>이번 글에서는 방명록 웹사이트 개발을 위한
기본적인 개발환경 설정과 Supabase 준비 과정을 진행했습니다.</p>
<p>다음 글에서는
실제로 데이터를 저장하고 불러오는 기능을 구현해보겠습니다 !</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[#31 #깃허브 계정 생성, 프로그래머스, 백준 잔디 연결]]></title>
            <link>https://velog.io/@int_1sy/31-%EA%B9%83%ED%97%88%EB%B8%8C-%EA%B3%84%EC%A0%95-%EC%83%9D%EC%84%B1-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%B0%B1%EC%A4%80-%EC%9E%94%EB%94%94-%EC%97%B0%EA%B2%B0</link>
            <guid>https://velog.io/@int_1sy/31-%EA%B9%83%ED%97%88%EB%B8%8C-%EA%B3%84%EC%A0%95-%EC%83%9D%EC%84%B1-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%B0%B1%EC%A4%80-%EC%9E%94%EB%94%94-%EC%97%B0%EA%B2%B0</guid>
            <pubDate>Wed, 25 Mar 2026 12:35:15 GMT</pubDate>
            <description><![CDATA[<p>코딩테스트 준비를 위해 꼭 필요한
<strong>GitHub, 백준, 프로그래머스, 백준허브</strong> 세팅 방법을 정리했습니다! ✨</p>
<hr>
<h2 id="1-github-회원가입">1. GitHub 회원가입</h2>
<p>👉 <a href="https://github.com/">깃허브 홈페이지</a></p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/272fb1bd-dc13-4f51-9c23-1aed18bc72a8/image.png" alt=""></p>
<ol>
<li>오른쪽 상단 <strong>Sign up</strong> 버튼 클릭</li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/7bf5f643-ffa5-485b-bb95-046415851164/image.png" alt=""></p>
<ol start="2">
<li><strong>Continue with Google</strong> 버튼 클릭</li>
<li><strong>학교 구글 계정으로 회원가입</strong></li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/7a1bea93-6e9e-45ed-9ee5-3f94f043a021/image.png" alt=""></p>
<ol start="4">
<li>Username 원하는 이름 작성 후 <strong>Create account</strong> 클릭</li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/30b02f4d-cedd-4a76-ac17-ee5ed68c0bfb/image.png" alt="">
<img src="https://velog.velcdn.com/images/int_1sy/post/d9c888b8-ba2a-4ae3-94df-c3cc5b952003/image.png" alt=""></p>
<ol start="5">
<li>인증 단계
👉 왼쪽 그림과 <strong>같지만 주변 배경이 왜곡된 이미지</strong> 선택 → <strong>Submit 클릭!</strong></li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/6403861e-5576-4a52-a8f3-d09b3220b355/image.png" alt="">
<img src="https://velog.velcdn.com/images/int_1sy/post/742939f7-f820-4de6-9423-21e1c87439ed/image.png" alt=""></p>
<ol start="6">
<li><p>계정 생성 성공! 🎉</p>
</li>
<li><p>오른쪽 상단 <strong>프로필 클릭</strong></p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/ac5bc2cb-92cc-465e-9f87-0166dbfb1a5d/image.png" alt=""></p>
<ol start="8">
<li>잔디(커밋 기록)가 생성됐는지 확인 </li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/2e0e41a0-4ed3-49d6-905c-c145f0dddc23/image.png" alt=""></p>
<hr>
<h2 id="2-백준-회원가입">2. 백준 회원가입</h2>
<p>👉 <a href="https://www.acmicpc.net/">https://www.acmicpc.net/</a></p>
<ol start="9">
<li><p>크롬에 <strong>백준 검색 후 접속</strong></p>
</li>
<li><p>오른쪽 상단 <strong>회원가입 클릭</strong></p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/e9283902-486d-4955-8873-c0d93dd3dcb3/image.png" alt="">
<img src="https://velog.velcdn.com/images/int_1sy/post/7f0fd834-325a-417d-b50d-2e860c489e2b/image.png" alt=""></p>
<ol start="11">
<li><p>사진처럼 입력 후 <strong>학교 이메일 사용</strong></p>
</li>
<li><p>회원가입 완료! 🎉</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/d12bb530-9779-4a72-af8a-1e276ffebe41/image.png" alt=""></p>
<hr>
<h2 id="3-프로그래머스-회원가입">3. 프로그래머스 회원가입</h2>
<p>👉 <a href="https://programmers.co.kr/">프로그래머스 링크</a></p>
<ol start="13">
<li>크롬에 <strong>프로그래머스 검색 후 접속</strong></li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/084f073f-afcc-4233-bee1-aac305e8d7a0/image.png" alt=""></p>
<ol start="14">
<li>오른쪽 상단 <strong>로그인 클릭</strong></li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/e7607c31-b31b-465c-a27b-932114ae61d3/image.png" alt=""></p>
<ol start="15">
<li><strong>GitHub 계정으로 시작하기 (고양이 아이콘)</strong> 클릭</li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/9e4daa84-215e-4774-91c3-3235a8d666e5/image.png" alt=""></p>
<ol start="16">
<li>초록색 <strong>Authorize</strong> 버튼 클릭</li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/a0371244-7854-4095-a3c3-a08362790eec/image.png" alt=""></p>
<ol start="17">
<li>회원가입 선택 (학교 이메일 확인!)</li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/bbca316f-c03b-48fb-91d4-94e6fd49cad6/image.png" alt=""></p>
<ol start="18">
<li>프로그래머스 가입 완료! 🎉</li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/9f7bd1af-cd98-45ec-9e3e-3ddb3d3a48b0/image.png" alt=""></p>
<hr>
<h2 id="🔗-4-백준허브-설치-및-연결">🔗 4. 백준허브 설치 및 연결</h2>
<p>👉 <a href="https://chromewebstore.google.com/detail/%EB%B0%B1%EC%A4%80%ED%97%88%EB%B8%8Cbaekjoonhub/ccammcjdkpgjmcpijpahlehmapgmphmk?utm_source=item-share-cb">백준허브 설치</a></p>
<ol start="19">
<li><strong>Chrome에 추가 클릭</strong></li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/5126f00e-48b3-434f-b40a-b3a84b16eeef/image.png" alt=""></p>
<ol start="20">
<li><strong>확장 프로그램 추가</strong></li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/61825dd5-6777-4f1f-9b84-f1ce9449bc79/image.png" alt="">
<img src="https://velog.velcdn.com/images/int_1sy/post/bbb341fe-4bdf-44e5-88cf-61ee7b17878c/image.png" alt=""></p>
<ol start="21">
<li>크롬 오른쪽 위 <strong>퍼즐 아이콘 클릭</strong></li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/75db25cc-b945-4190-948c-b294eb78c242/image.png" alt=""></p>
<ol start="22">
<li><strong>백준허브 선택</strong></li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/3b9b5713-9567-4d5e-8629-3ce0da558ac5/image.png" alt=""></p>
<ol start="23">
<li><strong>GitHub 인증하기 클릭</strong></li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/61bbc6ba-5182-410e-bbd2-aebb7f5d8349/image.png" alt=""></p>
<ol start="24">
<li><strong>Authorize BackjoonHub 클릭</strong></li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/887384b1-6585-4c34-b9f3-8dbfd7e68f81/image.png" alt="">
<img src="https://velog.velcdn.com/images/int_1sy/post/5b079235-596f-42b8-94bc-6136eb200a40/image.png" alt="">
<img src="https://velog.velcdn.com/images/int_1sy/post/303be550-91b8-4356-b6ae-347f96c396a3/image.png" alt=""></p>
<ol start="25">
<li><strong>새 저장소 생성 (비공개 추천)</strong>
👉 플랫폼별로 폴더 정리
👉 예: <code>CodingTest</code></li>
</ol>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/e38c5eac-06e5-44a0-bc07-8944dde428c9/image.png" alt=""></p>
<ol start="26">
<li>🎉 <strong>백준 + 프로그래머스 + GitHub 연동 완료!</strong></li>
</ol>
<hr>
<h2 id="✨-마무리">✨ 마무리</h2>
<p>이제 코딩 테스트를 풀면 자동으로 초록 체크 나오면 성공입니다!
다들 개발자 될 준비 완.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[#29 #리액트는 프레임워크가 맞을까?]]></title>
            <link>https://velog.io/@int_1sy/29-%EB%A6%AC%EC%95%A1%ED%8A%B8%EB%8A%94-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC%EA%B0%80-%EB%A7%9E%EC%9D%84%EA%B9%8C-w0gvdmi3</link>
            <guid>https://velog.io/@int_1sy/29-%EB%A6%AC%EC%95%A1%ED%8A%B8%EB%8A%94-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC%EA%B0%80-%EB%A7%9E%EC%9D%84%EA%B9%8C-w0gvdmi3</guid>
            <pubDate>Thu, 05 Mar 2026 05:06:01 GMT</pubDate>
            <description><![CDATA[<h2 id="리액트는-프레임워크일까-라이브러리일까">리액트는 프레임워크일까 라이브러리일까</h2>
<p>최근 전공 수업인 [웹응용(JavaScript)] 강의를 듣다가 오잉??하는 문장을 발견했습니다.</p>
<blockquote>
<ol start="3">
<li>바닐라 자바스크립트(바닐라 JS) : 어떠한 라이브러리(JQuery..)나 <strong>프레임워크(React, Vue, Angular....)</strong>를 사용하지 않는 순수 자바스크립트... (후략)</li>
</ol>
</blockquote>
<p>이 문장을 보고 제 머릿속엔 의문이 크게 들었습니다..</p>
<blockquote>
<p>&quot;어? 내가 알기로 React는 라이브러리 아닌가?&quot;</p>
</blockquote>
<p>프레임워크라고 하면 보통 서버와 애플리케이션 구조를 포함하는 큰 틀을 제공하는 기술을 떠올리곤 합니다.
예를 들어 Python에서는 <strong>Flask</strong>나 FastAPI 같은 것이 프레임워크로 불립니다.</p>
<p>반면 React는 UI를 만들기 위한 도구(프론트)이고, 백엔드나 데이터베이스를 사용하려면 별도의 서비스나 기술을 함께 사용해야 합니다.</p>
<p>그래서 이번 글에서는 <strong>라이브러리</strong>와 <strong>프레임워크</strong>의 차이를 정리하고, React의 정체성이 무엇인지 살펴보려고 합니다.</p>
<h4 id="1-리액트-공식-문서의-입장">1. 리액트 공식 문서의 입장</h4>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/ec4ac5fb-2e47-446a-87f0-11e90a744613/image.png" alt="">
위 사진은 리액트 공식문서의 가장 처음 페이지인데요, </p>
<blockquote>
<p><strong>&quot;사용자 인터페이스를 만들기 위한 JavaScript 라이브러리&quot;</strong></p>
</blockquote>
<p>공식 문서에서도 리액트는 라이브러리라고 말하고 있습니다.</p>
<h4 id="2-awsamazon-web-services의-입장">2. AWS(Amazon Web Services)의 입장</h4>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/68648334-37bc-4288-bf57-67283d354746/image.png" alt="">
반면 AWS와 같은 클라우드 서비스나 일부 기술 문서에서는 리액트를 일반적인 웹 애플리케이션 프레임워크 중 하나로 분류하곤 합니다.
공식문서에서는 라이브러리.. AWS에서는 프레임워크...</p>
<h3 id="프레임워크와-라이브러리-무엇이-다를까">프레임워크와 라이브러리, 무엇이 다를까?</h3>
<h4 id="프레임워크-framework">프레임워크 (Framework)</h4>
<blockquote>
<p>소프트웨어 엔지니어링에서 프레임워크는 새로운 애플리케이션을 효율적으로 개발할 수 있도록 하는 재사용 가능한 소프트웨어 구성 요소의 모음을 의미합니다.</p>
</blockquote>
<p>프레임워크는 일정한 표준과 프로토콜* 을 기반으로 만들어지며, 여러 개의 재사용 가능한 코드 모듈을 포함합니다.</p>
<pre><code>여기서 프로토콜* 이란
컴퓨터에서 데이터를 원활하게 주고받기 위해 약속된 규칙과 양식의 체계를 의미합니다.</code></pre><p>프레임워크는 높은 프로그래밍 <strong>표준에 맞게 설계</strong>되어 있기 때문에 <strong>코드의 품질을 향상</strong>시키는 데 도움을 줍니다.
또한 개발자가 기본적인 구조를 직접 구현하기보다는 <strong>비즈니스 로직* 에 더 집중</strong>할 수 있도록 해 줍니다.</p>
<pre><code>여기서 비즈니스 로직이란 현실 세계의 업무 규칙을 프로그램 코드로 구현한 핵심적인 부분을 의미합니다.</code></pre><p>또한 프레임워크는 개발자가 소프트웨어를 <strong>빠르게 개발</strong>하고 프로덕션 환경에 배포할 수 있도록 다양한 기능을 제공합니다.</p>
<pre><code>여기서 프로덕션 배포란 개발과 테스트가 완료된 소프트웨어를 실제 서비스 환경에 배포하는 단계를 의미합니다.</code></pre><p>예를 들어 다음과 같은 요소들이 포함될 수 있습니다.</p>
<pre><code>- API(서로 다른 소프트웨어가 상호 이해할 수 있도록하는 프로토콜

- 코드 라이브러리(발자가 코드에 연결할 수 있는 재사용 가능한 소프트웨어 함수 모음)

- 디버거(프로그래머가 코드에서 실수를 찾아 수정하는 데 도움이 되는 도구)

- 컴파일러(개발자가 소스 코드를 배포 가능한 애플리케이션 파일로 변환하는 데 사용하는 소프트웨어 도구)</code></pre><h4 id="라이브러리-library">라이브러리 (Library)</h4>
<blockquote>
<p>라이브러리는 소프트웨어를 개발할 때 프로그램에서 사용할 수 있는 재사용 가능한 코드와 자원의 모음입니다.</p>
</blockquote>
<p>라이브러리는 다음과 같은 요소들로 구성될 수 있습니다.</p>
<pre><code>- 미리 작성된 코드

- 함수와 클래스

- 데이터 구조

- 문서 및 도움말 자료

- 메시지 템플릿</code></pre><p>라이브러리는 <strong>독립적으로 실행되는 프로그램</strong>이 아니라, <strong>특정 기능을 수행하기 위해 만들어진 코드 모음</strong>입니다.</p>
<p>즉 라이브러리 자체만으로는 일반적인 프로그램처럼 실행할 수 없으며,
개발자가 프로그램 안에서 라이브러리의 기능을 직접 호출하여 사용해야 합니다.</p>
<h2 id="그래서-리액트가-뭐라고">그래서 리액트가 뭐라고?</h2>
<h1 id="리액트는-라이브러리이다">리액트는 라이브러리이다!!!</h1>
<p>왜냐하면 리액트는 기본적으로 V-DOM(Virtual DOM), 컴포넌트, 상태, 선언형 UI 등 여러가지의 혁신적인 기능을 가지고 있지만 실제 웹,앱을 만드는데 직접적으로 사용되는 부분이 아니기 때문에 리액트 자체는 프레임워크라고 말 할 수 없습니다.</p>
<p>예를 들면 </p>
<pre><code>라우팅 -&gt; React Router
상태 관리 -&gt; Redux / Zustand
스타일링 → styled-components / Tailwind CSS</code></pre><p>과 같이 리액트는 UI를 그리는 기능만을 가지고 있는 라이브러리입니다.</p>
<h3 id="그럼-왜-사람들이-프레임워크라고-여기냐">그럼 왜 사람들이 프레임워크라고 여기냐?</h3>
<p>리액트 기반 프레임워크가 현재 많이 사용되기 때문입니다.
<img src="https://velog.velcdn.com/images/int_1sy/post/14e2a01c-0e33-44b0-b37f-a64c96a8af4a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/73ea818b-422b-4e03-b235-cc4d13bb459c/image.png" alt="">
리액트에서 자주 사용하는 NEXT.js의 공홈에 있는 것과 같이
&quot;The React Framework for the Web&quot; = 리액트 프레임워크 웹용
처럼 각자 다른 라이브러리이지만 기본 내장이 되거나 긴밀하게 연관이 되어 있어</p>
<blockquote>
<p>리액트 자체는 도구(Library)지만, 우리가 리액트를 사용하는 방식은 이미 완성된 시스템(Framework) 안으로 들어와 버렸다</p>
</blockquote>
<p>라고 많은 사람들이 생각하기에 리액트가 프레임워크라는 말이 자주 나오는게 아닐까 싶습니다!</p>
<h2 id="마무리">마무리</h2>
<p>오늘은 리액트가 프레임워크인지 라이브러리인지에 대해 글을 작성해보았습니다.
수업 시간에 갑자기 들었던 생각을 더 심도있게 찾아보니 프레임워크와 라이브러리의 정의와 차이점에 더 깊게 이해할 수 있던 경험인 것 같아 좋은 기회였습니당🌸🌸</p>
<p>그럼 모두</p>
<h4 id="즐코">즐코!</h4>
<p>참고1 : <a href="https://aws.amazon.com/ko/what-is/framework/">AWS 프레임워크란?</a>
참고2 : <a href="https://ko.react.dev/">리액트 공식문서</a>
참고3 : <a href="https://blog.naver.com/neo3ds/223122514047?trackingCode=rss">[ IT 용어 ] 라이브러리 (Library)란 무엇인지 알아보자👀✨|작성자 네오</a>
참고4 : <a href="https://nextjs.org/">NEXT.js</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[#28 #Aice Basic 100점 정리]]></title>
            <link>https://velog.io/@int_1sy/28-Aice-Basic-100%EC%A0%90-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@int_1sy/28-Aice-Basic-100%EC%A0%90-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 12 Jan 2026 02:07:24 GMT</pubDate>
            <description><![CDATA[<h2 id="kt에서-주관하는-aice-시험이란">KT에서 주관하는 AICE 시험이란?</h2>
<p>최근 인공지능(AI)이 다양한 분야에서 활용되면서,
<strong>AI를 얼마나 잘 이해하고 실제로 활용할 수 있는지</strong>를 객관적으로 평가할 수 있는 기준의 필요성이 커지고 있습니다.
이러한 흐름 속에서 등장한 시험이 바로 <strong>AICE(AI Certificate for Everyone)</strong>입니다.</p>
<p>AICE는 <strong>KT</strong>가 주관하고,
한국경제신문과 함께 운영하는 <strong>AI 역량 인증 시험</strong>으로,
AI 전공자가 아니더라도 누구나 도전할 수 있도록 설계된 것이 특징입니다.</p>
<hr>
<h2 id="aice-시험의-목적">AICE 시험의 목적</h2>
<p>AICE 시험은 단순히 AI 이론을 암기하는 시험이 아닙니다.
AI 기술을 <strong>이해하고 실제로 활용할 수 있는 능력</strong>을 평가하는 데 목적이 있습니다.</p>
<ul>
<li>AI 기초 개념에 대한 이해</li>
<li>데이터를 기반으로 문제를 해석하는 능력</li>
<li>AI 기술을 실생활 또는 업무에 적용할 수 있는 사고력</li>
<li>AI 리터러시(AI literacy) 역량 검증</li>
</ul>
<p>즉, “AI를 얼마나 깊이 아는가”보다는
👉 <strong>“AI를 어떻게 활용할 수 있는가”</strong>에 초점을 둔 시험입니다.</p>
<hr>
<h2 id="aice-시험의-특징">AICE 시험의 특징</h2>
<h3 id="1-수준별-등급-구성">1. 수준별 등급 구성</h3>
<p>AICE는 응시자의 수준에 따라 여러 등급으로 나뉘어 있습니다.</p>
<ul>
<li><p><strong>Future / Junior</strong>
: 초·중·고 학생 대상, AI 친화력 및 기초 개념 중심</p>
</li>
<li><p><strong>Basic</strong>
: AI 입문자 대상, 기본적인 AI 원리와 활용 능력 평가</p>
</li>
<li><p><strong>Associate (ASSO)</strong>
: 데이터 분석 및 AI 모델 활용 등 실무 기초 역량 평가</p>
</li>
<li><p><strong>Professional</strong>
: AI 실무 및 심화 분석 역량을 평가하는 전문가 과정</p>
</li>
</ul>
<p>이처럼 난이도가 단계적으로 구성되어 있어
<strong>AI를 처음 접하는 학습자부터 실무 수준의 학습자까지</strong> 모두 도전할 수 있습니다.</p>
<p>특히 주목할 점은,
<strong>Basic 등급까지는 KT에서 자체적으로 제공하는 시험 플랫폼을 통해 시험이 진행되지만,
Associate 등급부터는 Python을 활용한 실제 데이터 처리 및 분석이 요구된다는 점</strong>입니다.
이 지점부터 본격적인 실무형 AI 시험이라는 인상을 받게 됩니다.</p>
<hr>
<h3 id="2-실무-중심-평가-방식">2. 실무 중심 평가 방식</h3>
<p>AICE 시험은 객관식 위주의 단순 이론 평가에 그치지 않습니다.
다음과 같은 요소들을 종합적으로 평가합니다.</p>
<ul>
<li>AI 관련 개념 이해 능력</li>
<li>데이터 해석 및 분석 능력</li>
<li>Python 기반 문제 해결 능력</li>
<li>AI 모델을 활용한 실습형 문제 해결 능력</li>
</ul>
<p>특히 상위 등급으로 갈수록
<strong>이론보다 실습과 문제 해결 능력의 비중이 크게 증가합니다.</strong></p>
<hr>
<h3 id="3-온라인-시험-방식">3. 온라인 시험 방식</h3>
<p>AICE 시험은 대부분 <strong>온라인으로 응시</strong>할 수 있으며,
등급에 따라 시험 시간과 문제 구성에는 차이가 있습니다.</p>
<p>예를 들어,</p>
<ul>
<li>Associate 등급의 경우 약 <strong>90분 내외</strong>로 진행되며</li>
<li>실습과 사고력 중심의 문제로 구성됩니다</li>
</ul>
<p>정해진 시간 안에 문제를 해결하는 방식으로,
AI를 단순히 “아는지”보다 <strong>“실제로 활용할 수 있는지”</strong>를 평가합니다.</p>
<hr>
<h2 id="aice-시험의-활용-가치">AICE 시험의 활용 가치</h2>
<p>AICE 자격증은 다음과 같은 측면에서 활용도가 높습니다.</p>
<ul>
<li>AI 역량을 객관적으로 증명할 수 있음</li>
<li>AI 관련 진로 및 전공 관심도를 효과적으로 어필 가능</li>
<li>포트폴리오 및 자기소개서에 활용 가능</li>
<li>AI 기초 학습의 방향성을 설정하는 데 도움</li>
</ul>
<p>특히 학생의 입장에서는
👉 <strong>AI 학습의 결과를 공식적으로 증명할 수 있는 인증 수단</strong>이라는 점에서 의미가 큽니다.</p>
<hr>
<h2 id="aice-재수-경험">AICE 재수 경험</h2>
<p>다소 부끄러운 이야기이지만,
필자는 <strong>AICE 시험 재수 경험자</strong>입니다..ㅎ</p>
<p>AICE 시험이 지금처럼 널리 알려지기 전부터
학교 방과후 수업을 통해 AICE를 준비하게 되었습니다.</p>
<p>샘플 문항과 자체 모의시험에서는 지속적으로 좋은 성적을 거두었고,
사전 점검 또한 문제없이 완료한 상태였기 때문에
시험에 대한 부담은 크지 않았습니다.</p>
<p>그러나 실제 시험 도중 문제가 발생하였습니다.
머신러닝 실행 버튼을 클릭한 이후,
<strong>총 시험 시간이 60분임에도 불구하고 약 20분 동안 로딩이 지속되는 상황</strong>등이 연속적으로 발생하였습니다.</p>
<p>문제 해결을 위해 감독관에게 여러 차례 문의를 남겼으나,
답변이 약 10분 간격으로 지연되면서
결과적으로 정상적인 시험 진행이 어려웠습니다.</p>
<p>이후 여러 응시자들의 후기를 확인해 본 결과,
유사한 경험을 한 사례가 적지 않다는 점을 알게 되었습니다.</p>
<p>따라서 AICE 시험을 준비하는 분들께는,
시험 도중 문제가 발생할 경우
<strong>당황하지 말고 즉시, 그리고 차분하게 문의를 남기는 것이 중요하다</strong>는 점을 꼭 말씀드리고 싶습니다!!</p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/c7a341de-8f8c-4ac9-bfa6-3a32b1950736/image.png" alt=""></p>
<p>(당시에는 상당히 당황스러운 경험이었습니다 ㅎㅎ;;)</p>
<h2 id="aice-공부-방법">AICE 공부 방법</h2>
<p>필자는 AICE를 준비하며 크게 세 가지 방법으로 공부를 진행하였습니다.</p>
<blockquote>
<ol>
<li>문제집을 바탕으로 관련 용어의 의미를 정리한 노트 작성</li>
</ol>
</blockquote>
<blockquote>
<ol start="2">
<li>AICE에서 제공하는 샘플 문항을 풀고, 유튜브 풀이 영상을 참고하여 부족한 개념 복습</li>
</ol>
</blockquote>
<blockquote>
<ol start="3">
<li>블로그와 티스토리에 정리된 AICE 시험 후기 및 팁 글을 활용한 최종 점검</li>
</ol>
</blockquote>
<p>특히 유튜브 강의의 경우
<a href="https://www.youtube.com/watch?v=ndndm-q8hQM&amp;list=PL3geb_qrBQYem8bEQ-Jazl6LWY2KlrrK3">엘리쌤과 인공지능 AICE BASIC 정리</a></p>
<p>플레이리스트의 모든 영상을 시청하며 학습을 진행하였습니다.</p>
<p>또한 <a href="https://blog.naver.com/onetoppro/223288942130">aice basic 티스토리 정리 글</a>을 집중적으로 참고하였으며,
개념 정리와 시험 흐름을 이해하는 데 큰 도움이 되었습니다.</p>
<p>마지막으로 제가 끄적끄적 작성해본 노트 제공하고 끝내도록 하겠습니다!<img src="https://velog.velcdn.com/images/int_1sy/post/629ade94-326c-4850-bb77-850ea1c7b606/image.png" alt=""></p>
<h4 id="그럼-모두-즐코">그럼 모두 즐코!</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[#27 #객체 지향 프로그래밍이 붕어빵과 붕어빵틀로 설명될 수 없는 이유]]></title>
            <link>https://velog.io/@int_1sy/27-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EC%9D%B4-%EB%B6%95%EC%96%B4%EB%B9%B5%EA%B3%BC-%EB%B6%95%EC%96%B4%EB%B9%B5%ED%8B%80%EB%A1%9C-%EC%84%A4%EB%AA%85%EB%90%A0-%EC%88%98-%EC%97%86%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@int_1sy/27-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EC%9D%B4-%EB%B6%95%EC%96%B4%EB%B9%B5%EA%B3%BC-%EB%B6%95%EC%96%B4%EB%B9%B5%ED%8B%80%EB%A1%9C-%EC%84%A4%EB%AA%85%EB%90%A0-%EC%88%98-%EC%97%86%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Fri, 19 Dec 2025 02:16:44 GMT</pubDate>
            <description><![CDATA[<h2 id="1-객체-지향-프로그래밍이란">1. 객체 지향 프로그래밍이란?</h2>
<p>객체 지향 프로그래밍(Object-Oriented Programming, OOP)은
프로그램을 <strong>객체들의 집합</strong>으로 보고,
이 객체들이 서로 <strong>상호작용하며 하나의 시스템을 구성</strong>하도록 설계하는 프로그래밍 방식이다.</p>
<p>사전적으로는 다음과 같이 정의됩니다.
<img src="https://velog.velcdn.com/images/int_1sy/post/2ef63793-9e28-4b17-a57a-359a69125ed4/image.png" alt="">
다소 추상적으로 느껴질 수 있지만, 이를 쉽게 풀어보면 다음과 같이 이해할 수 있습니다.</p>
<blockquote>
<p>객체 지향 프로그래밍은
<strong>현실 세계를 ‘객체’ 단위로 나누어 생각하며 이를 코드로 표현하는 방법</strong></p>
</blockquote>
<p>현실에서 사람, 자동차, 계좌, 게시글과 같은 개념을 떠올리듯
프로그램에서도 각각을 하나의 객체로 정의하고
그 객체들이 가진 속성과 행동을 중심으로 시스템을 설계합니다.</p>
<pre><code>클래스이름 객체이름 = new 클래스 이름</code></pre><p>이러한 형식으로 객체를 정의합니다.</p>
<hr>
<h2 id="2-객체object와-클래스class의-기본-개념">2. 객체(Object)와 클래스(Class)의 기본 개념</h2>
<p>객체 지향 프로그래밍을 이해하기 위해서는
<strong>객체(Object)</strong>와 <strong>클래스(Class)</strong>의 차이를 명확히 구분할 필요가 있습니다!
<img src="https://velog.velcdn.com/images/int_1sy/post/6ded0f9d-b481-43aa-bdb0-ffff9fc7c0ae/image.png" alt=""></p>
<h3 id="클래스class">클래스(Class)</h3>
<ul>
<li>객체를 만들기 위한 <strong>개념적 설계도</strong></li>
<li>“이 객체는 어떤 특징과 행동을 가진다”를 정의한 틀</li>
<li>추상적인 분류 또는 개념에 해당</li>
</ul>
<h3 id="객체object">객체(Object)</h3>
<ul>
<li>클래스를 기반으로 생성된 <strong>구체적인 실체</strong></li>
<li>실제로 값(데이터)과 행동(메서드)을 가지는 존재</li>
<li>데이터 및 수행하는 작업을 포함</li>
</ul>
<p>정리하면 다음과 같습니다,</p>
<ul>
<li>클래스: 개념, 분류, 설계도</li>
<li>객체: 그 설계도로 만들어진 실제 대상</li>
</ul>
<hr>
<h2 id="3-흔히-드는-예시와-그-차이점">3. 흔히 드는 예시와 그 차이점</h2>
<p>객체 지향 프로그래밍을 설명할 때 자주 등장하는 예시는 다음과 같습니다.</p>
<ul>
<li>과일 / 사과</li>
<li>과자 / 새우깡</li>
<li>자동차 / 소나타</li>
<li>붕어빵틀 / 붕어빵</li>
</ul>
<p>앞의 세 예시는 공통적인 구조를 가집니다.</p>
<h3 id="과일--사과">과일 / 사과</h3>
<h3 id="과자--새우깡">과자 / 새우깡</h3>
<h3 id="자동차--소나타">자동차 / 소나타</h3>
<h4 id="이-경우">이 경우,</h4>
<p>앞에 있는 객체가 상위 요소이고 뒤에 있는 객체가 앞에 있는 객체의 하위 요소입니다.
즉, 앞에 있는 객체(과일, 과자, 자동차)는 &quot;개념/분류&quot;이고,
뒤에 있는 객체들은 &quot;그 분류에 속하는 구체적인 존재&quot;입니다.</p>
<p>더 자세히,
앞에 있는 객체는 클래스로, 클래스는 &quot;이것은 어떠어떠한 특징을 가진 무엇이다&quot;라고 말하면 객체는 그거에 맞는 하위 분류로 그 개념에 맞는 여러개 중 하나가 될 수 있습니다.
즉,</p>
<ul>
<li>과일이라는 클래스에 속하는 객체가 사과</li>
<li>과자라는 클래스에 속하는 객체가 새우깡</li>
<li>자동차라는 클래스에 속하는 객체가 소나타</li>
</ul>
<p>이 구조는 객체 지향의 <strong>클래스-객체 관계</strong>를 정확히 표현합니다.</p>
<hr>
<h2 id="4-붕어빵틀--붕어빵-이건-뭔데">4. 붕어빵틀 / 붕어빵 이건 뭔데.</h2>
<p>반면, <code>붕어빵틀 / 붕어빵</code> 예시는 위의 구조와 성격이 다릅니다.<img src="https://velog.velcdn.com/images/int_1sy/post/4f06d31f-abf4-4e4e-bf78-7e1762d75ce0/image.png" alt=""></p>
<p>붕어빵틀이 붕어빵의 상위요소라고 할 수 있을까요?
붕어빵틀의 개념에 해당하는 구체적인 예세, 존재가 붕어빵이 맞을까요?</p>
<h4 id="아닙니다">아닙니다.</h4>
<p>붕어빵틀은 <strong>붕어빵을 만드는 도구</strong>이고,
붕어빵은 <strong>완성된 결과물</strong>입니다.</p>
<p>즉, 이 둘의 관계는
<strong>상위 개념과 하위 개념의 관계가 아니라, 제작 도구와 산출물의 관계</strong>이기 때문입니다.</p>
<p>다시 말해,</p>
<ul>
<li>붕어빵틀은 붕어빵의 ‘분류’가 아니며</li>
<li>붕어빵은 붕어빵틀이라는 개념에 포함되는 객체가 아니라고 말 할 수 있습니다.</li>
</ul>
<p>따라서 객체 지향 프로그래밍에서 말하는
<strong>클래스와 객체의 관계를 설명하기에는 개념적으로 정확하지 않은 예시</strong>라고 볼 수 있는거죠!</p>
<hr>
<h2 id="5-아니-그러면-그-사람들이-다-바보라고">5. 아니 그러면 그 사람들이 다 바보라고?</h2>
<p>그렇다면 왜 많은 설명에서
<code>클래스 = 붕어빵틀</code>, <code>객체 = 붕어빵</code>이라는 비유가 사용되었을까?라는 의문이 드실거라고 생각합니다. (저도 그랬거든요)</p>
<p>이는 객체 지향의 핵심 중 하나인
<strong>“하나의 클래스로 여러 객체를 생성한다”</strong>는 개념을
직관적으로 전달하기에 편리했기 때문이라고 생각합니다.</p>
<ul>
<li>하나의 틀</li>
<li>여러 개의 결과물</li>
</ul>
<p>이라는 구조는 초보자에게
클래스와 객체의 관계를 빠르게 이미지화하는 데 도움을 줄 수 있기 때문입니다.</p>
<p>하지만 이 설명은
<strong>이해를 돕기 위한 비유</strong>일 뿐,
객체 지향의 개념을 정확히 설명하는 예시는 아니라고 할 수 있는거죠!</p>
<hr>
<h2 id="6-보다-정확한-관점에서의-비유">6. 보다 정확한 관점에서의 비유</h2>
<p>객체 지향의 개념에 더 적합한 예시는 다음과 같습니다.</p>
<ul>
<li>붕어빵의 <strong>종류</strong> → 클래스</li>
<li>팥붕어빵, 슈크림붕어빵 → 객체</li>
</ul>
<p>이처럼 클래스는 ‘제작 도구’가 아니라
<strong>공통된 특징을 가진 개념적 분류</strong>로 이해하는 것이 바람직합니다.</p>
<hr>
<h2 id="7-마무리하며">7. 마무리하며</h2>
<p>이 글의 내용이 모든 분들의 생각과 완전히 일치하지 않을 수도 있고,
혹시 제가 잘못 이해한 부분이나 부족한 설명이 있을 수도 있다고 생각합니다.
저는 아직 배우는 과정에 있는 개발자로서,
이러한 개념들을 스스로 정리하고 고민해본 기록을 남기고자 이 글을 작성했습니다.</p>
<p>객체 지향 프로그래밍은 한 번에 완전히 이해하기 어려운 개념인 만큼,
다양한 관점과 설명을 접하는 것이 중요하다고 생각합니다.
글을 읽으시며 다른 의견이나 보완이 필요한 부분이 있다면
댓글로 자유롭게 공유해주시면 감사하겠습니다.</p>
<p>함께 배우고, 함께 성장하는 개발자가 되고 싶습니다.
읽어주셔서 감사합니다.</p>
<h4 id="모두-즐코">모두 즐코!</h4>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/18975ba0-0a71-4f44-8d7a-be2f301a6fe9/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[#26 #칭찬 도장 웹사이트 제작]]></title>
            <link>https://velog.io/@int_1sy/26-azmo8e4k</link>
            <guid>https://velog.io/@int_1sy/26-azmo8e4k</guid>
            <pubDate>Thu, 27 Nov 2025 09:32:26 GMT</pubDate>
            <description><![CDATA[<h1 id="✨-칭찬을-기록하면-성장이-보이기-시작한다--stampmarble-회고록">✨ 칭찬을 기록하면, 성장이 보이기 시작한다 — StampMarble 회고록</h1>
<blockquote>
<p>“분명 열심히 했는데,
그 순간의 칭찬이나 인정은 그냥 흘러가버리는 느낌…”</p>
</blockquote>
<p>칭찬을 받아도 잠깐 기분 좋고 끝.
내가 뭘 잘했고, 어떻게 성장했는지는 남지 않는다.</p>
<p>그래서 시작한 프로젝트가 바로
<strong>StampMarble(스탬프마블)</strong>이다 ✍️✨
이건 그냥 ‘칭찬 서비스’가 아니라,
<strong>칭찬을 기록으로 남겨 성장을 시각화하는 웹 서비스</strong>다.
<img src="https://velog.velcdn.com/images/int_1sy/post/5cd09975-bbff-47d5-9974-a9bbdba26275/image.png" alt=""></p>
<hr>
<h2 id="1-프로젝트-한눈에-보기">1. 프로젝트 한눈에 보기</h2>
<p><strong>프로젝트명</strong> : Stamp Marble
<strong>한 줄 소개</strong> : 칭찬을 ‘여행 기록’처럼 남겨, 성장을 눈으로 확인하는 서비스
<strong>핵심 키워드</strong> : 칭찬 · 성장 · 기록 · 여행 · 스탬프
<strong>기획 의도</strong> : 순간의 칭찬을 오래 남기는 구조 만들기</p>
<hr>
<h2 id="2-problem--왜-우리는-칭찬을-그냥-흘려보낼까">2. Problem : 왜 우리는 칭찬을 그냥 흘려보낼까?</h2>
<p>스탬프마블의 시작은 이 질문이었다.</p>
<blockquote>
<p><strong>“그냥 넘어가는 칭찬, 아쉽지 않았나요?”</strong></p>
</blockquote>
<p>학생들의 노력과 성취는</p>
<ul>
<li>금방 잊히고</li>
<li>기록으로 남지 않고</li>
<li>인정의 경험은 일회성으로 끝난다</li>
</ul>
<p>그래서
✔ 성취를 체계적으로 기록하고
✔ 칭찬을 시각적으로 남기고
✔ ‘성장 과정’ 자체를 느끼게 하는
서비스가 필요하다고 생각했다.</p>
<hr>
<h2 id="3-solution--칭찬을-여행-기록으로-바꾸다">3. Solution : 칭찬을 ‘여행 기록’으로 바꾸다</h2>
<p>StampMarble은
<strong>칭찬 = 여행 중 얻는 스탬프</strong>라는 컨셉에서 출발했다 ✈️</p>
<h3 id="✔-여행처럼-쌓이는-성장-기록">✔ 여행처럼 쌓이는 성장 기록</h3>
<ul>
<li>활동 → 칭찬 → 스탬프 획득</li>
<li>스탬프가 쌓일수록 나의 여정이 완성됨</li>
</ul>
<h3 id="✔-성장의-흐름이-한눈에-보이게">✔ 성장의 흐름이 한눈에 보이게</h3>
<ul>
<li>어디까지 왔는지</li>
<li>어떤 활동을 했는지</li>
<li>어떤 칭찬을 받았는지</li>
</ul>
<p>이걸 “기억”이 아니라 <strong>기록</strong>으로 남기고 싶었다.</p>
<hr>
<h2 id="4-service-01--여행사-기반-성장-참여-시스템">4. Service 01 : 여행사 기반 성장 참여 시스템</h2>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/f7254d95-9ccd-41c1-86f4-c99dc79e0f55/image.png" alt="">가장 중심이 되는 기능이다.</p>
<ul>
<li>활동 참여</li>
<li>성장 기록 자동 저장</li>
<li>활동별 스탬프 연결</li>
</ul>
<p>마치 여행지를 선택하듯
나의 성장 여정을 선택하고, 참여하고, 기록한다.</p>
<p>👉 “오늘 내가 한 활동이 그냥 사라지지 않게”</p>
<p>이게 목표였다.</p>
<hr>
<h2 id="5-service-02--도장stamp-발급을-통한-성장-기록">5. Service 02 : 도장(Stamp) 발급을 통한 성장 기록</h2>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/d4f43df1-d7bc-4b42-b116-1cae997dbe11/image.png" alt="">
칭찬을 받으면
<strong>도장(stamp)</strong>이 찍힌다.</p>
<ul>
<li><p>스탬프는 단순한 아이콘이 아니라
→ <em>“인정받은 경험의 증거”</em></p>
</li>
<li><p>날짜·활동·칭찬 내용과 함께 저장되어
→ 나만의 기록 페이지에 남는다</p>
</li>
</ul>
<p>책처럼 넘길 수 있는 UI로
“성장 다이어리” 같은 느낌을 주고 싶었다 📘</p>
<hr>
<h2 id="6-service-03--혜택쿠폰으로-이어지는-보상-시스템">6. Service 03 : 혜택(쿠폰)으로 이어지는 보상 시스템</h2>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/500b8465-a683-4cf0-9f26-af9ecf935101/image.png" alt="">
기록만 있으면 지속이 어렵다.
그래서 <strong>가벼운 보상 구조</strong>를 추가했다.</p>
<ul>
<li>스탬프 → 포인트</li>
<li>포인트 → 쿠폰/혜택</li>
<li>혜택은 ‘목적’이 아니라 ‘동기’</li>
</ul>
<p>칭찬을 보상 때문에 하는 게 아니라,
<strong>계속 참여하게 만드는 장치</strong>로만 설계했다.</p>
<hr>
<h2 id="7-사용자-시나리오">7. 사용자 시나리오</h2>
<h3 id="giver-칭찬하는-사람">Giver (칭찬하는 사람)</h3>
<ul>
<li>활동 생성</li>
<li>스탬프 발급</li>
<li>쿠폰 관리</li>
<li>참여 기록 확인</li>
</ul>
<h3 id="receiver-칭찬받는-사람">Receiver (칭찬받는 사람)</h3>
<ul>
<li>참여 기록 확인</li>
<li>스탬프 수집</li>
<li>성장 시각화</li>
<li>쿠폰 사용</li>
</ul>
<p>칭찬이 일방적인 행동이 아니라
<strong>관계 안에서 순환되도록</strong> 설계했다.</p>
<hr>
<h2 id="8-현재-한계점과-앞으로의-확장">8. 현재 한계점과 앞으로의 확장</h2>
<h3 id="현재-한계">현재 한계</h3>
<ul>
<li>모바일 최적화 부족</li>
<li>활동 종류 제한</li>
<li>개인 사용 위주 구조</li>
</ul>
<h3 id="앞으로-추가하고-싶은-기능">앞으로 추가하고 싶은 기능</h3>
<ul>
<li>학급/동아리 공동 운영</li>
<li>도장 자동 발급</li>
<li>여행지 공동 관리</li>
<li>모바일 앱 확장</li>
</ul>
<p>이 프로젝트는 끝이 아니라
<strong>확장 가능한 시작</strong>이라고 생각한다.</p>
<hr>
<h2 id="11-프로젝트를-통해-얻은-것">11. 프로젝트를 통해 얻은 것</h2>
<p>StampMarble을 하면서 얻은 가장 큰 성과는 이거다.</p>
<ul>
<li>서비스 관점에서 기획하는 사고</li>
<li>UX를 고려한 기능 설계</li>
<li>프론트–백–DB 흐름 이해</li>
<li>“사용자 경험이 먼저”라는 기준</li>
</ul>
<p>그리고 무엇보다,</p>
<blockquote>
<p><strong>기록은 곧 성장이라는 걸
직접 만들어보며 느꼈다.</strong></p>
</blockquote>
<hr>
<h2 id="마무리하며-🌱">마무리하며 🌱</h2>
<p>StampMarble은
“칭찬을 예쁘게 포장한 서비스”가 아니라,
<strong>성장을 남기고 싶어서 만든 기록 시스템</strong>이다.</p>
<p>이 프로젝트 덕분에
나도 내 성장 과정을
조금 더 또렷하게 바라볼 수 있게 됐다.</p>
<p>그리고 이 경험은
앞으로 어떤 프로젝트를 하든
분명 큰 기준이 될 것 같다.</p>
<blockquote>
<p>*<em>피피티 주소 : *</em> <a href="file:///C:/Users/user/Desktop/LeeSeoyoung/%EB%8B%A4%EB%9E%A9/%EC%8A%A4%ED%83%AC%ED%94%84%EB%A7%88%EB%B8%94_ppt.pdf">스템프마블.ppt</a></p>
</blockquote>
<hr>
<p>그럼 오늘도</p>
<h3 id="즐코-😆💙"><strong>즐코! 😆💙</strong></h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[#25 #LG CNS AI 지니어스 우수상 회고]]></title>
            <link>https://velog.io/@int_1sy/25-LG-CNS-AI-%EC%A7%80%EB%8B%88%EC%96%B4%EC%8A%A4-%EC%9A%B0%EC%88%98%EC%83%81-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@int_1sy/25-LG-CNS-AI-%EC%A7%80%EB%8B%88%EC%96%B4%EC%8A%A4-%EC%9A%B0%EC%88%98%EC%83%81-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Thu, 27 Nov 2025 09:32:08 GMT</pubDate>
            <description><![CDATA[<hr>
<h1 id="✨-7개월-동안-만들어낸-우리-팀의-첫-ai-서비스-sra">✨ 7개월 동안 만들어낸 우리 팀의 첫 AI 서비스, SRA</h1>
<p>안녕하세요! 이번 글은 LG CNS에서 주관하는 <strong>AI 지니어스 프로젝트</strong>를 약 7개월 동안 준비하고 완성한 과정들을 기록한 회고록입니다.
<img src="https://velog.velcdn.com/images/int_1sy/post/42dc20eb-0701-4c9a-98da-e1db880e2a92/image.jpg" alt="">
저에게는 단순한 대회 과제가 아니라, 실제로 불편했던 문제를 해결해보고 싶은 마음에서 시작된 꽤 진지한 도전이었어요.
그래도 열심히 하다 보니 과정 곳곳에 소소한 즐거움도 많아서, 이렇게 기록으로 남기고 싶어졌습니다!
(좋은 결과까지 받아서 더더욱 행복했던… 히히 ✨)
<img src="https://velog.velcdn.com/images/int_1sy/post/4b3a28a3-f1c7-4cb0-b283-08c1b9a5e73d/image.jpg" alt=""><img src="https://velog.velcdn.com/images/int_1sy/post/ebe7e45c-84d4-4aba-be67-206291799780/image.jpg" alt=""><img src="https://velog.velcdn.com/images/int_1sy/post/8d8328fe-d418-43e9-ba9d-70a9051c7090/image.jpg" alt=""></p>
<hr>
<h2 id="1-프로젝트-시작과-아이디어가-만들어지기까지">1. 프로젝트 시작과 아이디어가 만들어지기까지</h2>
<h3 id="📌-공학도서관에서-파이썬을-배우며-시작된-첫걸음">📌 공학도서관에서 파이썬을 배우며 시작된 첫걸음</h3>
<p>학교에서는 1학년 때 자바를 중심으로 배우지만, 저는 항상 “AI 시대에는 파이썬이 꼭 필요하지 않을까?” 하는 마음이 있었어요.
그래서 4월부터 7월까지 <strong>공학도서관 파이썬 특강</strong>을 들으며 기초를 차근차근 쌓았습니다.</p>
<p>그 과정 덕분에 자연스럽게 AI 지니어스 프로젝트에 참여할 수 있었고,
“아… 이걸 제대로 한번 해보고 싶다!”라는 본격적인 의지도 생겨났어요.</p>
<h3 id="📌-마스터데이에서-주제-선정하기">📌 마스터데이에서 주제 선정하기</h3>
<p>AI 지니어스는 반년 동안 특정 AI·ML 관련 수업을 모두 수강하고 과제를 완수해야 <strong>마스터데이 참가권</strong>이 주어졌습니다.</p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/5ea2288d-fe06-40c5-848b-049f8ffca089/image.png" alt=""></p>
<p>마스터데이에서는 다양한 주제가 있었는데,
저희 팀은 <strong>‘AI로 만드는 안전한 도시’</strong>를 선택했습니다.
도시에서 실제로 체감하는 불편함을 해결하고 싶었기 때문이에요.</p>
<p>저는 평소 떠오르는 아이디어를 바로바로 적어두는 <strong>아이디어 노트</strong>를 항상 들고 다니는데,
이번 프로젝트도 그 노트에서 출발했습니다.
작은 메모 하나가 실제 서비스 아이디어로 성장하는 걸 보면, 기록의 힘이 정말 크다는 걸 다시 느꼈어요!</p>
<hr>
<h2 id="2-우리-팀의-최종-아이디어-safe-ride-aisra">2. 우리 팀의 최종 아이디어: Safe Ride AI(SRA)</h2>
<p>혹시 <strong>&#39;킥라니&#39;</strong>라는 표현 알아시나요?
전동 킥보드를 위험하게 운행하는 사람들을 고라니처럼 갑자기 튀어나온다는 의미로 부르는 신조어예요.</p>
<p>그래서 저희 팀이 만든 서비스가 바로 <strong>SRA(Safe Ride AI)</strong>입니다!</p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/5d1c5e75-fbc5-4195-84ba-b49467f09861/image.png" alt=""></p>
<h3 id="sra의-목표는">SRA의 목표는?</h3>
<ul>
<li>헬멧 착용 여부 자동 확인</li>
<li>면허증 실명 인증</li>
<li>킥보드 QR 정보 확인</li>
<li>AI를 활용해 도시의 안전을 높이기</li>
</ul>
<p>저희 팀에는 디자인 전공자가 없었기 때문에
레퍼런스 사이트 <strong>10개 이상 분석</strong>, 피그마 초안 제작,
그리고 디자인과 선배·친구들에게 피드백을 받으면서
정말 한 땀 한 땀 UI를 완성해갔어요.</p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/8d068a1f-9701-4b76-9a4a-65ac103bd3e4/image.jpg" alt=""></p>
<p>디자인 경험이 많지 않아서 걱정했지만,
서비스의 목적이 뚜렷하다 보니 디자인 방향도 자연스럽게 잡히더라고요.</p>
<hr>
<h2 id="3-멘토링">3. 멘토링</h2>
<p>예선을 통과한 15팀에게는 전문 멘토링이 제공되는데,
저희 팀은 <strong>최종 본선 10팀 안에 꼭 들어가고 싶었기 때문에</strong>
멘토링 시간을 누구보다 열심히 활용했습니다.</p>
<ul>
<li>멘토링 전날 팀원들과 진행 상황 공유</li>
<li>질문 리스트 사전 작성</li>
<li>멘토링 직후 바로 온라인 회의</li>
<li>회의에서 TODO 리스트 확정</li>
</ul>
<p>짧은 30분이었지만, 그 시간을 얼마나 알차게 쓰느냐가 결과에 진짜 영향을 준다는 걸 깨달았어요! <img src="https://velog.velcdn.com/images/int_1sy/post/9f5a7e5b-67de-459e-931a-f339ee5219c1/image.png" alt="">
(멘토분도 열심히 하는거 보인다고 하셔서 더 햄복,,,)</p>
<hr>
<h2 id="4-기술-스택-및-기능-구현-상세">4. 기술 스택 및 기능 구현 상세</h2>
<h3 id="a-qr-확인-기능-html-css-javascript">A. QR 확인 기능 (HTML, CSS, JavaScript)</h3>
<ul>
<li>후면 카메라 실행 → QR 스캔</li>
<li>등록된 코드와 일치 여부 확인</li>
<li>성공 시 대여 화면 이동</li>
</ul>
<p>배포 과정에서 QR URL을 실제 서비스 URL로 연결해야 했는데,
시간상 여유가 부족해 기능을 끝까지 완성하지 못한 점이 아쉬워요.
그래도 덕분에 배포 구조를 다시 공부할 기회가 되었고,
“선택과 집중”의 중요성을 몸소 느끼게 되었어요.</p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/7b29131f-3080-4375-8b30-53ab66753040/image.jpg" alt=""></p>
<hr>
<h3 id="b-헬멧-착용-여부-확인-yolov10--fastapi">B. 헬멧 착용 여부 확인 (YOLOv10 + FastAPI)</h3>
<p>이 기능이 SRA의 핵심이었고,
저희 팀이 가장 많은 시간과 노력을 쏟아부은 기능이에요.
말 그대로 <strong>쓰라의 정체성!!</strong></p>
<h4 id="📌-데이터셋-문제-발생">📌 데이터셋 문제 발생</h4>
<p>처음에는 공개된 헬멧 데이터셋을 사용하려 했지만
라벨링이 엉망이라 정확도가 믿을 수 없을 정도로 낮았어요…</p>
<p>그래서 결국…</p>
<h4 id="📌-직접-데이터셋-제작">📌 직접 데이터셋 제작</h4>
<p>헬멧 착용 / 미착용 사진을 <strong>500장 이상 직접 촬영</strong>하고
LabelImg로 하나하나 라벨링했습니다.</p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/3504e06d-0528-4178-b0f8-de9ebb77ee2e/image.png" alt=""></p>
<p>진짜 손과 눈이 녹아 없어질 뻔했지만,
모델 성능이 엄청 좋아져서 보람도 그만큼 컸어요!</p>
<h4 id="📌-yolov10으로-모델-변경">📌 YOLOv10으로 모델 변경</h4>
<p>기존 YOLOv5 대신 <strong>YOLOv10</strong>을 사용하게 된 이유는:</p>
<ul>
<li>mAP 향상</li>
<li>학습 안정성</li>
<li>실시간 처리 성능 증가</li>
</ul>
<p>멘토님께 피드백 받으면서 모델 구조 자체를 다시 고민하게 되었고,
팀원들끼리 유튜브 강의 보면서 YOLO 스터디도 진행했어요!</p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/ee840926-b310-4039-962e-6236386924fe/image.png" alt=""></p>
<h4 id="📌-서버-연동-과정">📌 서버 연동 과정</h4>
<p>처음엔 Flask를 썼는데 이미지 처리에서 자꾸 오류가 났어요.
그래서 <strong>FastAPI로 전환</strong>하자 문제 없이 안정적으로 돌아갔습니다.</p>
<p>완성 직전에 오류가 나서 진짜 심장이 내려앉는 줄 알았던 순간도 있었어요 ㅋㅋㅋ</p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/1f5b8022-e66d-49e8-96bd-a6bd13883ac0/image.jpg" alt=""></p>
<hr>
<h3 id="c-면허증-실제-얼굴-매칭-facenet">C. 면허증-실제 얼굴 매칭 (faceNet)</h3>
<p>QR 인증 후 전면 카메라를 이용해
회원가입 시 등록한 면허증 사진과 실제 얼굴을 비교하는 기능입니다.</p>
<p>faceNet과 insightFace를 비교했는데,
저희 서비스 흐름과 정확성 기준을 만족한 모델이 faceNet이라 최종 채택했습니다.</p>
<p>threshold 조정, 유사도 거리 계산 등
성능 튜닝도 여러 번 반복했어요!</p>
<hr>
<h2 id="5-ai--센서-확장-실험">5. AI + 센서 확장 실험</h2>
<p>프로젝트 중 항상 들었던 질문이 있었어요.</p>
<h3 id="헬멧-인증할-때는-쓰다가-주행-중에-벗으면-어떻게-막을-건데"><strong>“헬멧 인증할 때는 쓰다가, 주행 중에 벗으면 어떻게 막을 건데?”</strong></h3>
<p>맞아요… 이건 기획 단계에서도 나왔던 우려였고,
소프트웨어만으로는 완전히 해결할 수 없는 부분이었어요.</p>
<p>그러던 중 멘토링에서
“헬멧에 센서를 달아보는 건 어떨까?”라는 조언을 받고
바로 하드웨어 실험을 병행하게 되었습니다!</p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/81a554ea-3238-4810-a7ad-a820a77bea07/image.jpg" alt=""></p>
<p>하지만…
저희는 <strong>뉴미디어소프트웨어과</strong> 학생들이기 때문에
하드웨어는 정말 하나하나가 난관이었어요 ㅋㅋㅋ</p>
<p>온도 센서를 잘못 사오고, 압력 센서가 작동 안 하고…
아두이노 없이 센서만 사는 실수를 하기도 하고…
담당 선생님께 정말 매일 찾아가 도움 요청하면서 해결해나갔습니다!</p>
<h3 id="📌-최종-적용-센서">📌 최종 적용 센서</h3>
<ul>
<li><strong>조도 센서</strong>: 헬멧 내부 밝기 감지</li>
<li><strong>LED</strong>: 착용 시 초록불 / 미착용 시 빨간불</li>
</ul>
<h3 id="📌-향후-넣고-싶은-센서">📌 향후 넣고 싶은 센서</h3>
<ul>
<li><strong>적외선 센서</strong>: 실제 머리 유무 감지</li>
<li><strong>가속도 센서</strong>: 주행 중 벗기, 충격 여부 감지</li>
</ul>
<p>프로젝트 기간이 제한적이다 보니 완벽한 결과물은 아니지만,
이 경험은 분명 더 발전된 SRA를 만드는 큰 발판이 될 거예요.</p>
<hr>
<h1 id="✨-55-lg-cns-본사-방문기">✨ 5.5 LG CNS 본사 방문기:</h1>
<p>1차 본선에 합격한 15개의 팀은 LG CNS 본사에 투어를 하고 간부분의 강의를 들을 수 있는 기회가 주어지는데요! 저는 그날 일정이 있어서.. 본사 투어를 못 갔어요(해커톤에서밤새코딩중이엿뜸)</p>
<p>그렇기에 전 첫 방문이였는데요 ㅎㅎ
전 서울에서 학교 다니지만 대구사람이여서 서울 당일치기를 하며 하루를 시작했습니다 ㅎㅎ
6시 첫차 타구 방문했는데용! 사실 그날 완성하느라 잠을 못자서 무수면 상태로 방문했답니다... ㅎㅎ</p>
<hr>
<h2 id="6-프로젝트를-마치며">6. 프로젝트를 마치며</h2>
<p>SRA 프로젝트는 단순히 기술을 구현하는 과제를 넘어
“AI가 실제 문제 해결에 어떻게 적용될 수 있을까?”를 진짜로 고민하게 해준 시간이었어요.</p>
<p>모델을 바꾸고, 데이터를 만들고, 서버를 고치고, 디자인을 수정하고…
그 모든 과정을 지나오면서
<strong>“아, 나 진짜 개발자로 성장하고 있구나”</strong>
하는 실감이 들었습니다.</p>
<p>특히 스스로 문제를 정의하고 해결책을 직접 만들어가는 경험은
앞으로 어떤 프로젝트를 하든 가장 큰 자산이 될 거라고 믿습니다!</p>
<hr>
<h2 id="7-앞으로의-계획">7. 앞으로의 계획</h2>
<ul>
<li>YOLO 모델 경량화 및 모바일 최적화</li>
<li>FastAPI ↔ 프론트엔드 연동 구조 개선</li>
<li>IR·가속도 센서 기반 헬멧 인증 고도화</li>
<li>더 직관적이고 편리한 UX 개발</li>
</ul>
<p>아직 부족한 부분도 많지만,
이번 프로젝트 덕분에 제가 만들고 싶은 서비스의 방향이 더 명확해졌어요.</p>
<hr>
<h2 id="🏆-마지막으로">🏆 마지막으로…</h2>
<p>오늘은 <strong>1200명 중 2등</strong>이라는, 인생에서 너무 소중한 순간을 다시 떠올리며
LG CNS AI 지니어스 회고록을 작성해봤습니다!</p>
<p>1년의 가장 큰 프로젝트였고
고난도 많았지만,
최종 발표에서 “잘했다”, “멋지다”, “대단하다”는 칭찬을 들으니
정말 뿌듯했고 행복했어요.</p>
<p>앞으로 이 프로젝트를 더 성장시켜
조금이라도 세상에 긍정적인 영향을 주는 서비스로 발전시키고 싶습니다.
<img src="https://velog.velcdn.com/images/int_1sy/post/9d0f24a1-8aaf-40a4-b3e6-dc87b2ea1502/image.jpg" alt=""></p>
<hr>
<h2 id="8ai-지니어스📸📸">8.AI 지니어스📸📸</h2>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/1f8a498e-dbb3-4a2f-85b7-3ef8d2f27a47/image.JPG" alt="쓰라 준비"> 전시하기 전에 열정적으로 준비...!<img src="https://velog.velcdn.com/images/int_1sy/post/4f831b9e-c04a-4342-93b9-e2288877bed4/image.jpg" alt="쓰라 마무리">
열심히 발표도 하구<img src="https://velog.velcdn.com/images/int_1sy/post/b34c9ba8-dfd2-4cab-aa19-be8aee2b6ab1/image.jpg" alt="쓰라 구역">전시도 성공적으로 설치하고 많은 분들의 관심도 받아서 감사했습니다!
<img src="https://velog.velcdn.com/images/int_1sy/post/94287c7f-dcee-4277-9885-64f6b4e8d7af/image.JPG" alt="쓰라 준비">발표도 세상 열심히 기차에서까지도 준비햇어요..<img src="https://velog.velcdn.com/images/int_1sy/post/b4a506c8-4e48-4b2d-9efe-67da67b43bd2/image.jpg" alt="">(LG본사화장실삐까뻔쩍해요..)<img src="https://velog.velcdn.com/images/int_1sy/post/7c5c8b1d-8538-40e0-a64b-5bfce5b2df4e/image.jpg" alt="">헬로우월드 후드티 입고 코딩하시는 개발자..!
<img src="https://velog.velcdn.com/images/int_1sy/post/8b173f95-c478-47be-abab-cf955d3143d2/image.jpg" alt=""><img src="https://velog.velcdn.com/images/int_1sy/post/bee748d0-80d2-4333-bb15-79c3da7e923f/image.jpg" alt=""></p>
<p>그럼 오늘도</p>
<h3 id="즐코-😆🤍"><strong>즐코!</strong> 😆🤍</h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[#22 #포트폴리오 준비(2)]]></title>
            <link>https://velog.io/@int_1sy/22-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%EC%A4%80%EB%B9%842</link>
            <guid>https://velog.io/@int_1sy/22-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%EC%A4%80%EB%B9%842</guid>
            <pubDate>Thu, 27 Nov 2025 09:30:46 GMT</pubDate>
            <description><![CDATA[<p>학교 테크폴리오 공모전 참가를 위한 웹사이트 형식 포트폴리오 제작 과정을 기록합니다. 디자인 능력이 미숙한 점을 인지하고, 프론트엔드 개발자 포트폴리오를 레퍼런스 삼아 개발자의 <strong>성장 과정과 기술 활용 능력</strong>을 나타내는 데 중점을 두었습니다.</p>
<h3 id="1-와이어프레임-및-구조-설계">1. 와이어프레임 및 구조 설계</h3>
<p>송진경 개발자의 포트폴리오를 기반으로 하여, 학생으로서의 <strong>기술 숙련도</strong>와 <strong>학습 기록</strong>을 강조하는 방향으로 수정했습니다. 포트폴리오는 방문자가 정보를 단계적으로 파악할 수 있도록 7개의 섹션으로 구성되었습니다.</p>
<table>
<thead>
<tr>
<th align="left">섹션</th>
<th align="left">목적 및 포함 내용</th>
<th align="left">강조 사항</th>
</tr>
</thead>
<tbody><tr>
<td align="left">Header</td>
<td align="left">첫 화면 및 자기소개 요약</td>
<td align="left">My Vibe 제시</td>
</tr>
<tr>
<td align="left">About Me</td>
<td align="left">인적 사항, 흥미 및 핵심 역량</td>
<td align="left">간결한 정보 전달</td>
</tr>
<tr>
<td align="left">Skills</td>
<td align="left">보유 기술 스택 및 숙련도</td>
<td align="left">기술에 대한 이해도 표현</td>
</tr>
<tr>
<td align="left">Projects</td>
<td align="left">주요 프로젝트 경험</td>
<td align="left">문제 해결 과정 위주 서술</td>
</tr>
<tr>
<td align="left"><strong>Velog</strong></td>
<td align="left"><strong>최신 학습 및 기술 기록</strong></td>
<td align="left"><strong>지속적인 학습 태도 입증 (핵심)</strong></td>
</tr>
<tr>
<td align="left">Education</td>
<td align="left">학업 배경, 수상 및 자격증</td>
<td align="left">객관적인 성과 제시</td>
</tr>
<tr>
<td align="left">Contact</td>
<td align="left">연락 정보 및 소셜 미디어 링크</td>
<td align="left">접근성 확보</td>
</tr>
</tbody></table>
<p>이 중 <strong>Velog 섹션</strong>은 개인의 활발한 학습 기록을 포트폴리오에 직접적으로 노출하여, 학습에 대한 진정성을 보여주기 위한 핵심 기능으로 설정되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/int_1sy/post/6bf93d03-e8c0-4775-bd49-db7f411be0d4/image.png" alt=""></p>
<h3 id="2-기술-구현-중점-사항">2. 기술 구현 중점 사항</h3>
<h4 id="a-velog-게시글-연동">A. Velog 게시글 연동</h4>
<ul>
<li><strong>목표:</strong> 작성된 벨로그 글 목록을 포트폴리오 페이지에 직접 가져와 <strong>실시간</strong>으로 보여주는 기능 구현.</li>
<li><strong>배경:</strong> 단순히 링크를 연결하는 것을 넘어, 많은 학습 기록이 담긴 벨로그 내용을 통해 <strong>지속적인 자기 계발 노력</strong>을 강조하기 위함.</li>
<li><strong>구현:</strong> Velog의 API 또는 RSS 피드 파싱 방식을 활용하여, 비동기 통신을 통해 최신 게시글 데이터를 불러와 카드 형태로 렌더링했습니다.</li>
</ul>
<h4 id="b-github-pages를-통한-배포">B. GitHub Pages를 통한 배포</h4>
<ul>
<li><strong>목표:</strong> 포트폴리오 배포 과정을 직접 경험하고, Git/GitHub 활용 능력을 <strong>버전 관리 외 배포까지 확장</strong>하는 것.</li>
<li><strong>배경:</strong> 협업에 필수적인 Git/GitHub 활용을 완벽히 습득하기 위해 배포 환경 구축까지 진행.</li>
<li><strong>구현:</strong> GitHub Pages를 이용해 소스 코드를 웹사이트로 배포하는 절차를 따랐으며, 이 과정은 향후 협업 시 CI/CD 환경에 대한 이해도를 높이는 데 기여했습니다.</li>
</ul>
<h3 id="3-프로젝트를-통한-발견-및-결론">3. 프로젝트를 통한 발견 및 결론</h3>
<p>이번 포트폴리오 제작을 통해 가장 크게 체감한 것은 <strong>기록의 중요성</strong>이었습니다.</p>
<ul>
<li><strong>개인의 경험:</strong> 평소 노션(Notion)에 프로젝트 기획, 사소한 결정, 파일, 수상 기록 등 모든 것을 습관적으로 기록해 두었습니다.</li>
<li><strong>결과:</strong> 포트폴리오 제작 시작 단계에서 자료를 찾는 데 시간을 허비하지 않고, <strong>바로 개발에 착수</strong>할 수 있었습니다. 이는 다른 참여자들이 자료 수집에 시간을 쏟는 것과 대비되는 부분이었으며, 웹사이트 개발이라는 <strong>보다 높은 수준의 목표</strong>를 달성할 수 있는 기반이 되었습니다.</li>
</ul>
<p><strong>결론적으로,</strong> 웹 포트폴리오는 단순한 성과 나열을 넘어, <strong>개발자로서의 학습 태도와 기록 습관</strong>을 입증하는 결과물이 되었습니다. 앞으로도 지속적인 업데이트를 통해 포트폴리오의 내용과 기능을 발전시켜 나갈 계획입니다.</p>
<hr>
<p>완성 웹사이트 링크 : <a href="https://dayeon-t.github.io/website_portfolio/#intro">https://dayeon-t.github.io/website_portfolio/#intro</a></p>
]]></description>
        </item>
    </channel>
</rss>