<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발지식소</title>
    <link>https://hyun-dell.tistory.com/</link>
    <description>하루하루 채워가는 개발지식,

복잡한 개념도 쉽고 따뜻하게 설명하는 곳.

초보자도, 현업자도 함께 성장하는 지식 채움의 공간</description>
    <language>ko</language>
    <pubDate>Wed, 6 May 2026 21:32:02 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>지식소 채움이</managingEditor>
    <image>
      <title>개발지식소</title>
      <url>https://tistory1.daumcdn.net/tistory/7960441/attach/e5e4ea1a52474fa8924ce6099d8a9042</url>
      <link>https://hyun-dell.tistory.com</link>
    </image>
    <item>
      <title>Django TestCase 활용방법</title>
      <link>https://hyun-dell.tistory.com/entry/Django-TestCase-%ED%99%9C%EC%9A%A9%EB%B0%A9%EB%B2%95</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Django에서의 테스트 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Django의 테스트는 크게 세 가지 특징이 있습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;unittest 기반&lt;/b&gt;&lt;br /&gt;Python의 unittest 모듈을 확장한 구조라 문법이 친숙합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;독립적인 테스트 데이터베이스&lt;/b&gt;&lt;br /&gt;실제 DB를 건드리지 않고, 테스트 실행 시 임시 데이터베이스가 자동으로 생성/삭제됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다양한 테스트 도구 제공&lt;/b&gt;&lt;br /&gt;클라이언트 시뮬레이션(self.client), ORM 테스트, API 응답 검증 등 웹 프레임워크 특화 기능을 제공합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. TestCase 기본 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드는 일반적으로 앱 디렉터리 안에 있는 tests.py 혹은 tests/ 디렉터리에 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from django.test import TestCase
from django.contrib.auth.models import User

class UserModelTest(TestCase):
    def setUp(self):
        # 테스트 실행 전마다 실행됨
        self.user = User.objects.create_user(username=&quot;testuser&quot;, password=&quot;password123&quot;)

    def test_user_creation(self):
        # User 모델이 정상적으로 생성되는지 확인
        self.assertEqual(self.user.username, &quot;testuser&quot;)

    def test_user_password(self):
        # 비밀번호 검증 메서드 테스트
        self.assertTrue(self.user.check_password(&quot;password123&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;setUp()&lt;/b&gt;: 각 테스트 실행 전마다 초기화 작업을 수행합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;test_로 시작하는 메서드&lt;/b&gt;: 실제 검증 로직을 담습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;assert 메서드&lt;/b&gt;: 기대값과 실제값을 비교합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 주요 assert 메서드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트에서 가장 많이 쓰는 메서드 몇 가지를 정리하면 다음과 같습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;self.assertEqual(a, b) &amp;rarr; 두 값이 같은지 확인&lt;/li&gt;
&lt;li&gt;self.assertNotEqual(a, b) &amp;rarr; 두 값이 다른지 확인&lt;/li&gt;
&lt;li&gt;self.assertTrue(x) / self.assertFalse(x) &amp;rarr; 불리언 조건 확인&lt;/li&gt;
&lt;li&gt;self.assertIn(a, b) &amp;rarr; a가 b 안에 포함되는지 확인&lt;/li&gt;
&lt;li&gt;self.assertRaises(Exception) &amp;rarr; 특정 예외 발생 여부 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 회원가입 시 비밀번호가 짧으면 에러를 발생시키는 테스트를 작성할 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;def test_short_password(self):
    with self.assertRaises(ValueError):
        User.objects.create_user(username=&quot;baduser&quot;, password=&quot;123&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 클라이언트 시뮬레이션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Django는 내장된 &lt;b&gt;테스트 클라이언트&lt;/b&gt;를 제공하여 실제 서버를 띄우지 않고도 HTTP 요청/응답을 테스트할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;from django.urls import reverse

class LoginViewTest(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(username=&quot;tester&quot;, password=&quot;password123&quot;)

    def test_login_success(self):
        response = self.client.post(
            reverse(&quot;login&quot;),
            {&quot;username&quot;: &quot;tester&quot;, &quot;password&quot;: &quot;password123&quot;}
        )
        self.assertEqual(response.status_code, 200)

    def test_login_fail(self):
        response = self.client.post(
            reverse(&quot;login&quot;),
            {&quot;username&quot;: &quot;tester&quot;, &quot;password&quot;: &quot;wrongpass&quot;}
        )
        self.assertEqual(response.status_code, 401)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;self.client.get(url) / self.client.post(url, data) &amp;rarr; 요청을 시뮬레이션합니다.&lt;/li&gt;
&lt;li&gt;response.status_code / response.json() 등을 활용하여 결과를 검증할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. API 테스트 (Django REST Framework와 함께)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Django REST Framework를 쓰는 경우, APIClient를 활용하면 더욱 직관적인 테스트가 가능합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;from rest_framework.test import APITestCase
from django.urls import reverse
from django.contrib.auth.models import User

class AuthAPITest(APITestCase):
    def setUp(self):
        self.user = User.objects.create_user(username=&quot;apiuser&quot;, password=&quot;password123&quot;)

    def test_token_obtain(self):
        url = reverse(&quot;token_obtain_pair&quot;)
        response = self.client.post(url, {&quot;username&quot;: &quot;apiuser&quot;, &quot;password&quot;: &quot;password123&quot;})
        self.assertEqual(response.status_code, 200)
        self.assertIn(&quot;access&quot;, response.data)
        self.assertIn(&quot;refresh&quot;, response.data)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;APITestCase는 TestCase를 확장한 클래스로, JSON 응답 처리와 API 요청 테스트에 최적화되어 있습니다.&lt;/li&gt;
&lt;li&gt;JWT, OAuth, CRUD API 테스트에 많이 활용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 커버리지 측정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트는 단순히 실행만 해서는 효과가 떨어집니다. &lt;b&gt;coverage.py&lt;/b&gt; 같은 도구를 쓰면 코드의 어느 부분이 테스트되었는지 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# 설치
pip install coverage

# 실행
coverage run manage.py test

# 리포트 출력
coverage report
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 어떤 함수가 테스트되었는지, 테스트가 누락된 부분은 어디인지 알 수 있어 품질 관리에 큰 도움이 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 테스트 실행 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 실행은 다음과 같이 간단합니다:&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python manage.py test
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 앱이나 모듈만 실행하려면:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;python manage.py test myapp.tests.UserModelTest
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;Django TestCase 작성 방법&lt;/b&gt;을 기초부터 실습 예제까지 살펴봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Django 테스트는 unittest 기반이며 독립적인 DB 환경에서 실행됩니다.&lt;/li&gt;
&lt;li&gt;TestCase, APITestCase를 활용하면 모델, 뷰, API까지 손쉽게 검증할 수 있습니다.&lt;/li&gt;
&lt;li&gt;self.client를 이용해 클라이언트 요청을 시뮬레이션할 수 있습니다.&lt;/li&gt;
&lt;li&gt;coverage와 같은 도구를 함께 사용하면 테스트 범위를 수치로 관리할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 작성은 처음에는 번거롭지만, 프로젝트가 커질수록 유지보수 비용을 크게 줄여줍니다. 작은 단위부터 꾸준히 작성해 나가면, 안정적이고 신뢰할 수 있는 서비스를 만드는 데 큰 도움이 될 것입니다.&lt;/p&gt;</description>
      <category>백엔드</category>
      <category>django</category>
      <category>DjangoTestCase</category>
      <category>Django테스트</category>
      <category>unittest</category>
      <category>단위테스트</category>
      <category>백엔드개발</category>
      <category>웹개발</category>
      <category>테스트자동화</category>
      <category>파이썬개발</category>
      <author>지식소 채움이</author>
      <guid isPermaLink="true">https://hyun-dell.tistory.com/33</guid>
      <comments>https://hyun-dell.tistory.com/entry/Django-TestCase-%ED%99%9C%EC%9A%A9%EB%B0%A9%EB%B2%95#entry33comment</comments>
      <pubDate>Wed, 24 Sep 2025 08:51:46 +0900</pubDate>
    </item>
    <item>
      <title>CSRF와 XSS 방어</title>
      <link>https://hyun-dell.tistory.com/entry/CSRF%EC%99%80-XSS-%EB%B0%A9%EC%96%B4</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. CSRF(Cross-Site Request Forgery)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSRF는 사용자가 &lt;b&gt;의도하지 않은 요청&lt;/b&gt;을 특정 웹 서비스로 보내게 만드는 공격입니다. 예를 들어, 사용자가 이미 은행 사이트에 로그인한 상태에서 악의적인 사이트를 방문했을 때, 공격자가 준비한 폼이나 스크립트를 통해 자동으로 송금 요청이 전송될 수 있습니다. 서버는 세션 쿠키를 기반으로 해당 요청을 합법적인 사용자 요청으로 오인하기 때문에 문제가 발생합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. 공격 시나리오&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 bank.com에 로그인 &amp;rarr; 세션 쿠키 발급&lt;/li&gt;
&lt;li&gt;공격자가 evil.com에 악성 HTML 폼 배치&lt;/li&gt;
&lt;li&gt;사용자가 evil.com을 방문 &amp;rarr; 브라우저가 자동으로 bank.com/transfer 요청 전송 (쿠키 포함)&lt;/li&gt;
&lt;li&gt;서버는 사용자가 보낸 정상 요청처럼 처리 &amp;rarr; 계좌 이체 성공&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-3. 방어 방법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CSRF 토큰 사용&lt;/b&gt;: 요청마다 서버가 난수 토큰을 발급하고, 클라이언트가 폼 전송 시 반드시 포함하도록 합니다. 서버는 토큰 일치 여부를 검증해 공격을 차단합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SameSite 쿠키 설정&lt;/b&gt;: 세션 쿠키를 SameSite=Lax 또는 Strict로 설정하여 다른 도메인에서 자동 전송되지 않도록 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중요 요청은 GET 대신 POST 사용&lt;/b&gt;: GET 요청은 브라우저가 예측하지 못한 상황에서 자동 실행될 수 있으므로, 데이터 변경은 반드시 POST/PUT/DELETE로 제한합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-4. Django에서의 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Django는 기본적으로 CSRF 보호를 내장하고 있습니다. 템플릿 내 폼에 {% csrf_token %}을 추가하면 자동으로 토큰이 삽입됩니다.&lt;/p&gt;
&lt;pre class=&quot;django&quot;&gt;&lt;code&gt;&amp;lt;form method=&quot;post&quot; action=&quot;/transfer/&quot;&amp;gt;
  {% csrf_token %}
  &amp;lt;input type=&quot;text&quot; name=&quot;to_account&quot;&amp;gt;
  &amp;lt;input type=&quot;number&quot; name=&quot;amount&quot;&amp;gt;
  &amp;lt;button type=&quot;submit&quot;&amp;gt;송금&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰에서 @csrf_protect 데코레이터 또는 CsrfViewMiddleware를 통해 토큰 검증이 자동 수행됩니다.&lt;br /&gt;API 서버의 경우, React/Vue 같은 프런트엔드와 연동 시 &lt;b&gt;CSRF 토큰을 헤더에 포함&lt;/b&gt;해야 하며, Django REST Framework에서는 X-CSRFToken 헤더를 기본적으로 지원합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. XSS(Cross-Site Scripting)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XSS는 공격자가 악성 스크립트를 사용자 브라우저에서 실행하게 하는 공격입니다. 사용자가 입력한 값이 적절히 필터링되지 않고 HTML에 삽입될 때 발생합니다. 이를 통해 공격자는 세션 탈취, 키로깅, 피싱 등 다양한 공격을 수행할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. 공격 유형&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;저장형 XSS(Persistent)&lt;/b&gt;: 악성 스크립트가 DB에 저장된 뒤 여러 사용자에게 노출됩니다. (예: 게시판 댓글에 &amp;lt;script&amp;gt; alert('해킹')&amp;lt;/script&amp;gt; 삽입)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;반사형 XSS(Reflected)&lt;/b&gt;: URL 파라미터에 포함된 스크립트가 그대로 응답에 반영됩니다. (예: /search? q=&amp;lt;script&amp;gt;...)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DOM 기반 XSS&lt;/b&gt;: 클라이언트 측 자바스크립트 코드에서 입력값을 검증 없이 DOM에 삽입할 때 발생합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. 방어 방법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;출력 인코딩(Escape)&lt;/b&gt;: HTML에 사용자 입력을 출력할 때 반드시 이스케이프 처리합니다. (&amp;lt; &amp;rarr; &amp;amp;lt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;입력 검증&lt;/b&gt;: 예상치 못한 태그, 이벤트 핸들러(onerror, onclick) 등을 필터링합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Content Security Policy(CSP)&lt;/b&gt;: script-src 'self' 같은 CSP 헤더를 설정하여 외부 스크립트 실행을 제한합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쿠키에 HttpOnly 설정&lt;/b&gt;: 세션 쿠키를 자바스크립트에서 접근할 수 없도록 차단합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-4. Django에서의 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Django 템플릿 엔진은 기본적으로 변수 출력 시 HTML 이스케이프 처리를 수행합니다.&lt;/p&gt;
&lt;pre class=&quot;django&quot;&gt;&lt;code&gt;&amp;lt;p&amp;gt;{{ comment }}&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 공격자가 &amp;lt;script&amp;gt; alert('XSS')&amp;lt;/script&amp;gt;를 입력하더라도 브라우저에는 &amp;amp;lt;script&amp;amp;gt;...&amp;amp;lt;/script&amp;amp;gt;로 표시됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, |safe 필터를 사용할 경우 이스케이프가 비활성화되므로, 반드시 신뢰할 수 있는 데이터에만 적용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 CSP를 적용하려면 Django 미들웨어에서 헤더를 설정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def security_headers(get_response):
    def middleware(request):
        response = get_response(request)
        response['Content-Security-Policy'] = &quot;default-src 'self'&quot;
        response['X-Content-Type-Options'] = &quot;nosniff&quot;
        return response
    return middleware
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CSRF&lt;/b&gt;는 사용자의 세션을 악용해 의도하지 않은 요청을 보내는 공격 &amp;rarr; 방어: CSRF 토큰, SameSite 쿠키, POST 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;XSS&lt;/b&gt;는 악성 스크립트를 브라우저에서 실행시키는 공격 &amp;rarr; 방어: 출력 인코딩, 입력 검증, CSP, HttpOnly 쿠키&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 보안이 &amp;ldquo;한 번 막고 끝&amp;rdquo;이 아니라, **여러 방어 기법을 조합해 다층 방어(Defense in Depth)**를 구축해야 합니다.&lt;br /&gt;예를 들어, Django의 기본 CSRF 보호를 사용하면서, 동시에 SameSite 쿠키를 설정하고, XSS 방지를 위해 CSP와 출력 이스케이프를 병행하는 식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션은 사용자 데이터와 신뢰를 기반으로 운영됩니다. CSRF와 XSS에 대한 이해와 방어는 개발자의 기본 역량이며, 이를 바탕으로 안전하고 신뢰할 수 있는 서비스를 제공할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>백엔드</category>
      <category>api보안</category>
      <category>csrf</category>
      <category>jwt</category>
      <category>restapi</category>
      <category>XSS</category>
      <category>백엔드개발</category>
      <category>보안취약점</category>
      <category>웹개발</category>
      <category>웹개발보안</category>
      <category>웹보안</category>
      <author>지식소 채움이</author>
      <guid isPermaLink="true">https://hyun-dell.tistory.com/32</guid>
      <comments>https://hyun-dell.tistory.com/entry/CSRF%EC%99%80-XSS-%EB%B0%A9%EC%96%B4#entry32comment</comments>
      <pubDate>Thu, 18 Sep 2025 08:20:19 +0900</pubDate>
    </item>
    <item>
      <title>Docker 멀티 컨테이너와 Compose, CI/CD 배포</title>
      <link>https://hyun-dell.tistory.com/entry/Docker-%EB%A9%80%ED%8B%B0-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%99%80-Compose-CICD-%EB%B0%B0%ED%8F%AC</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 멀티 컨테이너 구성의 필요성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너는 &amp;ldquo;하나의 컨테이너 = 하나의 프로세스&amp;rdquo;라는 원칙을 따르는 것이 이상적입니다. 즉, 하나의 컨테이너에 웹 서버, DB, 캐시를 모두 넣기보다 역할에 따라 분리하는 것이 바람직합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;b&gt;web&lt;/b&gt;: Django 또는 Flask와 같은 애플리케이션 서버&lt;/li&gt;
&lt;li&gt;&lt;b&gt;db&lt;/b&gt;: PostgreSQL, MySQL 등 데이터베이스 서버&lt;/li&gt;
&lt;li&gt;&lt;b&gt;cache&lt;/b&gt;: Redis, Memcached&lt;/li&gt;
&lt;li&gt;&lt;b&gt;proxy&lt;/b&gt;: Nginx, Caddy와 같은 리버스 프락시/로드밸런서&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;b&gt;유지보수성&lt;/b&gt;: 특정 서비스에 문제가 생겨도 해당 컨테이너만 재시작 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장성&lt;/b&gt;: 트래픽이 몰리면 웹 컨테이너만 수평 확장 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;표준화&lt;/b&gt;: 각 컨테이너가 독립적이므로 다른 프로젝트에도 쉽게 재활용 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Docker Compose 기본 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 컨테이너 환경을 일일이 docker run 명령어로 관리하는 것은 번거롭습니다. 이를 해결해 주는 도구가 &lt;b&gt;Docker Compose&lt;/b&gt;입니다.&lt;br /&gt;docker-compose.yml 파일 하나로 서비스 정의, 네트워크, 볼륨을 함께 관리할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시: Django + PostgreSQL + Nginx&lt;/h3&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;version: '3.8'

services:
  web:
    build: ./app
    container_name: django_app
    command: gunicorn myproject.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - ./app:/usr/src/app
    ports:
      - &quot;8000:8000&quot;
    depends_on:
      - db
    environment:
      - DATABASE_URL=postgres://user:password@db:5432/mydb

  db:
    image: postgres:14
    container_name: postgres_db
    restart: always
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data

  nginx:
    image: nginx:latest
    container_name: nginx_proxy
    volumes:
      - ./nginx/conf:/etc/nginx/conf.d
    ports:
      - &quot;80:80&quot;
    depends_on:
      - web

volumes:
  postgres_data:
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 포인트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;depends_on: 컨테이너 간 실행 순서 보장&lt;/li&gt;
&lt;li&gt;volumes: 데이터 영속성 확보&lt;/li&gt;
&lt;li&gt;environment: 환경변수를 통해 유연하게 설정&lt;/li&gt;
&lt;li&gt;ports: 외부와 연결할 포트 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 정의된 Compose 파일은 docker-compose up -d 한 줄로 실행 가능합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 로컬 개발 vs 운영 환경 분리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose는 환경에 따라 설정을 다르게 가져갈 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;로컬 개발&lt;/b&gt;: 코드 볼륨 마운트, 디버깅을 위한 포트 개방&lt;/li&gt;
&lt;li&gt;&lt;b&gt;운영 환경&lt;/b&gt;: 보안 강화, 불필요한 포트 차단, 로그 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 docker-compose.override.yml을 두어 로컬에서는 디버깅용 설정을 추가하고, 서버에서는 배포용 설정을 적용할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 배포 파이프라인(CI/CD) 구성하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 컨테이너 구성을 마쳤다면 이제 남은 과제는 &lt;b&gt;자동 배포&lt;/b&gt;입니다. 개발자가 코드를 푸시할 때마다 자동으로 빌드, 테스트, 배포가 이루어지면 생산성이 크게 향상됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CI/CD 기본 흐름&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;코드 푸시&lt;/b&gt; (GitHub, GitLab 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CI 서버&lt;/b&gt; (GitHub Actions, GitLab CI, Jenkins 등)이 Docker 이미지를 빌드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 실행&lt;/b&gt; (단위 테스트, 통합 테스트)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이미지 레지스트리에 푸시&lt;/b&gt; (Docker Hub, AWS ECR 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;운영 서버에서 pull &amp;amp; 재배포&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GitHub Actions 예시&lt;/h3&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;name: CI/CD Pipeline

on:
  push:
    branches: [ &quot;main&quot; ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Build Docker image
        run: docker build -t myrepo/myapp:${{ github.sha }} .

      - name: Login to DockerHub
        run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin

      - name: Push image
        run: docker push myrepo/myapp:${{ github.sha }}

  deploy:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: SSH to server and deploy
        uses: appleboy/ssh-action@v0.1.7
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            docker pull myrepo/myapp:${{ github.sha }}
            docker-compose down
            docker-compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 실무에서의 고려사항&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;보안&lt;/b&gt;: 환경 변수 관리 시 env 파일과 Secret Manager 활용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모니터링&lt;/b&gt;: Prometheus, Grafana, ELK 스택 연동&lt;/li&gt;
&lt;li&gt;&lt;b&gt;무중단 배포&lt;/b&gt;: Blue-Green Deployment, Rolling Update 전략 적용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스케일링&lt;/b&gt;: 필요하다면 Docker Swarm이나 Kubernetes로 확장 고려&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 컨테이너 구성은 현대 애플리케이션에서 사실상 필수적인 패턴입니다. Docker Compose를 통해 개발/운영 환경을 표준화하고, CI/CD 파이프라인까지 구축하면 프로젝트 생산성과 안정성이 크게 향상됩니다. 작은 사이드 프로젝트라도 초기부터 이 구조를 도입하면, 추후 확장과 협업이 훨씬 수월해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로는 Compose에서 한 단계 나아가 &lt;b&gt;Kubernetes&lt;/b&gt; 같은 오케스트레이션 도구로 확장하는 것도 고려할 수 있습니다. 그러나 그 출발점은 멀티 컨테이너와 Compose라는 점을 기억하면 좋겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>백엔드</category>
      <category>CICD</category>
      <category>Container</category>
      <category>docker</category>
      <category>dockerCompose</category>
      <category>멀티컨테이너</category>
      <category>배포자동화</category>
      <category>백엔드개발</category>
      <category>서버관리</category>
      <category>웹개발</category>
      <category>클라우드배포</category>
      <author>지식소 채움이</author>
      <guid isPermaLink="true">https://hyun-dell.tistory.com/31</guid>
      <comments>https://hyun-dell.tistory.com/entry/Docker-%EB%A9%80%ED%8B%B0-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%99%80-Compose-CICD-%EB%B0%B0%ED%8F%AC#entry31comment</comments>
      <pubDate>Tue, 2 Sep 2025 08:21:35 +0900</pubDate>
    </item>
    <item>
      <title>Next.js App Router vs Pages Router 비교</title>
      <link>https://hyun-dell.tistory.com/entry/Nextjs-App-Router-vs-Pages-Router-%EB%B9%84%EA%B5%90</link>
      <description>&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;h2 data-ke-size=&quot;size26&quot;&gt;1. Pages Router 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Pages Router&lt;/b&gt;는 Next.js 1.x부터 존재했던 기존 라우팅 방식입니다. pages/ 디렉터리 안에 파일을 생성하면 그 파일명이 자동으로 라우트가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;pages/
 ┣ index.js         &amp;rarr; /
 ┣ about.js         &amp;rarr; /about
 ┗ blog/[id].js     &amp;rarr; /blog/:id
&lt;/code&gt;&lt;/pre&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;getStaticProps, getServerSideProps 등 데이터 패칭 함수 제공&lt;/li&gt;
&lt;li&gt;Link 컴포넌트를 통한 클라이언트 라우팅 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  특징: &lt;b&gt;학습 곡선이 낮고 직관적&lt;/b&gt;, 기존 프로젝트에서 많이 사용됨.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. App Router 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;App Router&lt;/b&gt;는 Next.js 13에서 실험적으로 도입되고, 14부터 안정화된 새로운 라우팅 시스템입니다.&lt;br /&gt;app/ 디렉터리 안에서 라우트를 정의하며, React 18의 &lt;b&gt;서버 컴포넌트(Server Components)&lt;/b&gt; 개념을 활용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;app/
 ┣ page.js          &amp;rarr; /
 ┣ about/
 ┃  ┗ page.js       &amp;rarr; /about
 ┗ blog/
    ┗ [id]/
       ┗ page.js    &amp;rarr; /blog/:id
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;React Server Component(RSC) 지원&lt;/li&gt;
&lt;li&gt;layout.js를 통한 레이아웃 중첩/공유&lt;/li&gt;
&lt;li&gt;loading.js, error.js, not-found.js 등 라우트 단위 특수 파일 제공&lt;/li&gt;
&lt;li&gt;fetch 내장 서버 캐싱, Streaming &amp;amp; Suspense 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  특징: &lt;b&gt;서버 중심 렌더링&lt;/b&gt;과 &lt;b&gt;구조화된 라우팅 경험&lt;/b&gt;을 제공.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 주요 차이점 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구분 Pages Router App Router&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&gt;디렉토리&lt;/td&gt;
&lt;td&gt;pages/&lt;/td&gt;
&lt;td&gt;app/&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 패칭&lt;/td&gt;
&lt;td&gt;getStaticProps, getServerSideProps, getInitialProps&lt;/td&gt;
&lt;td&gt;서버 컴포넌트 안에서 직접 fetch 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;컴포넌트 종류&lt;/td&gt;
&lt;td&gt;기본적으로 클라이언트 컴포넌트&lt;/td&gt;
&lt;td&gt;서버 컴포넌트(기본), 필요시 &quot;use client&quot; 선언&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;레이아웃&lt;/td&gt;
&lt;td&gt;_app.js, _document.js 단일 구조&lt;/td&gt;
&lt;td&gt;layout.js 중첩 가능 (페이지별 공유 레이아웃 손쉽게 구현)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;특수 파일&lt;/td&gt;
&lt;td&gt;_app.js, _document.js, _error.js&lt;/td&gt;
&lt;td&gt;layout.js, error.js, loading.js, not-found.js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;코드 스플리팅&lt;/td&gt;
&lt;td&gt;자동 지원&lt;/td&gt;
&lt;td&gt;자동 + 서버 컴포넌트 기반 더 정교한 스플리팅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO&lt;/td&gt;
&lt;td&gt;SSR/SSG 모두 지원&lt;/td&gt;
&lt;td&gt;SSR/SSG + 서버 컴포넌트로 더 효율적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;학습 난이도&lt;/td&gt;
&lt;td&gt;상대적으로 쉬움&lt;/td&gt;
&lt;td&gt;초기 진입 장벽 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 예제 비교&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pages Router에서 데이터 패칭&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// pages/blog/[id].js
export async function getServerSideProps(context) {
  const res = await fetch(`https://api.example.com/blog/${context.params.id}`);
  const post = await res.json();

  return { props: { post } };
}

export default function BlogPost({ post }) {
  return&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;{post.title}&lt;/h1&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;App Router에서 데이터 패칭&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// app/blog/[id]/page.js
export default async function BlogPost({ params }) {
  const res = await fetch(`https://api.example.com/blog/${params.id}`, {
    cache: &quot;no-store&quot;, // SSR
  });
  const post = await res.json();

  return&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;{post.title}&lt;/h1&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  App Router에서는 **서버 컴포넌트 자체에서 fetch**를 직접 호출할 수 있습니다. 별도의 getServerSideProps 없이 깔끔하게 API 호출이 가능하죠.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 장단점 정리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pages Router&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;빠르게 MVP나 소규모 프로젝트 구축 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;App Router&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 장점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 컴포넌트 지원 (더 적은 JS 번들, 성능 향상)&lt;/li&gt;
&lt;li&gt;layout.js를 통한 강력한 레이아웃 구조&lt;/li&gt;
&lt;li&gt;Streaming &amp;amp; Suspense로 빠른 로딩 UX&lt;/li&gt;
&lt;li&gt;데이터 패칭 단순화 (fetch 내장)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;Pages Router 대비 생태계 자료가 아직 부족&lt;/li&gt;
&lt;li&gt;일부 라이브러리는 호환 이슈 발생 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 어떤 걸 선택해야 할까?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;신규 프로젝트&lt;/b&gt;라면   &lt;b&gt;App Router&lt;/b&gt; 권장&lt;br /&gt;(React 18 기능 활용, 구조화된 개발 경험, 장기적으로 표준)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기존 프로젝트 유지/점진적 이전&lt;/b&gt;   &lt;b&gt;Pages Router 유지 + 점진적 마이그레이션&lt;/b&gt;&lt;br /&gt;(대규모 서비스라면 한 번에 갈아타는 것보다는 모듈 단위로 이전하는 게 안전)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js는 단순히 React를 SSR/SSG로 감싸주는 프레임워크에서, 이제는 &lt;b&gt;풀스택 웹 애플리케이션 플랫폼&lt;/b&gt;으로 진화하고 있습니다.&lt;br /&gt;App Router는 그 중심에 있으며, 앞으로 Next.js의 &lt;b&gt;메인 라우팅 시스템&lt;/b&gt;으로 자리 잡을 것이 확실합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 학습 난이도가 조금 높더라도, 장기적으로는 &lt;b&gt;App Router를 익히고 실무에 적용&lt;/b&gt;하는 것이 큰 도움이 될 것입니다.&lt;br /&gt;다만 Pages Router 역시 여전히 강력하고 안정적인 선택지이므로, &lt;b&gt;프로젝트 상황에 맞게 적절히 선택&lt;/b&gt;하는 것이 최선입니다.&lt;/p&gt;</description>
      <category>프론트엔드</category>
      <category>app</category>
      <category>frontend</category>
      <category>nextjs</category>
      <category>Pages</category>
      <category>react</category>
      <category>Router</category>
      <category>ssg</category>
      <category>SSR</category>
      <category>웹개발</category>
      <category>프론트엔드</category>
      <author>지식소 채움이</author>
      <guid isPermaLink="true">https://hyun-dell.tistory.com/30</guid>
      <comments>https://hyun-dell.tistory.com/entry/Nextjs-App-Router-vs-Pages-Router-%EB%B9%84%EA%B5%90#entry30comment</comments>
      <pubDate>Wed, 27 Aug 2025 08:30:00 +0900</pubDate>
    </item>
    <item>
      <title>Tailwind CSS or ShadCN UI 가이드</title>
      <link>https://hyun-dell.tistory.com/entry/Tailwind-CSS-or-ShadCN-UI-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Tailwind CSS란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tailwind CSS는 &lt;b&gt;Utility-first&lt;/b&gt; 접근 방식을 기반으로 한 CSS 프레임워크입니다.&lt;br /&gt;일반적인 CSS 프레임워크(예: Bootstrap)가 미리 정의된 컴포넌트를 제공하는 것과 달리, Tailwind는 &lt;b&gt;작은 단위의 클래스&lt;/b&gt;를 조합해 원하는 UI를 직접 만들어 나가는 방식입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Tailwind의 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Utility-first&lt;/b&gt;: bg-blue-500, text-center, p-4 같은 작은 단위의 클래스를 조합.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;빠른 프로토타이핑&lt;/b&gt;: 별도의 CSS 파일 없이 바로 클래스 조합으로 디자인 가능.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;높은 커스터마이징&lt;/b&gt;: tailwind.config.js에서 색상, 폰트, spacing 단위를 직접 설정.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;반응형 대응이 쉬움&lt;/b&gt;: sm:, md:, lg: 같은 접두어로 반응형 스타일 지정.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 코드&lt;/h3&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;&amp;lt;button className=&quot;bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded&quot;&amp;gt;
  Click Me
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 불필요한 CSS 작성 없이도 클래스 조합으로 깔끔한 버튼을 만들 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. ShadCN UI란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ShadCN UI는 &lt;b&gt;Tailwind CSS를 기반으로 한 컴포넌트 라이브러리&lt;/b&gt;입니다.&lt;br /&gt;Next.js와 함께 자주 쓰이며, Radix UI와 같은 접근성 라이브러리를 내부적으로 활용합니다. 단순히 UI 키트를 제공하는 것이 아니라 &lt;b&gt;컴포넌트 코드를 직접 복사하여 내 프로젝트 안에서 관리&lt;/b&gt;하는 방식이 특징입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ShadCN UI의 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Tailwind 기반&lt;/b&gt;: 스타일은 Tailwind로 처리.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Radix UI 활용&lt;/b&gt;: 접근성이 보장된 UI 컴포넌트 제공.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;직접 관리 방식&lt;/b&gt;: npm으로 설치하는 게 아니라, 컴포넌트 코드를 프로젝트에 포함시켜 사용.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디자인 일관성&lt;/b&gt;: 다크 모드, 컬러 토큰 등 디자인 시스템이 잘 정리되어 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 코드 (ShadCN UI 버튼)&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import { Button } from &quot;@/components/ui/button&quot;

export default function Example() {
  return &amp;lt;Button variant=&quot;destructive&quot;&amp;gt;Delete&amp;lt;/Button&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 단순히 &amp;lt;Button&amp;gt; 태그만으로도 일관된 디자인과 상태(hover, focus, disabled 등)를 제공받을 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Tailwind vs ShadCN UI 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구분 Tailwind CSS ShadCN UI&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&gt;&lt;b&gt;성격&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;CSS 프레임워크&lt;/td&gt;
&lt;td&gt;컴포넌트 라이브러리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;사용 방식&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;클래스 조합&lt;/td&gt;
&lt;td&gt;컴포넌트 호출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;자유도&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;매우 높음&lt;/td&gt;
&lt;td&gt;중간 (제공된 디자인 패턴 안에서 조정)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;러닝 커브&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;처음엔 다소 헷갈리지만 빠르게 적응 가능&lt;/td&gt;
&lt;td&gt;Tailwind 지식이 있으면 쉽게 사용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;커스터마이징&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;config.js에서 무한 확장 가능&lt;/td&gt;
&lt;td&gt;컴포넌트 코드 직접 수정 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;사용 예시&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;완전히 커스텀 디자인이 필요할 때&lt;/td&gt;
&lt;td&gt;빠르게 통일감 있는 UI를 구축할 때&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 언제 무엇을 선택해야 할까?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Tailwind CSS만 사용하는 경우&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;완전히 커스텀 된 UI가 필요할 때&lt;/li&gt;
&lt;li&gt;디자이너와 협업하며 픽셀 단위까지 조정이 필요한 프로젝트&lt;/li&gt;
&lt;li&gt;디자인 시스템을 직접 정의하고 싶을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ShadCN UI를 사용하는 경우&lt;/b&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;일관된 UI를 유지하면서도 필요할 경우 수정할 수 있는 유연성이 필요할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 결론적으로 &lt;b&gt;Tailwind는 &amp;ldquo;디자인 도구&amp;rdquo;&lt;/b&gt;, **ShadCN은 &amp;ldquo;디자인 시스템의 구현체&amp;rdquo;**라고 이해하면 쉽습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 함께 쓰면 더 강력하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 많은 개발자들이 Tailwind와 ShadCN UI를 함께 사용합니다.&lt;br /&gt;ShadCN의 기본 컴포넌트를 활용하면서, 필요에 따라 Tailwind 유틸리티 클래스를 추가해 세부적인 스타일을 조정하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { Button } from &quot;@/components/ui/button&quot;

export default function Example() {
  return (
    &amp;lt;Button className=&quot;bg-gradient-to-r from-purple-500 to-pink-500&quot;&amp;gt;
      Custom Gradient
    &amp;lt;/Button&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 기본 버튼 컴포넌트에 Tailwind 유틸리티를 조합해 디자인 변형이 가능합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tailwind CSS와 ShadCN UI는 경쟁 관계라기보다는 &lt;b&gt;상호 보완적인 도구&lt;/b&gt;입니다.&lt;br /&gt;Tailwind는 &amp;ldquo;재료&amp;rdquo;, ShadCN UI는 &amp;ldquo;조리법&amp;rdquo;에 가깝습니다. 원하는 UI를 직접 조합해서 만들 수도 있고, 검증된 패턴을 가져다 빠르게 쓸 수도 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Tailwind만 사용&lt;/b&gt; &amp;rarr; 자유도 최우선&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ShadCN UI만 사용&lt;/b&gt; &amp;rarr; 생산성과 일관성 우선&lt;/li&gt;
&lt;li&gt;&lt;b&gt;함께 사용&lt;/b&gt; &amp;rarr; 가장 효율적&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 성격에 맞춰 적절히 선택하거나 병행하면, 더 빠르고 일관된 개발이 가능합니다.&lt;/p&gt;</description>
      <category>프론트엔드</category>
      <category>CSS</category>
      <category>nextjs</category>
      <category>react</category>
      <category>React개발</category>
      <category>shadcnui</category>
      <category>tailwindCSS</category>
      <category>웹개발</category>
      <category>프로튼엔드</category>
      <author>지식소 채움이</author>
      <guid isPermaLink="true">https://hyun-dell.tistory.com/29</guid>
      <comments>https://hyun-dell.tistory.com/entry/Tailwind-CSS-or-ShadCN-UI-%EA%B0%80%EC%9D%B4%EB%93%9C#entry29comment</comments>
      <pubDate>Tue, 26 Aug 2025 08:22:30 +0900</pubDate>
    </item>
    <item>
      <title>Next.js 프레임워크 SSR vs SSG 차이 완벽 정리</title>
      <link>https://hyun-dell.tistory.com/entry/Nextjs-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-SSR-vs-SSG-%EC%B0%A8%EC%9D%B4-%EC%99%84%EB%B2%BD-%EC%A0%95%EB%A6%AC</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. SSR(Server Side Rendering)이란?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSR은 사용자가 페이지에 접속했을 때 &lt;b&gt;서버에서 HTML을 즉시 생성해 전달&lt;/b&gt;하는 방식입니다. 브라우저는 완성된 HTML을 받아 곧바로 화면을 표시할 수 있고, 이후 자바스크립트가 실행되면서 인터랙티브 한 React 앱이 동작하게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;실시간 데이터 반영 가능&lt;/b&gt;: 요청 시마다 서버에서 데이터를 가져와 화면에 반영.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SEO 친화적&lt;/b&gt;: 검색 엔진이 완성된 HTML을 바로 수집 가능.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최초 응답 속도(FCP)는 빠름&lt;/b&gt;: 빈 화면 대신 바로 HTML을 내려주기 때문.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 예시&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// pages/index.js
export async function getServerSideProps() {
  const res = await fetch(&quot;https://api.example.com/data&quot;);
  const data = await res.json();

  return { props: { data } };
}

export default function Home({ data }) {
  return&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;서버에서 불러온 데이터: {data.message}&lt;/div&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  getServerSideProps 함수는 매 요청마다 실행되어 서버에서 데이터를 가져온 뒤 페이지를 생성합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. SSG(Static Site Generation)이란?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSG는 &lt;b&gt;빌드 시점에 HTML을 미리 생성해 두고,&lt;/b&gt; 사용자가 페이지를 요청하면 서버가 캐싱된 HTML을 그대로 반환하는 방식입니다. 즉, 페이지를 미리 만들어 두는 정적 사이트 생성 방식이죠.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;매우 빠른 응답 속도&lt;/b&gt;: HTML이 이미 준비되어 있으므로 서버 부하가 적음.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CDN 배포 최적화&lt;/b&gt;: 정적 파일이므로 전 세계 CDN에 배포 가능.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 변경 반영이 어렵다&lt;/b&gt;: 빌드 시점 이후 데이터가 변해도 반영되지 않음.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 예시&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// pages/index.js
export async function getStaticProps() {
  const res = await fetch(&quot;https://api.example.com/data&quot;);
  const data = await res.json();

  return { props: { data } };
}

export default function Home({ data }) {
  return&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;빌드 시 생성된 데이터: {data.message}&lt;/div&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  getStaticProps는 빌드 시점에만 실행되므로, 데이터가 자주 변하지 않는 페이지에 적합합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. SSR vs SSG 차이 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구분 SSR (Server Side Rendering) SSG (Static Site Generation)&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&gt;HTML 생성 시점&lt;/td&gt;
&lt;td&gt;&lt;b&gt;요청 시마다&lt;/b&gt; 서버에서 생성&lt;/td&gt;
&lt;td&gt;&lt;b&gt;빌드 시점&lt;/b&gt;에 미리 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;속도&lt;/td&gt;
&lt;td&gt;첫 요청은 빠름, 요청이 많으면 서버 부하 발생&lt;/td&gt;
&lt;td&gt;매우 빠름, CDN 캐싱 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 갱신&lt;/td&gt;
&lt;td&gt;요청마다 최신 데이터 반영&lt;/td&gt;
&lt;td&gt;빌드 후 데이터 변경 반영 어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버 의존성&lt;/td&gt;
&lt;td&gt;서버 필요&lt;/td&gt;
&lt;td&gt;서버 없이도 가능 (정적 배포)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;활용 예시&lt;/td&gt;
&lt;td&gt;뉴스, 날씨, 쇼핑몰 상품 페이지&lt;/td&gt;
&lt;td&gt;블로그, 문서 사이트, 회사 소개 페이지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 혼합 사용: ISR(Incremental Static Regeneration)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js는 SSR과 SSG의 장점을 결합한 &lt;b&gt;ISR&lt;/b&gt;도 제공합니다. ISR은 정적으로 생성된 페이지를 일정 주기(revalidate)마다 다시 빌드해 최신 데이터를 반영할 수 있는 기능입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export async function getStaticProps() {
  const res = await fetch(&quot;https://api.example.com/data&quot;);
  const data = await res.json();

  return {
    props: { data },
    revalidate: 60, // 60초마다 페이지 재생성
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  이 방식은 &lt;b&gt;대부분의 페이지를 정적으로 처리하면서도 최신성을 보장&lt;/b&gt;할 수 있어, 블로그나 쇼핑몰 등에서 널리 사용됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 실무 활용 사례&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SSR 추천 상황&lt;/b&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SSG 추천 상황&lt;/b&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ISR 추천 상황&lt;/b&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;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js는 단순히 React 앱을 만들 수 있는 프레임워크가 아니라, &lt;b&gt;다양한 렌더링 전략을 제공하는 풀스택 도구&lt;/b&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;b&gt;SSR&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;변하지 않는 콘텐츠가 많고 빠른 속도가 필요하다면 &lt;b&gt;SSG&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;두 가지 장점을 절충하고 싶다면 &lt;b&gt;ISR&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황에 맞는 방식을 선택하면 성능과 SEO, 그리고 개발 효율성까지 모두 챙길 수 있습니다. 앞으로 프로젝트를 설계할 때는 단순히 &amp;ldquo;React로 만든다&amp;rdquo;에서 한 걸음 더 나아가, &lt;b&gt;Next.js의 렌더링 전략&lt;/b&gt;을 함께 고려해 보시길 권장합니다.&lt;/p&gt;</description>
      <category>프론트엔드</category>
      <category>nextjs</category>
      <category>SEO최적화</category>
      <category>ssg</category>
      <category>SSR</category>
      <category>웹개발</category>
      <category>최적화</category>
      <category>프론트엔드개발</category>
      <author>지식소 채움이</author>
      <guid isPermaLink="true">https://hyun-dell.tistory.com/28</guid>
      <comments>https://hyun-dell.tistory.com/entry/Nextjs-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-SSR-vs-SSG-%EC%B0%A8%EC%9D%B4-%EC%99%84%EB%B2%BD-%EC%A0%95%EB%A6%AC#entry28comment</comments>
      <pubDate>Sun, 24 Aug 2025 15:50:43 +0900</pubDate>
    </item>
    <item>
      <title>Next.js로 블로그 만들기</title>
      <link>https://hyun-dell.tistory.com/entry/Nextjs%EB%A1%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Next.js란 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js는 &lt;b&gt;React 애플리케이션 개발을 위한 프레임워크&lt;/b&gt;입니다. React만으로도 블로그를 만들 수 있지만, Next.js는 다음과 같은 기능을 제공해 훨씬 효율적인 개발이 가능합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일 기반 라우팅&lt;/b&gt;: pages 폴더에 파일을 추가하는 것만으로 자동 라우팅&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SSR(서버사이드 렌더링)&lt;/b&gt; &amp;amp; &lt;b&gt;SSG(정적 사이트 생성)&lt;/b&gt; 지원 &amp;rarr; SEO 최적화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;API Routes&lt;/b&gt; &amp;rarr; 별도 백엔드 없이 간단한 API 구현 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이미지 최적화&lt;/b&gt; &amp;rarr; 성능 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  즉, 블로그와 같은 콘텐츠 중심 웹사이트를 구축하는 데 최적화된 프레임워크입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 개발 환경 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트 생성&lt;/h3&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;npx create-next-app@latest my-blog
cd my-blog
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http://localhost:3000에 접속하면 기본 Next.js 앱이 실행됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;폴더 구조&lt;/h3&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;my-blog/
 ├─ pages/         # 라우팅되는 페이지
 ├─ public/        # 정적 파일(이미지, favicon 등)
 ├─ styles/        # CSS, 글로벌 스타일
 └─ package.json
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 블로그 페이지 만들기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 홈 페이지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pages/index.js 수정:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function Home() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;개발지식소 블로그&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;Next.js로 만든 나만의 개발 블로그&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 글 상세 페이지 (파일 기반 라우팅)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pages/posts/first-post.js 생성:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function FirstPost() {
  return (
    &amp;lt;article&amp;gt;
      &amp;lt;h1&amp;gt;첫 번째 블로그 글&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;Next.js를 활용한 블로그 만들기 튜토리얼입니다.&amp;lt;/p&amp;gt;
    &amp;lt;/article&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  /posts/first-post 경로로 접속 가능!&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Markdown 지원하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그라면 글을 Markdown으로 작성하고 싶을 때가 많습니다. gray-matter와 remark 라이브러리를 사용하면 쉽게 구현할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install gray-matter remark remark-html
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;posts/hello.md 파일 작성:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;---
title: &quot;Hello Next.js&quot;
date: &quot;2025-08-21&quot;
---

이것은 마크다운으로 작성된 첫 블로그 글입니다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lib/posts.js에서 Markdown 파싱:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import fs from &quot;fs&quot;;
import path from &quot;path&quot;;
import matter from &quot;gray-matter&quot;;

const postsDirectory = path.join(process.cwd(), &quot;posts&quot;);

export function getSortedPostsData() {
  const fileNames = fs.readdirSync(postsDirectory);
  return fileNames.map(fileName =&amp;gt; {
    const filePath = path.join(postsDirectory, fileName);
    const fileContents = fs.readFileSync(filePath, &quot;utf8&quot;);
    const { data } = matter(fileContents);
    return { ...data, id: fileName.replace(/\.md$/, &quot;&quot;) };
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  이렇게 하면 posts 폴더에 Markdown 파일을 추가하는 것만으로 새로운 글이 자동 반영됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 블로그 레이아웃 꾸미기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그는 단순히 글만 보여주는 게 아니라, &lt;b&gt;레이아웃과 스타일링&lt;/b&gt;도 필요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 전역 스타일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;styles/globals.css 수정:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;body {
  font-family: sans-serif;
  margin: 0;
  padding: 20px;
  background: #fafafa;
}
h1 {
  color: #333;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 레이아웃 컴포넌트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;components/Layout.js:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function Layout({ children }) {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;header&amp;gt;
        &amp;lt;h2&amp;gt;개발지식소 블로그&amp;lt;/h2&amp;gt;
      &amp;lt;/header&amp;gt;
      &amp;lt;main&amp;gt;{children}&amp;lt;/main&amp;gt;
      &amp;lt;footer&amp;gt;
        &amp;lt;p&amp;gt;&amp;copy; 2025 개발지식소&amp;lt;/p&amp;gt;
      &amp;lt;/footer&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  각 페이지에서 &amp;lt;Layout&amp;gt;으로 감싸주면 블로그 일관성이 유지됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 배포하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js 블로그를 완성했다면 &lt;b&gt;Vercel&lt;/b&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;a href=&quot;https://vercel.com/&quot;&gt;Vercel&lt;/a&gt; 가입 &amp;rarr; GitHub 연동&lt;/li&gt;
&lt;li&gt;my-blog 레포지토리 push&lt;/li&gt;
&lt;li&gt;Vercel에서 프로젝트 Import &amp;rarr; 자동 빌드 및 배포&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  몇 초 만에 &lt;a href=&quot;https://my-blog.vercel.app&quot;&gt;https://my-blog.vercel.app&lt;/a&gt; 주소로 전 세계 어디서든 접속할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js는 블로그 제작에 최적화된 프레임워크입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일 기반 라우팅&lt;/b&gt;으로 손쉽게 페이지 추가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Markdown 지원&lt;/b&gt;으로 글 작성 편리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Vercel 배포&lt;/b&gt;로 즉시 배포&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 튜토리얼을 따라 했다면 기본적인 블로그 뼈대가 완성되었을 겁니다. 이후에는 댓글 기능, 검색, 태그 분류, 다크 모드 등 다양한 기능을 추가하며 자신만의 블로그로 발전시킬 수 있습니다.&lt;/p&gt;</description>
      <category>프론트엔드</category>
      <category>nextjs</category>
      <category>nextjs튜토리얼</category>
      <category>react</category>
      <category>리액트</category>
      <category>블로그제작</category>
      <category>웹개발</category>
      <category>웹프로그래밍</category>
      <category>프론트엔드</category>
      <author>지식소 채움이</author>
      <guid isPermaLink="true">https://hyun-dell.tistory.com/27</guid>
      <comments>https://hyun-dell.tistory.com/entry/Nextjs%EB%A1%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry27comment</comments>
      <pubDate>Sat, 23 Aug 2025 17:34:56 +0900</pubDate>
    </item>
    <item>
      <title>React 상태 관리 비교: Context, Redux, React Query</title>
      <link>https://hyun-dell.tistory.com/entry/React-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-%EB%B9%84%EA%B5%90-Context-Redux-React-Query</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 상태 관리란 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서 상태(state)는 컴포넌트의 렌더링 결과를 결정하는 데이터입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;지역 상태(Local State):&lt;/b&gt; useState, useReducer 등을 통해 개별 컴포넌트 안에서 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전역 상태(Global State):&lt;/b&gt; 여러 컴포넌트가 공유해야 하는 상태 (예: 사용자 로그인 정보, 테마, 언어 설정 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버 상태(Server State):&lt;/b&gt; 서버에서 가져온 데이터로, 동기화가 필요 (예: 게시글 목록, API 응답 데이터)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React의 상태 관리 도구들은 이 세 가지 범주를 효율적으로 다루는 데 초점을 맞추고 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Context API &amp;ndash; 전역 상태 관리의 기초&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context API는 React가 제공하는 기본 내장 기능으로, &lt;b&gt;props drilling 문제&lt;/b&gt;를 해결하기 위해 등장했습니다.&lt;br /&gt;즉, 여러 단계에 걸쳐 props를 전달하지 않고 전역적으로 데이터를 공급할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// ThemeContext.js
import { createContext } from &quot;react&quot;;

export const ThemeContext = createContext(&quot;light&quot;);

// App.js
&amp;lt;ThemeContext.Provider value=&quot;dark&quot;&amp;gt;
  &amp;lt;Toolbar /&amp;gt;
&amp;lt;/ThemeContext.Provider&amp;gt;

// Toolbar.js
const theme = useContext(ThemeContext);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;React 내장 기능 &amp;rarr; 별도 라이브러리 불필요&lt;/li&gt;
&lt;li&gt;소규모 전역 상태 관리에 적합 (테마, 언어, 인증 여부 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ 단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상태가 커질수록 &lt;b&gt;리렌더링 비용 증가&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;복잡한 로직 관리에는 불편&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  따라서 &lt;b&gt;규모가 작은 앱&lt;/b&gt;이나 &lt;b&gt;간단한 전역 데이터 공유&lt;/b&gt;에는 Context API만으로 충분합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Redux &amp;ndash; 복잡한 전역 상태 관리의 표준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redux는 가장 오래되고 널리 사용되는 상태 관리 라이브러리입니다. 상태를 &lt;b&gt;단일 스토어(Single Store)에&lt;/b&gt; 보관하고, 액션(Action)과 리듀서(Reducer)를 통해 상태를 업데이트합니다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;// store.js
import { configureStore, createSlice } from &quot;@reduxjs/toolkit&quot;;

const counterSlice = createSlice({
  name: &quot;counter&quot;,
  initialState: { value: 0 },
  reducers: {
    increment: (state) =&amp;gt; { state.value += 1 },
    decrement: (state) =&amp;gt; { state.value -= 1 }
  }
});

export const { increment, decrement } = counterSlice.actions;
export const store = configureStore({ reducer: { counter: counterSlice.reducer } });
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 장점&lt;/h3&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;b&gt;Redux DevTools&lt;/b&gt;를 통한 디버깅 가능&lt;/li&gt;
&lt;li&gt;대규모 애플리케이션에서 검증된 패턴&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ 단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보일러플레이트 코드가 많음 (&amp;rarr; Redux Toolkit으로 개선)&lt;/li&gt;
&lt;li&gt;학습 곡선이 상대적으로 가파름&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  Redux는 &lt;b&gt;대규모 프로젝트&lt;/b&gt;에서 팀 단위 협업이 필요할 때, 혹은 상태 추적과 디버깅이 중요한 경우 유리합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. React Query &amp;ndash; 서버 상태 관리의 혁신&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Query는 로컬 상태가 아닌 &lt;b&gt;서버 상태 관리&lt;/b&gt;에 초점을 맞춘 라이브러리입니다.&lt;br /&gt;API 요청과 캐싱, 동기화, 로딩/에러 상태 관리 등을 자동으로 처리해 주기 때문에 &amp;ldquo;Frontend용 데이터 요청 표준&amp;rdquo;으로 불립니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { useQuery } from &quot;@tanstack/react-query&quot;;

function PostList() {
  const { data, isLoading, error } = useQuery({
    queryKey: [&quot;posts&quot;],
    queryFn: () =&amp;gt; fetch(&quot;/api/posts&quot;).then(res =&amp;gt; res.json())
  });

  if (isLoading) return &amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;;
  if (error) return &amp;lt;p&amp;gt;Error...&amp;lt;/p&amp;gt;;

  return (
    &amp;lt;ul&amp;gt;
      {data.map(post =&amp;gt; &amp;lt;li key={post.id}&amp;gt;{post.title}&amp;lt;/li&amp;gt;)}
    &amp;lt;/ul&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;캐싱 자동화&lt;/b&gt; (같은 요청은 재사용)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리페치(refetch)&lt;/b&gt;, &lt;b&gt;무효화(invalidate)&lt;/b&gt;, &lt;b&gt;백그라운드 업데이트&lt;/b&gt; 지원&lt;/li&gt;
&lt;li&gt;API 기반 서비스에 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ 단점&lt;/h3&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  React Query는 &lt;b&gt;서버 데이터가 많은 서비스(예: 블로그, 쇼핑몰, 대시보드)에&lt;/b&gt; 특히 효과적입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 어떤 상황에서 무엇을 선택할까?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;소규모 앱 / 간단한 전역 데이터&lt;/b&gt; &amp;rarr; Context API&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대규모 앱 / 복잡한 상태 로직&lt;/b&gt; &amp;rarr; Redux (Redux Toolkit 필수)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;API 중심 서비스 / 서버 데이터 동기화&lt;/b&gt; &amp;rarr; React Query&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 이들을 &lt;b&gt;조합&lt;/b&gt;해서 사용하기도 합니다.&lt;br /&gt;예를 들어,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전역 UI 상태(테마, 다크모드) &amp;rarr; Context API&lt;/li&gt;
&lt;li&gt;복잡한 비즈니스 로직(인증, 권한, 장바구니) &amp;rarr; Redux&lt;/li&gt;
&lt;li&gt;서버 데이터(API 응답, 게시글, 유저 목록) &amp;rarr; React Query&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 &lt;b&gt;역할을 분리&lt;/b&gt;하면 프로젝트가 커져도 관리가 훨씬 수월해집니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React는 useState 하나로 시작하지만, 애플리케이션 규모가 커질수록 다양한 상태 관리 전략이 필요해집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Context API는 &lt;b&gt;작고 단순한 전역 상태&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Redux는 &lt;b&gt;예측 가능한 대규모 전역 상태 관리&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;React Query는 &lt;b&gt;서버 데이터 관리의 혁신&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 가지를 상황에 맞게 적절히 조합하는 것이 현명한 접근입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 React 18+, Next.js와의 결합, 그리고 서버 컴포넌트 시대까지 고려하면 상태 관리의 중요성은 더욱 커질 것입니다.&lt;br /&gt;개발자는 단순히 &amp;ldquo;도구 선택&amp;rdquo;을 넘어서, &lt;b&gt;상태의 성격을 구분하고 최적의 관리 방법을 설계하는 능력&lt;/b&gt;이 필요합니다.&lt;/p&gt;</description>
      <category>프론트엔드</category>
      <category>react</category>
      <category>reactcontext</category>
      <category>reactquery</category>
      <category>redux</category>
      <category>리덕스</category>
      <category>리액트</category>
      <category>상태관리</category>
      <category>웹개발</category>
      <category>코딩공부</category>
      <category>프론트엔드</category>
      <author>지식소 채움이</author>
      <guid isPermaLink="true">https://hyun-dell.tistory.com/26</guid>
      <comments>https://hyun-dell.tistory.com/entry/React-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-%EB%B9%84%EA%B5%90-Context-Redux-React-Query#entry26comment</comments>
      <pubDate>Fri, 22 Aug 2025 23:00:57 +0900</pubDate>
    </item>
    <item>
      <title>React 기본 Hook 성능까지 끌어올리는 사용법</title>
      <link>https://hyun-dell.tistory.com/entry/React-%EA%B8%B0%EB%B3%B8-Hook-%EC%84%B1%EB%8A%A5%EA%B9%8C%EC%A7%80-%EB%81%8C%EC%96%B4%EC%98%AC%EB%A6%AC%EB%8A%94-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;React의 기본 Hook(useState, useEffect, useRef, useContext)은 &amp;ldquo;상태를 쓴다/부수효과를 관리한다&amp;rdquo;로 끝나지 않습니다. &lt;b&gt;언제, 어떻게&lt;/b&gt; 쓰느냐에 따라 렌더 횟수, 불필요한 네트워크 호출, 이벤트 리스너 누수까지 좌우됩니다. 아래는 개념 + 실전 성능 팁을 한 번에 정리한 가이드입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) useState &amp;mdash; 초기화&amp;middot;업데이트만 잘해도 빨라진다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개념 요약&lt;/b&gt;&lt;br /&gt;컴포넌트 로컬 상태를 선언하고 업데이트합니다. 업데이트가 일어나면 컴포넌트는 다시 렌더 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;성능 팁 1: Lazy 초기화로 무거운 계산 1회로 제한&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;// ❌ 매 렌더마다 heavy() 실행됨
const [list, setList] = useState(heavy());

// ✅ 최초 마운트 시에만 heavy() 실행
const [list, setList] = useState(() =&amp;gt; heavy());
&lt;/code&gt;&lt;/pre&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;&lt;b&gt;성능 팁 2: 함수형 업데이트로 안정적 누적&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 이벤트 루프 안에서 여러 번 호출해도 안전
setCount(prev =&amp;gt; prev + 1);
&lt;/code&gt;&lt;/pre&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;&lt;b&gt;성능 팁 3: 파생 값은 상태로 두지 말 것&lt;/b&gt;&lt;br /&gt;리스트 정렬/필터처럼 &lt;b&gt;파생되는 값&lt;/b&gt;은 상태가 아니라 계산 결과로 두고, 필요시 useMemo로 메모이즈 하세요(아래에 예시).&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) useEffect &amp;mdash; &amp;ldquo;언제 실행되는가&amp;rdquo;를 설계하라&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개념 요약&lt;/b&gt;&lt;br /&gt;DOM 반영 이후 실행되는 부수효과(데이터 요청, 구독, 타이머 등)를 선언합니다. 의존성 배열(deps)로 &lt;b&gt;실행 타이밍&lt;/b&gt;을 제어합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;성능 팁 1: 의존성 최소화로 불필요한 호출 차단&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// ❌ query가 바뀔 때마다 네트워크 레이스가 발생할 수 있음
useEffect(() =&amp;gt; {
  fetch(`/api/search?q=${query}`).then(r =&amp;gt; r.json()).then(setData);
}, [query]);

// ✅ 디바운스 + AbortController로 중복 요청 줄이기
const timerRef = useRef(null);
useEffect(() =&amp;gt; {
  if (!query) return;
  const controller = new AbortController();
  clearTimeout(timerRef.current);
  timerRef.current = setTimeout(async () =&amp;gt; {
    try {
      const res = await fetch(`/api/search?q=${query}`, { signal: controller.signal });
      setData(await res.json());
    } catch (_) {/* 취소 시 무시 */}
  }, 300);

  return () =&amp;gt; {
    controller.abort();
    clearTimeout(timerRef.current);
  };
}, [query]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 타이핑 동안 발생하는 &lt;b&gt;중복 요청을 디바운스&lt;/b&gt;하고, 이전 요청은 &lt;b&gt;abort 하여&lt;/b&gt; 브라우저&amp;middot;서버 부담을 낮춥니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;성능 팁 2: 이벤트 리스너는 한 번만 등록&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// ❌ scrollY가 바뀔 때마다 리스너 재등록
useEffect(() =&amp;gt; {
  const onScroll = () =&amp;gt; setScrollY(window.scrollY);
  window.addEventListener('scroll', onScroll);
  return () =&amp;gt; window.removeEventListener('scroll', onScroll);
}, [scrollY]);

// ✅ 리스너는 1회 등록, 내부에서 상태 갱신
useEffect(() =&amp;gt; {
  const onScroll = () =&amp;gt; setScrollY(window.scrollY);
  window.addEventListener('scroll', onScroll);
  return () =&amp;gt; window.removeEventListener('scroll', onScroll);
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성에 상태를 넣어 &lt;b&gt;리스너를 재등록&lt;/b&gt;하는 실수를 줄이면 CPU 스파이크를 예방합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) useRef &amp;mdash; &amp;ldquo;렌더 없이 바뀌는 값&amp;rdquo;의 집&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개념 요약&lt;/b&gt;&lt;br /&gt;렌더 간에 &lt;b&gt;값을 기억&lt;/b&gt;하지만, 값이 바뀌어도 &lt;b&gt;렌더를 유발하지 않는&lt;/b&gt; 저장소입니다. DOM 노드 접근에도 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;성능 팁 1: 고빈도 이벤트(스크롤/리사이즈)에서 쓰로틀&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const ticking = useRef(false);
useEffect(() =&amp;gt; {
  const onScroll = () =&amp;gt; {
    if (ticking.current) return;
    ticking.current = true;
    requestAnimationFrame(() =&amp;gt; {
      setScrollY(window.scrollY); // 프레임당 1번만 갱신
      ticking.current = false;
    });
  };
  window.addEventListener('scroll', onScroll);
  return () =&amp;gt; window.removeEventListener('scroll', onScroll);
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useRef로 &lt;b&gt;작업 중 플래그&lt;/b&gt;를 관리하면 이벤트 폭주 시 렌더 빈도를 억제할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;성능 팁 2: &amp;ldquo;상태가 아닌&amp;rdquo; 캐시 보관&lt;/b&gt;&lt;br /&gt;API 응답 캐시, IntersectionObserver 인스턴스, 타이머 ID처럼 &lt;b&gt;렌더가 필요 없는 값&lt;/b&gt;을 ref에 넣어 재생성&amp;middot;재등록을 막습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4) useMemo / useCallback &amp;mdash; &amp;ldquo;계산/함수의 안정성&amp;rdquo; 확보&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개념 요약&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useMemo: 비싼 계산 결과를 &lt;b&gt;메모이즈&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;useCallback: 함수를 &lt;b&gt;참조 동일성&lt;/b&gt;으로 고정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;성능 팁 1: 무거운 파생값 메모이즈&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const sorted = useMemo(
  () =&amp;gt; items.slice().sort((a, b) =&amp;gt; a.score - b.score),
  [items]
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 배열 정렬&amp;middot;필터링을 렌더마다 반복하는 비용을 줄입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;성능 팁 2: 메모된 자식에게 안정적인 핸들러 전달&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const List = React.memo(({ items, onSelect }) =&amp;gt; /* ... */);

function Parent({ items }) {
  const onSelect = useCallback((id) =&amp;gt; { /* ... */ }, []);
  return &amp;lt;List items={items} onSelect={onSelect} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React.memo 된 자식에게 &lt;b&gt;매 렌더마다 새 함수&lt;/b&gt;를 넘기면 메모이제이션이 무의미해집니다. useCallback으로 &lt;b&gt;불필요한 자식 재렌더&lt;/b&gt;를 차단하세요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과용 금지: 값이 작거나 계산이 싸다면 useMemo/useCallback이 오히려 오버헤드가 될 수 있으니 &lt;b&gt;Profiler로 측정&lt;/b&gt; 후 적용합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5) useContext &amp;mdash; 전역 상태의 &amp;ldquo;파급 렌더&amp;rdquo;를 통제하라&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개념 요약&lt;/b&gt;&lt;br /&gt;트리 하위로 값을 공급합니다. 단, &lt;b&gt;Provider의 value가 바뀌면 모든 소비자&lt;/b&gt;가 다시 렌더 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;성능 팁 1: value 객체를 메모이즈&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// ❌ 매 렌더마다 새 객체 &amp;rarr; 모든 소비자 렌더
&amp;lt;UserContext.Provider value={{ user, setUser }}&amp;gt;

// ✅ 값 메모이즈로 불필요한 전파 억제
const ctxValue = useMemo(() =&amp;gt; ({ user, setUser }), [user]);
&amp;lt;UserContext.Provider value={ctxValue}&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;성능 팁 2: Context&amp;nbsp;분리&lt;/b&gt;&lt;br /&gt;상태 값과 액션(디스패치)을 &lt;b&gt;서로 다른 Context&lt;/b&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;6) 마무리&amp;nbsp;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 기본 Hook(useState, useEffect, useRef, useMemo, useContext)은 단순히 개념을 아는 것보다 &lt;b&gt;성능까지 고려해 사용하는 게 핵심&lt;/b&gt;입니다.&lt;br /&gt;useState는 lazy 초기화&amp;middot;함수형 업데이트, useEffect는 의존성 최소화와 cleanup으로 불필요한 연산을 줄일 수 있습니다.&lt;br /&gt;useRef는 렌더 없이 값 캐싱, useMemo/useCallback은 무거운 계산&amp;middot;핸들러를 안정화해 불필요한 렌더링을 줄입니다.&lt;br /&gt;마지막으로 useContext는 value 메모이즈와 분리 설계로 전역 상태 전파 렌더를 억제하면 효율적인 앱을 만들 수 있습니다.&lt;/p&gt;</description>
      <category>프론트엔드</category>
      <category>frontend</category>
      <category>react</category>
      <category>reacthooks</category>
      <category>uesMemo</category>
      <category>useCallback</category>
      <category>useEffect</category>
      <category>useRef</category>
      <category>useState</category>
      <category>성능최적화</category>
      <category>웹개발</category>
      <author>지식소 채움이</author>
      <guid isPermaLink="true">https://hyun-dell.tistory.com/25</guid>
      <comments>https://hyun-dell.tistory.com/entry/React-%EA%B8%B0%EB%B3%B8-Hook-%EC%84%B1%EB%8A%A5%EA%B9%8C%EC%A7%80-%EB%81%8C%EC%96%B4%EC%98%AC%EB%A6%AC%EB%8A%94-%EC%82%AC%EC%9A%A9%EB%B2%95#entry25comment</comments>
      <pubDate>Fri, 22 Aug 2025 08:39:18 +0900</pubDate>
    </item>
    <item>
      <title>블로그 소개</title>
      <link>https://hyun-dell.tistory.com/pages/About</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개발지식소는 하루하루 차곡차곡 쌓아가는 개발 지식의 저장소&lt;/b&gt;입니다.&lt;br /&gt;처음 코드를 배우면서부터 지금까지, 개발을 하다 보면 단순한 기술 문서만으로는 해결되지 않는 순간들이 많았습니다. 문법을 알고 있어도 실제 프로젝트에서는 전혀 다른 문제가 튀어나오고, 공식 문서만으로는 감이 오지 않는 경우가 많았죠. 그래서 저는 이 공간을 단순한 &amp;lsquo;기술 아카이브&amp;rsquo;가 아닌, &lt;b&gt;실제 경험을 나누고 함께 성장할 수 있는 열린 노트&lt;/b&gt;로 만들고 싶었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;운영 목적&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Python, Django, Docker, React, GitHub 등 &lt;b&gt;개발 전반의 지식을 정리하고 공유&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;초보 개발자도 따라 할 수 있는 실습 중심 가이드&lt;/b&gt;를 제공합니다.&lt;/li&gt;
&lt;li&gt;실제 프로젝트 속에서 마주친 문제와 그 해결 과정을 기록합니다.&lt;/li&gt;
&lt;li&gt;개발자 커뮤니티와 &lt;b&gt;서로 배우고 성장할 수 있는 소통 공간&lt;/b&gt;이 되길 바랍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 콘텐츠&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Django &amp;amp; Python&lt;/b&gt;: ORM, REST Framework, JWT 인증, 서버 배포 가이드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;백엔드 &amp;amp; 프런트엔드:&lt;/b&gt; 실무에서 바로 쓰이는 기술과 활용법 정리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;개발 도구 활용&lt;/b&gt;: Docker, GitHub, Gunicorn 등 효율적인 환경 세팅 방법&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실습 사례&lt;/b&gt;: 프로젝트 중 만난 오류와 그 해결 과정을 솔직하게 공유&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;앞으로의 방향&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발지식소는 단순한 기록을 넘어, &lt;b&gt;누구나 쉽게 참고할 수 있는 개발 학습 자료집&lt;/b&gt;이 되는 것이 목표입니다.&lt;br /&gt;기술은 빠르게 변화하지만, 그 속에서 매일 배우고 부딪히며 깨달은 것들을 꾸준히 정리한다면 누군가에게는 좋은 길잡이가 될 수 있다고 믿습니다. 앞으로도 최신 기술 트렌드와 실습 예제를 계속 업데이트하며, 더 많은 개발자들이 &lt;b&gt;함께 성장할 수 있는 공간&lt;/b&gt;으로 만들어 가겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Contact&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그에 대한 문의, 제안, 혹은 단순한 의견 교환도 환영합니다.&lt;br /&gt;  [pshyun1331@gmail.com]으로 편하게 연락 주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>지식소 채움이</author>
      <guid isPermaLink="true">https://hyun-dell.tistory.com/pages/About</guid>
      <pubDate>Thu, 21 Aug 2025 09:59:25 +0900</pubDate>
    </item>
  </channel>
</rss>