<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Dev.POST</title>
        <link>https://velog.io/</link>
        <description>🔥 코드 과정 중 자연스레 쌓인 경험과 지식을 기술 블로그로 작성해줍니다.</description>
        <lastBuildDate>Thu, 01 Aug 2024 06:55:38 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Dev.POST</title>
            <url>https://velog.velcdn.com/images/dev_post/profile/2fe58526-041d-47e9-8d17-3e9a9898b0b8/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Dev.POST. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_post" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Open Api Generator를 Android 프로젝트에 적용하기]]></title>
            <link>https://velog.io/@dev_post/Open-Api-Generator%EB%A1%9C-%EB%A9%80%ED%8B%B0-%EB%AA%A8%EB%93%88-Android-SDK-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_post/Open-Api-Generator%EB%A1%9C-%EB%A9%80%ED%8B%B0-%EB%AA%A8%EB%93%88-Android-SDK-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 01 Aug 2024 06:55:38 GMT</pubDate>
            <description><![CDATA[<p>개발을 하다 보면, API DTO에 변경에 어려움을 겪을 때가 있습니다. 우리가 흔히 사용하게 되는 REST API의 통신을 좀 더 간편하고 효율적으로 할 수 있는 방법 중 하나가 바로 Open API Generator입니다. 오늘은 OpenAPI Generator를 Android 프로젝트에 어떻게 적용할 수 있는지에 대해 이야기해보겠습니다.</p>
<h2 id="sdk에-openapi-generator-적용하기">SDK에 OpenAPI Generator 적용하기</h2>
<p>Android 프로젝트에 OpenAPI Generator를 적용하는 방법을 살펴볼게요. 해당 작업을 진행하면서 무엇을 했는지, 어떤 기준으로 작업을 진행했는지 공유하려 합니다.</p>
<h3 id="openapi-generator-란">OpenAPI Generator 란?</h3>
<p><img src="https://velog.velcdn.com/images/dev_post/post/d3e08065-1679-4855-acda-4c76d5633e1a/image.png" alt=""></p>
<p>Open Api Generator 에 대해 들어보신 적 있으신가요? Open API Generator는 OpenAPI Specification(OAS) 문서를 기반으로 클라이언트 SDK, 서버 스텁, 문서를 자동으로 생성해주는 도구입니다. 이를 통해 API 정의서만으로 유지보수 가능한 코드 생성과 통일된 API 요청/응답 처리를 가능하게 합니다.</p>
<p>오늘은 이를 안드로이드 프로젝트에 적용해볼겁니다!</p>
<h2 id="적용-과정">적용 과정</h2>
<h3 id="1-gradle-설정">1. Gradle 설정</h3>
<p>Open Api Generator를 프로젝트에 추가하려면, 먼저 build.gradle.kts 파일에서 필요한 플러그인을 추가해야 합니다.</p>
<pre><code class="language-kotlin">plugins {
    id(&quot;org.openapi.generator&quot;) version &quot;6.6.0&quot;
    id(&quot;de.undercouch.download&quot;) version &quot;5.6.0&quot;
}</code></pre>
<p>위와 같이 플러그인을 추가한 후 다른 필요한 의존성들도 추가합니다.</p>
<pre><code class="language-kotlin">dependencies {
    implementation(&quot;com.squareup.retrofit2:retrofit:2.9.0&quot;)
    implementation(&quot;com.squareup.retrofit2:converter-gson:2.9.0&quot;)
    implementation(&quot;com.squareup.okhttp3:logging-interceptor:4.9.0&quot;)
}</code></pre>
<h3 id="2-swagger-파일-다운로드-설정">2. Swagger 파일 다운로드 설정</h3>
<p>Swagger 파일을 원격에서 다운로드해야 하기 때문에 이를 위해 <code>Download</code> 태스크를 사용합니다. 이를 통해 최신 스펙의 Swagger 파일을 얻을 수 있습니다. </p>
<p>아래 코드를 app 모듈 수준 <code>build.gradle</code> 에 추가해줍니다.</p>
<pre><code class="language-kotlin">tasks.register&lt;Download&gt;(&quot;downloadSwagger&quot;) {
    src(&quot;https://api.example.com/api-json&quot;) 
    dest(file(&quot;$buildDir/api-swagger.json&quot;))
    onlyIfModified(true)
    useETag(true)
}</code></pre>
<h3 id="3-api-클라이언트-코드-생성하기">3. API 클라이언트 코드 생성하기</h3>
<p>이제 Open Api Generator를 사용하여 다운로드한 Swagger 파일을 기반으로 API 클라이언트 코드를 생성하겠습니다.</p>
<p>아래 코드 역시 app 모듈 수준 <code>build.gradle</code> 에 추가해줍니다.</p>
<pre><code class="language-kotlin">tasks.register&lt;GenerateTask&gt;(&quot;generateClient&quot;) {
    dependsOn(tasks.named(&quot;downloadSwagger&quot;))
    generatorName.set(&quot;kotlin&quot;)
    inputSpec.set(&quot;$buildDir/api-swagger.json&quot;)
    outputDir.set(&quot;$buildDir/generated&quot;)
    apiPackage.set(&quot;com.example.api&quot;)
    modelPackage.set(&quot;com.example.model&quot;)
    invokerPackage.set(&quot;com.example.invoker&quot;)
    configOptions.set(
        mapOf(
            &quot;library&quot; to &quot;jvm-retrofit2&quot;,
            &quot;dateLibrary&quot; to &quot;java8&quot;,
            &quot;omitGradleWrapper&quot; to &quot;true&quot;,
            &quot;sourceFolder&quot; to &quot;src/main/java&quot;,
            &quot;useCoroutines&quot; to &quot;false&quot;
        )
    )
}</code></pre>
<p>위 코드는 Open Api Generator를 사용하여 Kotlin 언어로 API 클라이언트를 생성하는 예제입니다. 각 옵션에 대해 자세히 살펴볼까요?</p>
<ul>
<li><code>dependsOn</code>: Swagger를 다운로드를 기다립니다.</li>
<li><code>generatorName</code>: 사용할 생성기 이름을 설정합니다. 여기서는 Kotlin을 사용합니다.</li>
<li><code>inputSpec</code>: Swagger 파일의 경로를 지정합니다.</li>
<li><code>outputDir</code>: 생성된 코드가 출력될 경로를 지정합니다.</li>
<li><code>apiPackage</code>, <code>modelPackage</code>, <code>invokerPackage</code>: 각각 API, 모델, 인보커 패키지 위치를 설정합니다.</li>
<li><code>configOptions</code>: 기타 설정 옵션들을 지정합니다. 예를 들어, Retrofit2 라이브러리를 사용하고 날짜 라이브러리는 <code>java8</code>을 사용하도록 설정합니다.</li>
</ul>
<h3 id="4-코드-생성">4. 코드 생성</h3>
<p>위 세팅이 끝났다면, 만들어준 <code>generateClient</code> 명령어를 터미널에 입력하여, 코드를 자동 생성해줍니다. 
만약 코드 업데이트를 원하는 경우 아래 명령어를 동일하게 터미널에 입력해주면 됩니다. 😃</p>
<pre><code>./gradlew generateClient</code></pre><h3 id="5-코드-통합">5. 코드 통합</h3>
<p>이렇게 생성된 코드를 프로젝트에 통합하여 사용합니다. <code>ApiClient</code> 를 생성하여 사용해주면 됩니다.</p>
<pre><code class="language-kotlin">// Api 로깅 추가
val apiClient = ApiClient().apply {
    setLogger { message -&gt; println(message) }
}

// 생성된 Api 인터페이스를 객체로 제공
val exampleApi = apiClient.createService(ExampleApi::class.java)

// 자동 생성된 메소드 사용
suspend fun getExampleData() {
    val response = exampleApi.getData() // Api Call
}</code></pre>
<p>위 예제에서는 생성된 <code>ApiClient</code>를 사용하여 API를 호출하는 방법을 보여줍니다.</p>
<p>위 방식을 따르면, 프로젝트에 적용된 2개 이상의 Swagger가 있더라도, 각각의 다른 Swagger 파일을 생성하여, 생성 파일끼리의 충돌로 인한 build error 역시 예방할 수 있습니다.</p>
<h3 id="마치며">마치며</h3>
<p>Open Api Generator를 사용하면 API 클라이언트 코드를 자동으로 생성할 수 있어, 개발 생산성을 크게 향상시킬 수 있습니다. 이번 기회를 통해 여러분도 프로젝트에 이를 적용해 보세요. 생각보다 간단하고 강력한 도구입니다. 질문이 있으시다면 언제든지 댓글로 남겨주세요. 감사합니다!</p>
<blockquote>
<p>🔥 해당 포스팅은 <a href="https://devpost-download.framer.website/">Dev.POST</a> 도움을 받아 작성되었습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[파이썬으로 오목 온라인 게임 만들기]]></title>
            <link>https://velog.io/@dev_post/%ED%8C%8C%EC%9D%B4%EC%8D%AC%EC%9C%BC%EB%A1%9C-%EC%98%A4%EB%AA%A9-%EA%B2%8C%EC%9E%84-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@dev_post/%ED%8C%8C%EC%9D%B4%EC%8D%AC%EC%9C%BC%EB%A1%9C-%EC%98%A4%EB%AA%A9-%EA%B2%8C%EC%9E%84-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Tue, 30 Jul 2024 02:45:11 GMT</pubDate>
            <description><![CDATA[<p>여러분, 가끔 친구들과 함께 보드 게임을 즐기기는 하나요? 오늘은 여러분과 함께 대표적인 보드 게임 중 하나인 오목을 파이썬으로 멀티플레이를 구현해보려고 합니다. 오목이란 무엇인지, 어떻게 동작하는지 궁금하시죠? 그럼 바로 시작해볼까요!</p>
<h1 id="오목이란">오목이란?</h1>
<p><img src="https://velog.velcdn.com/images/dev_post/post/11b0b948-491f-4be6-afff-66f223e4fd28/image.png" alt=""></p>
<p>오목은 검은 돌과 하얀 돌을 번갈아가며 두어 가로, 세로, 대각선 중 한 방향으로 연속된 다섯 개의 돌을 먼저 놓으면 이기는 게임입니다. 아주 간단한 규칙이지만 깊이 있는 전략이 필요한 게임이죠. 이번에 우리는 파이썬을 활용해서 이러한 오목 게임을 구현해보겠습니다.</p>
<h2 id="1게임의-핵심-구조-이해하기">1.게임의 핵심 구조 이해하기</h2>
<h3 id="필요-패키지-설치하기">필요 패키지 설치하기</h3>
<p>본격적으로 구현에 들어가기 전에 구현에 필요한 패키지를 설치해 봅시다. 이번 포스트에서는 pygame을 이용해서 구현했습니다.</p>
<pre><code class="language-bash">$ pip install pygame</code></pre>
<p>게임을 구현하기 위해서는 먼저 게임의 주요 구조를 이해해야 합니다. 오목 게임을 위한 클래스와 메서드를 통해 어떤 방식으로 게임이 진행되는지 살펴봅시다.</p>
<h3 id="omok-클래스-정의하기">Omok 클래스 정의하기</h3>
<p>오목 게임의 핵심 로직을 담당하는 <code>Omok</code> 클래스를 만들어보겠습니다. 이 클래스는 게임 보드와 현재 플레이어를 관리하고, 플레이어의 차례에 따라 돌을 놓으며, 금수(금지된 수)와 승리 조건을 체크하는 역할을 합니다.</p>
<pre><code class="language-python">class Omok:
    def __init__(self):
        self.board = [[0] * 19 for _ in range(19)]
        self.current_player = BLACK
        self.forbidden_moves = set()
        self.directions = [(1, 0), (0, 1), (1, 1), (1, -1)]</code></pre>
<ul>
<li><code>board</code>: 19x19 크기의 게임 보드입니다. 0은 빈 곳, 1은 검은 돌, 2는 하얀 돌을 나타냅니다.</li>
<li><code>current_player</code>: 현재 차례인 플레이어를 나타냅니다. 검은 돌로 시작합니다.</li>
<li><code>forbidden_moves</code>: 금지된 수의 위치를 저장하는 셋입니다.</li>
<li><code>directions</code>: 각 방향을 나타내는 튜플 리스트입니다.</li>
</ul>
<h3 id="돌을-놓기">돌을 놓기</h3>
<p>플레이어가 돌을 놓는 동작을 구현해보겠습니다. 돌을 놓고, 금수 업데이트, 승리 체크를 진행하는 메서드입니다.</p>
<pre><code class="language-python">def play(self, row, col):
    self.board[row][col] = self.current_player
    self.update_forbidden_moves()
    if self.check_win(row, col):
        return {
            &quot;board&quot;: self.board,
            &quot;forbidden_moves&quot;: self.forbidden_moves,
            &quot;winner&quot;: self.current_player,
            &quot;turn&quot;: -1,
        }

    self.current_player = 3 - self.current_player
    return {
        &quot;board&quot;: self.board,
        &quot;forbidden_moves&quot;: self.forbidden_moves,
        &quot;winner&quot;: -1,
        &quot;turn&quot;: self.current_player,
    }</code></pre>
<p>위의 메서드는 주어진 위치에 돌을 놓고, 금수와 승리 여부를 체크합니다. 승리 시에는 승리 정보를 리턴하고, 그렇지 않으면 다음 턴을 리턴합니다.</p>
<h3 id="승리-조건-체크">승리 조건 체크</h3>
<p>돌을 5개 연속으로 놓았는지를 체크하는 메서드입니다. 4가지 방향을 모두 체크하여 승리 조건을 만족하는지 확인합니다.</p>
<pre><code class="language-python">def check_win(self, row, col):
    for d in self.directions:
        count = 1
        for i in range(1, 6):
            r, c = row + d[0] * i, col + d[1] * i
            if 0 &lt;= r &lt; 19 and 0 &lt;= c &lt; 19 and self.board[r][c] == self.current_player:
                count += 1
            else:
                break

        for i in range(1, 6):
            r, c = row - d[0] * i, col - d[1] * i
            if 0 &lt;= r &lt; 19 and 0 &lt;= c &lt; 19 and self.board[r][c] == self.current_player:
                count += 1
            else:
                break

        if count == 5:
            return True
    return False</code></pre>
<p>승리 조건을 체크하는 방식은 각 방향으로 돌을 차례대로 확인하며 5개 연속인 경우를 찾는 것입니다.</p>
<h2 id="2네트워크-기능-추가하기">2.네트워크 기능 추가하기</h2>
<p>이제 오목 게임을 혼자가 아닌 친구와 같이 온라인으로 즐길 수 있도록 네트워크 기능을 추가해보겠습니다. 이를 위해 서버와 클라이언트를 구현할 것입니다. </p>
<h3 id="서버-클래스-정의하기">서버 클래스 정의하기</h3>
<p>서버는 두 명의 플레이어를 연결하고 돌을 주고받는 역할을 합니다.</p>
<pre><code class="language-python">class Server:
    def __init__(self, host=&quot;localhost&quot;, port=12345):
        self.host = host
        self.port = port
        self.socket = None
        self.clients = []
        self.game = None
        self.replay = []

    def setup_socket(self):
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.bind((self.host, self.port))
        self.socket.listen(2)
        print(f&quot;Server started on {self.host}:{self.port}&quot;)

    def setup_game(self):
        self.game = Omok()

    def handle_clients(self):
        turn = 0
        while True:
            data = self.clients[turn].recv(1024)
            if not data:
                print(&quot;no data&quot;)
                break
            data = pickle.loads(data)
            row, col = data
            result = copy.deepcopy(self.game.play(row, col))
            self.replay.append(result)
            response = pickle.dumps(result)
            for client_socket in self.clients:
                client_socket.send(response)
            if result[&quot;winner&quot;] != -1:
                break
            turn = 1 - turn

        for client_socket in self.clients:
            replay_data = pickle.dumps(self.replay)
            client_socket.send(replay_data)
            client_socket.recv(1024)
            client_socket.close()

    def run(self):
        self.setup_socket()
        self.setup_game()
        print(&quot;Waiting for clients...&quot;)
        init_data = {
            &quot;board&quot;: self.game.board,
            &quot;forbidden_moves&quot;: self.game.forbidden_moves,
            &quot;winner&quot;: -1,
            &quot;turn&quot;: 1,
            &quot;color&quot;: BLACK,
        }
        while len(self.clients) &lt; 2:
            client_socket, client_address = self.socket.accept()
            client_socket.send(pickle.dumps(init_data))
            self.clients.append(client_socket)
            init_data[&quot;color&quot;] = WHITE
        print(&quot;Game started&quot;)
        self.handle_clients()</code></pre>
<p>서버는 소켓을 설정하고, 두 명의 클라이언트를 연결한 뒤 게임을 진행합니다.</p>
<ul>
<li>setup_socket<ul>
<li>소켓을 생성하고, 호스트와 포트에 바인딩 합니다.</li>
<li>소켓을 클라이언트 연결을 수신할 수 있는 상태로 설정합니다 (listen).</li>
</ul>
</li>
<li>handle_clients<ul>
<li>첫 번째 플레이어부터 순차적으로 데이터를 수신하고, 게임 상태를 업데이트합니다.</li>
<li>모든 클라이언트에게 게임의 현재 상태를 전송합니다.</li>
<li>게임이 끝나면 리플레이 데이터를 모든 클라이언트에 전송하고 소켓을 닫습니다.</li>
</ul>
</li>
<li>run<ul>
<li>두 명의 클라이언트가 연결될 때까지 기다립니다.</li>
<li>각 클라이언트에게 초기 게임 데이터를 전송하고 게임을 시작합니다.</li>
</ul>
</li>
</ul>
<h3 id="클라이언트-클래스-정의하기">클라이언트 클래스 정의하기</h3>
<p>클라이언트는 서버에 접속하여 플레이어의 입력을 받고, 돌을 놓는 역할을 합니다.</p>
<pre><code class="language-python">class Client:
    def __init__(self, host, port):
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.connect((host, port))

    def receive(self):
        print(&quot;waiting for data&quot;)
        receive = self.socket.recv(16384)
        print(&quot;received data&quot;)
        return pickle.loads(receive)

    def send(self, data):
        self.socket.send(pickle.dumps(data))</code></pre>
<p>클라이언트는 서버와 연결을 유지하면서 데이터를 주고받습니다.</p>
<h3 id="플레이어-클래스-정의하기">플레이어 클래스 정의하기</h3>
<p>게임 플레이어의 움직임은 <code>Player</code> 클래스에서 처리됩니다. 이 클래스는 보드의 상태를 그려주고, 플레이어의 클릭 이벤트를 처리하며 서버와의 데이터를 주고받습니다.</p>
<p>플래이어의 차례와 게임 상황을 제어하는 <code>Player</code> 클래스를 정의해봅시다.</p>
<pre><code class="language-python">class Player:
    board = [[0] * 19 for _ in range(19)]
    forbidden_moves = None
    winner = None
    turn = None
    color = None

    def __init__(self, client):
        self.client = client
        self.init_pygame()
        self._draw_board()

    def init_pygame(self):
        pygame.init()
        pygame.display.set_caption(&quot;Omok&quot;)
        self.screen = pygame.display.set_mode(BOARD_SIZE, 0, 32)
        self.background = pygame.image.load(BACKGROUND).convert()
        self.clickable = False

    def _draw_board(self):
        outline = pygame.Rect(45, 45, 720, 720)
        pygame.draw.rect(self.background, BLACK, outline, 3)
        for i in range(18):
            for j in range(18):
                rect = pygame.Rect(45 + (40 * i), 45 + (40 * j), 40, 40)
                pygame.draw.rect(self.background, BLACK, rect, 1)
        for i in range(3):
            for j in range(3):
                coords = (165 + (240 * i), 165 + (240 * j))
                pygame.draw.circle(self.background, BLACK, coords, 5, 0)
        self.screen.blit(self.background, (0, 0))
        pygame.display.update()

    def _draw_stone(self, row, col, color):
        coords = (45 + col * 40, 45 + row * 40)
        if color == BLACK:
            color_value = (0, 0, 0)
        else:
            color_value = (255, 255, 255)
        pygame.draw.circle(self.screen, color_value, coords, 20, 0)
        pygame.display.update()

    def run(self):
        while True:
            self._update_board()
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    raise SystemExit
                if self.clickable and event.type == pygame.MOUSEBUTTONDOWN:
                    x, y = event.pos
                    row = int(round(((y - 45) / 40.0), 0))
                    col = int(round(((x - 45) / 40.0), 0))
                    if self.board[row][col] == 0 and not (row, col) in self.forbidden_moves:
                        self.client.send((row, col))
                        self.clickable = False

    def _update_board(self):
        data = self.client.receive()
        self.board = data[&quot;board&quot;]
        self.forbidden_moves = data[&quot;forbidden_moves&quot;]
        self.turn = data[&quot;turn&quot;]
        self.winner = data[&quot;winner&quot;]
        self._draw_board()
        for row in range(19):
            for col in range(19):
                if self.board[row][col] != 0:
                    self._draw_stone(row, col, self.board[row][col])
        self.clickable = self.turn == self.color
        if self.winner != -1:
            self._show_winner()

    def _show_winner(self):
        font = pygame.font.Font(None, 36)
        if self.winner == self.color:
            text = font.render(&quot;You Win&quot;, True, (0, 0, 0))
        else:
            text = font.render(&quot;You Lose&quot;, True, (0, 0, 0))
        text_rect = text.get_rect(centerx=self.screen.get_width() // 2, top=50)
        pygame.draw.rect(self.screen, (255, 255, 255), text_rect)
        self.screen.blit(text, text_rect)
        pygame.display.update()</code></pre>
<p>이제 플레이어는 서버와 연결 후 게임을 진행하는 동안 각자의 차례를 전달받고, 화면에 돌을 놓으며 게임을 진행합니다.</p>
<h2 id="3리플레이-기능-추가하기">3.리플레이 기능 추가하기</h2>
<p>추가적으로, 게임이 끝난 후 다시 리플레이를 볼 수 있는 기능을 구현해보겠습니다. 리플레이의 데이터는 게임이 진행되는 동안 저장해두고, 나중에 이를 불러와 재생할 수 있도록 합니다.</p>
<h3 id="리플레이-클래스-정의하기">리플레이 클래스 정의하기</h3>
<p>리플레이 데이터를 불러와서 게임 진행 과정을 재생하는 <code>Replay</code> 클래스를 만들어보겠습니다.</p>
<pre><code class="language-python">class Replay:
    def __init__(self, path):
        with open(path, &quot;rb&quot;) as f:
            self.data = pickle.load(f)
        self.pointer = 0
        self.init_pygame()
        self._draw_board()

    def init_pygame(self):
        pygame.init()
        pygame.display.set_caption(&quot;Omok&quot;)
        self.screen = pygame.display.set_mode(BOARD_SIZE, 0, 32)
        self.background = pygame.image.load(BACKGROUND).convert()

    def _draw_board(self):
        outline = pygame.Rect(45, 45, 720, 720)
        pygame.draw.rect(self.background, BLACK, outline, 3)
        for i in range(18):
            for j in range(18):
                rect = pygame.Rect(45 + (40 * i), 45 + (40 * j), 40, 40)
                pygame.draw.rect(self.background, BLACK, rect, 1)
        for i in range(3):
            for j in range(3):
                coords = (165 + (240 * i), 165 + (240 * j))
                pygame.draw.circle(self.background, BLACK, coords, 5, 0)
        self.screen.blit(self.background, (0, 0))
        pygame.display.update()

    def _draw_stone(self, row, col, color):
        coords = (45 + col * 40, 45 + row * 40)
        if color == BLACK:
            color_value = (0, 0, 0)
        else:
            color_value = (255, 255, 255)
        pygame.draw.circle(self.screen, color_value, coords, 20, 0)
        pygame.display.update()

    def _update(self):
        status = self.data[self.pointer]
        self.board = status[&quot;board&quot;]
        self.forbidden_moves = status[&quot;forbidden_moves&quot;]
        self._draw_board()
        for row in range(19):
            for col in range(19):
                if self.board[row][col] != 0:
                    self._draw_stone(row, col, self.board[row][col])

    def run(self):
        self._update()
        while True:
            pygame.time.wait(500)
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    exit()
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_LEFT:
                        self.pointer = max(0, self.pointer - 1)
                    elif event.key == pygame.K_RIGHT:
                        self.pointer = min(len(self.data) - 1, self.pointer + 1)
                    self._update()</code></pre>
<p>리플레이 클래스는 이전의 게임 데이터(<code>replay.pkl</code> 파일)에 접근하여 게임의 진행 상황을 다시 보여줍니다. 게임을 처음부터 끝까지 재생할 수 있도록 키보드 화살표(←,→)를 이용해 이전 턴과 다음 턴을 이동할 수 있습니다.</p>
<h1 id="마무리">마무리</h1>
<p><img src="https://velog.velcdn.com/images/dev_post/post/35cb8afb-7315-48ce-96a6-9bd336039d96/image.png" alt=""></p>
<p>이제 여러분도 파이썬을 이용해 오목 게임을 직접 구현해볼 수 있습니다. 소켓을 이용해 네트워크 기능을 추가하고, PyGame을 이용해 그래픽을 구현하였습니다. 그리고 마지막으로 리플레이 기능을 통해 게임을 다시 재생해볼 수도 있죠. 다양한 기능을 추가하며 자신만의 오목 게임을 더욱 발전시켜보세요!</p>
<p>프로그래밍 공부와 프로젝트 진행에 도움이 되길 바랍니다. 함께 해주셔서 감사합니다. 다음 포스트에서 또 만나요!</p>
<blockquote>
<p>🔥 해당 포스팅은 <a href="https://devpost-download.framer.website/">Dev.POST</a> 도움을 받아 작성되었습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flutter에 Firebase Crashlytics를 통합하여 앱의 안정성 향상시키기]]></title>
            <link>https://velog.io/@dev_post/Flutter%EC%97%90-Firebase-Crashlytics%EB%A5%BC-%ED%86%B5%ED%95%A9%ED%95%98%EC%97%AC-%EC%95%B1%EC%9D%98-%EC%95%88%EC%A0%95%EC%84%B1-%ED%96%A5%EC%83%81%EC%8B%9C%ED%82%A4%EA%B8%B0</link>
            <guid>https://velog.io/@dev_post/Flutter%EC%97%90-Firebase-Crashlytics%EB%A5%BC-%ED%86%B5%ED%95%A9%ED%95%98%EC%97%AC-%EC%95%B1%EC%9D%98-%EC%95%88%EC%A0%95%EC%84%B1-%ED%96%A5%EC%83%81%EC%8B%9C%ED%82%A4%EA%B8%B0</guid>
            <pubDate>Mon, 29 Jul 2024 02:57:41 GMT</pubDate>
            <description><![CDATA[<p>여러분, 혹시 앱이 갑자기 크래시가 나서 당황하신 적 있으신가요? </p>
<p>개발자로서 가장 크게 두려워하는 부분이 아닐까 싶습니다. 이럴 때 Firebase Crashlytics가 여러분의 믿음직한 동반자가 되어줄 수 있습니다. 오늘은 Firebase Crashlytics를 Flutter 프로젝트에 통합하고 사용하는 방법을 알아보겠습니다.</p>
<h2 id="firebase-crashlytics란">Firebase Crashlytics란?</h2>
<p><img src="https://velog.velcdn.com/images/dev_post/post/5baa7f6a-0b9e-49ab-8292-d0851cb06b40/image.png" alt=""></p>
<p>Firebase Crashlytics는 실시간으로 앱의 크래시 리포트를 수집하고 분석해주는 도구입니다. 이를 통해 특정 사용자 환경에서 발생하는 문제를 빠르게 파악하고, 수정할 수 있습니다. 실수로 발생한 버그도 빠르게 해결할 수 있는 엄청난 무기죠!</p>
<h2 id="1-firebase-crashlytics-설정하기">1. Firebase Crashlytics 설정하기</h2>
<p>첫 번째로 Firebase Crashlytics를 사용하려면 Firebase 프로젝트에 추가해야 합니다. 
Firebase 콘솔에 로그인하고, 프로젝트를 생성한 뒤 Firebase Crashlytics를 추가해주세요.</p>
<blockquote>
<p>참고)</p>
</blockquote>
<h4 id="firebase_crashlytics-package">firebase_crashlytics Package</h4>
<p><a href="https://pub.dev/packages/firebase_crashlytics">https://pub.dev/packages/firebase_crashlytics</a></p>
<blockquote>
</blockquote>
<h4 id="firebase_crashlytics-docs">firebase_crashlytics Docs</h4>
<blockquote>
</blockquote>
<p><a href="https://firebase.google.com/docs/crashlytics/get-started?platform=flutter&amp;hl=ko">Firebase Crashlytics 시작하기 (in. Flutter)</a></p>
</br>

<h3 id="firebase-crashlytics-초기화">Firebase Crashlytics 초기화</h3>
<p>Firebase Crashlytics를 사용하기 위해 필요한 초기화 코드가 있습니다. 이를 통해 사용자 정보와 로깅 기능을 설정할 수 있습니다. 아래와 같이 초기화 코드를 구현할 수 있습니다.</p>
<p><strong>crashlytics_service.dart</strong></p>
<pre><code class="language-dart">import &#39;dart:developer&#39;;

import &#39;package:firebase_crashlytics/firebase_crashlytics.dart&#39;;
import &#39;package:flutter/foundation.dart&#39;;
import &#39;package:package_info_plus/package_info_plus.dart&#39;;

class CrashlyticsService {
  static final FirebaseCrashlytics _crashlytics = FirebaseCrashlytics.instance;

  static Future&lt;void&gt; init(String userId) async {
    _crashlytics.setCrashlyticsCollectionEnabled(true);

    // 사용자 식별자 설정
    await _crashlytics.setUserIdentifier(userId);

    // 앱 버전 정보 가져오기
    final packageInfo = await PackageInfo.fromPlatform();
    await _crashlytics.setCustomKey(&#39;app_version&#39;, packageInfo.version);

    // Flutter fatal error 처리
    FlutterError.onError = _crashlytics.recordFlutterFatalError;

    // 비동기 에러 처리
    PlatformDispatcher.instance.onError = (error, stack) {
      _crashlytics.recordError(error, stack, fatal: true);
      return true;
    };

    log(&#39;[CrashlyticsService] Crashlytics 초기화 완료&#39;);
  }
}</code></pre>
<p><strong>main.dart</strong></p>
<pre><code class="language-dart">void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp();

  ...

  await CrashlyticsService.init(userId);

  runApp(MyApp());
}</code></pre>
<p>위 코드에서는 Crashlytics를 초기화하고, 사용자 식별자와 앱 버전을 설정하며, Flutter의 fatal error와 비동기 에러를 처리하는 방법을 보여줍니다.</p>
</br>

<h2 id="2-커스텀-로그-에러-기록-추가하기">2. 커스텀 로그, 에러 기록 추가하기</h2>
<h3 id="커스텀-로그-기록하기">커스텀 로그 기록하기</h3>
<p>Crashlytics는 커스텀 로그를 지원하여 개발자가 추가적인 정보를 기록할 수 있게 합니다. 이는 특정 오류 발생 시 더 많은 배경 정보를 제공하여 문제를 빠르게 해결할 수 있도록 도와줍니다.</p>
<p><strong>crashlytics_service.dart</strong></p>
<pre><code class="language-dart">class CrashlyticsService {
  static final FirebaseCrashlytics _crashlytics = FirebaseCrashlytics.instance;

  ...

  static void logEvent(String message) {
    _crashlytics.log(message);
    log(&#39;[CrashlyticsService] logEvent: $message&#39;);
  }
}</code></pre>
<p>이렇게 logEvent 메서드를 활용하면 필요한 정보를 실시간으로 기록할 수 있습니다.</p>
<h3 id="에러-기록하기">에러 기록하기</h3>
<p>다양한 에러가 발생할 때 이를 기록하여 Crashlytics에 보내줄 수 있습니다. 이는 문제 해결에 큰 도움이 되죠. 예를 들어, 특정 블록에서 예외가 발생할 때 아래와 같이 처리할 수 있습니다.</p>
<p><strong>crashlytics_service.dart</strong></p>
<pre><code class="language-dart">import &#39;package:firebase_crashlytics/firebase_crashlytics.dart&#39;;

class CrashlyticsService {
  ...

  static void recordError(dynamic exception, StackTrace stack, {bool fatal = false}) {
    FirebaseCrashlytics.instance.recordError(exception, stack, fatal: fatal);
    log(&#39;[CrashlyticsService] recordError: $exception&#39;);
  }
}</code></pre>
<p>위 코드에서는 <code>recordError</code> 메서드를 통해 발생한 예외와 스택 추적 내용을 기록합니다. 이 정보를 통해 어떤 상황에서 어떤 오류가 발생했는지 쉽게 파악할 수 있습니다.</p>
</br>

<h2 id="3-crashlytics를-통한-오류-기록">3. Crashlytics를 통한 오류 기록</h2>
<p>무언가 잘못된 코드가 있어 이를 기록하려고 합니다. 
예를 들면 디렉토리를 선택하는 과정에서 문제가 발생할 수 있습니다. </p>
<p>아래 코드를 보시죠:</p>
<pre><code class="language-dart">import &#39;package:file_picker/file_picker.dart&#39;;

class FileService {
  Future&lt;String?&gt; selectDirectory() async {
    try {
      ...
    } catch (e, stackTrace) {
      CrashlyticsService.recordError(e, stackTrace); // add line 
      log(&#39;Error selecting directory: $e&#39;);
    }
  }
}</code></pre>
<p>디렉토리 선택 중 오류가 발생하면 <code>CrashlyticsService.recordError</code>를 호출하여 기록합니다. 이를 통해 디렉토리 선택 시 발생하는 다양한 문제를 쉽게 추적할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_post/post/33727143-8626-43f4-b7af-ada347f4a8d4/image.png" alt=""></p>
<h2 id="결론">결론</h2>
<p>Firebase Crashlytics는 앱의 안정성을 극대화하는 데 필수적인 도구입니다. 이를 통해 실시간으로 오류를 추적하고, 빠르게 대응할 수 있습니다. 위에서 설명한 초기화, 로그 추가, 에러 기록 등의 방법을 활용해 여러분의 앱을 더욱 견고하게 만들어보세요. 앱 안정성을 높이는 것은 사용자 경험을 향상시키는 데 큰 도움이 됩니다. Crashlytics를 활용하여 멋진 앱을 만들어보세요!</p>
<blockquote>
<p>🔥 해당 포스팅은 <a href="https://devpost-download.framer.website/">Dev.POST</a> 도움을 받아 작성되었습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[디자인 패턴 이해하기: Factory,  Builder 패턴을 활용한 컴퓨터 구성 요소 설계 및 테스트]]></title>
            <link>https://velog.io/@dev_post/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-Factory-Builder-%ED%8C%A8%ED%84%B4%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%BB%B4%ED%93%A8%ED%84%B0-%EA%B5%AC%EC%84%B1-%EC%9A%94%EC%86%8C-%EC%84%A4%EA%B3%84-%EB%B0%8F-%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@dev_post/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-Factory-Builder-%ED%8C%A8%ED%84%B4%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%BB%B4%ED%93%A8%ED%84%B0-%EA%B5%AC%EC%84%B1-%EC%9A%94%EC%86%8C-%EC%84%A4%EA%B3%84-%EB%B0%8F-%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Wed, 24 Jul 2024 06:12:24 GMT</pubDate>
            <description><![CDATA[<p>소프트웨어 디자인 패턴을 이해하고 적용하는 것은 소프트웨어 개발에서 매우 중요한 일입니다. 이번 포스트에서는 &#39;팩토리 패턴&#39;과 &#39;빌더 패턴&#39;을 중점적으로 다루어 보겠습니다. 이 패턴들을 이해하면 코드의 구조를 더 명확하고 관리하기 쉽게 만들 수 있습니다. 자, 그럼 시작해 볼까요?</p>
<h2 id="디자인-패턴">디자인 패턴</h2>
<p><img src="https://velog.velcdn.com/images/dev_post/post/a1c8fc92-fefd-4a7e-9994-849f56259026/image.png" alt=""></p>
<p>디자인 패턴은 소프트웨어 디자인에서 흔히 발생하는 문제를 해결하기 위한 미리 만들어진 청사진입니다.</p>
<p>패턴은 특정 코드가 아니라 문제를 해결하기 위한 일반적인 개념입니다.</p>
<p>프로그래머로 일하면서 하나의 패턴도 모른 채 일할 수도 있고, 자신도 모르게 패턴을 사용할 수도 있습니다. 그렇다면 패턴을 배우는 이유는 무엇일까요?</p>
<ol>
<li>디자인 패턴은 소프트웨어 디자인에서 흔히 발생하는 문제에 대한 검증된 솔루션으로 구성된 툴킷으로, 이러한 문제에 직면하지 않더라도 패턴을 아는것은 object-oriented design에 대한 이해를 높이고, 더 나은 코드를 작성할 수 있게 도와줍니다.</li>
<li>디자인 패턴은 팀원들과의 의사소통을 돕습니다. 디자인 패턴은 특정한 이름을 가지고 있고, 이러한 이름을 통해 팀원들과 의사소통을 할 수 있습니다.</li>
</ol>
<h2 id="팩토리-패턴">팩토리 패턴</h2>
<p><img src="https://velog.velcdn.com/images/dev_post/post/53cfd2a9-03e6-4921-91f3-12b31350a134/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">
    출처: refactoring guru 사이트
  </figcaption>

<p>팩토리 패턴은 객체 생성 로직을 별도의 팩토리 클래스로 분리하는 디자인 패턴입니다. 이를 통해 코드의 유연성과 재사용성을 크게 향상시킬 수 있습니다.</p>
<h3 id="팩토리-패턴-예제">팩토리 패턴 예제</h3>
<p>먼저, CPU라는 인터페이스를 정의해 보겠습니다. 이 인터페이스에는 <code>process</code>라는 추상 메서드가 포함되어 있습니다.</p>
<pre><code class="language-python">from abc import ABC, abstractmethod

class CPU(ABC):
    @abstractmethod
    def process(self, tasks: list[int]) -&gt; list[list[int]]:
        pass</code></pre>
<p>CPU를 구현하는 두 가지 클래스, SingleCoreCPU와 DoubleCoreCPU를 만들어 보겠습니다.</p>
<pre><code class="language-python">class SingleCoreCPU(CPU):
    def process(self, tasks: list[int]) -&gt; list[list[int]]:
        return [tasks]

class DoubleCoreCPU(CPU):
    def process(self, tasks: list[int]) -&gt; list[list[int]]:
        return [tasks[::2], tasks[1::2]]</code></pre>
<ul>
<li>SingleCoreCPU: 한번에 한개의 task만 처리할 수 있는 순서를 return 해주는 class 입니다.</li>
<li>DoubleCoreCPU: 동시에 두개의 task들을 처리해주는 class 입니다.</li>
</ul>
<p>이제 팩토리 클래스를 만들어서 CPU 객체를 생성해 보겠습니다.</p>
<pre><code class="language-python">class CPUFactory:
    @staticmethod
    def make_cpu(type: str) -&gt; CPU:
        if type == &quot;single&quot;:
            return SingleCoreCPU()
        elif type == &quot;dual&quot;:
            return DoubleCoreCPU()
        else:
            raise ValueError(f&quot;CPU type {type} not supported&quot;)</code></pre>
<p>CPUFactory를 사용하여 CPU 객체를 생성할 수 있습니다. 예를 들어, SingleCoreCPU를 생성하려면 다음과 같이 작성합니다.</p>
<pre><code class="language-python">cpu = CPUFactory.make_cpu(&quot;single&quot;)
print(cpu.process([1, 2, 3, 4]))  # 결과: [[1, 2, 3, 4]]</code></pre>
<h2 id="추상-팩토리-패턴">추상 팩토리 패턴</h2>
<p>추상 팩토리 패턴은 관련된 객체들의 집합을 생성하는 인터페이스를 정의하는 패턴입니다. 팩토리 패턴의 확장형이라 할 수 있습니다.</p>
<h3 id="추상-팩토리-패턴-예제">추상 팩토리 패턴 예제</h3>
<p>RAM과 ROM이라는 추상 클래스를 정의해 보겠습니다.</p>
<pre><code class="language-python">from abc import ABC, abstractmethod
from pydantic import BaseModel

class Memory(BaseModel, ABC):
    data: list[int]

    @property
    def size(self) -&gt; int:
        return len(self.data)

    @abstractmethod
    def read(self, idx: int) -&gt; int:
        pass

    @abstractmethod
    def write(self, idx: int, value: int):
        pass</code></pre>
<p>이 추상 클래스를 상속받아 RAM과 ROM 클래스를 구현합니다.</p>
<pre><code class="language-python">class Ram(Memory):

    def read(self, idx: int) -&gt; int:
        return self.data[idx]

    def write(self, idx: int, value: int):
        self.data[idx] = value

class Rom(Memory):

    def read(self, idx: int) -&gt; int:
        return self.data[idx]

    def write(self, idx: int, value: int):
        raise ValueError(&quot;ROM is read-only&quot;)</code></pre>
<p>RAM (Random Access Memory) 과 ROM (Read-Only Memory)의 가장 큰 차이는 RAM은 메모리에 write 작업이 가능하지만 ROM은 write작업이 불가능하다는 점입니다.</p>
<p>RAM과 ROM을 생성하는 팩토리 클래스를 만들어 보겠습니다.</p>
<pre><code class="language-python">class MemoryFactory(ABC):
    @staticmethod
    @abstractmethod
    def make_memory(size: int = 0, data: list[int] = []) -&gt; Memory:
        pass

class RamFactory(MemoryFactory):
    @staticmethod
    def make_memory(size: int = 0, data: list[int] = []) -&gt; Ram:
        if data:
            raise ValueError(&quot;RAM cannot be initialized with data&quot;)
        return Ram(data=[0] * size)

class RomFactory(MemoryFactory):
    @staticmethod
    def make_memory(size: int = 0, data: list[int] = []) -&gt; Rom:
        if size &gt; 0:
            raise ValueError(&quot;ROM cannot be initialized with size&quot;)
        return Rom(data=data)</code></pre>
<h2 id="빌더-패턴">빌더 패턴</h2>
<p>빌더 패턴은 복잡한 객체를 단계별로 생성하는 패턴입니다. 이를 통해 객체 생성 과정을 명확하고 유연하게 관리할 수 있습니다.</p>
<h3 id="빌더-패턴-예제">빌더 패턴 예제</h3>
<p>먼저 Computer라는 추상 클래스를 정의해 봅시다. 이 클래스는 CPU, RAM, ROM을 포함하며, <code>bootstrap</code>이라는 추상 메서드를 가지고 있습니다.</p>
<pre><code class="language-python">from abc import ABC, abstractmethod
from pydantic import BaseModel, ConfigDict

class Computer(BaseModel, ABC):
    model_config = ConfigDict(arbitrary_types_allowed=True)

    cpu: CPU
    ram: Ram
    rom: Rom

    @abstractmethod
    def bootstrap(self) -&gt; dict[str, list[int] | list[list[int]]]:
        pass</code></pre>
<p>Laptop과 Desktop이라는 두 가지 구체적인 Computer 클래스를 구현합니다.</p>
<pre><code class="language-python">class Laptop(Computer):
    def bootstrap(self) -&gt; dict[str, list[int] | list[list[int]]]:
        state = {}
        state[&quot;cpu_processed&quot;] = self.cpu.process(tasks=[1, 2, 3, 4])
        state[&quot;ram_data&quot;] = self.ram.data
        state[&quot;rom_data&quot;] = self.rom.data
        return state

class Desktop(Computer):
    def bootstrap(self) -&gt; dict[str, list[int] | list[list[int]]]:
        state = {}
        state[&quot;cpu_processed&quot;] = self.cpu.process(tasks=[1, 2, 3, 4, 5, 6, 7, 8])
        state[&quot;ram_data&quot;] = self.ram.data
        state[&quot;rom_data&quot;] = self.rom.data
        return state</code></pre>
<p>마지막으로 ComputerBuilder 클래스를 만들어서 Laptop과 Desktop를 생성해 보겠습니다.</p>
<p>이때 Laptop의 경우는 SingleCPU에 RAM size 8, ROM data [1,2,3,4] Desktop 경우에는 DualCPU에 RAM size 16, ROM data [1,2,3,4,5,6,7,8]로 하겠습니다.</p>
<pre><code class="language-python">class ComputerBuilder:
    @staticmethod
    def build_computer(type: str) -&gt; Computer:
        if type == &quot;laptop&quot;:
            cpu = CPUFactory.make_cpu(&quot;single&quot;)
            ram = RamFactory.make_memory(size=8)
            rom = RomFactory.make_memory(data=[1, 2, 3, 4])
            return Laptop(cpu=cpu, ram=ram, rom=rom)
        elif type == &quot;desktop&quot;:
            cpu = CPUFactory.make_cpu(&quot;dual&quot;)
            ram = RamFactory.make_memory(size=16)
            rom = RomFactory.make_memory(data=[1, 2, 3, 4, 5, 6, 7, 8])
            return Desktop(cpu=cpu, ram=ram, rom=rom)
        else:
            raise ValueError(f&quot;Computer type {type} not supported&quot;)</code></pre>
<p>이제 ComputerBuilder를 사용하여 컴퓨터 객체를 생성하고 부트스트랩을 해보겠습니다.</p>
<pre><code class="language-python">computer = ComputerBuilder.build_computer(type=&quot;laptop&quot;)
state = computer.bootstrap()
print(state)
# 결과: {&#39;cpu_processed&#39;: [[1, 2, 3, 4]], &#39;ram_data&#39;: [0, 0, 0, 0, 0, 0, 0, 0], &#39;rom_data&#39;: [1, 2, 3, 4]}</code></pre>
<h2 id="테스트">테스트</h2>
<p>코드가 제대로 동작하는지 확인하려면 테스트가 필수적입니다. pytest를 사용하여 위의 클래스를 테스트해 볼 수 있습니다. 다음 코드를 통해 다양한 경우를 테스트할 수 있습니다.</p>
<pre><code class="language-python">import pytest

from computer import ComputerBuilder
from memory import RamFactory, RomFactory
from cpu import CPUFactory

@pytest.mark.computer
class TestComputer:
    def test_laptop(self):
        computer = ComputerBuilder.build_computer(type=&quot;laptop&quot;)
        state = computer.bootstrap()
        assert state[&quot;cpu_processed&quot;] == [[1, 2, 3, 4]]
        assert state[&quot;ram_data&quot;] == [0] * 8
        assert state[&quot;rom_data&quot;] == [1, 2, 3, 4]

    def test_desktop(self):
        computer = ComputerBuilder.build_computer(type=&quot;desktop&quot;)
        state = computer.bootstrap()
        assert state[&quot;cpu_processed&quot;] == [[1, 3, 5, 7], [2, 4, 6, 8]]
        assert state[&quot;ram_data&quot;] == [0] * 16
        assert state[&quot;rom_data&quot;] == [1, 2, 3, 4, 5, 6, 7, 8]

    def test_invalid_computer(self):
        with pytest.raises(ValueError):
            ComputerBuilder.build_computer(type=&quot;invalid&quot;)

@pytest.mark.memory
class TestMemory:
    def test_ram(self):
        memory = RamFactory.make_memory(size=8)
        assert memory.size == 8
        assert memory.read(0) == 0
        assert memory.read(7) == 0
        memory.write(0, 1)
        assert memory.read(0) == 1
        memory.write(7, 2)
        assert memory.read(7) == 2

    def test_rom(self):
        memory = RomFactory.make_memory(data=[1, 2, 3, 4])
        assert memory.size == 4
        assert memory.read(0) == 1
        assert memory.read(3) == 4
        with pytest.raises(ValueError):
            memory.write(0, 1)
        with pytest.raises(ValueError):
            memory.write(3, 4)

    def test_invalid_memory(self):
        with pytest.raises(ValueError):
            RamFactory.make_memory(data=[1, 2, 3, 4])
        with pytest.raises(ValueError):
            RomFactory.make_memory(size=8)

@pytest.mark.cpu
class TestCPU:
    def test_single_core(self):
        cpu = CPUFactory.make_cpu(type=&quot;single&quot;)
        assert cpu.process([1, 2, 3, 4]) == [[1, 2, 3, 4]]

    def test_dual_core(self):
        cpu = CPUFactory.make_cpu(type=&quot;dual&quot;)
        assert cpu.process([1, 2, 3, 4]) == [[1, 3], [2, 4]]

    def test_invalid_cpu(self):
        with pytest.raises(ValueError):
            CPUFactory.make_cpu(type=&quot;invalid&quot;)</code></pre>
<p>테스트를 실행하면 우리가 만든 코드가 예상대로 동작하는지 확인할 수 있습니다.</p>
<h2 id="마무리">마무리</h2>
<p>이번 포스트에서는 팩토리 패턴과 빌더 패턴을 사용하여 CPU, RAM, ROM으로 구성된 컴퓨터 객체를 만들어보았습니다. 이러한 디자인 패턴을 사용하면 전체 코드의 구조가 명확해지고, 유지 보수와 확장이 용이해집니다. 디자인 패턴을 더 깊이 이해하려면 다양한 예제를 통해 계속해서 학습하는 것이 중요합니다.</p>
<blockquote>
<p>🔥 해당 포스팅은 <a href="https://devpost-download.framer.website/">Dev.POST</a> 도움을 받아 작성되었습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flutter로 Mac 앱 개발 시 'window_manager' 패키지 활용법]]></title>
            <link>https://velog.io/@dev_post/Flutter%EB%A1%9C-Mac-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%8B%9C-windowmanager-%ED%8C%A8%ED%82%A4%EC%A7%80-%ED%99%9C%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@dev_post/Flutter%EB%A1%9C-Mac-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%8B%9C-windowmanager-%ED%8C%A8%ED%82%A4%EC%A7%80-%ED%99%9C%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Tue, 23 Jul 2024 06:21:31 GMT</pubDate>
            <description><![CDATA[<p>플러터를 이용해 데스크톱 Mac App을 개발해보신 적이 있나요? </p>
<p>특히 macOS 앱을 처음 실행할 때 검은 화면이 나타나는 현상을 겪어보신 분들이 많을 텐데요. 오늘은 이를 해결하고 <code>window_manager</code> 패키지를 활용하여 원하는 윈도우 크기 조정과 같은 기능들을 손쉽게 설정하는 방법을 소개해보려고 합니다.</p>
<h3 id="macos-앱-첫-실행-검은-화면-문제-해결">macOS 앱 첫 실행 검은 화면 문제 해결</h3>
<p><img src="https://velog.velcdn.com/images/dev_post/post/b4f1aeae-19d9-45c2-a174-e3e02e9dea2c/image.gif" alt=""></p>
<p>macOS 앱을 처음 실행할 때 검은 화면이 나타나는 현상은 꽤나 골치 아픈 버그 중 하나입니다. 
이를 해결하기 위해 <code>window_manager</code> 패키지의 [Hidden at launch] 기능을 이용해 쉽게 해결할 수 있습니다.</p>
<blockquote>
<p>window_manager &quot;Hidden at launch&quot;
<a href="https://pub.dev/packages/window_manager#hidden-at-launch">https://pub.dev/packages/window_manager#hidden-at-launch</a></p>
</blockquote>
<p>macOS는 아래와 같이 수정하면 됩니다.</p>
<p>Change the file <code>macos/Runner/MainFlutterWindow.swift</code></p>
<pre><code class="language-swift">import window_manager // add

class MainFlutterWindow extends NSWindow {
  override func awakeFromNib() {
    ...
    super.awakeFromNib();
  }

  // add
  override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) {
    super.order(place, relativeTo: otherWin)
    hiddenWindowAtLaunch()
  }
}</code></pre>
<p>Change the file <code>lib/main.dart</code></p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:window_manager/window_manager.dart&#39;;

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Must add this line.
  await windowManager.ensureInitialized();

  runApp(MyApp());
}
</code></pre>
<p>이 코드는 <code>NSWindow</code>의 <code>order</code> 메서드를 오버라이드하여, 창이 표시될 때 mac 앱이 빌드 되는 동안 띄워주는 검은 화면 대신 화면을 띄우지 않고 앱이 실행 되면 출력되게 합니다.</p>
<p>이를 통해 첫 실행 시 검은 화면 문제가 해결됩니다.</p>
<h3 id="윈도우-크기-위치-설정하기">윈도우 크기, 위치 설정하기</h3>
<p>이제 <code>window_manager</code> 패키지를 사용해 윈도우의 기본 크기와 위치를 설정하는 방법을 살펴보겠습니다. </p>
<p>Change the file <code>lib/main.dart</code></p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:window_manager/window_manager.dart&#39;;

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await windowManager.ensureInitialized();
  initWindows();

  runApp(MyApp());
}

Future&lt;void&gt; initWindows() async {
  // `window_manager` 초기화
  await windowManager.ensureInitialized();

  const windowOptions = WindowOptions(
    size: Size(1000, 1000),
    minimumSize: Size(800, 1000),
    center: true,
    backgroundColor: Colors.transparent,
    titleBarStyle: TitleBarStyle.hidden,
  );

  // 윈도우가 준비된 후 보여주기
  windowManager.waitUntilReadyToShow(windowOptions, () async {
    await windowManager.show();
    await windowManager.focus();
  });
}
</code></pre>
<p>위 코드는 <code>window_manager</code> 패키지를 초기화하고, 윈도우의 기본 크기, 최소 크기, 중앙 배치 여부, 배경 색상, 타이틀 바 스타일을 설정합니다. </p>
<p>그리고 윈도우가 준비된 후 표시되고 포커스가 이 윈도우로 이동하도록 합니다.</p>
<h4 id="windowoptions의-주요-속성"><code>WindowOptions</code>의 주요 속성:</h4>
<ul>
<li><code>size</code>: 윈도우의 기본 크기를 설정합니다.</li>
<li><code>minimumSize</code>: 윈도우의 최소 크기를 설정합니다.</li>
<li><code>center</code>: 윈도우를 화면 중앙에 배치할지 여부를 설정합니다.</li>
<li><code>backgroundColor</code>: 윈도우의 배경 색상을 설정합니다.</li>
<li><code>titleBarStyle</code>: 타이틀 바의 스타일을 설정합니다. <code>hidden</code>을 설정하면 타이틀 바가 숨겨집니다.</li>
</ul>
<p>이렇게 설정하면 macOS 애플리케이션을 처음 실행할 때 원하는 크기와 위치로 설정된 윈도우가 표시됩니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_post/post/0cfab91f-2a9a-488e-ba0b-6a350b1efdcd/image.gif" alt=""></p>
<p>여기서 중요한 점은 모든 작업을 비동기로 처리하여 사용자가 앱을 실행했을 때 자연스럽고 빠르게 윈도우가 표시되게 하는 것입니다.</p>
<h3 id="마무리">마무리</h3>
<p>오늘은 플러터 기반의 macOS 애플리케이션에서 자주 발생하는 검은 화면 문제와 이를 <code>window_manager</code> 패키지를 사용해 해결하는 방법을 알아보았습니다. 또한, 이 패키지를 활용해 윈도우의 기본 설정을 손쉽게 관리할 수 있는 방법을 소개했습니다.</p>
<p>이제 여러분도 원하는 대로 윈도우를 커스터마이징하여 보다 나은 사용자 경험을 제공할 수 있을 것입니다. Happy Coding!</p>
<blockquote>
<p>🔥 해당 포스팅은 <a href="https://devpost-download.framer.website/">Dev.POST</a> 도움을 받아 작성되었습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django 프로젝트에 Sentry 연동하기: 에러 모니터링 쉽게 하기]]></title>
            <link>https://velog.io/@dev_post/Django-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-Sentry-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0-%EC%97%90%EB%9F%AC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EC%89%BD%EA%B2%8C-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_post/Django-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-Sentry-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0-%EC%97%90%EB%9F%AC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EC%89%BD%EA%B2%8C-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 22 Jul 2024 08:50:48 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요 여러분! 오늘은 Django 프로젝트에 Sentry를 어떻게 설정하고 활용할 수 있는지 알아보려고 합니다. 최근 프로젝트에서 발생한 몇 가지 이슈와 그것을 해결하기 위해 Sentry를 적용한 경험을 바탕으로 설명드리겠습니다. 😊</p>
<h3 id="sentry란-무엇인가요">Sentry란 무엇인가요?</h3>
<p><img src="https://velog.velcdn.com/images/dev_post/post/48cc6d44-7d23-4435-8aa4-869ac2de5268/image.png" alt=""></p>
<p>먼저 Sentry가 무엇인지 간단히 알아볼까요? Sentry는 애플리케이션에서 발생하는 오류와 성능 문제를 모니터링하고 추적할 수 있는 도구입니다. 이 도구는 오류가 발생했을 때 알림을 보내 주며, 오류의 원인을 파악할 수 있는 다양한 정보를 제공합니다. 이를 통해 개발자는 문제를 신속하게 해결할 수 있습니다.</p>
<h3 id="sentry-설정하기">Sentry 설정하기</h3>
<p>본격적으로 들어가기 앞서서 Sentry 설정을 모두 마치고 dsn 까지 받은 상황이라고 가정하겠습니다!
이제 Django 프로젝트에 Sentry를 설정하는 방법을 살펴보겠습니다. 먼저, <code>pyproject.toml</code> 파일에 Sentry SDK를 추가해야 합니다.
저는 poetry로 python 가상환경을 관리하고 있으니 아래와 같은 명령어로 설치해줄게요!</p>
<pre><code>poetry add sentry_sdk</code></pre><p>다음으로, <code>.env</code> 파일에 Sentry DSN(Data Source Name)을 추가합니다. Sentry DSN은 Sentry 프로젝트 설정 페이지에서 확인할 수 있습니다.</p>
<pre><code class="language-env">SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0</code></pre>
<p>이제 Django 설정 파일을 수정하여 Sentry를 프로젝트에 통합해 봅시다. 먼저 <code>settings.py</code> 파일을 열고, 관련 모듈을 가져옵니다.</p>
<pre><code class="language-python">import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
from sentry_sdk.integrations.celery import CeleryIntegration</code></pre>
<p>그 다음, Sentry를 초기화합니다.</p>
<pre><code class="language-python">sentry_sdk.init(
    dsn=settings.SENTRY_DSN,
    integrations=[
        DjangoIntegration(),
        LoggingIntegration(level=logging.WARNING, event_level=logging.WARNING),
        CeleryIntegration(propagate_traces=False),
    ],
    environment=settings.APP_ENV,
)</code></pre>
<p>여기서 <code>settings.SENTRY_DSN</code>은 앞서 <code>.env</code> 파일에 설정한 DSN을 가리키고, <code>integrations</code>는 Django와 Celery, Logging 통합 설정을 의미합니다.
참고로 밑에 environment key를 이용해서 저는 dev서버와 prod 서버의 slack alert를 각각 다른 채널에 보내게 sentry에서 설정했습니다.
<img src="https://velog.velcdn.com/images/dev_post/post/6afcd0d5-80f6-43cb-94cc-5f317d47c2a2/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev_post/post/9d71a873-a0d0-4687-9a53-61ee45b27bca/image.png" alt=""></p>
<h3 id="sentry-적용-후-경험">Sentry 적용 후 경험</h3>
<p>Sentry를 적용한 후, 우리는 몇 가지 중요한 장점을 경험했습니다.</p>
<ol>
<li><strong>실시간 오류 알림</strong>: 에러가 발생할 때마다 즉시 알림을 받을 수 있어, 문제를 신속히 파악하고 대응할 수 있었습니다.</li>
<li><strong>상세한 오류 정보</strong>: 오류가 발생한 위치, 스택 트레이스, 사용자 환경 등 다양한 정보를 제공하여 문제 해결에 큰 도움이 되었습니다.</li>
<li><strong>성능 모니터링</strong>: 애플리케이션의 성능 문제도 함께 모니터링할 수 있어, 보다 안정적인 서비스를 제공할 수 있었습니다.</li>
<li><strong>중복 알림 제거</strong>: 어쩔 수 없이 중복되는 에러나 경고가 발생할 시 매번 보내는 형식이 아닌, 특정 시간동안은 중복된 알림이 안오게 설정할 수 있습니다.</li>
</ol>
<p>예를 들어, 특정 뷰에서 예상하지 못한 에러가 발생했을 때, Sentry를 통해 즉시 알림을 받을 수 있었으며, 상세한 로그와 함께 문제의 원인을 빠르게 찾아낼 수 있었습니다.</p>
<h3 id="결론">결론</h3>
<p>Sentry를 Django 프로젝트에 적용함으로써 우리는 보다 효율적으로 오류를 관리하고, 문제를 신속히 해결할 수 있게 되었습니다. 여러분도 Sentry를 통해 프로젝트의 안정성을 높여 보세요! 다음에 더 유익한 정보로 찾아뵙겠습니다. 감사합니다. 😊</p>
<hr>
<p>이렇게 Sentry를 통합하고 활용하는 방법을 알아보았습니다. 도움이 되셨다면 좋겠네요. 그럼 다음에 또 만나요!</p>
<blockquote>
<p>🔥 해당 포스팅은 <a href="https://devpost-download.framer.website/">Dev.POST</a> 도움을 받아 작성되었습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flutter EventBus로 결합도 낮추기: 페이지 간 업데이트 감지하기]]></title>
            <link>https://velog.io/@dev_post/Flutter-EventBus%EB%A1%9C-%EA%B2%B0%ED%95%A9%EB%8F%84-%EB%82%AE%EC%B6%94%EA%B8%B0-%ED%8E%98%EC%9D%B4%EC%A7%80-%EA%B0%84-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EA%B0%90%EC%A7%80%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_post/Flutter-EventBus%EB%A1%9C-%EA%B2%B0%ED%95%A9%EB%8F%84-%EB%82%AE%EC%B6%94%EA%B8%B0-%ED%8E%98%EC%9D%B4%EC%A7%80-%EA%B0%84-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EA%B0%90%EC%A7%80%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 19 Jul 2024 03:31:54 GMT</pubDate>
            <description><![CDATA[<h3 id="결합성-낮추기-flutter-eventbus로-페이지-간-업데이트-감지하기">결합성 낮추기: Flutter EventBus로 페이지 간 업데이트 감지하기</h3>
<p>Flutter 앱에서 push, pop 등으로 페이지 전달하는 코드 없이 Tab 등으로 위치 다른 페이지가 있을 때, <strong>한 페이지에서 일어난 변화를 다른 페이지가 어떻게 알 수 있을까요?</strong> </p>
<p><img src="https://velog.velcdn.com/images/dev_post/post/775273ca-14e3-4e96-9450-df358c21fbf5/image.png" alt=""></p>
<p>좌측</p>
<ul>
<li><strong>상황:</strong> A1 -&gt; A2 페이지 이동으로 이어진 상황</li>
<li><strong>데이터 전달:</strong> push, pop은 페이지 통해 데이터 전달 또는 후 액션 처리 하기 쉽죠</li>
</ul>
<p>우측 </p>
<ul>
<li><strong>상황:</strong> A1 -&gt; A2 페이지 이동으로 이어져있고, B1은 다른 탭 페이지 
(A2와 B1은 직접적인 페이지 연결 없음)</li>
<li><strong>데이터 전달:</strong> push, pop으로 연결되어있는 페이지가 아닌 상황에서 다른 탭의 B1 페이지에서 어떻게 데이터를 전달 또는 후 액션을 할 수 있을까요? 🤔</li>
</ul>
<p>전역적으로 처리 등 다양한 방법이 있겠지만 그 중에서도 EventBus를 이용하면 이런 문제를 훨씬 간단하게 해결할 수 있습니다. </p>
<p>오늘은 Flutter에서 EventBus를 활용해 결합성을 낮추고 페이지 간 소통을 쉽게 만드는 방법을 알아보겠습니다.</p>
<h4 id="eventbus란">EventBus란?</h4>
<p>먼저, EventBus가 무엇인지 짚고 넘어가죠. </p>
<p>간단하게 말해 EventBus는 애플리케이션 내의 다양한 컴포넌트들이 이벤트를 통해 서로 통신할 수 있게 해주는 도구입니다. <strong>이는 특히 서로 직접적인 참조가 없는 컴포넌트 간의 결합성을 낮출 때 유용합니다.</strong></p>
</br>

<h3 id="eventbus-구현하기">EventBus 구현하기</h3>
<p>자, 이제 예제 코드와 함께 EventBus의 구현을 살펴봅시다.</p>
<p>event_bus.dart</p>
<pre><code class="language-dart">import &#39;dart:async&#39;;

class EventBus {
  static final EventBus _instance = EventBus._internal();
  factory EventBus() =&gt; _instance;
  EventBus._internal();

  final _eventController = StreamController&lt;dynamic&gt;.broadcast();

  Stream&lt;T&gt; on&lt;T&gt;() =&gt;
      _eventController.stream.where((event) =&gt; event is T).cast&lt;T&gt;();

  void fire(event) =&gt; _eventController.add(event);

  void dispose() =&gt; _eventController.close();
}</code></pre>
<p>위 코드는 EventBus의 기본적인 구조를 보여줍니다. 각 부분을 하나씩 설명해볼게요:</p>
<ol>
<li><p><strong>싱글톤 패턴</strong>: EventBus 클래스는 싱글톤 패턴으로 구현되었습니다. 이는 어플리케이션 전역에서 하나의 인스턴스만 사용되도록 보장합니다.</p>
</li>
<li><p><strong>StreamController</strong>: <code>_eventController</code>는 이벤트를 관리하기 위해 사용됩니다. <code>.broadcast()</code>를 사용해 여러 리스너가 동일한 스트림을 구독할 수 있게 합니다.</p>
</li>
<li><p><strong>on<T>() 메서드</strong>: 특정 타입의 이벤트를 필터링해서 스트림으로 반환합니다. 이를 통해 특정 이벤트 타입만을 구독할 수 있습니다.</p>
</li>
<li><p><strong>fire(event) 메서드</strong>: 새로운 이벤트를 스트림에 추가합니다.</p>
</li>
<li><p><strong>dispose() 메서드</strong>: 더 이상 EventBus가 필요 없을 때 스트림을 닫습니다.</p>
</li>
</ol>
</br>

<h3 id="이벤트-모델-정의하기">이벤트 모델 정의하기</h3>
<p>이제 EventBus에서 사용할 이벤트 모델을 정의해봅시다. 여기서는 블로그가 생성되는 이벤트를 예제로 들어 설명하겠습니다.</p>
<p>events.dart</p>
<pre><code class="language-dart">class BlogCreatedEvent {
  final String blogId;
  BlogCreatedEvent(this.blogId);
}</code></pre>
<p>간단하죠? <code>BlogCreatedEvent</code> 클래스는 블로그 생성 시 이벤트를 트리거하기 위한 모델을 정의합니다.</p>
<blockquote>
<p><strong>파일 구조</strong>는 아래와 같이 참고하세요 !</p>
</blockquote>
<pre><code>project_lib/
├── core/
│   ├── event_bus/
│   │   ├── event_bus.dart
│   │   └── events.dart
│   └── utils/
│       └── ...
├── data/
│   └── ...
└── presentation/
    └── ...</code></pre></br>

<h3 id="eventbus-활용하기">EventBus 활용하기</h3>
<p>EventBus와 이벤트 모델이 준비되었으니, 실제로 이를 어떻게 활용하는지 살펴봅시다. </p>
<ol>
<li>블로그 생성 페이지에서 이벤트를 트리거하는 부분입니다:</li>
</ol>
<pre><code class="language-dart">import &#39;event_bus.dart&#39;;
import &#39;events.dart&#39;;

void createBlog() {
  try {
    // 블로그 생성 로직
  } catch (e) {
    // 에러 처리
  } finally {
    EventBus().fire(BlogCreatedEvent(blogId));
  }
}</code></pre>
<p>블로그가 성공적으로 생성되었을 때 <code>EventBus().fire(BlogCreatedEvent(blogId));</code>를 호출하여 이벤트를 발생시킵니다.</p>
<ol start="2">
<li>그리고 블로그 목록 페이지에서 이 이벤트를 구독해서 반응하는 부분입니다:</li>
</ol>
<pre><code class="language-dart">import &#39;event_bus.dart&#39;;
import &#39;events.dart&#39;;

class BlogListViewModel with ChangeNotifier {
  BlogListViewModel() {
    EventBus().on&lt;BlogCreatedEvent&gt;().listen(_onBlogCreated);
  }

  void _onBlogCreated(BlogCreatedEvent event) {
    // 목록 새로고침 로직
    fetchBlogs(isForce: true);
  }

  @override
  void dispose() {
    EventBus().dispose();
    super.dispose();
  }
}</code></pre>
<p>여기서는 <code>EventBus().on&lt;BlogCreatedEvent&gt;().listen(_onBlogCreated);</code>를 통해 특정 이벤트 타입을 구독하고, 이벤트 발생 시 블로그 목록을 새로고침합니다. </p>
<p>마지막으로, 메모리 누수를 방지하기 위해 <code>dispose()</code> 메서드 내에서 EventBus의 <code>dispose()</code>를 호출해 스트림을 닫습니다.</p>
<h3 id="결론">결론</h3>
<p>EventBus를 이용한 이 패턴의 장점은 페이지 간 결합성을 낮추면서도 서로의 상태 변화를 쉽게 감지하고 반응할 수 있다는 점입니다. Flutter 프로젝트에서 여러 페이지가 상호작용해야 하지만 서로 직접적으로 접근하지 않기를 원할 때, EventBus는 탁월한 해결책이 될 수 있습니다. </p>
<p>이제 여러분도 EventBus를 활용해 더 확장성 높고 유지보수하기 쉬운 Flutter 애플리케이션을 만들어보세요! 🍀</p>
<blockquote>
<p>🔥 해당 포스팅은 <a href="https://devpost-download.framer.website/">Dev.POST</a> 도움을 받아 작성되었습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Dev.POST: AI가 써주는 기술 블로그]]></title>
            <link>https://velog.io/@dev_post/Dev.POST-AI%EA%B0%80-%EC%8D%A8%EC%A3%BC%EB%8A%94-%EA%B8%B0%EC%88%A0-%EB%B8%94%EB%A1%9C%EA%B7%B8</link>
            <guid>https://velog.io/@dev_post/Dev.POST-AI%EA%B0%80-%EC%8D%A8%EC%A3%BC%EB%8A%94-%EA%B8%B0%EC%88%A0-%EB%B8%94%EB%A1%9C%EA%B7%B8</guid>
            <pubDate>Wed, 17 Jul 2024 06:57:20 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요
코딩만 하면 기술 블로그가 자동으로, Dev.POST팀 입니다 🙌</p>
<h2 id="여러분의-기술블로그는-안녕하신가요-">여러분의 기술블로그는 안녕하신가요 ?</h2>
<p><img src="https://velog.velcdn.com/images/dev_post/post/964ae34a-d0ad-41bb-b325-d51680a4bb6d/image.png" alt=""></p>
<ul>
<li>코드는 금방 작성하는데, 블로그 글 작성하는데 시간이 많이 걸리나요?</li>
<li>바쁜 업무에 치여 기술 블로그 운영을 소홀해 졌나요?</li>
<li>기술블로그 지속적으로 꾸준히 운영하기 어렵나?</li>
<li>기술블로그 개설 후 처음 다짐했던 마음가짐을 잊으셨나요?</li>
<li><strong>개발 과정에서 얻은 귀중한 경험과 지식이 기록되지 않고 사라지고 있진 않으신가요?</strong></li>
</ul>
</br>



<h2 id="devpost--ai가-써주는-기술블로그"><a href="https://devpost-download.framer.website/">Dev.POST</a> : AI가 써주는 기술블로그</h2>
<hr>
<p>Dev.POST는 개발자가 코드 작업에 집중하는 동안, 
자동으로 기술 블로그 글을 생성하여, <strong>바쁜 개발자도 기술 블로그를 손쉽게 운영할 수 있도록 도와줍니다.</strong></p>
<p><img src="https://velog.velcdn.com/images/dev_post/post/9de2fd96-601f-46b2-b765-fbe3129b55a7/image.png" alt=""></p>
<ul>
<li><p>시간과 노력을 절약하고, 경험을 효과적으로 기록할 수 있도록 도웁니다.</p>
</li>
<li><p>개발하는 동안 자연스레 쌓이는 경험이랑 지식을 자동으로 모아 정리합니다.</p>
</li>
<li><p>개인 성장이랑 지식 공유에 도움 되는 블로그를 운영할 수 있도록 글을 작성해줍니다.</p>
</li>
<li><p>커밋 메시지, 코드 변경 내용 등을 분석하여 블로그 글의 초안을 자동 작성합니다.</p>
</li>
<li><p>블로그 글 초안을 GitHub Pages, Notion, Velog, Tistory, Medium 등 다양한 플랫폼에 발행할 수 있도록, markdown(마크다운) 포맷으로 작성됩니다.</p>
</li>
<li><p>키워드에 대한 이론적인 내용은 신뢰할 수 있는 출처를 참고해 자동으로 작성해 줍니다.</p>
</li>
<li><p>그대로 예시 코드를 만들지 않습니다. 주요 기술을 찾아 자동으로 설명하기 좋은 형태로 변환하여 블로그 글을 작성해줍니다.</p>
</li>
</ul>
</br>


<h2 id="🤚-설치-전-요청사항">🤚 설치 전 요청사항</h2>
<hr>
<p><strong>Dev.POST팀은 여러분들이 궁금합니다 🥹</strong>
아래 구글 폼을 통해 30초만 투자해 저희에게 응답을 남겨주신다면, 감사하겠습니다 🙏
<a href="https://forms.gle/CAnBS7dNADBxzBMy5">https://forms.gle/CAnBS7dNADBxzBMy5</a></p>
<p><img src="https://velog.velcdn.com/images/dev_post/post/93ad7a4c-9a96-4daa-a55c-d0e972c7d683/image.png" alt=""></p>
<p></br></br></p>
<h2 id="다운로드-및-설치-방법">다운로드 및 설치 방법</h2>
<hr>
<h4 id="1-다운로드-링크-에-접속하셔서-mac-전용-데스크탑-앱을-다운로드-받습니다">1. <strong><a href="https://devpost-download.framer.website/">다운로드 링크</a> 에 접속하셔서 mac 전용 데스크탑 앱을 다운로드 받습니다.</strong></h4>
<p><img src="https://velog.velcdn.com/images/dev_post/post/f0b8802f-58cf-40c8-880f-2b372a36d615/image.png" alt=""></p>
<h4 id="2-dmg-파일을-실행한-뒤에-왼쪽-devpost를-application으로-옮겨주세요">2. <strong>dmg 파일을 실행한 뒤에 왼쪽 devpost를 application으로 옮겨주세요</strong></h4>
<p><img src="https://velog.velcdn.com/images/dev_post/post/27ba8f47-0760-41df-8bfc-c4ab4481880e/image.png" alt="">
  * 왼쪽 앱을 오른쪽 Applications 폴더에 드래그 해주세요.
  * 왼쪽 devpost 앱을 더블 클릭 해주세요.</p>
<h4 id="3-아래-화면과-같이-앱이-실행-되면-완료">3. <strong>아래 화면과 같이 앱이 실행 되면 완료!</strong></h4>
<p><img src="https://velog.velcdn.com/images/dev_post/post/2ccb6022-3f99-4d50-b7ec-d2ede650b783/image.png" alt=""></p>
<p></br></br></p>
<h2 id="피드백-제공-방법">피드백 제공 방법</h2>
<hr>
<p>*<em>앱의 왼쪽 사이드 바 하단에 있는 버튼을 눌러 피드백 *</em></p>
<p><img src="https://velog.velcdn.com/images/dev_post/post/3c27c21a-7e76-4168-87d2-c6dfc49acc37/image.png" alt=""></p>
<p><strong>또는 Dev.POST 팀과 자유롭게 소통할 수 있는 오픈채팅방 이용</strong></p>
<p>채널1: <a href="https://open.kakao.com/o/sthtzLBg">https://open.kakao.com/o/sthtzLBg</a> </p>
<p>채널2: <a href="https://open.kakao.com/o/saBuzLBg">https://open.kakao.com/o/saBuzLBg</a></p>
</br>

<p><img src="https://velog.velcdn.com/images/dev_post/post/d0936587-4406-4f56-a047-582192f6d893/image.png" alt=""></p>
<p>개발자 여러분의 피드백은 저희에게 매우 소중합니다 😘
불편하지 않더라도 사용하고 떠오르는 생각 자유롭게 전달해주시면 감사하겠습니다 !!</p>
<p>프로토타입 사용 중 궁금한 사항이나 도움이 필요하신 경우,언제든지 저희에게 연락해 주세요.</p>
<p><strong>Dev.POST의 관심을 가져주셔서 감사드리며,
여러분의 소중한 의견 기다리겠습니다 !</strong></p>
<p><img src="https://velog.velcdn.com/images/dev_post/post/05312374-a176-49f5-a061-730a913baaeb/image.png" alt=""></p>
<p>그럼 오늘 한 주도 즐코 모두모두 화이팅 ~💪 </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flutter에서 디바운스 검색기능 구현하기: API 호출 없이 로컬 데이터 사용하기]]></title>
            <link>https://velog.io/@dev_post/Flutter%EC%97%90%EC%84%9C-%EB%94%94%EB%B0%94%EC%9A%B4%EC%8A%A4-%EA%B2%80%EC%83%89%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-API-%ED%98%B8%EC%B6%9C-%EC%97%86%EC%9D%B4-%EB%A1%9C%EC%BB%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_post/Flutter%EC%97%90%EC%84%9C-%EB%94%94%EB%B0%94%EC%9A%B4%EC%8A%A4-%EA%B2%80%EC%83%89%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-API-%ED%98%B8%EC%B6%9C-%EC%97%86%EC%9D%B4-%EB%A1%9C%EC%BB%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 17 Jul 2024 05:53:20 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요, 여러분! 
Flutter로 검색 기능을 구현할 때 여러분은 어떤 어려움을 겪고 계신가요? </p>
<p>검색어를 입력할 때마다 리스트를 업데이트해야 해서 성능 저하와 불필요한 API 호출로 골머리를 앓고 있지는 않으신가요? 이번 글에서는 이러한 문제를 깔끔하게 해결할 수 있는 Debounce 기술에 대해 소개해드리려고 합니다!</p>
<h4 id="debounce-란"><code>Debounce</code> 란?</h4>
<p><code>Debounce</code>는 사용자 입력 같은 이벤트가 빠르게 연속으로 발생할 때, 이를 제어하여 불필요한 작업을 줄이는 기술입니다. </p>
<p><strong>간단히 말해 특정 시간 동안 이벤트를 모아 마지막 이벤트만 실행시키는 방식이죠.</strong> 이 기술은 검색 기능을 구현할 때 매우 유용합니다. 사용자가 타이핑을 멈출 때까지 기다린 후 검색을 실행할 수 있기 때문입니다.</p>
</br>

<h2 id="flutter에서-debounce-구현하기">Flutter에서 <code>debounce</code> 구현하기</h2>
<p>여기서는 Flutter 패키지 중 하나인 <code>easy_debounce</code>를 활용해 검색 기능을 구현해 보겠습니다. 먼저, <code>pubspec.yaml</code> 파일에 <code>easy_debounce</code> 패키지를 추가해줘야 합니다. </p>
<pre><code class="language-yaml">dependencies:
  easy_debounce: ^2.0.3</code></pre>
<p>그 후, 터미널에서 <code>flutter pub get</code> 명령어를 실행하여 패키지를 다운로드받습니다.</p>
<h3 id="ui-코드-작성">UI 코드 작성</h3>
<p>검색창을 구성하는 간단한 UI를 작성해보겠습니다.</p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:easy_debounce/easy_debounce.dart&#39;;

class SearchScreen extends StatelessWidget {
  final viewModel;

  const SearchScreen({Key? key, required this.viewModel}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(&#39;Debounce Search&#39;)),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              decoration: InputDecoration(hintText: &#39;검색어를 입력하세요&#39;),
              onChanged: (value) {
                EasyDebounce.debounce(
                  &#39;search-debouncer&#39;,
                  Duration(milliseconds: 400),
                  () =&gt; viewModel.onSearch(value),
                );
              },
            ),
            Expanded(
              child: ListView.builder(
                itemCount: viewModel.searchedResults.length,
                itemBuilder: (context, index) {
                  return ListTile(
                    title: Text(viewModel.searchedResults[index]),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}</code></pre>
<p>위 코드에서는 <code>TextField</code> 위젯에서 <code>onChanged</code> 콜백을 사용하고, 사용자가 입력할 때마다 <code>EasyDebounce.debounce</code>를 호출합니다. </p>
<p>400ms 동안 입력이 발생하지 않으면 <code>viewModel.onSearch</code> 메서드를 호출하여 검색을 시작하게 됩니다.</p>
<h4 id="viewmodel-작성">ViewModel 작성</h4>
<p>ViewModel에서 실제 검색 로직과 데이터 핸들링을 구현해보겠습니다.</p>
<pre><code class="language-dart">class ViewModel with ChangeNotifier {
  List&lt;String&gt; allData = [&#39;apple&#39;, &#39;banana&#39;, &#39;grape&#39;, &#39;orange&#39;];  // 전체 데이터 목록
  List&lt;String&gt; _searchedResults = [];

  List&lt;String&gt; get searchedResults =&gt; _searchedResults;

  void onSearch(String query) {
    final results = allData.where((item) =&gt; item.contains(query)).toList();
    _searchedResults
      ..clear()
      ..addAll(results);
    notifyListeners();
  }
}</code></pre>
<p>이 코드에서는 <code>allData</code> 리스트에 있는 데이터를 기준으로 검색을 수행합니다. <code>query</code>를 포함하는 아이템들만 <code>_searchedResults</code> 리스트에 추가하고, <code>notifyListeners()</code>를 호출하여 UI에 변경 사항을 반영합니다.</p>
<p>이제 사용자가 검색어를 입력할 때마다 실시간으로 (하지만 debounce를 통해 효율적으로) 검색 결과를 확인할 수 있습니다. Debounce 기술은 사용자의 경험을 개선할 뿐만 아니라 성능 최적화에도 큰 도움을 줍니다.</p>
<p>여러분도 이 방법을 통해 프로젝트에 검색 기능을 추가해 보세요! 😊</p>
<blockquote>
<p>🔥 해당 포스팅은 <a href="https://devpost-download.framer.website/">Dev.POST</a> 도움을 받아 작성되었습니다.</p>
</blockquote>
<p> <img src="https://velog.velcdn.com/images/dev_post/post/25d4e36a-d63f-4810-82a8-b095b1505db0/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[비전문가를 위한 Django 셋업 가이드: ML 엔지니어의 도전기]]></title>
            <link>https://velog.io/@dev_post/%EB%B9%84%EC%A0%84%EB%AC%B8%EA%B0%80%EB%A5%BC-%EC%9C%84%ED%95%9C-Django-%EC%85%8B%EC%97%85-%EA%B0%80%EC%9D%B4%EB%93%9C-ML-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EC%9D%98-%EB%8F%84%EC%A0%84%EA%B8%B0</link>
            <guid>https://velog.io/@dev_post/%EB%B9%84%EC%A0%84%EB%AC%B8%EA%B0%80%EB%A5%BC-%EC%9C%84%ED%95%9C-Django-%EC%85%8B%EC%97%85-%EA%B0%80%EC%9D%B4%EB%93%9C-ML-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EC%9D%98-%EB%8F%84%EC%A0%84%EA%B8%B0</guid>
            <pubDate>Fri, 12 Jul 2024 06:19:22 GMT</pubDate>
            <description><![CDATA[<h1 id="django-셋업-및-기본-api-구현-가이드">Django 셋업 및 기본 API 구현 가이드</h1>
<p>안녕하세요, 오늘은 장고(Django)를 이용해 프로젝트를 셋업하고 간단한 API를 만드는 과정을 함께 살펴보겠습니다. 장고는 파이썬 기반의 웹 프레임워크로, 빠르고 효율적인 웹 개발을 가능하게 합니다. </p>
<h2 id="시작하기-전-준비-사항">시작하기 전 준비 사항</h2>
<h3 id="장고란">장고란?</h3>
<p>먼저, 간단히 장고에 대해 알아볼까요? 장고는 파이썬으로 작성된 고수준의 웹 프레임워크로, 신속한 개발과 깔끔한 디자인을 강조합니다. 장고는 자동으로 관리자 인터페이스를 생성해주는 등 많은 편리한 기능을 제공하며, ORM(Object-Relational Mapping)도 제공하여 데이터베이스 연동을 쉽게 할 수 있습니다.</p>
<h2 id="셋업-과정">셋업 과정</h2>
<p>장고 프로젝트를 시작하는 기본 명령어부터 시작해보겠습니다.</p>
<pre><code class="language-bash">django-admin startproject project_name
cd project_name
django-admin startapp app_name</code></pre>
<p>이 명령어를 통해 기본 프로젝트와 앱 구조를 생성할 수 있습니다.</p>
<h2 id="프로젝트-설정-파일">프로젝트 설정 파일</h2>
<p>프로젝트의 설정 파일을 수정하여 필요한 구성요소를 추가합니다. 중요한 설정은 다음과 같습니다:</p>
<h3 id="비밀-키와-debug-모드-설정">비밀 키와 DEBUG 모드 설정</h3>
<p>비밀 키는 보안에 중요합니다. 배포 환경에서는 <code>DEBUG</code>를 <code>False</code>로 설정해야 합니다.</p>
<pre><code class="language-python">SECRET_KEY = &#39;your-secret-key&#39;
DEBUG = True
ALLOWED_HOSTS = [&#39;*&#39;]</code></pre>
<h3 id="설치된-앱">설치된 앱</h3>
<p>앱을 사용하기 위해 <code>INSTALLED_APPS</code>에 추가합니다.</p>
<pre><code class="language-python">INSTALLED_APPS = [
    &#39;django.contrib.admin&#39;,
    &#39;django.contrib.auth&#39;,
    &#39;django.contrib.contenttypes&#39;,
    &#39;django.contrib.sessions&#39;,
    &#39;django.contrib.messages&#39;,
    &#39;django.contrib.staticfiles&#39;,
    &#39;app_name&#39;,
]</code></pre>
<h2 id="데이터베이스-설정">데이터베이스 설정</h2>
<p>기본적으로 SQLite를 사용하지만, 필요에 따라 다른 DB를 사용할 수도 있습니다.</p>
<pre><code class="language-python">DATABASES = {
    &#39;default&#39;: {
        &#39;ENGINE&#39;: &#39;django.db.backends.sqlite3&#39;,
        &#39;NAME&#39;: BASE_DIR / &quot;db.sqlite3&quot;,
    }
}</code></pre>
<h2 id="기본-api-구현하기">기본 API 구현하기</h2>
<p>이제 기본적인 API를 만들어 보겠습니다. 예를 들어, 사용자 등록 API를 만든다고 가정해봅시다.</p>
<h3 id="모델-정의">모델 정의</h3>
<p>먼저, 사용자 모델을 정의합니다.</p>
<pre><code class="language-python">from django.db import models
import uuid

class User(models.Model):
    device_id = models.CharField(max_length=255, unique=True)
    uuid = models.UUIDField(default=uuid.uuid4, unique=True)

    def __str__(self):
        return self.device_id</code></pre>
<h3 id="뷰-작성">뷰 작성</h3>
<p>이제 데이터를 받아 처리할 뷰(view)를 작성합니다.</p>
<pre><code class="language-python">from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
import json
from .models import User

@csrf_exempt
def register_device(request):
    if request.method == &quot;POST&quot;:
        try:
            data = json.loads(request.body)
            device_id = data.get(&quot;device_id&quot;)

            if not device_id:
                return JsonResponse({&quot;error&quot;: &quot;Device ID is required&quot;}, status=400)

            device, created = User.objects.get_or_create(device_id=device_id, defaults={&quot;uuid&quot;: uuid.uuid4()})
            return JsonResponse({&quot;uuid&quot;: str(device.uuid)}, status=201 if created else 200)

        except json.JSONDecodeError:
            return JsonResponse({&quot;error&quot;: &quot;Invalid JSON&quot;}, status=400)

    return JsonResponse({&quot;error&quot;: &quot;Invalid request method&quot;}, status=405)</code></pre>
<h3 id="url-설정">URL 설정</h3>
<p>마지막으로, URL 패턴을 설정하여 API를 호출할 수 있게 합니다.</p>
<pre><code class="language-python">from django.urls import path
from .views import register_device

urlpatterns = [
    path(&#39;register/&#39;, register_device, name=&#39;register_device&#39;),
]</code></pre>
<h2 id="결론">결론</h2>
<p>이제 장고 프로젝트를 셋업하고 기본적인 사용자 등록 API를 구현하는 방법을 알게 되셨습니다. 이 과정을 통해 장고의 강력한 기능을 활용하여 웹 애플리케이션을 효과적으로 개발할 수 있습니다. 다음에는 좀 더 복잡한 기능을 추가하여 프로젝트를 확장해볼까요? Happy Coding! 😊</p>
<blockquote>
<p>🔥 해당 포스팅은 <a href="https://devpost-download.framer.website/">Dev.POST</a> 도움을 받아 작성되었습니다.</p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>