<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>kmj-1616.log</title>
        <link>https://velog.io/</link>
        <description>꾸준히 배우고 기록하는 개발자</description>
        <lastBuildDate>Sat, 07 Feb 2026 05:40:13 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>kmj-1616.log</title>
            <url>https://velog.velcdn.com/images/kmj-1616/profile/cac0a35c-18a9-4b07-8690-caa01f4415c8/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. kmj-1616.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kmj-1616" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Spring] Java Record와 정적 팩토리 메서드로 깔끔한 마이페이지 조회 API 구현하기]]></title>
            <link>https://velog.io/@kmj-1616/Spring-Java-Record%EC%99%80-%EC%A0%95%EC%A0%81-%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9C%EB%A1%9C-%EA%B9%94%EB%81%94%ED%95%9C-%EB%A7%88%EC%9D%B4%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%A1%B0%ED%9A%8C-API-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kmj-1616/Spring-Java-Record%EC%99%80-%EC%A0%95%EC%A0%81-%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9C%EB%A1%9C-%EA%B9%94%EB%81%94%ED%95%9C-%EB%A7%88%EC%9D%B4%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%A1%B0%ED%9A%8C-API-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 07 Feb 2026 05:40:13 GMT</pubDate>
            <description><![CDATA[<p>저희 <strong>Handshake</strong> 서비스의 마이페이지는 유저의 기본적인 프로필 정보뿐만 아니라, 포지션, 기술 스택, 네트워킹 목적 등 여러 연관 테이블의 데이터를 모아서 보여줘야 합니다.
이번 포스팅에서는 마이페이지 조회 API를 구현하고, 서비스 레이어의 가독성을 높이기 위해 <strong>정적 팩토리 메서드(Static Factory Method)</strong> 로 리팩토링한 과정을 공유하려고 합니다.</p>
<hr>
<h2 id="초기-api-구현">초기 API 구현</h2>
<p>가장 먼저 <code>Controller</code>, <code>Service</code>, <code>Repository</code>, <code>Response DTO</code>를 각각 작성하여 기능을 구현했습니다.</p>
<h3 id="📁-profileresponse-dto">📁 ProfileResponse (DTO)</h3>
<p>사용자 정보와 각 중간 테이블에서 가져올 ID 리스트를 담기 위해 Java의 Record 구조를 사용했습니다. 초기에는 <code>@Builder</code>를 사용하여 객체를 생성하도록 설계했습니다.</p>
<pre><code class="language-java">@Builder
public record ProfileResponse(
        String nickname,
        String profileImageUrl,
        Boolean experience,
        String career,
        String dsti,
        List&lt;Long&gt; positions,
        List&lt;Long&gt; techSkills,
        List&lt;Long&gt; networks,
        String githubId,
        String selfIntro
) { }</code></pre>
<h3 id="📁-userrepository--userprofileservice">📁 UserRepository &amp; UserProfileService</h3>
<p>먼저 <code>UserNetworkRepository</code>, <code>UserPositionRespository</code>, <code>UserTechSkillRepository</code> 에서 사용자 본체의 정보를 가져오는 쿼리 메서드를 추가했습니다.
<code>USerProfileService</code> 에서는 연관된 모든 ID 리스트를 취합하는 로직을 작성했습니다.</p>
<pre><code class="language-java">@Transactional(readOnly = true)
public ProfileResponse getMyProfile(Long userId) {
    User user = userRepository.findById(userId)
            .orElseThrow(() -&gt; new GlobalException(ErrorCode.USER_NOT_FOUND));

    // 각 중간 테이블에서 ID 리스트 추출
    List&lt;Long&gt; positionIds = userPositionRepository.findAllByUserId(userId).stream()
            .map(up -&gt; up.getPosition().getId()).toList();
    List&lt;Long&gt; techSkillIds = userTechSkillRepository.findAllByUserId(userId).stream()
            .map(up -&gt; up.getTechSkill().getId()).toList();
    List&lt;Long&gt; networkIds = userNetworkRepository.findAllByUserId(userId).stream()
            .map(up -&gt; up.getNetwork().getId()).toList();

    // 빌더 패턴을 사용하여 DTO 생성 (리팩토링 전)
    return ProfileResponse.builder()
            .nickname(user.getNickname())
            .profileImageUrl(user.getProfileImageUrl())
            .experience(user.getExperience())
            .career(user.getCareer())
            .dsti(user.getDsti())
            .positions(positionIds)
            .techSkills(techSkillIds)
            .networks(networkIds)
            .githubId(user.getGithubId())
            .selfIntro(user.getSelfIntro())
            .build();
}</code></pre>
<h3 id="📁-usercontroller">📁 UserController</h3>
<p><code>GET /user/info</code> 엔드포인트를 생성해 현재 인증된 유저의 정보를 반환하도록 연결했습니다.</p>
<pre><code class="language-java">@GetMapping(&quot;/info&quot;)
public ResponseBody&lt;ProfileResponse&gt; getInfo(@AuthenticationPrincipal Long userId) {
    ProfileResponse response = userProfileService.getMyProfile(userId);
    return ResponseBody.success(response);
}</code></pre>
<hr>
<h2 id="기존-구조의-문제점">기존 구조의 문제점</h2>
<p>위와 같이 구현했을 때 기능상 문제는 없었지만, 팀원의 코드 리뷰를 통해 몇 가지 아쉬운 점을 발견했습니다.</p>
<ol>
<li><p><strong>서비스 로직의 비대화</strong>: 서비스 레이어에서 DTO의 필드를 하나하나 매핑하다 보니 코드가 길어지고 가독성이 떨어졌습니다.</p>
</li>
<li><p><strong>캡슐화 부족</strong>: 응답 형식을 생성하는 책임이 서비스 레이어에 노출되어 있어, 필드가 추가되거나 변경될 때 서비스 코드를 매번 수정해야 했습니다.</p>
</li>
<li><p><strong>디버깅의 어려움</strong>: 빌더가 길어질수록 어떤 데이터가 어디서 잘못 매핑되었는지 한눈에 파악하기 어려웠습니다.</p>
</li>
</ol>
<p>이를 해결하기 위해 <strong>정적 팩토리 메서드(Static Factory Method)</strong> 구조로 리팩토링을 진행했습니다.</p>
<hr>
<h2 id="리팩토링-정적-팩토리-메서드-도입">리팩토링: 정적 팩토리 메서드 도입</h2>
<h3 id="🛠️-profileresponse-수정">🛠️ ProfileResponse 수정</h3>
<p>DTO 내부에 유저 엔티티와 ID 리스트들을 받아 직접 객체를 생성하는 <code>from</code> 메서드를 추가했습니다.</p>
<pre><code class="language-java">@Builder
public record ProfileResponse(
    // 필드 생략
) {
    public static ProfileResponse from(User user,
                                       List&lt;Long&gt; positionIds,
                                       List&lt;Long&gt; techSkillIds,
                                       List&lt;Long&gt; networkIds) {
        return ProfileResponse.builder()
                .nickname(user.getNickname())
                .profileImageUrl(user.getProfileImageUrl())
                .experience(user.getExperience())
                .career(user.getCareer())
                .dsti(user.getDsti())
                .positions(positionIds)
                .techSkills(techSkillIds)
                .networks(networkIds)
                .githubId(user.getGithubId())
                .selfIntro(user.getSelfIntro())
                .build();
    }
}</code></pre>
<h3 id="🛠️-userprofileservice-수정">🛠️ UserProfileService 수정</h3>
<p>이제 서비스단에서는 복잡한 빌더 로직이 사라지고, 단 한 줄로 조회가 가능해졌습니다.</p>
<pre><code class="language-java">@Transactional(readOnly = true)
public ProfileResponse getMyProfile(Long userId) {
    User user = userRepository.findById(userId)
            .orElseThrow(() -&gt; new GlobalException(ErrorCode.USER_NOT_FOUND));

    List&lt;Long&gt; positionIds = getPositionIds(userId);
    List&lt;Long&gt; techSkillIds = getTechSkillIds(userId);
    List&lt;Long&gt; networkIds = getNetworkIds(userId);

    // 정적 팩토리 메서드로 깔끔하게 반환
    return ProfileResponse.from(user, positionIds, techSkillIds, networkIds);
}</code></pre>
<hr>
<h2 id="정적-팩토리-메서드-도입의-장점">정적 팩토리 메서드 도입의 장점</h2>
<p>이렇게 정적 팩토리 메서드로 리팩토링을 해서 얻은 장점은 여러가지가 있습니다.</p>
<blockquote>
</blockquote>
<ol>
<li><strong>명확한 의도 표현</strong>: <code>from()</code>이라는 이름만 봐도 &quot;User 객체로부터 Response DTO를 만든다&quot;는 의도가 직관적으로 전달됩니다.</li>
<li><strong>변환 책임의 캡슐화</strong>: 엔티티를 DTO로 변환하는 상세 로직이 DTO 내부에 숨겨져 있어 서비스 코드가 간결해집니다.</li>
<li><strong>Builder를 통한 가독성과 유연성</strong>: 메서드 내부에서는 Builder를 사용하므로 필드가 많아도 가독성이 유지되며, 파라미터 순서로 인한 실수를 방지할 수 있습니다.</li>
<li><strong>유지보수의 용이성</strong>: 필드가 추가되거나 변경되어도 DTO 내부의 <code>from</code> 메서드만 수정하면 됩니다. 서비스 레이어까지 영향이 가지 않습니다.</li>
<li><strong>명시적인 설계 활용</strong>: <code>of</code>, <code>from</code> 등 관례적인 이름을 사용하여 다른 개발자가 코드를 볼 때도 의도를 쉽게 파악할 수 있습니다.<blockquote>
</blockquote>
</li>
</ol>
<p>이번 리팩토링을 통해 단순히 기능을 구현하는 것을 넘어, <strong>&quot;어떻게 하면 더 읽기 좋고 관리하기 쉬운 코드를 만들 것인가&quot;</strong>에 대해 깊이 고민해 볼 수 있었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[추천 시스템의 Cold Start를 어떻게 테스트할까? : 유저 페르소나 설계]]></title>
            <link>https://velog.io/@kmj-1616/%EC%B6%94%EC%B2%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%98-Cold-Start%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%A0%EA%B9%8C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8B%9C%EB%AE%AC%EB%A0%88%EC%9D%B4%EC%85%98-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@kmj-1616/%EC%B6%94%EC%B2%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%98-Cold-Start%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%A0%EA%B9%8C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8B%9C%EB%AE%AC%EB%A0%88%EC%9D%B4%EC%85%98-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Sun, 01 Feb 2026 06:39:54 GMT</pubDate>
            <description><![CDATA[<h2 id="테스트-데이터의-설계">테스트 데이터의 설계</h2>
<p>저희 프로젝트의 AI 담당 팀원이 추천 시스템의 로직을 크게 개선했습니다. 메타 정보와 텍스트를 결합한 <strong>하이브리드 임베딩</strong> 구조를 적용하고, <strong>Word2Vec</strong> 모델을 재학습하여 추천 품질을 높이는 작업이었습니다.</p>
<p>하지만 모델이 아무리 좋아도, 실제 서비스에서 마주할 다양한 유저 케이스를 검증하지 못하면 성능이 좋다고 할 수 없습니다. 특히 신규 유저에게 적절한 추천을 제공하는 <strong>Cold Start</strong>는 항상 어려운 문제인 것 같습니다.</p>
<p>저는 저희 서비스에서 가장 중요한 AI 추천 모델의 성능을 최대한으로 테스트하고, 학습 품질을 안정화하기 위해 <strong>240명의 가상 페르소나를 설계하고 데이터 파이프라인을 구축</strong>했습니다.</p>
<hr>
<h2 id="4가지-유저-페르소나">4가지 유저 페르소나</h2>
<p>단순히 랜덤한 값을 생성하는 것이 아니라, 모델의 강건성을 검증하기 위해 데이터의 특성 조합을 4가지 타입으로 세분화했습니다.</p>
<table>
<thead>
<tr>
<th align="left">유저 타입</th>
<th align="left">인원</th>
<th align="left">특징</th>
<th align="left">설계 의도</th>
<th align="left">목적</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>Cold Start</strong></td>
<td align="left">50</td>
<td align="left">데이터가 적은 막 가입한 유저</td>
<td align="left">데이터가 희소한 신규 유저 대응</td>
<td align="left">초기 탐색용 프로필의 추천 수렴 속도 분석</td>
</tr>
<tr>
<td align="left"><strong>Core Cluster</strong></td>
<td align="left">100</td>
<td align="left">직무 1개에 깊은 스택을 보유한 전형적인 개발자</td>
<td align="left">추천 시스템의 기준점인 전형적인 전문가 그룹 인식</td>
<td align="left">하이브리드 임베딩의 벡터 군집화 성능 검증</td>
</tr>
<tr>
<td align="left"><strong>Bridge</strong></td>
<td align="left">60</td>
<td align="left">여러 직무와 스택을 가진 하이브리드형</td>
<td align="left">직무 간 연관성 학습 (ex. FE+기획 등)</td>
<td align="left">다중 레이블 환경에서의 추천 확장성 테스트</td>
</tr>
<tr>
<td align="left"><strong>Outlier</strong></td>
<td align="left">30</td>
<td align="left">직무는 A인데, 스택은 B인 특이 케이스</td>
<td align="left">직무와 스택이 불일치하는 노이즈</td>
<td align="left">모델의 예외 처리 및 강건성 검증</td>
</tr>
</tbody></table>
<hr>
<h2 id="비즈니스-제약-조건을-코드로-구현하기">비즈니스 제약 조건을 코드로 구현하기</h2>
<p>데이터 엔지니어링 단계에서 가장 고민했던 점은 <strong>서비스의 제약 조건을 준수하면서도 유의미한 변동성을 만드는 것</strong>이었습니다.</p>
<h4 id="①-context-aware-스택-매핑">① Context-Aware 스택 매핑</h4>
<p>단순 랜덤 생성을 배제하고, <code>position_stacks</code> 매핑 테이블을 구축했습니다. 이는 AI 모델이 &quot;백엔드 개발자는 Java/Spring 스택을 가질 확률이 높다&quot;는 실제 문맥을 학습할 수 있습니다.</p>
<h4 id="②-예외-처리를-통한-데이터-무결성-확보">② 예외 처리를 통한 데이터 무결성 확보</h4>
<p>데이터 생성 중 발생할 수 있는 논리적 오류를 방지하기 위해 <code>min()</code> 함수를 활용했습니다. 예를 들어 설계 상 매핑된 스택이 3개뿐인 직무(ex. 기획/디자인)에서 5개를 샘플링하려 할 때 발생할 수 있는 <code>ValueError</code>를 방지해 파이프라인의 안정성을 높였습니다.</p>
<pre><code class="language-python"># 기술 스택이 부족하더라도 에러 없이 가능한 범위 내에서 최대치를 추출하도록 설계
skills = random.sample(available_skills, min(len(available_skills), random.randint(3, 5)))</code></pre>
<h4 id="③-bridge-유저-설계">③ Bridge 유저 설계</h4>
<p>저희 서비스의 기획상 직무는 최대 3개까지 선택이 가능합니다. 이를 반영해 2~3개의 직무를 가진 <strong>Bridge 유저</strong>를 설계했습니다. 이 타입은 하이브리드 임베딩이 &#39;프론트엔드 개발자이면서 디자인 툴도 다루는 유저&#39; 같은 경우의 벡터를 얼마나 정교하게 생성하는지를 테스트할 수 있습니다.</p>
<hr>
<h2 id="데이터-생성-파이썬-코드">데이터 생성 파이썬 코드</h2>
<p>아래는 실제 시뮬레이션 환경 구축에 사용한 전체 코드입니다. <code>pandas</code>를 활용해 최종적으로 엑셀 파일로 추출해 DB 적재를 준비했습니다.</p>
<pre><code class="language-python">import pandas as pd
import random

# 1. 기초 데이터 및 상수 설정
dsti_dims = [[&#39;P&#39;, &#39;E&#39;], [&#39;B&#39;, &#39;D&#39;], [&#39;W&#39;, &#39;A&#39;], [&#39;U&#39;, &#39;R&#39;]]
careers = [&#39;employed&#39;, &#39;job_seeking&#39;, &#39;freelancer&#39;, &#39;student&#39;]
networks_pool = [&#39;coffee_chat&#39;, &#39;study_group&#39;, &#39;side_project&#39;]

position_stacks = {
    &quot;frontend_developer&quot;: [&quot;javascript&quot;, &quot;typescript&quot;, &quot;react&quot;, &quot;vue&quot;, &quot;nextjs&quot;, &quot;svelte&quot;],
    &quot;backend_developer&quot;: [&quot;java&quot;, &quot;spring_framework&quot;, &quot;python&quot;, &quot;django&quot;, &quot;fastapi&quot;, &quot;nodejs&quot;, &quot;nestjs&quot;, &quot;go_language&quot;],
    &quot;data_ai_engineer&quot;: [&quot;mysql&quot;, &quot;postgresql&quot;, &quot;mongodb&quot;, &quot;redis_cache&quot;, &quot;oracle_database&quot;],
    &quot;game_developer&quot;: [&quot;pytorch&quot;, &quot;tensorflow&quot;, &quot;scikit_learn&quot;, &quot;pandas&quot;, &quot;spark_engine&quot;, &quot;hadoop_platform&quot;],
    &quot;embedded_systems_engineer&quot;: [&quot;csharp_language&quot;, &quot;cpp_language&quot;, &quot;unity_engine&quot;, &quot;unreal_engine&quot;, &quot;opengl_api&quot;, &quot;c_language&quot;, &quot;raspberry_pi_platform&quot;, &quot;arduino_platform&quot;],
    &quot;mobile_app_developer&quot;: [&quot;swift_language&quot;, &quot;kotlin&quot;, &quot;flutter&quot;, &quot;react_native&quot;],
    &quot;devops_infra_engineer&quot;: [&quot;aws&quot;, &quot;docker&quot;, &quot;kubernetes&quot;, &quot;firebase&quot;, &quot;jenkins&quot;, &quot;github_actions&quot;],
    &quot;product_planning_design&quot;: [&quot;figma_design_tool&quot;, &quot;photoshop_design_tool&quot;, &quot;illustrator_design_tool&quot;]
}

positions_list = list(position_stacks.keys())

def get_dsti():
    return &quot;&quot;.join([random.choice(d) for d in dsti_dims])

def get_common_meta(is_cold=False):
    career = random.choice(careers)
    exp = str(random.choice([True, False])).lower()
    net_count = 1 if is_cold else random.choices([1, 2, 3], weights=[0.5, 0.3, 0.2])[0]
    networks = &quot;, &quot;.join(random.sample(networks_pool, net_count))
    return career, exp, networks

# 2. 유저 페르소나 유형별 데이터 생성 로직

data = []
user_idx = 1

for _ in range(50):
    pos = random.choice(positions_list)

    # 스킬은 해당 직무에서 1~2개만 선택
    available_skills = position_stacks[pos]
    skills = random.sample(available_skills, min(len(available_skills), random.randint(1, 2)))

    career, exp, networks = get_common_meta(is_cold=True)

    data.append({
        &quot;type&quot;: &quot;Cold Start&quot;,
        &quot;nickname&quot;: f&quot;dummy_{user_idx:03d}&quot;,
        &quot;positions&quot;: pos,
        &quot;tech_skills&quot;: &quot;, &quot;.join(skills),
        &quot;career&quot;: career,
        &quot;experience&quot;: exp,
        &quot;networks&quot;: networks,
        &quot;dsti&quot;: get_dsti()
    })
    user_idx += 1

for _ in range(100):
    # 포지션을 순차적으로 돌면서 골고루 생성
    pos = positions_list[user_idx % len(positions_list)]

    # 해당 직무의 스택을 깊게(3~5개) 선택
    available_skills = position_stacks[pos]
    skills = random.sample(available_skills, min(len(available_skills), random.randint(3, 5)))

    career, exp, networks = get_common_meta()

    data.append({
        &quot;type&quot;: &quot;Core Cluster&quot;,
        &quot;nickname&quot;: f&quot;dummy_{user_idx:03d}&quot;,
        &quot;positions&quot;: pos,
        &quot;tech_skills&quot;: &quot;, &quot;.join(skills),
        &quot;career&quot;: career,
        &quot;experience&quot;: exp,
        &quot;networks&quot;: networks,
        &quot;dsti&quot;: get_dsti()
    })
    user_idx += 1

for _ in range(60):
    # 직무 2~3개 랜덤 선택
    pos_count = random.choices([2, 3], weights=[0.7, 0.3])[0]
    selected_positions = random.sample(positions_list, pos_count)

    # 선택된 직무들의 스택 풀을 모두 합침
    combined_skill_pool = []
    for p in selected_positions:
        combined_skill_pool.extend(position_stacks[p])

    # 합친 풀에서 중복 제거 후 4~5개 스택 랜덤 선택
    combined_skill_pool = list(set(combined_skill_pool))
    pick_count = min(len(combined_skill_pool), random.randint(4, 5))
    final_skills = random.sample(combined_skill_pool, pick_count)

    career, exp, networks = get_common_meta()

    data.append({
        &quot;type&quot;: &quot;Bridge&quot;,
        &quot;nickname&quot;: f&quot;dummy_{user_idx:03d}&quot;,
        &quot;positions&quot;: &quot;, &quot;.join(selected_positions),
        &quot;tech_skills&quot;: &quot;, &quot;.join(final_skills),
        &quot;career&quot;: career,
        &quot;experience&quot;: exp,
        &quot;networks&quot;: networks,
        &quot;dsti&quot;: get_dsti()
    })
    user_idx += 1

for _ in range(30):
    # 메인 직무 1개 선택
    main_pos = random.choice(positions_list)

    # 다른 직무들의 스택 풀 생성
    other_positions = [p for p in positions_list if p != main_pos]
    other_pos = random.choice(other_positions)

    # 내 직무 스택 1개 + 다른 직무 스택 2~3개
    my_skill = random.sample(position_stacks[main_pos], 1)
    other_pool = position_stacks[other_pos]
    weird_skills = random.sample(other_pool, min(len(other_pool), random.randint(2, 3)))

    final_skills = my_skill + weird_skills
    random.shuffle(final_skills)

    career, exp, networks = get_common_meta()

    data.append({
        &quot;type&quot;: &quot;Outlier&quot;,
        &quot;nickname&quot;: f&quot;dummy_{user_idx:03d}&quot;,
        &quot;positions&quot;: main_pos,
        &quot;tech_skills&quot;: &quot;, &quot;.join(final_skills),
        &quot;career&quot;: career,
        &quot;experience&quot;: exp,
        &quot;networks&quot;: networks,
        &quot;dsti&quot;: get_dsti()
    })
    user_idx += 1

# 3. 파일 저장
df = pd.DataFrame(data)
df.to_excel(&quot;dummy_users.xlsx&quot;, index=False)</code></pre>
<hr>
<h2 id="결과-및-회고">결과 및 회고</h2>
<p>팀원의 모델 개선 작업(Word2Vec 재학습 및 액션 로그 반영) 과정에서 제가 생성한 더미 데이터는 다음과 같은 역할을 했습니다.</p>
<ul>
<li><strong>가중치 최적화</strong>: 메타 정보와 텍스트 임베딩의 결합 가중치를 조정할 때, 의도적으로 설계된 Bridge 유저들의 추천 결과를 보며 로직을 미세 조정(Fine-tuning)할 수 있었습니다.</li>
</ul>
<ul>
<li><strong>데이터 표준화</strong>: 엑셀 데이터를 DB에 적재하는 과정에서 <code>meta_key</code> 소문자 통일, <code>experience</code> 값의 boolean 타입 처리 등 데이터 엔지니어링 측면의 정제를 수행하여 추천 API 인풋 데이터의 안정성을 확보했습니다.</li>
</ul>
<p>이번 작업을 통해 <strong>모델링만큼 중요한 것이 데이터의 분포 설계</strong>라는 것을 배웠습니다. 데이터 분석가는 단순히 주어진 데이터를 해석하기보다, 모델이 제대로 성능을 발휘하기 위해 데이터를 잘 설계할 수 있어야 함을 느낄 수 있었던 경험이었습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] Funnel 패턴으로 복잡한 온보딩 흐름 정복하기 (feat. Toss)]]></title>
            <link>https://velog.io/@kmj-1616/Next.js-Funnel-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EB%B3%B5%EC%9E%A1%ED%95%9C-%EC%98%A8%EB%B3%B4%EB%94%A9-%ED%9D%90%EB%A6%84-%EC%A0%95%EB%B3%B5%ED%95%98%EA%B8%B0-feat.-Toss</link>
            <guid>https://velog.io/@kmj-1616/Next.js-Funnel-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EB%B3%B5%EC%9E%A1%ED%95%9C-%EC%98%A8%EB%B3%B4%EB%94%A9-%ED%9D%90%EB%A6%84-%EC%A0%95%EB%B3%B5%ED%95%98%EA%B8%B0-feat.-Toss</guid>
            <pubDate>Sun, 25 Jan 2026 06:03:07 GMT</pubDate>
            <description><![CDATA[<h2 id="왜-nextjs와-funnel-패턴인가">왜 Next.js와 Funnel 패턴인가?</h2>
<p>지난 포스팅에서 CSR과 SSR의 차이를 공부하며 Next.js에 대해 알아봤습니다. 저희 팀이 개발 중인 <strong>HandShake</strong> 서비스는 검색 엔진 최적화(SEO)와 빠른 초기 로딩 속도가 중요했기에 Next.js를 선택했고, 저는 그 중 서비스의 첫인상인 <strong>&#39;온보딩(회원가입)&#39;</strong> 구현을 맡게 되었습니다.</p>
<p>우리 서비스의 온보딩은 단순한 가입이 아니라, 직무 및 기술 스택 선택과 <strong>개발 성향 테스트(a.k.a. DSTI)</strong> 까지 총 3단계로 이어집니다. 이를 매끄럽게 구현하기 위해 <a href="https://www.youtube.com/watch?v=NwLWX2RNVcw">Toss 개발자 컨퍼런스 SLASH 23</a>을 보며 공부한 <strong>Funnel 패턴</strong>을 도입하여 상태 관리의 복잡도를 해결해 보고자 합니다.</p>
<hr>
<h2 id="흩어진-페이지의-한계">흩어진 페이지의 한계</h2>
<p>Handshake에 소셜 로그인을 통해 처음 진입을 하면, 총 3단계의 프로필 정보 입력 과정을 거칩니다. 그래서 처음에는 단순히 /signup/1, /2, /3 이렇게 페이지를 나누면 되는 거 아닌가 생각했습니다.
하지만 이렇게 하면 몇 가지 불편한 점이 있습니다.</p>
<ul>
<li><strong>데이터 파편화</strong>: 1단계 기본 정보, 2단계 추가 정보, 3단계 개발 성향 유형 분석 결과를 마지막에 한꺼번에 API로 쏴야 하는데, 페이지가 갈리면 이 데이터를 들고 다니기가 매우 번거로워집니다. </li>
<li><strong>뒤로가기 제어</strong>: 사용자가 뒤로가기를 눌렀을 때의 데이터 보존 로직이 복잡해집니다.</li>
</ul>
<hr>
<h2 id="우리-서비스의-온보딩-및-api-흐름">우리 서비스의 온보딩 및 API 흐름</h2>
<p>기획된 디자인 요구사항을 바탕으로 설계한 단계별 로직입니다.</p>
<h3 id="설문조사-단계-및-요구사항">설문조사 단계 및 요구사항</h3>
<ul>
<li><strong>1단계: 기본 정보</strong><ul>
<li>닉네임 중복 확인 (<code>POST /user/nickname</code>): 통과 필수</li>
<li>모달을 통한 직무 및 기술 스택 선택 (최대 5개)</li>
</ul>
</li>
<li>*<em>2단계: 네트워킹 정보 *</em><ul>
<li>네트워킹 목적, Github ID, 자기소개(선택)</li>
</ul>
</li>
<li><strong>3단계: 개발 성향 유형(DSTI) 분석 테스트(메인)</strong><ul>
<li>12개의 질문을 한 페이지당 하나씩 배치</li>
<li>답변 선택 시 자동으로 데이터 저장 후 다음 질문으로 이동</li>
</ul>
</li>
<li><strong>4단계: 분석 결과 및 완료</strong><ul>
<li>결과 카드(캐릭터 이미지 및 각 코드 설명) 확인 후 홈으로 이동 </li>
</ul>
</li>
</ul>
<h3 id="api-호출-시점">API 호출 시점</h3>
<p>모든 데이터를 들고 있다가 <strong>3단계가 끝나는 시점</strong>에 연속 호출합니다.</p>
<ol>
<li><code>POST /user/dsti</code>: 테스트 결과를 먼저 등록해서 DSTI 유형 생성</li>
<li><code>POST /user/info</code>: 수집된 모든 프로필 정보와 DSTI 결과(유형 코드+캐릭터 이미지)를 합쳐 최종 회원가입 완료</li>
</ol>
<hr>
<h2 id="프로젝트-폴더-구조">프로젝트 폴더 구조</h2>
<p>프로젝트 규모가 커질 것을 대비해 <strong>Feature 중심 설계</strong>를 도입하여 <strong>도메인 단위의 응집도</strong>를 높였습니다. 동시에 백엔드와의 통신(Service) 및 데이터 규격(DTO)은 전역적으로 관리하여 데이터 모델의 <strong>일관성</strong>을 유지할 수 있도록 구성했습니다.</p>
<pre><code>src/
├── app/
│   └── (auth)/               # 라우트 그룹 (URL에 포함되지 않음)
│       └── login/page.tsx    # 소셜 로그인 페이지
│       └── signup/
│           └── page.tsx      # 온보딩 컨트롤 타워 (useFunnel 활용)
├── components/
│   └── ui/                   # 프로젝트 공통 UI (Button, Input, Progress 등)
├── features/
│   └── auth/                 # 🔵 인증/회원가입 관련 도메인 응집 (UI/로직)
│       ├── components/       # 각 가입 단계별 UI 
│       │   └── Step1Profile.tsx/ 
│       │   └── Step2Networking.tsx/ 
│       │   └── Step3DSTI.tsx/ 
│       └── hooks/            # Funnel 제어 등 도메인 특화 로직
│           └── useFunnel.tsx/ 
├── services/                 # 🟠 백엔드 API 통신 
│   ├── api.ts                
│   └── auth/               
│   │   └── api.ts/           # 인증 관련 API 엔드포인트 
│   │   └── hooks.ts/         # React-Query와 연결된 API 호출 훅
│   └── user/               
│       └── api.ts/           # 유저 정보 관련 API 엔드포인트 함수 정의
│       └── hooks.ts/         # React-Query와 연결된 API 호출 훅
├── types/                    # ⭐ API 응답 및 데이터 공통 타입 (DTO)
│   ├── auth.ts               # 인증 관련 타입 
│   └── user.ts               # 유저 프로필, 경력, 기술스택 등 핵심 데이터 타입
├── assets/icons              # 아이콘, 이미지 등 정적 파일
└── utils/                    # 토큰 관련 유틸</code></pre><h4 id="💡-포인트">💡 포인트</h4>
<ul>
<li><strong>Feature 기반 UI 응집</strong>: features/auth 내부에는 오직 해당 도메인의 <strong>화면 구성(Components)</strong>과 <strong>상태 흐름(Hooks)</strong>만 두어 UI 복잡도를 낮췄습니다.</li>
<li><strong>서비스 계층 분리</strong>: 여러 페이지나 다른 기능에서도 인증 관련 API를 재사용하기 쉽게 설계했습니다.</li>
<li><strong>DTO 중앙 관리</strong>: 백엔드 명세가 변경될 경우 src/types 폴더 내의 파일만 수정하면 프로젝트 전체에 반영되도록 관리 포인트를 일원화했습니다.</li>
</ul>
<hr>
<h2 id="핵심-구현-usefunnel-커스텀-훅">핵심 구현: <code>useFunnel</code> 커스텀 훅</h2>
<p>Toss의 <code>useFunnel</code> 라이브러리 콘셉트를 참고해서, <strong>현재 단계(step)를 관리</strong>하고 해당 단계일 때만 화면을 보여주는 컴포넌트를 반환하도록 설계했습니다.</p>
<pre><code class="language-typescript">import { useState } from &#39;react&#39;;

export type SignupStep = &#39;step1&#39; | &#39;step2&#39; | &#39;step3&#39; | &#39;step4&#39;;

export function useFunnel(defaultStep: SignupStep) {
  const [step, setStep] = useState&lt;SignupStep&gt;(defaultStep);

  const Step = ({ name, children }: { name: SignupStep; children: React.ReactNode }) =&gt; {
    return step === name ? &lt;&gt;{children}&lt;/&gt; : null;
  };

  return { step, setStep, Step };
}</code></pre>
<h4 id="💡-포인트-1">💡 포인트</h4>
<p>Toss의 <code>useFunnel</code>은 내부적으로 <code>React.Children</code>을 순회하며 현재 단계에 맞는 자식을 찾아내는 고도화된 인터페이스를 제공합니다. 하지만 라이브러리의 복잡한 내부 로직을 그대로 가져오기보다, <code>Step</code> 컴포넌트를 직접 정의하여 반환함으로써 <strong>구현은 단순화하되, Toss가 지향하는 &#39;선언적인 코드&#39;와 &#39;응집도&#39;</strong> 는 그대로 가져올 수 있도록 커스터마이징했습니다.
단순히 <code>step === &#39;step1&#39; &amp;&amp; &lt;Component /&gt;</code>라고 쓸 수도 있지만, 이렇게 컴포넌트로 추상화하면 부모 페이지의 JSX가 마치 <strong>&#39;하나의 큰 폼을 단계별로 명세한 것&#39;</strong> 처럼 읽혀 가독성이 좋아집니다.</p>
<hr>
<h2 id="메인-컨트롤-타워-signuppagetsx">메인 컨트롤 타워: <code>signup/page.tsx</code></h2>
<p>여기가 <strong>응집도</strong>의 핵심으로, 모든 단계의 데이터가 이 페이지의 <code>formData</code> 하나에 모입니다.</p>
<pre><code class="language-typescript">export default function SignupPage() {
  const { Step, setStep, step } = useFunnel(&#39;step1&#39;);
  const [formData, setFormData] = useState&lt;UserProfile&gt;(INITIAL_DATA);

  // 데이터 업데이트 핸들러
  const updateFormData = (newData: Partial&lt;UserProfile&gt;) =&gt; {
    setFormData((prev) =&gt; ({ ...prev, ...newData }));
  };

  return (
    &lt;main&gt;
      &lt;Progress value={calculateProgress(step)} /&gt;

      &lt;Step name=&quot;step1&quot;&gt;
        &lt;Step1Profile 
          data={formData} 
          onNext={(data) =&gt; { updateFormData(data); setStep(&#39;step2&#39;); }} 
        /&gt;
      &lt;/Step&gt;

      &lt;Step name=&quot;step2&quot;&gt;
        &lt;Step2Networking 
          data={formData} 
          onNext={(data) =&gt; { updateFormData(data); setStep(&#39;step3&#39;); }} 
          onPrev={() =&gt; setStep(&#39;step1&#39;)} 
        /&gt;
      &lt;/Step&gt;

      {/* ... 반복 ... */}
    &lt;/main&gt;
  );
}</code></pre>
<hr>
<h2 id="학습-포인트-및-회고">학습 포인트 및 회고</h2>
<blockquote>
</blockquote>
<ul>
<li><strong>데이터 흐름의 단방향성 확보</strong>: 각 단계별 컴포넌트가 비즈니스 로직을 직접 수행하지 않고, 입력받은 데이터를 부모에게 넘겨주는 구조를 택했습니다. 덕분에 데이터 흐름이 단방향으로 명확해졌고, 복잡한 온보딩 과정에서도 상태 추적이 용이해졌습니다.</li>
<li><strong>선언적인 UI와 추상화의 힘</strong>: <code>{step === &#39;step1&#39; &amp;&amp; &lt;Step1 /&gt;}</code> 같은 조건부 렌더링 대신, <code>&lt;Step name=&quot;step1&quot;&gt;</code> 컴포넌트로 감싸주었습니다. 이를 통해 JSX 가독성이 상승했고, 코드가 마치 <strong>&#39;단계별 명세서&#39;</strong>처럼 읽히는 추상화의 이점을 확인할 수 있습니다.</li>
<li><strong>사용자 경험(UX)</strong>: Next.js의 App Router 환경에서 실제 페이지를 이동하지 않고 컴포넌트만 교체되므로, 웹이지만 앱처럼 끊김 없는 전환 효과를 줄 수 있었습니다.</li>
<li><strong>가입 절차의 원자성</strong>: 데이터 누락이나 중간 이탈로 인한 불완전한 회원 정보 생성을 방지하기 위해, 중간 저장 방식 대신 마지막 단계에서 모든 데이터를 한 번에 처리하는 전략을 세웠습니다. 이는 DB의 정합성을 지키는 데에도 유리합니다. </li>
</ul>
<p>단순히 기능을 구현하는 것을 넘어, <strong>&quot;어떻게 하면 더 읽기 쉬운 코드를 짤 것인가?&quot;</strong>와 <strong>&quot;어떻게 하면 중복을 줄이고 재사용할 수 있을까?&quot;</strong>를 고민해볼 수 있는 시간이었습니다.
Toss의 좋은 레퍼런스를 우리 프로젝트의 규모에 맞게 적절히 변형하여 적용해본 의미 있는 경험이었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python 개발자의 React & Next.js 탐구: CSR vs SSR]]></title>
            <link>https://velog.io/@kmj-1616/Python-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-React-Next.js-%ED%83%90%EA%B5%AC%EA%B8%B0-CSR-vs-SSR</link>
            <guid>https://velog.io/@kmj-1616/Python-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-React-Next.js-%ED%83%90%EA%B5%AC%EA%B8%B0-CSR-vs-SSR</guid>
            <pubDate>Tue, 13 Jan 2026 16:03:30 GMT</pubDate>
            <description><![CDATA[<p>싸피에서의 지난 6개월간 웹 개발을 할 때 Python, Vue.js, Django를 사용했다. 그런데 이번 프로젝트에서는 Java와 React 기반으로 진행하게 되었다. 프론트엔드 팀원들이 React와 Next.js 중 어떤 걸로 할지 의견을 나누는데, 그 사이에서 난 &quot;두 가지가 차이가 뭐길래?&quot;라는 궁금증이 생겼다.</p>
<p>이번 프로젝트에서는 백엔드를 주로 담당하지만, 전체적인 기술 구조도 이해하면서 프론트엔드도 참여해 보고 싶었기에 이 부분은 풀고 가야 할 문제다. 이 문제의 해결을 위해 CSR과 SSR을 공부해오라는 숙제(?)를 받았고, 공부하면서 이해한 내용을 정리해보려고 한다.</p>
<hr>
<h2 id="1-근본적인-차이-라이브러리-vs-프레임워크">1. 근본적인 차이: 라이브러리 vs 프레임워크</h2>
<p>React와 Next.js를 비교할 때 가장 먼저 알아야 할 것은 두 도구의 &#39;성격&#39;이다. </p>
<h3 id="react-library">React (Library)</h3>
<p>React는 <strong>&#39;UI를 만들기 위한 도구&#39;</strong> 이다.
레고 블록 같은 컴포넌트 기반 개발이기 때문에, 재사용성과 복잡한 코드의 단순화와 같은 장점이 있다.
하지만 라우팅(React Router), 상태 관리(Redux/Recoil), 빌드 설정(Webpack/Vite) 등을 개발자가 직접 선택하고 조합해야 한다는 한계점이 있다. 자유도가 높지만 설정 책임도 개발자에게 있다는 것이다. </p>
<h3 id="nextjs-framework">Next.js (Framework)</h3>
<p>이에 비해 Next.js는 React를 기반으로 한 <strong>&#39;풀스택 프레임워크&#39;</strong> 이다.
React의 한계를 보완하면서, 정해진 폴더 구조를 따르면 라우팅이 자동으로 되고, SSR(서버사이드 렌더링)/SSG(정적 사이트 생성)/ISR(증분 정적 재생성) 등 다양한 렌더링 전략을 기본 제공한다. 
그래서 특히 SEO(검색 엔진 최적화)와 초기 로딩 속도 최적화에 강점을 가지고 있다. </p>
<hr>
<h2 id="2-렌더링-방식의-차이-csr-vs-ssr">2. 렌더링 방식의 차이: CSR vs SSR</h2>
<p>그렇다면 두 도구를 선택하는 가장 큰 기준은 무엇일까? 바로 <strong>&#39;화면을 어디서 그리는가?&#39;</strong> 에 있다. </p>
<h3 id="클라이언트-사이드-렌더링-csr">클라이언트 사이드 렌더링 (CSR)</h3>
<ul>
<li><strong>주체</strong>: 브라우저(Client)</li>
<li><strong>과정</strong>: 서버는 빈 <code>index.html</code>과 JS 파일만 보낸다. 브라우저가 JS를 실행해 DOM을 그린다.</li>
<li><strong>장점</strong>: 첫 로딩 후 페이지 이동이 매우 빠르고 서버 부담이 적다.</li>
<li><strong>단점</strong>: JS 파일이 커지면 첫 화면을 보기까지 오래 걸리고, 검색 엔진이 빈 페이지로 오해할 수 있다. </li>
</ul>
<h3 id="서버-사이드-렌더링-ssr">서버 사이드 렌더링 (SSR)</h3>
<ul>
<li><strong>주체</strong>: 서버(Node.js Server)</li>
<li><strong>과정</strong>: 사용자가 요청하면 <strong>서버에서 데이터를 채운 HTML</strong>을 미리 만들어 보낸다. 서버에서 완성된 HTML을 먼저 보여주고, 그 위에 자바스크립트를 입혀 대화형 기능을 활성화하는 Hydration 과정을 거치게 된다. </li>
<li><strong>장점</strong>: 초기 로딩 속도가 빨라 사용자 경험이 좋고, 검색 엔진이 읽기 최적화되어 있다(SEO). (그래서 요즘 대부분의 서비스가 Next.js를 사용한다고 한다.)</li>
<li><strong>단점</strong>: 페이지 이동 시마다 서버 요청이 발생할 수 있고, 서버 리소스 비용이 발생한다.</li>
</ul>
<hr>
<h2 id="3-그럼-언제-무엇을-써야-할까">3. 그럼 언제 무엇을 써야 할까?</h2>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="left">React</th>
<th align="left">Next.js</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>렌더링 방식</strong></td>
<td align="left">Client Side</td>
<td align="left">Server Side / Static / Client (혼합)</td>
</tr>
<tr>
<td align="left"><strong>SEO</strong></td>
<td align="left">불리함 (별도 작업 필요)</td>
<td align="left"><strong>매우 유리함</strong></td>
</tr>
<tr>
<td align="left"><strong>라우팅</strong></td>
<td align="left">React Router 설치 필요</td>
<td align="left">파일 시스템 기반 (자동)</td>
</tr>
<tr>
<td align="left"><strong>초기 로딩</strong></td>
<td align="left">상대적으로 느림 (JS 로드 대기)</td>
<td align="left"><strong>매우 빠름</strong> (HTML 즉시 렌더링)</td>
</tr>
<tr>
<td align="left"><strong>주요 활용</strong></td>
<td align="left">대시보드, 개인화 앱, 관리자 페이지</td>
<td align="left">이커머스, 블로그, 마케팅 페이지</td>
</tr>
</tbody></table>
<hr>
<h2 id="백엔드-관점에서의-결정적-차이는">백엔드 관점에서의 결정적 차이는?</h2>
<p>내가 느낀 두 방식의 결정적 차이는 <strong>인프라 아키텍처의 변화</strong>이다.</p>
<h3 id="reactcsr-환경">React(CSR) 환경</h3>
<ul>
<li>백엔드는 단순 <strong>Rest API 서버</strong> 역할만 수행한다. </li>
<li>CORS 설정만 잘 해주면 프론트엔드와 깔끔하게 분리된다.<h3 id="nextjsssr-환경">Next.js(SSR) 환경</h3>
</li>
<li>프론트엔드 영역에도 <strong>Node.js 서버</strong>가 존재하게 된다.</li>
<li><strong>인증(Auth):</strong> 쿠키/세션 처리 시 &#39;브라우저-Next서버-Java서버&#39; 간의 토큰 전달 과정을 고민해야 한다.</li>
<li><strong>통신:</strong> 브라우저에서 Java로 직접 호출할지, Next 서버를 프록시(Proxy)로 거칠지 설계가 필요하다.</li>
</ul>
<hr>
<h2 id="마치며">마치며</h2>
<p>결국 &quot;React냐 Next.js냐&quot;의 선택은 우리가 만드는 서비스가 <strong>&#39;사용자 인터랙션이 중심인 서비스인가?(React)&#39; 또는 &#39;초기 로딩 속도가 중요한가?(Next.js)</strong> 에 달려 있다. </p>
<p>백엔드 개발자로서도 프론트엔드가 데이터를 가져가는 방식을 이해하니 전체적인 시스템 아키텍처를 설계하는 데 큰 도움이 된 공부였다. 이제 이 개념을 바탕으로 본격적인 프로젝트 세팅에 들어가 보자! </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준 파이썬] 8958.OX퀴즈]]></title>
            <link>https://velog.io/@kmj-1616/%EB%B0%B1%EC%A4%80-%ED%8C%8C%EC%9D%B4%EC%8D%AC-8958.OX%ED%80%B4%EC%A6%88</link>
            <guid>https://velog.io/@kmj-1616/%EB%B0%B1%EC%A4%80-%ED%8C%8C%EC%9D%B4%EC%8D%AC-8958.OX%ED%80%B4%EC%A6%88</guid>
            <pubDate>Fri, 15 Aug 2025 10:11:25 GMT</pubDate>
            <description><![CDATA[<h1 id="bronze-ii-ox퀴즈---8958">[Bronze II] OX퀴즈 - 8958</h1>
<p><a href="https://www.acmicpc.net/problem/8958">문제 링크</a></p>
<h3 id="문제-설명">문제 설명</h3>
<p><img src="https://velog.velcdn.com/images/kmj-1616/post/a4fbfe52-a4a8-433c-8e2f-cf636e6395bd/image.png" alt=""></p>
<h3 id="제출-코드">제출 코드</h3>
<pre><code class="language-python">T = int(input())

for _ in range(1, T+1):
    string = input()
    stack = []
    score = 0   # 점수 기록

    for ch in string:    # OX 결과를 확인하는데
        if ch == &#39;O&#39;:    # O면 push하고
            stack.append(ch)
            score += len(stack) # 스택 길이(O의 개수)만큼 점수 추가
        else:       # X면 스택 비우고 다음 확인  
            stack.clear()
    print(score)</code></pre>
<ul>
<li>O이면 리스트에 추가하고 그 길이만큼 점수를 추가하고, X가 나오면 비우는 로직으로 작성했다.</li>
<li>최근 stack을 배워서 연습해보고자 top 인덱스를 사용해서 풀려고 했는데, 풀다 보니 누적 점수를 계산해야 해서 append 외엔 다른 게 딱히 필요하지 않았다. (일단은 자료구조 연습용으로 하던 거 제출함)</li>
</ul>
<h3 id="다른-코드">다른 코드</h3>
<p>내 코드는 메모리 32412KB, 시간 40ms였는데 다른 파이썬 코드들은 31120KB, 시간 32ms여서 확인해 봤다.</p>
<pre><code class="language-python">T = int(input())

for _ in range(T):
    string = input()
    score = 0
    combo = 0            # 누적된 O의 개수를 저장하는 변수 
    for ch in string:
        if ch == &#39;O&#39;:
            combo += 1    # O이면 콤보 추가 
            score += combo
        else:
            combo = 0    # X가 나오면 다시 0으로 초기화 
    print(score)</code></pre>
<p>이렇게 누적 개수를 세는 변수를 사용하면, 리스트 연산을 하지 않아도 돼서 메모리를 덜 사용하고 시간이 빠르다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준 파이썬] 2884.알람 시계]]></title>
            <link>https://velog.io/@kmj-1616/%EB%B0%B1%EC%A4%80-2884.%EC%95%8C%EB%9E%8C-%EC%8B%9C%EA%B3%84</link>
            <guid>https://velog.io/@kmj-1616/%EB%B0%B1%EC%A4%80-2884.%EC%95%8C%EB%9E%8C-%EC%8B%9C%EA%B3%84</guid>
            <pubDate>Sat, 09 Aug 2025 14:16:29 GMT</pubDate>
            <description><![CDATA[<h1 id="bronze-iii-알람-시계---2884">[Bronze III] 알람 시계 - 2884</h1>
<p><a href="https://www.acmicpc.net/problem/2884">문제 링크</a> </p>
<h3 id="문제-설명">문제 설명</h3>
<p><img src="https://velog.velcdn.com/images/kmj-1616/post/81807246-838d-4ca5-982e-aed567735720/image.png" alt="문제"></p>
<h3 id="제출-코드">제출 코드</h3>
<pre><code class="language-python">H, M = map(int, input().split())

if H != 0 and M &lt; 45:
    print(H-1, M+15) # M-45+60
elif H != 0 and M &gt;= 45:
    print(H, M-45)
elif H == 0 and M &lt; 45:
    print(23, M+15)
elif H == 0 and M &gt;= 45:
    print(0, M-45)
</code></pre>
<ul>
<li>H와 M은 정수형이고, 공백 기준으로 나눠 입력받는다.</li>
<li>H(시간)이 0인 경우와 아닌 경우, M(분)이 45 이상인 경우와 미만인 경우로 나눠 조건문 4개로 작성했다.</li>
</ul>
<h3 id="다른-코드-1">다른 코드 1</h3>
<pre><code class="language-python">H, M = map(int, input().split())

if M &lt; 45:
    M += 15
    if H == 0:
        print(23, M)
    else:
        print(H-1, M)
else:
    print(H, M-45)</code></pre>
<p>처음 제출한 코드가 겹치는 부분이 많은 것 같아서, 조건문을 깔끔하게 바꾼 버전도 제출해봤는데 잘 동작했다. </p>
<h3 id="다른-코드-2">다른 코드 2</h3>
<pre><code class="language-python">H, M = map(int, input().split())

M -= 45  
if M &lt; 0:  
    M += 60
    H -= 1
    if H &lt; 0:
        H = 23

print(H, M)</code></pre>
<ul>
<li>조금 더 깔끔하게는, 일단 45분을 빼준다.<ul>
<li>15를 더해줬던 앞의 코드보다 직관적이다!</li>
</ul>
</li>
<li>이때, 분이 음수가 되면 60을 더하고 1시간을 빼준다. 분이 양수면 H는 입력값 그대로다.</li>
<li>만약 H가 음수가 되면(H가 0이었을 경우 해당), H는 23이 되게 한다. </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준 파이썬] 1152.단어의 개수]]></title>
            <link>https://velog.io/@kmj-1616/%EB%B0%B1%EC%A4%80-1152.%EB%8B%A8%EC%96%B4%EC%9D%98-%EA%B0%9C%EC%88%98</link>
            <guid>https://velog.io/@kmj-1616/%EB%B0%B1%EC%A4%80-1152.%EB%8B%A8%EC%96%B4%EC%9D%98-%EA%B0%9C%EC%88%98</guid>
            <pubDate>Fri, 01 Aug 2025 08:53:21 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><img src="https://velog.velcdn.com/images/kmj-1616/post/0d584da9-ff56-4cc5-a6f6-afde919a6ab8/image.png" alt=""></p>
<h2 id="제출-코드">제출 코드</h2>
<pre><code class="language-python">string = input().split()
print(len(string))</code></pre>
<ul>
<li>split(): 공백 기준으로 나누기 </li>
</ul>
<h2 id="추가-학습">추가 학습</h2>
<p>코드 제출 후 다른 사람들이 작성한 코드를 보던 중에 처음 보는 라이브러리 sys를 사용한 코드를 발견했다.</p>
<pre><code class="language-python">import sys
T = sys.stdin.readline().strip()
print(len(T))</code></pre>
<ul>
<li><p>sys.stdin.readline()
: T에 문자열을 입력받는다. 반복문으로 여러 줄을 연속적으로 입력받아야 하는(정렬, 이진 탐색, 최단 경로 문제 등)의 경우 input()보다 속도가 빠르기 때문에 시간 초과로 오답 판정을 받는 일이 발생하지 않는다고 한다. </p>
</li>
<li><p>strip()
: 문자열의 앞뒤 공백을 제거한다. </p>
</li>
<li><p>정수형으로 입력받고 싶다면?</p>
<pre><code class="language-python">import sys
a=int(sys.stdin.readline())</code></pre>
</li>
<li><p>참고한 블로그: <a href="https://mgyo.tistory.com/166">https://mgyo.tistory.com/166</a></p>
</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>