<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>외향형 개발자 성장기</title>
    <link>https://red-horse.tistory.com/</link>
    <description>재미를 찾아 떠다니는 C# 개발자</description>
    <language>ko</language>
    <pubDate>Thu, 28 May 2026 01:56:09 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Red_Horse</managingEditor>
    <image>
      <title>외향형 개발자 성장기</title>
      <url>https://tistory1.daumcdn.net/tistory/5097079/attach/76d2bef6649142a3b20f092e58767dd0</url>
      <link>https://red-horse.tistory.com</link>
    </image>
    <item>
      <title>WebSocket 채팅 시스템</title>
      <link>https://red-horse.tistory.com/168</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;기반 구성 사항 : &lt;a title=&quot;WebSocket Base&quot; href=&quot;https://red-horse.tistory.com/167&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://red-horse.tistory.com/167&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774173294979&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;WebSocket(Chat System)&quot; data-og-description=&quot;WebSocket?HTTP는 요청-응답 구조입니다.클라이언트가 요청하면 서버가 응답하고 연결을 닫습니다.서버가 먼저 데이터를 보낼 방법이 없는데 WebSocket은 이 문제를 핸드세이크로 해결합니다.클라이&quot; data-og-host=&quot;red-horse.tistory.com&quot; data-og-source-url=&quot;https://red-horse.tistory.com/167&quot; data-og-url=&quot;https://red-horse.tistory.com/167&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b3icoF/dJMb9iIGqm3/8FpMDWZDpWnORVpQ2RKJRk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/UYhds/dJMb9jOmhfF/KBTdXIkFQ2pxzDpkF0KF9k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/boJxwa/dJMb9fZuDYX/vZXSKvm7XIIVZKHrw54PE1/img.png?width=656&amp;amp;height=986&amp;amp;face=0_0_656_986&quot;&gt;&lt;a href=&quot;https://red-horse.tistory.com/167&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://red-horse.tistory.com/167&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b3icoF/dJMb9iIGqm3/8FpMDWZDpWnORVpQ2RKJRk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/UYhds/dJMb9jOmhfF/KBTdXIkFQ2pxzDpkF0KF9k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/boJxwa/dJMb9fZuDYX/vZXSKvm7XIIVZKHrw54PE1/img.png?width=656&amp;amp;height=986&amp;amp;face=0_0_656_986');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;WebSocket(Chat System)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;WebSocket?HTTP는 요청-응답 구조입니다.클라이언트가 요청하면 서버가 응답하고 연결을 닫습니다.서버가 먼저 데이터를 보낼 방법이 없는데 WebSocket은 이 문제를 핸드세이크로 해결합니다.클라이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;red-horse.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774173300760&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;채팅 시스템 구조
WebSocket/Chat/
├── ChatMessage.cs           &amp;larr; 메시지 프로토콜 (JSON)
├── ChatRoomManager.cs       &amp;larr; 룸 멤버십 관리
└── ChatWebSocketHandler.cs  &amp;larr; 채팅 이벤트 처리

Components/Pages/Chat/
├── ChatPage.razor           &amp;larr; 채팅 UI
├── ChatPageBase.cs          &amp;larr; JS interop + Blazor 상태 관리
└── ChatPage.razor.css       &amp;larr; 스타일

wwwroot/js/
└── chat.js                  &amp;larr; 브라우저 WebSocket 클라이언트&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;메시지 프로토콜 설계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버와 클라이언트가 주고받는 모든 메시지는 JSON 형식으로 통일했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;type 필드로 메시지 종류를 구분합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ChatMessage 모델&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774173372728&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ChatMessage
{
    [JsonPropertyName(&quot;type&quot;)]
    public string Type { get; set; } = string.Empty;

    [JsonPropertyName(&quot;roomId&quot;)]
    public string? RoomId { get; set; }

    [JsonPropertyName(&quot;userId&quot;)]
    public string? UserId { get; set; }

    [JsonPropertyName(&quot;userName&quot;)]
    public string? UserName { get; set; }

    [JsonPropertyName(&quot;content&quot;)]
    public string? Content { get; set; }

    [JsonPropertyName(&quot;timestamp&quot;)]
    public string Timestamp { get; set; } = DateTime.UtcNow.ToString(&quot;o&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;전체 메시지 흐름&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774173399495&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;클라이언트 연결
  └─ 서버 &amp;rarr; 클라이언트: { &quot;type&quot;: &quot;connected&quot;, &quot;content&quot;: &quot;&amp;lt;connectionId&amp;gt;&quot; }
  └─ 클라이언트 &amp;rarr; 서버: { &quot;type&quot;: &quot;join&quot;, &quot;roomId&quot;: &quot;general&quot;, &quot;userId&quot;: &quot;...&quot;, &quot;userName&quot;: &quot;홍길동&quot; }
  └─ 서버 &amp;rarr; 룸 전체:   { &quot;type&quot;: &quot;joined&quot;, &quot;roomId&quot;: &quot;general&quot;, &quot;userId&quot;: &quot;...&quot;, &quot;userName&quot;: &quot;홍길동&quot; }

채팅
  └─ 클라이언트 &amp;rarr; 서버: { &quot;type&quot;: &quot;message&quot;, &quot;content&quot;: &quot;안녕하세요!&quot; }
  └─ 서버 &amp;rarr; 룸 전체:   { &quot;type&quot;: &quot;message&quot;, &quot;roomId&quot;: &quot;general&quot;, &quot;userId&quot;: &quot;...&quot;, &quot;userName&quot;: &quot;홍길동&quot;, &quot;content&quot;: &quot;안녕하세요!&quot;, &quot;timestamp&quot;: &quot;...&quot; }

퇴장
  └─ 클라이언트 &amp;rarr; 서버: { &quot;type&quot;: &quot;leave&quot; }
  └─ 서버 &amp;rarr; 룸 전체:   { &quot;type&quot;: &quot;left&quot;, &quot;roomId&quot;: &quot;general&quot;, &quot;userId&quot;: &quot;...&quot;, &quot;userName&quot;: &quot;홍길동&quot; }

연결 유지 (30초 주기)
  └─ 서버 &amp;rarr; 클라이언트: { &quot;type&quot;: &quot;ping&quot; }
  └─ 클라이언트 &amp;rarr; 서버: { &quot;type&quot;: &quot;pong&quot; }  &amp;larr; Blazor가 자동 응답&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;type&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;방향&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;connected&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Server -&amp;gt; Client&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;연결 성공, content에 connectionId 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;join&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Client -&amp;gt; Server&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;룸 입장 요청&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;joined&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Server -&amp;gt; Client&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;룸 전체에 입장 알림 브로드캐스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;message&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;양방향&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;채팅 메시지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;leave&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Client -&amp;gt; Server&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;퇴장 요청&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;left&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Server -&amp;gt; Client&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;퇴장 알ㄹ미 브로드캐스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;ping / pong&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Server -&amp;gt; Client&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;연결 유지 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;error&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Server -&amp;gt; Client&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;오류 메시지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774173646228&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ChatRoomManager
{
    // roomId &amp;rarr; RoomEntry (멤버 집합 + lock 객체 묶음)
    private readonly ConcurrentDictionary&amp;lt;string, RoomEntry&amp;gt; _rooms = new();
    // connectionId &amp;rarr; roomId (한 연결은 하나의 룸만 참여)
    private readonly ConcurrentDictionary&amp;lt;string, string&amp;gt; _connectionRoom = new();

    public void JoinRoom(string roomId, string connectionId)
    {
        var entry = _rooms.GetOrAdd(roomId, _ =&amp;gt; new RoomEntry());
        lock (entry)
        {
            entry.Members.Add(connectionId);
        }
        _connectionRoom[connectionId] = roomId;
    }

    public string? LeaveRoom(string connectionId)
    {
        if (!_connectionRoom.TryRemove(connectionId, out var roomId)) return null;

        if (_rooms.TryGetValue(roomId, out var entry))
            lock (entry) { entry.Members.Remove(connectionId); }

        return roomId;
    }

    public IReadOnlyList&amp;lt;string&amp;gt; GetRoomMembers(string roomId)
    {
        if (_rooms.TryGetValue(roomId, out var entry))
            lock (entry) { return entry.Members.ToList(); }
        return [];
    }

    private sealed class RoomEntry
    {
        public HashSet&amp;lt;string&amp;gt; Members { get; } = [];
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Room 멤버십 관리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- Chat Room접속 관리에 대한 전반적인 프로세스를 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후 개선 방향성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 현재 User(1) : Room(1) 연결 방식 1:N으로 구성 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- DB 구성을 통한 과거 채팅 내역 관리 구조 적용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;핵심 처리 로직&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774173857179&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ChatWebSocketHandler : WebSocketHandlerBase
{
    private readonly ChatRoomManager _roomManager;
    private readonly ConcurrentDictionary&amp;lt;string, (int count, DateTime windowStart)&amp;gt; _rateLimit = new();

    private const int MaxContentLength = 1000;
    private const int MaxRoomIdLength = 50;
    private const int MaxUserNameLength = 30;
    private const int RateLimitPerSecond = 5;

    public override async Task ReceiveAsync(WebSocketConnection connection, string message)
    {
        // 1. Rate Limiting 체크
        if (IsRateLimited(connection.ConnectionId))
        {
            await SendErrorAsync(connection.ConnectionId, &quot;Too many messages. Slow down.&quot;);
            return;
        }

        // 2. JSON 파싱
        var msg = JsonSerializer.Deserialize&amp;lt;ChatMessage&amp;gt;(message, JsonOptions);
        if (msg is null) return;

        // 3. 타입별 분기
        switch (msg.Type)
        {
            case &quot;join&quot;:    await HandleJoinAsync(connection, msg);    break;
            case &quot;message&quot;: await HandleMessageAsync(connection, msg); break;
            case &quot;leave&quot;:   await HandleLeaveAsync(connection);        break;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSocketHandlerBase를 상속하고, 메시지 타입별로 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Join 처피 - UserId / UserName 서버 측 저장&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774173935012&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private async Task HandleJoinAsync(WebSocketConnection connection, ChatMessage msg)
{
    // 입력 검증
    if (string.IsNullOrWhiteSpace(msg.RoomId) || msg.RoomId.Length &amp;gt; MaxRoomIdLength)
    {
        await SendErrorAsync(connection.ConnectionId, &quot;Invalid roomId&quot;);
        return;
    }
    if (string.IsNullOrWhiteSpace(msg.UserName) || msg.UserName.Length &amp;gt; MaxUserNameLength)
    {
        await SendErrorAsync(connection.ConnectionId, &quot;Invalid userName&quot;);
        return;
    }

    // 이미 다른 룸에 있으면 먼저 나가기
    if (_roomManager.GetUserRoom(connection.ConnectionId) is not null)
        await HandleLeaveAsync(connection);

    // UserId와 UserName을 서버(connection 객체)에 저장
    // 이후 메시지에서 클라이언트가 보내는 값은 무시
    connection.UserId = msg.UserId ?? connection.ConnectionId;
    connection.UserName = msg.UserName.Trim();

    _roomManager.JoinRoom(msg.RoomId, connection.ConnectionId);

    await BroadcastToRoomAsync(msg.RoomId, new ChatMessage
    {
        Type = &quot;joined&quot;,
        RoomId = msg.RoomId,
        UserId = connection.UserId,
        UserName = connection.UserName  // 서버에 저장된 값 사용
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;connection.UserName = msg.UserName에서 클라이언트 값을 서버에 저장하고, 이후 모든 메시지에서는 connection.UserName(서버 저장값)을 사용합니다. 클라이언트가 메시지마다 다른 이름을 보내도 무시됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Message 처리(브로드캐스트)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774174039861&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private async Task HandleMessageAsync(WebSocketConnection connection, ChatMessage msg)
{
    var roomId = _roomManager.GetUserRoom(connection.ConnectionId);
    if (roomId is null)
    {
        await SendErrorAsync(connection.ConnectionId, &quot;Join a room first&quot;);
        return;
    }

    if (string.IsNullOrWhiteSpace(msg.Content)) return;
    if (msg.Content.Length &amp;gt; MaxContentLength)
    {
        await SendErrorAsync(connection.ConnectionId, $&quot;Message too long (max {MaxContentLength})&quot;);
        return;
    }

    await BroadcastToRoomAsync(roomId, new ChatMessage
    {
        Type = &quot;message&quot;,
        RoomId = roomId,
        UserId = connection.UserId,
        UserName = connection.UserName,  // 클라이언트 값 무시
        Content = msg.Content.Trim()
    });
}

private async Task BroadcastToRoomAsync(string roomId, ChatMessage msg, string? exclude = null)
{
    var json = JsonSerializer.Serialize(msg);
    var tasks = _roomManager.GetRoomMembers(roomId)
        .Where(id =&amp;gt; id != exclude)
        .Select(id =&amp;gt; ConnectionManager.SendAsync(id, json));

    await Task.WhenAll(tasks); // 룸 내 전체 전송을 병렬로
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Rate Liniting(초당 메시지 제한)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774174214512&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private bool IsRateLimited(string connectionId)
{
    var now = DateTime.UtcNow;
    var state = _rateLimit.GetOrAdd(connectionId, _ =&amp;gt; (0, now));

    int newCount;
    DateTime windowStart;

    if ((now - state.windowStart).TotalSeconds &amp;gt;= 1)
    {
        // 1초 창이 지나면 카운터 초기화
        newCount = 1;
        windowStart = now;
    }
    else
    {
        newCount = state.count + 1;
        windowStart = state.windowStart;
    }

    _rateLimit[connectionId] = (newCount, windowStart);
    return newCount &amp;gt; RateLimitPerSecond; // 초당 5개 초과 시 차단
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬라이딩 윈도우 대신 고정 윈도우 방식입니다. 구현이 단순하고 채팅같은 UX에서는 충분합니다. 초당 5개 초과 메시지를 보내면 오류 메시지를 받고 해당 메시지는 무시됩니다. 연결을 끊지 않기 때문에 정상 사용자가 실수로 빠르게 입력하는 경우에 친화적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔트 - Blazor + JavaScript&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript를 사용해야 하는 이유?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blazor Server는 이미 내부적으로 SignalR WebSocket을 사용해 서버와 통신합니다. C# 코드에서 두 번째 WebSocket 연결을 직접 열 수 없습니다. 브라우저에서 WebSocket을 다루는 것은 JavaScript의 영역입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774174467846&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;구조
Blazor(C#) &amp;larr;── JS Interop ───&amp;rarr; chat.js &amp;larr;── WebSocket ───&amp;rarr; 서버&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blazor에서 JS 함수를 호출하거, JS에서 서버로부터 메시지를 받으면 다시 Blazor 메서드를 호출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;chat.js - 브라우저 WebSocket 클라이언트&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774174545476&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;window.ChatWs = (() =&amp;gt; {
    let ws = null;
    let dotnetRef = null;
    let wsUrl = null;
    let reconnectTimer = null;
    let reconnectDelay = 1000;       // 초기 재연결 대기 1초
    const maxReconnectDelay = 30000; // 최대 30초
    let manualClose = false;

    function connect(dotnetObjRef, url) {
        if (ws &amp;amp;&amp;amp; ws.readyState !== WebSocket.CLOSED) {
            manualClose = true;
            ws.close();
        }
        dotnetRef = dotnetObjRef;
        wsUrl = url;
        manualClose = false;
        _open();
    }

    function _open() {
        ws = new WebSocket(wsUrl);

        ws.onopen = () =&amp;gt; {
            reconnectDelay = 1000; // 성공 시 딜레이 초기화
        };

        ws.onmessage = (event) =&amp;gt; {
            if (dotnetRef) {
                dotnetRef.invokeMethodAsync('OnMessageReceived', event.data);
            }
        };

        ws.onclose = () =&amp;gt; {
            dotnetRef?.invokeMethodAsync('OnMessageReceived',
                JSON.stringify({ type: 'disconnected' }));

            // 자동 재연결 (지수 백오프)
            if (!manualClose) {
                reconnectTimer = setTimeout(() =&amp;gt; {
                    _open();
                    reconnectDelay = Math.min(reconnectDelay * 2, maxReconnectDelay);
                }, reconnectDelay);
            }
        };
    }

    function send(message) {
        if (ws &amp;amp;&amp;amp; ws.readyState === WebSocket.OPEN) ws.send(message);
    }

    function disconnect() {
        manualClose = true;
        clearTimeout(reconnectTimer);
        if (ws) { ws.close(); ws = null; }
        dotnetRef = null;
    }

    return { connect, send, disconnect };
})();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지수 백오프(Exponential Backoff):&amp;nbsp;네트워크 일시 끊김 후 재연결을 시도할 때,&amp;nbsp;서버가 부하 상태라면 모든 클라이언트가 동시에 재연결을 시도해 부하를 더 키웁니다.&amp;nbsp;대기 시간을 1초&amp;nbsp;&amp;rarr;&amp;nbsp;2초&amp;nbsp;&amp;rarr;&amp;nbsp;4초&amp;nbsp;&amp;rarr;&amp;nbsp;...&amp;nbsp;&amp;rarr;&amp;nbsp;30초로 늘려가면서 이를 방지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;manualClose&amp;nbsp;&lt;/span&gt;플래그&lt;span&gt;:&amp;nbsp;&lt;/span&gt;사용자가&lt;span&gt;&amp;nbsp;&quot;&lt;/span&gt;나가기&lt;span&gt;&quot;&amp;nbsp;&lt;/span&gt;버튼을&lt;span&gt; &lt;/span&gt;눌러&lt;span&gt; &lt;/span&gt;명시적으로&lt;span&gt; &lt;/span&gt;연결을&lt;span&gt; &lt;/span&gt;닫는&lt;span&gt; &lt;/span&gt;경우와&lt;span&gt;,&amp;nbsp;&lt;/span&gt;네트워크&lt;span&gt; &lt;/span&gt;문제로&lt;span&gt; &lt;/span&gt;연결이&lt;span&gt; &lt;/span&gt;끊기는&lt;span&gt; &lt;/span&gt;경우를&lt;span&gt; &lt;/span&gt;구분합니다&lt;span&gt;.&amp;nbsp;&lt;/span&gt;전자는&lt;span&gt; &lt;/span&gt;재연결하지&lt;span&gt; &lt;/span&gt;않고&lt;span&gt;,&amp;nbsp;&lt;/span&gt;후자만&lt;span&gt; &lt;/span&gt;재연결합니다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;ChatPageBase.cs - Blazor 상태 관리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774174605865&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ChatPageBase : ComponentBase, IAsyncDisposable
{
    [Inject] private IJSRuntime JS { get; set; } = null!;
    [Inject] private NavigationManager Nav { get; set; } = null!;

    protected string UserName { get; set; } = string.Empty;
    protected string RoomId { get; set; } = &quot;general&quot;;
    protected string InputText { get; set; } = string.Empty;
    protected string? ConnectionId { get; private set; }
    protected bool IsConnected { get; private set; }
    protected List&amp;lt;ChatDisplayMessage&amp;gt; Messages { get; } = [];

    private DotNetObjectReference&amp;lt;ChatPageBase&amp;gt;? _dotnetRef;

    protected async Task ConnectAsync()
    {
        _dotnetRef = DotNetObjectReference.Create(this);

        // http:// &amp;rarr; ws://, https:// &amp;rarr; wss:// 자동 변환
        var wsUrl = Nav.BaseUri
            .Replace(&quot;https://&quot;, &quot;wss://&quot;)
            .Replace(&quot;http://&quot;, &quot;ws://&quot;)
            .TrimEnd('/') + &quot;/ws/chat&quot;;

        await JS.InvokeVoidAsync(&quot;ChatWs.connect&quot;, _dotnetRef, wsUrl);
    }

    // JS &amp;rarr; C# 콜백 (JSInvokable)
    [JSInvokable]
    public async Task OnMessageReceived(string json)
    {
        var msg = JsonSerializer.Deserialize&amp;lt;ChatMessage&amp;gt;(json, ...);
        if (msg is null) return;

        switch (msg.Type)
        {
            case &quot;connected&quot;:
                ConnectionId = msg.Content;
                IsConnected = true;
                // 연결 직후 자동으로 룸 입장 요청
                await SendWsAsync(new ChatMessage
                {
                    Type = &quot;join&quot;,
                    RoomId = RoomId,
                    UserId = ConnectionId,
                    UserName = UserName
                });
                break;

            case &quot;joined&quot;:
            case &quot;left&quot;:
            case &quot;message&quot;:
                Messages.Add(new ChatDisplayMessage(msg));
                await ScrollToBottomAsync();
                break;

            case &quot;ping&quot;:
                await SendWsAsync(new ChatMessage { Type = &quot;pong&quot; });
                return; // UI 갱신 불필요

            case &quot;pong&quot;:
                return; // 무시

            case &quot;disconnected&quot;:
                IsConnected = false;
                ConnectionId = null;
                break;

            case &quot;error&quot;:
                Messages.Add(new ChatDisplayMessage(new ChatMessage
                {
                    Type = &quot;system&quot;,
                    Content = $&quot;[오류] {msg.Content}&quot;
                }));
                break;
        }

        await InvokeAsync(StateHasChanged); // Blazor UI 갱신
    }

    public async ValueTask DisposeAsync()
    {
        if (IsConnected) await DisconnectAsync(); // 페이지 벗어날 때 자동 정리
        _dotnetRef?.Dispose();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DotNetObjectReference:&amp;nbsp;JS에서 C#&amp;nbsp;메서드(OnMessageReceived)를 호출하기 위한 핸들입니다.&amp;nbsp;Dispose를 반드시 호출해야 메모리 누수가 없습니다.&amp;nbsp;DisposeAsync에서 처리했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InvokeAsync(StateHasChanged):&amp;nbsp;WebSocket &lt;span&gt;메시지는&lt;/span&gt; Blazor&lt;span&gt;의&lt;/span&gt; &lt;span&gt;렌더링&lt;/span&gt; &lt;span&gt;스레드&lt;/span&gt; &lt;span&gt;외부에서&lt;/span&gt; &lt;span&gt;도착합니다&lt;/span&gt;.&amp;nbsp;InvokeAsync&lt;span&gt;로&lt;/span&gt; &lt;span&gt;렌더링&lt;/span&gt; &lt;span&gt;스레드에&lt;/span&gt; UI &lt;span&gt;갱신을&lt;/span&gt; &lt;span&gt;예약해야&lt;/span&gt; &lt;span&gt;합니다&lt;/span&gt;.&lt;/p&gt;</description>
      <category>C#/이모저모</category>
      <author>Red_Horse</author>
      <guid isPermaLink="true">https://red-horse.tistory.com/168</guid>
      <comments>https://red-horse.tistory.com/168#entry168comment</comments>
      <pubDate>Sun, 22 Mar 2026 19:19:35 +0900</pubDate>
    </item>
    <item>
      <title>WebSocket</title>
      <link>https://red-horse.tistory.com/167</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WebSocket?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP는 요청-응답 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 요청하면 서버가 응답하고 연결을 닫습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 먼저 데이터를 보낼 방법이 없는데 WebSocket은 이 문제를 핸드세이크로 해결합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1774171169405&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;클라이언트 -&amp;gt; 서버: HTTP Upgrade 요청
서버 -&amp;gt; 클라이언트: 101 Switching Protocols

이후: TCP 연결 위에서 양방향 통신(연결 유지)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 연결되면 서버도 먼저 데이터를 보낼 수 있고, 연결은 명시적으로 닫을 때까지 유지됩니다. TCP 위의 애플리케이션 프로토콜이라는 게 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;아키텍처 설계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 WebSocket을 열고 닫는 것이 아니라, 여러 기능에서 재사용 가능한 인프라를 목표로 설계했습니다. 채팅뿐 아니라 나중에 운동 세션 실시간 공유, 알림 들을 추가할 때도 같은 구조를 쓸 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;&lt;b&gt;레이어&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;WebSocketConnection&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;소켓 래퍼. Send 동시성 보호, LastActivity 추적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;WebSocketConnecionManager&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;전체 연결 풀. Send/Broadcast/연결 수 제한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;IWebSocketHandler&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;연결 해제 수신 이벤트 인터페이스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;WebSocketHanlerBase&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;공통 로직 (DisconnectedAsync 시 풀에서 제거)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;WebSocketMiddleware&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;HTTP 업그레이드, 수신 루프, Ping/Pong, Origin 검증&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774171473377&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class WebSocketConnection : IDisposable
{
    public string ConnectionId { get; } = Guid.NewGuid().ToString();
    public System.Net.WebSockets.WebSocket Socket { get; init; } = null!;
    public string? UserId { get; set; }
    public string? UserName { get; set; }
    public DateTime ConnectedAt { get; } = DateTime.UtcNow;
    public DateTime LastActivity { get; private set; } = DateTime.UtcNow;

    public bool IsOpen =&amp;gt; Socket.State == WebSocketState.Open;

    private readonly SemaphoreSlim _sendLock = new(1, 1);

    public void UpdateLastActivity() =&amp;gt; LastActivity = DateTime.UtcNow;

    public async Task SendAsync(string message, CancellationToken ct = default)
    {
        if (!IsOpen) return;

        await _sendLock.WaitAsync(ct);
        try
        {
            if (!IsOpen) return;
            var buffer = Encoding.UTF8.GetBytes(message);
            await Socket.SendAsync(buffer, WebSocketMessageType.Text, true, ct);
        }
        finally
        {
            _sendLock.Release();
        }
    }

    public void Dispose() =&amp;gt; _sendLock.Dispose();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SemaphoreSlim 필요성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSocket은 동시에 Send가 하나만 허용됩니다. 같은 소켓에 두 곳에서 동시에 SendAsync를 호출하면 런타임 예외가 납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- PingLoop: 30초마다 Ping 전송(좀비 PC 체크용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- BroadcastToRoom: 채팅 메시지를 룸 내 전체에 전송(관리자 전체 메시지 전송용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SemaphoreSlim(1, 1)로 Socket당 Send를 직렬화해서 이 충돌을 막습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt; ※ SemaphoreSlim : 동시 접근제어를 위한 동시성 제어 기술(lock은 동기 코드로 비동기를 지원하는 SemaphoreSlim을 사용&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774171929313&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class WebSocketConnectionManager
{
    private readonly ConcurrentDictionary&amp;lt;string, WebSocketConnection&amp;gt; _connections = new();
    private readonly int _maxConnections;

    public WebSocketConnectionManager(int maxConnections = 1000)
    {
        _maxConnections = maxConnections;
    }

    public WebSocketConnection? AddConnection(System.Net.WebSockets.WebSocket socket)
    {
        if (_connections.Count &amp;gt;= _maxConnections) return null; // 연결 수 제한
        var connection = new WebSocketConnection { Socket = socket };
        _connections[connection.ConnectionId] = connection;
        return connection;
    }

    public Task SendAsync(string connectionId, string message, CancellationToken ct = default)
    {
        if (_connections.TryGetValue(connectionId, out var connection))
            return connection.SendAsync(message, ct); // 내부 SemaphoreSlim으로 직렬화됨
        return Task.CompletedTask;
    }

    public Task BroadcastAsync(string message, string? excludeConnectionId = null, CancellationToken ct = default)
    {
        var tasks = _connections.Values
            .Where(c =&amp;gt; c.IsOpen &amp;amp;&amp;amp; c.ConnectionId != excludeConnectionId)
            .Select(c =&amp;gt; c.SendAsync(message, ct));
        return Task.WhenAll(tasks);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConcurrentDictionary 사용 이유&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 연결은 다수의 스레드에서 동시에 추가/제거됩니다. 일반 Dictionary에 lock 없이 접근하면 데이터 손상이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774172049144&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface IWebSocketHandler
{
    Task OnConnectedAsync(WebSocketConnection connection);
    Task OnDisconnectedAsync(WebSocketConnection connection, Exception? exception);
    Task ReceiveAsync(WebSocketConnection connection, string message);
}

public abstract class WebSocketHandlerBase : IWebSocketHandler
{
    protected readonly WebSocketConnectionManager ConnectionManager;

    protected WebSocketHandlerBase(WebSocketConnectionManager connectionManager)
    {
        ConnectionManager = connectionManager;
    }

    public virtual Task OnConnectedAsync(WebSocketConnection connection) =&amp;gt; Task.CompletedTask;

    public virtual Task OnDisconnectedAsync(WebSocketConnection connection, Exception? exception)
    {
        ConnectionManager.RemoveConnection(connection.ConnectionId);
        return Task.CompletedTask;
    }

    public abstract Task ReceiveAsync(WebSocketConnection connection, string message);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후 신규 기능을 추가할 시에 WebSocketHanlerBase을 상속하고 ReceiveAsync만 구현하여 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 연결 풀 관리, Ping, 수신 루프 같은 인프라 코드는 손댈 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;★WebSocketMiddlewawre(핵심 루프)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. HTTP -&amp;gt; WebSocket Upgreade + 보안 체크&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774172247964&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public async Task InvokeAsync(HttpContext context, IServiceProvider serviceProvider)
{
    if (!context.WebSockets.IsWebSocketRequest)
    {
        await _next(context);
        return;
    }

    // Origin 검증: 허용된 도메인이 아니면 403 반환
    if (_options.AllowedOrigins.Count &amp;gt; 0)
    {
        var origin = context.Request.Headers.Origin.ToString();
        if (!_options.AllowedOrigins.Contains(origin))
        {
            context.Response.StatusCode = 403;
            return;
        }
    }

    // 연결 수 초과 거부
    var socket = await context.WebSockets.AcceptWebSocketAsync();
    var connection = connectionManager.AddConnection(socket);
    if (connection is null)
    {
        await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, &quot;Server full&quot;, CancellationToken.None);
        return;
    }
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 수신 루프 -&amp;gt; 멀티프레임 조합&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774172311980&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private async Task ReceiveLoopAsync(WebSocketConnection connection, IWebSocketHandler handler, CancellationToken ct)
{
    var buffer = ArrayPool&amp;lt;byte&amp;gt;.Shared.Rent(_options.BufferSize); // GC 부담 감소
    using var messageBuilder = new MemoryStream();                  // 프레임 조합용

    try
    {
        while (!ct.IsCancellationRequested &amp;amp;&amp;amp; connection.IsOpen)
        {
            // 수신 타임아웃 (기본 2분)
            using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
            timeoutCts.CancelAfter(_options.ReceiveTimeout);

            var result = await connection.Socket.ReceiveAsync(buffer, timeoutCts.Token);

            connection.UpdateLastActivity();

            if (result.MessageType == WebSocketMessageType.Close) break;
            if (result.MessageType != WebSocketMessageType.Text) continue;

            // 메시지 크기 제한 (기본 64KB)
            if (messageBuilder.Length + result.Count &amp;gt; _options.MaxMessageSize)
            {
                messageBuilder.SetLength(0);
                await connection.SendAsync(&quot;{\&quot;type\&quot;:\&quot;error\&quot;,\&quot;content\&quot;:\&quot;Message too large\&quot;}&quot;, ct);
                continue;
            }

            messageBuilder.Write(buffer, 0, result.Count);

            // EndOfMessage가 true일 때만 처리 (멀티프레임 조합 완료)
            if (!result.EndOfMessage) continue;

            var message = Encoding.UTF8.GetString(messageBuilder.ToArray());
            messageBuilder.SetLength(0);

            await handler.ReceiveAsync(connection, message);
        }
    }
    finally
    {
        ArrayPool&amp;lt;byte&amp;gt;.Shared.Return(buffer); // 반드시 반환
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 프레임을 신경 써야 하는 이유?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 메시지가 버퍼 크기(4KB)를 초과하면 WebSocket은 자동으로 여러 프레임으로 나눠 보냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 프레임을 독립적으로 처리하면 잘린 JSON이 파싱에 실패합니다. MemoryStream으로 프레임을 조합하다가 EndOfMessage = true인 마지막 프레임이 왔을 때 한 번에 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ArrayPool &amp;lt;byte&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- new byte [4096]은 연결마다 GC 힙에 새 객체를 할당합니다. 연결이 수천 개면 GC 압박이 생깁니다. ArrayPool은 사용이 끝난 배열을 풀에 반납하고 재사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Ping 푸프 - 좀비 연결 감지&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774172552781&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private async Task PingLoopAsync(WebSocketConnection connection, CancellationToken ct)
{
    while (!ct.IsCancellationRequested &amp;amp;&amp;amp; connection.IsOpen)
    {
        await Task.Delay(_options.PingInterval, ct); // 30초 대기

        // 마지막 활동이 60초 이상 없으면 좀비로 판단하고 연결 종료
        if (DateTime.UtcNow - connection.LastActivity &amp;gt; _options.PingInterval * 2)
            break;

        await connection.SendPingAsync(ct); // SemaphoreSlim 보호 하에 전송
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀비 연결?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 클라이언트가 WI-FI를 끊거나 앱을 강제 종료하면 TCP FIN 패킷이 전달되지 않습니다. 서버는 연결이 살아있다고 착각하고 _connections에 영원히 남겨둡니다. Ping을 주기적으로 보내서 응답이 없는 연결을 탐지하고 정리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. Task.WhenAny - 두 루프 동시 실행&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774172643097&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var receiveTask = ReceiveLoopAsync(connection, handler, cts.Token);
var pingTask = PingLoopAsync(connection, cts.Token);

var completed = await Task.WhenAny(receiveTask, pingTask);
await completed; // 먼저 끝난 Task를 다시 await &amp;rarr; 예외 전파 보장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수신 루프와 Ping 루프를 동시에 실행합니다. 둘 중 하나가 먼저 종료되면 (예: 클라이언트 연결 해제, Ping 타임아웃) finally 블록에서 나머지도 CancellationToken으로 종료시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;await completed의 중요성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- Task.WhenAny는 먼저 완료된 Task 객체를 반환할 뿐, 그 Task의 예외를 전파하지 않습니다. 반드시 다시 await 해야 예외가 올라옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>C#/이모저모</category>
      <author>Red_Horse</author>
      <guid isPermaLink="true">https://red-horse.tistory.com/167</guid>
      <comments>https://red-horse.tistory.com/167#entry167comment</comments>
      <pubDate>Sun, 22 Mar 2026 18:52:06 +0900</pubDate>
    </item>
    <item>
      <title>Redis</title>
      <link>https://red-horse.tistory.com/166</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Redis는 In-Memory 데이터 구조 저장소로, 다양한 자료형을 지원합니다. 그 중에서도 String은 Redis에서 가장 기본적이면서도 널리 사용되는 자료형입니다. 실제로 Redis 사용 사례의 90% 이상이 String 자료형을 활용한다고 할 수 있을 정도로 중요하고 유용한 자료형입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Redis String의 내부 구조: SDS (Simply Dynamic String)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;SDS란?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Redis String은 내부적으로 SDS(Simply Dynamic String) 구조를 사용합니다. 이는 C언어의 전통적인 null-terminated string이 아닌, Redis가 자체적으로 구현한 동적 문자열 구조입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;SDS의 주요 장점&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;1. 빠른 문자열 길이 조회&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;시간 복잡도: O(1)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;C의 strlen()은 O(n)이지만, SDS는 길이 정보를 미리 저장하여 즉시 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;2. 버퍼 오버플로우 방지&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;문자열 추가 시 자동으로 메모리 재할당&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;안전한 메모리 관리로 시스템 크래시 방지&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;3. 바이너리 세이프 (Binary Safe)&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;null 바이트(\0)를 포함한 모든 바이너리 데이터 저장 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이미지, 직렬화된 객체 등도 String으로 저장 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;4. 효율적인 APPEND 연산&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;미리 할당된 여유 공간을 활용하여 문자열 추가 최적화&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Redis String의 3가지 인코딩 방식&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Redis는 메모리 효율성을 위해 값의 특성에 따라 다른 인코딩 방식을 사용합니다:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;1. 정수값 저장 (int encoding)&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SET counter 123
# 내부적으로 정수로 저장하여 메모리 절약
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;2. embstr 인코딩 (&amp;le; 44바이트)&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SET short_text &quot;Hello Redis&quot;
# 44바이트 이하의 문자열은 연속된 메모리에 할당
# RedisObject와 SDS가 하나의 메모리 블록에 저장됨
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;3. raw 인코딩 (&amp;gt; 44바이트)&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SET long_text &quot;This is a very long string that exceeds 44 bytes limit...&quot;
# 44바이트 초과 시 RedisObject와 SDS를 별도 메모리에 분리 할당
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;핵심 String 명령어들&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;기본 저장 및 조회&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;SET 명령어&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Redis에서 90% 이상 사용되는 가장 기본적인 데이터 저장 명령어입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# 기본 사용법
SET users:1000:name &quot;Redhorse&quot;
SET users:1000:email &quot;redhorse@example.com&quot;

# 키 네이밍 컨벤션: 콜론(:)을 사용한 계층적 구조
SET project:redis:version &quot;7.0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;GET 명령어&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;저장된 데이터를 검색하는 명령어입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET users:1000:name
# 결과: &quot;Redhorse&quot;

GET nonexistent_key  
# 결과: (nil)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;카운터 명령어들&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Redis String의 강력한 기능 중 하나는 원자적(Atomic) 카운터 연산입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;INCR / DECR 명령어&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 방문 횟수 카운터
SET users:1000:visits 0
INCR users:1000:visits    # 1 증가 &amp;rarr; 1
INCR users:1000:visits    # 1 증가 &amp;rarr; 2

DECR users:1000:visits    # 1 감소 &amp;rarr; 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Race Condition 방지: INCR/DECR은 원자적 연산이므로 동시성 문제가 발생하지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;INCRBY / DECRBY 명령어&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 특정 값만큼 증가/감소
INCRBY users:1000:visits 5    # 5 증가
DECRBY users:1000:visits 2    # 2 감소

# 점수 시스템 예제
SET game:player:1000:score 150
INCRBY game:player:1000:score 50   # 50점 추가 &amp;rarr; 200
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;다중 작업 명령어&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;MSET (Multiple SET)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;# 여러 키-값을 한 번에 설정
MSET users:1000:name &quot;Redhorse&quot; users:1000:email &quot;redhorse@example.com&quot; users:1000:country &quot;Korea&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;MGET (Multiple GET)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 여러 키의 값을 한 번에 조회
MGET users:1000:name users:1000:email users:1000:age

# 결과 예시:
# 1) &quot;Redhorse&quot;
# 2) &quot;redhorse@example.com&quot;  
# 3) (nil)  # users:1000:age가 존재하지 않는 경우
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;RTT(Round-Trip Time) 감소: MGET/MSET을 사용하면 네트워크 왕복 시간을 크게 줄일 수 있습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;# 비효율적: 3번의 네트워크 통신
GET users:1000:name
GET users:1000:email  
GET users:1000:country

# 효율적: 1번의 네트워크 통신
MGET users:1000:name users:1000:email users:1000:country
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;SET 명령어의 고급 옵션들&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;만료 시간 설정&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;SETEX (SET with EXpire)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 300초(5분) 후 자동 만료
SETEX session:abc123 300 &quot;user_data&quot;

# 캐시 구현 예제
SETEX cache:weather:seoul 3600 &quot;22&amp;deg;C, Sunny&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;PSETEX (밀리초 단위)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 5000밀리초(5초) 후 만료
PSETEX temp:token:xyz789 5000 &quot;temporary_token&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;SET 명령어의 EX/PX 옵션&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# EX: 초 단위 만료
SET cache:user:1000 &quot;cached_data&quot; EX 3600

# PX: 밀리초 단위 만료  
SET cache:user:1000 &quot;cached_data&quot; PX 3600000

# NX: 키가 존재하지 않을 때만 설정
SET users:1000:name &quot;Redhorse&quot; NX

# XX: 키가 이미 존재할 때만 설정  
SET users:1000:name &quot;NewName&quot; XX
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;조건부 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;SETNX (SET if Not eXists)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 분산 락(Distributed Lock) 구현
SETNX lock:resource:db1 &quot;locked_by_server_A&quot;

# 결과가 1이면 락 획득 성공, 0이면 실패
# 중복 처리 방지에 유용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;실제 사용 사례들&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;1. 사용자 세션 관리&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# 사용자 로그인 시
SET session:a1b2c3d4 &quot;user:1000&quot; EX 1800  # 30분 세션

# 세션 확인
GET session:a1b2c3d4
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;2. 캐시 구현&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;# API 응답 캐싱 (1시간)
SET cache:api:weather:seoul &quot;22&amp;deg;C,Sunny,Humidity:65%&quot; EX 3600

# 데이터베이스 쿼리 결과 캐싱
SET cache:db:user:1000:profile &quot;{\&quot;name\&quot;:\&quot;Redhorse\&quot;,\&quot;role\&quot;:\&quot;developer\&quot;}&quot; EX 600
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;3. 카운터 시스템&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 일일 방문자 수
INCR stats:daily_visitors:2024-03-02

# API 호출 제한 (Rate Limiting)
SET rate_limit:api:user:1000 1 EX 60    # 1분간 1회
INCR rate_limit:api:user:1000
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;4. 분산 락&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# 중복 처리 방지를 위한 락
SET lock:payment:order:12345 &quot;processing&quot; NX EX 30

# 처리 완료 후 락 해제
DEL lock:payment:order:12345  
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;성능 최적화 팁&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;1. 키 네이밍 전략&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;# 좋은 예: 계층적이고 의미있는 구조
users:1000:profile
users:1000:settings:notification
orders:2024:03:02:summary

# 피해야 할 예: 너무 길거나 의미 불명확
this_is_a_very_long_key_name_that_wastes_memory
user_data_abcd1234
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;2. 메모리 효율성&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 작은 정수는 자동으로 정수 인코딩 사용
SET counter 42        # int 인코딩

# 44바이트 이하로 유지하면 embstr 인코딩
SET short_msg &quot;Hello&quot;  # embstr 인코딩

# 불필요하게 긴 문자열 피하기
SET data &quot;very long string...&quot;  # raw 인코딩 (메모리 사용량 증가)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;3. 배치 연산 활용&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;# 여러 번의 GET 대신 MGET 사용
MGET users:1000:name users:1000:email users:1000:role

# 여러 번의 SET 대신 MSET 사용  
MSET cache:A &quot;dataA&quot; cache:B &quot;dataB&quot; cache:C &quot;dataC&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;주의사항 및 베스트 프랙티스&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;1. 메모리 사용량 고려&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Redis는 In-Memory 데이터베이스이므로 메모리 사용량을 항상 모니터링해야 합니다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;대용량 문자열 저장 시 메모리 부족 위험이 있습니다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;2. 만료 시간 설정&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 캐시 데이터는 반드시 TTL 설정
SET cache:expensive_query &quot;result&quot; EX 3600

# 세션 데이터도 적절한 만료 시간 설정
SET session:token &quot;user_data&quot; EX 1800
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;3. 원자성 보장&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;# 카운터 연산 시 INCR/DECR 사용 (GET + SET 조합 피하기)
INCR page_views    # ✅ 원자적 연산

# 다음과 같은 방식은 Race Condition 위험
GET page_views     # ❌ 비원자적
SET page_views 101 # ❌ 중간에 다른 클라이언트가 수정할 수 있음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;4. 키 존재 여부 확인&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# 조건부 설정 활용
SET lock:resource &quot;owner&quot; NX    # 키가 없을 때만 설정
SET config:updated &quot;yes&quot; XX     # 키가 있을 때만 업데이트
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Redis String은 단순해 보이지만 매우 강력하고 유연한 자료형입니다. SDS 구조의 장점과 다양한 인코딩 방식을 통해 메모리 효율성을 제공하며, 풍부한 명령어 세트를 통해 다양한 사용 사례를 지원합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;특히 캐싱, 세션 관리, 카운터, 분산 락 등의 시나리오에서 Redis String의 진가를 발휘할 수 있습니다. 원자적 연산과 RTT 최적화를 통해 높은 성능을 보장하므로, 현대적인 웹 애플리케이션에서 필수적인 도구라고 할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;다음 포스트에서는 Redis의 다른 자료형들(List, Set, Hash, Sorted Set)에 대해서도 자세히 알아보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;참고 자료:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a href=&quot;https://redis.io/docs/data-types/strings/&quot;&gt;Redis 공식 문서 - String&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a href=&quot;https://redis.io/docs/reference/internals/&quot;&gt;Redis 내부 구조와 SDS&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>ZeroBase/Redis</category>
      <author>Red_Horse</author>
      <guid isPermaLink="true">https://red-horse.tistory.com/166</guid>
      <comments>https://red-horse.tistory.com/166#entry166comment</comments>
      <pubDate>Mon, 2 Mar 2026 16:48:37 +0900</pubDate>
    </item>
    <item>
      <title>웹 애플리케이션(.NET)</title>
      <link>https://red-horse.tistory.com/165</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1. ASP.NET MVC (Server-Side Rendering)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;동작 방식:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;전통적인 서버 사이드 렌더링 방식&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;클라이언트 요청 &amp;rarr; 서버에서 HTML 생성 &amp;rarr; 완성된 HTML 반환&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;각 페이지 이동마다 전체 페이지 새로고침&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;SEO 친화적 (검색엔진이 콘텐츠 쉽게 크롤링)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;초기 로딩 빠름 (서버에서 완성된 HTML 전송)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;서버 리소스 활용 (클라이언트 부담 적음)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;.NET Framework 4.8 같은 레거시 환경에서도 안정적&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;단점:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;페이지 전환시 깜빡임&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;서버 부하 높음 (매 요청마다 HTML 생성)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;인터랙티브한 UX 구현 어려움&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;JavaScript로 추가 작업 필요&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;적합한 경우:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;콘텐츠 중심 웹사이트&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;SEO가 중요한 경우&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;레거시 시스템 유지보수&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1764511039030&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Controller
public class ProductController : Controller
{
    public ActionResult Index()
    {
        var products = _productService.GetAll();
        return View(products); // 서버에서 HTML 생성
    }
    
    public ActionResult Detail(int id)
    {
        var product = _productService.GetById(id);
        return View(product); // 페이지 전체 새로고침
    }
}
```

**플로우:**
```
사용자: /Product/Index 요청
    &amp;darr;
서버: DB 조회 &amp;rarr; Razor 뷰 엔진으로 HTML 생성
    &amp;darr;
브라우저: 완성된 HTML 받아서 전체 렌더링
    &amp;darr;
사용자: 상세페이지 클릭 (/Product/Detail/1)
    &amp;darr;
서버: 다시 HTML 생성
    &amp;darr;
브라우저: 페이지 전체 새로고침 (깜빡임)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2. Blazor Server (SSR + SignalR)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;동작 방식:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;초기 HTML은 서버에서 렌더링&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이후 SignalR로 실시간 양방향 통신&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;UI 이벤트 &amp;rarr; SignalR &amp;rarr; 서버 처리 &amp;rarr; DOM 업데이트 전송&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;빠른 초기 로딩&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;작은 다운로드 크기 (클라이언트에 전체 앱 불필요)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;서버에서 로직 실행 (보안에 유리)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;C# 코드만으로 개발 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;단점:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;지속적인 서버 연결 필요 (동시 사용자 많으면 부담)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;네트워크 지연 시 반응 느림&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;서버 종속적 (오프라인 불가)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;SignalR 연결 관리 복잡도&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;적합한 경우:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;기업 내부 시스템 (안정적 네트워크)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;실시간 데이터 처리&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;보안이 중요한 애플리케이션&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1764511070397&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Component (.razor)
@page &quot;/products&quot;
@inject ProductService ProductService

&amp;lt;h3&amp;gt;상품 목록&amp;lt;/h3&amp;gt;
@foreach (var product in products)
{
    &amp;lt;div @onclick=&quot;() =&amp;gt; ShowDetail(product.Id)&quot;&amp;gt;
        @product.Name
    &amp;lt;/div&amp;gt;
}

@code {
    private List&amp;lt;Product&amp;gt; products;
    
    protected override async Task OnInitializedAsync()
    {
        products = await ProductService.GetAllAsync();
    }
    
    private async Task ShowDetail(int id)
    {
        // UI만 부분 업데이트 (페이지 새로고침 없음)
        selectedProduct = await ProductService.GetByIdAsync(id);
    }
}
```

**플로우:**
```
초기 로딩: 서버에서 HTML 생성 &amp;rarr; 브라우저 렌더링
    &amp;darr;
SignalR 연결 수립 (WebSocket)
    &amp;darr;
사용자: 버튼 클릭
    &amp;darr;
SignalR로 이벤트를 서버에 전송
    &amp;darr;
서버: 처리 후 변경된 UI 정보만 전송
    &amp;darr;
브라우저: DOM만 부분 업데이트 (새로고침 없음)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3. Blazor WebAssembly (CSR)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;동작 방식:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;전체 앱을 WebAssembly로 클라이언트에 다운로드&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;브라우저에서 .NET 런타임 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;SPA처럼 동작 (클라이언트에서 모든 렌더링)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;SPA 경험 (빠른 페이지 전환, 부드러운 UX)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;서버 부하 최소화&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;오프라인 작동 가능 (PWA)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;API 서버와 분리 배포 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;단점:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;초기 로딩 느림 (전체 앱 다운로드 필요)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;SEO 어려움 (초기 HTML 거의 비어있음)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;브라우저 호환성 이슈 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;클라이언트 성능에 의존&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;적합한 경우:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;인터랙티브한 웹 앱&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;PWA 구축&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;API 기반 아키텍처&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1764511188528&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Component (.razor)
@page &quot;/products&quot;
@inject HttpClient Http

&amp;lt;h3&amp;gt;상품 목록&amp;lt;/h3&amp;gt;
@foreach (var product in products)
{
    &amp;lt;div @onclick=&quot;() =&amp;gt; ShowDetail(product.Id)&quot;&amp;gt;
        @product.Name
    &amp;lt;/div&amp;gt;
}

@code {
    private List&amp;lt;Product&amp;gt; products;
    
    protected override async Task OnInitializedAsync()
    {
        // 브라우저에서 직접 API 호출
        products = await Http.GetFromJsonAsync&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;(&quot;api/products&quot;);
    }
    
    private async Task ShowDetail(int id)
    {
        // 브라우저에서 모든 처리 실행
        selectedProduct = await Http.GetFromJsonAsync&amp;lt;Product&amp;gt;($&quot;api/products/{id}&quot;);
    }
}
```

**플로우:**
```
첫 방문: 전체 앱 다운로드 (.dll, 런타임 등) &amp;rarr; 느림
    &amp;darr;
브라우저에서 .NET 런타임 시작
    &amp;darr;
사용자: 페이지 이동 또는 버튼 클릭
    &amp;darr;
브라우저에서 C# 코드 직접 실행 (WebAssembly)
    &amp;darr;
필요시 API 서버에 데이터만 요청
    &amp;darr;
브라우저에서 UI 업데이트 (빠른 반응, SPA 경험)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Blazor 8+ (Auto/통합 렌더링)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;.NET 8부터는 Static SSR, Interactive Server, Interactive WebAssembly, Interactive Auto 모드를 컴포넌트 단위로 선택 가능합니다.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;렌더 모드 옵션:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;@rendermode InteractiveServer: Blazor Server 방식&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;@rendermode InteractiveWebAssembly: WASM 방식&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;@rendermode InteractiveAuto: 초기 Server &amp;rarr; 이후 WASM 전환&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Static SSR (기본): MVC처럼 정적 HTML만&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>C#/이모저모</category>
      <author>Red_Horse</author>
      <guid isPermaLink="true">https://red-horse.tistory.com/165</guid>
      <comments>https://red-horse.tistory.com/165#entry165comment</comments>
      <pubDate>Sun, 30 Nov 2025 23:11:19 +0900</pubDate>
    </item>
    <item>
      <title>임시 파일 자동정리(.NET TimerQueue &amp;amp; .NET IHostedService)</title>
      <link>https://red-horse.tistory.com/164</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;웹 어플리케이션 운영시 파일을 생성하고 다운로드 링크로 제공할시에 정상적인 플로우로 다운로드 및 임시파일 삭제가 이루어지면 문제가 없지만 간혹 프로세스 진행중에 브라우저를 닫거나 예기치 못한 오류로 종료가 되었을때 임시 파일이 서버에 쌓이는 문제가 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이러한 문제를 해결하기 위해 .NET Timer Queue에 정리하는 프로세스를 예약해놓고 주기적으로 해당 동작이 수행되게 구성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;서버 자동 삭제의 장점&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저 상태와 무관&lt;/li&gt;
&lt;li&gt;확실한 정리 보장&lt;/li&gt;
&lt;li&gt;관리 편의성&lt;/li&gt;
&lt;li&gt;리소스 효율적 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Timer의 동작 원리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;_timer = new Timer(callback, state, dueTime, period);
```
**내부 동작:**

1. Timer 생성 시 OS의 타이머 큐에 등록
2. 대기 중에는 CPU/쓰레드 사용 안 함 (리소스 0%)
3. 지정된 시간이 되면 OS가 하드웨어 인터럽트 발생
4. ThreadPool에서 쓰레드를 빌려 콜백 실행
5. 실행 완료 후 쓰레드 반납

**비유:**  
알람시계를 맞춰놓고 자는 것과 같습니다. 알람 시간까지 당신은 아무것도 하지 않고(CPU 사용 없음), 시간이 되면 알람이 울립니다.

### 리소스 사용량
```
Timer 1개당:
- 메모리: ~1KB
- CPU: 0% (대기 중)
- 쓰레드: 실행 시점에만 잠깐 사용

10개의 Timer를 돌려도:
- 메모리: ~10KB
- CPU: 여전히 0%
```&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;.NET Framework 4.8 MVC 구현&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1. 서비스 클래스 생성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;csharp&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;// Services/TempFileCleanupService.cs
using System;
using System.IO;
using System.Threading;
using System.Web;

public class TempFileCleanupService
{
    private static Timer _timer;
    
    public static void Start(params string[] folderPaths)
    {
        // 다음 정각까지 대기 시간 계산
        var now = DateTime.Now;
        var nextHour = now.Date.AddHours(now.Hour + 1);
        var delay = nextHour - now;
        
        _timer = new Timer(
            Cleanup,
            folderPaths,
            delay,                       // 첫 실행: 다음 정각
            TimeSpan.FromHours(1)        // 이후: 1시간마다
        );
    }
    
    private static void Cleanup(object state)
    {
        string[] folders = state as string[];
        
        foreach (var folder in folders)
        {
            CleanupFolder(folder);
        }
    }
    
    private static void CleanupFolder(string folderPath)
    {
        try
        {
            if (!Directory.Exists(folderPath))
                return;
            
            var files = Directory.GetFiles(folderPath);
            int deletedCount = 0;
            
            foreach (var file in files)
            {
                try
                {
                    var fileInfo = new FileInfo(file);
                    // 30분 이상 된 파일 삭제
                    if (DateTime.Now - fileInfo.CreationTime &amp;gt; TimeSpan.FromMinutes(30))
                    {
                        File.Delete(file);
                        deletedCount++;
                    }
                }
                catch (Exception ex)
                {
                    // 개별 파일 삭제 실패 시 로깅
                    System.Diagnostics.Debug.WriteLine($&quot;파일 삭제 실패: {file}, {ex.Message}&quot;);
                }
            }
            
            if (deletedCount &amp;gt; 0)
            {
                System.Diagnostics.Debug.WriteLine($&quot;{folderPath}: {deletedCount}개 파일 삭제됨&quot;);
            }
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine($&quot;{folderPath} 정리 실패: {ex.Message}&quot;);
        }
    }
    
    // 선택사항: 명시적 종료
    public static void Stop()
    {
        _timer?.Dispose();
        _timer = null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2. Global.asax.cs에서 시작&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;csharp&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;public class MvcApplication : HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        
        // 임시 파일 자동 정리 시작
        TempFileCleanupService.Start(
            Server.MapPath(&quot;~/Temp&quot;),
            Server.MapPath(&quot;~/Content/Present&quot;),
            Server.MapPath(&quot;~/Uploads/Temp&quot;)
        );
    }
    
    // 선택사항: 애플리케이션 종료 시 정리
    protected void Application_End()
    {
        TempFileCleanupService.Stop();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;.NET 8 Blazor Server 구현&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Blazor에서는 IHostedService를 사용하는 것이 표준 방식입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1. 서비스 클래스 생성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;csharp&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;// Services/TempFileCleanupService.cs
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

public class TempFileCleanupService : IHostedService, IDisposable
{
    private Timer _timer;
    private readonly ILogger&amp;lt;TempFileCleanupService&amp;gt; _logger;
    private readonly IWebHostEnvironment _env;
    private readonly IConfiguration _config;
    
    public TempFileCleanupService(
        ILogger&amp;lt;TempFileCleanupService&amp;gt; logger,
        IWebHostEnvironment env,
        IConfiguration config)
    {
        _logger = logger;
        _env = env;
        _config = config;
    }
    
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation(&quot;임시 파일 정리 서비스 시작&quot;);
        
        // 다음 정각까지 대기
        var now = DateTime.Now;
        var nextHour = now.Date.AddHours(now.Hour + 1);
        var delay = nextHour - now;
        
        _timer = new Timer(
            Cleanup,
            null,
            delay,
            TimeSpan.FromHours(1)
        );
        
        return Task.CompletedTask;
    }
    
    private void Cleanup(object state)
    {
        try
        {
            // appsettings.json에서 설정 읽기
            var folders = _config.GetSection(&quot;TempFileCleaning:Folders&quot;).Get&amp;lt;string[]&amp;gt;();
            var maxAgeMinutes = _config.GetValue&amp;lt;int&amp;gt;(&quot;TempFileCleaning:MaxAgeMinutes&quot;);
            
            foreach (var folder in folders)
            {
                string fullPath = Path.Combine(_env.ContentRootPath, folder);
                CleanupFolder(fullPath, maxAgeMinutes);
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, &quot;파일 정리 중 오류 발생&quot;);
        }
    }
    
    private void CleanupFolder(string folderPath, int maxAgeMinutes)
    {
        try
        {
            if (!Directory.Exists(folderPath))
                return;
            
            var files = Directory.GetFiles(folderPath);
            int deletedCount = 0;
            
            foreach (var file in files)
            {
                try
                {
                    var fileInfo = new FileInfo(file);
                    if (DateTime.Now - fileInfo.CreationTime &amp;gt; TimeSpan.FromMinutes(maxAgeMinutes))
                    {
                        File.Delete(file);
                        deletedCount++;
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, $&quot;파일 삭제 실패: {file}&quot;);
                }
            }
            
            if (deletedCount &amp;gt; 0)
            {
                _logger.LogInformation($&quot;{folderPath}: {deletedCount}개 파일 삭제됨&quot;);
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, $&quot;폴더 정리 실패: {folderPath}&quot;);
        }
    }
    
    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation(&quot;임시 파일 정리 서비스 종료&quot;);
        _timer?.Change(Timeout.Infinite, 0);
        return Task.CompletedTask;
    }
    
    public void Dispose()
    {
        _timer?.Dispose();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2. appsettings.json 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;{
  &quot;Logging&quot;: {
    &quot;LogLevel&quot;: {
      &quot;Default&quot;: &quot;Information&quot;
    }
  },
  &quot;TempFileCleaning&quot;: {
    &quot;Folders&quot;: [
      &quot;wwwroot/Temp&quot;,
      &quot;wwwroot/Uploads/Temp&quot;,
      &quot;Content/Present&quot;
    ],
    &quot;MaxAgeMinutes&quot;: 30
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3. Program.cs에서 등록&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;csharp&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;var builder = WebApplication.CreateBuilder(args);

// Razor Pages, Blazor 등 기본 서비스 등록
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();

// 백그라운드 서비스 등록
builder.Services.AddHostedService&amp;lt;TempFileCleanupService&amp;gt;();

var app = builder.Build();

// 미들웨어 설정
app.UseStaticFiles();
app.UseRouting();

app.MapBlazorHub();
app.MapFallbackToPage(&quot;/_Host&quot;);

app.Run();
```

## IHostedService 동작 원리

많은 분들이 `StartAsync`가 어떻게 호출되는지 궁금해하십니다.

### 호출 흐름
```
1. Program.cs에서 서비스 등록:
   builder.Services.AddHostedService&amp;lt;TempFileCleanupService&amp;gt;();
   &amp;rarr; &quot;이 서비스는 백그라운드 서비스입니다&quot; 라고 표시
   
2. app.Run() 실행:
   &amp;rarr; ASP.NET Core 런타임이 시작됨
   
3. 런타임이 자동으로:
   &amp;rarr; 모든 IHostedService를 찾음
   &amp;rarr; 각 서비스의 StartAsync() 호출
   
4. 애플리케이션 실행 중...
   
5. 애플리케이션 종료 시:
   &amp;rarr; 모든 IHostedService의 StopAsync() 호출&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;MVC vs Blazor 비교&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;csharp&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;// MVC (.NET Framework 4.8)
// 수동으로 직접 호출
protected void Application_Start()
{
    TempFileCleanupService.Start(folders); // 개발자가 호출
}

// Blazor (.NET 8)
// 프레임워크가 자동 호출
builder.Services.AddHostedService&amp;lt;TempFileCleanupService&amp;gt;();
// app.Run() 시점에 자동으로 StartAsync() 실행&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;차이점&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;항목&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;.NET Framework MVC&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;.NET 8 Blazor&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;진입점&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Global.asax.cs&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Program.cs&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;시작 방식&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;수동 호출&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;자동 호출 (IHostedService)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;설정 방식&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;코드에 직접&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;appsettings.json&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;로깅&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Debug.WriteLine&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;ILogger (구조화된 로깅)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;종료 처리&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Application_End&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;StopAsync (자동)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;DI 지원&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;제한적&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;완전 지원&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;주의사항&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;1. 폴더 권한 확인&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;IIS 애플리케이션 풀 계정이 해당 폴더에 대한 삭제 권한을 가지고 있어야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;2. 네트워크 드라이브 사용 시&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;네트워크 드라이브의 파일을 삭제할 때는 네트워크 지연을 고려해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #1b711d;&quot;&gt;관련문서&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #000000;&quot;&gt;- &lt;a title=&quot;Microsoft MVC 공식권장 방법&quot; href=&quot;https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-10.0&amp;amp;tabs=visual-studio&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-10.0&amp;amp;tabs=visual-studio&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1764250202663&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Background tasks with hosted services in ASP.NET Core&quot; data-og-description=&quot;Learn how to implement background tasks with hosted services in ASP.NET Core.&quot; data-og-host=&quot;learn.microsoft.com&quot; data-og-source-url=&quot;https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-10.0&amp;amp;tabs=visual-studio&quot; data-og-url=&quot;https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-10.0&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gNOHw/hyZOsCfBjQ/hutBBDKF4VBfs8ukwdmx71/img.png?width=456&amp;amp;height=456&amp;amp;face=0_0_456_456&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-10.0&amp;amp;tabs=visual-studio&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-10.0&amp;amp;tabs=visual-studio&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gNOHw/hyZOsCfBjQ/hutBBDKF4VBfs8ukwdmx71/img.png?width=456&amp;amp;height=456&amp;amp;face=0_0_456_456');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Background tasks with hosted services in ASP.NET Core&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Learn how to implement background tasks with hosted services in ASP.NET Core.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;learn.microsoft.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>C#/이모저모</category>
      <author>Red_Horse</author>
      <guid isPermaLink="true">https://red-horse.tistory.com/164</guid>
      <comments>https://red-horse.tistory.com/164#entry164comment</comments>
      <pubDate>Thu, 27 Nov 2025 22:32:28 +0900</pubDate>
    </item>
    <item>
      <title>IIS(Internet Information Services)</title>
      <link>https://red-horse.tistory.com/163</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Microsoft가 만든 웹 서버 소프트웨어&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;식당으로 비유:
- ASP.NET MVC 앱 = 요리사가 만든 음식
- IIS = 식당 건물 + 웨이터 (손님에게 음식을 서빙)
- 브라우저 = 손님&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// 역할
1. 브라우저 요청 받기
   사용자: &quot;http://localhost/Home/Index 주세요!&quot;
   
2. ASP.NET 앱에게 전달
   IIS: &quot;야, ASP.NET! /Home/Index 처리해줘&quot;
   
3. 결과를 브라우저에게 돌려주기
   IIS: &quot;여기 HTML 페이지 나왔습니다~&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;타 웹 서버들과 비교&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;웹&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;서버&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;&amp;nbsp;주로 사용하는 언어운영체제&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;IIS&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;C# (ASP.NET)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Windows&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Apache&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;PHP, Python&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Linux/Windows&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Nginx&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;모든 언어&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Linux/Windows&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Tomcat&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Java&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Linux/Windows&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Visual Studio에서 개발할 시&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;개발 중 (F5 실행시)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;IIS Express 사용
- IIS의 경량 버전
- 개발자 PC에서만 동작
- 자동으로 실행됨 (포트: 44300 같은 번호)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;실제 서버 배포 시&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;asciidoc&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;IIS (full version) 사용
- Windows Server에 설치
- 실제 사용자들이 접속
- 포트 80 (HTTP) 또는 443 (HTTPS)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;실제 사용 예시&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;당신의 개발 환경:
┌─────────────────────────────────┐
│  Visual Studio (F5 실행)         │
│         &amp;darr;                       │
│  IIS Express 자동 시작            │
│         &amp;darr;                       │
│  ASP.NET MVC 앱 실행              │
│         &amp;darr;                       │
│  브라우저 자동 열림:                 │
│  https://localhost:44300        │
└─────────────────────────────────┘

실제 서버 환경:
┌─────────────────────────────────┐
│  Windows Server                 │
│         &amp;darr;                       │
│  IIS 설치 및 설정                  │
│         &amp;darr;                       │
│  ASP.NET MVC 앱 배포              │
│         &amp;darr;                       │
│  사용자 접속:                      │
│  https://company.com            │
└─────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;IIS Manager (관리 도구)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Windows에서 IIS를 설정할 때 사용하는 GUI 프로그램:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;할 수 있는 것들:
✅ 웹사이트 추가/삭제
✅ 포트 번호 변경 (80, 443, 8080 등)
✅ SSL 인증서 설정 (HTTPS)
✅ 애플리케이션 풀 관리 (메모리, CPU 제한)
✅ 가상 디렉토리 설정&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;IIS는 .net Framework 버전(.net core 이전버전)에서 Window만 지원되어 사용한 구조로서 .net core부터는 실행되는 서버의 운영체제 제한이 없어졌습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>C#/이모저모</category>
      <author>Red_Horse</author>
      <guid isPermaLink="true">https://red-horse.tistory.com/163</guid>
      <comments>https://red-horse.tistory.com/163#entry163comment</comments>
      <pubDate>Thu, 27 Nov 2025 00:29:07 +0900</pubDate>
    </item>
    <item>
      <title>파일 생성 및 다운로드 관리(물리 경로 vs 가상 경로)_Web</title>
      <link>https://red-horse.tistory.com/162</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;물리 경로 (Physical Path)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;csharp&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;// Server.MapPath()로 변환
string physicalPath = Server.MapPath(&quot;~/Content/Data&quot;);
// 결과: C:\Projects\MyApp\Content\Data

// 사용: 파일 시스템 작업
Directory.CreateDirectory(physicalPath);
File.WriteAllBytes(Path.Combine(physicalPath, &quot;file.xlsx&quot;), bytes);
File.Delete(Path.Combine(physicalPath, &quot;file.xlsx&quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;특징:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;실제 디스크 경로&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;운영체제 파일 시스템이 이해하는 경로&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;C#의 File, Directory 클래스가 사용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;가상 경로 (Virtual Path)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;csharp&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;// 웹 URL 경로
string virtualPath = &quot;/Content/Data/file.xlsx&quot;;

// 사용: 다운로드 링크
return Json(new { Data = virtualPath });&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;특징:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;웹 서버가 이해하는 경로&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;브라우저가 접근하는 경로&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;도메인 + 가상 경로 = 완전한 URL&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;분리해서 사용하는 이유&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1. 역할이 다름&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;csharp&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;// 물리 경로로 다운로드 링크 만들면?
string physicalPath = &quot;C:\\Projects\\MyApp\\Content\\file.xlsx&quot;;
return Json(new { Data = physicalPath });

// JavaScript에서
window.location.href = &quot;C:\\Projects\\MyApp\\Content\\file.xlsx&quot;;
// 브라우저는 이걸 이해 못 함!
// 로컬 파일 시스템 접근은 보안상 차단됨! (브라우저에서 PC로 접근하려는 모습)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;csharp&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;// 가상 경로로 다운로드 링크
string virtualPath = &quot;/Content/file.xlsx&quot;;
return Json(new { Data = virtualPath });

// JavaScript에서
window.location.href = &quot;/Content/file.xlsx&quot;;
// 브라우저가 이해 할수 있는 경로!
// 실제 요청: http://localhost/Content/file.xlsx
// 실행되고 있는 프로젝트 경로로 접근&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2. 서버 환경마다 물리 경로가 다름&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764169256677&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;개발 서버: C:\Projects\MyApp\Content\file.xlsx
스테이징: D:\WebApps\MyApp\Content\file.xlsx
운영 서버: E:\inetpub\wwwroot\MyApp\Content\file.xlsx

가상 경로는 모든 환경에서 동일: /Content/file.xlsx
물리 경로는 환경마다 다름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3. 보안&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;csharp&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;// 물리 경로 노출 위험
return Json(new { Data = &quot;C:\\Projects\\MyApp\\Content\\file.xlsx&quot; });
// 사용자가 서버 구조를 알게 됨!(PC 경로 구성을 외부에 보이게됨)

// 가상 경로는 안전
return Json(new { Data = &quot;/Content/file.xlsx&quot; });
// 서버 구조는 숨겨짐&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;동작 원리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1. 파일 저장 (물리 경로)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;csharp&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;// Step 1: 물리 경로 얻기
string physicalPath = Server.MapPath(&quot;~/Content/Data&quot;);
// C:\Projects\MyApp\Content\Data

// Step 2: 파일 시스템에 저장
File.WriteAllBytes(Path.Combine(physicalPath, &quot;file.xlsx&quot;), bytes);
// C:\Projects\MyApp\Content\Data\file.xlsx 생성&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #1b711d;&quot;&gt;&lt;b&gt;물리 경로를 사용하는 이유&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;File.WriteAllBytes()는 운영체제 API 호출&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;운영체제는 물리 경로만 이해함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;/Content/file.xlsx 같은 가상 경로는 이해 못 함(브라우저 접근용)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2. 다운로드 링크 (가상 경로)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;csharp&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;// Step 1: 가상 경로 생성
string virtualPath = &quot;/Content/Data/file.xlsx&quot;;

// Step 2: 클라이언트에 전달
return Json(new { Data = virtualPath });&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;window.location.href = &quot;/Content/Data/file.xlsx&quot;;
// 브라우저가 요청: http://localhost/Content/Data/file.xlsx
//서버에서 (IIS/ASP.NET):**

1. 요청 받음: GET /Content/GoodsSelectJson/file.xlsx
2. 가상 경로를 물리 경로로 변환: C:\Projects\MyApp\Content\Data\file.xlsx
3. 파일 읽기
4. 응답 전송&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;csharp&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;// Controllers/PresentController.cs
public ActionResult PresentInfomation(...)
{
    // 1. 물리 경로로 파일 저장
    string physicalFolder = Server.MapPath(&quot;~/Content/Present&quot;);
    // C:\Projects\MyApp\Content\Present
    
    if (!Directory.Exists(physicalFolder))
    {
        Directory.CreateDirectory(physicalFolder);
        // 물리 폴더 생성: C:\Projects\MyApp\Content\Present
    }
    
    string fileName = &quot;items_20241126.xlsx&quot;;
    string physicalFilePath = Path.Combine(physicalFolder, fileName);
    // C:\Projects\MyApp\Content\Present\items_20241126.xlsx
    
    File.WriteAllBytes(physicalFilePath, excelBytes);
    // 운영체제 파일 시스템에 저장
    
    // 2. 가상 경로로 다운로드 링크 생성
    string virtualPath = $&quot;/Content/Present/{fileName}&quot;;
    // /Content/Present/items_20241126.xlsx
    
    return Json(new
    {
        Success = true,
        Data = virtualPath  // 가상 경로 반환
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// JavaScript
$.ajax({
    success: function(result) {
        // result.Data = &quot;/Content/Present/items_20241126.xlsx&quot;
        
        // 3. 브라우저가 다운로드 요청
        window.location.href = result.Data;
        // 실제 요청: http://localhost/Content/Present/items_20241126.xlsx
    }
});
```
```
4. IIS/ASP.NET 처리(자동으로 됨)
요청: GET /Content/Present/items_20241126.xlsx

IIS:
- 가상 경로: /Content/Present/items_20241126.xlsx
- 물리 경로 변환: C:\Projects\MyApp\Content\Present\items_20241126.xlsx
- 파일 읽기
- 응답 전송 (파일 다운로드)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;파일 삭제&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;csharp&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;// Controller
[HttpPost]
public ActionResult DeleteTemporaryFile(string fileUrl)
{
    // fileUrl = &quot;/Content/Present/file.xlsx&quot; (가상 경로)
    
    // 가상 경로를 물리 경로로 변환
    string physicalPath = Server.MapPath(&quot;~&quot; + fileUrl);
    // C:\Projects\MyApp\Content\Present\file.xlsx
    
    // 물리 경로로 삭제
    if (File.Exists(physicalPath))
    {
        File.Delete(physicalPath);
    }
    
    return Json(new { Success = true });
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369; font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;만약 IIS가 없다면?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;csharp&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;csharp&quot;&gt;&lt;code&gt;// 이렇게 직접 구현해야 함
public ActionResult DownloadFile(string fileName)
{
    // 가상 경로를 물리 경로로 변환
    string physicalPath = Server.MapPath($&quot;~/Content/Present/{fileName}&quot;);
    
    if (!File.Exists(physicalPath))
    {
        return HttpNotFound();
    }
    
    // 파일 읽기
    byte[] fileBytes = File.ReadAllBytes(physicalPath);
    
    // 파일 전송
    return File(fileBytes, &quot;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet&quot;, fileName);
}

// 사용
// window.location.href = &quot;/Present/DownloadFile?fileName=file.xlsx&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;비교표&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;항목&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;물리 경로&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;가상 경로&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;C:\Projects\MyApp\Content\file.xlsx&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;/Content/file.xlsx&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;사용 대상&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;파일 시스템 (C#)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;웹 서버 (브라우저)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;사용 시점&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;파일 생성/삭제/읽기&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;다운로드 링크/URL&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;환경 의존성&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;환경마다 다름&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #ef5369;&quot;&gt;모든 환경 동일&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;보안&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #ef5369;&quot;&gt;서버 구조 노출&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;안전&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;브라우저 접근&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #ef5369;&quot;&gt;불가능&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;가능&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;File.WriteAllBytes&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;가능&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #ef5369;&quot;&gt;불가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;window.location&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #ef5369;&quot;&gt;불가능&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;가능&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>C#/이모저모</category>
      <author>Red_Horse</author>
      <guid isPermaLink="true">https://red-horse.tistory.com/162</guid>
      <comments>https://red-horse.tistory.com/162#entry162comment</comments>
      <pubDate>Thu, 27 Nov 2025 00:18:06 +0900</pubDate>
    </item>
    <item>
      <title>Http -&amp;gt; Https Domain 변경</title>
      <link>https://red-horse.tistory.com/161</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;환경 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;서버&lt;/b&gt;: 리눅스 Ubuntu&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;웹서버&lt;/b&gt;: Nginx (리버스 프록시)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;도메인&lt;/b&gt;: 가비아에서 구매한 도메인&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;네트워크&lt;/b&gt;: iptime 공유기 환경&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;SSL&lt;/b&gt;: Let's Encrypt 무료 인증서&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;목표&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;https://jenkins.[도메인] - 브라우저 경고 없는 안전한 접속&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;https://fitnesspt.[도메인] - 브라우저 경고 없는 안전한 접속&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;준비 단계: certbot 설치&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761322079965&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt update 
sudo apt install certbot
sudo apt install python3-certbot-nginx # Nginx와 acme챌린지를 위한 플러그인
sudo apt install python3-certbot-apache # Apache와 acme챌린지를 위한 플러그인&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1단계: 도메인 DNS 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;가비아 관리콘솔 설정&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;makefile&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;호스트명: jenkins
레코드타입: A
값: [서버공인IP]

호스트명: fitnesspt  
레코드타입: A
값: [서버공인IP]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;주요 이슈와 해결&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;문제&lt;/b&gt;: 도메인 접속 시 공유기 관리페이지로 리다이렉트 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;원인&lt;/b&gt;: 80포트 포트포워딩 누락&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;해결&lt;/b&gt;: iptime 포트 포워딩 규칙 추가&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjmgMr/dJMb9Nu23RV/fy8skQpigOqjMQ8CWu6OlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjmgMr/dJMb9Nu23RV/fy8skQpigOqjMQ8CWu6OlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjmgMr/dJMb9Nu23RV/fy8skQpigOqjMQ8CWu6OlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjmgMr%2FdJMb9Nu23RV%2Ffy8skQpigOqjMQ8CWu6OlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1066&quot; height=&quot;148&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3단계: Nginx 리버스 프록시 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Jenkins 설정 (/etc/nginx/sites-available/jenkins)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;nginx&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;server {
    listen 80;
    server_name jenkins.[도메인];

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host jenkins.[도메인];
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # Jenkins 쿠키 도메인 수정 (중요!)
        proxy_cookie_domain 127.0.0.1 jenkins.[도메인];
        proxy_cookie_domain localhost jenkins.[도메인];
        
        proxy_redirect http://127.0.0.1:8080/ http://jenkins.[도메인]/;
        proxy_redirect http://localhost:8080/ http://jenkins.[도메인]/;
        
        proxy_buffering off;
        proxy_request_buffering off;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;주요 이슈와 해결&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;문제&lt;/b&gt;: Jenkins 접속 시 /login/login.cgi로 리다이렉트, &quot;Not Found&quot; 오류 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;원인&lt;/b&gt;: Jenkins의 쿠키와 세션이 새 도메인에서 작동하지 않음 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;해결&lt;/b&gt;: proxy_cookie_domain 설정 추가&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;nginx 설정 활성화&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;awk&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;sudo ln -s /etc/nginx/sites-available/jenkins /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;sites-available 위치에는 설정 파일만 있고 실제 실행되는건 sites-enable에서 연결되어 있는 파일을 읽어서 실행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnJLKw/dJMb9Nu23VW/3pjdBKu4HTTS15RzaqeN20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnJLKw/dJMb9Nu23VW/3pjdBKu4HTTS15RzaqeN20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnJLKw/dJMb9Nu23VW/3pjdBKu4HTTS15RzaqeN20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnJLKw%2FdJMb9Nu23VW%2F3pjdBKu4HTTS15RzaqeN20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;996&quot; height=&quot;194&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;4단계: Let's Encrypt SSL 인증서 발급&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;acme-challenge 경로 설정&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;nginx파일에 다음 내용 추가(jenkins, fitnesspt)&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;nginx&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;location ^~ /.well-known/acme-challenge/ {
    root /var/www/html;
    try_files $uri =404;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;웹 루트 디렉토리 준비(SSL 인증서 발급시 도메인 소유권을 확인하기 위해 사용할 경로)&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;groovy&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;sudo mkdir -p /var/www/html/.well-known/acme-challenge
sudo chown -R www-data:www-data /var/www/html
sudo chmod -R 755 /var/www/html&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;주요 이슈와 해결&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;문제&lt;/b&gt;: SSL 인증서 발급 시 &quot;Timeout during connect&quot; 오류 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;원인&lt;/b&gt;: iptime에서 해외 IP 접근 차단 설정 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;해결&lt;/b&gt;: 인증서 발급 시에만 임시로 해외 IP 차단 해제&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;SSL 인증서 발급&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 해외 IP 차단 임시 해제 후
sudo certbot certonly --nginx --cert-name [저장될 이름] -w /var/www/html -d jenkins.[도메인] -d fitnesspt.[도메인]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;5단계: HTTPS 최종 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Jenkins HTTPS 설정&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;nginx&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;# HTTP &amp;rarr; HTTPS 리다이렉트
server {
    listen 80;
    server_name jenkins.[도메인];
    return 301 https://$server_name$request_uri;
}

# HTTPS 설정
server {
    listen 443 ssl http2;
    server_name jenkins.[도메인];

    ssl_certificate /etc/letsencrypt/live/[인증서이름]/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/[인증서이름]/privkey.pem;

    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host jenkins.[도메인];
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-Port 443;
        
        proxy_cookie_domain 127.0.0.1 jenkins.[도메인];
        proxy_cookie_domain localhost jenkins.[도메인];
        
        proxy_redirect http://127.0.0.1:8080/ https://jenkins.[도메인]/;
        proxy_redirect http://localhost:8080/ https://jenkins.[도메인]/;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;자동 갱신 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;sudo systemctl status certbot.timer
sudo certbot renew --dry-run&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1678&quot; data-origin-height=&quot;196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kzjfN/dJMb9QrMahR/fmHAl6z8J8m10n4YiM2cm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kzjfN/dJMb9QrMahR/fmHAl6z8J8m10n4YiM2cm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kzjfN/dJMb9QrMahR/fmHAl6z8J8m10n4YiM2cm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkzjfN%2FdJMb9QrMahR%2FfmHAl6z8J8m10n4YiM2cm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1678&quot; height=&quot;196&quot; data-origin-width=&quot;1678&quot; data-origin-height=&quot;196&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>ZeroBase/Infra</category>
      <author>Red_Horse</author>
      <guid isPermaLink="true">https://red-horse.tistory.com/161</guid>
      <comments>https://red-horse.tistory.com/161#entry161comment</comments>
      <pubDate>Sat, 25 Oct 2025 01:06:50 +0900</pubDate>
    </item>
    <item>
      <title>CSS Position - 요소 배치의 모든 것</title>
      <link>https://red-horse.tistory.com/160</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Position&lt;/b&gt;은 HTML 요소를 원하는 위치에 배치하기 위한 CSS 속성입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;top&lt;/b&gt;, &lt;b&gt;bottom&lt;/b&gt;, &lt;b&gt;left&lt;/b&gt;, &lt;b&gt;right&lt;/b&gt; 속성과 함께 사용하여 정확한 위치를 지정할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1. 부모-자식 관계 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;기본 원칙&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;부모 요소: position: relative
자식 요소: position: absolute

&amp;rarr; 자식 요소는 부모를 기준으로 배치됨
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;CSS
.parent {
    position: relative;  /* 기준점 설정 */
    width: 600px;
    height: 300px;
    background-color: dodgerblue;
}

.child {
    position: absolute;  /* 부모 기준 절대 위치 */
    width: 200px;
    height: 100px;
    right: 0;    /* 부모의 우측에 붙음 */
    bottom: 0;   /* 부모의 하단에 붙음 */
    background-color: crimson;
}


HTML
    &amp;lt;div class=&quot;parent&quot;&amp;gt;
        &amp;lt;div class=&quot;child&quot;&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dPrERV/dJMb8WrUgrj/bwpTGA2HMG8MvrShAXbDQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dPrERV/dJMb8WrUgrj/bwpTGA2HMG8MvrShAXbDQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dPrERV/dJMb8WrUgrj/bwpTGA2HMG8MvrShAXbDQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdPrERV%2FdJMb8WrUgrj%2FbwpTGA2HMG8MvrShAXbDQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1212&quot; height=&quot;606&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;핵심 포인트:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;position을 선언해야 top, bottom, left, right 속성 사용 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;부모의 relative가 없으면 자식은 브라우저 전체를 기준으로 배치됨&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2. 원하는 위치에 정렬&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;4가지 기본 정렬&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;CSS
.container{
	position: relative;
	width: 500px;
	height: 500px;
	border: 1px solid #000;
}

/* 좌측 상단 */
.box1{
	position: absolute;
	background-color: dodgerblue;
	width: 100px;
	height: 100px;
}

/* 우측 상단 */
.box2{
	position: absolute;
	background-color: gold;
	width: 100px;
	height: 100px;
	right: 0;
}

/* 좌측 하단 */
.box3{
	position: absolute;
	background-color: #cc4f4f;
	width: 100px;
	height: 100px;
	bottom: 0;
}

/* 우측 하단 */
.box4{
	position: absolute;
	background-color: purple;
	width: 100px;
	height: 100px;
	right: 0;
	bottom: 0;
}

HTML
    &amp;lt;div class=&quot;container&quot;&amp;gt;
        &amp;lt;div class=&quot;box1&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;box2&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;box3&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;box4&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;1008&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csI9Rw/dJMb9XYDq5g/eYUZIYt3z4J5lIV8H5phU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csI9Rw/dJMb9XYDq5g/eYUZIYt3z4J5lIV8H5phU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csI9Rw/dJMb9XYDq5g/eYUZIYt3z4J5lIV8H5phU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsI9Rw%2FdJMb9XYDq5g%2FeYUZIYt3z4J5lIV8H5phU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1014&quot; height=&quot;1008&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;1008&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3. 중앙 정렬 (완벽한 중앙)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;CSS
.center-container{
	position: relative;
	width: 500px;
	height: 500px;
	border: 1px solid #000;
}
        
.box-center{
	background-color: dodgerblue;
	width: 100px;
	height: 100px;
	position: absolute;

	/* 1단계: 좌측 상단을 중앙으로 */
	left: 50%;
	top: 50%;
    
	/* 2단계: 자신의 크기만큼 역방향 이동 */
	transform: translate(-50%, -50%);
}

HTML
    &amp;lt;div class=&quot;center-container&quot;&amp;gt;
        &amp;lt;div class=&quot;box-center&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;1014&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/be8BQw/dJMb9NV5uC6/Tjr4kGvif6m2qfzdrTEiCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/be8BQw/dJMb9NV5uC6/Tjr4kGvif6m2qfzdrTEiCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/be8BQw/dJMb9NV5uC6/Tjr4kGvif6m2qfzdrTEiCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbe8BQw%2FdJMb9NV5uC6%2FTjr4kGvif6m2qfzdrTEiCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1010&quot; height=&quot;1014&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;1014&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;핵심 포인트:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;left: 50%, top: 50%: 요소의 &lt;b&gt;좌측 상단&lt;/b&gt;을 중앙에 배치&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;transform: translate(-50%, -50%): 요소를 &lt;b&gt;자신의 크기 절반&lt;/b&gt;만큼 역방향 이동&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;브라우저 크기와 관계없이 항상 중앙 유지&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;4. 부모 요소 밖으로 배치&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위치 계산 방식&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;핵심: %는 부모(relative)의 크기를 기준으로 계산

부모 크기: 300px &amp;times; 200px
top: 100% &amp;rarr; 200px 아래
left: 100% &amp;rarr; 300px 오른쪽
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;4가지 밖으로 배치&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;CSS
/* 하단 중앙 밖 */
.box-out1{
	background-color: dodgerblue;
	width: 100px;
	height: 100px;
	position: absolute;
	top: 100%;
	left: 50%;
	transform: translate(-50%, 0%);
}

/* 우측 하단 밖 */
.box-out2{
	background-color: gold;
	width: 100px;
	height: 100px;
	position: absolute;
	top: 100%;
	left: 100%;
	transform: translate(-100%, 0%);
}

/* 우측 상단 밖 */
.box-out3{
	background-color: purple;
	width: 100px;
	height: 100px;
	position: absolute;
	left: 100%;
}

HTML
    &amp;lt;div class=&quot;out-container&quot;&amp;gt;
        &amp;lt;div class=&quot;box-out1&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;box-out2&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;box-out3&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ucqFr/dJMb9PGngRj/J3GDvkZstuI1ViL9SXKzk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ucqFr/dJMb9PGngRj/J3GDvkZstuI1ViL9SXKzk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ucqFr/dJMb9PGngRj/J3GDvkZstuI1ViL9SXKzk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FucqFr%2FdJMb9PGngRj%2FJ3GDvkZstuI1ViL9SXKzk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;814&quot; height=&quot;602&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;602&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;주의사항&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;자식 요소의 크기는 자동으로 고려되지 않습니다!&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;/* 잘못된 예 */
.box-out {
    top: 100%;
    left: 50%;
    /* transform 없음 &amp;rarr; 박스가 중앙에서 벗어남 */
}

/* 올바른 예 */
.box-out {
    top: 100%;
    left: 50%;
    transform: translate(-50%, 0);  /* 필수! */
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Transform의 매개변수 &lt;/b&gt;&lt;b&gt;translate(X, Y)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;transform: translate(-50%, -50%);
                     &amp;uarr;      &amp;uarr;
                   좌우    상하
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;값의 의미:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;양수(+)&lt;/b&gt;: 오른쪽, 아래로 이동&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;음수(-)&lt;/b&gt;: 왼쪽, 위로 이동&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;%&lt;/b&gt;: 자신의 크기 기준&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;예시:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;transform: translate(-50%, 0);    /* 왼쪽으로 절반, 상하 이동 없음 */
transform: translate(0, -100%);   /* 좌우 이동 없음, 위로 전체 */
transform: translate(-100%, -100%); /* 왼쪽 위로 완전히 이동 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;실전 활용 예시&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1. 모달 창 중앙 배치&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.modal {
    position: fixed;     /* 화면 기준 고정 */
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    z-index: 1000;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2. 툴팁 하단 중앙 표시&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.tooltip-container {
    position: relative;
}

.tooltip {
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translate(-50%, 5px);  /* 5px 간격 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3. 배지 우측 상단 배치&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.avatar {
    position: relative;
}

.badge {
    position: absolute;
    top: 0;
    right: 0;
    transform: translate(50%, -50%);  /* 반만 삐져나오게 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;핵심 정리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;속성&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;역할&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;기준점&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;relative&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;기준점 역할&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;원래 위치&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;absolute&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;절대 위치 지정&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;부모 relative&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;fixed&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;화면 고정&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;브라우저 화면&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;% 계산 기준&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;top, bottom, left, right의 % &amp;rarr; &lt;b&gt;부모(relative) 크기 기준&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;transform: translate()의 % &amp;rarr; &lt;b&gt;자신의 크기 기준&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #1b711d;&quot;&gt;Transform이 필요한 이유&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;position만으로는 요소의 좌측 상단 모서리만 제어 가능&lt;/b&gt; &amp;rarr; transform으로 요소 자체를 이동시켜 정확한 위치 조정&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>ZeroBase/웹 디자인</category>
      <author>Red_Horse</author>
      <guid isPermaLink="true">https://red-horse.tistory.com/160</guid>
      <comments>https://red-horse.tistory.com/160#entry160comment</comments>
      <pubDate>Fri, 17 Oct 2025 23:44:57 +0900</pubDate>
    </item>
    <item>
      <title>Jenkins CI/CD 구축</title>
      <link>https://red-horse.tistory.com/159</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;이 가이드는 Ubuntu 22.04 환경에서 Jenkins를 설치하고, .NET 프로젝트의 CI/CD 파이프라인을 구축하는 전 과정을 다룹니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1. 환경 준비 및 기본 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1.1 시스템 업데이트&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1.2 SSH 서버 설정 (원격 접속용)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;# SSH 서버 설치
sudo apt install openssh-server -y

# SSH 서비스 시작 및 자동 시작 설정
sudo systemctl start ssh
sudo systemctl enable ssh

# SSH 서비스 상태 확인
sudo systemctl status ssh&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1.3 방화벽 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# UFW 방화벽 활성화
sudo ufw enable

# SSH 포트 허용 (기본 22번 포트)
sudo ufw allow 22/tcp

# Jenkins 웹 포트 허용 (8080)
sudo ufw allow 8080/tcp

# 애플리케이션 포트 허용 (예: 5117)
sudo ufw allow 5117/tcp

# 방화벽 상태 확인
sudo ufw status&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;⋇iptime에서 지원하는 DDNS 기능을 사용할 경우 포트포워딩을 별도로 설정해 주어야합니다.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2. Jenkins 설치 및 초기 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2.1 Java 설치 (Jenkins 필수 요구사항)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;jboss-cli&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;# OpenJDK 17 설치
sudo apt install openjdk-17-jdk -y

# Java 버전 확인
java -version

# JAVA_HOME 환경변수 설정
echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' &amp;gt;&amp;gt; ~/.bashrc
echo 'export PATH=$PATH:$JAVA_HOME/bin' &amp;gt;&amp;gt; ~/.bashrc
source ~/.bashrc&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2.2 Jenkins 저장소 추가 및 설치&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;단계 1: Jenkins GPG 키 다운로드 및 추가&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;awk&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;# Jenkins 공식 보안 키를 다운로드하여 시스템에 추가
curl -fsSL https://pkg.jenkins.io/debian/jenkins.io-2023.key | sudo tee /usr/share/keyrings/jenkins-keyring.asc &amp;gt; /dev/null&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GPG 키?&lt;/b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #666666; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;패키지가 진짜 Jenkins에서 만든 것인지 확인하는 디지털 서명입니다. 가짜 패키지 설치를 방지하기 위함.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;명령어 설명:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;curl -fsSL: Jenkins 웹사이트에서 보안 키 파일을 다운로드&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;sudo tee: 다운로드한 키를 시스템 보안 키 저장소에 저장&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;/usr/share/keyrings/: Ubuntu에서 패키지 보안 키를 저장하는 표준 위치&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;단계 2: Jenkins 저장소를 시스템에 추가&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;php&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;# Jenkins 패키지 저장소를 APT 소스 목록에 추가
echo &quot;deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian binary/&quot; | sudo tee /etc/apt/sources.list.d/jenkins.list &amp;gt; /dev/null&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;단계 3: 패키지 목록 업데이트&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;nginx&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;# 새로 추가된 Jenkins 저장소를 포함하여 패키지 목록 업데이트
sudo apt update&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;단계 4: Jenkins 설치&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;mipsasm&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;# Jenkins 패키지 설치
sudo apt install jenkins -y&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;단계 5: Jenkins 서비스 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;# Jenkins 서비스 시작
sudo systemctl start jenkins

# 시스템 부팅 시 Jenkins 자동 시작 설정
sudo systemctl enable jenkins

# Jenkins 서비스 상태 확인
sudo systemctl status jenkins&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;설치 성공 확인:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;sudo systemctl status jenkins 명령어 실행 시 &lt;b&gt;&lt;span style=&quot;background-color: #000000; color: #ffffff;&quot;&gt;Active: active (running)&lt;/span&gt;&lt;/b&gt; 상태여야 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;만약 실패했다면 sudo journalctl -u jenkins로 로그 확인&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2.3 Jenkins 초기 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;crystal&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;# Jenkins 초기 관리자 비밀번호 확인
sudo cat /var/lib/jenkins/secrets/initialAdminPassword&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;웹 브라우저에서 http://[서버IP]:8080에 접속하여:&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;위에서 확인한 초기 비밀번호 입력&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;&quot;Install suggested plugins&quot;&lt;/b&gt; 선택 (권장)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;관리자 계정 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Jenkins URL 설정&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3. .NET 개발 환경 구축&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3.1 .NET SDK 설치&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;# Microsoft 패키지 저장소 추가
wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb

# .NET SDK 설치
sudo apt update
sudo apt install -y dotnet-sdk-8.0

# 설치 확인
dotnet --version&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3.2 Git 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# Git 설치 (보통 이미 설치되어 있음)
sudo apt install git -y

# Git 사용자 정보 설정 (사용자)
sudo -u jenkins git config --global user.name &quot;사용자 이름&quot;
sudo -u jenkins git config --global user.email &quot;이메일&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;4. systemd 서비스 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;4.1 애플리케이션용 systemd 서비스 생성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;#.Net 서비스 파일 생성
sudo nano /etc/systemd/system/{프로젝트명}.service&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;서비스 파일 내용&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[Unit]
Description=&quot;프로젝트명&quot; Service
After=network.target

[Service]
Type=simple
User=jenkins
Group=jenkins
#WorkingDirectory=/var/lib/jenkins/deployments/프로젝트명
WorkingDirectory=&quot;프로젝트 빌드파일 위치&quot;

# 환경변수
Environment=ASPNETCORE_ENVIRONMENT=Development
Environment=ASPNETCORE_URLS=http://0.0.0.0:5117

#ExecStart=/usr/bin/dotnet 프로젝트.dll
ExecStart=&quot;프로젝트 실행파일(.dll)&quot;

Restart=always
RestartSec=5
KillMode=mixed
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;4.2 환경변수 파일 생성 (민감한 정보 보호)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;awk&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;# 환경변수 디렉토리 생성
sudo mkdir -p /etc/fitness-api

# 환경변수 파일 생성
sudo nano /etc/fitness-api/environment&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;환경변수 파일 내용 예시:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 데이터베이스 연결 문자열
ConnectionStrings__DefaultConnection=Server=localhost;Database=FitnessDB;User=dbuser;Password=your_secure_password;

# JWT 설정
JwtSettings__SecretKey=your_jwt_secret_key_here
JwtSettings__Issuer=Issuer
JwtSettings__Audience=Audience
JwtSettings__ExpiryMinutes=1440

# API 키들
ExternalApi__GoogleMaps=your_google_maps_api_key
ExternalApi__Payment=your_payment_gateway_key

# 로깅 설정
Serilog__MinimumLevel=Information&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;4.3 서비스 권한 설정 및 등록&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 환경변수 파일 권한 설정 (보안 강화)
sudo chown root:jenkins /etc/프로젝트 경로/environment
sudo chmod 640 /etc/프로젝트 경로/environment

# 배포 디렉토리 생성 및 권한 설정
sudo mkdir -p /var/lib/jenkins/deployments/프로젝트 폴더
sudo chown -R jenkins:jenkins /var/lib/jenkins/deployments

# systemd 서비스 등록
sudo systemctl daemon-reload
sudo systemctl enable 프로젝트명.service&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;5. Jenkins Job 생성 및 배포 스크립트&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;5.1 Jenkins Job 생성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;사전 준비: Git 인증 정보 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Private repository이거나 Push 권한이 필요한 경우 반드시 인증 정보를 설정해야 합니다.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;GitHub Personal Access Token 사용 (권장)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;GitHub에서 토큰 생성:&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;GitHub &amp;rarr; Settings &amp;rarr; Developer settings &amp;rarr; Personal access tokens &amp;rarr; Tokens (classic)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&quot;Generate new token&quot; 클릭&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;권한 선택: repo (전체 저장소 접근) 체크&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;토큰 복사 (&lt;b&gt;한 번만 보여줌으로 별도의 메모장에 작성해두고 사용하길 권장합니다.&lt;/b&gt;)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Jenkins에 인증 정보 추가:&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Jenkins 관리 &amp;rarr; Manage Credentials &amp;rarr; System &amp;rarr; Global credentials&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&quot;Add Credentials&quot; 클릭&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Kind: &lt;b&gt;&quot;Username with password&quot;&lt;/b&gt; 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Username: GitHub 사용자명&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Password: 위에서 생성한 &lt;b&gt;Personal Access Token&lt;/b&gt; 입력&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;ID: github-credentials (기억하기 쉬운 이름)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Description: GitHub Personal Access Token&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Jenkins Job 생성 단계:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Jenkins 대시보드 &amp;rarr; &lt;b&gt;&quot;새 항목&quot;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;항목 이름: FitnessPT_API_Deploy&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;&quot;Freestyle project&quot;&lt;/b&gt; 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;&quot;소스 코드 관리&quot;&lt;/b&gt; &amp;rarr; Git 선택&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Repository URL&lt;/b&gt;:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;HTTPS 방식: 깃 레포주소&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Credentials&lt;/b&gt;: 위에서 생성한 인증 정보 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Branch&lt;/b&gt;: */main&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;연결 테스트:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Repository URL과 Credentials 설정 후 &lt;b&gt;&quot;Test Connection&quot;&lt;/b&gt; 버튼 클릭&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;성공 시: &quot;Credentials verified for user [username], repository [repo-name]&quot;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;실패 시: 인증 정보나 URL 확인 필요&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;5.2 배포 스크립트 생성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Jenkins Execute Shell에 입력할 스크립트&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash

echo &quot;  자동배포를 시작합니다...&quot;

# ================================================================
#   프로젝트 설정
# ================================================================
PROJECT_NAME=&quot;프로젝트명&quot;
SERVICE_NAME=&quot;.Service 이름&quot;
APP_PORT=&quot;5117&quot;
BUILD_CONFIG=&quot;Release&quot;
DEPLOY_PATH=&quot;/var/lib/jenkins/deployments/$PROJECT_NAME&quot;

# 색상 정의 (가독성 향상)
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'

# ================================================================
#  ️ 배포 프로세스
# ================================================================

echo -e &quot;${YELLOW}  배포 설정:${NC}&quot;
echo &quot;  &amp;bull; 프로젝트: $PROJECT_NAME&quot;
echo &quot;  &amp;bull; 서비스명: $SERVICE_NAME&quot;
echo &quot;  &amp;bull; 포트: $APP_PORT&quot;
echo &quot;  &amp;bull; 빌드 구성: $BUILD_CONFIG&quot;
echo &quot;  &amp;bull; 배포 경로: $DEPLOY_PATH&quot;
echo &quot;&quot;

# 1. 기존 서비스 중지
echo -e &quot;${YELLOW}  기존 서비스 중지 중...${NC}&quot;
sudo systemctl stop $SERVICE_NAME || echo &quot;서비스가 실행 중이 아닙니다.&quot;

# 2. 이전 배포 파일 백업 (선택사항)
if [ -d &quot;$DEPLOY_PATH&quot; ]; then
    echo -e &quot;${YELLOW}  이전 배포 파일 백업 중...${NC}&quot;
    sudo cp -r $DEPLOY_PATH ${DEPLOY_PATH}_backup_$(date +%Y%m%d_%H%M%S) || true
fi

# 3. 배포 디렉토리 생성/정리
echo -e &quot;${YELLOW}  배포 디렉토리 준비 중...${NC}&quot;
sudo mkdir -p $DEPLOY_PATH
sudo rm -rf $DEPLOY_PATH/*

# 4. .NET 프로젝트 빌드
echo -e &quot;${YELLOW}  .NET 프로젝트 빌드 중...${NC}&quot;
dotnet clean
dotnet restore
dotnet publish -c $BUILD_CONFIG -o $DEPLOY_PATH --no-restore

if [ $? -ne 0 ]; then
    echo -e &quot;${RED}❌ 빌드 실패!${NC}&quot;
    exit 1
fi

# 5. 파일 권한 설정
echo -e &quot;${YELLOW}  파일 권한 설정 중...${NC}&quot;
sudo chown -R jenkins:jenkins $DEPLOY_PATH
sudo chmod +x $DEPLOY_PATH/$PROJECT_NAME

# 6. 서비스 시작
echo -e &quot;${YELLOW}  서비스 시작 중...${NC}&quot;
sudo systemctl start $SERVICE_NAME

# 7. 서비스 상태 확인
sleep 5
SERVICE_STATUS=$(sudo systemctl is-active $SERVICE_NAME)

if [ &quot;$SERVICE_STATUS&quot; = &quot;active&quot; ]; then
    echo -e &quot;${GREEN}✅ 서비스가 성공적으로 시작되었습니다!${NC}&quot;
    
    # 헬스체크
    echo -e &quot;${YELLOW}  헬스체크 진행 중...${NC}&quot;
    sleep 10
    
    if curl -f http://localhost:$APP_PORT/health &amp;gt; /dev/null 2&amp;gt;&amp;amp;1; then
        echo -e &quot;${GREEN}✅ 헬스체크 성공!${NC}&quot;
    else
        echo -e &quot;${YELLOW}⚠️  헬스체크 실패 (서비스는 실행 중)${NC}&quot;
    fi
    
    # 최종 결과 출력
    echo &quot;&quot;
    echo &quot;━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━&quot;
    echo -e &quot;${GREEN}  배포 완료!${NC}&quot;
    echo &quot;━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━&quot;
    echo &quot;프로젝트: $PROJECT_NAME&quot;
    echo &quot;포트: $APP_PORT&quot;
    echo &quot;URL: http://localhost:$APP_PORT&quot;
    echo &quot;배포 위치: $DEPLOY_PATH&quot;
    echo &quot;로그 확인: sudo journalctl -u $SERVICE_NAME -f&quot;
    echo &quot;━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━&quot;
    echo &quot;&quot;
    
    # 실행 중인 프로세스 정보
    echo &quot;실행 중인 프로세스:&quot;
    ps aux | grep &quot;$PROJECT_NAME&quot; | grep -v grep || echo &quot;프로세스 정보 조회 실패&quot;
    
else
    echo -e &quot;${RED}❌ 서비스 시작 실패!${NC}&quot;
    echo &quot;서비스 로그 확인:&quot;
    sudo journalctl -u $SERVICE_NAME --no-pager -l
    exit 1
fi

echo -e &quot;${GREEN}✅ 모든 작업 완료!${NC}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;빌드 스크립트의 경우 프로젝트의 설정마다 일부 차이가 있을 수 있습니다.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;기본적인 .NET 프로젝트 기준으로 작성된 스크립트로서 빌드 오류가 발생시 프로젝트에 맞추어 수정이 필요할 수 있습니다.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;6. 보안 설정 및 환경변수 관리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;6.1 Jenkins 사용자 sudo 권한 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;nginx&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;# Jenkins 사용자에게 특정 명령어만 sudo 권한 부여
sudo visudo&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;다음 내용 추가&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# Jenkins가 서비스 관리만 할 수 있도록 제한
jenkins ALL=(ALL) NOPASSWD: /bin/systemctl start 프로젝트명, /bin/systemctl stop 프로젝트명, /bin/systemctl restart 프로젝트명, /bin/systemctl status 프로젝트명, /bin/journalctl -u 프로젝트명*&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;6.2 환경변수 보안 관리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 환경변수 파일 암호화 (선택사항)
sudo apt install gpg -y

# 환경변수 파일 백업
sudo cp /etc/프로젝트명/environment /etc/프로젝트명/environment.backup

# 권한 재확인
sudo ls -la /etc/프로젝트명/&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;7. 트러블슈팅 및 모니터링&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;7.1 로그 모니터링&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# Jenkins 로그 확인
sudo journalctl -u jenkins -f

# 애플리케이션 로그 확인
sudo journalctl -u 프로젝트명 -f

# 시스템 로그 확인
sudo tail -f /var/log/syslog&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;7.2 포트 및 프로세스 확인&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 포트 사용 현황 확인
sudo netstat -tulpn | grep :5117

# 실행 중인 프로세스 확인
ps aux | grep 프로젝트명

# 서비스 상태 확인
sudo systemctl status 프로젝트명&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;7.3 일반적인 문제 해결&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;문제 1: 빌드 실패&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;# .NET SDK 버전 확인
dotnet --version

# 프로젝트 의존성 복원
dotnet restore --force

# 캐시 정리
dotnet clean&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;문제 2: 서비스 시작 실패&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 서비스 로그 상세 확인
sudo journalctl -u 프로젝트명 -n 50

# 환경변수 파일 문법 검사
sudo systemd-analyze verify /etc/systemd/system/프로젝트명.service

# 권한 문제 해결
sudo chown -R jenkins:jenkins /var/lib/jenkins/deployments&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;문제 3: 포트 충돌&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 포트를 사용하는 프로세스 찾기
sudo lsof -i :{프로젝트 내부 포트}

# 해당 프로세스 종료
sudo kill -9 [PID]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;마무리 및 추가 개선사항&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;완성된 CI/CD 파이프라인의 특징&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;자동화&lt;/b&gt;: Git 푸시 &amp;rarr; 자동 빌드 &amp;rarr; 자동 배포&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;보안&lt;/b&gt;: 환경변수로 민감 정보 보호&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;안정성&lt;/b&gt;: systemd 서비스로 프로세스 관리&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;모니터링&lt;/b&gt;: 상세한 로깅 및 헬스체크&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;확장성&lt;/b&gt;: 다른 프로젝트에도 쉽게 적용 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;세팅 이후 내용을 정리한 가이드로서 진행중 이슈가 있을 수 있습니다.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;도움이 필요한 부분은 댓글을 남겨주시면 내용 수정 및 추가 가이드 안내 드리겠습니다.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>CICD/Jenkins</category>
      <author>Red_Horse</author>
      <guid isPermaLink="true">https://red-horse.tistory.com/159</guid>
      <comments>https://red-horse.tistory.com/159#entry159comment</comments>
      <pubDate>Fri, 17 Oct 2025 00:43:59 +0900</pubDate>
    </item>
  </channel>
</rss>