<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>닿망징창 여정</title>
    <link>https://datdaradanadat.tistory.com/</link>
    <description>커다란 자갈돌은 짱돌 빙글빙글 돌아가는 맷돌 
FE 개발자를 위한 여정을 엉망진창 기록하자</description>
    <language>ko</language>
    <pubDate>Sun, 16 Aug 2026 09:33:26 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>닿다라다나닷</managingEditor>
    <image>
      <title>닿망징창 여정</title>
      <url>https://tistory1.daumcdn.net/tistory/6698733/attach/4887fcdb7a1e485cba6c67125151327c</url>
      <link>https://datdaradanadat.tistory.com</link>
    </image>
    <item>
      <title>[Flutter] Linux 환경 Flutter/Dart 초기세팅</title>
      <link>https://datdaradanadat.tistory.com/250</link>
      <description>&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;OS: Linux (Ubuntu)&lt;/li&gt;
&lt;li&gt;IDE: VS Code, Android Studio&lt;/li&gt;
&lt;li&gt;목표: Flutter + Dart 개발 환경 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방법 ① VS Code 마켓플레이스 기반 설치 (&lt;a href=&quot;https://docs.flutter.dev/get-started/quick&quot;&gt;공식문서&lt;/a&gt;)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter&quot;&gt;Flutter 확장 프로그램&lt;/a&gt; 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VS Code 실행 후 (Ctrl+P)에서 아래 명령어 실행으로 확장 설치한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`ext install Dart-Code.flutter`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✔️ Flutter 확장 설치 시 Dart 확장도 함께 설치된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Flutter SDK 자동 설치&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(Ctrl + Shift + P) `Flutter: New Project` 실행&lt;/li&gt;
&lt;li&gt;Flutter SDK 미설치 상태일 경우 &lt;b&gt;SDK 다운로드 경로 선택 요청&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;VS Code가 자동으로 Flutter SDK 다운로드 및 설정 진행&lt;/li&gt;
&lt;li&gt;몇 분 정도 소요 예정된다. 터미널 창, VSCode 다시 시작 시 Flutter 사용 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방법 ② Git Clone 기반 Flutter SDK 설치 (수동 방식)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1, Flutter SDK 다운로드&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;git clone https://github.com/flutter/flutter.git -b stable ~/flutter&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 환경 변수 수동 설정&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;echo 'export PATH=&quot;$PATH:$HOME/flutter/bin&quot;' &amp;gt;&amp;gt; ~/.bashrcsource ~/.bashrc&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인: `flutter --version`&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(공통) flutter doctor실행&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Flutter 개발 환경 점검 도구&lt;/li&gt;
&lt;li&gt;현재 시스템이 Flutter 개발을 수행할 수 있는 상태인지 &lt;b&gt;자동으로 진단&lt;/b&gt;한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[✓] Flutter (Channel stable) // Flutter SDK 설치 여부 확인
[✓] Dart // Dart SDK 상태 확인
[✓] VS Code // DE 연동 상태 확인 (VS Code, Android Studio 등)
// 플랫폼별 빌드 환경 확인
[!] Android toolchain
[✓] Linux toolchain
[✓] Chrome&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;[!] : 추가 설정 필요&lt;/li&gt;
&lt;li&gt;[✗] : 필수 구성 요소 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Android 개발 환경 (공통)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 / Linux 데스크톱 앱만 개발한다면&lt;br /&gt;&amp;rarr; Android Studio는 필수 아님&lt;/li&gt;
&lt;li&gt;Android 앱을 개발한다면&lt;br /&gt;&amp;rarr; 사실상 필수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 역할: Android SDK 제공자&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IDE라기보다 Android 빌드 인프라라고 보는 게 정확하다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Android Studio 설치 (&lt;a href=&quot;https://docs.flutter.dev/platform-integration/android/setup&quot;&gt;공식문서&lt;/a&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;a href=&quot;https://developer.android.com/studio?hl=ko&quot;&gt;공식 사이트&lt;/a&gt; 에서 설치&lt;/li&gt;
&lt;li&gt;압축 해제 후 파일 경로로 이동해서 `bin/studio.sh` 입력&lt;/li&gt;
&lt;li&gt;실행 및 SDK 설치 : Help imporve Android Studio 외에 모두 [Next]를 선택해도 무관합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Android Studio 없이 SDK만 설치&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt; ️ 장점&lt;/h4&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;SDK 버전 및 경로를 직접 통제 가능&lt;/li&gt;
&lt;li&gt;CI/CD, 서버 환경에 매우 적합&lt;/li&gt;
&lt;li&gt;IDE 의존성 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt; ️ 단점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SDK / Build Tools / Platform을 직접 관리&lt;/li&gt;
&lt;li&gt;Emulator 설정이 불편&lt;/li&gt;
&lt;li&gt;초기 세팅 시 실수 가능성 존재&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Android Studio + VS Code 조합을 표준으로 시작하는 것을 권장한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;안정성 기준에서 공식 지원하며, 초기 도입에 설명 비용이 적다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;IDE 커스터마이징보다 빌드 성공, 디바이스 실행, 환경 차이 최소화의 중요성이 더 높다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;도입 안정화 이후 전환을 제안한다 : CI/빌드 서버/ 고급 사용자&lt;/li&gt;
&lt;/ol&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;1. android studio 플러그인, SDK 설치&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;초기 화면 &amp;gt; Plugin &amp;gt; Flutter 플러그인 설치&lt;/li&gt;
&lt;li&gt;Customize &amp;gt; All settings... &amp;gt; sdk 검색 &amp;gt; Android SDK &amp;gt; [SDK Tools] 탭 메뉴 클릭 &amp;gt; [Android SDK Command-line Tools] 설치 및 체크 활성화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완료 후 android studio 를 재시작 한다.&lt;br /&gt;&lt;b&gt;2. 라이선스 동의&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`flutter doctor --android-licenses` 라이선스를 읽고, Accept 동의&lt;br /&gt;&lt;b&gt;3. Java 21 버전 (기본 세팅) &lt;/b&gt;으로 사용해도 무관하다&lt;br /&gt;4.&lt;b&gt; Linux desktop 앱&lt;/b&gt; 개발 계획 여부에 따라 추가 설치를 하지 않아도 무관하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Dart 기본 개념 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정적 타입 기반&lt;/b&gt; 언어, &lt;b&gt;JIT(개발 중)&lt;/b&gt; + &lt;b&gt;AOT(배포 시)&lt;/b&gt; 컴파일, 객체지향 + 함수형 패러다임 혼합&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 기본 변수 선언&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;String name = 'Alice';
int age = 20;
bool isAdmin = false;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;타입 추론 (var)&lt;/h4&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;var name = 'Alice'; // 컴파일 타임에 String으로 고정&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;var는 &lt;b&gt;동적 타입이 아니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;한 번 추론된 타입은 변경 불가하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;변경 불가 변수&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;final now = DateTime.now(); // 런타임 상수
const pi = 3.14;            // 컴파일 타임 상수&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;final: 한 번만 할당&lt;/li&gt;
&lt;li&gt;const: 컴파일 시점에 값 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Null Safety가 기본이다.&lt;/h3&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;String name;        // ❌ null 불가
String? nickname;  // ✅ null 허용&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;null 체크&lt;/h4&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;if (nickname != null) {
  print(nickname.length);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;null 연산자&lt;/h4&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;nickname?.length;     // null-safe 접근
nickname ?? 'guest';  // 기본값&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 함수 기본 문법&lt;/h3&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;int add(int a, int b) {
  return a + b;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;화살표 함수&lt;/h4&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;int add(int a, int b) =&amp;gt; a + b;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;선택 / 이름 있는 파라미터&lt;/h4&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;void greet({required String name, int age = 0}) {}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;Flutter API의 대부분이 &lt;b&gt;이름 있는 파라미터&lt;/b&gt;를 사용&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 컬렉션 (List / Map)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;List&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;List&amp;lt;int&amp;gt; numbers = [1, 2, 3];&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Map&lt;/h4&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;Map&amp;lt;String, int&amp;gt; scores = {
  'math': 90,
  'eng': 80,
};&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Collection if / for&lt;/h4&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;var items = [
  if (isAdmin) 'admin',
  for (var n in numbers) n * 2,
];&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 클래스와 객체&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 것이 객체&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자 문법이 간결&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter 위젯 구조 이해에 필수&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;class User {
  final String name;
  int age;

  User(this.name, this.age);

  void grow() {
    age++;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 불변 객체 개념 (중요)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dart에서는 값 변경보다 새 객체 생성이 일반적이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;final list = [1, 2, 3];
final newList = [...list, 4];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter UI 갱신 원리와 직결됨&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 비동기 처리 (async / await, Future)&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;Future&amp;lt;String&amp;gt; fetchData() async {
  await Future.delayed(Duration(seconds: 1));
  return 'data';
}
final data = await fetchData();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dart는 단일 스레드 이벤트 루프&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Future, async, await 개념 필수&lt;/p&gt;</description>
      <category>  FrontEnd</category>
      <category>flutter</category>
      <category>초기세팅</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/250</guid>
      <comments>https://datdaradanadat.tistory.com/250#entry250comment</comments>
      <pubDate>Tue, 30 Dec 2025 09:13:41 +0900</pubDate>
    </item>
    <item>
      <title>  Storybook 디자인 시스템 구축 - 독립된 환경</title>
      <link>https://datdaradanadat.tistory.com/249</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;일관된 사용자 경험을 제공하고 개발 및 디자인 프로세스를 효율화하기 위해 디자인 시스템을 구축하는 것이 중요하다고 느꼈다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;특히, 프로젝트 &lt;a href=&quot;https://syncspot.kr/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;싱크스팟&lt;/a&gt; 은 디자이너의 부재로 디자인 시스템이 명확히 존재하지 않았으며, 기획자는 프로젝트의 컴포넌트의 구조와 변동사항 적용을 독립된 환경에서 검증하고 싶어했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;프론트엔드에서 스토리북 Storybook 을 사용해 디자인 시스템 구축과 협업 효율을 극대화 할 수 있다고 판단해 도입했다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;디자이너/기획자와의 소통 비효율: 완성된 화면이나 개발 서버를 통해서만 UI를 공유하고 피드백을 주고받는 과정에서 시간 소요가 크고, 디자인 시스템의 일관성 유지의 어려움&lt;/li&gt;
&lt;li&gt;독립적인 컴포넌트 개발 환경의 부재: 전체 애플리케이션을 실행해야만 개별 컴포넌트의 동작을 확인할 수 있어 개발 속도가 느려지고 디버깅이 번거로움&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제들을 해결하고 `공용 UI 컴포넌트`와 `디자인 토큰`을 체계적으로 관리하며 시각적으로 문서화할 필요성을 느끼게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결책으로 &lt;span data-token-index=&quot;1&quot;&gt;스토리북 `Storybook`&lt;/span&gt; 도입 결정!&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;&lt;span style=&quot;text-align: start;&quot;&gt;스토리북 Storybook&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 컴포넌트 개발을 위한 오픈 소스 도구 &lt;a href=&quot;https://storybook.js.org/docs&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://storybook.js.org/docs&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션이나 모바일 애플리케이션을 개발할 때, 각각의 UI 컴포넌트를 독립적으로 빌드하고 시각적으로 문서화하여 개발 효율성을 높이는 데 활용&lt;/p&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;문서화: 컴포넌트의 props와 사용 방법, 예시 등을 명확하게 문서화하여 팀원 간의 공유와 협업을 편리하게 한다.&lt;/li&gt;
&lt;li&gt;재사용성과 일관성: 디자인 시스템의 핵심인 재사용 가능한 컴포넌트를 구축하고, 가이드라인의 일관된 UI를 유지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순한 개발 도구를 넘어, 디자이너, 기획자, 개발자 모두가 함께 UI/UX를 이해하고 소통하는 `공유 공간`의 역할을 수행한다고 이해했다.&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;적용 방법&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;npx sb init&lt;br /&gt;yarn storybook&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일의 위치에 대한 팀 규칙 논의가 많았다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. story 폴더 내에 정리하는가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 테스트하는 파일과 같은 위치에 두는가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파일의 스토리 작성 여부를 파악하기 용이하게 테스트파일과 같은 위치에 두기로 결정했다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;261&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPSD9o/btsPxjuI2Ha/VRnmx72B3lQLOYsx20kc10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPSD9o/btsPxjuI2Ha/VRnmx72B3lQLOYsx20kc10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPSD9o/btsPxjuI2Ha/VRnmx72B3lQLOYsx20kc10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPSD9o%2FbtsPxjuI2Ha%2FVRnmx72B3lQLOYsx20kc10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;261&quot; height=&quot;552&quot; data-origin-width=&quot;261&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Introduction 문서 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱크스팟의 UI 컴포넌트에 대한 포괄적인 소개, 가이드라인 제공&lt;/p&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;screencapture-localhost-6006-2025-07-23-15_44_21.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;5872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ufKhE/btsPwLygVwe/0tcKpHVOD2WN1geOCGCfq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ufKhE/btsPwLygVwe/0tcKpHVOD2WN1geOCGCfq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ufKhE/btsPwLygVwe/0tcKpHVOD2WN1geOCGCfq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FufKhE%2FbtsPwLygVwe%2F0tcKpHVOD2WN1geOCGCfq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2880&quot; height=&quot;5872&quot; data-filename=&quot;screencapture-localhost-6006-2025-07-23-15_44_21.png&quot; data-origin-width=&quot;2880&quot; data-origin-height=&quot;5872&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ API 요청 대신 Mock 목 데이터 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 백엔드 API와의 연동 없이도 컴포넌트의 다양한 상태를 재현할 수 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서 React Query를 사용하여 데이터를 관리하고 있기 때문에, 실제 API 호출 없이 컴포넌트가 다양한 데이터 상태(로딩, 성공, 에러, 빈 데이터 등)에서 어떻게 동작하는지 테스트하는 것이 중요하다.&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;여기서 핵심은 MSW(Mock Service Worker)와 같은 네트워크 요청 가로채기 도구를 사용하지 않고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Query의 캐시를 직접 조작하여 목 데이터를 제공하는 방법이다. `queryClient.setQueryData`를 활용하여 이 문제를 해결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작동 원리: React Query는 데이터를 가져오기 전에 내부 캐시를 먼저 확인한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 해당 쿼리 키에 대한 데이터가 캐시에 이미 존재한다면, 실제 API 호출 없이 캐시된 데이터를 사용한다.&lt;/li&gt;
&lt;li&gt;이 원리를 활용하여 스토리북 데코레이터에서 미리 queryClient.setQueryData를 통해 캐시에 원하는 목 데이터를 주입한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1753276566247&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//..
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: false,
      refetchOnWindowFocus: false,
    },
  },
});

const mockRoomDetailInfo = {
  '1': {
    data: {
      name: '테스트 방',
      memo: '',
      memberCount: 2,
      emails: ['test1@example.com', 'test2@example.com'],
    },
  },
  '2': {
  ///...
};

const meta = {
  title: 'Common/Modal/RoomDetailInfoModal',
  component: RoomDetailInfoModal,
  tags: ['autodocs'],
  argTypes: {
    room: {
      description: '방 정보',
      control: 'object',
    },
    onClose: {
      description: '모달을 닫을 때 호출되는 함수',
      action: 'closed',
    },
  },
  parameters: {
    layout: 'centered',
  },
  decorators: [
    (Story) =&amp;gt; {
      queryClient.setQueryData(
        ['roomDetailInfo', '1'],
        mockRoomDetailInfo['1'],
      );
      ///...

      return (
        &amp;lt;QueryClientProvider client={queryClient}&amp;gt;
          &amp;lt;Story /&amp;gt;
        &amp;lt;/QueryClientProvider&amp;gt;
      );
    },
  ],
} satisfies Meta&amp;lt;typeof RoomDetailInfoModal&amp;gt;;

export default meta;
type Story = StoryObj&amp;lt;typeof meta&amp;gt;;

export const Default: Story = {
  args: {
    room: {
      roomId: '1',
      roomName: '테스트 방',
    },
    onClose: () =&amp;gt; {},
  },
  parameters: {
    docs: {
      description: {
        story: '방 상세 정보 모달의 기본 디자인입니다.',
      },
    },
  },
};

export const LongMemo: Story = {
///...
};

export const ManyMembers: Story = {
///...
};&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스토리 파일 상단에 테스트에 사용할 `QueryClient 인스턴스`를 생성한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;defaultOptions 설정에서 retry: false, refetchOnWindowFocus: false와 같이 &lt;b&gt;실제 네트워크 요청을 방지하는 옵션&lt;/b&gt;을 추가한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;테스트 시나리오별로 필요한 목 데이터를 `mockRoomDetailInfo` 에 정의한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 데이터는 컴포넌트가 API로부터 받을 응답 형태와 동일하게 구성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스토리북의 `decorators 속성`을 활용한다. 데코레이터 함수
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정의된 QueryClient 인스턴스의 setQueryData 메서드를 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;['roomDetailInfo', '1']와 같은 특정 쿼리 키에 미리 정의해둔 목 데이터를 주입한다.&lt;/li&gt;
&lt;li&gt;각 스토리(Default, LongMemo, ManyMembers)에서 사용할 roomId에 맞춰 적절한 데이터를 캐시에 넣어준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;테스트할 컴포넌트를&amp;nbsp;QueryClientProvider로 감싸주고, 앞서 생성한&amp;nbsp;queryClient&amp;nbsp;인스턴스를&amp;nbsp;client&amp;nbsp;prop으로 전달하여 React Query 환경을 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;decorators에서 각 쿼리 키에 대한 mockRoomDetailInfo를 setQueryData로 직접 설정함으로써 RoomDetailInfoModal 컴포넌트가 useQuery 훅을 통해 데이터를 요청할 때 실제 서버 대신 이 목 데이터를 받게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 호출 없이 컴포넌트의 다양한 데이터 상태를 안정적이고 빠르게 테스트할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트를 개발하고 테스트할 때 메인 애플리케이션의 전역 상태나 라우팅, 복잡한 환경 설정으로부터 자유롭다는 큰 장점을 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 프로젝트와의 독립성 때문에 React Query와 같은 데이터 관리를 스토리북에서 사용할 때 추가적인 설정이 필요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메인 애플리케이션과의 분리: React 애플리케이션에서는 최상위 컴포넌트(App.tsx 또는 index.tsx)에서 QueryClientProvider를 한 번만 설정하여 애플리케이션 전역에서 React Query 기능을 사용할 수 있도록 한다. 하지만 스토리북 환경은 전역 설정과 무관하게 동작한다.&lt;/li&gt;
&lt;li&gt;스토리 QueryClient 설정: React Query를 사용하는 컴포넌트를 스토리북에서 테스트하려면, 해당 컴포넌트의 스토리 파일 내에서 QueryClient 인스턴스를 생성하고, 이를 QueryClientProvider로 감싸주어야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;독립된 환경에서의 쿼리 설정&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스토리 파일 내부에 const queryClient = new QueryClient(...)를 정의하고, decorators 함수 안에서 &amp;lt;QueryClientProvider client={queryClient}&amp;gt;로 &amp;lt;Story /&amp;gt;를 감싸준 것&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;독립적인 캐시 관리: 각 스토리(또는 스토리가 속한 스토리 파일)는 자신만의 독립적인 React Query 캐시를 가지게 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, 한 스토리에서 setQueryData로 캐시에 데이터를 넣거나 invalidateQueries로 캐시를 무효화해도 다른 스토리에는 영향을 미치지 않아 `테스트의 격리성`을 보장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 방식은 각 컴포넌트 스토리가 완벽하게 자체적인 React Query 환경에서 작동하게 하여, 실제 애플리케이션 환경을 모방하면서도 복잡한 설정 없이 컴포넌트의 데이터 의존성을 효과적으로 관리할 수 있게 한다.&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;  .storybook/preview.ts 전역에서 선언된 QueryCient 인스턴스를 사용하고, 각 스토리에서 `loaders`, `clear` 를 사용해서 중복을 줄이는 방법도 있다. &lt;u&gt;개선점으로 파악되었고, 리팩토링할 예정이다.&lt;/u&gt;&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;  도입 효과, 성과&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;: 스토리북은 디자이너, 기획자, 개발자 모두가 동일한 시각 언어를 공유하는 공간이 된다. 더 이상 스크린샷이나 추상적인 설명이 아닌, 실제 동작하는 UI 컴포넌트를 보며 소통하므로 오해를 줄이고 피드백 주기를 단축시켰다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;병렬 개발 가능&lt;/b&gt;: 백엔드 API가 완성되지 않아도 프론트엔드 개발자는 스토리북 환경에서 목 데이터를 활용하여 독립적으로 UI 컴포넌트를 개발하고 테스트할 수 있게 되었다. 이는 전체 개발 일정을 앞당기는 데 기여했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;: 모든 UI 컴포넌트와 디자인 토큰이 스토리북을 통해 한곳에 모여 관리되므로, 재사용성이 크게 향상되었고, 새로운 기능 개발 시 기존 컴포넌트를 활용하여 일관된 UI를 빠르게 구축할 수 있었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;시각적 문서화 자동화&lt;/b&gt;:&amp;nbsp;Docs&amp;nbsp;기능을 활용하여 컴포넌트의 사용법, props, 예시 코드 등이 자동으로 문서화되어, 새로운 팀원이 프로젝트에 합류하거나 기존 컴포넌트의 사용법을 확인할 때 매우 유용하게 활용되고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;: UI 변경사항이 발생했을 때, 전체 애플리케이션을 구동할 필요 없이 스토리북에서 해당 컴포넌트만 빠르게 확인하고 테스트할 수 있어 디버깅 및 수정 시간이 단축되었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예측 가능한 UI 동작&lt;/b&gt;: 컴포넌트의 모든 가능한 상태와 시나리오를 스토리북에서 미리 테스트하고 검증함으로써, 실제 서비스에 배포되었을 때 발생할 수 있는 UI 관련 버그를 최소화할 수 있었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스토리북의 도입은 단순히 기술 스택 하나를 추가한 것이 아니라, 프론트엔드 개발 팀의 문화와 워크플로우 전반에 걸쳐 긍정적인 변화를 가져왔다. 이는 앞으로 싱크스팟의 품질과 효율성을 한 단계 더 끌어올리는 중요한 기반이 될 것이다.&lt;/p&gt;</description>
      <category>  FrontEnd</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/249</guid>
      <comments>https://datdaradanadat.tistory.com/249#entry249comment</comments>
      <pubDate>Wed, 23 Jul 2025 22:22:59 +0900</pubDate>
    </item>
    <item>
      <title>[도서 리뷰] 개발자의 글쓰기 - 김철수 지음</title>
      <link>https://datdaradanadat.tistory.com/248</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;책 내용&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;708&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dElZVp/btsPqAje2D6/83kkeA4RpmKID9CcWkvN30/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dElZVp/btsPqAje2D6/83kkeA4RpmKID9CcWkvN30/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dElZVp/btsPqAje2D6/83kkeA4RpmKID9CcWkvN30/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdElZVp%2FbtsPqAje2D6%2F83kkeA4RpmKID9CcWkvN30%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;708&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;708&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 개발자가 알아야 하는 글쓰기 기본&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문장과 단락을 구조화 하는 법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심을 먼저 말한 뒤, 필요에 따라 부가 설명을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex. 입력 데이터는 3차원 벡터다. 색상 RGB 값을 각각 사용하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개조식 방식 + 글머리 기호를 활용한다. 서술의 목적에 따라, 들여쓰기와 번호에 위계를 맞춰야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 문서도 마찬가지로 문단간의 위계 + 탭과 같은 스타일과 계층이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기 쉽기 위해서는 계층이 필수!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;비즈니스 문서 문장 부호&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰따옴표 &quot;&quot; : 책의 제목, 신문 이름&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은따옴표 '' : 소제목, 예술작품의 제목, 상호, 법률, 규정. + 강조, 비교&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;외래어, 영어&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 선언 시 반대되는 단어를 사용하자. 프로그램 안에서 일관성, 개연성의 합의가 될 정도로 통&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 개발 시간을 줄여주는 이름 짓기와 주석 쓰기&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;네이밍 컨벤션&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;창조가 아니라 조합! 규칙에 따라 잘 조합하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영어 표기법 - 파스칼 &amp;amp; 카멜 : 영어의 대문자 표기 원칙 상속&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;중요하거나 크거나 특정한 것을 가리키거나 제목에 해당하는 명사 - 첫글자 대문자&lt;/li&gt;
&lt;li&gt;그런 명사들이 이어질때 첫글자 모두 대문자&lt;/li&gt;
&lt;li&gt;명사, 관사가 아닌 동사, 형용사 등은 소문자를 쓴다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파스칼 표기법 : 첫글자 대문자. 클래스 &amp;amp; 인터페이스 이름에 주로 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 프로그래밍에서 가장 주요하고, 높은 위치. 고유명사처럼 특정되며 명사로 되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 명사 여러개를 뭍여 쓰는 경우 각 명사의 첫글자를 모두 대문자로 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포함하는 큰 개념, 고유명사나 호칭 같은 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카멜 표기법 : 첫글자 빼고 나머지 단어의 첫글자 대문자. 함수 &amp;amp; 변수 이름에 주로 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수는 동작을 시키는 명령어 개념으로 - 동사&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수 - 형용사&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상수 : 모두 대문자, 언더스코어 _ 로 단어를 연결&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 변해서는 안된다는 점을 강조, 주의하기 위해 가독성을 위한 대문자를 사용했지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통합개발환경 IDE 가 발샌해서 굳이 상수를 모두 대문자로 쓸 필요가 있는지 의문이다. 개인의 자유이지만 회사 내에서 통일하는 것이 좋다.&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;amp; 모듈 : 모두 소문자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스나 함수보다 더 높은 위치가 될텐데, 실제로는 소문자만 쓴다. 이들은 클래스를 모으거나 함수를 담아놓은 통에 불과하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 패키지, 클래스 이름이나 모듈, 함수 이름을 헷갈릴 수 있어서 모두 소문자로 쓰기로 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BEM block, element, modifier 표기법 : CSS 에서 사용하는 표기법 `대상--요소__상태`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대상의 요소나 부분을 의미할때 언더스코어 두개로 연결한다. 이것들의 상태나 속성을 의미할 대는 하이픈 두개--로 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;뭐가 어떻든 가독성과 소통이 먼저다!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;따라할 것이 아니라, 그렇게 쓰는 이유를 알고 써야 한다!! - 내 가치관과 유사하다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘은 개발툴이 많이 발전되어서, 대소문자와 무관한 색상으로 가독성을 챙길 수는 있다. 그러나 소통은 컨벤션이 중요하다. 변수나 함수명에 따라 의미하는 바에 대한 이해도를 통일하기 위해 컨벤션 규칙을 챙기는 것이 매우 중요하다. `가독성`, `소통`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;변수 이름을 잘 짓는 법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈소스 네이밍 특징들 &amp;rarr; 가낭 많이 쓰는 변수 이름 &amp;rarr; i, LOG, result&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;a, d 처럼 의미없는 글자를 변수로 쓰는 것은 좋지 않다. 변수 자체를 보고 의미와 데이터를 파악할 수 ㅣㅇㅆ는 정보를 담아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 이름, 짧은 이름이 중요한게 아니다! &lt;b&gt;  검색 잘 되는 이름  &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거는 오탈자 때문에 길이가 문제가 될 수 있었지만, 개발도구가 검색해서 알려준다. 변수 이름의 길이와 오탈자의 상관관계는 이제 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수 접두어 - m, n, is, btn, img 등등의 헝가리안 표기법 유익했지만, 개발도구가 변수 타입이 자동으로 표시되기 때문에 레거시라고 봐도 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 약어를 쓰는게 좋은지? 안좋은지? &amp;rarr; 서비스 이름, 패키지 이름, 클래스 이름에 약어 사용 정도   (AWS S3 등)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보편성을 기준으로 선택하는게 중요하다. 보편적으로 이해할 수 있는 정도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복수형은 언제 사용?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열이나 여러개의 변수명을 가리킬때 종종! 그런데 함수명 중간에는 s 가 명확히 눈에 보이지 않기 때문에, array 나 listof 과 같은 표시방법을 사용하기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 단어를 앞에 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;totalVisitor 의 경우 - total 보다 visitor 을 먼저 검색하는 경우가 있으니, visitor 가 중요하다! 중요한 것이 앞에 와야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발도구가 자동으로 찾아주기 때문에 큰 문제가 생기지는 않겠지만, 기본 원칙으로 생각하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;좋은 이름의 기준 SMART&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;easy to&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Search : 검색
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검색에 유용하게 도입! 한단계 상위 범주의 이름을 접두어로 태그처럼 사용!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Mix : 조합
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;태그, title 과의 조합으로 h1, p 태그의 스타일링에 모두 사용할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Agree : 수긍
&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;Remember : 기억
&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;Type : 입력
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연속된 철자, 묵음, ie/ei, sion/tion,. uous/ous/us 잘 틀리는 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이름 잘 지으면 주석 줄일 수 있다!&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주석없이 코드짜는 연습을 하자~&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유를 알려주거나, 성능 등 새롭게 발견한 것, 예상 질문과 답, todo, 주의 xxx, hack 개선 아이디어 주기, 협업 보완 요청, 속마음 등을 자유롭게 표현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 업데이트와 동일하게 주석도 잊지않고 업데이트 해주기! 잘못된 데이터가 있을 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 사용자와 소통하는 에러 메시지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;404 에 대하여 구글은 아주 심플하게 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국 사이트들은 아주 상세하게 설명하거나, 고객센터를 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;404 의 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 사용자가 잘못된 링크를 입력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 클릭했는데 없는 페이지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번은 안죄송해도 되지만, 2번은 죄송해야 한다. 없는 페이지를 유지하는 것을 깨진 링크, 죽은 링크, 나쁜 링크라고 한다. 애초에 이 링크가 없도록 관리하는게 맞음.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;개발자용 에러메시지 / 사용자용 에러메시지&amp;nbsp; 분리해 작성해라! &amp;rarr;&lt;b&gt; Kampus 프로젝트에서의 개선점&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C 의 #define 전처리기를 사용해 메크로로 한번에 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예외처리 작성법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류의 내용과 원인 고지 - 예외처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 메시지의 목적 : 사용자에게 에러가 발생했음을 고지. 즉 사용자가 해결해야 하는 부분이 기때문에, 해결방법이 추가되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발생 원인이 긴 경우에는 1. 해결방법 2. 에러 원인 3. 에러 내용 순서로 말하는게 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예, 아니오 버튼으로 구성하지 않고, 행동에 대해 구체적으로 서술하는 버튼을 유지하는게 좋다. (ex. 나가기, 변경하기 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인, 취소 위치의 경우, 서비스 내에서 명확한 일관성을 따르는 것이 좋다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호 여러번 입력으로 인한 해킹공격을 방지하기 위해 로그인 입력횟수를 제한하는 것도 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;에러메시지 대신 예방 메시지를 사용하자&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex. 캘린더 날짜 선택에서 지난 날짜는 선택할 수 없는 상황&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 지난 날짜를 선택하면 alert 경고창으로 지난날짜를 선택할 수 없다고 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 지난 날짜 선택이 불가능하도록 막아놓는다. (비활성화)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  사용자가 첫 번째 select에서 선택한 값에 따라 두 번째 select의 선택 옵션을 동적으로 제한하는 것은 가능한가??&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이 의문에 대해 블로그 글 추가 작성 예정! `싱크스팟 시간투표 로직`&lt;br /&gt;&lt;br /&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재확인 + 취소 여러 요청이 많아 혼돈됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 재확인 : 사용자의 의사결정을 믿을 수 없다고 본다. 할수 있는 모든 경고를 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 결제 직후 취소버튼 표출 : 사용자가 자신의 행동을 주체적으로 책임지게 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 결정에 재확인을 한다면, 시스템도 불완전해질수밖에 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;꼭 필요한지 검토하고 수행하자. 개발자의 철학에 달렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 릴리스 문서, 장애 보고서 - 독자 관점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체인지로그 changelog 너무 짧지도, 길지도 않게!&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비슷한 로그끼리 분류
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발 관점의 작업 (ex. 기능 추가, 기능 개선, 오류 수정) &amp;rarr; 독자 : 개발자인 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소프트웨어 릴리스 문서에서 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사용자 관점 (ex. 게임 준비, 게임 중, 게임 종료) &amp;rarr; 일반 사용자인 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;문장 단위로 요약 : `개조식`&lt;/li&gt;
&lt;li&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;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예문 정리본&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; 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;/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;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;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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;체인지 로그는 고객을 위한 것&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고객에게 끼치는 영향 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 문제 + 고객의 문제를 합치되, 고객의 문제를 앞쪽, 개발자의 문제를 짧게 개조식으로 표현한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선점에 따라오는 고객의 효과, 혜택을 분명하게 이야기 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 사용을 유도하는 문장도 굿  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;only 고객만이 아닌, 개발자의 문제도 함께 기재해도 좋다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검토중인 항목을 기재해서, 서비스 현황을 알리는 방식도 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그 버전을 올리는 명확한 기준이 없음&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;깃허브 Semantic Versioning 제시함&lt;/h4&gt;
&lt;figure id=&quot;og_1752822328922&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;유의적 버전 2.0.0&quot; data-og-description=&quot;Semantic Versioning spec and website&quot; data-og-host=&quot;semver.org&quot; data-og-source-url=&quot;https://semver.org/lang/ko/&quot; data-og-url=&quot;https://semver.org/lang/ko/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://semver.org/lang/ko/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://semver.org/lang/ko/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;유의적 버전 2.0.0&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Semantic Versioning spec and website&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;semver.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;X.Y.Z&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`X` Major `Y` Minow `Z` Patch&lt;/li&gt;
&lt;li&gt;X : 0인 것은 초기 내부 개발에서만 사용. 최초 공개는 1.0.0 이어야 한다.&lt;/li&gt;
&lt;li&gt;Y : 기존 버전과 호환되는 새로운 기능 추가. 큰 규모의 패치&lt;/li&gt;
&lt;li&gt;Z : 작은 규모의 패치. X, Y 를 올리면 Z 는 0 으로 초기화 되어야 한다.&lt;/li&gt;
&lt;li&gt;pre release 사전배포가 필요한 경우 `X.Y.Z-alpha.1` 과 같은 식별자 사용한다&lt;/li&gt;
&lt;li&gt;배포된 버전은 변경해서는 안된다. 변경이 있다면 버전을 올려야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://datdaradanadat.tistory.com/247&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;태그 배포 도입한 경험!&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;릴리스 문서 (노트) = 문제 해결 보고서&lt;/h4&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;문제점 : 잘못된 시스템 설정, 프로그램 비 최적화, 잘못된 DB 설계&lt;/li&gt;
&lt;li&gt;해결책 : 시스템 설정 변경&lt;/li&gt;
&lt;li&gt;후속 계획 : 프로그램 최적화, DB 재설계&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;&amp;nbsp;&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;/li&gt;
&lt;li&gt;개요 : 종합 글&lt;/li&gt;
&lt;li&gt;새로운 기능&lt;/li&gt;
&lt;li&gt;개선사항&lt;/li&gt;
&lt;li&gt;버그 수정&lt;/li&gt;
&lt;li&gt;영향과 주의사항 : 릴리스로 인한 영향과 개발자의 주의사항&lt;/li&gt;
&lt;li&gt;연략서 : 문의나 의견 접수를 위한 담당자 이름과 연락처 정보&lt;/li&gt;
&lt;li&gt;면책 : 법적 문제 발생 시 책임의 한계에 관한 내용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떠한 행동을 유도할때 필수, 권장, 선택인지 명확히 서술어로 구분해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외 사항을 명시해야만 면책될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;비즈니스 장애 보고서&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;질문에 대답하는 신속한 글쓰기
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대화식 &amp;rarr; 개조식&lt;/li&gt;
&lt;li&gt;장애 내용, 영향, 원인, 상황, 결과, 핵심 원인, 향후 대책&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;원인과 이유를 찾는 분석적 글쓰기
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;5 Whys&lt;/li&gt;
&lt;li&gt;사물, 현상, 동작이 문제를 초래하는 원인&lt;/li&gt;
&lt;li&gt;사람이 문제를 초래하는 원인 &amp;rarr; 대다수 재발의 원인
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소통의 문제 &amp;rarr; 여러사람 참조로 해결 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;상사를 고려하는 비즈니스 관점의 글쓰기 (비개발자)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결제 뿐만 아니라 접속 기능 실패일때도 &amp;rarr; 기대 매출 손실 결과에 도래할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 개발 가이드 - 설명, 묘사, 논증, 서사&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;용도 : 독자 관점에서 이 서비스를 이용하는 이유.&amp;nbsp;
&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;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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글, 그림, 도식화, 사진을 통한 묘사도 효과적!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;협업 시에는 비개발직군의 주관적 묘사 &amp;rarr; 객관적 묘사로 변경하여 논의 &amp;rarr; 개발에 필요한 주관적 묘사로 업그레이드 하는 것이 좋다. 주도권을 가지고 논의하면 추후 수정사항이 단축될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의견에 필요한 것 `근거` 를 활용해서 논증하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  개발자가 직접 체험한 결과를 기반으로 하는 주장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 거칠거나, 공손하거나 중립적 XXX 확신이 없어 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;~ 하십시오, ~하지 마십시오 의 표현을 활용하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`주장 - 이유` 와 `문제 - 답` 의 거리를 가깝게 작성하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서사 : 화면의 흐름에 맞게 설명하는 것. 시간, 중요도 등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서 &amp;rarr; 단계 &amp;rarr; 목차 로 정돈하여 글을 읽기 쉽게 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 기술 블로그 쉽게 쓰고 운영하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거 글쓰기 = 문제해결 과정으로 학습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 충분히 고민하고, 해결 방안을 전략적으로 작성하는 흐름&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논리의 부족함이 있다면, 근거가 완벽해질 때까지 쓰지 말것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;주제 의식 X 소재 의식 O
&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;/li&gt;
&lt;li&gt;독자 수준 X&amp;nbsp; 자기 수준 O
&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;/li&gt;
&lt;li&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;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;저, 술, 편, 집 - 기술 블로그 유형 4가지&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&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;목차 중요함 : 1차원 단방향&lt;/li&gt;
&lt;li&gt;성공 / 실패 루트를 구분한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성공 최종 루트 이후 &amp;rarr; 실패 문제해결 팁 정리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;본인의 경우, 개발 경험 흐름에 맞춰서 작성하는게 일반적이었다. &lt;a style=&quot;color: #9d9d9d;&quot; href=&quot;https://datdaradanadat.tistory.com/244&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Flat config&lt;/a&gt; 에 대해 작성한 내용이 대표적이다. &lt;a style=&quot;color: #9d9d9d;&quot; href=&quot;https://tech.kakaoent.com/front-end/2022/221110-error-boundary/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;참고한 글 흐름&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;나의 경험을 알리기에는 최적이지만, 결과를 보러온 독자들에게는 불편할 수도 있겠다는 생각이 든다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;책의 방식도 점진적으로 적용해보자!!&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;본문 &amp;rarr; 맺음말 &amp;rarr; 머릿말 순서로 작성하자. 머릿말은 간략히 적는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;/li&gt;
&lt;li&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;/li&gt;
&lt;li&gt;집 : 흩어진 자료를 한데 모아 정리한 것
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명령어 모음, 팁, 00가지 규칙&lt;/li&gt;
&lt;li&gt;편집된 글 모음. 주로 조회수가 높다 ㅋㅋㅋ&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기업의 기술 블로그 운영&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;개발자 수준을 높이고, 채용과 연관이 깊음&lt;/li&gt;
&lt;li&gt;노하우를 체계적으로 축적할 수 있다. 일종의 사내 문서 관리가 된다&lt;/li&gt;
&lt;li&gt;개발자가 꾸준히 스스로 공부하도록 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사의 문화를 반영한다. 각 기업마다 문서의 구조가 다른 편이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사의 교육 수준 &amp;rarr; 개발자의 개발 수준&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Pari Writing 짝 글쓰기&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;공통된 지식 체계는 훨씬 보편적인 편이다.&lt;/li&gt;
&lt;li&gt;자연스러운 팀 회고가 된다.&lt;/li&gt;
&lt;li&gt;부담과 중압감에서 가볍다고 느낀다. 심리적 안정감&lt;/li&gt;
&lt;li&gt;팀워크, 소통 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  느낀 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 내가 글을 잘 쓰고 있는지 고민이었다. 개발의 비중보다, 글쓰기의 비중이 높아져서 혼란스럽기도 했다. 과거 다독왕 경험을 살려 도서관에서 책을 고르는 도중에 만난 책이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책을 읽으면서 내가 고민하던 부분에서 나아가는 방향이 맞았다는 확신을 가지게 되었다. 특히, 개발자와 디자이너 간의 협업에서 발생할 수 있는 소통의 문제를 해결하기 위한 글쓰기의 중요성을 깨달았다. 예를 들어, 함수와 변수 이름을 짓는 것부터 시작해, 주석을 어떻게 작성해야 하는지에 대한 구체적인 팁들이 인상 깊었다. 이러한 세부적인 내용들이 실제로 협업을 할 때 큰 도움이 될 것이라는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 더 좋은 DX, UX 등 협업관계를 구축하기 위해 고려할 점에 대한 설명을 통해 변화하고 개선하는 것이 좋겠다고 느꼈다. 글쓰기가 단순히 정보를 전달하는 수단이 아니라, 팀원 간의 이해를 돕고, 프로젝트의 성공을 이끌어내는 중요한 요소라는 점을 다시 한번 상기시켰다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 개발적으로도 개선할 점을 찾은 부분이 많았다. 코드의 가독성을 높이거나 에러 처리 방식, 그리고 팀원들이 쉽게 이해할 수 있도록 정보를 전달하는 방법에 대해 많은 인사이트를 얻었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 `글쓰기는 단순한 기술이 아니라 소통의 도구`라는 것이다. 개발자라면 코드뿐만 아니라 그 코드에 대한 설명과 의도를 잘 전달할 수 있어야 한다. 이 책을 통해 글쓰기의 중요성을 다시 한번 깨닫고, 앞으로의 개발 생활에 큰 도움이 될 것 같다.&lt;/p&gt;</description>
      <category>회고</category>
      <category>25년</category>
      <category>도서</category>
      <category>에러</category>
      <category>전략</category>
      <category>회고</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/248</guid>
      <comments>https://datdaradanadat.tistory.com/248#entry248comment</comments>
      <pubDate>Fri, 18 Jul 2025 17:24:46 +0900</pubDate>
    </item>
    <item>
      <title> ️ 안정성 배포전략 - CI 코드품질 검사, CD 태그 기반 배포</title>
      <link>https://datdaradanadat.tistory.com/247</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;단순했던 첫 배포 파이프라인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kampus 프로젝트는 초기 Git Flow 를 따르자고 정했다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;860&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhs1Js/btsPhuL3LS0/GGwKXip4mhoPiCZkMdDnQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhs1Js/btsPhuL3LS0/GGwKXip4mhoPiCZkMdDnQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhs1Js/btsPhuL3LS0/GGwKXip4mhoPiCZkMdDnQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhs1Js%2FbtsPhuL3LS0%2FGGwKXip4mhoPiCZkMdDnQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;860&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;860&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;feature 브랜치에서 develop 으로 PR 을 올렸을 때 &amp;rarr; Squash and merge &amp;rarr; merge 후 remote branch 삭제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 단위로 개발한 브랜치이기 때문에, 개발서버에도 기능 단위 커밋을 관리할 수 있다. 브랜치를 삭제하므로 merge commit 을 남길 필요가 없다.&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;develop 브랜치에서 main 브랜치로 배포 돌릴 때 &amp;rarr; Rebase and merge&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 기능을 배포할때 병합한다. 특정 기능에서 문제가 발생했을 경우 롤백이 필요하다. merge commit 을 남길 필요가 없다.&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;Git Flow : 초기에는 지키려고 노력했지만, 데모데이가 다가오며 많아지는 merge 와 배포에 흐름이 깨졌다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  MVP 가 많아지면서 단순 기능구현이 많아졌고, merge 과정에서 작업한 코드가 사라지는 문제가 발생했다. QA, 진행사항 검증 시에 동료가 작업한 부분이 업데이트 되지 않아 다른 파트에서 혼란을 주기도 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfSaQK/btsPh7bLr8S/b2FDQCHPrsn3VfxEFDOafk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfSaQK/btsPh7bLr8S/b2FDQCHPrsn3VfxEFDOafk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfSaQK/btsPh7bLr8S/b2FDQCHPrsn3VfxEFDOafk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfSaQK%2FbtsPh7bLr8S%2Fb2FDQCHPrsn3VfxEFDOafk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1812&quot; height=&quot;558&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;558&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드리뷰를 필수로 두었기 때문에 동료들끼리 부담도 조금 있었던 것 같다. open 되어있는 기간이 길어지면서 프로젝트 작업사항이 늘어지는 경향도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 develop 브랜치를 merge + 배포 브랜치로 사용해서 push 할때마다 배포서버가 돌아가고, 불필요한 비용과 불안정성 문제가 발생했다. 결국 최근까지는 배포 서버를 잠시 꺼두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  코드는 계속 합치되, 배포는 필요할 때만 하고 싶다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 계정이 나의 것이 아니기 때문에, IAM 역할(Role)이나 키(Key)를 자유롭게 생성하거나 수정할 수 없는 상황이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 develop 브랜치만을 사용해서 위 배포전략을 도입할 수 있는지 확인해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포되는 코드에 버전을 붙여서 명시적으로 참조할 수 있도록 git tag based deployment 전략도 함께 사용할 것이다. 위 문제점을 함께 해결할 수 있는 방법으로 판단했다.&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;  CI 와 CD 의 역할의 구분&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 배포 서비스는 하나의 main.yml 에서 필요한 작업만을 수행했다.&lt;/p&gt;
&lt;pre id=&quot;code_1752545697896&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Deploy

on:
  push:
    branches: ['develop']

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Set up Node
        uses: actions/setup-node@v3
        with:
          node-version: 20
          cache: 'npm'

      - name: Prepare Environment
        run: rm -f .env.production &amp;amp;&amp;amp; touch .env.production

      - name: Set ENV Variables
        run: |
          echo &quot;VITE_GOOGLE_CLIENT_ID=${{ secrets.VITE_GOOGLE_CLIENT_ID }}&quot; &amp;gt;&amp;gt; .env.production
          echo &quot;VITE_KAKAO_JS_KEY=${{ secrets.VITE_KAKAO_JS_KEY }}&quot; &amp;gt;&amp;gt; .env.production
          echo &quot;VITE_KAKAO_REDIRECT_URI=${{ secrets.VITE_KAKAO_REDIRECT_URI }}&quot; &amp;gt;&amp;gt; .env.production
          echo &quot;VITE_API_BASE_URL=${{ secrets.VITE_API_BASE_URL }}&quot; &amp;gt;&amp;gt; .env.production
          echo &quot;VITE_API_SOCKET_URL=${{ secrets.VITE_API_SOCKET_URL }}&quot; &amp;gt;&amp;gt; .env.production

      - name: Install Dependencies
        run: npm install --force

      - name: Build Production
        run: CI=false npm run build --if-present

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-2

      - name: Deploy to S3
        env:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: aws s3 sync --region ap-northeast-2 dist s3://kampus-bucket --delete

      - name: Invalidate CloudFront Cache
        run: aws cloudfront create-invalidation --distribution-id ${{secrets.AWS_DISTRIBUTION_ID}} --paths &quot;/*&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;develop 브랜치에 코드가 푸시될 때마다 &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;`on: push: branches: ['develop']` : develop 브랜치에 푸시될때 아래 워크플로우 전체를 실행한다.&lt;/li&gt;
&lt;li&gt;`Checkout` : 워크플로우가 실행될 가상 환경에 해당 저장소의 코드를 내려받는다.&lt;/li&gt;
&lt;li&gt;`Set up Node` : Node.js 환경 설정. `cache: 'npm'` 설정으로 의존성 설치 속도를 향상시킬 수 있다.&lt;/li&gt;
&lt;li&gt;`Prepare Environment &amp;amp; Set ENV Variables` : 빌드 시 필요한 환경 변수(API 키 등)를 .env.production 파일에 기록한다. 이 변수들은 GitHub Secrets에서 안전하게 가져온다. 즉 git actions 를 배포시스템으로 사용한다면, github secrets 에 개발 env 와 동일한 환경변수를 등록해야 한다.&lt;/li&gt;
&lt;li&gt;`Install Dependencies` : npm install --force 명령으로 package.json에 명시된 모든 의존성을 설치. &lt;s&gt;--force 옵션은 충돌을 무시하고 강제로 설치한다.&lt;/s&gt; 문제가 발생할 수 있기 때문에, 사용을 지양해야 한다.&lt;/li&gt;
&lt;li&gt;`Build Production` : npm run build 명령으로 실제 서비스될 웹사이트 파일들을 생성하여 dist 폴더에 담는다. 즉 빌드한다.&lt;/li&gt;
&lt;li&gt;`Deploy to S3` : 빌드된 dist 폴더의 내용을 S3 버킷에 동기화한다. --delete 옵션은 기존 파일 중 불필요한 것을 삭제한다.&lt;/li&gt;
&lt;li&gt;`Invalidate CloudFront Cache` : CloudFront CDN에 저장된 이전 버전의 캐시를 모두 무효화하여, 모든 사용자가 즉시 최신 버전의 웹사이트를 볼 수 있도록 한다. (&lt;i&gt;과거 프로젝트에서 버전 업데이트에 일주일 이상이 걸려 난감함을 겪은 적이 있다. 기간은 예상할 수 없었다.&lt;/i&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 파일에서 의존성 설치, 빌드, 동기화 모든 작업을 진행한다. 코드품질, 테스트 등에 대한 검증은 없었다. 기존 작업을 ci.yml, cd.yml 두개의 파일로 구분하고, 역할 또한 분류하기로 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CI : 코드 품질 검사, 빌드 가능성 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`.github/workflows/ci.yml` 파일에 정의된 역할은 develop 브랜치로 들어오는 모든 코드 변경을 Push, Pull Request 할 때에 꼼꼼히 검증하는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 스타일 `Prettier`과 문법 `ESLint`을 확인한 뒤, 최종적으로 프로젝트를 빌드`npm run build`하여 &lt;b&gt;배포 가능한 상태인지&lt;/b&gt;를 테스트했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 워크플로우는 절대 배포를 실행하지 않는다.&lt;/p&gt;
&lt;pre id=&quot;code_1752546865703&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: CI

on:
  push:
    branches: ['develop']
  pull_request:
    branches: ['develop']

permissions:
  contents: read

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

      - name: Set up Node
        uses: actions/setup-node@v3
        with:
          node-version: 20
          cache: 'npm'

      - name: Install Dependencies
        run: npm ci

      - name: Run Code Quality Check
        run: npm run prettier:check &amp;amp;&amp;amp; npm run lint

      - name: Prepare Environment
        run: rm -f .env.production &amp;amp;&amp;amp; touch .env.production

      - name: Set ENV Variables
        run: |
          echo &quot;VITE_GOOGLE_CLIENT_ID=${{ secrets.VITE_GOOGLE_CLIENT_ID }}&quot; &amp;gt;&amp;gt; .env.production
          echo &quot;VITE_KAKAO_JS_KEY=${{ secrets.VITE_KAKAO_JS_KEY }}&quot; &amp;gt;&amp;gt; .env.production
          echo &quot;VITE_KAKAO_REDIRECT_URI=${{ secrets.VITE_KAKAO_REDIRECT_URI }}&quot; &amp;gt;&amp;gt; .env.production
          echo &quot;VITE_API_BASE_URL=${{ secrets.VITE_API_BASE_URL }}&quot; &amp;gt;&amp;gt; .env.production
          echo &quot;VITE_API_SOCKET_URL=${{ secrets.VITE_API_SOCKET_URL }}&quot; &amp;gt;&amp;gt; .env.production

      - name: Build Production
        run: CI=false npm run build --if-present

      - name: Upload Build Artifact
        if: github.event_name == 'push' &amp;amp;&amp;amp; github.ref == 'refs/heads/develop'
        uses: actions/upload-artifact@v4
        with:
          name: dist-${{ github.sha }}
          path: dist/
          retention-days: 30&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`on: push || pull_request` : 기존에는 push 일때 main을 실행했다. 코드 품질 검사는 pr 시에도 검사하도록 하고, 실패가 발생하면 pr 단계에서 수정할 수 있다.&lt;/li&gt;
&lt;li&gt;`Install Dependencies` : ci 단계에서는 package-lock.json 을 사용해서 의존성을 설치하는 `npm ci` 방식을 권장하고 있다.&lt;/li&gt;
&lt;li&gt;`Run Code Quality Check` : prettier 와 eslint 를 검사하는 단계이다. run 에 적힌 명령어들을은 package.json script 에 등록되어 있어야 한다. write 수정하는 단계가 아닌, check 검사를 실행한다.&lt;/li&gt;
&lt;li&gt;build (환경변수 이후 단계) :
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PR : develop에 합치기 전에 코드가 문제없는지 &lt;b&gt;검증&lt;/b&gt;하는 것이 주 목적이다. 빌드가 성공하는지 확인이 필요하다.&lt;/li&gt;
&lt;li&gt;push : 빌드된 파일이 함께 담겨서 merge 되어야 한다. &lt;a href=&quot;https://datdaradanadat.tistory.com/247#%F0%9F%8F%9A%EF%B8%8F%20Artifact%20%EC%95%84%ED%8B%B0%ED%8C%A9%ED%8A%B8-1&quot;&gt;Artifact 아티팩트와 함께 설명&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  CI 는 코드품질 검사라면서? 빌드를 꼭 해야하나?&lt;/h4&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;push 할때 빌드파일을 함께 보낸다면, develop 브랜치에 있는 코드는 &lt;i&gt;&lt;b&gt;언제나 빌드가 성공하는 상태 &lt;/b&gt;&lt;/i&gt;임을 보장하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 PR에서 빌드를 해보지 않으면, 코드 리뷰까지 마친 코드가 develop에 머지된 후에야 빌드 실패를 발견하게 될 수 있다. 결국, 다른 팀원의 작업에 영향을 줄 수 있는 불안정한 상태를 만들 수 있다.&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;PR 단계에서 미리 빌드를 해봄으로써, 다음과 같은 문제들을 사전에 차단할 수 있다.&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;타입스크립트 컴파일 에러 (Kampus 는 해당사항 없음)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ci 에서 빌드한 파일을 cd 에서 다운받아서 S3 에 배포하는 방법 ? `Artifact`&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ci 에서 사용한 코드와 cd 에서 사용한 코드가 100% 동일하다고 보장할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cd 에서 프로젝트 빌드 시간을 단축할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; ️ Artifact 아티팩트&lt;/h3&gt;
&lt;figure id=&quot;og_1752568397907&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;워크플로 아티팩트 다운로드 - GitHub Docs&quot; data-og-description=&quot;기본적으로 GitHub는 90일 동안 빌드 로그와 아티팩트를 저장하며 리포지토리 유형에 따라 이 보존 기간을 사용자 지정할 수 있습니다. 자세한 내용은 리포지토리에 대한 GitHub Actions 설정 관리을(&quot; data-og-host=&quot;docs.github.com&quot; data-og-source-url=&quot;https://docs.github.com/ko/actions/how-tos/managing-workflow-runs-and-deployments/managing-workflow-runs/downloading-workflow-artifacts&quot; data-og-url=&quot;https://docs-internal.github.com/ko/actions/how-tos/managing-workflow-runs-and-deployments/managing-workflow-runs/downloading-workflow-artifacts&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/S3JK0/hyZm75lyHS/KpQnsUJokNbwpB0ry6zAJ1/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/dCkJQ6/hyZnjLt2Og/497nNhfklvK0CYObvJwOE0/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/quSC3/hyZnsuOinG/gl0MOsyg6i3VbpAuNMxH6K/img.png?width=670&amp;amp;height=860&amp;amp;face=0_0_670_860&quot;&gt;&lt;a href=&quot;https://docs.github.com/ko/actions/how-tos/managing-workflow-runs-and-deployments/managing-workflow-runs/downloading-workflow-artifacts&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.github.com/ko/actions/how-tos/managing-workflow-runs-and-deployments/managing-workflow-runs/downloading-workflow-artifacts&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/S3JK0/hyZm75lyHS/KpQnsUJokNbwpB0ry6zAJ1/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/dCkJQ6/hyZnjLt2Og/497nNhfklvK0CYObvJwOE0/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/quSC3/hyZnsuOinG/gl0MOsyg6i3VbpAuNMxH6K/img.png?width=670&amp;amp;height=860&amp;amp;face=0_0_670_860');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;워크플로 아티팩트 다운로드 - GitHub Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;기본적으로 GitHub는 90일 동안 빌드 로그와 아티팩트를 저장하며 리포지토리 유형에 따라 이 보존 기간을 사용자 지정할 수 있습니다. 자세한 내용은 리포지토리에 대한 GitHub Actions 설정 관리을(&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프라인의 각 단계에서 생성되는 결과물&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드 프로젝트의 경우, npm run build를 실행했을 때 생성되는 dist 또는 build 폴더를&amp;nbsp;&lt;b&gt;빌드 아티팩트&lt;/b&gt;라고 한다. 이 외에도 테스트 결과 리포트, Docker 이미지 등도 아티팩트가 될 수 있다.&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;일반적으로 권장되는 CI/CD 파이프라인에서는 CI 단계에서 &lt;b&gt;코드 품질 검사, 테스트, 빌드&lt;/b&gt;까지 모두 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 CI에서 빌드까지 마친 후, 이 아티팩트를 CD 단계에 넘겨주어 배포만 진행하게 하는 것!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장점&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;신뢰성 및 일관성 보장: &quot;테스트를 통과한 바로 그 결과물&quot; 배포
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CI에서 빌드를 마친 아티팩트는 검증이 완료된 상태&lt;/li&gt;
&lt;li&gt;만약 CD에서 다시 소스 코드를 받아와 빌드하면, 그 사이에 의존성이 미세하게 변경되는 등 &lt;b&gt;예측 불가능한 변수&lt;/b&gt;가 생길 수 있다. 아티팩트 방식은 이러한 위험을 차단한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;배포 속도 증가
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CD 파이프라인에서는 무거운 npm install이나 시간이 오래 걸리는 npm run build 과정을 생략한다.&lt;/li&gt;
&lt;li&gt;이미 만들어진 아티팩트를 내려받아 배포만 하면 되므로, 배포 단계가 매우 빠르고 가벼워진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;단점&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;초기 설정의 복잡도
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;actions/upload-artifact, actions/download-artifact&lt;/li&gt;
&lt;li&gt;아티팩트를 업로드하고 다운로드하는 과정이 워크플로우에 추가되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;GitHub Actions의 경우 기본 제공 용량이 있지만, 초과 시 비용 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 CI에서 `코드 검사 &amp;rarr; 테스트 &amp;rarr; 빌드 &amp;rarr; 아티팩트 업로드`를 수행하고, CD에서는 `아티팩트 다운로드 &amp;rarr; 배포`만 담당하도록 역할을 구분하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ci, cd 로 트리거가 독립된 워크플로우로 구분되어 있어도, 동일 커밋 기반이기 때문에 파일을 공유할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://datdaradanadat.tistory.com/247#%F0%9F%91%80%20CI%20%EC%99%80%20CD%20%EC%9D%98%20%EC%97%AD%ED%95%A0%EC%9D%98%20%EA%B5%AC%EB%B6%84-1&quot;&gt;CI `Upload Build Artifact` 단계&lt;/a&gt;에서 빌드된 파일을 아티팩트에 업로드 하도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; if 조건을 통해 pr 생성시에는 아티팩트에 업로드하지 않고, push 할때에만 배포용 아티팩트를 생성하고 업로드 하도록 설정했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;github.event_name == 'push' &amp;amp;&amp;amp; github.ref == 'refs/heads/develop'&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 아티팩트 이름을 커밋 해시와 연결하여 고유하게 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CD : v *태그 기반의 실제 서버 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`github/workflows/cd.yml`에서는 develop 브랜치에서 push 작업을 하다가, 코드가 안정화 되었거나 런칭해도 괜찮다고 판단할때 실행되도록 설계했다.&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;CI가 만들어둔 아티팩트를 정확히 찾아 다운로드한 뒤, AWS S3에 업로드하고 CloudFront 캐시를 무효화한다.&lt;/p&gt;
&lt;pre id=&quot;code_1752550549313&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Manual Release and Deploy

on:
  workflow_dispatch:

jobs:
  release-and-deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Checkout develop branch
        uses: actions/checkout@v4
        with:
          ref: 'develop'
          fetch-depth: 0

      - name: Get latest commit SHA
        id: get_sha
        run: echo &quot;sha=$(git rev-parse HEAD)&quot; &amp;gt;&amp;gt; $GITHUB_OUTPUT

      - name: Get next version
        id: version
        run: |
          LATEST_TAG=$(git describe --tags --abbrev=0 2&amp;gt;/dev/null || echo &quot;v0.0.0&quot;)
          VERSION_PARTS=(${LATEST_TAG//./ })
          MAJOR=${VERSION_PARTS[0]#v}
          MINOR=${VERSION_PARTS[1]}
          PATCH=${VERSION_PARTS[2]}
          NEW_PATCH=$((PATCH + 1))
          NEW_TAG=&quot;v${MAJOR}.${MINOR}.${NEW_PATCH}&quot;
          echo &quot;new_tag=$NEW_TAG&quot; &amp;gt;&amp;gt; $GITHUB_OUTPUT

      - name: Download Build Artifact
        uses: actions/download-artifact@v4
        with:
          name: dist-${{ steps.get_sha.outputs.sha }}
          path: dist/
          github-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-2

      - name: Deploy to S3
        run: aws s3 sync ./dist s3://kampus-bucket --delete

      - name: Invalidate CloudFront Cache
        run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_DISTRIBUTION_ID }} --paths &quot;/*&quot;

      - name: Create Release and Tag
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ steps.version.outputs.new_tag }}
          name: 'Release ${{ steps.version.outputs.new_tag }}'
          generate_release_notes: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`on: workflow_dispatch` : 수동 배포 버튼 역할
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자가 직접 GitHub 저장소의 Actions 탭에서 'Run workflow' 버튼을 눌러야 시작된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;`permissions: contents: write` :&amp;nbsp;Git 태그를 생성하고 GitHub Release를 발행하는 등, 저장소의 내용에 쓰기 작업을 할 수 있도록 권한을 부여한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 권한이 없으면 Create Release and Tag 단계에서 실패&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;`Checkout develop branch` : develop 브랜치의 최신 소스 코드를 가져옴
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`fetch-depth: 0` : Git의 전체 히스토리를 가져와, 나중에 버전 태그를 찾고 릴리즈 노트를 생성할 때 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;`Get latest commit SHA` : 체크아웃한 develop 브랜치의 가장 최신 커밋 해시(고유 ID)를 찾아 저장한다. 이 값은 나중에 정확한 빌드 아티팩트를 찾는 데 사용된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`id: get_sha` : 이 단계의 ID를 get_sha로 지정하여, 나중에 steps.get_sha.outputs.sha 형태로 결과를 참조할 수 있다.&lt;/li&gt;
&lt;li&gt;`echo &quot;sha=...&quot; &amp;gt;&amp;gt; $GITHUB_OUTPUT: git rev-parse HEAD` : sha라는 이름의 출력 변수로 만듬&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;`Get next version`: 저장소의 마지막 태그를 기준으로 다음 패치 버전을 자동으로 계산한다. (예: v1.0.0 &amp;rarr; v1.0.1)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`LATEST_TAG=$(git describe ... || echo &quot;v0.0.0&quot;): git describe` : 마지막 태그를 찾되, 태그가 하나도 없으면 v0.0.0에서 시작하도록 처리한다.&lt;/li&gt;
&lt;li&gt;`NEW_PATCH=$((PATCH + 1))` : 셸 스크립트를 이용해 마지막 버전 숫자를 1 증가시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;`Download Build Artifact` : CI 워크플로우가 이전에 만들어 둔 빌드 결과물인 아티팩트를 다운로드한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`name: dist-${{ steps.get_sha.outputs.sha }}` : `Get latest commit SHA` 단계에서 찾은 커밋 해시를 이용해, 정확히 일치하는 아티팩트의 이름을 지정하여 다운로드한다.   &lt;b&gt;CI와 CD를 연결하는 핵심 고리&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;main.yml 의 환경변수, S3 버킷 업로드, 캐시무효화 동일&lt;/li&gt;
&lt;li&gt;`Create Release and Tag` : releas 태그, 릴리즈 노트 통제
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배포가 완료된 후, 계산된 새 버전으로 Git 태그를 생성&lt;/li&gt;
&lt;li&gt;해당 버전의 릴리즈 노트를 GitHub에 자동으로 발행&lt;/li&gt;
&lt;li&gt;`uses: softprops/action-gh-release@v2` : 릴리즈 발행을 쉽게 처리해 주는 인기 있는 커뮤니티 액션&lt;/li&gt;
&lt;li&gt;`generate_release_notes: true` : 마지막 릴리즈 이후의 모든 커밋 메시지를 자동으로 취합하여 릴리즈 노트 본문을 만들어 준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  release 태그 &amp;amp; 릴리즈노트 자동화 배포 ❌ 트리거 통제 ⭕️&lt;/h3&gt;
&lt;pre id=&quot;code_1752555363924&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  create-release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    needs: ci
    if: needs.ci.result == 'success' &amp;amp;&amp;amp; github.event_name == 'push' &amp;amp;&amp;amp; github.ref == 'refs/heads/develop'
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get next version
        id: version
        run: |
          LATEST_TAG=$(git describe --tags --abbrev=0 2&amp;gt;/dev/null || echo &quot;v0.0.0&quot;)
          VERSION_PARTS=(${LATEST_TAG//./ })
          MAJOR=${VERSION_PARTS[0]#v}
          MINOR=${VERSION_PARTS[1]}
          PATCH=${VERSION_PARTS[2]}
          NEW_PATCH=$((PATCH + 1))
          NEW_TAG=&quot;v${MAJOR}.${MINOR}.${NEW_PATCH}&quot;
          echo &quot;new_tag=$NEW_TAG&quot; &amp;gt;&amp;gt; $GITHUB_OUTPUT
          
      - name: Create Release Notes and Tag
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ steps.version.outputs.new_tag }}
          name: &quot;Release ${{ steps.version.outputs.new_tag }}&quot;
          generate_release_notes: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드처럼 ci 가 돌아가고, cd 이전에 태그와 릴리즈 노트를 자동화 시킨다. 그리고 release 생성과 문서를 기반으로 cd 를 돌아가게 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 이 과정까지 자동화 할 경우, 배포 전략에 문제가 발생한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  배포와 머지 모두 develop 브랜치를 사용한다. 머지는 배포되지 않는다. 태그가 있는 머지는 배포된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그&amp;amp;릴리즈 노트 자동화 할 경우, 모든 머지는 배포 프로세스를 진행하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 전략은 브랜치 배포 플로우가` features &amp;rarr; develop 개발서버 &amp;rarr; main 배포서버` 개발서버와 배포서버가 구분되어있을때, 개발서버에서 배포서버로 넘길때 모든 스텝을 자동화 할 수 있다.&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;그러나 Kampus 프로젝트는 코드 혼란의 비용을 줄이기 위해 develop 브랜치 하나만 사용하기로 결정했었다. 하나의 브랜치를 사용해서 워크플로우를 생성할 경우, push 와 배포는 구분되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;workflow_dispatch&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을 구분할 수 있는 것이 워크플로우를 트리거하는 이벤트 &lt;a href=&quot;https://docs.github.com/ko/actions/reference/events-that-trigger-workflows#workflow_dispatch&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;workflow_dispatch&lt;/a&gt; 이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;자동화의 주체를 ⭕️이벤트 중심⭕️으로 실행한다!&lt;br /&gt;❌ 사람, 코드, 브랜치 ❌&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Github 브라우저의 Actions 에서 GUI 로 간편하게 배포할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;574&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NkpAu/btsPi2OZceS/Qgk3E2VhDor4etty81uLv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NkpAu/btsPi2OZceS/Qgk3E2VhDor4etty81uLv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NkpAu/btsPi2OZceS/Qgk3E2VhDor4etty81uLv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNkpAu%2FbtsPi2OZceS%2FQgk3E2VhDor4etty81uLv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;574&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;574&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;gh workflow run cd.yml&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크립트로도 배포 트리거를 실행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에는 기재되어있는 inputs 은 실행 시점에 선택하거나 변경할 옵션이 없고, tags 은 테스트할 시나리오가 없어서, environment 는 단일 production 배포 환경을 사용하기 때문에 필요없는 옵션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 inputs 옵션을 사용해서 배포의 롤백 기능을 극대화 할 수 있다고 한다.&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;여기에서 cd.yml 워크플로우를 실행할 수 있는 트리거를 사용하는 것!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 배포 이후, 해당 커밋에 `v* 버전`을 태그와 릴리즈노트를 자동화하여 부여하고, 기록한다.&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;  최종 배포 아키텍처 시나리오&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1263&quot; data-origin-height=&quot;402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSAQbb/btsPkwVMY5b/kAnKhfV7Za4RpHXGRk9FiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSAQbb/btsPkwVMY5b/kAnKhfV7Za4RpHXGRk9FiK/img.png&quot; data-alt=&quot;CI 전략 아키텍처&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSAQbb/btsPkwVMY5b/kAnKhfV7Za4RpHXGRk9FiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSAQbb%2FbtsPkwVMY5b%2FkAnKhfV7Za4RpHXGRk9FiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1263&quot; height=&quot;402&quot; data-origin-width=&quot;1263&quot; data-origin-height=&quot;402&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CI 전략 아키텍처&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RXdeQ/btsPiWuFJas/Y2W9ihdNcdKISKdyfhObo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RXdeQ/btsPiWuFJas/Y2W9ihdNcdKISKdyfhObo1/img.png&quot; data-alt=&quot;CD 전략 아키텍처&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RXdeQ/btsPiWuFJas/Y2W9ihdNcdKISKdyfhObo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRXdeQ%2FbtsPiWuFJas%2FY2W9ihdNcdKISKdyfhObo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1248&quot; height=&quot;394&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CD 전략 아키텍처&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 수립된 배포 파이프라인은 CI(지속적 통합)와 CD(지속적 배포)의 역할을 명확히 분리하여, 코드 통합은 자동화하되 실제 배포는 개발자의 통제하에 두는 것을 목표로 했다.&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;개발 통합, 코드품질 검사 (CI 자동 실행)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;develop 브랜치로 코드를 push하거나 Pull Request를 merge&lt;/li&gt;
&lt;li&gt;해당 이벤트를 감지한 ci.yml 워크플로우가 자동으로 실행&lt;/li&gt;
&lt;li&gt;PR : ci.yml은 코드 품질 검사 `Lint` `Prettier` 의존성 설치 `npm ci` 프로젝트 빌드 순서로 작업&lt;/li&gt;
&lt;li&gt;Push : PR 시에 작업했던 코드품질검사, 의존성 설치, 프로젝트 빌드를 작업한다. 그 결과물인 `아티팩트`을 고유한 커밋 해시와 함께 저장하며 작업을 종료한다. 이 단계에서는 배포가 실행되지 않았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배포 결정과 수동 트리거 실행 (CD 수동 실행)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;팀의 결정에 따라 배포가 필요한 시점에 CD 를 진행한다. 코드가 안정화 되거나, 기능 업데이트가 필요한 경우에 해당한다.&lt;/li&gt;
&lt;li&gt;담당자는 GitHub 저장소의 Actions 탭으로 이동&lt;/li&gt;
&lt;li&gt;`Manual Release and Deploy` 워크플로우를 찾아 `Run workflow` 버튼을 클릭하여 수동으로 실행한다.&lt;/li&gt;
&lt;li&gt;수동 트리거는 cd.yml 워크플로우를 develop 브랜치의 가장 최신 커밋을 기준으로 실행시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배포 및 릴리즈 자동화 (CD 작업 수행)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;cd.yml은 먼저 develop 브랜치의 최신 커밋 해시를 기준으로, CI 단계에서 생성된 빌드 아티팩트를 다운로드한다.&lt;/li&gt;
&lt;li&gt;배포 플로우 : 다운로드한 아티팩트를 AWS S3에 배포하고 CloudFront 캐시를 무효화한다.&lt;/li&gt;
&lt;li&gt;릴리즈 자동화 플로우 :
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;배포가 성공적으로 완료된 후, 기존의 마지막 태그를 조회하여 다음 패치 버전을 자동으로 계산한다.&lt;br /&gt;(ex. v1.1.0 &amp;rarr; v1.1.1)&lt;/li&gt;
&lt;li&gt;계산된 새 버전으로 Git 태그를 생성하고, 해당 버전의 변경 내역을 담은 릴리즈 노트를 GitHub에 자동으로 발행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;CI (Pull Request 시)&lt;/b&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;: develop 브랜치로의 PR 생성/업데이트&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수행 작업&lt;/b&gt;: 코드 검증 &amp;rarr; 빌드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과&lt;/b&gt;: PR의 통합 안정성 사전 검증.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CI (Merge 시)&lt;/b&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;: develop 브랜치로의 Push(Merge)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수행 작업&lt;/b&gt;: 코드 검증 &amp;rarr; 빌드 &amp;rarr; &lt;b&gt;아티팩트 저장&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과&lt;/b&gt;: develop 브랜치의 무결성 최종 확인 및 배포용 빌드 결과물 준비.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CD (수동 실행 시)&lt;/b&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;: 최신 아티팩트 다운로드 &amp;rarr; 배포 &amp;rarr; 버전 태그 및 릴리즈 노트 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과&lt;/b&gt;: 프로덕션 환경에 최신 버전이 배포되고, 버전 이력이 기록됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  느낀점 &amp;amp; 기대효과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;# 관련 &lt;a href=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/194/commits/92cf7f7b563f42a9722d04ade64af0f5f04821f9&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;커밋&lt;/a&gt;, &lt;a href=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/194&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PR&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 push 이벤트를 기반으로 동작하는 초기 파이프라인에서 시작하여, 우리 Kampus 팀의 배포 전략과 규칙을 코드로 녹여내는 과정을 거쳤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 `좋은 CI/CD`란 ❌무조건적인 자동화❌가 아니라, ⭕️팀의 작업 방식에 맞춰 &lt;b&gt;필요한 제어권을 갖는 것⭕️&lt;/b&gt;임을 깨달았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프라인을 설계하는 것은 &lt;b&gt;우리의 개발 및 배포 규칙을 명확히 정의하는 과정&lt;/b&gt;과 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;비용 절감&lt;/b&gt;: 모든 머지(Merge)마다 발생했던 배포 과정을 없애고, 필요할 때만 실행함으로써 S3 및 CloudFront 사용 비용을 최소화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배포 안정성 확보&lt;/b&gt;: 오직 담당자의 명시적인 실행에 의해서만 배포가 진행되므로, 개발 중인 기능이나 불안정한 코드가 프로덕션 환경에 배포될 위험이 사라졌다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;개발 생산성 향상&lt;/b&gt;: 개발자들은 프로덕션 배포에 대한 부담 없이 develop 브랜치에 자유롭게 코드를 통합할 수 있게 되어, 개발 속도와 심리적 안정감을 높였다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업한 PR&lt;/p&gt;
&lt;figure id=&quot;og_1752569041729&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;[CHORE] git hooks 자동화, build 전에 Prettier와 ESLint 검사 by chae-dahee &amp;middot; Pull Request #194 &amp;middot; IT-Cotato/10th-Kampus-&quot; data-og-description=&quot;개요 Resolves: #192 discussions : #184 PR 유형 어떤 변경 사항이 있나요? 새로운 기능 추가 빌드 부분 혹은 패키지 매니저 수정 작업 내용 커밋 add 된 파일만 검사합니다 (lint-staged 역할) 커밋을 실행했을&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/194&quot; data-og-url=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/194&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/w0SD6/hyZjvz4rrg/3XRGpMMeMEVKZYB5SMJqx0/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_133_1038_202,https://scrap.kakaocdn.net/dn/gGOxM/hyZngBeHh5/wZRxC2wuTckKv2NFxdSVSK/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_133_1038_202&quot;&gt;&lt;a href=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/194&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/194&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/w0SD6/hyZjvz4rrg/3XRGpMMeMEVKZYB5SMJqx0/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_133_1038_202,https://scrap.kakaocdn.net/dn/gGOxM/hyZngBeHh5/wZRxC2wuTckKv2NFxdSVSK/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_133_1038_202');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[CHORE] git hooks 자동화, build 전에 Prettier와 ESLint 검사 by chae-dahee &amp;middot; Pull Request #194 &amp;middot; IT-Cotato/10th-Kampus-&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;개요 Resolves: #192 discussions : #184 PR 유형 어떤 변경 사항이 있나요? 새로운 기능 추가 빌드 부분 혹은 패키지 매니저 수정 작업 내용 커밋 add 된 파일만 검사합니다 (lint-staged 역할) 커밋을 실행했을&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel 무료 배포에서도 develop 가능한 작업이어서 함께 소개하면 좋을듯 합니다&lt;/p&gt;
&lt;figure id=&quot;og_1752569230517&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;  Vercel 무료 자동 배포, Github Action으로 CICD 구축&quot; data-og-description=&quot;Vercel 무료 배포firebase로 로그인, 이메일전송 등의 서비스를 구축하고 있다. 미리 배포한 후 연동하려고 하는데 멘토님께서 firebase 배포가 아닌 vercel 배포를 추천하셨다.Git Organization 에서 FE, BE 레&quot; data-og-host=&quot;datdaradanadat.tistory.com&quot; data-og-source-url=&quot;https://datdaradanadat.tistory.com/177&quot; data-og-url=&quot;https://datdaradanadat.tistory.com/177&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dDxhOy/hyZjzCruDQ/VWZ6lqVRPaUpDYXK2EF4W1/img.png?width=719&amp;amp;height=643&amp;amp;face=0_0_719_643,https://scrap.kakaocdn.net/dn/jQrlW/hyZjjM4Gfe/6fDLzrToNMAFvgwHGOFeWK/img.png?width=719&amp;amp;height=643&amp;amp;face=0_0_719_643,https://scrap.kakaocdn.net/dn/kRTX1/hyZnkXVPLX/fu8oJTB0IIpAVfu1fvVam0/img.png?width=1746&amp;amp;height=459&amp;amp;face=0_0_1746_459&quot;&gt;&lt;a href=&quot;https://datdaradanadat.tistory.com/177&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://datdaradanadat.tistory.com/177&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dDxhOy/hyZjzCruDQ/VWZ6lqVRPaUpDYXK2EF4W1/img.png?width=719&amp;amp;height=643&amp;amp;face=0_0_719_643,https://scrap.kakaocdn.net/dn/jQrlW/hyZjjM4Gfe/6fDLzrToNMAFvgwHGOFeWK/img.png?width=719&amp;amp;height=643&amp;amp;face=0_0_719_643,https://scrap.kakaocdn.net/dn/kRTX1/hyZnkXVPLX/fu8oJTB0IIpAVfu1fvVam0/img.png?width=1746&amp;amp;height=459&amp;amp;face=0_0_1746_459');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;  Vercel 무료 자동 배포, Github Action으로 CICD 구축&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Vercel 무료 배포firebase로 로그인, 이메일전송 등의 서비스를 구축하고 있다. 미리 배포한 후 연동하려고 하는데 멘토님께서 firebase 배포가 아닌 vercel 배포를 추천하셨다.Git Organization 에서 FE, BE 레&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;datdaradanadat.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  FrontEnd</category>
      <category>CICD</category>
      <category>eslint</category>
      <category>JavaScript</category>
      <category>kampus</category>
      <category>React</category>
      <category>리액트</category>
      <category>배포</category>
      <category>자바스크립트</category>
      <category>전략</category>
      <category>프론트엔드</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/247</guid>
      <comments>https://datdaradanadat.tistory.com/247#entry247comment</comments>
      <pubDate>Tue, 15 Jul 2025 17:23:25 +0900</pubDate>
    </item>
    <item>
      <title>  Husky, lint-staged, commitlint를 이용한 Git Hook 품질 검사 관리</title>
      <link>https://datdaradanadat.tistory.com/246</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BJbXH/btsPcEHaXQa/ADZv45n7FTPonKejEXu3E0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BJbXH/btsPcEHaXQa/ADZv45n7FTPonKejEXu3E0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BJbXH/btsPcEHaXQa/ADZv45n7FTPonKejEXu3E0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBJbXH%2FbtsPcEHaXQa%2FADZv45n7FTPonKejEXu3E0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 PR &lt;a href=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/183&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#183&lt;/a&gt;, &lt;a href=&quot;https://datdaradanadat.tistory.com/244&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;  린트 eslintrc.cjs -&amp;gt; eslint.config.js : Flat Config 업그레이드!&lt;/a&gt; 에서 eslint.config.js 를 제대로 도입하게 되면서, 프로젝트의 코드 품질을 일관되게 유지하고 팀원 간의 협업 효율성을 높이기 위해 husky, lint-staged, commitlint 세 가지 도구의 도입을 제안했다.&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;github discussion 활용, 배포 회의때 동료에게 설명하였다.&lt;/p&gt;
&lt;figure id=&quot;og_1752805038072&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;eslint 기반 Git Hook 품질 검사 도구 - Husky, lint-staged, commitlint &amp;middot; IT-Cotato 10th-Kampus-FE &amp;middot; Discussion #184&quot; data-og-description=&quot;이번에 PR #183 에서 eslint.config.js 를 제대로 도입하게 되면서, 프로젝트의 코드 품질을 일관되게 유지하고 팀원 간의 협업 효율성을 높이기 위해 husky, lint-staged, commitlint 세 가지 도구의 도입을 제&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/discussions/184&quot; data-og-url=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/discussions/184&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bCzvOV/hyZnhmWiYw/1xyqJ9UzQUmDgYJejsiwrk/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_133_1038_202,https://scrap.kakaocdn.net/dn/DHmh6/hyZjFXkppE/FUyutkGHFakjBuVCTKMQMK/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_133_1038_202&quot;&gt;&lt;a href=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/discussions/184&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/discussions/184&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bCzvOV/hyZnhmWiYw/1xyqJ9UzQUmDgYJejsiwrk/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_133_1038_202,https://scrap.kakaocdn.net/dn/DHmh6/hyZjFXkppE/FUyutkGHFakjBuVCTKMQMK/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_133_1038_202');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;eslint 기반 Git Hook 품질 검사 도구 - Husky, lint-staged, commitlint &amp;middot; IT-Cotato 10th-Kampus-FE &amp;middot; Discussion #184&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이번에 PR #183 에서 eslint.config.js 를 제대로 도입하게 되면서, 프로젝트의 코드 품질을 일관되게 유지하고 팀원 간의 협업 효율성을 높이기 위해 husky, lint-staged, commitlint 세 가지 도구의 도입을 제&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업한 PR 이다.&lt;/p&gt;
&lt;figure id=&quot;og_1752139083712&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;[CHORE] git hooks 자동화, build 전에 Prettier와 ESLint 검사 by chae-dahee &amp;middot; Pull Request #194 &amp;middot; IT-Cotato/10th-Kampus-&quot; data-og-description=&quot;개요 Resolves: #192 discussions : #184 PR 유형 어떤 변경 사항이 있나요? 새로운 기능 추가 빌드 부분 혹은 패키지 매니저 수정 작업 내용 커밋 add 된 파일만 검사합니다 (lint-staged 역할) 커밋을 실행했을&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/194&quot; data-og-url=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/194&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bsa4zu/hyZfBALh9W/tfV288kLKAk7wSccFewtK0/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_133_1038_202,https://scrap.kakaocdn.net/dn/3aNUM/hyZjzBlhRa/fWmH5MIO28IlL7PY4h1jU1/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_133_1038_202&quot;&gt;&lt;a href=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/194&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/194&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bsa4zu/hyZfBALh9W/tfV288kLKAk7wSccFewtK0/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_133_1038_202,https://scrap.kakaocdn.net/dn/3aNUM/hyZjzBlhRa/fWmH5MIO28IlL7PY4h1jU1/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_133_1038_202');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[CHORE] git hooks 자동화, build 전에 Prettier와 ESLint 검사 by chae-dahee &amp;middot; Pull Request #194 &amp;middot; IT-Cotato/10th-Kampus-&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;개요 Resolves: #192 discussions : #184 PR 유형 어떤 변경 사항이 있나요? 새로운 기능 추가 빌드 부분 혹은 패키지 매니저 수정 작업 내용 커밋 add 된 파일만 검사합니다 (lint-staged 역할) 커밋을 실행했을&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &amp;nbsp;Git Hooks 자동화 시스템 Husky&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git Hooks : Git 특정 이벤트(commit, add, push) 발생 전후로, 자동으로 특정 스크립트hook을 실행할 수 있도록 하는 기능. 이때 코드 품질, 커밋 메시지 컨벤션 일관성 검사 과정을 자동적으로 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 커밋된 코드는 필수로 formatting(prettier) 되어야 하고, push 된 코드는 elinst 가 pass 이여야만 한다는 규칙을 자동화로 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://typicode.github.io/husky/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;husky&lt;/a&gt; 는 Git Hooks 를 쉽게 사용할 수 있는 도구이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;husky 를 사용하지 않을 경우, 직접 .git/hooks 디렉토리에 직접 스크립트를 작성해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;husky 를 사용하여 .husky/pre-commit 등 디렉토리에 명령어를 사용하여 돌아갈 수 있도록 한다. (v4 이하 package.json 필드에 직접 정의하는 방식)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt; 개발자가 수동으로 검사 도구(eslint, prettier)를 실행하는 것을 잊더라도, 자동으로 실행되므로 휴먼 에러를 방지하고, 팀 전체가 동일한 코드 품질 기준을 따르도록 강제할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, husky 를 통해 lint-staged 와 commitlint 실행을 자동화 하는 것이 핵심이다.&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;1. 개발 의존성으로 husky 를 설치한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;npm install -D husky&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 이후 husky 에 설정할 lint-staged, commitlint 설치가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2-1. lint-staged, commiltint 를 먼저 설정한 후&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;a href=&quot;https://datdaradanadat.tistory.com/246#%E2%98%84%EF%B8%8F%20husky%20%EC%97%90%20lint-staged%2C%20commitlint%20%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0-1&quot;&gt;다시 husky 설정을 진행한다&lt;/a&gt;&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;✚ staging 된 파일에 대한 자동 린팅 lint-staged&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;husky 에서 직접 eslint, prettier 를 실행시킬 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 검사를 수행한 후 커밋이 진행되기까지 꽤 긴 시간을 필요로 한다. eslint 와 prettier 가 모든 파일을 검사하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/lint-staged/lint-staged&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;lint-staged&lt;/a&gt;&amp;nbsp;는 커밋하기 위해&amp;nbsp;&lt;b&gt;add&amp;nbsp;스테이징 된 파일&lt;/b&gt;에 대해서만 검사를 할 수 있도록 한다. staged 된 파일들 &amp;rarr; 변경사항이 있고 품질검사가 필요한 파일들만 검사하여 시간을 단축하고, 효율성을 높인다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;npm install -D lint-staged&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;package.json 파일에 lint-stage 를 정의하여, prettier 와 eslint 로 검사할 수 있도록 설정한다.&lt;/h3&gt;
&lt;pre id=&quot;code_1752130327818&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;devDependencies&quot;: {
	&quot;lint-staged&quot;: &quot;^16.1.2&quot;,
},
&quot;lint-staged&quot;: {
  &quot;*.{js,jsx,ts,tsx}&quot;: [
    &quot;eslint --fix&quot;,
    &quot;prettier --write&quot;
  ],
  &quot;*.{json,css,md}&quot;: [
     &quot;prettier --write&quot;
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  eslint 에 대해서 warn 에 대한 경고는 건너띄고 싶다면 `--quiet` 옵션을 뒤에 붙여줄 수 있다. 그러나 build 배포단계에서 eslint 를 검사할때 warn 도 마찬가지로 문제라고 검사하고 실패를 발생시키기 때문에, 해당 옵션은 사용하지 않는 것을 추천한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋 전에 변경된 코드에 대한 피드백(formatting, linting 관련)을 즉시 받아볼 수 있어 코드 품질 문제를 미리 발견하고 수정할 수 있다. 또한, 전체 프로젝트에 대한 검사 비용 없이 코드 스타일을 관리할 수 있습니다.&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;  커밋 컨벤션 검사 commitlint&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://commitlint.js.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;commitlint&lt;/a&gt; 는 Git 커밋 메시지가 팀의 규칙을 따르는지 검사하는 도구이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋 메시지를 정해진 규칙에 따라 작성하도록 강제하여, 팀의 커밋 히스토리를 깔끔하고 일관되게 만들어주는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트가 커지고 팀원이 늘어날수록 Git 커밋 히스토리를 관리하는 일은 중요해진다. 어떤 변경사항이 있었는지 쉽게 파악하고, 특정 기능을 찾거나 버그의 원인을 추적할 때 잘 정리된 커밋 메시지는 도움이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋 이력만 보더라도 어떤 변경사항이 있었는지 쉽게 파악할 수 있게 되어 코드 리뷰나 프로젝트 히스토리 추적이 용이해진다. 규칙에 어긋나는 메시지일 경우 커밋이 실패하고 어떤 규칙을 위반했는지 알려준다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;npm install -D @commitlint/cli @commitlint/config-conventional&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Conventional Commits 커밋 메시지 형식 규칙을 기반으로 커밋 메시지의 일관성, 가독성을 확보한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;type(scope): subject&lt;br /&gt;&amp;lt;--빈 줄--&amp;gt;&lt;br /&gt;body&lt;br /&gt;&amp;lt;--빈 줄--&amp;gt;&lt;br /&gt;footer&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;여기서 가장 중요한 type은 해당 커밋이 어떤 성격의 변경사항인지를 나타낸다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 프로젝트에서는 아래와 같은 타입을 정의하여 사용하고 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-sheets-baot=&quot;1&quot; data-sheets-root=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;타입 (Type)&lt;/td&gt;
&lt;td&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;feat&lt;/td&gt;
&lt;td&gt;새로운 기능 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fix&lt;/td&gt;
&lt;td&gt;버그 수정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docs&lt;/td&gt;
&lt;td&gt;문서 수정 (README.md 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;style&lt;/td&gt;
&lt;td&gt;코드 포맷팅, 세미콜론 누락 등 (기능 변경 없음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;refactor&lt;/td&gt;
&lt;td&gt;코드 리팩토링&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test&lt;/td&gt;
&lt;td&gt;테스트 코드 추가 또는 수정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;chore&lt;/td&gt;
&lt;td&gt;빌드 업무, 패키지 매니저 설정 등 (프로덕션 코드 변경 없음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;perf&lt;/td&gt;
&lt;td&gt;성능 개선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ci&lt;/td&gt;
&lt;td&gt;CI/CD 관련 설정 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;build&lt;/td&gt;
&lt;td&gt;빌드 시스템 또는 외부 종속성 관련 변경사항&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋 유형을  위 10가지로 제한하기 위해 명시적으로 선언한다.&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;commitlint.config.js &amp;rarr; @commitlint/config-conventional 패키지에 미리 정의된, 가장 대중적인 Conventional Commits 규칙을 상속받아 기본으로 사용하기 위해 extends 를 설정한다.&lt;/p&gt;
&lt;pre id=&quot;code_1752131174662&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default {
  extends: ['@commitlint/config-conventional'],
  rules: {
    // 커밋 타입 규칙
    'type-enum': [
      2,
      'always',
      [
        'feat', // 새로운 기능
        'fix', // 버그 수정
        'docs', // 문서 수정
        'style', // 코드 포맷팅, 세미콜론 누락 등 (기능 변경 없음)
        'refactor', // 코드 리팩토링
        'test', // 테스트 코드 추가/수정
        'chore', // 빌드 업무 수정, 패키지 매니저 수정 등
        'perf', // 성능 개선
        'ci', // CI/CD 관련
        'build', // 빌드 시스템 또는 외부 종속성에 영향을 주는 변경사항
      ],
    ],

    // 커밋 타입 소문자, 비어있으면 안됨
    'type-case': [2, 'always', 'lower-case'],
    'type-empty': [2, 'never'],

    // 제목 50자 이하, 비어있거나 마침표로 끝나면 안됨
    'subject-max-length': [2, 'always', 50],
    'subject-empty': [2, 'never'],
    'subject-full-stop': [2, 'never', '.'],

    // 본문, 푸터 72자 이하로 줄바꿈
    'body-max-line-length': [2, 'always', 72],
    'footer-max-line-length': [2, 'always', 72],
  },

  parserPreset: {
    parserOpts: {
      // 기본 형식: type: subject
      // 예: feat: 새로운 기능 추가
      headerPattern: /^(\w*): (.*)$/,
      headerCorrespondence: ['type', 'subject'],
      maxLineLength: 72,
    },
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 프로젝트만의 커밋 규칙을 지정하기위해 rules 를 통해 덮어씌웠다.&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;commitlint의 모든 규칙은 기본적으로 세 부분으로 이루어진 배열 `[Level, Applicable, Value]`&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;Level (강도)&lt;/b&gt;: 규칙 위반 시 어떻게 처리할지 결정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;0: 규칙을 적용하지 않음 (off)&lt;/li&gt;
&lt;li&gt;1: 경고(Warning)만 표시하고 커밋은 허용&lt;/li&gt;
&lt;li&gt;2: 오류(Error)를 표시하고 커밋 중단&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Applicable (적용 조건)&lt;/b&gt;: 규칙을 언제 적용할지&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;always: 항상 규칙을 지켜야 함&lt;/li&gt;
&lt;li&gt;never: 절대 규칙을 위반하면 안 됨 (예: 제목 끝에 마침표를 쓰지 마라)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Value (값)&lt;/b&gt;: 규칙에 사용할 구체적인 값 (예시는 아래와 같음)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커밋 타입 type
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;type-enum&lt;/b&gt;: feat, fix 등 허용할 커밋 타입의 목록을 정의한다. 여기에 정의되지 않은 타입으로 커밋하면 오류를 발생시킨다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;type-case&lt;/b&gt;: 커밋 타입은 항상 lower-case(소문자)여야 한다는 규칙&lt;/li&gt;
&lt;li&gt;&lt;b&gt;type-empty&lt;/b&gt;: 커밋 타입은 비어 있을 수 없다는 규칙&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;커밋 제목 subject
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;subject-empty&lt;/b&gt;: 커밋의 제목은 비어 있을 수 없다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;subject-max-length&lt;/b&gt;: 커밋 제목의 최대 길이를 50자로 제한한다. 제목은 간결하게 작성하는 것이 좋기 때문이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;subject-full-stop&lt;/b&gt;: 커밋 제목은 마침표(.)로 끝나면 안 된다는 규칙&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;커밋 본문 body, 푸터 footer
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;body-max-line-length&lt;/b&gt;: 커밋 본문의 한 줄은 최대 72자를 넘지 않도록 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;footer-max-line-length&lt;/b&gt;: 커밋 푸터의 한 줄도 최대 72자를 넘지 않도록 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;parserPreset 을 통해 커밋 메시지의 형식을 규정한다. 해당 규칙으로 커밋 메시지를 해석(파싱)한다고 정의하는 부분
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;headerPattern&lt;/b&gt;: 커밋 메시지의 헤더(첫 줄)가 어떤 정규식 패턴을 따라야 하는지 정의한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`^(\w*): (.*)$`는 `(타입): (제목)` 형식&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;headerCorrespondence&lt;/b&gt;: headerPattern의 각 그룹이 무엇에 해당하는지 알려준다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 번째 그룹(\w*) type, 두 번째 그룹(.*) subject&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  만일 규칙을 지정하고 싶지 않다면, 해당 속성을 지우면 된다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;feat: 드롭다운 컴포넌트의 디자인 변경

Dropdown 컴포넌트의 색상 변경입니다.
&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☄️ husky 에 lint-staged, commitlint 설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Husky를 활성화하고, Git 이벤트와 위에서 설정한 도구들을 연결한다. (이전 단계에서 설치만 함)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Husky 초기화&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;npx husky init&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.husky 폴더가 생성되며 Husky가 활성화된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. pre-commit Hook 추가 (lint-staged 연동)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋 직전에 lint-staged를 실행하도록 아래 명령어로 pre-commit hook을 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 블로그 글에서는 echo 를 사용했는데, &lt;a href=&quot;https://typicode.github.io/husky/migrate-from-v4.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서 Migrate from v4&lt;/a&gt; 에는 npm, yarn 을 사용할 시 훨씬 간편하게 훅을 추가할 수 있다고 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;npx husky add .husky/pre-commit &quot;npx lint-staged&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.husky/pre-commit 파일이 아래처럼 생성된다. 이제 커밋 시마다 자동으로 코드 스타일과 품질 검사가 이루어집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvpgwv/btsPcZq00Xv/kNQUvHpfYoU66GWc8JuKwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvpgwv/btsPcZq00Xv/kNQUvHpfYoU66GWc8JuKwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvpgwv/btsPcZq00Xv/kNQUvHpfYoU66GWc8JuKwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbvpgwv%2FbtsPcZq00Xv%2FkNQUvHpfYoU66GWc8JuKwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;402&quot; height=&quot;174&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;174&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  lint-staged 가 아닌, prettier 와 eslint 를 직접 선언하게 되면 &amp;rarr; 커밋 때에 프로젝트의 모든 파일에 대해 검사하게 되어 비효율적으로 동작한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. commit-msg Hook 추가 (commitlint 연동)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋 메시지를 검사하도록 아래 명령어로 commit-msg hook을 추가한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;npx husky add .husky/commit-msg &quot;npx commitlint --edit $1&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✨ 여기서 `$1`은 Husky가 commit-msg 훅을 실행할 때, 작성된 커밋 메시지가 임시로 저장된 파일의 경로를 commitlint에 전달하는 매우 중요한 파라미터이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;548&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3jEDy/btsPce9TqrF/VxvQBpSMkJ3VNJClwyCvl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3jEDy/btsPce9TqrF/VxvQBpSMkJ3VNJClwyCvl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3jEDy/btsPce9TqrF/VxvQBpSMkJ3VNJClwyCvl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3jEDy%2FbtsPce9TqrF%2FVxvQBpSMkJ3VNJClwyCvl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;548&quot; height=&quot;206&quot; data-origin-width=&quot;548&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1752134490864&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/sh
. &quot;$(dirname &quot;$0&quot;)/_/husky.sh&quot;

npx --no-install commitlint --edit $1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  위처럼 초기화 될 수도 있는데,&lt;a href=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/194/commits/003354f400fee10f5c67262cb0b9217995ac7d49&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; husky deprecated 경고&lt;/a&gt;가 발생하기 때문에 위의 두줄은 제거해도 좋다.&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;  실행 흐름&lt;/h2&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;개발자가 파일을 수정하고 `git add .` 명령어로 파일들을 staged 상태로 만든다.&lt;/li&gt;
&lt;li&gt;`git commit -m &quot;feat: 새로운 기능 추가&quot;` 명령어를 실행한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Husky&lt;/b&gt;: pre-commit 이벤트를 감지하고 `.husky/pre-commit 파일`을 실행한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;lint-staged&lt;/b&gt;: `npx lint-staged`가 실행되어 staged된 파일들에 대해 &lt;b&gt;ESLint&lt;/b&gt;와&lt;b&gt; Prettier&lt;/b&gt;를&lt;b&gt; 실행&lt;/b&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Husky&lt;/b&gt;: pre-commit이 성공하면 commit-msg 이벤트를 감지하고 `.husky/commit-msg 파일`을 실행한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;commitlint&lt;/b&gt;: `npx commitlint`가 실행되어 작성된 커밋 메시지가 conventional 규칙에 맞는지 검사한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;435&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AQNha/btsPcUQKagG/SqfCkNbrqsYP5qiKuASfr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AQNha/btsPcUQKagG/SqfCkNbrqsYP5qiKuASfr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AQNha/btsPcUQKagG/SqfCkNbrqsYP5qiKuASfr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAQNha%2FbtsPcUQKagG%2FSqfCkNbrqsYP5qiKuASfr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;435&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;435&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkKRaD/btsPdKmiMRj/8cmEof94lmYlR6UKE0qGj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkKRaD/btsPdKmiMRj/8cmEof94lmYlR6UKE0qGj0/img.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;521&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;52.29&quot; data-filename=&quot;blob&quot; style=&quot;width: 51.6814%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkKRaD/btsPdKmiMRj/8cmEof94lmYlR6UKE0qGj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkKRaD%2FbtsPdKmiMRj%2F8cmEof94lmYlR6UKE0qGj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;521&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGBrFu/btsPep22B4I/dlVU87OhAC6RQ873gbUuPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGBrFu/btsPep22B4I/dlVU87OhAC6RQ873gbUuPK/img.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;571&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;47.71&quot; data-filename=&quot;blob&quot; style=&quot;width: 47.1558%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGBrFu/btsPep22B4I/dlVU87OhAC6RQ873gbUuPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGBrFu%2FbtsPep22B4I%2FdlVU87OhAC6RQ873gbUuPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;571&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&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;커밋이 중단&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. &lt;b&gt;성공&lt;/b&gt;: 모든 검사를 통과하면 비로소 &lt;b&gt;커밋이 최종적으로 완료&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;간단히 요약하자면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  git commit을 실행하면 Husky가 각 단계를 가로채 아래 작업들을 실행&lt;br /&gt;1-1. 코드 자동 수정 및 검사 (pre-commit 단계): Husky가 lint-staged를 실행시켜, staged 상태인 파일들의 코드 포맷팅(Prettier)과 문법 오류(ESLint)를 자동으로 수정하고 검사한다. 여기서 문제가 발생하면 커밋 중단&lt;br /&gt;1-2. 커밋 메시지 검사 (commit-msg 단계): 1-1 코드 검사를 통과하면, Husky가 이번엔 commitlint를 실행시켜서 커밋 메시지가 정해진 규칙(커밋 컨벤션)에 맞는지 검사한다. 여기서 규칙을 어기면 커밋 중단&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  도입 후 기대효과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/194&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;# 관련 PR,&lt;/a&gt;&lt;a href=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/194/commits/8516eb3ae9bf41e48bdeac7445114fe8166f3088&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; 커밋 (수정O)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 도구들을 도입한 것은 단순히 기능을 추가하는 것을 넘어서, 팀의 개발 문화를 발전시키는 중요한 과정이라고 생각했다. 초기 설정에 드는 시간은 짧았지만, 그로 인해 얻는 장기적인 효과는 매우 클 것으로 기대했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;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;학습 곡선 없는 생산성 향상&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 모든 변화는 개발자가 git commit을 실행하는 순간 &lt;b&gt;자동으로 적용&lt;/b&gt;된다. 팀원들이 새로운 명령어나 복잡한 절차를 배울 필요가 없으므로, 별도의 학습 곡선 없이 팀 전체의 개발 생산성을 즉시 높일 수 있었다.&lt;/li&gt;
&lt;li&gt;코드 리뷰에서는 이제 스타일 지적 대신 핵심 로직에 더 집중할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;명확한 협업 기준 정립&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;p data-ke-size=&quot;size16&quot;&gt;결론적으로, 이 시스템의 도입은 적은 초기 투자로 &lt;b&gt;장기적인 개발 효율성&lt;/b&gt;과 &lt;b&gt;코드 베이스의 안정성&lt;/b&gt;을 모두 챙길 수 있는 전략이었다. 처음에는 규칙에 맞춰 커밋 메시지를 작성하거나, 코드의 규칙을 따르는 것이 다소 번거롭게 느껴질 수 있다. 하지만 이 노력이 모여 전체 프로젝트의 히스토리를 명확하게 만들고, 미래에 발생할 수 있는 문제를 더 쉽게 해결하는 데 큰 도움이 될 수 있다.&lt;/p&gt;</description>
      <category>  FrontEnd</category>
      <category>CICD</category>
      <category>JavaScript</category>
      <category>npm</category>
      <category>리액트</category>
      <category>배포</category>
      <category>일관성</category>
      <category>자바스크립트</category>
      <category>전략</category>
      <category>프론트엔드</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/246</guid>
      <comments>https://datdaradanadat.tistory.com/246#entry246comment</comments>
      <pubDate>Thu, 10 Jul 2025 18:04:21 +0900</pubDate>
    </item>
    <item>
      <title>  리액트 계층 + 컴포넌트 단위 ErrorBoundary - 효과적으로 서버 요청 관리하기</title>
      <link>https://datdaradanadat.tistory.com/245</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Kampus 프로젝트는 유학생 커뮤니티 서비스이다. MVP 와 기능이 굉장히 다양한데, 에러 처리가 파악이 안되어서 어려움이 있었다.&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;백엔드에서 정의한 에러를 throw 해서 선언적으로 처리할 수는 없을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통 에러 처리 로직을 단순화 할 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 뿐만 아니라 개발자에서 어떤 부분(컴포넌트)에서 어떤 에러가 발생했는지 명확하게 알릴 수는 없을까? (빈화면 금지..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 고민을 해결하기 위해 React 16 의 ErrorBoundary를 도입하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;#참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/devlasbe/error-boundary?tab=readme-ov-file&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/devlasbe/error-boundary?tab=readme-ov-file&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ko.react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/bvaughn/react-error-boundary&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/bvaughn/react-error-boundary&lt;/a&gt;&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;Error Boundary 에러 경계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  하위 컴포넌트에서 발생하는 자바스크립트 에러를 잡아서 fallback UI 를 보여주는 리액트 컴포넌트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선언적으로 에러를 처리하는 방법 중 하나이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목적 : 리액트 렌더링 프로세스 내에서 발생하는 자바스크립트 런타임 에러를 catch 하는 것&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;*리액트 렌더링 프로세스 : react Element tree(render phase) -&amp;gt; Fider = V DOM (commit phase) -&amp;gt; 실제 DOM 에 반영 -&amp;gt; 브라우저 렌더링 프로세스&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예기치 못한 에러가 발생해 throw 작업을 하지 못했더라도,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 렌더링 프로세스 내부에서 발생했다면 errorBoundary를 catch 를 할 수 있다!&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;하위에 포함되는 컴포넌트에서 에러를 throw 하기 위하여, 상위에 일괄적으로 묶는다. App.jsx&lt;/p&gt;
&lt;pre id=&quot;code_1750916770978&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import './styles/global.css';
import { RouterProvider } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import AppRouter from '@/routes/AppRouter';
import { ApiErrorBoundary } from '@/components/common/error/ApiErrorBoundary';
import { Suspense } from 'react';
import { SuspenseFallback } from '@/components/common/error/SuspenseFallback';

function App() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;QueryClientProvider client={queryClient}&amp;gt;
          &amp;lt;ApiErrorBoundary&amp;gt;
            &amp;lt;Suspense fallback={&amp;lt;SuspenseFallback /&amp;gt;}&amp;gt;
              &amp;lt;RouterProvider router={AppRouter} /&amp;gt;
            &amp;lt;/Suspense&amp;gt;
          &amp;lt;/ApiErrorBoundary&amp;gt;
      &amp;lt;/QueryClientProvider&amp;gt;
    &amp;lt;/&amp;gt;
  );
}

export default App;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;리액트 컴포넌트 공식문서의 ErrorBoundary에 관한 내용&lt;/a&gt;에서는 클래스를 직접 선언하는 방법도 있지만, &lt;a href=&quot;https://github.com/bvaughn/react-error-boundary&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;react-error-boundary 라이브러리&lt;/a&gt; 사용을 적극 권장하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1746&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2ZSAF/btsOTx8t1TC/iIA6ZbuC3k98E44Nhudi21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2ZSAF/btsOTx8t1TC/iIA6ZbuC3k98E44Nhudi21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2ZSAF/btsOTx8t1TC/iIA6ZbuC3k98E44Nhudi21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2ZSAF%2FbtsOTx8t1TC%2FiIA6ZbuC3k98E44Nhudi21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1746&quot; height=&quot;480&quot; data-origin-width=&quot;1746&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 에러 바운더리가 API 통신 실패(ex. 500 서버 에러) 자체를 감지하는 것이 아니다. 대신, API 호출이 실패하여 data가 undefined가 되었을 때, 코드에서 data.user.name처럼 존재하지 않는 값에 접근하려다 발생하는 &lt;b&gt;렌더링 에러&lt;/b&gt;를 캐치한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 비동기문 안에서는 throw 한 에러를 catch 하지 못할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 구조(렌더링 프로세스)와 연관이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EventLoop &amp;rarr; CallStack &amp;rarr; v8 JS 엔진 &amp;rarr; api&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 흐름으로 구성되었고, C++ 로 작성되었다.&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;여기에서 비동기 로직은 event loop 에서 처리를 한다. 이를 실행 컨텍스트를 가진다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ fetch 서버 통신&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ event 사용자 이벤트 : onClick, onChange 등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ setTimeout 등등...&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;에러 캐치(ErrorBoundary) 는 JS 엔진에서 실행 컨텍스트를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JS 엔진 : 리액트 컴포넌트 번들링 &amp;rarr; JS 파일의 script 태그 확인 &amp;rarr; JS 엔진에 전달 &amp;rarr; 파싱 &amp;rarr; 실행 컨텍스트 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 주체가 각각의 실행 컨텍스트를 가지는데, 속한 영역이 다르기 때문에 비동기에서는 에러를 캐치하지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ErrorBoundary 는 리액트의 기능이므로 JS 엔진에 해당하는 실행 컨텍스트에서 에러를 캐치한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기의 throw 는 EventLoop에서 발생하는 것이므로 JS 엔진인 ErrorBoundary에서 catch 할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TanStack Query, API Interceptor 와 역할이 중복되는 것 같은데?&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;391&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bz6jF1/btsOUdoQTsd/EK1k6ZlDe4M2VIjFe9SxU1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bz6jF1/btsOUdoQTsd/EK1k6ZlDe4M2VIjFe9SxU1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bz6jF1/btsOUdoQTsd/EK1k6ZlDe4M2VIjFe9SxU1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbz6jF1%2FbtsOUdoQTsd%2FEK1k6ZlDe4M2VIjFe9SxU1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;618&quot; height=&quot;391&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;391&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Query 를 도입한 목적은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 상태 (API 데이터)를 효율적으로 관리하기 위함&lt;/li&gt;
&lt;li&gt;캐싱, 로딩, 리트라이, 에러처리, 상태 공유를 선언형으로 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`React Query`의 에러 핸들링은 &quot;데이터 요청 중 발생한 예외&quot;만 감치하고, 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`API Interceptor`는 사용자의 권한과 관련되어 api 요청을 관리한다. (ex. 401, 403 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 에러가 발생할 수 있는 여러 예외 상황이 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트 렌더링 중 JS 오류 (e.g., undefined.map)&lt;/li&gt;
&lt;li&gt;조건부 렌더링 실수 (data.property.subkey 에서 data 가 null 인 경우)&lt;/li&gt;
&lt;li&gt;사이드 이펙트 중 발생하는 오류 (useEffect 내부에서 throw)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;React Query, API Interceptor는 &quot;API fetch 과정에서 발생한 문제&quot;를 다루는 것이고,&lt;br /&gt;React ErrorBoundary는 &quot;렌더링 중 컴포넌트 계층에서 발생한 예외&quot;를 처리한다.&lt;br /&gt;따라서 모두 설정, 사용해야 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750914780422&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;@tanstack/react-query&quot;: &quot;^5.62.15&quot;,
&quot;react-error-boundary&quot;: &quot;^6.0.0&quot;,&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련해서 &lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/189&quot;&gt;동료와 코드리뷰 한 PR&lt;/a&gt; 을 확인하면,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1888&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pBVaS/btsO1Jo7Otc/NNMJHr2cUPncfhwbN5kUX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pBVaS/btsO1Jo7Otc/NNMJHr2cUPncfhwbN5kUX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pBVaS/btsO1Jo7Otc/NNMJHr2cUPncfhwbN5kUX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpBVaS%2FbtsO1Jo7Otc%2FNNMJHr2cUPncfhwbN5kUX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1888&quot; height=&quot;614&quot; data-origin-width=&quot;1888&quot; data-origin-height=&quot;614&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;인터셉터는 &quot;요청&quot;에 대한 제한, 라우팅은 &quot;화면 진입&quot;에 대한 제한하는 용도로 사용한다고 생각했습니다.&lt;br /&gt;인터셉터로 캐치할 경우, 비동기 상태에 따라 화면깜빡임이나 컴포넌트가 잠깐 렌더링 될 위험이 있습니다. 화면 렌더링에 대한 관점으로 route 가드를 작성하였습니다. 인터셉터에 대한 이중 방어라고 판단했습니다. 또한 페이지 별로 권한에 따른 구분을 명시적으로 표현하는 것도 필요하다고 생각했습니다!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;React Query &lt;/b&gt;: API에서 데이터를 가져오고, 로딩/성공/실패를 구분해 상태를 관리하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ApiErrorBoundary &lt;/b&gt;: 실패 상태가 발생했을 때, 유저에게 어떻게 보여줄지 UI 차원에서 책임지겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UnknownFallback&lt;/b&gt;&amp;nbsp;: 예상하지 못한 JS 오류로부터 앱 전체를 보호하는 범용적인 처리기로, 마지막 보루 역할을 하겠다.&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;  똑똑하게 에러 처리하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어떻게 에러를 보여줄지 결정! - 에러코드에 대한 논의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http status가 에러 상태여도 Error 객체 error.response.data를 통해 받을 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1734507666095&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export function getErrorPayload(error) {
  const response = error?.response;
  const data = response?.data;
  return {
    code: data?.code || data?.errorCode || 'UNKNOWN',
    message: data?.message || '알 수 없는 에러가 발생했습니다.',
    status: response?.status || 500,
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드에서 전달하는 에러코드를 아래처럼 정리해두었다. 형식이 명확히 지정되어 있기 때문에 code, message, status 를 그대로 활용한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sFUs1/btsO3C3rw1R/fIKmYPpaPInswQQk0LTHGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sFUs1/btsO3C3rw1R/fIKmYPpaPInswQQk0LTHGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sFUs1/btsO3C3rw1R/fIKmYPpaPInswQQk0LTHGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsFUs1%2FbtsO3C3rw1R%2FfIKmYPpaPInswQQk0LTHGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;328&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;328&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kampus 서버만의 에러코드와 메시지는 개발의 편의성 DX 를 위해 구현된 부분이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;293&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KhURl/btsO2SyVx4z/8PiatKpaIlANz5Kk6RPUAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KhURl/btsO2SyVx4z/8PiatKpaIlANz5Kk6RPUAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KhURl/btsO2SyVx4z/8PiatKpaIlANz5Kk6RPUAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKhURl%2FbtsO2SyVx4z%2F8PiatKpaIlANz5Kk6RPUAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;293&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;293&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이부분을 그대로 활용해서 사용자에게 에러를 표시하려고 했지만, 이 방법은 어려울 것 같다는 팀원의 의견을 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 해당 에러코드는 개발자를 위한 것. 사용자가 본다고 해서 해결할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 각각의 에러에 따라 프론트 개발자가 처리를 해야한다. - 작업 예정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 외국인 유학생을 위한 프로젝트로써 사용자에게 영어로 표기되어야 하는데, 에러메시지를 전체적으로 변경하기에는 무리가 있다.&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;위 표 중에서 `POST-006`&amp;nbsp; 과 같은 경우에는 표기가 필요하다고 생각했는데, 보안적이나 사용자 측면에서 좋지 않을 것 같다는 말에 동의하게 되었다. 이러한 에러코드의 경우, 영문 toast로 프론트에서 처리하기로 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;QueryClient 세팅으로 에러 throw 하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;api 요청 &amp;rarr; 로딩중 Suspense Fallback UI 표시&amp;nbsp; api 에러 발생 &amp;rarr; queryFn 내부 쿼리에서 에러 캐치 &amp;rarr; 쿼리 훅이 throwOnError 로 에러를 throw함 &amp;rarr; ApiErrorBoundary의 ErrorBoundary에서 포착 &amp;rarr;&amp;nbsp;FallBack UI 표시&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;App.jsx 에서 QueryClient 세팅&lt;/p&gt;
&lt;pre id=&quot;code_1734507997471&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: false,
      throwOnError: true,
    },
    mutations: {
      throwOnError: false,
      onError: (error) =&amp;gt; {
        toast.error(`[${error.code}] ${error.message}`);
      },
    },
  },
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;defaultOptions 로 쿼리 세팅을 지정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;throwOnError : 기본값이 false 이고, true 로 설정할 경우 발생한 에러를 전파한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;(property) throwOnError?: ThrowOnError&amp;lt;unknown, Error, unknown, QueryKey&amp;gt; | undefined&lt;br /&gt;&lt;br /&gt;Whether errors should be thrown instead of setting the&amp;nbsp;error&amp;nbsp;property. If set to&amp;nbsp;true&amp;nbsp;or&amp;nbsp;suspense&amp;nbsp;is&amp;nbsp;true, all errors will be thrown to the error boundary. If set to&amp;nbsp;false&amp;nbsp;and&amp;nbsp;suspense&amp;nbsp;is&amp;nbsp;false, errors are returned as state. If set to a function, it will be passed the error and the query, and it should return a boolean indicating whether to show the error in an error boundary (true) or return the error as state (false). Defaults to&amp;nbsp;false.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;queries 의 에러는 Fallback UI 를 표시하기 위해 throw 해서 ErrorBoundary 로 받고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mutations의 에러는 toast message 만 띄워지고, throw 하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  queries 는 데이터를 조회 read 하는 목적이고, UI 렌더링과 직접적인 연관이 있어서 ErrorBoundary의 fallback UI 로 처리하는 것이 맞다. 그러나 mutations 는 데이터를 변경 create, update, delete 하기 위한 목적이고, UI 렌더링과는 직접적으로 연결되지 않는다. 사용자 피드백 중심으로 처리하는 것이 맞기 때문에 throw 해서 fallback UI 를 보여주지 않고, toast message 로 처리하도록 결정했다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;에러 바운더리 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-query는 suspense: true를 사용할 경우, 에러 발생 시 &lt;b&gt;Suspense &amp;rarr; ErrorBoundary 경로&lt;/b&gt;로 에러를 전파한다. 따라서 컴포넌트 단위의 내부에서 suspense, ErrorBoundary를 함께 사용해야 한다. 이를 간소화 하기 위해 ErrorWrapper 함수를 생성했다.&lt;/p&gt;
&lt;pre id=&quot;code_1751532164481&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export function ErrorWrapper({ children }) {
  return (
    &amp;lt;ApiErrorBoundary&amp;gt;
      &amp;lt;Suspense fallback={&amp;lt;SuspenseFallback /&amp;gt;}&amp;gt;{children}&amp;lt;/Suspense&amp;gt;
    &amp;lt;/ApiErrorBoundary&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  핵심 작업 ApiErrorBoundary&lt;/h3&gt;
&lt;pre id=&quot;code_1751517795847&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { ErrorBoundary } from 'react-error-boundary';
import { Player } from '@lottiefiles/react-lottie-player';
import errorAnimation from '@/assets/lottie/error.json';
import { useQueryErrorResetBoundary } from '@tanstack/react-query';
// ... imports

export function ApiFallback({ resetErrorBoundary }) {
  return (
    &amp;lt;div className=&quot;mx-4 flex flex-col items-center justify-center&quot;&amp;gt;
      &amp;lt;Player src={errorAnimation} loop autoplay speed={1} className=&quot;h-16&quot; /&amp;gt;
      // ... UI
    &amp;lt;/div&amp;gt;
  );
}

export function ApiErrorBoundary({ children, fallbackRender = ApiFallback }) {
  const { reset } = useQueryErrorResetBoundary();
  return (
    &amp;lt;ErrorBoundary onReset={reset} fallbackRender={fallbackRender}&amp;gt;
      {children}
    &amp;lt;/ErrorBoundary&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&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://tanstack.com/query/latest/docs/framework/react/reference/useQueryErrorResetBoundary&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;useQueryErrorResetBoundary&lt;/a&gt;` 훅을 사용하여 onReset 콜백을 구성한다. 내부의 다시시도 버튼을 누르면, 실패했던 query의 api 요청을 초기화하고 재요청 할 수 있다.&lt;/li&gt;
&lt;li&gt;fallbackRender 옵션을 props 로 받는다. 기본값으로 ApiFallback을 지정한다. 이는 아래의 &lt;a href=&quot;https://datdaradanadat.tistory.com/245#%F0%9F%91%91%20%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%20%EC%98%81%EC%97%AD%20%EB%B3%84%20fallback%20UI%20%EB%A5%BC%20%EB%9D%84%EC%9A%B0%EB%8A%94%20%EC%A0%84%EB%9E%B5!-1&quot;&gt;컴포넌트 영역별 fallback UI&lt;/a&gt; 를 위함이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;바로 위에 설명한 ErrorWrapper 에서 설정해 두었다. ErrorWrapper 만 호출하면 간편히 사용할 수 있다!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;알 수 없는 에러 UnknownFallback&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 에러가 api 통신과 관련된 것은 아니다!&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;예측하지 못한 다양한 원인으로 발생할 수 있는 렌더링 에러를 위한 UnknownFallback을 표시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에서는 전체화면을 덮는 더 포괄적인 에러 UI 로 활용했다.&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;또한, 화면 url 을 이동했을때, 에러를 재감지할 수 있도록 location.key 에 따라 reset 하도록 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1751519438080&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export function UnknownFallback({ resetErrorBoundary }) {
  const location = useLocation();

  useEffect(() =&amp;gt; {
    resetErrorBoundary();
  }, [location.key]);

  return (
    &amp;lt;div className=&quot;relative flex h-full w-full flex-col items-center justify-center&quot;&amp;gt;
      &amp;lt;Player
        src={bgErrorAnimation}
        loop
        autoplay
        speed={1}
        className=&quot;absolute inset-0 z-0 h-full w-full opacity-70&quot;
      /&amp;gt;
      &amp;lt;div className=&quot;relative z-10 mx-auto flex flex-col items-center ...&quot;&amp;gt;
        &amp;lt;h2 className=&quot;text-2xl mb-3 font-extrabold text-primary-red&quot;&amp;gt;
          Error Occurred
        &amp;lt;/h2&amp;gt;
        &amp;lt;p className=&quot;mb-4 text-base text-neutral-80&quot;&amp;gt;
          We apologize for the inconvenience.
          &amp;lt;br /&amp;gt;
          Please try again later.
        &amp;lt;/p&amp;gt;
        &amp;lt;ButtonRound
          text=&quot;Retry&quot;
          onClick={resetErrorBoundary}
          theme={BUTTON_THEMES.BASE}
        /&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두개의 fallback 모두에 Retry 버튼으로 에러 바운더리를 reset 할 수 있도록 했다. `resetErrorBoundary`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그리고 이 화면을 표시하도록 AppRouter.jsx 에서 설정했다.&lt;/h4&gt;
&lt;pre id=&quot;code_1751519526993&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const AppRouter = createBrowserRouter([
  {
    path: path.root,
    element: (
      &amp;lt;Layout&amp;gt;
        &amp;lt;ApiErrorBoundary fallbackRender={UnknownFallback}&amp;gt;
          &amp;lt;Suspense fallback={&amp;lt;SuspenseFallback /&amp;gt;}&amp;gt;
            &amp;lt;Outlet /&amp;gt;
          &amp;lt;/Suspense&amp;gt;
        &amp;lt;/ApiErrorBoundary&amp;gt;
      &amp;lt;/Layout&amp;gt;
    ),
    errorElement: (
      &amp;lt;Layout&amp;gt;
        &amp;lt;NotFound /&amp;gt;
      &amp;lt;/Layout&amp;gt;
    ),
    children: [
    ///...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppRouter 최상단에서 모든 페이지 Outlet 을 Boundary 고 감싸서, 분기되어 처리되지 않는 모든 종류의 렌더링 에러를 여기에서 잡는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;왜 ApiErrorBoundary 와 UnkownFallback 을 구분한거지 ❓&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`&amp;lt;ApiErrorBoundary fallbackRender={UnknownFallback}&amp;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;위의 ApiErrorBoundary 코드에서 props 로 fallbackRender 를 받을 수 있도록 했었다.&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;AppRouter 에서는 전체화면으로 에러를 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 ErrorWrapper 의 ApiErrorBoundary의 ApiErrorFallback 으로 영역 화면으로 에러를 보여준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTZGwy/btsO09arxZa/ivEJMKsw9jc2XvzzpgXXY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTZGwy/btsO09arxZa/ivEJMKsw9jc2XvzzpgXXY0/img.png&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;1384&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4899%; margin-right: 10px;&quot; data-widthpercent=&quot;50.07&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTZGwy/btsO09arxZa/ivEJMKsw9jc2XvzzpgXXY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTZGwy%2FbtsO09arxZa%2FivEJMKsw9jc2XvzzpgXXY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;648&quot; height=&quot;1384&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RXqvm/btsO3Lk2MVS/7wEDT1ow3u9Cof1vhkBUl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RXqvm/btsO3Lk2MVS/7wEDT1ow3u9Cof1vhkBUl0/img.png&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;1388&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;49.93&quot; style=&quot;width: 49.3473%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RXqvm/btsO3Lk2MVS/7wEDT1ow3u9Cof1vhkBUl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRXqvm%2FbtsO3Lk2MVS%2F7wEDT1ow3u9Cof1vhkBUl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;648&quot; height=&quot;1388&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(디자인은 수정 단계에 있다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 아래 바로 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  컴포넌트 영역 별 fallback UI 를 띄우는 전략!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://fe-developers.kakaoent.com/2022/221110-error-boundary/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;카카오웹툰 FE 기술블로그&lt;/a&gt;를 참고했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 작성한&lt;/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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 고민을 해결하는 방법으로, 컴포넌트 영역 별 ErrorBoundary 처리 방법을 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 에러가 앱 전체를 마비시킬 필요는 없다. 페이지의 특정 부분 하나의 데이터를 불러오지 못했다고 해서, 에러와 관련없는 모든 요소까지 먹통이 되어서는 안된다. 이를 컴포넌트로 분리하고, 각 요소들을 독립적으로 관리한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;명확한 관심사 분리를 통해 컴포넌트 단위의 응집도는 높이고, 결합도를 낮출 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 API 관련된 에러를 캐치하는 ApiErrorBoundary.jsx에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;컴포넌트 레벨&lt;/b&gt;로 에러를 표시한다 - `ApiFallback`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;루트 레벨&lt;/b&gt;에서 에러를 표시한다 - `GlobalFallback`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;두개로 역할을 구분하여 선언하기 위함&lt;/i&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;Mypage 페이지의 예시코드&lt;/p&gt;
&lt;pre id=&quot;code_1751532737947&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const MyPage = () =&amp;gt; {
  return (
    &amp;lt;div className=&quot;flex h-full w-full flex-col ...&quot;&amp;gt;
      &amp;lt;NotificationButton /&amp;gt;
      &amp;lt;div className=&quot;flex h-full w-full flex-col ...&quot;&amp;gt;
        {/* 사용자 정보 api 데이터 의존성이 높은 핵심 영역 */}
        &amp;lt;ErrorWrapper&amp;gt;
          &amp;lt;MyMainData /&amp;gt;
        &amp;lt;/ErrorWrapper&amp;gt;

        {/* 다른 설정 항목들은 정상적으로 렌더링된다 */}
        &amp;lt;div className=&quot;mt-[1.25rem] ...&quot;&amp;gt;
          &amp;lt;div&amp;gt;Service Settings&amp;lt;/div&amp;gt;
          &amp;lt;button onClick={() =&amp;gt; navigate(...) }&amp;gt;
            My Information
          &amp;lt;/button&amp;gt;
          ///....&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://datdaradanadat.tistory.com/245#%EC%97%90%EB%9F%AC%20%EB%B0%94%EC%9A%B4%EB%8D%94%EB%A6%AC%20%EC%84%A4%EC%A0%95-1&quot;&gt;위에서 설정한 ErrorWrapper&lt;/a&gt; 를 사용하여, 사용자 정보를 불러오는 api 와 그 데이터를 활용하는 영역을 분리하고, 감쌌다. 그 데이터와 무관한 항목들은 정상적으로 렌더링 된다.&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;컴포넌트 레벨의 에러 처리 계층은 전역 에러 처리의 ApiErrorBoundary 로 throw 되지 않고, ErrorWrapper 의 ApiErrorBoundary 로 던져진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AppRouter 의 ApiErrorBounday ❌&lt;/li&gt;
&lt;li&gt;ErrorWrapper 의 ApiErrorBoundary ⭕️&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ErrorWrapper 로 묶이지 않은 컴포넌트나 페이지들은 AppRouter 의 ApiErrorBoundary 로 던져지게 된다.&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;역할 기반 라우터 Role-Based Router 설계로 깔끔하게 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 커지면 수십 개의 라우트가 생겨나고, 각 라우트마다 접근 권한(로그인한 사용자만, 관리자만 등)을 체크하는 로직이 필요하다. 이를 일일이 설정하는 것은 반복적이고 실수하기 쉽다. 그리고 페이지 별로 명시적으로 구분하는 역할이 필요하다고 생각했다. 권한 별 UI 렌더링을 관리하는 방법을 적용했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1888&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5qZBk/btsO3f8w796/mIAFleuM6ZmUhPMlKfOLkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5qZBk/btsO3f8w796/mIAFleuM6ZmUhPMlKfOLkK/img.png&quot; data-alt=&quot;또나온,.. 제 설명입니다 &amp;amp;gt;&amp;amp;lt;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5qZBk/btsO3f8w796/mIAFleuM6ZmUhPMlKfOLkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5qZBk%2FbtsO3f8w796%2FmIAFleuM6ZmUhPMlKfOLkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1888&quot; height=&quot;614&quot; data-origin-width=&quot;1888&quot; data-origin-height=&quot;614&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;또나온,.. 제 설명입니다 &amp;gt;&amp;lt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;createAuthRouter&lt;/b&gt; 헬퍼 함수를 통해 이 문제를 해결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppRouter.jsx 예시&lt;/p&gt;
&lt;pre id=&quot;code_1751533735303&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const createAuthRouter = (routeType, children) =&amp;gt; {
  const authRouter = children.map((child) =&amp;gt; ({
    element:
      routeType === 'PRIVATE' ? (
        &amp;lt;PrivateRoute /&amp;gt;
      ) : routeType === 'PUBLIC' ? (
        &amp;lt;PublicRoute /&amp;gt;
      ) : routeType === 'ADMIN' ? (
        &amp;lt;AdminRoute /&amp;gt;
      ) : (
        &amp;lt;NotFound /&amp;gt;
      ),
    children: [child],
  }));
  return authRouter;
};

const AppRouter = createBrowserRouter([
  {
    path: '/',
    element: &amp;lt;Layout&amp;gt; {/* ... 전역 에러 바운더리 ... */} &amp;lt;/Layout&amp;gt;,
    children: [
      { path: '/', element: &amp;lt;SplashScreen /&amp;gt; },

      // PUBLIC 라우트 그룹
      ...createAuthRouter('PUBLIC', [
        { path: '/login', element: &amp;lt;LoginPage /&amp;gt; },
        { path: '/signup', element: &amp;lt;SignupPage /&amp;gt; },
      ]),

      // PRIVATE 라우트 그룹
      ...createAuthRouter('PRIVATE', [
        { path: '/mypage', element: &amp;lt;MyPage /&amp;gt; },
        { path: '/dashboard', element: &amp;lt;Dashboard /&amp;gt; },
      ]),

      // ADMIN 라우트 그룹
      ...createAuthRouter('ADMIN', [
        { path: '/admin/users', element: &amp;lt;AdminUserPage /&amp;gt; },
      ]),
    ],
  },
]);&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-describing
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;createBrouwserRouter 설정을 보면, 어떤 권한을 필요로 하는지 한눈에 볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;반복 제거 : 상위에서 하나의 Layout, 하나의 Error Boundary 로 묶어 반복 작업을 생략했다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존에는 모든 페이지 라우트 마다 Layout 으로 묶어주어야 했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;847&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/soSvG/btsO4qBeF1T/OAni08VjpTtIKTewHXOTck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/soSvG/btsO4qBeF1T/OAni08VjpTtIKTewHXOTck/img.png&quot; data-alt=&quot;기존 코드 입니다. element 에서 layout 으로 모든 경로마다 묶어주어야 했음&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/soSvG/btsO4qBeF1T/OAni08VjpTtIKTewHXOTck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsoSvG%2FbtsO4qBeF1T%2FOAni08VjpTtIKTewHXOTck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;847&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;847&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기존 코드 입니다. element 에서 layout 으로 모든 경로마다 묶어주어야 했음&lt;/figcaption&gt;
&lt;/figure&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  겪었던 문제점과 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/189&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;# 작업한 PR&lt;/a&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;634&quot; data-start=&quot;219&quot;&gt;  react-error-boundary 내부에서 다시 throw 해도 전역 ErrorBoundary로 전달되지 않음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;634&quot; data-start=&quot;219&quot;&gt;처음에는 GlobalErrorBoundary를 최상위에 두고 모든 오류를 한 곳에서 처리하려 했지만, 자식 컴포넌트에서 이미 ErrorBoundary로 한 번 잡힌 에러는 다시 전파되지 않았다.&lt;/li&gt;
&lt;li data-end=&quot;634&quot; data-start=&quot;219&quot;&gt;이는 React의 동작 특성 때문으로, 한 번 포착된 에러는 이후 단계에서 재전파되지 않으며, 다시 throw 해도 상위 바운더리가 감지하지 못합니다.&lt;/li&gt;
&lt;li data-end=&quot;634&quot; data-start=&quot;219&quot;&gt;이로인해 &lt;a href=&quot;https://fe-developers.kakaoent.com/2022/221110-error-boundary/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;직접 에러바운더리를 구현한 참고한 자료&lt;/a&gt;와는 조금 다른 로직을 가지게 되었다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;634&quot; data-start=&quot;219&quot;&gt;추후 해결점을 찾으면 블로그 글을 추가 작성하도록 하겠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;890&quot; data-start=&quot;636&quot;&gt;비동기 에러(Promise.reject, fetch 등)는 ErrorBoundary에서 감지되지 않음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;890&quot; data-start=&quot;636&quot;&gt;렌더링 중이 아닌 비동기 로직에서 발생한 에러는 React의 ErrorBoundary가 아닌, React Query, try-catch, interceptor 등을 통해 처리해야 했다. &amp;rarr; ErrorBoundary는 렌더링 중 throw된 동기적 JS 오류만 포착할 수 있기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1103&quot; data-start=&quot;892&quot;&gt;컴포넌트 레벨과 전역 레벨의 Fallback UI 관리 기준 혼란
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1103&quot; data-start=&quot;892&quot;&gt;처음에는 모든 Fallback UI를 전역에서 처리하려 했지만, 일부 영역만 오류가 발생했을 경우 전체 UI를 마비시키는 문제가 있었다.&lt;/li&gt;
&lt;li data-end=&quot;1103&quot; data-start=&quot;892&quot;&gt;이후 각 UI의 역할에 따라 &quot;ApiErrorBoundary vs UnknownFallback&quot; 역할 분리 전략을 착안했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 ErrorBoundary 적용 과정에서 React의 예외 처리 메커니즘과 실제 컴포넌트 구조 설계의 상호작용을 깊이 이해할 수 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1550&quot; data-start=&quot;1232&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;915&quot; data-start=&quot;736&quot;&gt;영역 단위 Fallback UI 적용을 통한 응집도 강화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;915&quot; data-start=&quot;736&quot;&gt;오류가 발생한 컴포넌트 단위에서 에러를 처리할 수 있도록 ApiErrorBoundary, UnknownFallback, ErrorWrapper를 계층적으로 설계하여, 서비스 전체 중단 없이 독립적 복구가 가능하도록 구성했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1077&quot; data-start=&quot;917&quot;&gt;역할 기반 라우터 설계로 반복 제거 및 유지보수성 확보
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1077&quot; data-start=&quot;917&quot;&gt;createAuthRouter()를 통해 라우트 권한(ADMIN, PRIVATE, PUBLIC)을 명시적으로 표현함으로써 중복 코드 제거 및 코드 가독성 향상, 향후 유지보수와 변경 대응이 쉬워졌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1256&quot; data-start=&quot;1079&quot;&gt;전역에서 예외 상황을 명시적으로 감지 가능하도록 처리 구조 고도화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1256&quot; data-start=&quot;1079&quot;&gt;API 요청, 렌더링 실패, 인증 실패 등 다양한 원인에 대해 각각에 최적화된 처리 체계를 분리 적용함으로써, 예외 발생 지점을 명확하게 파악하고, 사용자와 개발자 모두에게 적절한 피드백을 제공할 수 있는 구조를 구현했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;앞으로의 프로젝트 개선 방향&lt;br /&gt;- 에러 코드와 메시지의 사용자 노출 정책 정립&lt;br /&gt;- Sentry 등 APM 도구와의 통합으로 에러 로그 수집 자동화 - 서버 시행중&lt;br /&gt;- ErrorBoundary 재사용성을 고려한 컴포넌트 추상화&lt;/blockquote&gt;</description>
      <category>  FrontEnd</category>
      <category>JavaScript</category>
      <category>결합도</category>
      <category>라우팅</category>
      <category>리액트</category>
      <category>에러</category>
      <category>에러바운더리</category>
      <category>응집도</category>
      <category>자바스크립트</category>
      <category>전략</category>
      <category>프론트엔드</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/245</guid>
      <comments>https://datdaradanadat.tistory.com/245#entry245comment</comments>
      <pubDate>Fri, 27 Jun 2025 12:43:14 +0900</pubDate>
    </item>
    <item>
      <title>  린트 eslintrc.cjs -&amp;gt; eslint.config.js : Flat Config 업그레이드! 배포에서 검사하기</title>
      <link>https://datdaradanadat.tistory.com/244</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://eslint.org/docs/latest/use/configure/configuration-files-deprecated&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;eslint 8버전 공식문서&lt;/a&gt;를 확인하면, 아래 종류의 파일을 린트 포맷 설정하는 파일로 작성할 수 있다고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;449&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/37T43/btsOv3mvNnr/fw5WMqzC1H52biRE1b9Lbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/37T43/btsOv3mvNnr/fw5WMqzC1H52biRE1b9Lbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/37T43/btsOv3mvNnr/fw5WMqzC1H52biRE1b9Lbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F37T43%2FbtsOv3mvNnr%2Ffw5WMqzC1H52biRE1b9Lbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;449&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;449&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 &lt;a href=&quot;https://eslint.org/docs/latest/use/configure/configuration-files&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;eslint v.8.23.0&lt;/a&gt; 부터 flat config 가 표준이 되었다고 한다. 즉 eslintrc.~ 형식을 지양 (❌) 하는 것&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. eslint.config.js 가 무엇인지 알고, eslintrc.cjs 의 차이점이 무엇인지 정리해보자.&lt;br /&gt;2. Kampus 프로젝트에는 어떤 방식을 사용할까?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kampus 프로젝트는 런닝커브와 유연성으로 인해 자바스크립트 언어로 구성되어 있다. (타입스크립트로 개선하자는 이야기가 있다!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;unused imports, vars 를 감지하여 warn 을 표시하는 규칙&lt;/u&gt;이 필요했다. &lt;b&gt;린트가 없는 상황에서 -&amp;gt; 미사용 변수 및 함수, import 문이 곳곳에서 표시되었기 때문이다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  eslint.config.js : 새로운 flat config 설정방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;eslintrc.cjs 의 계층적 설정 방식에서 벗어나 &lt;b&gt;단일 JavaScript 기반 구성 파일&lt;/b&gt;로 ESLint를 설정하는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 CommonJS 형식을 사용하는 eslintrc.cjs 에서 벗어나, ESM 모듈 형식으로 처리하겠다는 뜻이다.&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;flat config 가 무엇인지, 어떤점이 왜 바뀐걸까? - what, where, why&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 224px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 28.6821%; height: 19px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 26.8217%; height: 19px;&quot;&gt;기존 eslintrc.js&lt;/td&gt;
&lt;td style=&quot;width: 27.8295%; height: 19px;&quot;&gt;새로운 eslint.config.js - Flat Config&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;장단점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 28.6821%; height: 19px;&quot;&gt;구성 형식&lt;/td&gt;
&lt;td style=&quot;width: 26.8217%; height: 19px;&quot;&gt;객체 기반 - json, js, cjs&lt;/td&gt;
&lt;td style=&quot;width: 27.8295%; height: 19px;&quot;&gt;배열 기반 - JS 파일 (ESM)&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;Node.js 의 개발 방향과 일치한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;width: 28.6821%; height: 38px;&quot;&gt;파일 형식 및 위치&lt;/td&gt;
&lt;td style=&quot;width: 26.8217%; height: 38px;&quot;&gt;.eslintrc, .eslintrc.js, .eslintrc.json, 등 다양한 형식&lt;/td&gt;
&lt;td style=&quot;width: 27.8295%; height: 38px;&quot;&gt;반드시 루트에 eslint.config.js 하나&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 28.6821%; height: 19px;&quot;&gt;병합 방식&lt;/td&gt;
&lt;td style=&quot;width: 26.8217%; height: 19px;&quot;&gt;디렉토리 기반의 자동 병합 Merge&lt;/td&gt;
&lt;td style=&quot;width: 27.8295%; height: 19px;&quot;&gt;명시적 구성, 병합 없음&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 28.6821%; height: 19px;&quot;&gt;환경 설정&lt;br /&gt;(env, parserOptions 등)&lt;/td&gt;
&lt;td style=&quot;width: 26.8217%; height: 19px;&quot;&gt;분산된 설정&lt;br /&gt;- env, parserOptions&lt;/td&gt;
&lt;td style=&quot;width: 27.8295%; height: 19px;&quot;&gt;languageOptions로 통합&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 36px;&quot;&gt;
&lt;td style=&quot;width: 28.6821%; height: 36px;&quot;&gt;플러그인 참조&lt;/td&gt;
&lt;td style=&quot;width: 26.8217%; height: 36px;&quot;&gt;문자열 이름으로 참조&lt;br /&gt;('react')&lt;/td&gt;
&lt;td style=&quot;width: 27.8295%; height: 36px;&quot;&gt;직접 import 및 객체로 사용&lt;br /&gt;(import react from 'eslint-plugin-react')&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;참조 객체의 작성 실수를 개선할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;width: 28.6821%; height: 38px;&quot;&gt;적용 범위 설정&lt;/td&gt;
&lt;td style=&quot;width: 26.8217%; height: 38px;&quot;&gt;전역 또는 오버라이드(overrides) 사용&lt;/td&gt;
&lt;td style=&quot;width: 27.8295%; height: 38px;&quot;&gt;배열 요소마다 files, ignores로 적용 범위 명시&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;depth 를 한단계 낮추고, 복잡한 상황을 한단계 낮춘다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 28.6821%; height: 19px;&quot;&gt;표현 방식&lt;/td&gt;
&lt;td style=&quot;width: 26.8217%; height: 19px;&quot;&gt;선언적 + 암시적&lt;/td&gt;
&lt;td style=&quot;width: 27.8295%; height: 19px;&quot;&gt;선언적 + 명시적&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;예상 가능한 동작을 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 28.6821%; height: 17px;&quot;&gt;구조적 유연성&lt;/td&gt;
&lt;td style=&quot;width: 26.8217%; height: 17px;&quot;&gt;한정적 (오버라이드 기반 조건부 설정)&lt;/td&gt;
&lt;td style=&quot;width: 27.8295%; height: 17px;&quot;&gt;파일별 조건 설정 가능 (*.ts, *.tsx, *.js별 설정 분리 용이)&lt;/td&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 구조의 암시적 병합 방식이 예상치 못한 룰을 적용할 가능성이 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정이 분산되거나, 조건부 룰, 성능 개선 및 예측 가능성을 위해 변경한 것으로 생각된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 rule 을 세분화하여 관리한다는 부분에서 상당한 고민이 보였다.&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;Flat Config는 설정 충돌 자체를 구조적으로 차단한다. 루트 최상단 하나의 설정 파일만 인정하고, 병합 없이 통합된 객체 및 배열 내에서 펼쳐서 설정을 함으로써 통일성과 예측성을 향상시켰다.&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;왜 flat config의 ESM 기반인가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nodejs.org/docs/latest/api/esm.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;  Node.js 가 ESM 을 권장 방향으로 전환 중&lt;/a&gt;이다. 이에 맞춰 eslint 도 ESM 기반으로 설정하도록 파일을 채택한 것으로 보인다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ESM vs CommonJS 가 무엇인가?&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;CommonJS - CJS&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;ESM - ECMAScript Modules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;개념&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Node.js 의 기본 모듈 시스템&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;ES6 에서 도입된 표준 모듈 시스템&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;문법&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;require() / module.exports 사용&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;import / export 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;- 내보내기&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;module.exports = 함수 or 객체&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;export default, export const&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;- 가져오기&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;const 모듈 = require('모듈')&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;import 모듈 from '모듈'&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;동기비동기&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;동기 로딩 - 비동기를 지원하지 않는다&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;기본적으로 비동기 처리를 지원한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;실행 순서&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;즉시 실행&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;호이스팅 후 실행&lt;br /&gt;단, import 가 최상단에 위치해야 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;파일 확장자&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;.js, .cjs&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;.mjs, &quot;type&quot;: &quot;module&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;트리 쉐이킹 Tree Shaking&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;불가능&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;가능 - 정적분석기반&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CRA, VITE 로 프로젝트를 구성했을 시에, package.json 파일을 확인하면, 앞부분에 `&quot;type&quot;: &quot;module&quot;` 이 작성되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Kampus 프로젝트가 ESM 으로 빌드된다는 뜻이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; ESLint 최신(9)버전을 적용했을 때 발생한 문제&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;에디터의 setting.json - `&quot;eslint.useFlatConfig&quot;: true,` 옵션을 작성해야 함 (모든 팀원이 설정해야함)&lt;/li&gt;
&lt;li&gt;eslint-plugin-tailwindcss 등 여러 플러그인과 호환되는 버전
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`eslint-plugin-react`, `eslint-plugin-react-hooks` 와 같이 필수로 사용되는 플러그인의 경우 -&amp;gt; eslint 9버전의 flat config 가 반영이 되었는지는 의문이다.&lt;/li&gt;
&lt;li&gt;기타 사용하는 플러그인에서 eslint 9버전의 패치를 반영해서 업데이트 및 안정화가 되었는지 확인해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.jadru.com/eslint-920-is-not-ready&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여러 문제가 있었는데, 지금은 어느정도 해결된 것으로 보인다.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;린트 초기화 시 여러 객체 생성 -&amp;gt; 독립 config 로 인정되어 재설정 되는 문제
&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;a href=&quot;https://datdaradanadat.tistory.com/244#4.%20rule%20%EC%A0%81%EC%9A%A9%20-%20%ED%95%98%EB%82%98%EC%9D%98%20%EA%B0%9D%EC%B2%B4%20%EC%82%AC%EC%9A%A9%EC%9C%BC%EB%A1%9C%20%EB%8F%85%EB%A6%BD%20config%20%EB%A5%BC%20%ED%86%B5%ED%95%9C%20%EC%A4%91%EB%B3%B5%20%EC%84%A0%EC%96%B8%20%EA%B4%80%EB%A6%AC-1&quot;&gt;아래&lt;/a&gt;에서 해결하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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;린트 도입이 시급&lt;/b&gt;했고, 일단 eslintrc.cjs 로 다운그레이드를 시도했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프로젝트 세팅의 문제였다면 다운그레이드 후에도 린트가 동작하지 않았어야 했는데, 다행히도 CommonJS 로 작성된 린트는 동작했다. &lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;eslintrc.cjs 다운그레이드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 설치되어있는 eslint 버전을 9.x.x 가 아닌, 8.x.x 버전으로 변경한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;npm install --save-dev \ eslint@8.56.0 \ @eslint/js@8.56.0 \ eslint-plugin-react@7.33.2 \ eslint-plugin-react-hooks@4.6.0 \ eslint-plugin-react-refresh@0.4.5 \ eslint-plugin-unused-imports@4.1.4 \ eslint-config-prettier@9.1.0 \ eslint-plugin-prettier@5.1.3 \ globals@15.14.0&lt;/blockquote&gt;
&lt;pre id=&quot;code_1749387802725&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  &quot;devDependencies&quot;: {
    &quot;@eslint/js&quot;: &quot;^8.56.0&quot;,
    &quot;eslint&quot;: &quot;^8.56.0&quot;,
    &quot;eslint-config-prettier&quot;: &quot;^9.1.0&quot;,
    &quot;eslint-plugin-prettier&quot;: &quot;^5.1.3&quot;,
    &quot;eslint-plugin-react&quot;: &quot;^7.33.2&quot;,
    &quot;eslint-plugin-react-hooks&quot;: &quot;^4.6.0&quot;,
    &quot;eslint-plugin-react-refresh&quot;: &quot;^0.4.5&quot;,
    &quot;eslint-plugin-unused-imports&quot;: &quot;^4.1.4&quot;,
    &quot;globals&quot;: &quot;^15.14.0&quot;,
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하지 않는 import 문을 규칙에 추가하기 위해 eslint-plugin-unused-imports 를 포함했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 터미널 명령어를 통해 eslint 초기화를 진행했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;npx eslint&amp;nbsp;--init&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 세팅에 맞는 응답을 선택하면 된다. 린트 버전이 9라면 자동으로 config 파일이, 그 이하라면 rc.js 파일이 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  에러 발생&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;.eslintrc.js is treated as an ES module file as it is a .js file whose nearest parent package.json contains &quot;type&quot;: &quot;module&quot; which declares all .js files in that package scope as ES modules. Instead either rename .eslintrc.js to end in .cjs, change the requiring code to use dynamic import() which is available in all CommonJS modules, or change &quot;type&quot;: &quot;module&quot; to &quot;type&quot;: &quot;commonjs&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽어보면 알겠지만, package.json 이 ESM 으로 처리하겠다는 동작을 사용한다. eslintrc.js 는 CommonJS 형식으로 작성되어 있어, Node.js 가 이를 `require()` 로 불러올 수 없어 다음 에러가 발생한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 파일명을 `.eslintrc.cjs` 로 변경하여 자동으로 Common.JS 로 인식되도록 했다.&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;최종 eslintrc.cjs 코드는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1749394591444&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;module.exports = {
  root: true,
  ignorePatterns: ['dist/', 'node_modules/'],
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true,
    },
  },
  settings: {
    react: {
      version: 'detect',
    },
  },
  plugins: [
    'react',
    'react-hooks',
    'react-refresh',
    'prettier',
    'unused-imports',
  ],
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'plugin:prettier/recommended',
    'prettier',
  ],
  rules: {
    // react
    'react/react-in-jsx-scope': 'off',
    'react/prop-types': 'off',
    // react-refresh
    'react-refresh/only-export-components': [
      'warn',
      { allowConstantExport: true },
    ],

    // prettier
    'prettier/prettier': 'warn',

    // unused imports
    'no-unused-vars': 'off',
    'unused-imports/no-unused-imports': 'warn',
    'unused-imports/no-unused-vars': [
      'warn',
      {
        vars: 'all',
        varsIgnorePattern: '^_',
        args: 'after-used',
        argsIgnorePattern: '^_',
      },
    ],
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트의 문제가 아니라면... Flat Config 방식도 적용할 수 있을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다소 복잡하고 엄격한 여러 규칙을 지정할 때 flat config 를 선택한다고 한다. Kampus 는 기본적인 리액트 규칙 + 미사용 변수를 감지하고, 코드를 정돈하기 위해 사용하는데, 새로운 방식이 맞을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하는 우려가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이러한 경우, 테스트 적용을 한번 시도해보는 것도 좋은 방법이다.&lt;br /&gt;테스트가 성공적으로 동작한다면, 프로젝트 모듈 환경 ESM 에 적합한 Flat Config 방식을 선택하자고 판단했다. &lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*&lt;a href=&quot;https://typescript-eslint.io/packages/typescript-eslint/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;타입스크립트&lt;/a&gt;를 사용하면 typescript-eslint 플러그인에 필요한 dependency로 모두 설치되어있다고 한다!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. eslint , 플러그인을 @latest 로 설치한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 모든 플러그인의 어떤 버전이 안정화되었는지 확인하지 못했다..  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 사용방법에는 flat config 방식이 적용되었다는 문서를 보았다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;npm install --save-dev eslint@latest @eslint/js@latest eslint-plugin-react@latest eslint-plugin-react-hooks@latest eslint-plugin-react-refresh@latest eslint-config-prettier@latest eslint-plugin-unused-imports@latest&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;package.json - eslint 관련된 부분&lt;/p&gt;
&lt;pre id=&quot;code_1749536592892&quot; class=&quot;perl&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;  &quot;devDependencies&quot;: {
    &quot;@eslint/js&quot;: &quot;^9.28.0&quot;,
    &quot;eslint&quot;: &quot;^9.28.0&quot;,
    &quot;eslint-config-prettier&quot;: &quot;^10.1.5&quot;,
    &quot;eslint-plugin-prettier&quot;: &quot;^5.1.3&quot;,
    &quot;eslint-plugin-react&quot;: &quot;^7.37.5&quot;,
    &quot;eslint-plugin-react-hooks&quot;: &quot;^5.2.0&quot;,
    &quot;eslint-plugin-react-refresh&quot;: &quot;^0.4.20&quot;,
    &quot;eslint-plugin-unused-imports&quot;: &quot;^4.1.4&quot;,
    &quot;globals&quot;: &quot;^15.15.0&quot;,
    &quot;prettier&quot;: &quot;^3.4.2&quot;,
  }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 에디터 setting.json 에 flat config 활성화를 설정한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useFlatConfig 설정의 경우 vscode 에디터와 관련된 부분이다. &lt;a href=&quot;https://eslint.org/docs/latest/use/configure/migration-guide#visual-studio-code-support&quot;&gt;해당 문서&lt;/a&gt;를 보시면, vscode-eslint 확장프로그램의 v.3.0.10 이전버전에서는 해당 설정이 true 여야하고, 이후는 따로 설정 없이도 지원한다. useFlatConfig 설정 작성하지 않으려면, 익스텐션 업데이트 확인하고 자유롭게 설정하면 된다~&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flat config 와 관련된 명령어는 `eslint.useFlatConfig` 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://eslint.org/docs/latest/use/configure/migration-guide#visual-studio-code-support&quot;&gt;공식문서&lt;/a&gt;에는 `eslint.experimental.useFlatConfig`로 작성되어있는데, 나의 vscode 에디터는 버전이 달랐다. 에디터에서 위 명령어로 설정하라고 추천했다.&lt;/p&gt;
&lt;pre id=&quot;code_1749536592893&quot; class=&quot;actionscript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;  &quot;eslint.run&quot;: &quot;onSave&quot;,
  &quot;eslint.format.enable&quot;: true,
  &quot;editor.codeActionsOnSave&quot;: {
    &quot;source.fixAll&quot;: &quot;explicit&quot;,
    &quot;source.fixAll.eslint&quot;: &quot;explicit&quot;
  },
  &quot;eslint.useFlatConfig&quot;: true,
  &quot;eslint.validate&quot;: [
    &quot;javascript&quot;,
    &quot;javascriptreact&quot;,
    &quot;typescript&quot;,
    &quot;typescriptreact&quot;
  ],&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원에게 `eslint.useFlatConfig` 또는 `eslint.experimental.useFlatConfig` 를 설정하라고 전달했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. npx eslint --init 으로 eslint.config.js 파일 생성했다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 아래코드처럼 생성된다.&lt;/p&gt;
&lt;pre id=&quot;code_1749536592894&quot; class=&quot;routeros&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;import js from '@eslint/js';
import globals from 'globals';
import pluginReact from 'eslint-plugin-react';
import { defineConfig } from 'eslint/config';

export default defineConfig([
  {
    files: ['**/*.{js,mjs,cjs,jsx}'],
    plugins: { js },
    extends: ['js/recommended'],
  },
  {
    files: ['**/*.{js,mjs,cjs,jsx}'],
    languageOptions: { globals: globals.browser },
  },
  pluginReact.configs.flat.recommended,
]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ESM 모듈이기 때문에 import, export default 형식을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*만일 require(), module.exports 형식으로 작성할 경우, eslint 는 CommonJS 모듈형식으로 작성되었기 때문에 적절히 동작하지 않는다. (파일 형식과 작성법이 일치하지 않아, 파일 및 규칙을 못찾는 듯)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. rule 적용 - 하나의 객체 사용으로 독립 config 를 통한 중복 선언 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rule 을 적용하기 위해서는 &lt;b&gt;하나의 객체에 모든 설정을 담아야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열의 여러 객체로 분리할 경우, 독립 config 로 취급되기 때문에 files 에 해당하는 rule 을 작성해도, 마지막 배열 설정(pluginReact.configs.flat.recommended)으로 인해 꺼둔 설정이(off) 다시 켜질 수 있다! -&amp;gt;   재설정..되는 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 위 설정의 형식을 아래처럼 변경했다.&lt;/p&gt;
&lt;pre id=&quot;code_1749536592895&quot; class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;import js from '@eslint/js';
import globals from 'globals';
import { defineConfig } from 'eslint/config';
import pluginReact from 'eslint-plugin-react';
import pluginReactHooks from 'eslint-plugin-react-hooks';
import pluginReactRefresh from 'eslint-plugin-react-refresh';
import unusedImports from 'eslint-plugin-unused-imports';
import pluginPrettier from 'eslint-plugin-prettier';

export default defineConfig([
  {
    ignores: [
      'node_modules/**',
      'dist/**',
      '.dist/**',
      'build/**',
      'public/**',
      'src/assets/**',
      'ios/**',
      'android/**',
      '.husky/**',
      '.github/**',
    ],
    files: ['**/*.{js,mjs,cjs,jsx}'],
    settings: {
      react: {
        version: 'detect',
      },
    },
    plugins: {
      js,
      'unused-imports': unusedImports,
      react: pluginReact,
      'react-hooks': pluginReactHooks,
      'react-refresh': pluginReactRefresh,
      prettier: pluginPrettier,
    },
    extends: [js.configs.recommended, pluginReact.configs.flat.recommended],
    languageOptions: { globals: globals.browser },
    rules: {
      'react/prop-types': 'off',
      'react/react-in-jsx-scope': 'off',
      'react-hooks/rules-of-hooks': 'error',

      'no-unused-vars': 'off',
      'unused-imports/no-unused-imports': 'warn',
      'unused-imports/no-unused-vars': [
        'warn',
        {
          vars: 'all',
          varsIgnorePattern: '^_',
          args: 'after-used',
          argsIgnorePattern: '^_',
        },
      ],
      'prettier/prettier': 'warn',
    },
  },
]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/eslint-plugin-react&quot;&gt;eslint-plugin-react 플러그인 공식문서&lt;/a&gt;를 확인하면 extends 속성 배열에 flat 설정을 병합하라고 되어있다.&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;또한, 사용하는 플러그인의 공식문서를 일일히 방문하여 flat config 방식을 지원하는지 점검하고, 문서의 사용방법으로 작성하였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/eslint-plugin-react&quot;&gt;eslint-plugin-react&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/eslint-plugin-react-hooks&quot;&gt;eslint-plugin-react-hooks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/eslint-plugin-react-refresh&quot;&gt;eslint-plugin-react-refresh&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/eslint-plugin-unused-imports&quot;&gt;eslint-plugin-unused-imports&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;init 과 별개로, ignores 옵션으로 lint 를 검사하지 않을 예외파일을 작성하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES Lint와 리액트 버전을 기재했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`version: detect` 로 프로젝트에 설치되어있는 리액트 버전을 감지해서 린트에게 알려준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  prettier --write . &amp;amp;&amp;amp; eslint --fix .&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 두 작업을 한번에 처리하도록 npm 명령어 스크립트를 추가했다.&lt;/p&gt;
&lt;pre id=&quot;code_1752125360162&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;vite&quot;,
    &quot;build&quot;: &quot;vite build&quot;,
    &quot;lint&quot;: &quot;eslint .&quot;,
    &quot;prettier:check&quot;: &quot;prettier --check .&quot;,
    &quot;prettier:fix&quot;: &quot;prettier --write .&quot;,
    &quot;format&quot;: &quot;prettier --write . &amp;amp;&amp;amp; eslint --fix .&quot;,
    &quot;preview&quot;: &quot;vite preview&quot;,
    &quot;prepare&quot;: &quot;husky install&quot;
  },&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`npm run format` 을 실행하면, 프로젝트 내부 파일들을 전체적으로 확인하고, prettier 는 수정(업데이트)를 자동으로 실행된다. eslint 는 어떤 파일에서 warn 또는 error 가 어떠한 규칙으로 인해 발생하는지 기재된다. 터미널 출력을 기반으로 파일에 방문해서 직접 수정해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 추후 프로젝트 진행중에 도입하니, 수정할 것이 많았다.. 역시 초기에 규칙을 명확히 지정하는게 좋았을 것 같다는 생각이 든다  &lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  CICD 파이프라인 - prettier, eslint 검사 추가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 코드가 정해진 품질 기준을 통과했음을 보장하기 위해 CI/CD 파이프라인에 코드 검증 단계를 추가했다.&lt;/p&gt;
&lt;figure id=&quot;og_1752135006045&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;[CHORE] git hooks 자동화, build 전에 Prettier와 ESLint 검사 by chae-dahee &amp;middot; Pull Request #194 &amp;middot; IT-Cotato/10th-Kampus-&quot; data-og-description=&quot;개요 Resolves: #192 discussions : #184 PR 유형 어떤 변경 사항이 있나요? 새로운 기능 추가 빌드 부분 혹은 패키지 매니저 수정 작업 내용 커밋 add 된 파일만 검사합니다 (lint-staged 역할) 커밋을 실행했을&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/194/commits/7edec89aecb9fa98199590b126fcde9dd5389ab3&quot; data-og-url=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/194&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/SH19V/hyZjnt500i/RMi95TBgGkNCj17lLfVTd1/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_133_1038_202,https://scrap.kakaocdn.net/dn/lD6l5/hyZf6guVxQ/eVospSXfkqnJ82GPGYksKK/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_133_1038_202&quot;&gt;&lt;a href=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/194/commits/7edec89aecb9fa98199590b126fcde9dd5389ab3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/194/commits/7edec89aecb9fa98199590b126fcde9dd5389ab3&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/SH19V/hyZjnt500i/RMi95TBgGkNCj17lLfVTd1/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_133_1038_202,https://scrap.kakaocdn.net/dn/lD6l5/hyZf6guVxQ/eVospSXfkqnJ82GPGYksKK/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_133_1038_202');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[CHORE] git hooks 자동화, build 전에 Prettier와 ESLint 검사 by chae-dahee &amp;middot; Pull Request #194 &amp;middot; IT-Cotato/10th-Kampus-&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;개요 Resolves: #192 discussions : #184 PR 유형 어떤 변경 사항이 있나요? 새로운 기능 추가 빌드 부분 혹은 패키지 매니저 수정 작업 내용 커밋 add 된 파일만 검사합니다 (lint-staged 역할) 커밋을 실행했을&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.github/main.yml &amp;rarr; Kampus 프로젝트의 CICD 배포 프로세스가 적혀있는 파일이다.&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;기존에 배포(Deploy) 스크립트만 있던 워크플로우 파일(main.yml)에 코드 품질을 검증하는 단계를 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1752136635952&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- name: Run Prettier Check
  run: npm run prettier:check
- name: Run ESLint
  run: npm run lint&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;의존성 설치(Install Dependencies) 직후, 프로젝트 빌드(Build) 직전&lt;/b&gt;에 검사 단계를 넣는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1752137039806&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      # (Checkout, Set up Node, Prepare Environment, Set ENV Variables) ...

      - name: Install Dependencies
        run: npm install

      # ===============================================
      - name: Run Prettier Check
        run: npm run prettier:check

      - name: Run ESLint
        run: npm run lint
      # ===============================================

      - name: Build Production
        run: CI=false npm run build --if-present
      
      # (Configure AWS Credentials, Deploy to S3, Invalidate CloudFront Cache) ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;Run Prettier Check&lt;/b&gt;: `npm run prettier:check` 스크립트를 실행
&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;Run ESLint&lt;/b&gt;: `npm run lint` 스크립트를 실행
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드에 잠재적인 버그나 안티 패턴, 규칙 위반 등이 없는지 검사 &amp;rarr; warn, error 모두 포함한다&lt;/li&gt;
&lt;li&gt;여기서도 단 하나의 오류라도 발견되면 파이프라인은 중단된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 간단한 prettier 와 eslint 를 검사하는 두 단계를 추가하는 것만으로 다음과 같은 이점을 얻을 수 있다.&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;li&gt;&lt;b&gt;신뢰성 있는 코드 베이스&lt;/b&gt;: develop 브랜치에 있는 코드는 언제나 &quot;깨끗한 상태&quot;임을 보장받을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 CI/CD 파이프라인이 우리 프로젝트의 든든한 최종 수문장 역할을 하게 되었다. 이를 통해 일관성 있고, 더 나은 코드를 만드는 데 집중할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;449&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wXj6h/btsPc8OWWDH/vrJCvCV3UAOsv9lxEJIsTk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wXj6h/btsPc8OWWDH/vrJCvCV3UAOsv9lxEJIsTk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wXj6h/btsPc8OWWDH/vrJCvCV3UAOsv9lxEJIsTk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwXj6h%2FbtsPc8OWWDH%2FvrJCvCV3UAOsv9lxEJIsTk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;449&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;449&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  협업 효과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/183&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;작업 과정 PR&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;추가적으로 프로젝트의 코드 품질을 일관되게 유지하고 팀원 간의 협업 효율성을 높이기 위해 코드 품질 검사를 자동화하는 husky, lint-staged, commitlint 세 가지 도구의 도입을 제안했다.&lt;/p&gt;
&lt;figure id=&quot;og_1758120885187&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;  Husky, lint-staged, commitlint를 이용한 Git Hook 품질 검사 관리&quot; data-og-description=&quot;이번에 PR #183,   린트 eslintrc.cjs -&amp;gt; eslint.config.js : Flat Config 업그레이드! 에서 eslint.config.js 를 제대로 도입하게 되면서, 프로젝트의 코드 품질을 일관되게 유지하고 팀원 간의 협업 효율성을 높이&quot; data-og-host=&quot;datdaradanadat.tistory.com&quot; data-og-source-url=&quot;https://datdaradanadat.tistory.com/246&quot; data-og-url=&quot;https://datdaradanadat.tistory.com/246&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bl3v1E/hyZJLVcjVQ/DxmIKPTGWHWAZvtdn9kLeK/img.jpg?width=800&amp;amp;height=450&amp;amp;face=343_150_785_320,https://scrap.kakaocdn.net/dn/ccdS6S/hyZJizn2mI/i7aFXsEOfGT9Z8lm71oTQK/img.jpg?width=800&amp;amp;height=450&amp;amp;face=343_150_785_320,https://scrap.kakaocdn.net/dn/cavIgk/hyZJjdXJ9l/Tg0KpBZcP0H91FOq6Yi4sK/img.png?width=800&amp;amp;height=571&amp;amp;face=0_0_800_571&quot;&gt;&lt;a href=&quot;https://datdaradanadat.tistory.com/246&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://datdaradanadat.tistory.com/246&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bl3v1E/hyZJLVcjVQ/DxmIKPTGWHWAZvtdn9kLeK/img.jpg?width=800&amp;amp;height=450&amp;amp;face=343_150_785_320,https://scrap.kakaocdn.net/dn/ccdS6S/hyZJizn2mI/i7aFXsEOfGT9Z8lm71oTQK/img.jpg?width=800&amp;amp;height=450&amp;amp;face=343_150_785_320,https://scrap.kakaocdn.net/dn/cavIgk/hyZJjdXJ9l/Tg0KpBZcP0H91FOq6Yi4sK/img.png?width=800&amp;amp;height=571&amp;amp;face=0_0_800_571');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;  Husky, lint-staged, commitlint를 이용한 Git Hook 품질 검사 관리&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이번에 PR #183,   린트 eslintrc.cjs -&amp;gt; eslint.config.js : Flat Config 업그레이드! 에서 eslint.config.js 를 제대로 도입하게 되면서, 프로젝트의 코드 품질을 일관되게 유지하고 팀원 간의 협업 효율성을 높이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;datdaradanadat.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 코드리뷰에서 css 스타일링 관련된 코멘트가 60% 이상 감소&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미적용 상태 -&amp;gt; eslintrc.cjs 로 긴급 도입 -&amp;gt; eslint.config.js 로 디벨롭 적용하는 과정을 통해, 코드 리뷰와 유지보수 측면에서 정적 분석 도구의 필요성을 명확히 인식하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각자 다른 prettier, tailwind 정렬 extention 설정으로 다른 코드 스타일을 적용하고 있어 코드 스타일이 일관되지 않았다. eslint + prettier 규칙을 규정하면서 스타일을 자동으로 일치할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특히 `HeadWind` 확장 프로그램이 prettier 와 충돌하면서 코드의 일부가 반복적으로 덮어씌워지는 현상이 발생함을 확인했다.&lt;/b&gt; 익스텐션 사용을 중지하고, prettier 설정으로 통일하는 규칙을 지정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 런타임 JS 오류 건수 15% 감소&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미사용 변수, import, no-undef, console 등으로 발생하는 Deploy Workflow 단계의 falied 를 감소시킬 수 있었다. 린트는 자바스크립트의 잠재적인 오류를 사전에 감지한다. 개발자가 사전에 오류를 인지하고 자동으로 수정할 수 있는 환경을 제공하여, 빌드 실패율을 감소시켰다. 휴먼에러 또한 감소했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 협업 효율성 향상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kampus 프로젝트의 MVP 가 다양함에 따라, 프론트엔드 팀원들은 기능담당자를 교체하며 코드 작성 및 성능을 2차로 검토하는 역할을 하고 있다. 교체된 기능의 코드 스타일에 관한 리뷰는 30% 이상 감소하였다. 사소한 컨벤션 차이로 코드 리뷰 과정이 비효율적으로 진행되던 과정이 - 코드 스타일의 일관성이 확보되면서 신규 기능 파악 및 온보딩 속도가 향상되었고, 공통화 개발도 보다 원활하게 이루어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 효과들은 8월 런칭을 목표로 하는 프로젝트 일정 관리와 품질 확보에 실질적인 기여를 하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;eslint.config.js -&amp;gt; Flat Config -&amp;gt; ESM -&amp;gt; Node.js 개발 트렌드&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Five Ws 라는 개념이 있다. Who, What, When, Where, Why 를 기반으로 정리해보자면&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Who&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;eslint + 관련되어 업데이트한 플러그인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;What&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;eslint.config.js ? flat config ? ESM ? 무엇인가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;When&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;v 8.23.0 부터 flat config 로 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Where&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Kampus 프로젝트에 두가지 방식 모두 적용해보자.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Why&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Kampus 프로젝트에 어떤 방식이 적합한가?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;린트 설정을 다양한 관점에서 조사하고 정리하면서, 기술적 개념을 명확히 이해할 수 있었고 블로그 글 또한 체계적으로 구성할 수 있었다. 다양한 버전과 관점을 직접 프로젝트에 적용해보며 어떠한 방식이 더 적합한지 판단할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ESLint 설정은 단순히 스타일을 맞추기 위한 도구를 넘어, 코드 품질과 협업 효율을 동시에 개선할 수 있는 핵심 요소임을 확인할 수 있었다. 공식 문서 및 다양한 사례를 기반으로 규칙을 선별하고 직접 구성하는 과정을 통해, 팀 상황에 최적화된 설정을 도입하는 것이 왜 중요한지를 체감했다.&lt;/p&gt;
&lt;p data-end=&quot;658&quot; data-start=&quot;526&quot; data-ke-size=&quot;size16&quot;&gt;이번 경험을 통해, 무분별한 도구 도입이 아닌 &lt;b&gt;명확한 기준과 목적에 따른 린트 전략 수립이 개발 생산성과 팀워크에 실질적인 영향을 미친다&lt;/b&gt;는 사실을 검증할 수 있었다. 향후에도 기술 도입 시 이러한 접근 방식을 지속해나가고자 한다.&lt;/p&gt;</description>
      <category>  FrontEnd</category>
      <category>CICD</category>
      <category>eslint</category>
      <category>React</category>
      <category>vite</category>
      <category>프론트엔드</category>
      <category>회고</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/244</guid>
      <comments>https://datdaradanadat.tistory.com/244#entry244comment</comments>
      <pubDate>Sun, 8 Jun 2025 23:59:13 +0900</pubDate>
    </item>
    <item>
      <title> ️ FSD 아키텍처 적용을 통한 기능 중심 설계 리팩토링</title>
      <link>https://datdaradanadat.tistory.com/243</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;아키텍처의 중요성을 깨닫다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 학생 개발자들은 다양하고 깊은 개발 경험을 쌓기에 급급하다. 싱크스팟 프로젝트를 처음 개발할 때, 사용해 봤으니까 라는 이유로 고전적인 계층형 아키텍처를 활용해 설계했다. 기능이 고도화되고, 관리해야할 파일이 많아진 현재 시점에서 리팩토링이 필요하다는 것을 느꼈다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제상황, 변화의 필요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 싱크스팟의 폴더구조는 고전적인 계층형 아키텍처를 기준으로 삼았다.&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;레이어 + 기능 분산: 하나의 기능이 components, pages, queries, mutations, types, constants 등 여러 위치에 흩어져 있다.
&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;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;책임 기능이 반복됨에 따른 중복 구조로 발생하는 문제점이 있다.&lt;br /&gt;기능 중심이 아닌, 역할 중심 구조화로 인해 유지보수성과 코드 응집도가 저하된다.&amp;nbsp;&lt;/blockquote&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 역할 중심 폴더 구조&lt;/p&gt;
&lt;pre id=&quot;code_1748340770751&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;src/components/location/AddressDisplay.tsx  
src/hooks/location/useAutoScroll.ts  
src/apis/location/deletePlaceDelete.ts&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FSD 아키텍처의 기능 중심 폴더 구조&lt;/p&gt;
&lt;pre id=&quot;code_1748340788023&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; entities/location/
 ┣  api
 ┣  model
 ┣  types
 ┣  lib
 ┗  constants&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;코드 모듈화 단위 : 하나의 폴더에 몇백줄로 작성되어있는 코드 -&amp;gt; 기능 단위 캡슐화를 통해 재사용성, 코드응집력 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  기존 아키텍처 분석 - 무엇이 문제였나?&lt;/h3&gt;
&lt;pre id=&quot;code_1748341596566&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;src
 ┣ apis
 ┃ ┣ 도메인
 ┃ ┗ instance.ts
 ┣ assets
 ┣ components
 ┃ ┣ auth
 ┃ ┃ ┣ constants
 ┃ ┃ ┃ ┗ index.ts
 ┃ ┃ ┣ oauth
 ┃ ┣ common
 ┃ ┣ error ┣ boundary
 ┃ ┣ layout
 ┃ ┗ 도메인
 ┣ constants
 ┃ ┣ api.ts
 ┃ ┗ path.ts
 ┣ hooks
 ┃ ┣ 도메인
 ┃ ┗ 공통
 ┣ pages
 ┃ ┗ 도메인
 ┣ routes
 ┣ state
 ┃ ┣ mutations
 ┃ ┃ ┗ 도메인
 ┃ ┣ queries
 ┃ ┃ ┗ 도메인
 ┃ ┗ store
 ┣ styles
 ┣ types
 ┃ ┣ 도메인
 ┃ ┗ 전역
 ┣ utils
 ┣ App.tsx
 ┣ main.tsx
 ┗ vite-env.d.ts&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 폴더구조에서 확인할 수 있듯이, 도메인 즉 기능이 반복된다. 따라서 하나의 기능이 전체적으로 수정된다면 모든 파일을 방문해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`src/page/도메인` `src/type/도메인` `src/api/도메인` `src/state/mutations/도메인`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 비즈니스 로직이 여러 레이어에 분선되어있어 의존성이 혼재되어 있었다. 작은 단위의 테스트를 진행하거나 유사기능을 여러 폴더에 중복되게 생성하였다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;❓FSD 아키텍처 또한 반복되는 세그먼트(ui, api, model) 을 가지지 않는가? 는 의문이 들 수 있다. &lt;br /&gt;FSD 도입은 단순히 폴더 중복을 줄이자는 논점이 아니라, 기능 중심으로 구조를 재조직해 유지보수성과 코드 응집력을 높이기 위한 설계 접근이다. 폴더 구조 중복 해소는 기능적 관점에서 그에 따라오는 부수적인 이점에 가깝다. 중요한 것은 폴더명이 반복되는지 여부가 아니라, 그 반복이 `기능 중심`인지 `역할 중심`인지에 따라 구조적 응집도가 달라진다는 점이다. FSD의 기능(도메인) 단위의 캡슐화와 책임 집중은 기능 단위의 작업으로 개선할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;1. constants, hooks, utils 의 위치&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;src/constants, src/utils 내부의 파일은 공통 전역에서 사용되는 함수를 모아두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능(도메인)마다 필요한 함수는 `src/components/time/utils/timeFormat.ts` `src/components/auth/constants/index.s` 등 컴포넌트 하위에 위치해있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  해당 파일은 컴포넌트에 해당하는 것이 아니다. 유틸에 해당하는 동작을 수행하기 때문에, 현재 컴포넌트 폴더에 위치한 것은 적절하지 않다고 판단했다. time 뿐만 아니라 기능(도메인)에 사용되는 다른 함수도 컴포넌트 폴더에 위치하고 있어 문제라고 생각한다.&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;2. 페이지, 컴포넌트 모듈화를 통한 레거시 분해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 몇몇 페이지 코드에 모든 로직이 들어있었다. &lt;a href=&quot;https://github.com/syncspot-team/Syncspot-FE/blob/design/%23210/src/pages/location/LocationEnterPage.tsx&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[GITHUB]&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 컴포넌트로 분리해서 리팩토링 했다. &lt;a href=&quot;https://github.com/syncspot-team/Syncspot-FE/commit/928adb6c244ff3ac7e409fc79c1ab4b1232fd1ee#diff-3ba2e60c0569f3ab0bc14101031eb3acac6ffb5505ab59c7df1e30dae5343e80&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[GITHUB]&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 단위 컴포넌트로 구분했을때, 여러 컴포넌트가 구성되게 된다. 이때 각 컴포넌트 분리 기준은 관심사, 재사용성, 영역 등이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 기능을 수정하려면 여러 계층에 걸쳐서 수정해야하는 변경전파 범위가 크고, 코드 시나리오 기반으로 추적하거나 재사용하는데 비효율적이다. 아토믹, FSD 등 다른 아키텍처에서 지향하는 책임 단위의 모듈화를 구성하기에는 어려움이 있다. 컴포넌트 경계의 기준이 모호하기도 하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 한 파일에 모든 로직을 모아놓은 개발방식은 유지보수성을 저해하고, 협업에서도 불리하게 적용될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 의존성 결합도 증가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 폴더가 서로 의존할 수 있어 의도치않은 순환 참조가 발생하거나, 코드 수정으로 인한 사이드 이펙트를 예상하기 어렵다. 의존성 방향이라는 개념이 없기 때문에 불필요한 결합도가 점차 증가할 수 밖에 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 정리할 수 있는 적절한 규칙이 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;여러 아키텍처 개선 방법을 고민하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱크스팟 프로젝트는 2차 스프린트가 끝나고, 개인 코드 리팩토링과 Sentry 로 모니터링하여 에러 발생 시 해결하는 &lt;b&gt;운영 과정을 진행 중&lt;/b&gt;이다. 즉 현재 급한 기능 HotFix 는 없다. 지금이 제대로 된 아키텍처를 도입할 적절한 시기라고 생각했다. 초기 도입 후 적응 + 코드 리팩토링 병행하며 구조를 더 탄탄히 다질 수 있는 기회였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Atomic Design 아토믹 디자인패턴 : UI 구성 단의에 대한 명확한 계층화. 비즈니스 로직/기능 단위 구분이 어렵다.&lt;/li&gt;
&lt;li&gt;Domain-Driven Design 도메인 중심 디자인 패턴 : 도메인/유스케이스 중심. 대규모 도메인 복잡성에 유리하고, 오버엔지니어링 위험이 있다.&lt;/li&gt;
&lt;li&gt;헥사고날 아키텍처 Hexagonal : 도메인 로직을 중심으로 내부와 외부 어댑터를 분리한다. 위 문제를 해결하지만, 프론트엔드에 적용 시 UI 와 분리된 로직으로 인해 불필요한 응집도 저하, 과도한 추상화를 할 위험이 있다.&lt;/li&gt;
&lt;li&gt;Micro Forntends : 여러 앱을 팀 단위로 분산 개발 배포한다. 병렬 개발에 최적화 되어있으며, 대규모 조직에 적합하다.&lt;/li&gt;
&lt;li&gt;Feature-Sliced Design FSD : 도메인 중심 수직 분리, 경계가 명확하다. 기능단위 확장이 잦은 팀이 주로 사용하며, 초기 설계 진입장벽 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  FSD 를 싱크스팟에 도입하자!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 스토리북이 적용되어 있기 때문에 아토믹 디자인과의 조합이 기대되었다. 그러나 아토믹 디자인은 UI 가 중심이 되었는데, 싱크스팟에는 전문적인 디자이너가 부재했다.. 원하는 단위로 화면을 다시 만들거나 구성해달라고 요청할 수 없었다. 또한 원자 라는 단위로 관리하기에는 현재 프로젝트 코드가 크게 작성되어 있었다.&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;그 다음으로 고려한 아키텍처가 FSD 이다. 싱크스팟은 MVP 가 명확하다. 중간지점 찾기, 장소투표, 시간투표, 이를 진행하기 위한 온보딩 도메인, 모임 관리와 더불어서 유저 관리하는 기능이다. 이러한 코드 및 기능 작성은 도메인 단위로 명확히 분리되어있다. 기능 추가 및 변경 시 응집도를 더 많이 챙길 수 있는 구조 변경이 필요했다. 초기 도입 비용이 많지만, 이후 점진적으로 도입할 수 있어 현재 운영관리중인 프로젝트 상황에 적절하다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/syncspot-team/Syncspot-FE/discussions/230&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;팀원과 의견을 주고받은 discussions&lt;/a&gt; 에서 일부를 첨부한다. 아래와 같은 위험이 있지만, 위에 작성한 확장성과 문제해결에 필요하다고 느끼게 되었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;폴더구조에 대해서 관심을 가지고 개선하려고 노력하신 점 감사합니다. 사실 개발초기부터 리팩토링을 하는 지금까지 초기 폴더구조가 이어져오던터라 변경하는 것에 걱정이 있었어요. 바뀐 폴더구조에 대해 적응하는것도 하나의 비용이라고 생각했고, 지금 상황에 꼭 필요한가?라는 의문도 있었거든요. 또 리팩토링 과정도 파일에 대한 코드 리팩토링이 대부분이었고, 별다른 기능추가가 없었기 때문에 폴더 구조를 바꾸는 것에 대한 의구심이 있었어요.&lt;br /&gt;하지만 말씀해주신 확장성을 고려한다면 폴더구조를 개선하는 것도 좋아보입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FSD 아키텍처가 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;FSD 의 레이어 : 추상화, 다형성을 달성한다.&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FSD 의 슬라이스, 세그먼트 : 캡슐화를 달성한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부에서 필요하거나, 필요하지 않은 것을 구분하여 공개 API 를 export 한다.&lt;/li&gt;
&lt;li&gt;슬라이스의 내부 세그먼트에 직접적으로 접근하지 않고, 공개 AI 를 통해 접근하여 코드를 명확히 격리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3가지로 단계가 구분된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;389&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnftvm/btsOhIbrysA/8QFczhp2bQtYPROPAqGSZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnftvm/btsOhIbrysA/8QFczhp2bQtYPROPAqGSZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnftvm/btsOhIbrysA/8QFczhp2bQtYPROPAqGSZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcnftvm%2FbtsOhIbrysA%2F8QFczhp2bQtYPROPAqGSZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;389&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;389&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;레이어 Layers : app, pages, widgets, features, entities, shared&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최상위 디렉토리이다. 레이어의 수는 최대 7개 이고, 이 레이어 중 일부만 선택해서 구성할 수도 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;app : 애플리케이션 로직이 초기화 되는곳
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;provider, router, 전역 styles, 전역 layout 등을 이곳에서 정의한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748416002709&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; app
 ┣  layout ┣ Layout.tsx, index.ts
 ┣  providers
 ┃ ┣ APIErrorBoundary.tsx
 ┃ ┣ APIErrorFallback.tsx
 ┃ ┗ index.ts
 ┣  routes
 ┃ ┣  guards
 ┃ ┃ ┣ PrivateRoute.tsx
 ┃ ┃ ┣ PublicRoute.tsx
 ┃ ┃ ┗ index.ts
 ┃ ┗ AppRouter.tsx
 ┣  styles
 ┃ ┗ global.css
 ┗ App.tsx&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;processes : 단계별 등록 프로세스를 처리한다. 더이상 사용되지 않는다.&lt;/li&gt;
&lt;li&gt;pages : 애플리케이션의 페이지가 포함된다. 동일 또는 하위 레이어 (widgets 등)의 모임으로 구성될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748416536291&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; pages
 ┣  about
 ┃ ┣  ui
 ┃ ┃ ┗ AboutPage.tsx
 ┃ ┗ index.ts&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;widgets : 페이지에 사용되는 독립적인 UI 컴포넌트&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748416597363&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; widgets
 ┣  about
 ┃ ┣  model
 ┃ ┃ ┣ index.ts
 ┃ ┃ ┣ mobileTabOption.ts
 ┃ ┃ ┗ teamData.ts
 ┃ ┗  ui
 ┃ ┃ ┣  featureSection
 ┃ ┃ ┃ ┣ MidpointSection.tsx
 ┃ ┃ ┃ ┣ PlaceVoteSection.tsx
 ┃ ┃ ┃ ┗ TimeVoteSection.tsx
 ┃ ┃ ┣ AboutService.tsx
 ┃ ┃ ┣ Footer.tsx
 ┃ ┃ ┣ TabNavigation.tsx
 ┃ ┃ ┗ index.ts&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;features : 비즈니스 가치를 전달하는 사용자 시나리오와 기능을 다룬다.&lt;/li&gt;
&lt;li&gt;entities : 비즈시트 엔티티를 나타낸다. 사용자 리뷰 댓글 등이 포함된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;features, entities 레이어는 선택적 레이어 이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;shared : 특정 비즈니스 로직에 종속되지 않은 재사용 가능한 컴포넌트와 유틸리티가 포함된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UI 키트, axios 설정, 애플리케이션 설정, 비즈니스 로직에 묶이지 않은 헬퍼 등이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  features VS entities 나누는 기준의 주관성 문제를 위한 규칙 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 레이어의 명확한 기준과 비교가 어려웠다. 어떤 비즈니스 로직이 features 에 들어가야 하는지, entites 에 들어가는지 모호했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원은 아래처럼 의견을 내기도 했다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;features&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;entities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;UI, 사용자 상호작용에 집중&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;도미엔 중심의 데이터 처리 로직&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 두 레이어는 각자 다른 역할(슬라이스)를 가지기 때문에 features 레이어에서 api 를 사용하지 않는 방향은 어려웠다. 데이터 entites 정보를 features가 기능 로직으로 조합하거나 사용자의 동적 작업을 담당하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘을 명확히 구분할 수 있도록 아래 조건을 제시했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`entities api` : 기본적인 데이터 조회 `R`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`features api` : 동사로 표현되는 요청 `CUD` (ex. 게시물을 북마크한다, 메시지를 전송한다 등)&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;데이터를 다루는 방식 CRUD 의 동작에 한해서, 사용자와 상호작용이 필요한 CUD 의 작업을 features 레이어에 작성하자고 규칙을 지정했다. 도표화 하면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ Entities Layer 구분 기준&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 아래 조건 중 하나라도 해당되면&amp;nbsp;entities에 배치&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 86px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;height: 10px;&quot;&gt;구분 기준&lt;/td&gt;
&lt;td style=&quot;height: 10px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;핵심 비즈니스 도메인 API&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;프로젝트의 핵심 기능을 구성하는 도메인 단위 API (예: 투표 결과, 사용자 정보 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;공통 재사용 가능성&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;여러 feature에서 공통으로 재사용되는 경우 (중복 방지를 위한 분리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;단순 조회(R) 중심의 API&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;기본적인 데이터 조회만을 담당하며, 복잡한 사용자 동작이나 흐름 제어 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;복잡한 사용자 인터랙션이 아님&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;사용자 행위가 API 흐름을 제어하지 않고, 단순 렌더링이나 필터링 등 수동적 처리일 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ Features Layer 구분 기준&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 아래 조건 중 하나라도 해당되면&amp;nbsp;features에 배치&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;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entities 조합 및 흐름 제어&lt;/td&gt;
&lt;td&gt;여러 entities를 조합해 비즈니스 로직을 수행하거나 흐름을 제어하는 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용자 동작 중심&lt;/td&gt;
&lt;td&gt;사용자 액션(클릭, 입력 등)에 따라 API 요청 흐름이 발생하는 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CUD 중심 동사 기반 API&lt;/td&gt;
&lt;td&gt;데이터를 생성(Create), 수정(Update), 삭제(Delete)하는 행위 중심의 API 처리 (ex. 장소 등록, 투표 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인터랙션 제어 및 상태 변화 포함&lt;/td&gt;
&lt;td&gt;비즈니스 로직에 따라 상태 변화 및 복잡한 UI 상호작용을 포함하는 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;슬라이스 Slice : 비즈니스 엔티티&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 도메인 및 기능을 중심으로 그룹화 하는 것이다. 관련된 코드는 구조적으로 디렉토리 내에 그룹지을 수 있지만, 격리 계층 갭슐화 공개 규칙을 지켜야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;page, widgets, features, entities 는 도메인 기능이 같아도, 다른 이름을 가질 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1748490112466&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; page
 ┗  profile
 features
 ┣  user
 ┣  auth
 ┗  favorites
 entities
 ┗  user&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세그먼트 Segment : 목적(역할) 에 따라 슬라이스 내의 코드를 나눈다.&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;api : 필요한 서버 요청&lt;/li&gt;
&lt;li&gt;UI : 슬라이스의 UI 컴포넌트&lt;/li&gt;
&lt;li&gt;model : 비즈니스 로직. 상태와의 상호작용. actions, selectors 등이 해당한다.&lt;/li&gt;
&lt;li&gt;lib : 슬라이스 내에서 사용되는 보조 기능&lt;/li&gt;
&lt;li&gt;config : 슬라이스에 필요한 구성값. 구성 세그먼트는 거의 필요하지 않다.&lt;/li&gt;
&lt;li&gt;consts : 필요한 상수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 고전적인 계층형 아키텍쳐에서 구분되어있는 역할을 위 세그먼트로 구분하였다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 203px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;기존 고전적인 계층형 아키텍처&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;FSD 아키텍처&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 57px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 57px;&quot;&gt;api/도메인&lt;br /&gt;state/queries/도메인&lt;br /&gt;state/mutations/도메인&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 57px;&quot;&gt;api&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;components/~.tsx&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;ui&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 55px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 55px;&quot;&gt;state/store/~&lt;br /&gt;type/도메인&lt;br /&gt;hooks/ 도메인&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 55px;&quot;&gt;model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;components/도메인/utils&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;lib&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;페이지네이션 limit, 쿼리 키, 차트 옵션 등&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;compoenents/도메인/constants&lt;br /&gt;버튼 타입 enum, 탭 key, 에러 메시지 등&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;constants&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❓ FSD 규칙을 eslint 로 강제할 수 있을까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;eslint 린트는 코드 품질을 위한 검사도구 이다. 이를 폴더를 만들 때에도 제한을 둘 수 있을까? 틀린 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히는 FSD 아키텍츠의 계층 규칙을 코드 내부에서 지켰나 확인하는 방법이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드에서 사용하는 폴더에 대하여 import 문 규칙
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;index 경유&lt;/li&gt;
&lt;li&gt;import 순서&lt;/li&gt;
&lt;li&gt;계층 레이어 간 import 방향 제어&lt;/li&gt;
&lt;li&gt;slice 간 import 금지 (features/&amp;lt;A&amp;gt; 에서 features/&amp;lt;B&amp;gt; 접근 방지)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FSD 의 모든 규칙을 지정하고, 신뢰성과 안정성 있는 대표적인 FSD 플러그인은 찾지 못했다. -&amp;gt; &lt;a href=&quot;https://github.com/import-js/eslint-plugin-import&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;eslint-plugin-import&lt;/a&gt; : 계층 단방향 import 규칙을 지정하는 라이브러리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 직접 코드로 작성하여, 레이어 간의 경우의 수를 차단하는 방법이 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1748499126969&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    // 1. 레이어 간 import 순서 강제
    'import/order': ['error', {
      groups: [['builtin', 'external'], 'internal', ['parent', 'sibling', 'index']],
      pathGroups: [
        { pattern: 'app/**', group: 'internal', position: 'before' },
        { pattern: 'pages/**', group: 'internal', position: 'before' },
        { pattern: 'widgets/**', group: 'internal' },
        { pattern: 'features/**', group: 'internal' },
        { pattern: 'entities/**', group: 'internal' },
        { pattern: 'shared/**', group: 'internal', position: 'after' },
      ],
      alphabetize: { order: 'asc', caseInsensitive: true },
      'newlines-between': 'always',
    }],

    // 2. import 시 절대경로 index.ts 통해서만 import 강제
    'no-restricted-imports': ['error', {
      patterns: [
        // 내부 구조 직접 import 금지
        {
          group: ['features/*/*/*', 'entities/*/*/*', 'shared/*/*/*'],
          message: '모듈 public API(index.ts) 를 통해 import 하세요',
        },
        {
          group: ['state/**', 'store/**'],
          message: '전역 상태는 직접 import 하지 마세요. features 또는 entities 단에서 사용하세요.',
        },
      ],
    }],
  },

    // 3. slice 간 import 금지
    {
      files: ['src/features/**'],
      rules: {
        'no-restricted-imports': ['error', {
          patterns: ['features/*'],
        }],
      },
    },&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱크스팟 프로젝트의 경우, 현재 마이그레이션 및 적용 초기 단계이기 때문에 구조에 대한 논의 이후 FSD 에 lint를 적용하기로 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 규칙의 경우에도, 기본적인 부분만 제한하고 유연하게 가져가는게 좋을 것 같다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  FSD 아키텍처 - 싱크스팟 마이그레이션 ing...&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FSD 는 초기 적용 비용이 높은 편이다. 특히 거의 모든 코드에서 참조하고 있는 shared 파일이 이동하면, 많은 양의 import 문이 수정되었다. 따라서 PR 단위가 커지며, 팀에서는 부정적인 경험이 쌓일 수밖에 없다. 이는 런닝커브를 유발 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 app 레이어와 shared 레이어도 분류해서 작업하는 것을 추천한다.&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;초기 단계에서의 app, share 레이어의 변경사항을 아래 도표로 표현했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;app 레이어 구성요소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프로바이더, 라우터, 전역 스타일, 전역 타입 선언 등&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;기존&lt;/td&gt;
&lt;td&gt;개선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;components/layout/&amp;hellip;&lt;/td&gt;
&lt;td&gt;app/layout/&amp;hellip; &amp;rarr; RoomLayout 의 경우 유저 feature 로 이동할 예정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;components/error/boundary/&amp;hellip;&lt;/td&gt;
&lt;td&gt;app/providers/&amp;hellip;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;rarr; app 레이어는 index 가 필요없지만, app/routes 에서 사용하기 때문에 export 를 모아서 작성하였음&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;components/common/routes/privateRoute&amp;hellip;&lt;/td&gt;
&lt;td&gt;app/routes/AppRouter.tsx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;app/routes/guards/&amp;hellip;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;styles/global.css&lt;/td&gt;
&lt;td&gt;app/styles/global.css&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;src/App.tsx&lt;/td&gt;
&lt;td&gt;app/App.tsx&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*&lt;b&gt;개선점&lt;/b&gt; : 컴포넌트에 해당하지 않는 layout, porvider, guards 등이 app 레이어로 위치 -&amp;gt; 기능에 따른 명확한 위치로 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;shared&amp;nbsp; 레이어구성요소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UI 키트, axios 설정, 애플리케이션 설정, 비즈니스 로직에 묶이지 않은 헬퍼&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역에서 재사용 가능한 컴포넌트와 유틸리티 포함&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;기존&lt;/td&gt;
&lt;td&gt;개선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;src/assets/&amp;hellip;&lt;/td&gt;
&lt;td&gt;shared/assets/&amp;hellip; &amp;rarr; 이미지, 폰트 공유&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;src/constants/..&lt;/td&gt;
&lt;td&gt;shared/constants/&amp;hellip; &amp;rarr; api, path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;s&gt;src/components/error/boundary/&amp;hellip;&lt;/s&gt;&lt;/td&gt;
&lt;td&gt;&lt;s&gt;shared/error/.. &amp;rarr; error fallback 설정&lt;br /&gt;&lt;/s&gt;초기에 shard 로 판단하였으나, 상위 app 레이어의 provider 를 참조(import)하게되어 단방향 계층구조를 위반함 &amp;rarr; app 레이어로 이동함&lt;s&gt;&lt;br /&gt;&lt;/s&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;src/utiles/&amp;hellip;&lt;/td&gt;
&lt;td&gt;shared/utils/.. &amp;rarr; 전역 유틸리티&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;src/hooks/&amp;hellip; , src/types/&amp;hellip;&lt;/td&gt;
&lt;td&gt;shared/hooks/&amp;hellip; , shared/types/&amp;hellip;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;src/components/common/&amp;hellip;&lt;/td&gt;
&lt;td&gt;shared/ui/&amp;hellip; &amp;rarr; 전역 컴포넌트 Button Input toast&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;utils 의 renderShareButton.tsx 의 경우, entites/shareModal 으로 이동이 예상된다.&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;hooks, types 의 경우 초기 shared 에 포함시킨 후&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;page widgets feature 에 따라 구분되는 기능에 포함되는 타입을 이동시키는 &lt;b&gt;점진적 개선방식&lt;/b&gt;으로 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;page, widgets 예시 작업 - About&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;about 구성은 해당 페이지이다.&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1748502772009&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Syncspot | 모두가 편하게 만나는 지름길&quot; data-og-description=&quot;중간지점 찾기부터 시간/장소 투표까지 한 번에 해결하세요!&quot; data-og-host=&quot;syncspot.kr&quot; data-og-source-url=&quot;https://syncspot.kr/about&quot; data-og-url=&quot;https://syncspot.co.kr/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://syncspot.kr/about&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://syncspot.kr/about&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Syncspot | 모두가 편하게 만나는 지름길&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;중간지점 찾기부터 시간/장소 투표까지 한 번에 해결하세요!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;syncspot.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 위주의 작업이고, 작은 단위이기 때문에 초기작업과 함께 간단한 about 예시를 작성하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;page 레이어 구성요소&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;페이지들의 평면 목록(widgets)을 담고 있는 레이어 - widgets 의 조립만을 담당함&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 코드는 widgets 로 구성되어야 하고, api 등의 segements 요소는 widgets 로 변경 및 이동해야 한다.&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;개선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;src/pages/about/AboutPage.tsx&lt;/td&gt;
&lt;td&gt;src/pages/about/ui/AboutPage.tsx&lt;br /&gt;&amp;rarr; 한곳에서 모두 작성된 내용을 widgets 로 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;src/pages/about/index.ts &amp;rarr; 페이지 export&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;widgets 레이어 구성요소&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;header 의 경우 추가 정리가 필요하다.&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;개선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모바일 타입 상수&lt;br /&gt;Footer 팀원 정보 데이터&lt;/td&gt;
&lt;td&gt;widgets/about/model/mobileTabOption teamData &amp;rarr; FSD 원칙에 따르면 widgets 는 model 슬라이스를 가질 수 없음. 그러나 widgets 내부에서만 사용되고, api 요청 (핵심도메인) 활용하지 않기 때문에 내부에 배치함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;src/pages/about/AboutPage.tsx&lt;br /&gt;페이지 파일에 모두 작성&lt;/td&gt;
&lt;td&gt;widgets/about/ui/featureSection/MidpointSection PlaceVoteSection TimeVoteSection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;src/pages/about/AboutPage.tsx&lt;br /&gt;페이지 파일에 모두 작성&lt;/td&gt;
&lt;td&gt;widgets/about/AboutService Footer TabNavigation index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;components/layout/header&lt;br /&gt;components/users/&amp;hellip; (menu 관련 파일)&lt;/td&gt;
&lt;td&gt;widgets/header/ui/&amp;hellip; &amp;rarr; 추후 feature, entites 재배치, components/users/&amp;hellip; menu 관련된 부분 통합&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  도입 후 얻은 성과, 인사이트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;아직 논의중인 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 브랜치에서 적응 후에 develop 브랜치에 머지하자
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;실제 개발환경에서 부딪힌다면 학습 속도가 향상될 것이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a style=&quot;color: #0969da;&quot; href=&quot;https://dev.to/m_midas/feature-sliced-design-the-best-frontend-architecture-4noj&quot;&gt;Feature-Sliced Design: The Best Frontend Architecture&lt;/a&gt; 글에서는 문제를 나중이 아닌 지금 해결해야 한다고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;However, when working with Feature-Sliced Design, problems need to be addressed &quot;now&quot; rather than &quot;later.&quot; Issues in the code and deviations from the concepts become immediately apparent&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;stories 테스트 코드의 위치
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스토리의 경우, 도메인 운영과 별도의 문서 영역이기 때문에 FSD 외부에서 관리해야 한다.&lt;/li&gt;
&lt;li&gt;✅`채택` ui 와 나란히 배치하면 작성이 필요한 컴포넌트를 즉각적으로 확인할 수 있다.(테스트코드와 유사한 장점)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;react-query 서버상태관리 코드의 위치
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;state 레이어 생성 후, 모아서 관리&lt;/li&gt;
&lt;li&gt;도메인(slices) 별 api segments 세그먼트에서 관리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 구조에는 state 폴더 안에 store 폴더로 클라이언트 상태가 포함되어있고, mutations, queries 폴더로 서버상태가 포함되어있다.&lt;/li&gt;
&lt;li&gt;폴더 목적이 명확하지 않다고 생각되어, &lt;b&gt;2. 도메인의 api 세그먼트에 query 문 위치&lt;/b&gt;하는 방식을 제안했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;부수효과&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이외
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;팀 커뮤니케이션 개선 - 카카오톡 X 깃허브, Discussion, 슬랙 등의 업무 도구 활용&lt;/li&gt;
&lt;li&gt;MVP 비즈니스 도메인에 대한 이해도 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  FrontEnd</category>
      <category>프론트엔드</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/243</guid>
      <comments>https://datdaradanadat.tistory.com/243#entry243comment</comments>
      <pubDate>Thu, 29 May 2025 15:35:23 +0900</pubDate>
    </item>
    <item>
      <title>  React 에서 SockJS 를 이용한 WebSocket의 적절한 위치</title>
      <link>https://datdaradanadat.tistory.com/242</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;React JS - Spiring Java 환경&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어떤 라이브러리를 선택해야 할까?&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기본적인 &lt;b&gt;Websocket API&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;html5 이전, 사파리, 파이어폭스 등 지원하지 않는 경우가 있다.&lt;/li&gt;
&lt;li&gt;간단한 메시징이라면 사용해도 괜찮지만, 다양한 기능을 위해서는 다른 라이브러리 사용을 추천한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Socket.io 라이브러리&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Node.js 를 지원한다.&lt;/li&gt;
&lt;li&gt;서버도 &lt;a href=&quot;http://Socket.io&quot;&gt;Socket.io&lt;/a&gt; 를 함께 사용해야 하기 때문에, Java Websocket 과는 호환되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SockJS-client 라이브러리 + STOMP.js&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/web/websocket.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Springframework 매뉴얼&lt;/a&gt;에서 SockJs 를 사용하여 웹소켓을 구현하기를 제시한다.&lt;/li&gt;
&lt;li&gt;서버는 SplingFramework SockJS fallback 을 사용해 웹소켓을 구축하고&lt;/li&gt;
&lt;li&gt;클라이언트는 SockJS 라이브러리를 사용해 구축된 웹소켓 서버에 연결하고, STOMP/stompjs 프로토콜 통신을 통해 실시간 메시지를 송수신한다.&lt;/li&gt;
&lt;li&gt;STOMP.js
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;WebSocket 위에서 동작하며, 메시지 송수신을 보다 간편하게 처리할 수 있게 해준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;구독, 발생, 헤더 인증&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  SockJS 를 통해 소켓 통신을 연결하고, STOMP 를 통해 메시지를 송수신 합니다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;sockjs/socket-client&lt;/h3&gt;
&lt;figure id=&quot;og_1745298630354&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - sockjs/sockjs-client: WebSocket emulation - Javascript client&quot; data-og-description=&quot;WebSocket emulation - Javascript client. Contribute to sockjs/sockjs-client development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/sockjs/sockjs-client&quot; data-og-url=&quot;https://github.com/sockjs/sockjs-client&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b0yGfc/hyYFyX1OG6/mB2ZN7aftA6aF2HFlj5vHk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/UiVns/hyYIhOQsNc/4sR46LX9pRqpxeoMZX5jd1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/sockjs/sockjs-client&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/sockjs/sockjs-client&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b0yGfc/hyYFyX1OG6/mB2ZN7aftA6aF2HFlj5vHk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/UiVns/hyYIhOQsNc/4sR46LX9pRqpxeoMZX5jd1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - sockjs/sockjs-client: WebSocket emulation - Javascript client&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;WebSocket emulation - Javascript client. Contribute to sockjs/sockjs-client development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 라이브러리를 사용해서 코드에 적용하는데에 문제가 있었음.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Uncaught ReferenceError: global is not defined 에러 발생&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 에러는 스크립트에 직접 global 객체를 정의하라는 해결책이 구글링에 많았으나, 근본적인 해결책이 아니라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;global 은 Node.js 환경에서 사용할 수 있는 전역객체이다. 프로젝트는 react js 환경이기때문에 이 에러가 발생한 것으로 확인된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 global 객체를 직접 정의하는 것은 적절한 해결 방법이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;a href=&quot;https://github.com/sockjs/sockjs-client/issues/439&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 깃허브 이뷰 439&lt;/a&gt; 에서 원인과 해결책을 확인할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sockjs/socket-client 의 빌드 버전을 사용해야한다. dist 빌드파일을 임포트하여 해결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`import sockjs from &quot;sockjs-client/dist/sockjs&quot;`&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;sockjs 를 호출할때 vscode 에서 자동 import 구문은 dist 버전이 아니었어서 놓친 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서를 꼼꼼히 확인하거나, 프로젝트 스펙과 유사한 깃허브 검색 및 탐방하는 경험이 문제해결에 큰 도움이 되었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;STOMP.js&lt;/h3&gt;
&lt;figure id=&quot;og_1745305524128&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - stomp-js/stompjs: Javascript and Typescript Stomp client for Web browsers and node.js apps&quot; data-og-description=&quot;Javascript and Typescript Stomp client for Web browsers and node.js apps - stomp-js/stompjs&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/stomp-js/stompjs&quot; data-og-url=&quot;https://github.com/stomp-js/stompjs&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bYJoGe/hyYH8j3DUw/tcK2CCyHfTkqgdVafYh7Lk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/b8WR7m/hyYFyDKpCS/5FdcXnd0f5r684Dr8gvRzk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/stomp-js/stompjs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/stomp-js/stompjs&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bYJoGe/hyYH8j3DUw/tcK2CCyHfTkqgdVafYh7Lk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/b8WR7m/hyYFyDKpCS/5FdcXnd0f5r684Dr8gvRzk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - stomp-js/stompjs: Javascript and Typescript Stomp client for Web browsers and node.js apps&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Javascript and Typescript Stomp client for Web browsers and node.js apps - stomp-js/stompjs&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sockjs 로 커넥션 생성 후, stompjs 의 client 로 STOMP 클라이언트 활성화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 채팅 리스트 구독, 채팅방 구독을 통해 요청이 있을 경우를 감지한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  웹소켓 서버를 open 하는 적절한 위치&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컴포넌트 렌더 : 하나의 페이지에서 웹소켓 서버 관리, 채팅리스트와 채팅창은 컴포넌트로 렌더&lt;/li&gt;
&lt;li&gt;커스텀 hook 사용 : 각각 다른 서버에 연결할때 적절&lt;/li&gt;
&lt;li&gt;`Context API` : 다른 페이지, 하나의 웹소켓에 적절&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 세 위치에서 관리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캠퍼스 프로젝트는 navbar 의 채팅페이지 접속 시 채팅목록 표시, 리스트 클릭 시 chatId 채팅방 표시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 &lt;a href=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/85/commits/8773b956b136e159d62e35d2d3e6d80d97063b89&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;2. 커스텀 hook&lt;/b&gt; 을 이용하여 코드를 작성&lt;/a&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅 리스트 페이지와 채팅 룸 페이지가 각각 다른 웹소켓 서버를 연결하게 되는 문제가 발생&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;3번 방식&lt;/b&gt; `Context API` 를 사용하게 될 경우&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;웹소켓을 통해 새로운 채팅 알림 수신은 웹소켓 서버 통신이다. 새로운 채팅이 생성되면 알림을 전달할 수 있어야 한다고 생각하여 &lt;a href=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/85/commits/a17038da0e2d7ca4b4b1cc3daeaa8e87834a2d14&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;전체 페이지에서 소켓통신을 열도록 개선&lt;/a&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 백엔드에게 확인 결과, 추후 FCM 알림에 새로운 채팅 메시지 알림이 등록될 예정이라고 했다. 따라서 클라이언트 전역에서 웹소켓 통신을 오픈할 필요가 없는 문제가 발생했다.&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;1번 컴포넌트 렌더링 + 2번 커스텀 hook&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;chatPage 에서 선택한 chatRoomId, 웹소켓 서버를 관리하여 &lt;a href=&quot;http://%20https://github.com/IT-Cotato/10th-Kampus-FE/blob/develop/src/pages/chat/chat.jsx&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;하나의 페이지에서 하나의 웹소켓 서버를 사용할 수 있도록 개선&lt;/a&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역 페이지 코드에서 hook 을 통해 웹소켓 서버를 오픈하고, 웹소켓 통신의 hook 내부에서 수행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트 전역 레이아웃 설정에 묶이지 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 commit : &lt;a href=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/118/commits/e5d29c26429bdb7361738fd7d5288d2af24c3351&quot;&gt;https://github.com/IT-Cotato/10th-Kampus-FE/pull/118/commits/e5d29c26429bdb7361738fd7d5288d2af24c3351&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 렌더링 방식에서, 프로젝트 전역에 설정한 레이아웃이 적용되는 문제 발생&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅 목록 의 경우에 전역 레이아웃이 필요하고 채팅방의 경우 전역 레이아웃이 필요하지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외적으로 채팅 페이지 코드에서 ChatLayout 컴포넌트를 생성해여 navbar 렌더링 여부를 설정&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;✨&amp;nbsp;배운 점&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  다양한 방식 고민과 시도를 통한 문제 해결&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.notion.so/3-1ba4a1f2e62e80e4b327ee1dfbd017a0?pvs=21&quot;&gt;문제상황 3. 웹소켓의 적절한 위치&lt;/a&gt; 에서 웹소켓을 구현할 수 있는 3가지 방식을 조사하고, 모든 방식을 구현해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 두가지 방식을 조합하여 문제를 해결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 FCM 알림 시스템 등에 대해 조사하고, 동료 개발자들에게 질문하며 넓은 지식을 쌓을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 방식을 모두 시도해본것이 실패가 아닌, 다음 단계를 위한 발판이었음을 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로도 넓은 시야로 미션을 점검하고, 주도적으로 역량을 펼쳐나갈 수 있게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;413563618-728fc2da-8d9b-4b05-814d-2aae243415c8-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGIopr/btsNvU5d9kk/nMAvADXo3OYzP49Kap8QP0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGIopr/btsNvU5d9kk/nMAvADXo3OYzP49Kap8QP0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGIopr/btsNvU5d9kk/nMAvADXo3OYzP49Kap8QP0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bGIopr/btsNvU5d9kk/nMAvADXo3OYzP49Kap8QP0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;780&quot; data-filename=&quot;413563618-728fc2da-8d9b-4b05-814d-2aae243415c8-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/IT-Cotato/10th-Kampus-FE/pull/85&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;# 작업 PR&lt;/a&gt;&lt;/p&gt;</description>
      <category>  FrontEnd</category>
      <category>kampus</category>
      <category>React</category>
      <category>코테이토</category>
      <category>프론트엔드</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/242</guid>
      <comments>https://datdaradanadat.tistory.com/242#entry242comment</comments>
      <pubDate>Wed, 23 Apr 2025 10:09:18 +0900</pubDate>
    </item>
    <item>
      <title>웹소켓 채팅 구현 전에, 원리 발표하기</title>
      <link>https://datdaradanadat.tistory.com/241</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTP 통신&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 환경에서는 HTTP 요청을 통해 유저, 즉 클라이언트와 서버가 상태와 데이터를 주고받는다.&lt;br /&gt;HTTP는 TCP/IP 프로토콜 스택의 응용 계층에서 동작하는 프로토콜&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;방식&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/12knC/btsNrevwp7q/MbhPBKwjjEAf4UAfBXl5f0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/12knC/btsNrevwp7q/MbhPBKwjjEAf4UAfBXl5f0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/12knC/btsNrevwp7q/MbhPBKwjjEAf4UAfBXl5f0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F12knC%2FbtsNrevwp7q%2FMbhPBKwjjEAf4UAfBXl5f0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;1125&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 HTTP 요청을 전송하면, 서버는 해당 데이터를 반환한다.&lt;br /&gt;&lt;br /&gt;서버는 클라이언트의 요청이 있어야만 데이터를 응답하며, 데이터 반환 후 통신이 종료된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;한계&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에는 모든 서비스를 HTTP로 개발할 수 있었으나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 실시간 네트워킹, 대규모 사용자 처리 등 복잡한 개념이 필요함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex. 실시간 채팅이나 게임 등&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;이러한 상황을 구현하기 위해 주기적으로 서버에 요청을 보내는 방법이 사용되었다. HTTP&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTTP 실시간 통신 구현 방법&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Vb2YD/btsNpDKTbcg/LfcvFkuURY6YgwvK0rCue1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Vb2YD/btsNpDKTbcg/LfcvFkuURY6YgwvK0rCue1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Vb2YD/btsNpDKTbcg/LfcvFkuURY6YgwvK0rCue1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVb2YD%2FbtsNpDKTbcg%2FLfcvFkuURY6YgwvK0rCue1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;1125&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP는 다음 세 가지 방법으로 실시간 통신을 구현했다:&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;Polling&lt;/b&gt;: 클라이언트가 주기적으로 서버에 GET 요청을 보내고, 서버가 데이터를 응답한다. 자원 효율성이 떨어짐.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Long Polling&lt;/b&gt;: 서버에서 새로운 이벤트가 준비될 때까지 대기하고, 준비되면 응답한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Streaming&lt;/b&gt;: 대용량 비디오-오디오 처리에 유리하지만, 클라이언트는 첫 요청 이외에 추가 요청을 할 수 없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모든 방법이 HTTP 통신이기에 헤더가 크다는 문제점이 있음.&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;웹소켓 프로토콜&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 문제점을 해결하기 위해 웹소켓을 사용한다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;HTML5에서 나온 표준 기술로, TCP 기반의 양방향 통신을 지원한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c22SCa/btsNpZUnpRN/X5BFeY88EeLa5ksaJJ31d0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c22SCa/btsNpZUnpRN/X5BFeY88EeLa5ksaJJ31d0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c22SCa/btsNpZUnpRN/X5BFeY88EeLa5ksaJJ31d0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc22SCa%2FbtsNpZUnpRN%2FX5BFeY88EeLa5ksaJJ31d0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;1125&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹소켓을 사용하려면 핸드쉐이크를 통해 클라이언트와 서버 간의 연결을 설정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 접속은 HTTP를 사용하고, 이후 데이터 통신은 웹소켓 프로토콜을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;HTML5 이전 브라우저 지원&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML5 이전의 브라우저에서는 웹소켓을 지원하지 않기 때문에 Socket.io, SockJS 등의 기술을 사용해 실시간 웹을 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핸드쉐이크 과정&lt;/h3&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;클라이언트가 웹소켓 연결을 위한 HTTP GET 요청을 전송&lt;/li&gt;
&lt;li&gt;서버가 요청을 통해 프로토콜을 웹소켓으로 업그레이드&lt;/li&gt;
&lt;li&gt;웹소켓이 열리면서 연결이 유지&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 클라이언트 -&amp;gt; 서버 : 핸드쉐이크 요청 HTTP request&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청의 헤더&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_슬라이드11.png&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;693&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mPcjI/btsNpTmENeM/IdKdvK0zkkTNf4qzmpDBp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mPcjI/btsNpTmENeM/IdKdvK0zkkTNf4qzmpDBp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mPcjI/btsNpTmENeM/IdKdvK0zkkTNf4qzmpDBp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmPcjI%2FbtsNpTmENeM%2FIdKdvK0zkkTNf4qzmpDBp0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1284&quot; height=&quot;693&quot; data-filename=&quot;edited_슬라이드11.png&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;693&quot;/&gt;&lt;/span&gt;&lt;/figure&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;Host&lt;/b&gt;: 요청을 보낼 서버의 호스트&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Upgrade&lt;/b&gt;: 웹소켓 프로토콜로의 전환 요청&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Connection&lt;/b&gt;: 연결을 업그레이드 요청&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WebSocket Key&lt;/b&gt;: 클라이언트가 생성한 인증 키&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Version&lt;/b&gt;: 클라이언트가 지원하는 웹소켓 프로토콜 버전&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 서버의 핸드쉐이크 응답&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 클라이언트의 핸드쉐이크 요청에 대해 다음과 같은 헤더로 응답한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 응답 이후 http 요청 통신에서 웹소켓 통신으로 업그레이드 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_슬라이드13.png&quot; data-origin-width=&quot;2031&quot; data-origin-height=&quot;623&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pID2l/btsNrhe13YN/UHYAOgJiDym5ukOKzPUo2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pID2l/btsNrhe13YN/UHYAOgJiDym5ukOKzPUo2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pID2l/btsNrhe13YN/UHYAOgJiDym5ukOKzPUo2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpID2l%2FbtsNrhe13YN%2FUHYAOgJiDym5ukOKzPUo2k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2031&quot; height=&quot;623&quot; data-filename=&quot;edited_슬라이드13.png&quot; data-origin-width=&quot;2031&quot; data-origin-height=&quot;623&quot;/&gt;&lt;/span&gt;&lt;/figure&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;Switching Protocols&lt;/b&gt;: 프로토콜 전환 승인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Upgrade&lt;/b&gt;: 웹소켓 프로토콜로의 전환 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Connection&lt;/b&gt;: 연결이 업그레이드 되었음을 명시&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WebSocket Accept&lt;/b&gt;: 클라이언트의 인증 키 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 웹소켓 연결&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹소켓 서버로 클라이언트와 서버는 연결&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;슬라이드14.png&quot; data-origin-width=&quot;3387&quot; data-origin-height=&quot;1905&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lw6Kx/btsNoqjK35n/tM2zDsatfJhMnt4DP0yK8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lw6Kx/btsNoqjK35n/tM2zDsatfJhMnt4DP0yK8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lw6Kx/btsNoqjK35n/tM2zDsatfJhMnt4DP0yK8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flw6Kx%2FbtsNoqjK35n%2FtM2zDsatfJhMnt4DP0yK8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3387&quot; height=&quot;1905&quot; data-filename=&quot;슬라이드14.png&quot; data-origin-width=&quot;3387&quot; data-origin-height=&quot;1905&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결된 상태를 open이라고 하며, 필요 시 close를 통해 통신이 종료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;웹소켓 서버에서 데이터 송수신? STOMP&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결된 상태에서 HTTP 요청은 웹소켓 프로토콜로 전환되어 보내진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`HTTP(s)` -&amp;gt; ws(s) 전환&lt;br /&gt;&lt;br /&gt;서버는 업데이트된 데이터를 자유롭게 전송한다. 클라이언트의 따로 요청이 없어도 원할 때 전송할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 웹소켓 서버에 연결되어 통신한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터 전송 형식&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹소켓 통신에 사용되는 데이터는 UTF-8 인코딩을 통해 메시지 단위로 전송 `0x00(데이터 UTF 8 인코딩 값)0xff`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 UTF8 해독 형식을 제공하는 서브 프토로콜 STOMP(Simple Text Oriented Message Protocol) 을 사용해서 어플리케이션 단계에서 메시지를 해독한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 1 : 웹소켓 서버 연결 코드 - Socket.io 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;server.js&lt;/p&gt;
&lt;pre id=&quot;code_1744954290449&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//웹소켓 서버 열기
const io = socketIo(server);

//클라이언트 서버 연결될때 실행
io.on(&quot;connection&quot;, (socket) =&amp;gt; {
  console.log(&quot;클라이언트가 연결되었습니다.&quot;);

  //클라이언트 &amp;gt; 서버 메시지 수신
  socket.on(&quot;chat message&quot;, (msg) =&amp;gt; {
    console.log(`클라이언트에게 받은 메시지: ${msg}`);
    //서버 &amp;gt; 클라이언트 메시지 전송
    io.emit(&quot;chat message&quot;, msg);
  });

  //웹소켓 연결 close
  socket.on(&quot;disconnect&quot;, () =&amp;gt; {
    console.log(&quot;클라이언트가 연결 해제되었습니다.&quot;);
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;index.html&lt;/p&gt;
&lt;pre id=&quot;code_1744954623455&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//웹서버 입장
const socket = io();
//웹서버 close
function disconnect() {
	socket.disconnect();
    alert(&quot;연결이 해제되었습니다.&quot;);
}

//클라이언트 &amp;gt; 서버 메시지 전송 함수
function sendMessage() {
    const input = document.getElementById(&quot;messageInput&quot;);
    const message = input.value;
    //소켓 전송
    socket.emit(&quot;chat message&quot;, message);
    input.value = &quot;&quot;;
    return false;
}

//서버 &amp;gt; 클라이언트 메시지 수신 함수
socket.on(&quot;chat message&quot;, (msg) =&amp;gt; {
    const item = document.createElement(&quot;li&quot;);
    item.textContent = msg;
    document.getElementById(&quot;messages&quot;).appendChild(item);
    console.log(`서버에게 받은 메시지: ${msg}`);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 2 : 코테이토 사이트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코테이토 사이트에서 웹소켓을 통해 실시간으로 문제풀이를 진행할때 소켓통신 사용된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;슬라이드18.png&quot; data-origin-width=&quot;3387&quot; data-origin-height=&quot;1905&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PEJoa/btsNreW3Vxe/Qt5lVWhgsXRHPFsnYKAqk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PEJoa/btsNreW3Vxe/Qt5lVWhgsXRHPFsnYKAqk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PEJoa/btsNreW3Vxe/Qt5lVWhgsXRHPFsnYKAqk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPEJoa%2FbtsNreW3Vxe%2FQt5lVWhgsXRHPFsnYKAqk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3387&quot; height=&quot;1905&quot; data-filename=&quot;슬라이드18.png&quot; data-origin-width=&quot;3387&quot; data-origin-height=&quot;1905&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자가 문제를 제출하면, 서버가 실시간으로 유저에게 신호를 전달하고,&lt;br /&gt;&lt;br /&gt;유저는 제출 버튼이 활성화되는 실행을 실시간으로 받게 됨.&lt;br /&gt;&lt;br /&gt;클라이언트의 request 요청 없이,, 이벤트가 발생한 즉시 응답이 온다!&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;Socket.io 동작 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹페이지가 열리는 브라우저는 인식해서, Socket.io는 웹소켓을 지원하면 웹소켓 방식으로 동작하고, 지원하지 않는 브라우저라면 일반 HTTP를 이용해 실시간 통신을 흉내낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*참고 : 아래의 기술을 하나의 api로 융합해 적합한 기술을 선정한다고 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;WebSocket, FlashSocket, AJAX Long Polling, AJAX Multi part Streaming, IFrame, JSONP Polling&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이외로, 웹소켓은 아니지만 아파치 kafka 도 같이 많이 사용한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대량의 데이터를 실시간으로 처리하는 분산 스트리밍 플랫폼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Socket.io보다 더 복잡하지만, 순서가 보장되는 부분이 있다는 장점이 있다.&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;웹소켓의 한계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹소켓 사용 시 메모리 증가와 서버 부담 등의 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 빠르게 포워딩해야하고, 사용자가 늘어나면 메모리가 늘어나고, 서버 비용이 증가함!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ 서버가 내려가면 전체적으로 문제가 발생한다는 점&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;슬라이드21.png&quot; data-origin-width=&quot;3387&quot; data-origin-height=&quot;1905&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dt1khP/btsNqBZP3rt/ZpCIXEFAa0tgMwWxGKpvT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dt1khP/btsNqBZP3rt/ZpCIXEFAa0tgMwWxGKpvT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dt1khP/btsNqBZP3rt/ZpCIXEFAa0tgMwWxGKpvT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdt1khP%2FbtsNqBZP3rt%2FZpCIXEFAa0tgMwWxGKpvT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3387&quot; height=&quot;1905&quot; data-filename=&quot;슬라이드21.png&quot; data-origin-width=&quot;3387&quot; data-origin-height=&quot;1905&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 서버에 부담이 가는 부분을 해결하기 위해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebRTC(web RealTime Communication)와 같은 기술을 사용해 클라이언트끼리 데이터를 송수신할 수 있도록 하는 기술도 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버보다 빠르지만, 확장성에는 제약이 존재하기도 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹소켓은 실시간 통신에서 유용하지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 트래픽 양에 따른 비용 부담과 코드 복잡성 등의 단점이 있다. 에러 메시지의 디버깅에도 어려움이 많음.&lt;br /&gt;&lt;br /&gt;따라서 프로젝트에 꼭 필요한 기술인지 고민하고 적용하는 것이 중요하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발표사진&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_4362.jpg&quot; data-origin-width=&quot;2532&quot; data-origin-height=&quot;1170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Q55q7/btsNrmt3G5H/GKDSodeDLZLE1o9XK6kv2K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Q55q7/btsNrmt3G5H/GKDSodeDLZLE1o9XK6kv2K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q55q7/btsNrmt3G5H/GKDSodeDLZLE1o9XK6kv2K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQ55q7%2FbtsNrmt3G5H%2FGKDSodeDLZLE1o9XK6kv2K%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2532&quot; height=&quot;1170&quot; data-filename=&quot;IMG_4362.jpg&quot; data-origin-width=&quot;2532&quot; data-origin-height=&quot;1170&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 발표를 통해 Kampus 유학생 커뮤니티 프로젝트에서 웹소켓을 도입하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 통신의 동작원리를 이해함으로써 소켓통신의 세팅과 통신규칙을 정할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2편 프로젝트에 웹소켓 도입기 : 적절한 위치&lt;/p&gt;</description>
      <category>  FrontEnd</category>
      <category>Node.js</category>
      <category>기술면접</category>
      <category>코테이토</category>
      <category>프론트엔드</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/241</guid>
      <comments>https://datdaradanadat.tistory.com/241#entry241comment</comments>
      <pubDate>Fri, 18 Apr 2025 14:52:41 +0900</pubDate>
    </item>
    <item>
      <title>  TypeScript, PWA 환경에서 브라우저 Notification API 사용</title>
      <link>https://datdaradanadat.tistory.com/240</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Notification API 도입 이유?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1003&quot; data-origin-height=&quot;1600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/prIPU/btsNpbzS2bg/qDPpuKTCftOy09ymHAcKoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/prIPU/btsNpbzS2bg/qDPpuKTCftOy09ymHAcKoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/prIPU/btsNpbzS2bg/qDPpuKTCftOy09ymHAcKoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FprIPU%2FbtsNpbzS2bg%2FqDPpuKTCftOy09ymHAcKoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;479&quot; data-origin-width=&quot;1003&quot; data-origin-height=&quot;1600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시 나날모아 프로젝트는 query 를 이용해서 값 변동사항이 있을 경우, 홈에서 알림 컴포넌트에서 모아서 정보를 제공해주고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 배포 과정에서 서비스 워커를 등록하여 PWA 세팅을 해주었기 때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이부분을 리팩토링 과정에서 디벨롭할 수 있는 부분이 무엇이 있을까 고민하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PWA, FCM&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Progressive Web Application 은 웹의 장점과 네이티브 앱의 장점을 모두 사용할 수 있도록 개발된 웹 앱 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 &lt;b&gt;오프라인에서 동작, 설치, 동기화, 푸시알림&lt;/b&gt; 등이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스워커를 등록함에 있어서 일종의 프록시 서버 역할을 합니다. 이부분에 의해 오프라인에서 동작이 가능하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 브라우저에서 처음 접속 시 동의를 받는 알림 권한에서 거절을 할 경우,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 세팅에서 클릭을 통해, 디바이스- 앱 - 알림 권한 On 을 하는 기능은 네이티브 앱에서만 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한번 Notification.requestPermission 로 노출한 팝업은&amp;nbsp;&lt;a href=&quot;https://devjonas.medium.com/trigger-notification-popup-with-javascript-9f5849c2ee9c&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;사용자가 허용/차단 하면 다시 프로그래밍 방식으로 팝업 노출이 불가능하다는 점&lt;/b&gt;&lt;/a&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;Web Push 서비스를 통해서 서버의 푸시 알림을 브라우저에 전달해야 합니다. 해당 프로토콜을 구현하기 위해서 FCM 을 많이 사용하고 있습니다. 다만 이부분은 백엔드의 비용 및 시간 문제로 구현하지 않기로 결정하였습니다. (Kampus 프로젝트에서 도입예정)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  클라이언트 브라우저 웹 푸시까지 구현하기로 결정&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/Notifications_API/Using_the_Notifications_API&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Notification API &lt;/a&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;PWA 상용에 알림 기능을 백프로 적용이 어렵다고 판단하여 간단하지만 필요한 부분을 채울 수 있기 위해 선택하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안타깝게도 백그라운드 알림은 FCM 또는 Push API 가 필수로 필요하며, 따라서 포그라운드 알림을 위해 Notification API 를 단독으로 사용하게 되었습니다.&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;constansts 로 분리하여 코드를 동작하게 했습니다.&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;⚙️ Context API로 Notification 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 페이지에 있던, 알림이 발생하면 Notification api 가 캐치되도록 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`NotificationContext` 로 컨텍스트를 생성해서 알림을 관리하는데 필요한 함수와 상태를 전역적으로 공유합니다. 이를 통해 컴포넌트 간의 데이터 전달이 간편해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용자 권한 요청 Notification.permission()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 사이트 첫 접속 시 알림 권한 동의 메시지가 발생합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1744873689423&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;NotificationContext.Provider value={{ fireNotificationWithTimeout }}&amp;gt;
    {children}
&amp;lt;/NotificationContext.Provider&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;1992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6cB0R/btsNo3oDYX5/wc6WWED7SJUfUCUKV5zgK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6cB0R/btsNo3oDYX5/wc6WWED7SJUfUCUKV5zgK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6cB0R/btsNo3oDYX5/wc6WWED7SJUfUCUKV5zgK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6cB0R%2FbtsNo3oDYX5%2Fwc6WWED7SJUfUCUKV5zgK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;657&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;1992&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;알림 콘텐츠 생성, 클릭 이벤트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 알림 내부 콘텐츠를 세팅하고, 호출해서 사용할때 title, body 로 채웁니다.&lt;/p&gt;
&lt;pre id=&quot;code_1744873757814&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const fireNotificationWithTimeout = (
    title: string,
    timeout: number,
    options: NotificationOptions = {},
  ) =&amp;gt; {
    if (Notification.permission !== 'granted') return

    const newOption = {
      badge: logo,
      icon: logo,
      ...options,
    }

    if (!notificationRef.current) {
      setNotificationTimer(timeout)

      notificationRef.current = new Notification(title, newOption)

      setNotificationClickEvent()
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;constansts 로 작성된 알림 함수 fireNotificationWithTimeout 를 아래처럼 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1744873799467&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// invitations가 변경될 때마다 알림 발송
  useEffect(() =&amp;gt; {
    if (invitations &amp;amp;&amp;amp; invitations.length &amp;gt; 0) {
      invitations.forEach((invitation) =&amp;gt; {
        fireNotificationWithTimeout('새로운 초대', 5000, {
          body: `${invitation.inviterName}님으로부터 초대가 도착했습니다.`,
        })
      })
    }
  }, [invitations, fireNotificationWithTimeout])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발생한 알림을 클릭하면, 푸시 알림이 발생한 화면으로 이동하도록 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1744873830195&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const setNotificationClickEvent = () =&amp;gt; {
    if (notificationRef.current) {
      notificationRef.current.onclick = (event) =&amp;gt; {
        event.preventDefault()
        window.focus()
        notificationRef.current?.close()
      }
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⏰ Throttling 이벤트를 사용한 타이머 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러번의 알림이 중복적으로 발생할 경우 ( 띵 x 30) 사용자에게 불편함을 야기할 것이라고 판단했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;알림이 일정시간 후 자동으로 닫도록 타이머를 설정합니다.&lt;/li&gt;
&lt;li&gt;여러 알림이 중복될 경우 호출을 delay 하여 콜백 함수를 한번만 실행하도록 합니다.&lt;/li&gt;
&lt;li&gt;그후 다시 호출할 수 있도록 timerRef 를 초기화하여 delay 시간이 지나면 다시 호출 할 수 았도록 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 연속적인 호출을 방지하고, 알림 자동 닫힘 기능을 구현함으로 사용자 경험을 개선하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Debounce vs Throttling&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발생하는 여러 이벤트를 한번에 묶을때 사용하는 이벤트 `Debounce`&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;`Throttling`&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Debounce&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연속해서 호출되는 이벤트 함수들 중 마지막 함수만 일정시간 delay 후에 실행되도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 특정 시간이 지난 후에 이벤트를 발생하도록 하기! 아래 이미지는 400ms 동안 이벤트를 발생하지 않도록 설정한 것 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2AkzH/btsNrhTT4ve/4eUfo2tE1uArLcQK7vwEQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2AkzH/btsNrhTT4ve/4eUfo2tE1uArLcQK7vwEQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2AkzH/btsNrhTT4ve/4eUfo2tE1uArLcQK7vwEQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2AkzH%2FbtsNrhTT4ve%2F4eUfo2tE1uArLcQK7vwEQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;760&quot; height=&quot;167&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;282&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 이벤트 이후 지정된 시간만큼 delay 가 발생하면, 즉 이벤트가 발생하지 않으면, 함수를 실행합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검색창 자동완성 : 사용자가 타이핑을 멈춘 후에 검색 요청을 보낸다. -&amp;gt; 싱크스팟 주소 검색에 적용!&lt;/li&gt;
&lt;li&gt;윈도우 리사이즈 이벤트 : 리사이즈가 끝난 후 한번만 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 요청을 전송하면 너무 큰 성능 낭비일 테니, 마지막 이벤트를 확인한 후에 요청을 전송하는 것 입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Throttle&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연속해서 호출되는 이벤트 함수들 중 일정 간격마다 한 번씩 만 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 실행 시간 기준으로, 일정 시간 간격이 지나기 전까지는 무시합니다.&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;/li&gt;
&lt;li&gt;무한 스크롤에서 스크롤 위치 감지&lt;/li&gt;
&lt;li&gt;마우스 움직임 추적, UI 드래그 처리&lt;/li&gt;
&lt;li&gt;실시간 API 요청 제한&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일정한 간격의 이벤트가 반복적으로 발생할 경우 Throttling 지정 시간에 맞춰 실행되지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일정하지 않아 예를들어 지정시간이 초과된 다음에 발생하거나, 지정시간이 되기 전에 발생하면, 이벤트는 실행되지 않을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*시간*이 중요한 포인트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;푸시 알림이 너무 많이 발생하지 않도록 하기위하여 Throttle 이 적합한 기술이라고 판단하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Throttling 코드 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;throttle 함수는 커스텀 hook 으로 추출하여 관심사를 분리하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1744874183569&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useRef } from 'react'

const useThrottle = () =&amp;gt; {
  const timerRef = useRef&amp;lt;NodeJS.Timeout | null&amp;gt;(null)

  const throttle = (callback: () =&amp;gt; void, delay: number) =&amp;gt; {
    if (!timerRef.current) {
      timerRef.current = setTimeout(() =&amp;gt; {
        // delay time이 끝나면 callback을 실행
        callback()
        timerRef.current = null
      }, delay)
    }
  }

  return { throttle }
}

export default useThrottle&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;throttle 함수를 사용해서 타이머 세팅하기&lt;/p&gt;
&lt;pre id=&quot;code_1744874220620&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const setNotificationTimer = (timeout: number) =&amp;gt; {
    throttle(() =&amp;gt; {
      if (notificationRef.current) {
        notificationRef.current.close()
        notificationRef.current = null
      }
    }, timeout)
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;notification api 를 설정한 전체코드는 &lt;a href=&quot;https://github.com/nanalmoa/nanalmoa/pull/67/commits/7a22840b020cb98d768560201e6a158711e2ece7&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Commit 7a22840&lt;/a&gt; 에서 확인할 수 있습니다.&lt;/p&gt;</description>
      <category>  FrontEnd</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/240</guid>
      <comments>https://datdaradanadat.tistory.com/240#entry240comment</comments>
      <pubDate>Thu, 17 Apr 2025 16:19:14 +0900</pubDate>
    </item>
    <item>
      <title>  카카오톡 Share, 이메일 EmailJS 공유 SDK + SEO 개발일지</title>
      <link>https://datdaradanadat.tistory.com/239</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 서비스 개선 방향&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 공유 형식은 브라우저 기본 url을 사용해 사용자가 직접 공유하거나, url copied 버튼을 통해 복사 기능을 제공하였습니다. 이에 따라 사용자 경험이 부족하다고 느끼게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 공유 방법을 직접적으로 제공해주면, 함께 친구들과 공유하여 사용하는 서비스적 측면이 향상될 것이라고 생각하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3KZ7h/btsNlxCtsvL/0vRg37OlE0LC5J6ceBQWOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3KZ7h/btsNlxCtsvL/0vRg37OlE0LC5J6ceBQWOK/img.png&quot; data-origin-width=&quot;1054&quot; data-origin-height=&quot;1070&quot; data-is-animation=&quot;false&quot; style=&quot;width: 48.6573%; margin-right: 10px;&quot; data-widthpercent=&quot;49.23&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3KZ7h/btsNlxCtsvL/0vRg37OlE0LC5J6ceBQWOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3KZ7h%2FbtsNlxCtsvL%2F0vRg37OlE0LC5J6ceBQWOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1054&quot; height=&quot;1070&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byWmxc/btsNkG1JmyY/IkvKBDelUICy3xlKkNnJc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byWmxc/btsNkG1JmyY/IkvKBDelUICy3xlKkNnJc1/img.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1260&quot; data-is-animation=&quot;false&quot; style=&quot;width: 50.18%;&quot; data-widthpercent=&quot;50.77&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byWmxc/btsNkG1JmyY/IkvKBDelUICy3xlKkNnJc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyWmxc%2FbtsNkG1JmyY%2FIkvKBDelUICy3xlKkNnJc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;1260&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오톡 소셜 로그인, 구글 소셜 로그인을 제공하는 싱크스팟 서비스 이기 때문에, 활용하여서 카카오톡, 이메일 공유하기 기능을 고려하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 많이 사용하는 인스타그램 서비스를 활용한 공유하기 기능도 고민하였으나, Meta에서 공식적으로 지원하는 적절한 API 가 없음에 따라 안타깝게도 제외하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  SEO 태그&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존에 우리 링크를 카카오톡에 공유하면 아래처럼 표시됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qrO2I/btsNoOkh6p3/WJHs7IOiVvfYNP582yKIyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qrO2I/btsNoOkh6p3/WJHs7IOiVvfYNP582yKIyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qrO2I/btsNoOkh6p3/WJHs7IOiVvfYNP582yKIyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqrO2I%2FbtsNoOkh6p3%2FWJHs7IOiVvfYNP582yKIyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;722&quot; height=&quot;236&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;236&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아무런 정보가 없는 상태에서 -&amp;gt; 썸네일 이미지, 제목, 내용 등을 표시하도록 설정하겠습니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;OG (Open Graph) 오픈그래프 태그&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://developers.facebook.com/docs/sharing/webmasters/&quot;&gt;오픈 그래프&lt;/a&gt;는 페이스북이 기존의 메타데이터 표기 방법을 참조하여 만들었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;HTML 파일의 메타정보를 쉽게 표시하기 위해서 제목, 설명, 이미지 등의 요소들을 사용할 수 있도록 정의해놓은 프로토콜 입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;페이스북 뿐만 아니라, 트위터, 링크드인, 카카오, 네이버 등에서 사용되고 있기 때문에 등록했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;og:title - 사이트의 제목&lt;/li&gt;
&lt;li&gt;og:type - 사이트의 종류 예) website&lt;/li&gt;
&lt;li&gt;og:image - 사이트를 나타낼 대표 이미지(미리보기 이미지)&lt;/li&gt;
&lt;li&gt;og:url - 사이트의 대표 url&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;  프로젝트 코드&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SEO 정보, OG 태그 메타정보, twitter cards 도 함께 등록해 주었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;추가로, 구글 애널리틱스로 실사용자 및 서비스 운영관리를 하고 있기 때문에 관련 스크립트도 추가했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1752926804505&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt; &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;link rel=&quot;icon&quot; type=&quot;image/svg+xml&quot; href=&quot;/favicon.svg&quot; /&amp;gt;
    &amp;lt;meta
      name=&quot;viewport&quot;
      content=&quot;width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0&quot;
    /&amp;gt;
    &amp;lt;title&amp;gt;Syncspot - 모두가 편하게 만나는 지름길&amp;lt;/title&amp;gt;

    &amp;lt;!-- Primary Meta Tags --&amp;gt;
    &amp;lt;meta name=&quot;title&quot; content=&quot;Syncspot - 모두가 편하게 만나는 지름길&quot; /&amp;gt;
    &amp;lt;meta
      name=&quot;description&quot;
      content=&quot;중간지점 찾기부터 시간/장소 투표까지 한 번에 해결하세요.&quot;
    /&amp;gt;
    &amp;lt;meta
      name=&quot;keywords&quot;
      content=&quot;Syncspot, 중간지점 찾기, 약속잡기, 장소 투표하기, 시간 투표하기, 모임, 만나기, 위치 공유&quot;
    /&amp;gt;
    &amp;lt;meta name=&quot;author&quot; content=&quot;모락 Morak&quot; /&amp;gt;
    &amp;lt;meta name=&quot;robots&quot; content=&quot;index, follow&quot; /&amp;gt;
    &amp;lt;meta name=&quot;language&quot; content=&quot;Korean&quot; /&amp;gt;
    &amp;lt;meta name=&quot;revisit-after&quot; content=&quot;7 days&quot; /&amp;gt;
    &amp;lt;meta name=&quot;generator&quot; content=&quot;Syncspot&quot; /&amp;gt;
    &amp;lt;meta name=&quot;copyright&quot; content=&quot;&amp;copy; 2024 Morak&quot; /&amp;gt;
    &amp;lt;meta name=&quot;coverage&quot; content=&quot;Worldwide&quot; /&amp;gt;
    &amp;lt;meta name=&quot;distribution&quot; content=&quot;Global&quot; /&amp;gt;
    &amp;lt;meta name=&quot;rating&quot; content=&quot;General&quot; /&amp;gt;
    &amp;lt;meta name=&quot;geo.region&quot; content=&quot;KR&quot; /&amp;gt;
    &amp;lt;meta name=&quot;geo.placename&quot; content=&quot;Seoul&quot; /&amp;gt;
    &amp;lt;link rel=&quot;canonical&quot; href=&quot;https://syncspot.co.kr/&quot; /&amp;gt;

    &amp;lt;!-- Open Graph / Facebook --&amp;gt;
    &amp;lt;meta property=&quot;og:type&quot; content=&quot;website&quot; /&amp;gt;
    &amp;lt;meta property=&quot;og:url&quot; content=&quot;https://syncspot.co.kr/&quot; /&amp;gt;
    &amp;lt;meta
      property=&quot;og:title&quot;
      content=&quot;Syncspot - 모두가 편하게 만나는 지름길&quot;
    /&amp;gt;
    &amp;lt;meta
      property=&quot;og:description&quot;
      content=&quot;중간지점 찾기부터 시간/장소 투표까지 한 번에 해결하세요!&quot;
    /&amp;gt;
    &amp;lt;meta
      property=&quot;og:image&quot;
      content=&quot;https://private-user-images.githubusercontent.com/96279437/...&quot;
    /&amp;gt;

    &amp;lt;!-- Twitter --&amp;gt;
    &amp;lt;meta property=&quot;twitter:card&quot; content=&quot;summary_large_image&quot; /&amp;gt;
    &amp;lt;meta property=&quot;twitter:url&quot; content=&quot;https://syncspot.co.kr/&quot; /&amp;gt;
    &amp;lt;meta
      property=&quot;twitter:title&quot;
      content=&quot;Syncspot - 모두가 편하게 만나는 지름길&quot;
    /&amp;gt;
    &amp;lt;meta
      property=&quot;twitter:description&quot;
      content=&quot;중간지점 찾기부터 시간/장소 투표까지 한 번에 해결하세요!&quot;
    /&amp;gt;
    &amp;lt;meta
      property=&quot;twitter:image&quot;
      content=&quot;https://private-user-images.githubusercontent.com/96279437/...&quot;
    /&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;!-- Google tag --&amp;gt;
  &amp;lt;script
    async
    src=&quot;https://www.googletagmanager.com/gtag/js?id=%VITE_GOOGLE_ANALYTICS_ID%&quot;
  &amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;script&amp;gt;
    window.dataLayer = window.dataLayer || [];
    function gtag() {
      dataLayer.push(arguments);
    }
    gtag('js', new Date());

    gtag('config', '%VITE_GOOGLE_ANALYTICS_ID%');
  &amp;lt;/script&amp;gt;
  &amp;lt;!------------------------&amp;gt;&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;meta 정보를 업데이트 했는데도 이미지 등 정보가 변경이 안되었다면, 카카오 서버에 페이지의 정보가 아직 캐싱되지 않은 것입니다. (빠르면 1시간 이내 최대 24시간 이내에 적용 시간이 걸린다고 합니다.)&lt;/li&gt;
&lt;li&gt;캐싱 삭제를 진행하고 싶으면,&amp;nbsp;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://developers.kakao.com/tool/clear/og&quot;&gt;카카오 개발자 사이트&lt;/a&gt;에서 가능합니다. (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://developers.facebook.com/tools/debug/&quot;&gt;페이스북 캐싱 삭제&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &amp;nbsp;카카오톡 공유하기 Share&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오톡 소셜 로그인, 카카오맵 사용을 위해 생성해둔 kakao developers 의 syncspot 애플리케이션에 Share 기능을 추가하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9dusw/btsNnUyvMAJ/OG0H02rPdKifu36Equ4tp1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9dusw/btsNnUyvMAJ/OG0H02rPdKifu36Equ4tp1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9dusw/btsNnUyvMAJ/OG0H02rPdKifu36Equ4tp1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/9dusw/btsNnUyvMAJ/OG0H02rPdKifu36Equ4tp1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;404&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*아래 글은 developers 애플리케이션 등록 이후 과정을 기록하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚙️ 카카오톡의 공유하기 api 사용하기 위한 설치 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/javascript/getting-started&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 링크에서 SDK 를 다운로드&lt;/a&gt; 해주어야 합니다. /download 페이지로 들어가서 Full SDK &amp;lt;/&amp;gt; 를 클릭해서 버전에 따른 코드 링크를 복사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/javascript/getting-started#init&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;프로젝트 폴더 최상단의 index.html 파일 body 태그 안에 붙여넣습니다.&lt;/a&gt; integrity 와 corssorigin 은 애플리케이션, 버전마다 다르게 지정됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, Kakao 스크립트가 재랜더링 시에 키를 init 해주어야 합니다. 초기화 코드의 키는 환경변수로 지정한 카카오 애플리케이션 키를 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1744680474895&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script src=&quot;https://t1.kakaocdn.net/kakao_js_sdk/2.7.4/kakao.min.js&quot;
integrity=&quot;sha384-DKYJ~~~~&quot;
crossorigin=&quot;anonymous&quot;
&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    Kakao.init('%VITE_KAKAO_JAVASCRIPT_KEY%');
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공유 이벤트 리스너&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaotalk-share/js-link&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 홈페이지의 문서 &amp;gt; 메시지 &amp;gt; 카카오톡 공유: JavaScript&lt;/a&gt; 에서 사용방법을 확인합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;864&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0JhGF/btsNljdFtHt/6IbEcevlJhyVca18CjiX9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0JhGF/btsNljdFtHt/6IbEcevlJhyVca18CjiX9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0JhGF/btsNljdFtHt/6IbEcevlJhyVca18CjiX9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0JhGF%2FbtsNljdFtHt%2F6IbEcevlJhyVca18CjiX9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1712&quot; height=&quot;864&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;864&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaotalk-share/common#type&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;위 문서&lt;/a&gt;를 보면 공유 API, 메시지 API 두 종류가 존재하는 것으로 판단됩니다. 싱크스팟 프로젝트는 메신저 내용을 전달하는 것이 아닌, 싱크스팟의 기능을 공유하여 함께 서비스를 사용하는 것이 목표이기 때문에 공유 API 를 사용하였습니다.&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;앞서 세팅에서 설치한 SDK 의 Kakao.Share 모듈을 사용하면 공유 기능을 구현할 수 있다고 설명합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;syncspot 프로젝트는 타입스크립트를 사용하고 있기 때문에 Kakao 형식을 declare global 로 지정을 해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 카카오맵의 kakao 모듈 을 사용하고 있습니다. 대소문자를 구분하여 kakao 모듈과 Kakao 모듈은 다르기 때문에 두가지 모두 선언해주어야 사용할 수 있습니다. - 타입스크립트&lt;/p&gt;
&lt;pre id=&quot;code_1744680722602&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;declare global {
  interface Window {
    kakao: any;
    Kakao: any;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오톡에서 제공하는 템플릿 중 피드 feed 를 선택하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNab5h/btsNiYVaP2v/C11N2xULPjQeSCNVIwtV61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNab5h/btsNiYVaP2v/C11N2xULPjQeSCNVIwtV61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNab5h/btsNiYVaP2v/C11N2xULPjQeSCNVIwtV61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNab5h%2FbtsNiYVaP2v%2FC11N2xULPjQeSCNVIwtV61%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;434&quot; height=&quot;728&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 리스트, 현재 위치 공유, 커머스, 텍스트, 캘린더 가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 만든 버튼을 사용하기 위해 useKakaoShare 커스텀 훅에서 Kakao.Share.sendCustom() 함수를 사용하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1744805262135&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { ShareType } from '@src/types/shareType';

interface IShareKakao {
  descriptionType: ShareType;
  url: string;
}

export function useShareKakao({ descriptionType, url }: IShareKakao) {
  if (window.Kakao) {
    window.Kakao.Share.sendDefault({
      objectType: 'feed',
      content: {
        title: 'SyncSpot 중간지점찾기',
        description: descriptionType,
        imageUrl:
          'https://private-user-images.githubusercontent.com/96279437/...',
        link: {
          webUrl: url,
        },
      },
      buttons: [
        {
          title: '자세히 보러 가기',
          link: {
            webUrl: url,
          },
        },
      ],
    });
  } else {
    console.error('Kakao SDK is not loaded.');
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 ShareType 를 상수로 지정하여서 변동에 유연하게 적용하도록 하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1744805339629&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const SHARE_TYPE = {
  LOCATION_ENTER: '내 장소를 입력하고 중간 지점을 찾아보세요!',
  LOCATION_RESULT:
    '입력한 장소를 기준으로 중간 지점을 정했어요. 여기서 만날까요?',
  LOCATION_RECOMMENDATIONS: '중간 지점 근처에서 가기 좋은 장소를 추천해드려요!',
  PLACE_CREATE:
    '중간 지점 후보 중에서 고를 수 있도록 장소 투표 방을 만들어보세요.',
  PLACE_VOTE:
    '모임 장소를 결정해야 한다면? 중간 지점 후보 중에서 원하는 곳에 투표해 주세요.',
  PLACE_RESULT: '투표 완료! 이번 모임은 여기에서 만나요.',
  TIME_CREATE: '언제 만날지 정할 수 있도록 시간 투표 방을 만들어 보세요.',
  TIME_VOTE: '만날 수 있는 날짜와 시간을 투표해 주세요!',
  TIME_RESULT: '투표 결과! 이번 만남은 이때 진행돼요.',
  ABOUT: '싱크스팟 서비스와 팀 모락에 대해 알아보아요!',
} as const;

export type ShareType = (typeof SHARE_TYPE)[keyof typeof SHARE_TYPE];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 타입에 해당할 경우에 동작하도록 카카오 버튼 컴포넌트를 구성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1744805379524&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import IconOauthKakao from '@src/assets/icons/IconOauthKakao.svg?react';
import { useShareKakao } from '@src/hooks/share/useKakaoShare';

import { SHARE_TYPE, ShareType } from '@src/types/shareType';
import { PATH } from '@src/constants/path';

export interface IShare {
  url: string;
}

export default function KakaoShare({ url }: IShare) {
  const selectedRoomId = localStorage.getItem('selectedRoomId');

  if (selectedRoomId === null) {
    return false;
  }

  const pathToShareTypeMap: Record&amp;lt;string, ShareType&amp;gt; = {
    [PATH.LOCATION_ENTER(selectedRoomId)]: SHARE_TYPE.LOCATION_ENTER,
    [PATH.LOCATION_RESULT(selectedRoomId)]: SHARE_TYPE.LOCATION_RESULT,
    [PATH.LOCATION_RECOMMENDATIONS(selectedRoomId)]:
      SHARE_TYPE.LOCATION_RECOMMENDATIONS,
    [PATH.PLACE_CREATE(selectedRoomId)]: SHARE_TYPE.PLACE_CREATE,
    [PATH.PLACE_VOTE(selectedRoomId)]: SHARE_TYPE.PLACE_VOTE,
    [PATH.PLACE_RESULT(selectedRoomId)]: SHARE_TYPE.PLACE_RESULT,
    [PATH.TIME_CREATE(selectedRoomId)]: SHARE_TYPE.TIME_CREATE,
    [PATH.TIME_VOTE(selectedRoomId)]: SHARE_TYPE.TIME_VOTE,
    [PATH.TIME_RESULT(selectedRoomId)]: SHARE_TYPE.TIME_RESULT,
    [PATH.ABOUT]: SHARE_TYPE.ABOUT,
  };

  const handleKakaoShare = () =&amp;gt; {
    const descriptionType = pathToShareTypeMap[window.location.pathname];
    if (descriptionType) {
      useShareKakao({ descriptionType, url });
    } else {
      console.error('매칭되는 SHARE_TYPE이 없습니다.');
    }
  };
  return (
    &amp;lt;button
      className=&quot;flex items-center w-full gap-2 p-2 rounded-lg lg:p-4 hover:bg-gray-200&quot;
      onClick={handleKakaoShare}
    &amp;gt;
      &amp;lt;IconOauthKakao className=&quot;size-6&quot; /&amp;gt;
      &amp;lt;span&amp;gt;카카오톡&amp;lt;/span&amp;gt;
    &amp;lt;/button&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &amp;nbsp;이메일 공유하기 EmailJS&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 많이 사용되는 Gmail API 를 고려해보았으나, 링크공유 목적과 적합하지 않은 다양한 부가기능이 존재했기 때문에 사용을 피하게 되었습니다.&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;서버를 거치지 않을 필요가 있는지&lt;/li&gt;
&lt;li&gt;템플릿 + 링크 변수를 지정할 수 있는지&lt;/li&gt;
&lt;li&gt;무료 플랜&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 고려하여 API 를 탐색하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1744805417939&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Send email directly from your code | EmailJS&quot; data-og-description=&quot;No server side code required. Add static or dynamic attachments, dynamic parameters, captcha code and more. Start with our free tier!&quot; data-og-host=&quot;www.emailjs.com&quot; data-og-source-url=&quot;https://www.emailjs.com/&quot; data-og-url=&quot;https://www.emailjs.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cbaiby/hyYIce6xSm/H6oC7aECfiamdRFFPq41y1/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200&quot;&gt;&lt;a href=&quot;https://www.emailjs.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.emailjs.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cbaiby/hyYIce6xSm/H6oC7aECfiamdRFFPq41y1/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Send email directly from your code | EmailJS&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;No server side code required. Add static or dynamic attachments, dynamic parameters, captcha code and more. Start with our free tier!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.emailjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서버 설정 없이 클라이언트 측에서 직접 API 를 통해 이메일을 보낼 수 있음&lt;/li&gt;
&lt;li&gt;템플릿, 변수 지정 가능. 원하는 메시지 전송도 가능&lt;/li&gt;
&lt;li&gt;월 200개까지 무료&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다 간단한 과정으로 이메일을 전송할 수 있는 라이브러릴 EmailJS 를 선택하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;403&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OWPB1/btsNmcFBg5e/WYzLw0bAQ7eknVSQGIr341/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OWPB1/btsNmcFBg5e/WYzLw0bAQ7eknVSQGIr341/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OWPB1/btsNmcFBg5e/WYzLw0bAQ7eknVSQGIr341/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/OWPB1/btsNmcFBg5e/WYzLw0bAQ7eknVSQGIr341/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;403&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;403&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로그인 사용자는 발신자 정보가 자동으로 입력되어 수신자 이메일만으로 공유가 가능하도록 단계를 축소했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;비로그인 사용자가 공유할 경우엔 서비스 소개 페이지로 연결하여, 접근 권한 문제를 해결함과 동시에 자연스러운 서비스 유입을 유도했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚙️ EmailJS 서비스 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Services 생성 &amp;gt; Gmail &amp;gt; ConnectAccount 이메일 전송할 지메일과 연동합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service ID 의 경우 env 에서 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eamil Templates 에서 메일 템플릿을 &lt;a href=&quot;http://[Dynamic%20variables in templates | EmailJS](https://www.emailjs.com/docs/user-guide/dynamic-variables-templates/)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;커스텀&lt;/a&gt; 할 수 있습니다. {{변수명}} 을 지정해서 템플릿을 지정할 수 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 form 태그의 자식요소 name과 같아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;message 에 공유할 싱크스팟 링크를 전달하고, 서비스회원의 이름을 지정하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 이미지 첨부는 Code Editor html 수정에서 가능합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공유 이벤트 리스너 SDK&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리 설치&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$ npm install --save @emailjs/browser $ yarn add @emailjs/browser&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 카카오와 마찬가지로 useEamilShare.ts 훅을 만들어서 호출해서 사용했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1744807658654&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useState } from 'react';
import emailjs from '@emailjs/browser';
import { useGetUserInfoQuery } from '@src/state/queries/users/useGetUserInfoQuery';

interface ISendEmail {
  toEmail: string;
  message: string;
}

export function useEmailShare() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState&amp;lt;string | null&amp;gt;(null);
  const [success, setSuccess] = useState(false);

  const SERVICE_ID = import.meta.env.VITE_EMAILJS_SERVICE_ID;
  const TEMPLATE_ID = import.meta.env.VITE_EMAILJS_TEMPLATE_ID;
  const PUBLIC_KEY = import.meta.env.VITE_EMAILJS_PUBLIC_KEY;

  //공유하는 유저 이름 호출
  const { data: userInfo } = useGetUserInfoQuery();

  const sendEmail = ({ toEmail, message }: ISendEmail) =&amp;gt; {
    setLoading(true);
    const templateParams = {
      to_email: toEmail,
      from_name: userInfo.data.name,
      message: message,
    };

    emailjs
      .send(SERVICE_ID, TEMPLATE_ID, templateParams, PUBLIC_KEY)
      .then(() =&amp;gt; {
        setSuccess(true);
        setError(null);
        setTimeout(() =&amp;gt; setSuccess(false), 5000);
      })
      .catch(() =&amp;gt; {
        setError('이메일 전송에 실패했습니다. 다시 시도해주세요.');
        setSuccess(false);
        setTimeout(() =&amp;gt; setError(null), 5000);
      })
      .finally(() =&amp;gt; {
        setLoading(false);
      });
  };

  return { sendEmail, loading, error, success };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메일 전송 시간이 꽤 소모됨에 따라 loading 상태를 지정하여 &amp;hellip; 으로 표시하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;templateParams 에서 템플릿 변수명과 데이터를 맞추었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;env 값의 경우 아래에서 확인!&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SERVICE_ID &amp;gt; Email Services 메뉴&lt;/li&gt;
&lt;li&gt;TEMPLATE_ID &amp;gt; Email Templates 메뉴&lt;/li&gt;
&lt;li&gt;PUBLIC_KEY &amp;gt; account &amp;gt; public key&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  FrontEnd</category>
      <category>JavaScript</category>
      <category>React</category>
      <category>싱크스팟</category>
      <category>코테이토</category>
      <category>회고</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/239</guid>
      <comments>https://datdaradanadat.tistory.com/239#entry239comment</comments>
      <pubDate>Wed, 16 Apr 2025 21:51:57 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] REST, REST API, RESTful 특징</title>
      <link>https://datdaradanadat.tistory.com/226</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;REST 정의&lt;/h2&gt;
&lt;ul data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;RE&lt;/b&gt;presentational&lt;b&gt;S&lt;/b&gt;tate&lt;b&gt;T&lt;/b&gt;ransfer&lt;/li&gt;
&lt;li&gt;월드 와이드 웹 과 같은 분산 하이퍼미디어 시스템을 위한 소프트웨어 아키텍처의 한 형식
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 기존 기술, HTTP 프로토콜을 그대로 활용할 수 있어 웹의 장점을 최대활용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;클라이언트와 서버 사이의 통신 방식 중 하나이다.&lt;/li&gt;
&lt;li&gt;자원을 정의하고, 자원에 대한 상태(주소를 지정)를 통신 네트워크 아키텍처의 모음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;즉, 자원(Resource)의 표현(Representation)에 의한 상태 전달&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자원의 표현? 해당 소프트웨어가 관리하는 모든 것(자원)을 표현하기 위한 이름(표현)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(ex. DB 의 학생정보가 자원 -&amp;gt; `students` 표현)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태전달? 데이터가 요청되는 시점에서 자원의 상태(정보)를 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;HTTP URI(Uniform Resource Identifier)를 통해 자원(Resource)을 명시하고, HTTP Method(POST, GET, PUT, DELETE)를 통해 해당 자원에 대한 CRUD Operation을 적용하도록 설계된 아키텍쳐&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* CRUD Operation : Create 생성 `POST`, Read 조회 `GET`, Update 수정 `PUT`, Delete 삭제 `DELETE`, HEAD header정보조회 `HEAD`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 REST 가 필요한가?&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;li&gt;다양한 브라우저와 안드로이폰, 아이폰과 같은 모바일 디바이스에서도 통신을 할 수 있어야 한다&lt;/li&gt;
&lt;li&gt;타 플랫폼에 대한 지원을 위해 서비스 자원에 대한 아키텍처를 세우고 이용하는 방법 &amp;gt; REST&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;REST 구성요소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자원 Resource : URI&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 자원은 고유한 ID 가 존재하고, 이 자원은 서버에 저장된다.&lt;/li&gt;
&lt;li&gt;자원을 구별하는 ID 는 HTTP URI 이다. (ex. `/student/:std_id` )&lt;/li&gt;
&lt;li&gt;Client는 URI를 이용해서 자원을 지정하고, 자원의 상태(정보)에 대한 조작을 서버에 요청한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행위 Verb : HTTP Method&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 프로토콜의 메소드 method 사용&lt;/li&gt;
&lt;li&gt;`POST`, `GET`, `DELETE`, `PUT`&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표현 Representation of Resource&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt; Client가 자원의 상태에 대해 조작을 요청하면 서버는 이에 해당하는 응답(Representation)을 전송한다.&lt;/li&gt;
&lt;li&gt;응답은 자원의 상태(정보)를 전달한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;`JSON`, `XML`&lt;/b&gt;, `TEXT`, `RSS` 등으로 표현될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;REST 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서버-클라이언트 구조 (&lt;b&gt;Server-Client)&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;자원을 요청하는 쪽이 Client, 자원이 있는 쪽이 Server가 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Client: 사용자 인증이나 context(세션, 로그인 정보) 등을 직접 관리하고 책임진다.&lt;/li&gt;
&lt;li&gt;REST Server: API를 제공하고 비즈니스 로직 처리 및 저장을 책임진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;아키텍처를 단순화시키고 작은 단위로 분리함으로써
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Client-Server가 독립적으로 개선될 수 있도록 해준다.&lt;/li&gt;
&lt;li&gt;서로 간 의존성이 줄어든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;무상태 (&lt;b&gt;Stateless)&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;HTTP 프로토콜과 동일하게 무상태성을 갖는다.&lt;/li&gt;
&lt;li&gt;요청에서 Client의 context를 Server에 저장하지 않는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션과 쿠키와 같은 정보를 신경쓰지 않아 구현이 단순해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Server는 각각의 요청을 완전히 별개의 것으로 인식하고 처리한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API 서버는 Client의 요청만을 단순 처리&lt;/li&gt;
&lt;li&gt;이전 요청이 다음 요청의 처리에 연관되어서는 안된다.&lt;/li&gt;
&lt;li&gt;Server의 처리 방식에 일관성을 부여하고 부담이 줄어들며, 서비스의 자유도가 높아진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;캐시 처리 가능 (&lt;b&gt;Cacheable)&lt;/b&gt;&lt;br /&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;웹 표준 HTTP 프로토콜에서 사용하는 기존의 인프라 활용&lt;/li&gt;
&lt;li&gt;캐싱 기능 : Last-Modified 태그 E-Tag를 이용해 구현 - 대량의 요청에서 사용&lt;/li&gt;
&lt;li&gt;빠른 응답시간&lt;/li&gt;
&lt;li&gt;전체 응답시간, 성능, 서버의 자원 이용률 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;계층화&lt;/b&gt;&amp;nbsp;(Layered System)&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;Client는 REST API Server만 호출한다.&lt;/li&gt;
&lt;li&gt;REST Server는 다중 계층으로 구성될 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API Server 앞단에 보안, 로드밸런싱, 암호화, 사용자 인증 등을 추가해 구조상 유연성&lt;/li&gt;
&lt;li&gt;로드밸런싱, 공유 캐시 등을 통해 확장성과 보안성을 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;PROXY, 게이트웨이 같은 네트워크 기반의 중간 매체를 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Code-On-Demand(optional)&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;Server로부터 스크립트를 받아서 Client에서 실행한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java applet이나 JavaScript를 제공해 Server가 Client가 실행시킬 수 있는 로직을 전송&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;(Uniform Interface)&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;URI로 지정한 Resource에 대한 조작은 통일되고 한정적인 인터페이스로 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요 원칙!!&lt;/b&gt;&lt;/p&gt;
&lt;ul  data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;URI는 정보의 자원을 표현해야 한다.&lt;/li&gt;
&lt;li&gt;자원의 대한 행위는 HTTP method로 표현한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;REST 장단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점&lt;/p&gt;
&lt;ul  data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 프로토콜의 인프라를 그대로 사용하므로 REST API 사용을 위한 별도의 인프라를 구축할 필요가 없다.&lt;/li&gt;
&lt;li&gt;HTTP 표준 프로토콜을 따르는 모든 플랫폼에서 사용 가능하다.&lt;/li&gt;
&lt;li&gt;API가 의도하는 메시지를 쉽고 명확하게 파악할 수 있다.&lt;/li&gt;
&lt;li&gt;Server와 Client의 역할을 명확하게 분리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점&lt;/p&gt;
&lt;ul data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;표준이 존재하지 않는다.&lt;/li&gt;
&lt;li&gt;사용할 수 있는 method가 제한적이다.&lt;/li&gt;
&lt;li&gt;구형 브라우저가 아직 지원하지 못하는 부분이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REST API&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API : Application Programming Interface 서로 정보를 교환 가능하도록&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;565&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/onvFl/btsLAypEpGv/MrOcycL1IpwUKre9MK26Jk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/onvFl/btsLAypEpGv/MrOcycL1IpwUKre9MK26Jk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/onvFl/btsLAypEpGv/MrOcycL1IpwUKre9MK26Jk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FonvFl%2FbtsLAypEpGv%2FMrOcycL1IpwUKre9MK26Jk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;565&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;565&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST의 특징을 기반으로 서비스 API를 구현하는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 OpenAPI나 마이크로 서비스를 제공하는 기업 대부분은 REST API를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;각 요청이 어떤 정보나 동작을 위한 것인지를 그 요청의 모습(URI) 자체로 추론 가능하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사내 시스템 - REST 기반으로 시스템을 분산하여 유지보수, 운용에 편리&lt;/li&gt;
&lt;li&gt;델파이 클라이언트, 자바, C#, 웹 등을 이용해 클라이언트를 제작할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점&lt;/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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설계 원칙&lt;/h3&gt;
&lt;ul  data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;URI는 명사를 사용한다. (동사 X)&lt;/li&gt;
&lt;li&gt;URI는 소문자로만 구성한다. (대문자 X)&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하이픈 `-` 사용한다. (언더바 `_` X)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CRUD 기능을 나타내는 것은 URI 에 사용하지 않는다.&lt;/li&gt;
&lt;li&gt;/로 계층 관계를 표현한다.&lt;/li&gt;
&lt;li&gt;URI 마지막 문자로 /를 포함하지 않는다.&lt;/li&gt;
&lt;li&gt;HTTP 응답 상태 코드를 사용한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도큐먼트(객체 인스턴스와 유사) 이름 : 단수 명사&lt;/li&gt;
&lt;li&gt;컬렉션(서버 디렉터리) 이름 : 복수 명사&lt;/li&gt;
&lt;li&gt;스토어(클라이언트 리소스) 이름 : 복수명사&lt;/li&gt;
&lt;li&gt;Get /members/1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;파일확장자는 URI에 포함하지 않는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Accept header 을 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RESTful API&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST의 설계 규칙을 잘 지켜서 설계된 API를 RESTful한 API라고 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;즉, REST 원리를 따르는 시스템, 서비스를 RESTful 하다고 한다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그렇다면 RESTful 하지 못한 것?&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;경로에 resoure, id 이외의 정보가 들어가는 경우 /member/updateName&lt;/li&gt;
&lt;li&gt;CRUD 기능을 모두 POST 로 처리하는 API&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SOAP API&lt;/b&gt; - Simple Object Access Protocol : 그 자체로 프로토콜이며, 보안이나 메시지 전송 등에 더 많은 표준들이 정해져있기 때문에 조금 더 복잡하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;목적&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일관적인 컨벤션을 통해 이해하기 쉽고 사용하기 쉬운 REST API를 만드는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청을 보내는 주소만으로도 어떤 것을 요청하는 지 파악할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능이 중요할 경우 필수는 아니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;# 참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://aws.amazon.com/ko/what-is/restful-api/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://aws.amazon.com/ko/what-is/restful-api/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@wngkdroqkf441/Frontend-%EA%B8%B0%EC%88%A0-%EB%A9%B4%EC%A0%91-%EB%8C%80%EB%B9%84-RestAPI&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@wngkdroqkf441/Frontend-%EA%B8%B0%EC%88%A0-%EB%A9%B4%EC%A0%91-%EB%8C%80%EB%B9%84-RestAPI&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hahahoho5915.tistory.com/54&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://hahahoho5915.tistory.com/54&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  FrontEnd</category>
      <category>기술면접</category>
      <category>프론트엔드</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/226</guid>
      <comments>https://datdaradanadat.tistory.com/226#entry226comment</comments>
      <pubDate>Fri, 27 Dec 2024 16:10:09 +0900</pubDate>
    </item>
    <item>
      <title>♾️ 무한 스크롤, 이미지 Lazy Loading으로 성능 개선</title>
      <link>https://datdaradanadat.tistory.com/225</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;기존 더보기 버튼으로 구현되어있던 페이지네이션 pagenation 을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useInfiniteQuery, Intersection Observer API를 이용하여 무한 스크롤 리팩토링 하였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 더보기 구현 방식&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1520&quot; data-origin-height=&quot;818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dzepwX/btsLu3v2cnR/XIZyizSZAvAKQfPjByBxnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dzepwX/btsLu3v2cnR/XIZyizSZAvAKQfPjByBxnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dzepwX/btsLu3v2cnR/XIZyizSZAvAKQfPjByBxnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdzepwX%2FbtsLu3v2cnR%2FXIZyizSZAvAKQfPjByBxnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1520&quot; height=&quot;818&quot; data-origin-width=&quot;1520&quot; data-origin-height=&quot;818&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;먼저 서버에서 사용하는 데이터 형식을 확인하면,&amp;nbsp;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1854&quot; data-origin-height=&quot;932&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d4GsYK/btsLrVUgnIO/epBbPnnvngQN6jLZtwKuOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d4GsYK/btsLrVUgnIO/epBbPnnvngQN6jLZtwKuOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d4GsYK/btsLrVUgnIO/epBbPnnvngQN6jLZtwKuOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd4GsYK%2FbtsLrVUgnIO%2FepBbPnnvngQN6jLZtwKuOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1854&quot; height=&quot;932&quot; data-origin-width=&quot;1854&quot; data-origin-height=&quot;932&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공연 전체조회 시 쿼리로 page 를 필수로 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 codename(장르), title(검색), date(공연날짜)는 필터링이나 상세공연정보 detail 에 접속할때 사용한다.&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;즉, 이러한 형식으로 요청을 전송하면 데이터는 json 배열로 8개의 각 공연 객체가 전송된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clY73V/btsLsjm3aHo/Ki1YrS2MQGoVLR6hQkk0rK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clY73V/btsLsjm3aHo/Ki1YrS2MQGoVLR6hQkk0rK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clY73V/btsLsjm3aHo/Ki1YrS2MQGoVLR6hQkk0rK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclY73V%2FbtsLsjm3aHo%2FKi1YrS2MQGoVLR6hQkk0rK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;853&quot; height=&quot;654&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;654&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터를 불러오는 react-query 코드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 api url -&amp;gt; 검색, 카테고리, 페이지네이션, 날짜를 이용한 공연상세&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 기능을 모두 사용하기 때문에 useQuery 를 사용하여 파라미터 인자값을 옵셔널로 전송하기 위해 도입하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멘토님께서 코드의 활용도를 높이기 위해 사용하는 것을 추천하셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트에서 처음 도입하여 조금 미흡하였지만, 이후 3개의 프로젝트에서 또 사용하며 훅, refetch, invalidate 등 다양한 기술을 활용해 실력을 디벨롭했다~&lt;/p&gt;
&lt;pre id=&quot;code_1734942856753&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//Performances.api.ts
export const fetchPerformances = async (
  codename?: string,
  title?: string,
  page?: number,
  date?: string,
): Promise&amp;lt;IPerformancePayload[]&amp;gt; =&amp;gt; {
  try {
    // const processedCodename = codename ? codename.split('/')[0] : undefined;
    const response = await httpClient.get&amp;lt;IPerformancePayload[]&amp;gt;(`/events`, {
      params: {
        codename,
        title,
        page,
        date,
      },
    });
    if (Array.isArray(response.data)) {
      return response.data as IPerformancePayload[];
    } else {
      throw new Error('API 응답 데이터 형식 오류: 배열이 아닙니다', response.data);
    }
  } catch (error) {
    throw new Error('API 요청 오류' +error + '');
    return [];
  }
};

//usePerfromances.ts
export interface IPerformancePayload {
  title: string;
  image: string;
  codename: string;
  date: string;
  [key: string]: any; // 추가 속성들을 허용
}

export const usePerformances = ({ codename, title, page = 1 }: { codename?: string; title?: string; page: number }) =&amp;gt; {
  const res = useQuery&amp;lt;IPerformancePayload[]&amp;gt;({
    queryKey: [
      'performances',
      {
        codename,
        title,
        page,
      },
    ],
    queryFn: () =&amp;gt; fetchPerformances(codename, title, page),
    placeholderData: keepPreviousData,
  });

  return {
    ...res,
    data: res.data || [],
  };
};

//Home.tsx
  const {
    data: performances,
    isLoading,
    isError,
    error,
  } = usePerformances({
    page,
    codename: selectedCategory.includes(`/`) ? selectedCategory.split(`/`)[0] : selectedCategory || undefined,
    title: searchTerm || undefined,
  });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;페이지네이션의 더보기로 전체 데이터를 나열하려면&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 데이터 prev 에 다음페이지 불러오는 데이터를 합쳐서 allPerformances 로 관리했다.&lt;/p&gt;
&lt;pre id=&quot;code_1734942910449&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; const [page, setPage] = useState&amp;lt;number&amp;gt;(1);
  const [allPerformances, setAllPerformances] = useState&amp;lt;IPerformancePayload[]&amp;gt;([]);
  
   // Performances 데이터 업데이트
  useEffect(() =&amp;gt; {
    if (performances &amp;amp;&amp;amp; Array.isArray(performances)) {
      if (page === 1) {
        setAllPerformances(performances);
      } else if (performances.length &amp;gt; 0) {
        setAllPerformances((prev) =&amp;gt; [...prev, ...performances]);
      }
    }
  }, [performances]);

  // 페이지네이션 로직을 포함하여 공연 데이터 로드
  const loadMorePerformances = useCallback(() =&amp;gt; {
    if (!isLoading &amp;amp;&amp;amp; performances.length &amp;gt; 0) {
      setPage((prevPage) =&amp;gt; prevPage + 1);
      // setAllPerformances([]);
    }
  }, [isLoading, performances]);
  
    // 로딩 및 에러 상태 처리
  if (isLoading &amp;amp;&amp;amp; allPerformances.length === 0) return &amp;lt;div&amp;gt;로딩 중...&amp;lt;/div&amp;gt;;

  if (isError) {
    // 에러 메시지 처리
    if (error instanceof Error) {
      return (
        &amp;lt;div&amp;gt;
          데이터를 불러오는 데 문제가 발생했습니다.
          &amp;lt;p&amp;gt;{error.message}&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
      );
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이때 발생한 트러블 슈팅이 코드의 복잡성을 높였다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;page=1 의 performance가 allPerformances 에 prev 에 들어가고, page=2 의 performance 가 새로 받아오는 ...performances 값으로 set이 새롭게 되어야 했는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 비동기의 문제로 setPage가 늦게 실행되어 page=1의 공연값이 두번 중복되어 나오는 문제가 발생하거나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. setPerformances가 원하는대로 동작하지 않았다. - 히스토리 관리의 문제점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 페이지네이션된 데이터에서(allPerformances) 검색이나 카테고리를 적용하는 문제점이 발생하였다. api 를 refetch 해야하는데!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모바일 형식으로 디자인하였기 때문에 페이지 형식이 아닌 스크롤 형식으로 설계하였는데,&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;스크롤 형식은 더보기 버튼을 클릭하는 것보다 무한스크롤이 더 적절하다고 판단하여 리팩토링을 진행하였다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;useInfiniteQuery&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글에 `useInfinitequery 무한스크롤` 검색하면 엄청나게 많은 글이 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올리브영, 카카오엔터프라이즈 등 다양한 기업에서도 많이 도입하고 있는것을 알 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;lt;공식문서, 사용 개념&amp;gt;&lt;/h3&gt;
&lt;figure id=&quot;og_1734944854309&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;useInfiniteQuery | TanStack Query React Docs&quot; data-og-description=&quot;tsx const { fetchNextPage, fetchPreviousPage, hasNextPage, hasPreviousPage, isFetchingNextPage, isFetchingPreviousPage, promise, ...result } = useInfiniteQuery({ queryKey, queryFn: ({ pageParam }) = f...&quot; data-og-host=&quot;tanstack.com&quot; data-og-source-url=&quot;https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery&quot; data-og-url=&quot;https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dDjVOF/hyXOoic3hp/y0mLePmmHOnrhAKpGc1bkK/img.png?width=3000&amp;amp;height=1704&amp;amp;face=0_0_3000_1704,https://scrap.kakaocdn.net/dn/cN4GFn/hyXSpmcNjm/hsJJHcRKZPojmx4pTkmETK/img.png?width=3000&amp;amp;height=1704&amp;amp;face=0_0_3000_1704&quot;&gt;&lt;a href=&quot;https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dDjVOF/hyXOoic3hp/y0mLePmmHOnrhAKpGc1bkK/img.png?width=3000&amp;amp;height=1704&amp;amp;face=0_0_3000_1704,https://scrap.kakaocdn.net/dn/cN4GFn/hyXSpmcNjm/hsJJHcRKZPojmx4pTkmETK/img.png?width=3000&amp;amp;height=1704&amp;amp;face=0_0_3000_1704');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;useInfiniteQuery | TanStack Query React Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;tsx const { fetchNextPage, fetchPreviousPage, hasNextPage, hasPreviousPage, isFetchingNextPage, isFetchingPreviousPage, promise, ...result } = useInfiniteQuery({ queryKey, queryFn: ({ pageParam }) = f...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tanstack.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무한 스크롤을 편하게 구현할 수 있도록 한다. 기존 버튼클릭방식도 데이터를 계속 쌓아주기 때문에 사용하기 오히려 좋다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 useQuery 사용(return, option 속성 동일)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가된 특히 다른 리턴값은 returns fetchNextPage, hasNextPage이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션 option 은 getNextPageParam이다.&lt;/p&gt;
&lt;pre id=&quot;code_1734945125789&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const {
  fetchNextPage,
  fetchPreviousPage,
  hasNextPage,
  hasPreviousPage,
  isFetchingNextPage,
  isFetchingPreviousPage,
  promise,
  ...result
} = useInfiniteQuery({
  queryKey,
  queryFn: ({ pageParam }) =&amp;gt; fetchPage(pageParam),
  initialPageParam: 1,
  ...options,
  getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =&amp;gt;
    lastPage.nextCursor,
  getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams) =&amp;gt;
    firstPage.prevCursor,
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 공식 문서에서 페이지네이션 구현 시 꼭 사용했던 것만 소개해보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;return&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;data : 요청을 통해 받아온 데이터&lt;/li&gt;
&lt;li&gt;fetchNextPage : 다음 페이지의 데이터 불러온다&lt;/li&gt;
&lt;li&gt;hasNextPage : 가져돌 다음 페이지의 데이터가 존재하는지 확인한다. (boolean)&lt;/li&gt;
&lt;li&gt;refetch, isLoading, isError, error 리턴 추가로 사용함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;option&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`getNextPageParam:(lastPage, allPages)` : 새 데이터를 수신하면, 마지막 페이지와 모든페이지의 전체배열, pageParam 정보를 수신한다. 다음 페이지가 없을 경우 undefined 또는 null 을 반환한다. 반환값이 다음 api 호출의 pageParam 으로 들어간다.&lt;/li&gt;
&lt;li&gt;select : 반환되는 데이터를 필터링, 계층화 등 변환할 경우 사용하는 것이 성능에 좋다. useInfiniteQuery의 경우 페이지가 쌓이는 형태이기 때문에 구조가 복잡해져서 가공하는 편이 코드에 좋다.&amp;nbsp;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;(상세, 전체의 차이점은 place, org_link 두개)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;initialPageParam : 페이지네이션 값 초기값 선언&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트 활용코드 - 옵션, 로직&lt;/h3&gt;
&lt;pre id=&quot;code_1734946859354&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useInfiniteQuery } from '@tanstack/react-query';
import { fetchPerformances } from '@/apis/Performances.api';

export interface IPerformancePayload {
  title: string;
  image: string;
  codename: string;
  date: string;
}

export const getPerformances = ({ codename, title }: IPerformancePayload) =&amp;gt; {
  const { data, fetchNextPage, hasNextPage, refetch, isLoading, isError, error } = useInfiniteQuery({
    queryKey: ['products'],
    queryFn: ({ pageParam = 1 }) =&amp;gt; fetchPerformances(codename, title, pageParam as number),
    getNextPageParam: (lastPage, allPages) =&amp;gt; {
      if (lastPage.length === 0) {
        return undefined;
      }
      return allPages.length + 1;
    },
    initialPageParam: 1,
    select: (data) =&amp;gt; ({
      pages: data.pages.map((page) =&amp;gt;
        page.map((item) =&amp;gt; ({
          title: item.title,
          image: item.image,
          codename: item.codename,
          date: item.date,
        })),
      ),
      pageParams: data.pageParams,
    }),
  });
  return {
    performance: data?.pages.flat() || [],
    fetchNextPage,
    hasNextPage,
    refetch,
    isLoading,
    isError,
    error,
  };
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 page 까지 매개변수로 받았는데, useInfiniteQuery 를 도입하면서 제거했다.&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;queryFn 는 api 호출하는 옵션인데, 기본적으로 `pageParam` 이라는 옵션으로 제공한다. 첫 페이지는 1로 지정한다.&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;getNextPageParam 으로 다음페이지가 있는지 확인하고, lastPage의 데이터가 비었으면 끝! undefined 를 리턴한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝이 아닐경우에는 `allPages.length+1` 을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 length 인지? 데이터를 확인해보면 다음처럼 배열로 묶인 객체 형태로 데이터가 전송되기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ij7d3/btsLtscBeQ5/SjvsjJ52onsZCPcWaY1TS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ij7d3/btsLtscBeQ5/SjvsjJ52onsZCPcWaY1TS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ij7d3/btsLtscBeQ5/SjvsjJ52onsZCPcWaY1TS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIj7d3%2FbtsLtscBeQ5%2FSjvsjJ52onsZCPcWaY1TS1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;656&quot; height=&quot;400&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;initialPageParam을 1로 초기화 한다.&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;전체 페이지 로드에서 사용하는 데이터 항목 (4개)만 select 필터링해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;org_link 와 place 데이터가 제외된걸 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dNiV5X/btsLuYhggNB/Y9u1DXhhIISHFpjwZt3N70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dNiV5X/btsLuYhggNB/Y9u1DXhhIISHFpjwZt3N70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dNiV5X/btsLuYhggNB/Y9u1DXhhIISHFpjwZt3N70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdNiV5X%2FbtsLuYhggNB%2FY9u1DXhhIISHFpjwZt3N70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1066&quot; height=&quot;368&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카테고리, 검색(title) 이 변경될 경우, refetch 를 통해 데이터를 다시 가져온다. 항상 최신의 데이터를 제공한다.&lt;/p&gt;
&lt;pre id=&quot;code_1734961861933&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  // 카테고리 변경 핸들러
  const handleCategoryChange = useCallback(
    (category: string) =&amp;gt; {
      startTransition(() =&amp;gt; {
        setSelectedCategory(category === '전체' ? '' : category);
        setSearchTerm('');
      });
      refetch();
    },
    [refetch],
  );

  // 검색어 변경 핸들러
  const handleSearchChange = useCallback(
    (term: string) =&amp;gt; {
      startTransition(() =&amp;gt; {
        setSearchTerm(term);
      });
      refetch();
    },
    [refetch],
  );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이렇게 하면 페이지네이션 요청을 위한 react-query 작성 완료&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Intersection Observer API 위치 감지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무한 스크롤에서 스크롤을 감지하는 두가지 방법&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. onScroll 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘텐츠의 전체 길이와 현재 스크롤한 길이를 비교하여 스크롤 바닥을 감지하는 방법&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1272&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xagPL/btsLtxkQ6p3/X3uYY2ubkcuzYnmk8Gf9S0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xagPL/btsLtxkQ6p3/X3uYY2ubkcuzYnmk8Gf9S0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xagPL/btsLtxkQ6p3/X3uYY2ubkcuzYnmk8Gf9S0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxagPL%2FbtsLtxkQ6p3%2FX3uYY2ubkcuzYnmk8Gf9S0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1272&quot; height=&quot;904&quot; data-origin-width=&quot;1272&quot; data-origin-height=&quot;904&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scrollHeight : 콘텐츠(HTML) 전체의 길이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;clientHeight : 스크린 상의 높이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scrollTop : clientHeight 윗부분. 스크롤을 맨밑으로 내리면 scrollHeight - UI 높이값 = scrollTop&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무한 스크롤 감지할 스크롤 위치에서는 (최하단)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scrollHeight - scrollTop 값은 clientHeight 가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최하단 이전에는 scrollHeight - scrollTop 값 &amp;gt; clientHeight 상태이다. 같아질때 스크롤이 바닥에 닿는다고 감지할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점 : 스크롤이 움직일때마다 이벤트 핸들러가 호출되어 성능문제가 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점 해결 : Throttle, Debounce 최적화&amp;nbsp; - 이벤트를 한번만 호출시키도록 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 복잡한 과정을 간편하게 해결한게 Intersection Observer!! (polyfill 방식도 존재)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Intersection Observer API&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 요소 element 가 viewport에 교차하는 여부를 비동기적으로 감지할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크롤을 내려서 페이지의 마지막 요소에 닿았을때 감지하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 다음 페이지의 데이터를 자동으로 가져오도록 메소드를 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기적으로 실행되어 스레드에 영향을 주지 않고, 요소의 변경사항을 관찰할 수 있다. 리플로우 현상도 방지할 수 있다.&lt;/p&gt;
&lt;figure id=&quot;og_1734962757025&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - thebuilder/react-intersection-observer: React implementation of the Intersection Observer API to tell you when an eleme&quot; data-og-description=&quot;React implementation of the Intersection Observer API to tell you when an element enters or leaves the viewport. - thebuilder/react-intersection-observer&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/thebuilder/react-intersection-observer&quot; data-og-url=&quot;https://github.com/thebuilder/react-intersection-observer&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/1YdMR/hyXSsXzG09/hKDzFiVqwehBiYdbmPfLVK/img.png?width=1200&amp;amp;height=600&amp;amp;face=991_125_1034_171,https://scrap.kakaocdn.net/dn/c7qXdw/hyXSxLllLq/IoVGECxoqY7uazS09dpwt0/img.png?width=1200&amp;amp;height=600&amp;amp;face=991_125_1034_171&quot;&gt;&lt;a href=&quot;https://github.com/thebuilder/react-intersection-observer&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/thebuilder/react-intersection-observer&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/1YdMR/hyXSsXzG09/hKDzFiVqwehBiYdbmPfLVK/img.png?width=1200&amp;amp;height=600&amp;amp;face=991_125_1034_171,https://scrap.kakaocdn.net/dn/c7qXdw/hyXSxLllLq/IoVGECxoqY7uazS09dpwt0/img.png?width=1200&amp;amp;height=600&amp;amp;face=991_125_1034_171');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - thebuilder/react-intersection-observer: React implementation of the Intersection Observer API to tell you when an eleme&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;React implementation of the Intersection Observer API to tell you when an element enters or leaves the viewport. - thebuilder/react-intersection-observer&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리가 존재하지만, 간단히 훅을 작성하면 쉽게 사용할 수 있다고 하여 직접 작성해보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1734963478073&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { InfiniteQueryObserverResult } from '@tanstack/react-query';
import { useCallback, useEffect, useRef } from 'react';

type IntersectionObserverProps = {
  hasNextPage: boolean | false;
  fetchNextPage: () =&amp;gt; Promise&amp;lt;InfiniteQueryObserverResult&amp;gt;;
};

function useIntersect({ hasNextPage, fetchNextPage }: IntersectionObserverProps) {
  const ref = useRef&amp;lt;HTMLDivElement&amp;gt;(null);

  const handleIntersect: IntersectionObserverCallback = useCallback(
    ([entry]: IntersectionObserverEntry[]) =&amp;gt; {
      if (entry?.isIntersecting &amp;amp;&amp;amp; hasNextPage) {
        fetchNextPage();
      }
    },
    [fetchNextPage, hasNextPage],
  );

  useEffect(() =&amp;gt; {
    let observer: IntersectionObserver;
    if (ref.current) {
      observer = new IntersectionObserver(handleIntersect, { threshold: 0.1 });
      observer.observe(ref.current);
    }
    return () =&amp;gt; observer &amp;amp;&amp;amp; observer.disconnect();
  }, [ref, handleIntersect]);

  return ref;
}

export default useIntersect;&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;&amp;nbsp;hasNextPage : 다음 페이지가 있는지를 나타내는 불리언&lt;/li&gt;
&lt;li&gt;fetchNextPage : 다음 페이지 데이터를 가져오는 비동기 함수&lt;/li&gt;
&lt;li&gt;useRef 로 감지할 요소를 지정한다.&lt;/li&gt;
&lt;li&gt;Intersection Observer 콜백 : handleIntersect 함수로 element 가 뷰포트에 들어오는지 판단한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`entry.isIntersecting` : 요소가 뷰포트에 들어왔는지를 나타내는 불리언 값&lt;/li&gt;
&lt;li&gt;hasNextPage : 다음 페이지가 있는지 확인&lt;/li&gt;
&lt;li&gt;두 조건이 참일 경우 : fetchNextPage()를 호출해 다음 페이지의 데이터를 가져온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;useEffect로 컴포넌트가 마운트 될때 Intersection Observer를 설정하고, 언마운트 될때 해제한다.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`observer = new IntersectionObserver(handleIntersect, { threshold: 0.1 });`
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;observer 객체를 생성&lt;/li&gt;
&lt;li&gt;handleIntersect를 콜백으로 전달한다.&lt;/li&gt;
&lt;li&gt;threshold 를 0.1 로 설정하여, 요소의 10%가 뷰포트에 들어올때 호출된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;`observer.observe(ref.current)`를 통해 지정된 DOM 요소를 관찰&lt;/li&gt;
&lt;li&gt;컴포넌트가&amp;nbsp;언마운트될&amp;nbsp;때,&amp;nbsp;observer.disconnect()를&amp;nbsp;호출하여&amp;nbsp;메모리&amp;nbsp;누수를&amp;nbsp;방지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  문제점 - ref 렌더링 높이에 따른 IntersectionObserver 감지 불가능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ref 에서 요소가 존재하지 않으면 데이터가 더이상 받아와지지 않았다. (page 4까지만 동작함) 기존 많은 블로그 코드는 아래처럼 작성됨..&lt;/p&gt;
&lt;pre id=&quot;code_1734965308517&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; {hasNextPage ? (
        &amp;lt;div ref={target} /&amp;gt;
      ) : (
        &amp;lt;div className=&quot;loadMoreBtn&quot; style={{ backgroundColor: 'lightgray' }}&amp;gt;
          더이상 데이터가 없습니다.
        &amp;lt;/div&amp;gt;
      )}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Intersection Observer 는 뷰포트와 관찰하는 요소 사이의 교차 상태를 감지하는 API 이다. 판단을 위해 bounding box 요소의 경계를 사용한다.위처럼 지정할 경우 div 에 아무 내용도 없기 때문에 &lt;b&gt;렌더링된 높이가 0&lt;/b&gt;일수 있다. 이러한 경우 요소를 감지할 수 없어 Intersection Observer가 호출되지 않는다.&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;ref div를 아래처럼 영역이 존재한다면 무한스크롤이 정상동작하였다. -&amp;gt; &lt;b&gt;test code&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734969157861&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; &amp;lt;div ref={target} className=&quot;loadMoreBtn&quot;&amp;gt;
          더보기 로딩중...
        &amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3120&quot; data-origin-height=&quot;2136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDPV9g/btsLuHUBQoS/p66AN0ljUCCuevzXovdt7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDPV9g/btsLuHUBQoS/p66AN0ljUCCuevzXovdt7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDPV9g/btsLuHUBQoS/p66AN0ljUCCuevzXovdt7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDPV9g%2FbtsLuHUBQoS%2Fp66AN0ljUCCuevzXovdt7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3120&quot; height=&quot;2136&quot; data-origin-width=&quot;3120&quot; data-origin-height=&quot;2136&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; &lt;b&gt;develop&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 형식과 비슷한 div 모양으로 만드니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직관적으로 UI 가 로딩중이라는 티가 전혀 나지 않아&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;loading spinner 를 적용했다. (이외의 로딩에도 전체 적용)&lt;/p&gt;
&lt;pre id=&quot;code_1734970278685&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.lds-ellipsis,
.lds-ellipsis div {
  box-sizing: border-box;
}
.lds-ellipsis {
  display: inline-block;
  position: relative;
  left: 45%;
  width: 80px;
  height: 80px;
}
.lds-ellipsis div {
  position: absolute;
  top: 30px;
  width: 13.33333px;
  height: 14px;
  border-radius: 50%;
  background: #eb6b83;
  animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.lds-ellipsis div:nth-child(1) {
  left: 0px;
  animation: lds-ellipsis1 0.6s infinite;
}
.lds-ellipsis div:nth-child(2) {
  left: 0px;
  animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(3) {
  left: 24px;
  animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(4) {
  left: 48px;
  animation: lds-ellipsis3 0.6s infinite;
}
@keyframes lds-ellipsis1 {
  0% {
    transform: scale(0);
  }
  100% {
    transform: scale(1);
  }
}
@keyframes lds-ellipsis3 {
  0% {
    transform: scale(1);
  }
  100% {
    transform: scale(0);
  }
}
@keyframes lds-ellipsis2 {
  0% {
    transform: translate(0, 0);
  }
  100% {
    transform: translate(24px, 0);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;11mb.gif&quot; data-origin-width=&quot;754&quot; data-origin-height=&quot;1046&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DQ5JP/btsLtpNS5c7/Drdv8CYjnBtbYnIMfO0vj1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DQ5JP/btsLtpNS5c7/Drdv8CYjnBtbYnIMfO0vj1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DQ5JP/btsLtpNS5c7/Drdv8CYjnBtbYnIMfO0vj1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/DQ5JP/btsLtpNS5c7/Drdv8CYjnBtbYnIMfO0vj1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;754&quot; height=&quot;1046&quot; data-filename=&quot;11mb.gif&quot; data-origin-width=&quot;754&quot; data-origin-height=&quot;1046&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로인해 추가된 사항은 스크롤 표시와 Image lazy Loading 이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 모바일 타겟이기에 스크롤을 display:none 했는데, 무한스크롤하면 스크롤이 어디있는지 확인하는게 좋다고 판단해 표시했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 lazy Loading 은 Intersection Observer API 를 사용하는 방법이 있어 적용중이다. (완료되면 추가!)&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; 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2주간의 짧은 개발과정에서 useInfiniteQuery 없이 페이지네이션을 구현하기위해 마주했던 수많은 오류를 생각하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진작에 이 존재를 알고 도입했다면 좋았겠다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래머스와 동아리 프로젝트 활동에서 배운것은 '왜' 이 기술을 도입해야하는지 먼저 생각하고 여러 방법 중 최선의 선택을 하는것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더보기 페이지네이션을 무한스크롤로 개선하며 useInfiniteQuery를 도입한 경험은 사용자 측면을 깊게 고려하고, 코드(기술)의 복잡성을 줄이는 긍정적인 경험을 주었다.&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;더 나아가 로딩 상태, 이미지 lazy loading 등 다양한 생각으로 넓혀갈 수 있어 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 깃허브&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1734978129036&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - programmers-project-july/ShowMailer-FE&quot; data-og-description=&quot;Contribute to programmers-project-july/ShowMailer-FE development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/programmers-project-july/ShowMailer-FE&quot; data-og-url=&quot;https://github.com/programmers-project-july/ShowMailer-FE&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/hbvVt/hyXOnjlfae/zUrxKk4gtKte0HZKJNjUHk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/jXHD3/hyXOih4ugv/7mxkZ8HaW1MbDvrWRcUpL0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/programmers-project-july/ShowMailer-FE&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/programmers-project-july/ShowMailer-FE&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/hbvVt/hyXOnjlfae/zUrxKk4gtKte0HZKJNjUHk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/jXHD3/hyXOih4ugv/7mxkZ8HaW1MbDvrWRcUpL0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - programmers-project-july/ShowMailer-FE&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to programmers-project-july/ShowMailer-FE development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  FrontEnd</category>
      <category>프론트엔드</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/225</guid>
      <comments>https://datdaradanadat.tistory.com/225#entry225comment</comments>
      <pubDate>Tue, 24 Dec 2024 03:24:00 +0900</pubDate>
    </item>
    <item>
      <title>Error Boundary 원리</title>
      <link>https://datdaradanadat.tistory.com/224</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;에러경계 Error Boundary : 하위 컴포넌트에서 발생하는 자바스크립트 에러를 잡아서 fallback UI 를 보여주는 공식 리액트 컴포넌트.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선언적으로 에러를 처리하는 방법. 공식 리액트가 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Error Boundary가 캐치할 수 없는 코드 를 있는 코드로 변경해보기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 브라우저의 구조를 뜯어보아 Error Boundary의 목적을 알아보기&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;기본 사용 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App.tsx&lt;/p&gt;
&lt;pre id=&quot;code_1734498860969&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function App: React.FC()(
//&amp;lt;QueryClientProvider client={queryClient}&amp;gt; 리액트쿼리 쓰려면!
  &amp;lt;ErrorBoundary fallback={&amp;lt;p&amp;gt;Something went wrong&amp;lt;/p&amp;gt;}&amp;gt;
    &amp;lt;MyComponent /&amp;gt;
  &amp;lt;/ErrorBoundary&amp;gt;
//&amp;lt;/QueryClientProvider&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ErrorBoundary 컴포넌트 하위에 존재하는 컴포넌트에 에러throw 를 적용하기 위해, 상위에서 묶는다.&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;ErrorBoundary.tsx&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;리액트 공식문서&lt;/a&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734499100532&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface ErrorBoundaryProps {
  children: React.ReactNode;
}

interface ErrorBoundaryState {
  hasError: boolean;
}

class ErrorBoundary extends React.Component&amp;lt;ErrorBoundaryProps, ErrorBoundaryState&amp;gt; {
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error) {
      // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
      // Example &quot;componentStack&quot;:
    //   in ComponentThatThrows (created by App)
    //   in ErrorBoundary (created by App)
    //   in div (created by App)
    //   in App
       logErrorToMyService(error, info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      // return this.props.fallback;
      return &amp;lt;h1&amp;gt;error Boundary!&amp;lt;/h1&amp;gt;
    }

    return this.props.children; 
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;myComponent.tsx&lt;/p&gt;
&lt;pre id=&quot;code_1734503268236&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const fetchTodos = async()=&amp;gt;{
	try{
		const res = await fetch(&quot;api 주소&quot;);
        const data = awati res.json();
        setTodos(data);
    }catch(error){
        console.error(&quot;fetchError&quot;,error);
        setError(error as Error);
  //throw error; //동작안함
  }};
  
  if(error){
  	throw error; //동작함
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;왜 비동기문 안에서는 throw&amp;nbsp; 한 것을 catch를 못했을까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;브라우저 구조 - 렌더링 프로세스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;event loop&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;callstack&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;v8 JS 엔진&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;api&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구성되어있고, C++ 로 작성되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기로직 - Event Loop 에서 처리한다 = 실행 컨텍스트를 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fetch 서버와 통신&lt;/li&gt;
&lt;li&gt;event 사용자 이벤트 - onClick, onChange 등&lt;/li&gt;
&lt;li&gt;setTimeout&lt;/li&gt;
&lt;li&gt;등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JS 엔진도 실행 컨텍스트를 가진다. 에러캐치는 여기에서 한다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 다른 주체가 각각의 실행컨텍스트를 가지는데, 속한게 다르기 때문에 비동기는 캐치를 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JS 엔진 - 에러 바운더리 / Event Loop - 비동기&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;JS 엔진??&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 컴포넌트는 번들링이 되서 Js 파일이 script 태그를 만나서 Js 엔진에&amp;nbsp; 전달된다. 그럼 파싱해서 실행 컨텍스트를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ErrorBoundary 는 리액트의 기능이기 때문에 JS 엔진에 해당하는 실행컨텍스트에서 에러를 캐치하는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비동기 내부에서 throw를 한 것은 EventLoop 에서 에러가 발생한 것이기 때문에, JS엔진에서 catch 를 할 수 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ErrorBoundary 의 목적은 리액트 렌더링 프로세스 내에서 발생하는 JavaScript 런타임 에러를 catch 하는 것!&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*리액트 렌더링 프로세스 : react Element tree(render phase) -&amp;gt; Fider = V DOM (commit phase) -&amp;gt; 실제 DOM 에 반영 -&amp;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;예기치 못한 에러가 발생해 throw 작업을 하지 못했더라도,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 렌더링 프로세스 내부에서 발생했다면 errorBoundary를 catch 를 할 수 있다!&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;2. ErrorBoundary , ReactQuery 프로젝트에 적용&lt;/p&gt;</description>
      <category>  FrontEnd</category>
      <category>프론트엔드</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/224</guid>
      <comments>https://datdaradanadat.tistory.com/224#entry224comment</comments>
      <pubDate>Wed, 18 Dec 2024 16:18:19 +0900</pubDate>
    </item>
    <item>
      <title>⏳ Vitest 테스트 도구를 적용해보기</title>
      <link>https://datdaradanadat.tistory.com/223</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cx3pkx/btsOuEBbhr1/GPVfleWsXQsrklM8w3SZ5k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cx3pkx/btsOuEBbhr1/GPVfleWsXQsrklM8w3SZ5k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cx3pkx/btsOuEBbhr1/GPVfleWsXQsrklM8w3SZ5k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcx3pkx%2FbtsOuEBbhr1%2FGPVfleWsXQsrklM8w3SZ5k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 테스트를 해야하나?&lt;/h2&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;h2 data-ke-size=&quot;size26&quot;&gt;왜 Vitest 를 사용하기로 했는지?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://vitest.dev/guide/&quot;&gt;vitest&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;vite 설정을 그대로 사용할 수 있어 간편&lt;/li&gt;
&lt;li&gt;jest 보다 상대적으로 빠름&lt;/li&gt;
&lt;li&gt;ESM&lt;/li&gt;
&lt;li&gt;jest와 호환되는 api 를 제공한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jestjs.io/docs/getting-started&quot;&gt;Jest&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;bable, typescript, node, react, angular, vue 지원&lt;/li&gt;
&lt;li&gt;bable 세팅해야함 - PWA 프로젝트의 안정성 제공&lt;/li&gt;
&lt;li&gt;commonJS&lt;/li&gt;
&lt;li&gt;공식문서와 사용예시 등 참고문서가 많다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 많은 테스트 모듈이 있지만(@testing-library 등) 기본 테스트의 집합체가 Jest 이고, 가장많이 사용되고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Jest 를 선택하지 않은 이유는, &lt;b&gt;클라이언트 프로젝트가 Vite, ESM 으로 빌드되었기 때문&lt;/b&gt;이다. 타입스크립트도 사용중&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/nanalmoa/nanalmoa&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;nanalmoa 깃허브&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 환경에서도 Jest 를 사용할 수 있지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vite는 가볍고 빠르다는 장점으로 bable을 미설치하는데, Jest 를 사용하려면 전체설치해야한다. 즉 라이브러리 의존성이 늘어나기 때문에&amp;nbsp; 속도도 조금 저하되는 단점이 있다. Vite 에서 Jest api 를 호환되고, vite 세팅을 그대로 사용해도 되는 vitest 를 사용하기로 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 기업이 Jest 를 쓰기 때문에 Jest 를 사용해보고 싶었다. 그러나 프로젝트의 적합도를 고려해보았을때 Vitest 가 맞고, Jest 와 테스트코드 작성법은 매우 유사하기 때문에 경험도에서도 문제는 없다고 판단했다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치 및 설정 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vite/5.0.0, node/18.0.0 이상이 필요하다. : 현재 vite/5.4.11 node/20.10.0 을 사용중이므로 문제 없음&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 개발 의존성 라이브러리 설치&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;yarn add -D @types/jest@^29.5.14 @types/react@^18.3.1 @types/react-dom@18.3.1 @testing-library/dom@^10.4.0 @testing-library/jest-dom@^6.6.3 @testing-library/react@^16.1.0 @testing-library/user-event@^14.5.2 jsdom@^25.0.1 vitest@^2.1.8&lt;/blockquote&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;vitset, jsdom (ReactDOM 환경 활성화 하기)&amp;nbsp; : `yarn add vitest` `yarn add jsdom`&amp;nbsp;&lt;/li&gt;
&lt;li&gt;React 테스팅 라이브러리 : `yarn add @testing-library/react`&lt;/li&gt;
&lt;li&gt;테스트 실행을 위한 DOM 환경 라이브러리 : `yarn add @testing-library/jest-dom` `yarn add @testing-library/dom`&lt;/li&gt;
&lt;li&gt;유저가 발생시키는 이벤트 테스트를 위한 라이브러리 : `yarn add @testing-library/user-event`&lt;/li&gt;
&lt;li&gt;테스트 타입 설치 : 타입스크립트를 사용하지 않는다면 미설치 `yarn add @types/jest` `yarn add @types/react` `yarn add @types/react-dom`&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제상황&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. @testing-library 의존성 중목 문제로 설치를 인식하지 못함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어 yarn 으로 설치했을 경우 dependencies와 devDependencies 모두에 포함되어, 타입스크립트가 어떤 패키지를 사용할 지에 문제가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;devDependencies 에 남기고, dependencies 에는 제거한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재설치하여서 해결!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &quot;@testing-library/react&quot;: &quot;^16.1.0&quot;, 16버전 이상일 경우 dom 수동으로 설치해 주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;최종 package.json&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test 스크립트 추가&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;test : watch 모드로 전체 파일 테스트 계속 실행&lt;/li&gt;
&lt;li&gt;test:run : 일회성으로 전체 파일 테스트 실행&lt;/li&gt;
&lt;li&gt;뒤에 파일명 추가하면, 해당 파일만 테스트 ex.`yarn test:run button.test.tsx`&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1733853472339&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  &quot;scripts&quot;: {
    &quot;test&quot;: &quot;vitest&quot;,
    &quot;test:run&quot;: &quot;vitest run&quot;,
    ///...
  &quot;dependencies&quot;: {
    &quot;@types/jest&quot;: &quot;^29.5.14&quot;,
    &quot;@types/react&quot;: &quot;^18.3.1&quot;,
    &quot;@types/react-dom&quot;: &quot;18.3.1&quot;,
    ///...
  &quot;devDependencies&quot;: {
    &quot;@testing-library/dom&quot;: &quot;^10.4.0&quot;,
    &quot;@testing-library/jest-dom&quot;: &quot;^6.6.3&quot;,
    &quot;@testing-library/react&quot;: &quot;^16.1.0&quot;,
    &quot;@testing-library/user-event&quot;: &quot;^14.5.2&quot;,
    &quot;jsdom&quot;: &quot;^25.0.1&quot;,
    &quot;vitest&quot;: &quot;^2.1.8&quot;
    ///...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. vite.config.ts 의 test 추가, vitest.setup.ts 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vite를 사용하지 않는다면 vitest.config.ts 를 생성해야하고, 이미 사용중이라면 vite config 에 test 을 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;타입으로 vitest 유형에 대한 참조를 추가해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://vitest.dev/guide/#configuring-vitest&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt; : need to add a reference to Vitest types using a&amp;nbsp;&lt;b&gt;triple slash directive&lt;/b&gt;&amp;nbsp;at the top of your config file.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;//vite.config.ts&lt;/p&gt;
&lt;pre id=&quot;code_1733853703244&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/// &amp;lt;reference types=&quot;vitest/config&quot; /&amp;gt;
import { defineConfig } from 'vite'

export default defineConfig({
  test: {
    // ... Specify options here.
    globals: true,
    environment: 'jsdom',
    setupFiles: './vitest.setup.ts',
  },
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dom 가져오기 위해 import 하는 셋업코드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;//vitest.setup.ts&lt;/p&gt;
&lt;pre id=&quot;code_1733853757251&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import '@testing-library/jest-dom'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 셋업파일을 타입스크립트가 정상적으로 인식할 수 있도록 tsconfig.json의 include 에 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1733853793989&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  &quot;include&quot;: [&quot;src&quot;, &quot;vitest.setup.ts&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. test 코드 작성&lt;/h3&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 코드 커버리지 상승 효과!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 폴더로 묶는 경우도 있지만, 동일위치에 두기로 결정!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`example.test.ts`, `example.spec.ts`와 같은 형식으로 파일명을 지정하면 자동을 인식한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리팀은 컴포넌트는 PascalCase, 이외의 파일은 kebap-case 를 사용한다. 테스트코드 파일명도 이 컨벤션을 따르기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수 테스트코드 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 예시를 참고하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 돌릴 원본 함수를 작성한다. `sum.ts`&lt;/p&gt;
&lt;pre id=&quot;code_1733927482685&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export function sum(a: number, b: number): number {
    return a + b;
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덧셈함수가 예상한대로 동작하는지 확인하기 위한 test코드 작성한다. sum.test.ts&lt;/p&gt;
&lt;pre id=&quot;code_1733927574500&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { expect, test } from 'vitest';
import { sum } from './sum';

test('덧셈 테스트 1 + 2 = 3', () =&amp;gt; {
  expect(sum(1, 2)).toBe(3);
});&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;test() : 테스트 케이스 정의. 조건을 실행하고 예상된 결과를 검증한다&lt;/li&gt;
&lt;li&gt;expect() : 테스트 할 값에 대한 예상 값 설정한다.&lt;/li&gt;
&lt;li&gt;toBe() : expect 와 함께 사용되며 값과 데이터 타입이 일치하는 지 비교한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 테스트코드 예시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼 컴포넌트를 생성 후, props 렌더링, 스타일링 렌더링이르 테스트한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;//Button.tsx&lt;/p&gt;
&lt;pre id=&quot;code_1733983737614&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { cn } from '@/utils/cn'
import { LoadingSpinner } from '.'

type Props = {
  theme?: 'outline' | 'solid'
  text: string
  isLoading?: boolean
} &amp;amp; React.ComponentProps&amp;lt;'button'&amp;gt;

const Button = ({
  type,
  theme = 'solid',
  className,
  text,
  disabled,
  isLoading,
  ...props
}: Props) =&amp;gt; {
  return (
    &amp;lt;button
      type={type || 'button'}
      className={cn(
        'flex items-center justify-center text-nowrap rounded-md px-3 py-2',
        theme === 'outline'
          ? 'border-primary-base text-primary-base border bg-white'
          : 'bg-primary-base text-white',
        disabled &amp;amp;&amp;amp;
          theme === 'outline' &amp;amp;&amp;amp;
          'border border-neutral-400 bg-white text-neutral-400',
        disabled &amp;amp;&amp;amp; theme === 'solid' &amp;amp;&amp;amp; 'bg-neutral-400 text-white',
        isLoading &amp;amp;&amp;amp; 'px-5',
        className,
      )}
      disabled={disabled}
      {...props}
    &amp;gt;
      {isLoading ? &amp;lt;LoadingSpinner className=&quot;h-4 w-4 sm:h-6 sm:w-6&quot; /&amp;gt; : text}
    &amp;lt;/button&amp;gt;
  )
}

export default Button&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 버튼 텍스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 로딩일 경우 로딩스피너&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. disabled 로 비활성화, 클릭불가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 테마에 따라 스타일링 분기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등의 기능을 가지고 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;//Button.test.tsx&lt;/p&gt;
&lt;pre id=&quot;code_1733983830679&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Button } from '@/components/common'
import { describe, expect, vi, test } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

describe('Button', () =&amp;gt; {
  test('기본 type 속성 확인', () =&amp;gt; {
    render(&amp;lt;Button text=&quot;버튼&quot; /&amp;gt;)
    expect(screen.getByText('버튼')).toHaveAttribute('type', 'button')
  })

  test('텍스트 렌더링 테스트', () =&amp;gt; {
    //렌더링 하고
    render(&amp;lt;Button text=&quot;로그인&quot; /&amp;gt;)
    //요소를 찾는다.
    expect(screen.getByText('로그인')).toBeInTheDocument()
  })

  test('로딩일때, loading spinner 렌더링', () =&amp;gt; {
    render(&amp;lt;Button text=&quot;loading&quot; isLoading={true} /&amp;gt;)
    const spinner = screen.getByRole('status') //LoadingSpinner.tsx 의 role 값
    expect(spinner).toBeInTheDocument()
  })

  test('로딩중일때 텍스트 미표시', () =&amp;gt; {
    render(&amp;lt;Button text=&quot;loading&quot; isLoading={true} /&amp;gt;)
    expect(screen.queryByText('loading')).not.toBeInTheDocument()
    //텍스트를 loading으로 지정해도, not.toBeInTheDocument 로 문서에 요소가 포함되어있지 않은지 검증
  })

  test('disabled 일때 버튼 비활성화 UI', () =&amp;gt; {
    render(&amp;lt;Button text=&quot;cant click&quot; disabled={true} /&amp;gt;)
    expect(screen.getByText('cant click')).toBeDisabled()
  })

  test('disabled 클릭 이벤트 차단', async () =&amp;gt; {
    const handleClick = vi.fn()
    render(&amp;lt;Button text=&quot;클릭&quot; onClick={handleClick} disabled={true} /&amp;gt;)
    //버튼 클릭 시뮬레이션
    await userEvent.click(screen.getByText('클릭'))
    expect(handleClick).not.toHaveBeenCalled()
    //클릭횟수 not 없음
  })

  test('클릭 이벤트', async () =&amp;gt; {
    const handleClick = vi.fn() //모의 함수 생성
    render(&amp;lt;Button text=&quot;클릭&quot; onClick={handleClick} /&amp;gt;)
    await userEvent.click(screen.getByText('클릭'))
    expect(handleClick).toHaveBeenCalledTimes(1)
    //클릭 핸들러 한 번 호출되었는지 검증
  })

  test('props 전파 확인', () =&amp;gt; {
    render(&amp;lt;Button text=&quot;버튼&quot; aria-label=&quot;속성&quot; /&amp;gt;)
    expect(screen.getByLabelText('속성')).toBeInTheDocument()
  })
  
  //스타일 렌더링 테마
  test('커스텀 클래스 적용 확인', () =&amp;gt; {
    render(&amp;lt;Button text=&quot;버튼&quot; className=&quot;custom-class&quot; /&amp;gt;)
    expect(screen.getByText('버튼')).toHaveClass('custom-class')
  })

  test('solid 테마일 때 스타일 적용', () =&amp;gt; {
    render(&amp;lt;Button text=&quot;버튼&quot; theme=&quot;solid&quot; /&amp;gt;)
    const button = screen.getByText('버튼')
    expect(button).toHaveClass('bg-primary-base')
    expect(button).toHaveClass('text-white')
  })

  test('outline 테마일 때 스타일 적용', () =&amp;gt; {
    render(&amp;lt;Button text=&quot;버튼&quot; theme=&quot;outline&quot; /&amp;gt;)
    const button = screen.getByText('버튼')
    expect(button).toHaveClass('border-primary-base')
    expect(button).toHaveClass('text-primary-base')
  })
})&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;render() : 컴포넌트 렌더링&lt;/li&gt;
&lt;li&gt;expect() : 동일&lt;/li&gt;
&lt;li&gt;screen.~ : ~의 요소가 화면에 존재하는 것&lt;/li&gt;
&lt;li&gt;toBeInTheDocument() : 특정 요소가 문서 안에 존재하는지 검증한다.&lt;/li&gt;
&lt;li&gt;toBeDisabled() :&lt;/li&gt;
&lt;li&gt;이외의 machers 메소드는 jest 공식문서 참고 : &lt;a href=&quot;https://jestjs.io/docs/using-matchers&quot;&gt;https://jestjs.io/docs/using-matchers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 machers 에서 테스트에 적절한 machers 매칭 메소드를 찾는거에서 생각이 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 많이쓰이는 toBe()는 `===` 처럼 값과 타입을 모두 비교하고, toEqual() 은 값만 비교한다. 이외에도 static 으로 판단하는 등 메소드가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;# 참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://vitest.dev/guide/why.html&quot;&gt;Vitest 공식문서&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jestjs.io/docs/using-matchers&quot;&gt;Jest machers 공식문서&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://careerly.co.kr/qnas/4968&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;테스트 코드 폴더구조 모듈 관리 글&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@wswy17/Vitest-Vite%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC#%EC%84%A4%EB%AA%85-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Vitest로 테스트하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@wswy17/Vitest-Vite%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC#%EC%84%A4%EB%AA%85-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; Vitest | Vite를 위한 테스트 프레임워크&lt;/a&gt;&lt;/p&gt;</description>
      <category>  FrontEnd</category>
      <category>TypeScript</category>
      <category>메서드</category>
      <category>타입스크립트</category>
      <category>테스트</category>
      <category>프론트엔드</category>
      <category>회고</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/223</guid>
      <comments>https://datdaradanadat.tistory.com/223#entry223comment</comments>
      <pubDate>Thu, 12 Dec 2024 15:16:05 +0900</pubDate>
    </item>
    <item>
      <title>[JS] Lv.2 예상 대진표</title>
      <link>https://datdaradanadat.tistory.com/222</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; 문제설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;△△ 게임대회가 개최되었습니다. 이 대회는 N명이 참가하고, 토너먼트 형식으로 진행됩니다. N명의 참가자는 각각 1부터 N번을 차례대로 배정받습니다. 그리고, 1번&amp;harr;2번, 3번&amp;harr;4번, ... , N-1번&amp;harr;N번의 참가자끼리 게임을 진행합니다. 각 게임에서 이긴 사람은 다음 라운드에 진출할 수 있습니다. 이때, 다음 라운드에 진출할 참가자의 번호는 다시 1번부터 N/2번을 차례대로 배정받습니다. 만약 1번&amp;harr;2번 끼리 겨루는 게임에서 2번이 승리했다면 다음 라운드에서 1번을 부여받고, 3번&amp;harr;4번에서 겨루는 게임에서 3번이 승리했다면 다음 라운드에서 2번을 부여받게 됩니다. 게임은 최종 한 명이 남을 때까지 진행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 처음 라운드에서 A번을 가진 참가자는 경쟁자로 생각하는 B번 참가자와 몇 번째 라운드에서 만나는지 궁금해졌습니다. 게임 참가자 수 N, 참가자 번호 A, 경쟁자 번호 B가 함수 solution의 매개변수로 주어질 때, 처음 라운드에서 A번을 가진 참가자는 경쟁자로 생각하는 B번 참가자와 몇 번째 라운드에서 만나는지 return 하는 solution 함수를 완성해 주세요.&amp;nbsp;&lt;b&gt;단, A번 참가자와 B번 참가자는 서로 붙게 되기 전까지 항상 이긴다고 가정합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;936&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dSFZxI/btsKYYKFcxW/o8MkM0wYK2wjQK0d08hpX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dSFZxI/btsKYYKFcxW/o8MkM0wYK2wjQK0d08hpX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dSFZxI/btsKYYKFcxW/o8MkM0wYK2wjQK0d08hpX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdSFZxI%2FbtsKYYKFcxW%2Fo8MkM0wYK2wjQK0d08hpX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1514&quot; height=&quot;936&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;936&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️나의 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토너먼트. 1~N번 의 사람이 짝지어서 게임 진행함. 승자만 진출함. 번호는 다시 배정된다(N/2) 이때 A,B 번이 몇번째 라운드에서 만나는지 return&amp;nbsp; &amp;gt; 이전까지는 항상 이긴다고 가정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A, B를 2로 나누고 반올림을 하면 다음 라운드의 번호가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.나눠서 반올림한 번호가 같아지면 만나기 때문에 조건으로 두고 반복문 돌린다&lt;/p&gt;
&lt;pre id=&quot;code_1702898217840&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function solution(n,a,b)
{
    var ans = 0;
    while (a !== b){
        a = Math.ceil(a/2);
        b = Math.ceil(b/2);
        ans++;
    }
    return ans;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;parseInt 로 하면 나누기의 나머지를 버린다. - 걍 삭제!&lt;/p&gt;</description>
      <category>JavaScript</category>
      <category>Lv.2</category>
      <category>자바스크립트</category>
      <category>프로그래머스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/222</guid>
      <comments>https://datdaradanadat.tistory.com/222#entry222comment</comments>
      <pubDate>Thu, 28 Nov 2024 16:59:11 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] 주소창에 주소를 입력했을 때의 흐름</title>
      <link>https://datdaradanadat.tistory.com/221</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYPn0p/btsKX8ra59X/9kpoQ0igvpFEtMhZDZlDQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYPn0p/btsKX8ra59X/9kpoQ0igvpFEtMhZDZlDQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYPn0p/btsKX8ra59X/9kpoQ0igvpFEtMhZDZlDQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYPn0p%2FbtsKX8ra59X%2F9kpoQ0igvpFEtMhZDZlDQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;888&quot; height=&quot;674&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;674&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 사용자가 웹 브라우저 주소창에 www.google.com을 입력한다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력어가 URL 이라면 해당 주소의 페이지를 요청하러 갈 것이고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 URL 이 아닌 키워드를 입력했다면 각 브라우저(크롬, 익스플로어, 파이어폭스 등)의 기본 검색엔진을 통해 해당 키워드를 검색 할 것이다.&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;이때 protocol 을 따로 입력하면 https나 http로 요청하겠지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 프로토콜 부분을 생략하고 도메인부분을 입력하게 되면 기본적으로 http로 요청한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. DNS 서버에 DNS 쿼리를 보낸다.&lt;/h2&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;웹 브라우저는 입력된 URL 을 해석하여 도메인 이름을 IP 주소로 변환하기 위해 요청을 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐싱된 DNS 기록을 통해 도메인 이름과 대응하는 IP 주소를 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://datdaradanadat.tistory.com/27#%E2%AD%90DNS-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;DNS Domain Name System&lt;/a&gt; : 도메인 네임 목록과 도메인 네임과 연결된 IP 주소 등의 리소스를 유지 및 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`www.google.com` 도메인 이름을 숫자 IP 주소 `142.250.206.196`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dns.google/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt;에서 검색하면 확인 가능 - DNS server 는 도메인 이름과 매칭되는 IP 주소를 가지고 있는 데이터 서버이다.&lt;/p&gt;
&lt;figure id=&quot;og_1732608053648&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;DNS - MDN Web Docs 용어 사전: 웹 용어 정의 | MDN&quot; data-og-description=&quot;도메인 네임 시스템(Domain Name System, DNS) 은 인터넷에 연결된 리소스에 대한 계층적이고 분산된 명명 시스템입니다. DNS는 도메인 네임 목록과 도메인 네임과 연결된 IP 주소 등의 리소스를 유지 및&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/ko/docs/Glossary/DNS&quot; data-og-url=&quot;https://developer.mozilla.org/ko/docs/Glossary/DNS&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dRnKMe/hyXDlL8iOV/dbKATOFvKKy5oFZDrd8I71/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Glossary/DNS&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/ko/docs/Glossary/DNS&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dRnKMe/hyXDlL8iOV/dbKATOFvKKy5oFZDrd8I71/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;DNS - MDN Web Docs 용어 사전: 웹 용어 정의 | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;도메인 네임 시스템(Domain Name System, DNS) 은 인터넷에 연결된 리소스에 대한 계층적이고 분산된 명명 시스템입니다. DNS는 도메인 네임 목록과 도메인 네임과 연결된 IP 주소 등의 리소스를 유지 및&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;499&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvogFF/btsKXAIxpbo/jtJej39rwwxZAvZFtJDU50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvogFF/btsKXAIxpbo/jtJej39rwwxZAvZFtJDU50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvogFF/btsKXAIxpbo/jtJej39rwwxZAvZFtJDU50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvogFF%2FbtsKXAIxpbo%2FjtJej39rwwxZAvZFtJDU50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;750&quot; height=&quot;499&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;499&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. DNS 서버에 도메인 이름을 전달한다. DNS query를 날린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 통신사(KT, SKT 등)의 DNS 서버에 도착해서 물어본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 만일 통신사의 DNS 서버에 없을 경우, 루트 DNS 서버로 넘어가서 물어본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.1. www.google.&lt;u&gt;&lt;b&gt;com&lt;/b&gt;&lt;/u&gt;&amp;nbsp; 의 .com 을 분류해서, com DNS 서버로 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.2. 최상위 도메인 (TLD) DNS 서버는 (kr, jp) 국가코드 or (.net, .com) 일반 최상위 도메인 코드로 나뉘기 때문에 이 기준으로 구분한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lVILS/btsKWNn7l6Q/fWT0l425F55aRbyQjSnCZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lVILS/btsKWNn7l6Q/fWT0l425F55aRbyQjSnCZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lVILS/btsKWNn7l6Q/fWT0l425F55aRbyQjSnCZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlVILS%2FbtsKWNn7l6Q%2FfWT0l425F55aRbyQjSnCZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;665&quot; height=&quot;350&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 루트 DNS 서버가 연결해준 COM DNS 서버로 와서 도메인 이름을 찾는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. COM&amp;nbsp;DNS&amp;nbsp;서버가&amp;nbsp;도메인&amp;nbsp;등록&amp;nbsp;정보를&amp;nbsp;가지고&amp;nbsp;있는&amp;nbsp;권한&amp;nbsp;있는&amp;nbsp;DNS&amp;nbsp;서버로&amp;nbsp;연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5.1. 가비아 DNS : 국내 도메인 호스팅을 담당하는 곳. COM DNS 서버에서 가비아 DNS 서버를 연결해줄 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 가비아 DNS 에 해당 도메인 이름을 가진 IP 주소를 가지고 있다. IP 주소를 반환한다. `142.250.206.196`&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. IP 주소로 웹서버에 TCP 3-way handshake 로 연결을 수립한다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 반환된 IP 주소, 즉 해당 서버에 TCP/IP 프로토콜을 사용하여 연결을 시도한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;676&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b00yt9/btsKWIf9eZZ/LEiNI9AtfqzMea4Yj0r5Lk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b00yt9/btsKWIf9eZZ/LEiNI9AtfqzMea4Yj0r5Lk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b00yt9/btsKWIf9eZZ/LEiNI9AtfqzMea4Yj0r5Lk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb00yt9%2FbtsKWIf9eZZ%2FLEiNI9AtfqzMea4Yj0r5Lk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1254&quot; height=&quot;676&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;676&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3-way handshake 를 이용해 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3-way &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Glossary/TCP_handshake&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;handshake&lt;/a&gt; : TCP 프로토콜을 사용하여 클라이언트와 서버 간의 연결을 설정하는 과정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. SYN (Synchronize): 클라이언트가 서버에 연결 요청을 보낸다. 이때 클라이언트는 SYN 플래그가 설정된 패킷을 전송하고, 이 패킷에는 클라이언트의 초기 시퀀스 번호(ISN)가 포함된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. SYN-ACK (Synchronize-Acknowledgment):&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.1. 서버는 클라이언트의 SYN 패킷을 수신하고, 연결 요청을 수락한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.2. 서버는 SYN-ACK 플래그가 설정된 패킷을 클라이언트에게 보낸다. 이 패킷에는 서버의 초기 시퀀스 번호(ISN)와 클라이언트의 SYN에 대한 확인 응답(Acknowledgment) 번호가 포함된다. 확인 응답 번호는 클라이언트의 ISN + 1로 설정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. ACK (Acknowledgment): 클라이언트는 서버의 SYN-ACK 패킷을 수신하고, 연결이 성공적으로 설정되었음을 확인하는 ACK 패킷을 서버에 보낸다. 이 ACK 패킷에는 서버의 ISN + 1이 포함된다.&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;4. 클라이언트는 웹서버로 HTTP 요청메시지를 통해 html 문서를 요청한다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http 요청메시지 형식&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mfu95/btsKWhQu5xg/hcPM3lcCifChX5INWPugKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mfu95/btsKWhQu5xg/hcPM3lcCifChX5INWPugKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mfu95/btsKWhQu5xg/hcPM3lcCifChX5INWPugKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmfu95%2FbtsKWhQu5xg%2FhcPM3lcCifChX5INWPugKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1174&quot; height=&quot;538&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;538&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;221&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/weLiM/btsKWKdY1Wm/pJMCfhFKFfKuSOXIMh6Nik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/weLiM/btsKWKdY1Wm/pJMCfhFKFfKuSOXIMh6Nik/img.png&quot; data-alt=&quot;예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/weLiM/btsKWKdY1Wm/pJMCfhFKFfKuSOXIMh6Nik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FweLiM%2FbtsKWKdY1Wm%2FpJMCfhFKFfKuSOXIMh6Nik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;656&quot; height=&quot;221&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;221&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP 통신을 통해서 `142.250.206.196`의 구글 서버에 GET 요청을 하게 된다.&lt;/p&gt;
&lt;h3 id=&quot;6-웹-어플리케이션서버was와-데이터베이스에서-우선-웹페이지-작업을-처리한다&quot; style=&quot;text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;웹 어플리케이션서버(WAS)와 데이터베이스에서 우선 웹페이지 작업을 처리 후 서버에 전송&lt;/h3&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;웹서버 혼자서 모든 로직을 수행하고 데이터를 관리할 수 있다면 간단하겠지만, 그렇게 될 경우 서버에 과부하가 일어날 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 서버의 일을 돕는 조력자 역할을 하는 것이 웹 어플리케이션이다&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;WAS는 사용자의 컴퓨터나 장치에 웹 어플리케이션을 수행해주는 미들웨어를 말한다.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;브라우저로부터 요청을 받으면, 웹서버는 페이지의 로직이나 데이터베이스의 연동을 위해서 WAS에게 이들의 처리를 요청한다. 그러면 WAS는 이 요청을 받아 동적인 페이지처리를 담당하고, DB에서 필요한 데이터 정보를 받아서 파일을 생성한다.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 웹 서버는 HTTP 응답 메시지를 보낸다.&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2054&quot; data-origin-height=&quot;1698&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Utdfc/btsKW44o1N5/kzxF11lXtlBm8clNkCFp10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Utdfc/btsKW44o1N5/kzxF11lXtlBm8clNkCFp10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Utdfc/btsKW44o1N5/kzxF11lXtlBm8clNkCFp10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUtdfc%2FbtsKW44o1N5%2FkzxF11lXtlBm8clNkCFp10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2054&quot; height=&quot;1698&quot; data-origin-width=&quot;2054&quot; data-origin-height=&quot;1698&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 페이지와 함께 HTTP 응답 메시지로 보내준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;status code로 서버요청에 따른 상태를 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://datdaradanadat.tistory.com/220#Response%20Status%20Code%20%EC%9D%91%EB%8B%B5%20%EC%83%81%ED%83%9C%20%EC%BD%94%EB%93%9C-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;상태코드 - 닷다라다나닷&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csARZX/btsKWhQvv1D/dQjWtKxpEpiuZxKltwwUYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csARZX/btsKWhQvv1D/dQjWtKxpEpiuZxKltwwUYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csARZX/btsKWhQvv1D/dQjWtKxpEpiuZxKltwwUYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsARZX%2FbtsKWhQvv1D%2FdQjWtKxpEpiuZxKltwwUYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;738&quot; height=&quot;320&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 웹 브라우저 출력&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 html 데이터는 웹 페이지 데이터로 변환되고, 웹 브라우저에 의해 출력된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d7b8v6/btsKVJteOIz/BXB7K8VE8WwmFW7edhaPz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d7b8v6/btsKVJteOIz/BXB7K8VE8WwmFW7edhaPz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d7b8v6/btsKVJteOIz/BXB7K8VE8WwmFW7edhaPz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd7b8v6%2FbtsKVJteOIz%2FBXB7K8VE8WwmFW7edhaPz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;850&quot; height=&quot;190&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. html 골격 렌더링, HTML 태그를 확인하고 이미지, CSS 스타일시트, 자바스크립트 파일 등과 같은 웹 페이지의 추가 요소에 대한 GET 요청을 보낸다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정적 파일(Static File)은 브라우저에서 캐싱되므로 다음에 페이지를 방문할 때 다시 가져올 필요가 없다. 단,&amp;nbsp;브라우저의&amp;nbsp;캐시&amp;nbsp;정책에&amp;nbsp;따라&amp;nbsp;다를&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.1 html 파싱해서 DOM 트리를 구축&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.2. CSS파일 링크를 찾아 CSS파일을 받아오고, CSS 오브젝트 모델을 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. DOM 트리와 CSS 오브젝트 모델을 통해 RenderTree 렌더 트리를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.렌더트리를 이용해서 각각의 노드들의 위치를 지정하는 레이아웃 과정을 거친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 화면 paint 그린다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;1010&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/P0OSr/btsKXIT4YUf/cM03d628Xgeocxl2DGY54K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/P0OSr/btsKXIT4YUf/cM03d628Xgeocxl2DGY54K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/P0OSr/btsKXIT4YUf/cM03d628Xgeocxl2DGY54K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FP0OSr%2FbtsKXIT4YUf%2FcM03d628Xgeocxl2DGY54K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1720&quot; height=&quot;1010&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;1010&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>  FrontEnd</category>
      <category>기술면접</category>
      <category>네트워크</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <category>프론트엔드</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/221</guid>
      <comments>https://datdaradanadat.tistory.com/221#entry221comment</comments>
      <pubDate>Tue, 26 Nov 2024 17:41:41 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] HTTP - Hypertext Transfer Protocol</title>
      <link>https://datdaradanadat.tistory.com/220</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OSI 7 계층 : 네트워크 통신을 위한 표준 프로토콜 구조 정의한다. 상위 계층일수록 사용자와 더 가까운 기능을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로토콜 : 컴퓨터 네트워크에서 데이터 통신을 위한 규칙이나 약속을 의미한다. 이를 통해 다른 시스템끼리 상호작용할 수 있고, 데이터가 정확하고 일관되게 전송될 수 있도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OSI 7 Layer의 응용-전송-세션-표현-애플리케이션 계층 중 애플리케이션에 통신에서 사용되는 프로토콜이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 종류의 프로토콜 중&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HTTP 는 웹페이지와 데이터를 전송하는 프로토콜 이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저와 서버 간에 데이터를 주고받기위한 방식으로 사용하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;비연결성 Connectionless : 클라이언트와 서버의 연결을 지속하지 않고, 각 요청마다 새로운 연결을 잇는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각각의 http 요청에 독립적으로 응답을 보낸 후 연결을 끊는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;무상태성 Stateless: 각 요청은 독립적이고, 이전 요청의 상태를 기억하지 않는다. 클라이언트의 상태를 서버는 저장할 필요가 없기때문이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이전 통신의 정보를 모르기 때문에, 매번 인증을 해줘야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;요청/ 응답 모델 : 클라이언트가 요청을 보내면 서버가 응답을 반환하는 구조&lt;/li&gt;
&lt;li&gt;메서드 : GET, POST, PUT, DELETE. 클라이언트가 서버에서 사용자 요청의 목적을 알리는 수단이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 는 평문 데이터를 전송하는 프로토콜이기 때문에, 중요한 정보를 주고받으면, 제 3자에 의해 조회될 위험이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPS : HTTP 에 암호화가 추가된 프로토콜이다. SSL을 적용한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSL : Secure Socket Layer. 인터넷을 통해 전달되는 정보를 보호하기 위해 개발한 통신 규약&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래는 TCP와 직접 통신했지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPS 에서 HTTP 는 SSL과 통신하고 &amp;gt; SSL이 TCP와 통신함 = 암호화, 증명서, 안전성 보호를 이용할 수 있게 된다.&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;무상태성 stateless&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 데이터 요청이 서로 독립적으로 관리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매 통신마다 필요한 모든 정보를 담아서 보내야한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&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;h4 data-ke-size=&quot;size20&quot;&gt;장점&lt;/h4&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;h4 data-ke-size=&quot;size20&quot;&gt;단점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트와 서버 간의 연속된 상태정보를 필요로 하는 경우에 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EX) 로그인 세션, 장바구니 등&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;쿠키 Cookie : 클라이언트 측에 상태정보를 저장한다. 요청에 서버에 같이 전송한다.&lt;/li&gt;
&lt;li&gt;세션 Session : 서버가 클라이언트의 상태 정보를 저장하는 방법이다. (세션 ID를 쿠키 형태로 송수신)&lt;/li&gt;
&lt;li&gt;토큰 기반 인증 : 서버로부터 인증토큰을 받아서 요청시에 같이 전송한다. RESTful API 에 자주 사용된다.&lt;/li&gt;
&lt;li&gt;로컬스토리지&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt;HTTP 버전&lt;/h2&gt;
&lt;h3 style=&quot; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;HTTP 0.9&lt;/h3&gt;
&lt;p style=&quot; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;초기 http 버전 - 1996&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;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;간단한 구조로 구현에 용이함.&lt;/p&gt;
&lt;p style=&quot; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;요청당 연결을 생성, 종료해야 하므로 오버헤드가 크고 성능이 떨어진다.&lt;/p&gt;
&lt;h3 style=&quot; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;HTTP 1.1&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;li&gt;캐시 제어 및 조건부 요청 기능 강화&lt;/li&gt;
&lt;li&gt;host 헤더가 필수로 추가되어 여러 도메인을 지원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;연결 재사용과 캐시기능으로 데이터 전송 속도가 향상되었다.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;TCP 사용으로 인해 대기 시간이 발생할 수 있다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;HTTP 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;/li&gt;
&lt;li&gt;헤더 압축: HPACK을 사용하여 헤더 크기를 줄인다.&lt;/li&gt;
&lt;li&gt;서버 푸시: 서버가 클라이언트 요청 없이 리소스를 미리 전송할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지연 시간 감소 및 대역폭 효율성 개선됨 - 성능향상&lt;/p&gt;
&lt;p style=&quot; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;초기 구현 복잡할 수 있음.&lt;/p&gt;
&lt;p style=&quot; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;구형 클라이언트와 서버에 호환성에 문제가 발생함.&lt;/p&gt;
&lt;h3 style=&quot; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;HTTP 3&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;QUIC 프로토콜 기반: UDP를 사용으로 더 빠른 연결 성능&lt;/li&gt;
&lt;li&gt;0-RTT 연결 설정: 초기 연결 시간을 단축&lt;/li&gt;
&lt;li&gt;패킷 손실 처리: 손실된 패킷만 재전송하여 성능을 개선한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;모바일 환경 및 불안정한 네트워크에서 더 나은 성능을 가진다.&lt;/p&gt;
&lt;p style=&quot; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;QUIC로 서버와 클라이언트의 지원이 필요하며, 초기 채택이 제한적이다.&lt;/p&gt;
&lt;h4 style=&quot; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;QUIC?&lt;/h4&gt;
&lt;p style=&quot; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Quick&amp;nbsp;UDP&amp;nbsp;Internet&amp;nbsp;Connections&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;google 개발, http 3의 기반이 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;TCP 대신 UDP 를 사용해 연결 설정, 데이터 전송한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;0-RTT 연결 설정 : 핸드쉐이크 없이 데이터 전송을 가능하게 한다.&lt;/li&gt;
&lt;li&gt;다중화&lt;/li&gt;
&lt;li&gt;내장된 암호화&lt;/li&gt;
&lt;li&gt;패킷 손실 복구&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;특성&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;HTTP 1.0&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;HTTP 1.1&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;HTTP 2&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;HTTP 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;출시 연도&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;1996&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;1999&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;2015&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;2020&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;연결 방식&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;비지속적&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;지속적&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;지속적&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;지속적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;요청/응답 처리&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;단일&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;다중&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;다중화 지원&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;다중화 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;헤더 처리&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;기본 헤더&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;캐시 및 조건부 요청 지원&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;헤더 압축 HPACK&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;헤더 압축 QUIC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;서버 푸시&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;없음&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;없음&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;지원&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;전송 프로토콜&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;TCP&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;TCP&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;TCP&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;QUIC (UDP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;성능&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;느림&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;개선됨&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;크게 향상됨&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;매우 향상됨&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;URL&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에 자원을 요청하기 위해 입력하는 영문주소&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;707&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyCkZg/btsKNWztwg1/KVVFIUKrPNn98yTpY1RES0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyCkZg/btsKNWztwg1/KVVFIUKrPNn98yTpY1RES0/img.png&quot; data-alt=&quot;출처 : https://velog.io/@simoniful&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyCkZg/btsKNWztwg1/KVVFIUKrPNn98yTpY1RES0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyCkZg%2FbtsKNWztwg1%2FKVVFIUKrPNn98yTpY1RES0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;707&quot; height=&quot;168&quot; data-origin-width=&quot;707&quot; data-origin-height=&quot;168&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://velog.io/@simoniful&lt;/figcaption&gt;
&lt;/figure&gt;
&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;Request 요청&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트에서 서버에 데이터 처리에 필요한 요청을 보내는 메시지&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구조&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;라인 Line&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 HTTP Request Methods : 요청의 종류&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET 조회 : 존재하는 데이터 요청
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터가 헤더 URL 에 노출되므로 보안적으로 중요한 데이터는 전달하면 안된다.&lt;/li&gt;
&lt;li&gt;EX) http://localhost:8080/boardList?name=제목&amp;amp;contents=내용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;POST 생성 : 새로운 데이터 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터는 바디에 추가된다. URL 에 데이터가 노출되지 않아 GET 보다 안전하다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;EX) http://localhost:8080/addBoard&lt;/li&gt;
&lt;li&gt;PUT, DELETE 동작도 수행 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;PUT 변경 : 존재하는 특정 데이터 업데이트. 해당 데이터가 없으면 추가 생성된다.&lt;/li&gt;
&lt;li&gt;PATCH 수정 : 존재하는 특정 데이터 일부 업데이트&lt;/li&gt;
&lt;li&gt;DELETE 삭제 : 존재하는 데이터 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URI(Uniform Resource Identifier): 요청할 리소스의 위치를 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP&amp;nbsp;버전:&amp;nbsp;사용하는&amp;nbsp;HTTP&amp;nbsp;프로토콜의&amp;nbsp;버전을&amp;nbsp;명시한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Method URI HTTP버전&lt;br /&gt;GET /index.html HTTP/1.1&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;헤더 Headers&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`키: 값` 형태로 구성.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Content-Type : 요청 데이터 형식을 정의. `type/subtype` 형식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Authorization : 클라이언트가 서버에 인증 정보를 제공하기 위해 사용된다. 보안이 필요한 API 요청에서 사용된다. 인증방법과 인증정보를 포함한다. EX) Basic Authentication, Bearer Token&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Headers: {&lt;br /&gt;&amp;nbsp; &amp;nbsp; Host: 요청을 보내는 목표(타겟)의 주소. 즉, 요청을 보내는 웹사이트의 기본 주소(ex. www.apple.co.kr)&lt;br /&gt;&amp;nbsp; &amp;nbsp; User-Agent: 요청을 보내는 클라이언트의 대한 정보 (ex. chrome, firefox, safari, explorer)&lt;br /&gt;&amp;nbsp; &amp;nbsp; Content-Type: 해당 요청이 보내는 메세지 body의 타입 (ex. application/json)&lt;br /&gt;&amp;nbsp; &amp;nbsp; Content-Length: body 내용의 길이&lt;br /&gt;&amp;nbsp; &amp;nbsp; Authorization: 회원의 인증/인가를 처리하기 위해 로그인 토큰&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등 이 있음.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;본문 Body&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택적 요소. JSON, XML 등 형식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;post 에서 데이터를 같이 보낼때 사용된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;{ &lt;br /&gt;&amp;nbsp; &amp;nbsp; &quot;username&quot;: &quot;example&quot;,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &quot;password&quot;: &quot;12345&quot;&lt;br /&gt;}&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Response 응답&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트의 요청에 대한 결과를 서버가 전달하는 역할을 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구조&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;라인 Line&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 버전: 사용하는 HTTP 프로토콜의 버전&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 코드(Status Code): 요청의 처리 결과를 나타내는 숫자 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 메시지(Status Message): 상태 코드를 설명하는 짧은 메시지&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;HTTP버전 상태코드 상태메시지&lt;br /&gt;HTTP/1.1 200 OK&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;헤더 Headers&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`키: 값` 형태로 구성.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 헤더와 같음. 응답은 추가정보(메타데이터)를 담고있는 부분이 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;본문 Body&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스의 실제 내용을 포함한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML 문서, JSON 데이터 등의 형식&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;Response Status Code 응답 상태 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm 에서 쉽게 확인할 수 있는 패키지가 있다&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://www.npmjs.com/package/http-status-code&quot;&gt;http-status-code&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 통신 시 정상적으로 응답이 되었는지, 안되었는지 파악할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청을 처기한 결과를 3자리 숫자로 나타낸다. 정보응답, 성공, 리다이렉션, 클라이언트 오류, 서버 오류 등의 상태가 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;정보 응답 (1xx)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;100 Continue: 클라이언트가 요청을 계속 진행해도 좋다는 의미&lt;/li&gt;
&lt;li&gt;101 Switching Protocols: 클라이언트가 요청한 프로토콜로 전환할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;성공 (2xx)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;200 OK: 요청이 성공적으로 처리되었음&lt;/li&gt;
&lt;li&gt;201 Created: 요청이 성공적으로 처리되었고, 새로운 리소스가 생성되었음&lt;/li&gt;
&lt;li&gt;202 Accepted: 요청이 수락되었으나, 아직 처리되지 않았음.&lt;/li&gt;
&lt;li&gt;204 No Content: 요청이 성공하였으나, 반환할 내용이 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;리다이렉션 (3xx)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;300 Multiple Choices: 요청한 리소스에 여러 개의 선택이 있음&lt;/li&gt;
&lt;li&gt;301 Moved Permanently: 요청한 리소스가 영구적으로 다른 URI로 이동했음&lt;/li&gt;
&lt;li&gt;302 Found: 요청한 리소스가 임시적으로 다른 URI에 위치하고 있음&lt;/li&gt;
&lt;li&gt;304 Not Modified: 클라이언트가 캐시한 리소스가 수정되지 않았음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;클라이언트 오류 (4xx)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;400 Bad Request: 서버가 요청을 이해할 수 없음. 잘못된 구문이 포함된 경우.&lt;/li&gt;
&lt;li&gt;401 Unauthorized: 요청한 리소스에 접근하기 위한 인증이 필요함.&lt;/li&gt;
&lt;li&gt;403 Forbidden: 요청이 이해되었으나, 서버가 요청을 거부했음&lt;/li&gt;
&lt;li&gt;404 Not Found: 요청한 리소스를 찾을 수 없음&lt;/li&gt;
&lt;li&gt;409 Conflict: 요청이 서버의 현재 상태와 충돌할 때 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;서버 오류 (5xx)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;500 Internal Server Error: 서버에서 요청을 처리하는 동안 예기치 않은 오류가 발생했음&lt;/li&gt;
&lt;li&gt;501 Not Implemented: 서버가 요청된 기능을 지원하지 않음&lt;/li&gt;
&lt;li&gt;502 Bad Gateway: 서버가 게이트웨이 또는 프록시 역할을 할 때, 상위 서버로부터 잘못된 응답을 받았음&lt;/li&gt;
&lt;li&gt;503 Service Unavailable: 서버가 현재 요청을 처리할 수 없는 상태임을 의미함. (예: 유지보수 중)&lt;/li&gt;
&lt;li&gt;504 Gateway Timeout: 서버가 상위 서버로부터 적시에 응답을 받지 못했음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;웹 동작 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://www.naver.com&quot;&gt;www.naver.com&amp;nbsp;&lt;/a&gt; 에 접속할때 생기는 네트워크 과정&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JBzyd/btsKPHHnBPT/Uo4rcrIxI5DC2ueXXMFTx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JBzyd/btsKPHHnBPT/Uo4rcrIxI5DC2ueXXMFTx0/img.png&quot; data-alt=&quot;출처 : https://mangkyu.tistory.com/91&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JBzyd/btsKPHHnBPT/Uo4rcrIxI5DC2ueXXMFTx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJBzyd%2FbtsKPHHnBPT%2FUo4rcrIxI5DC2ueXXMFTx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;888&quot; height=&quot;674&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;674&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://mangkyu.tistory.com/91&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 브라우저에 URL 을 입력한다.&lt;/li&gt;
&lt;li&gt;DNS 서버에 도메인 네임으로 서버의 진짜 주소를 찾는다.&lt;/li&gt;
&lt;li&gt;IP 주소로 웹서버에 TCP 3 handshake 로 연결 수립한다.&lt;/li&gt;
&lt;li&gt;클라이언트는 웹 서버로 HTTP 요청 메시지를 보낸다.&lt;/li&gt;
&lt;li&gt;웹서버는 HTTP 응답 메시지를 보낸다.&lt;/li&gt;
&lt;li&gt;도착한 HTTP 응답 메시지는 웹 페이지 데이터로 변환되고, 웹 브라우저에 의해 출력된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  FrontEnd</category>
      <category>JavaScript</category>
      <category>기술면접</category>
      <category>오블완</category>
      <category>자바스크립트</category>
      <category>컴퓨터네트워크</category>
      <category>티스토리챌린지</category>
      <category>프론트엔드</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/220</guid>
      <comments>https://datdaradanadat.tistory.com/220#entry220comment</comments>
      <pubDate>Tue, 19 Nov 2024 20:05:43 +0900</pubDate>
    </item>
    <item>
      <title>[브라우저] 브라우저 저장소 차이점 LocalStorage, Session Storage, Cookie</title>
      <link>https://datdaradanadat.tistory.com/219</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;로그인을 구현해본 개발자라면 경험해 본적 있을것!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Web Storage 웹스토리지&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;키/값을 쌍으로 데이터를 저장하고, 키를 기반으로 데이터를 조회하는 패턴이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에 저장해야하는 데이터, 즉 클라이언트에서 수시로 변경되는 데이터를 웹 스토리지에 저장하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 를 사용하지 않고, 데이터를 임시적으로 저장할때 사용하는 곳이라고 이해하면 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;용량의 제한이 없다&lt;/li&gt;
&lt;li&gt;한번 저장하면 영구적으로 존재한다.&lt;/li&gt;
&lt;li&gt;보안적인 문제가 될 수 있다.&lt;/li&gt;
&lt;li&gt;LocalStorage, Session Storage 가 존재하며, 데이터의 지속성에 따라 제공된다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;탭/창 마다 공유할 데이터는 localStorage에, 개별데이터는 SessionStorage 에 저장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EX) 글 임시저장, 쇼핑몰 장바구니, 좋아요 콘텐츠 등&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;gt; Application 어플리케이션 &amp;gt; 좌측 Storage 저장용량 탭&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에서 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1202&quot; data-origin-height=&quot;1408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qd8hn/btsKGzXHjC9/cLESFWb3Phq14altsyIynK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qd8hn/btsKGzXHjC9/cLESFWb3Phq14altsyIynK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qd8hn/btsKGzXHjC9/cLESFWb3Phq14altsyIynK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqd8hn%2FbtsKGzXHjC9%2FcLESFWb3Phq14altsyIynK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1202&quot; height=&quot;1408&quot; data-origin-width=&quot;1202&quot; data-origin-height=&quot;1408&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LocalStorage 로컬스토리지&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;li&gt;도메인마다 별도의 로컬스토리지가 생성된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동일한 Origin을 가진 프로토콜, 도메인, 포트를 공유하는 모든 탭/ 윈도우에 전역적으로 공유 가능하다.&lt;/li&gt;
&lt;li&gt;http, https 는 다른 스토리지로 구분됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;문자열, JS object 객체 - 저장 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;삭제???&lt;/b&gt; removeItem 또는 개발자도구에서   표시를 누르면 지워진다. 즉, 직접 지워줘야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 브라우저나 운영체제에 의해 자동으로 지워지는 경우가 있다고 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1731410905211&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;localStorage.setItem(&quot;accessToken&quot;, &quot;datdaradanadat&quot;);	// 저장 (키, 값)
localStorage.getItem(&quot;accessToken&quot;);			// datdaradanadat
localStorage.removeItem(&quot;accessToken&quot;);			// accessToken 키 삭제
localStorage.clear();							// 전체 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EX) 개인화 : 사용자 선호, 테마 등의 세팅, 사용자 정보&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점 : 용량이 커서 (5~10MB), 큰 데이터도 저장가능&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;HTML5 이전 브라우저는 작동하지 않는다.&lt;/li&gt;
&lt;li&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;/ul&gt;
&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;HTTPS를 통한 데이터 전송&lt;/li&gt;
&lt;li&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SessionStorage&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션을 종료할때 클라이언트에 대한 정보가 삭제된다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션 session
&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;&amp;nbsp;세션 스토리지의 세션 : &lt;b&gt;가장 작은 단위인 탭단위&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;현재 페이지가 브라우징 되고있는 브라우저 컨텍스트 내에서만 데이터가 유지된다.&lt;/li&gt;
&lt;li&gt;즉, 탭/창 각각 별개의 영역으로 구분하고, 서로 침범할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;탭/창 을 닫으면 (페이지 세션 끝날때) 데이터가 삭제된다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;즉, 휘발성 데이터를 저장할대 사용한다.&lt;/li&gt;
&lt;li&gt;동일한 Origin을 가진 프로토콜, 도메인, 포트로 구분한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;문자열, JS object 객체 - 저장 가능하다.&lt;/li&gt;
&lt;li&gt;cookie&amp;nbsp;문제점 - '도메인이 같으면 항상 쿠키를 보낸다'는 조건을 해결한 것.
&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EX) 일회성 로그인, 입력 폼 저장 등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731415698846&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sessionStorage.setItem(&quot;accessToken&quot;,&quot;datdaradanadat&quot;);	// 저장 (키, 값)
sessionStorage.getItem(&quot;accessToken&quot;);			// datdaradanadat
sessionStorage.removeItem(&quot;accessToken&quot;);
sessionStorage.clear( );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점 : 용량이 커서 (5~10MB), 큰 데이터도 저장가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점 : HTML5 이전 브라우저는 작동하지 않는다.&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;HTTP Cookie 쿠키&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 쿠키 : 서버가 사용자의 웹 브라우저에 전송하는 작은 데이터 조각 (4KB)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;클라이언트에 대한 정보를 사용자 PC의 하드디스크에 보관하기 위해서&lt;br /&gt;웹 사이트(서버)에서 클라이언트의 웹 브라우저에 전송하는 정보&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 쿠키를 저장해 두었다가, 동일한 서버에 HTTP 재요청 시 저장된 데이터를 함께 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠키는 두 요청이 동일한 클라이언트에서 들어왔는지 아닌지를 판단할때 사용한다. 이를통해 로그인 상태를 유지할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 프로토콜은 상태가 없는데 stateless, 정보를 기억시켜주기 때문이다.&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;서버, 클라이언트 양쪽에서 쿠키 데이터를 사용하는 api 가 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 req, res 둘다 모든 쿠키를 넘겨야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 별로 데이터가 분리되지만, 같은 브라우저라면 값을 공유했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변화를 감지할 수 없다. (웹스토리지는 Storage event가 실행돼 변화를 감지하고 갱신해 반영한다.)&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;Secure 쿠키는 HTTPS 프로토콜 상에서 암호화된 요청일 경우에만 전송된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 지워져도 되거나, 조작되거나 가로채이더라도 큰 지장이 없는 수준의 정보들만을 보관하도록 되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 정보를 저장할 때는 반드시 암호화를 적용하고, HttpOnly, Secure와 같은 속성을 사용하여 보안을 강화해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;LifeTime&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 종료되면 자동으로 삭제된다. (sessionStorage가 쿠키의 이 점을 도입하였음)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션 쿠키 (Expires, Max-Age 속성이 없는 쿠키) : 현재 세션이 끝날 때 삭제된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저는 &quot;현재 세션&quot;이 끝나는 시점을 정의하여, 재시작할 때 세션을 복원해 세션 쿠키가 무기한 존재할 수 있도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;영속적인 쿠키 :&amp;nbsp;Expires&amp;nbsp;속성에 명시된 날짜에 삭제되거나,&amp;nbsp;Max-Age&amp;nbsp;속성에 명시된 기간 이후에 삭제된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1731418524266&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Set-Cookie: id=[Cookie-Id]; Expires=Wed, 21 Oct 2024 07:28:00 GMT;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버의 시간이 아니라 쿠키가 저장되는 클라이언트의 시간 기준&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;사용&lt;/h4&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;광고 7일동안 보지 않기 - 만료일, 지속시간 duration 기능(웹스토리지에 없음)&lt;/li&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;트래킹 Tracking
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 행동 기록 및 분석&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpOnly 플래그가 설정되지 않은 경우, 기본 쿠키들은 javascript로부터 읽을 수 잇다.&lt;/p&gt;
&lt;pre id=&quot;code_1731418959239&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;document.cookie = &quot;dah_cookie=datdaradanadat&quot;; //새로운 쿠키 생성
document.cookie = &quot;apple_cookie=sweetapple&quot;;
console.log(document.cookie);
// logs &quot;dah_cookie=datdaradanadat; pple_cookie=sweetapple&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모은 요청마다 쿠키가 함게 전송되기 때문에 성능이 떨어지는 원인이 될수도 있음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;mobile data connecions 성능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Cookie 대신 LocalStorage&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML5 이전에는 쿠키를 썻는데, 이후에 로컬스토리지로 많이 변경되었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 HTTP 요청에서 데이터를 주고받을 필요가 없다.&lt;/li&gt;
&lt;li&gt;전체 트래픽, 낭비되는 대역폭의 양 줄일 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대역폭 Bandwidth : 특정&amp;nbsp;시간&amp;nbsp;동안&amp;nbsp;네트워크를&amp;nbsp;통해&amp;nbsp;전송될&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;최대&amp;nbsp;데이터의&amp;nbsp;양&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠기의 용량은 4KB 이기 때문에, 많이 대체되었음.&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;WebStorage vs Cookie 차이점&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 138px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;WebStorage&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;Cookie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;제한&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;용량제한&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;용량제한, 시간제한, 갯수제한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;시간제한 설정&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;불가능&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;데이터 형식&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;문자열, JS object&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;문자열&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;데이터 전송&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;선택, 가공해서 전송가능&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;모든 쿠키 전송. 가공시 side effect 존재함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;세션 정의?&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;같은 오리진이라도, (sessionStorage) 다른탭이면 다른 세션이라고 정의&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;같은 오리진이면, 다른 탭/창 일지라도, 같은 세션이라고 정의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;이벤트&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;변경 감지 event&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;변경 감지 불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;‼️ 키포인트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스토리지는 보안에 취약하기 때문에 중요한 정보를 저장하는 것은 안된다. 일단 데이터는 암호화 되는것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 쿠키는 웹스토리지보다 보안에 취약하다. 통신과정에서 가로채기 쉬움&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HttpOnly, Secure 와 같은 속성을 사용해 보안을 강화할 수 있다.&lt;/li&gt;
&lt;li&gt;당연히 중요한 정보는 암호화하여 보관한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹스토리지의 객체 저장은 json.stringity 를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;로그인 방식&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿠키 : 모든 http 통신에 로그인정보가 포함되어있어야 하기 때문에 쿠키에 사용자 정보를 담는것이 효율적이라는 의견&lt;/li&gt;
&lt;li&gt;로컬스토리지 : Instance 하나로 묶어서 데이터를 통신할 수 있다. 그러나 사용자 정보가 영구적으로 보관되는건 위험하기도 하고, 보안 강화하는 플래그가 부족하다.&lt;/li&gt;
&lt;li&gt;세션스토리지
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 도메인을 두개의 창을 열고, 하나의 창에서 로그인했을 경우&lt;/li&gt;
&lt;li&gt;다른 창에도 로그인 상태가 되어야 한다.&lt;/li&gt;
&lt;li&gt;세션스토리지는 다른 탭으로 구분되어, 로그인 상태가 공유되지 않는다.&lt;/li&gt;
&lt;li&gt;로그인에서는 사용불가!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션 기반 인증 시스템
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션 id 는 쿠키 헤더에 담아서 오기 때문에, 기본적으로 쿠키에서 관리하는것이 적절하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;토큰 기반 인증 시스템
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;accessToken : 암호화된 사용자 정보를 가지고 있기 때문에, 스토리지에 저장하는 것은 보안적으로 위험하다. 전역상태관리로 메모리에 저장하자!&lt;/li&gt;
&lt;li&gt;refreshToken : 사용자 정보를 직접 가지고 있지 않고, accessToken을 암호화를 풀 키 값이다. 이 토큰을 탈취된다고 한들, 사용자 정보가 탈취되는것은 아님! 그나마 중요도가 낮음.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버와 통신시, refreshToken을 통해 사용자 인증하자!&lt;/li&gt;
&lt;li&gt;쿠키의 `HttpOnly`, `Secure`, `SameSite`, `anti-CSRFtokens` 옵션으로 보안을 강화하자!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;결론! accessToken은 메모리, refreshToekn은 쿠키에 저장하자!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  FrontEnd</category>
      <category>JavaScript</category>
      <category>기술면접</category>
      <category>오블완</category>
      <category>자바스크립트</category>
      <category>토큰</category>
      <category>티스토리챌린지</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/219</guid>
      <comments>https://datdaradanadat.tistory.com/219#entry219comment</comments>
      <pubDate>Tue, 12 Nov 2024 22:48:44 +0900</pubDate>
    </item>
    <item>
      <title>[브라우저] CSR, SSR</title>
      <link>https://datdaradanadat.tistory.com/218</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1730700067957&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;SSR vs CSR 로 프로젝트 구성하기&quot; data-og-description=&quot;SPA vs MPASPA : Single Page Application하나의 페이지 내에서 동적으로 콘텐츠를 로드하고 업로드 하는 방식이다.리액트, 앵귤러, 뷰와 같은 프레임워크가 개발하는 웹 어플리케이션ex. 클릭 시 고정되어&quot; data-og-host=&quot;datdaradanadat.tistory.com&quot; data-og-source-url=&quot;https://datdaradanadat.tistory.com/147&quot; data-og-url=&quot;https://datdaradanadat.tistory.com/147&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cdaJyz/hyXsYJWbV3/ekMIHdfZV4mWbu5WWPAiq0/img.png?width=694&amp;amp;height=831&amp;amp;face=0_0_694_831,https://scrap.kakaocdn.net/dn/XcmQH/hyXs4XGDXx/34DHvhKKnJqjPQRkmOrG2k/img.png?width=694&amp;amp;height=831&amp;amp;face=0_0_694_831,https://scrap.kakaocdn.net/dn/tRYsq/hyXs4QVsPo/IBbLkRuoBiKj0KtvEuL3C1/img.png?width=1816&amp;amp;height=964&amp;amp;face=0_0_1816_964&quot;&gt;&lt;a href=&quot;https://datdaradanadat.tistory.com/147&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://datdaradanadat.tistory.com/147&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cdaJyz/hyXsYJWbV3/ekMIHdfZV4mWbu5WWPAiq0/img.png?width=694&amp;amp;height=831&amp;amp;face=0_0_694_831,https://scrap.kakaocdn.net/dn/XcmQH/hyXs4XGDXx/34DHvhKKnJqjPQRkmOrG2k/img.png?width=694&amp;amp;height=831&amp;amp;face=0_0_694_831,https://scrap.kakaocdn.net/dn/tRYsq/hyXs4QVsPo/IBbLkRuoBiKj0KtvEuL3C1/img.png?width=1816&amp;amp;height=964&amp;amp;face=0_0_1816_964');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;SSR vs CSR 로 프로젝트 구성하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SPA vs MPASPA : Single Page Application하나의 페이지 내에서 동적으로 콘텐츠를 로드하고 업로드 하는 방식이다.리액트, 앵귤러, 뷰와 같은 프레임워크가 개발하는 웹 어플리케이션ex. 클릭 시 고정되어&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;datdaradanadat.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기술의 선택과 적용에 고민을 하며 작성한 개발글&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론과 기술면접에 대비해 좀 더 깊게 작성해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSR, CSR 은 렌더링 하는 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPA, MPA 는 페이지 구성된 어플리케이션 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식은 `CSR - SPA` `SSR - MPA` 로 많이 사용되고, React 의 경우 SPA 를 기본적으로 사용한다고 알고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 서버측 html 적용 등(node.js...) 추가적인 세팅을 통해 구현하면 SPA - SSR 로 구현할 수 있다.&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;즉 React 등 JS 기반 라이브러리는 HTML, CSS, JS 파일이 하나씩 나오기 때문에 SPA-CSR 이 되지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSR 로 구현하면 별도 렌더링으로 동작하기 때문에 MPA 가 될 수 있다.&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;렌더링&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저의 화면에 내용을 표시하는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트게 접근하면, 서버에 요청을 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면에 그려지는건 HTML 인데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`SSR` 서버가 렌더링 할 경우, HTML, View 등의 리소스를 해석하고, 렌더링해서 클라이언트에게 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`CSR` 클라이언트가 렌더링 할 경우, 서버는 리소스를 제공하고, 클라이언트가 해설하여 렌더링한다.&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;CSR (Client-side Rendering) - SPA (Single Page Application)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSR : 클라이언트에서 직접 페이지를 렌더링 하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPA : 하나의 단일 페이지로 구성된 어플리케이션&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;클라이언트가 첫 접속 시에 서버에 HTML, JS, CSS 리소스 파일을 서버에 요청하고, 모두다 받아온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 자바스크립트를 사용해 사용자의 상호작용에 따라 동적으로 페이지를 렌더링한다. - 서버와 통신 json&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 이후에는 리로딩 없이, 필요한 부분만 서버와 json 으로 통신해 화면을 갱신한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 HTML 에서 JS 로 동적으로 갱신되는 UI이다. interactive&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 페이지는 이동되지 않지만, 사용자는 이동되는 것처럼 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 측에 동적 dom 을 생성해서 동작하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;렌더링 과정&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 클라이언트가 url 에 접속하면, 서버에 http req 를 통해서 html 을 다운로드 한다.&lt;br /&gt;2. 첫 로드 html 은 빈페이지 이지만, JS CSS 파일을 불러오는 링크가 포함되어있고, 링크를 통해 다운로드 한다.&lt;br /&gt;3. 브라우저가 JS 코드를 실행하고, 이 안에서 React 가 구동되며 VirtualDOM 에 콘텐츠를 렌더링한다.&lt;br /&gt;4. VirtualDOM이 구성되면, 이를 브라우저의 DOM(root 하위)에 붙인다.&lt;br /&gt;5. 브라우저의 렌더링 트리 구성, 페인즈, 플로우 등의 과정을 거쳐 페이지를 그리고&lt;br /&gt;6. 브라우저에 페이지가 나타난다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장단점&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장점 : &lt;span style=&quot;text-align: left;&quot;&gt;첫 접속으로 페이지를 만든 이후에는,&amp;nbsp;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수시로 서버에 요청할 필요가 없으므로, 서버의 부담을 줄여준다.&lt;/li&gt;
&lt;li&gt;동적으로 빠르게 렌더링 된다.&lt;/li&gt;
&lt;li&gt;중복 리소스 요청이 없다.&lt;/li&gt;
&lt;li&gt;데이터가 오프라인에서도 사용할 수 있어서 로컬테이터를 캐시(cache) 하기 유리하다.&lt;/li&gt;
&lt;li&gt;컴포넌트 개발에 용이하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;단점 : 첫 접속으로 초기 렌더링의 경우에,&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;`webpack의 codesplitting`으로 해결하는 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SEO 에 좋지 않다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Search&amp;nbsp;Engine&amp;nbsp;Optimization&amp;nbsp;:&amp;nbsp;검색엔진&amp;nbsp;최적화에&amp;nbsp;불리하다.&lt;/li&gt;
&lt;li&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;자바스크립트 엔진 내장 크롤러 : 구글&lt;/li&gt;
&lt;li&gt;이외의 대부분은 내장되어있지않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;heaer 메타데이터의 확정&lt;/li&gt;
&lt;li&gt;보안이슈 : 사용자 정보를 세션으로 관리 불가능, `localstroage` `Cookie`로 저장해야 하는데, XSS 공격에 취약하다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CSR, SSR 혼합으로 해결한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SSR Server-side Rendering - MPA Multiple Page Application&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSR : 서버에서 페이지를 렌더링해서 클라이언트에 전달하는 방식이다.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MPA : 여러개&amp;nbsp;페이지로 구성된 어플리케이션&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다른 페이지로 이동할 때마다, 서버에 새로운 페이지를 요청하고&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉시 서버연산을 통해 렌더링하고, 완성된 페이지 형태 HTML 을 응답한다.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;렌더링 과정&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 클라이언트가 url 에 접속하면, 서버에 http req 를 보낸다.&lt;br /&gt;2. 서버는 요청 즉시 페이지를 만든다.&lt;br /&gt;2-1. Data Fetching (API call, Hydrate 등)을 미리 해서, 빈 페이지가 아닌, 초기 콘텐츠가 로딩된 페이지를 만든다.&lt;br /&gt;2-2. 브라우저는 이 HTML 페이지를 받아 페이지 DOM 에 그린다. - 페이지가 사용자에게 보인다.&lt;br /&gt;3. 브라우저가 페이지를 그리면서, 동시에 태그 등을 통해 JS, CSS 파일 등을 로드한다.&lt;br /&gt;4. 로딩된 JS 를 실행한다.&lt;br /&gt;5. Interactive 한 페이지 구성이 완료된다.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장단점&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장점&lt;/h4&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;SEO 가능하다
&lt;ul style=&quot;list-style-type: disc; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Search Engine Optimization : 검색엔진 최적화&lt;/li&gt;
&lt;li&gt;JS 엔진이 없는 검색로봇이 크롤링하기 적합하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;단점&lt;/h4&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;새로고침, 페이지 이동 시 화면이 깜빡거린다 (UX) &amp;gt; HTML `href` 태그를 통해 페이지 이동&lt;/li&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JS 파일이 로드되기 전까지는 인터렉션 되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기술 적용&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;822&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Aw6zR/btsKuHCzdpP/jIGYoh1EulpIPiQMgcR5Ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Aw6zR/btsKuHCzdpP/jIGYoh1EulpIPiQMgcR5Ek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Aw6zR/btsKuHCzdpP/jIGYoh1EulpIPiQMgcR5Ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAw6zR%2FbtsKuHCzdpP%2FjIGYoh1EulpIPiQMgcR5Ek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;822&quot; height=&quot;502&quot; data-origin-width=&quot;822&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. SSR 용 페이지 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React(SPA) 의 CSR 동작의 단점을 해결하기 위해, API 통신이 아닌, 실제 데이터를 포함한채로 렌더링되는 SSR 용 페이지를 별도를 만들어서 사용한다. 현재는 서버가 아닌 프론트엔드에서 Next.js, Nust.js, Node.js 를 이용해서 만들게 되고 백엔드와는 API 로만 통신된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 서버측 코드로 프론트에서 페이지를 개발한다. 개발자원을 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js 의 경우는 SPA-CSR 환경에서 SEO 에 유리하기 위해 SSR 을 도입하였고, 바벨 웹팩의 툴로 환경설정 할 수있도록 지원해주어 React 개발자들이 많이 사용한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Next.js 작동 과정&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. SSR 방식&lt;br /&gt;1-1. 클라이언트가 처음 페이지에 접속했을때, 서버는 렌더링 될 HTML 값을 응답한다.&lt;br /&gt;2. 브라우즈는 추가적으로 JS 번들을 다운로드 받아 실행한다.&lt;br /&gt;3. CSR 방식&lt;br /&gt;3-1. 사용자가 다른 페이지로 이동할 때에는 서버가 아닌, 클라이언트에서 처리하여 이동한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. ReactDOM - hydration&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 SSR과의 차이점은 hydration이라는 개념을 통해 서버에서 미리 렌더링된 HTML에 React가 생명을 불어넣어 주는 것이다. 리액트 컴포넌트를 렌더링 하기 위해 빈 DOM 을 사용하는 대신, 모든 컴포넌트가 HTML로 렌더링 된 이미 빌드된 DOM 을 사용한다.&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;렌더링 과정에서 컴포넌트들을 렌더링하고, 이벤트 핸들러를 연결하는 JS 를 동작하게하는 이 과정을 `hydration` `하이드레이션`이라고 한다. 하이드레이션 후 앱은 이벤트가 동작할 수 있게된다. interactive&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하이드레이션 Hydration 과정&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. SSR 로 서버에서 렌더링 된 html 전송&lt;br /&gt;2. 클라이언트에서 JS 번들 다운로드&lt;br /&gt;3. Hydration 과정&lt;br /&gt;3.1. JS 실행되면 서버 렌더링 된 html 을 기반으로 Virtual Dom 가상돔을 생성한다.&lt;br /&gt;3.2. 리액트는 기존 html 요서와 가상돔을 비교하여 어떤 부분이 동적으로 업데이트 되어야 하는지 판단한다.&lt;br /&gt;4. 리액트는 각 html 요소에 대한 이벤트 핸들러를 연결한다. 인터랙티브&lt;br /&gt;5. CSR 로 페이지 동적 업데이트&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기존 리액트 동작 방식&lt;/h4&gt;
&lt;pre id=&quot;code_1730706952952&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const root = document.querySelector(&quot;#root&quot;);
ReactDOM.render(&amp;lt;App name=&quot;Saeloun&quot; /&amp;gt;, root);

//전송 html
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSR 방식으로 hydrate 설정된 동작 방식&lt;/p&gt;
&lt;pre id=&quot;code_1730706989581&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// index.js
ReactDOM.hydrate(&amp;lt;App name=&quot;Saeloun&quot;/&amp;gt;, document.getElementById('root'));

//server.js
import React from &quot;react&quot;;
import ReactDOMServer from &quot;react-dom/server&quot;;

app.use(&quot;/&quot;, (req, res, next) =&amp;gt; {
  fs.readFile(path.resolve(&quot;./build/index.html&quot;), &quot;utf-8&quot;, (err, data) =&amp;gt; {
    if (err) {
      console.log(err);
      return res.status(500).send(&quot;Some error happened&quot;);
    }
    
    return res.send(ReactDOMServer.renderToString(&amp;lt;App name=&quot;Saeloun&quot; /&amp;gt;)
    )
  });
});

//App.js
import React from &quot;react&quot;;

function App(props) {
  return (
    &amp;lt;div&amp;gt;
      Hello {props.name}!
    &amp;lt;/div&amp;gt;
  )
}

export default App;

//전송 html
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;root&quot;&amp;gt;
      &amp;lt;h1&amp;gt;Hello Saeloun!&amp;lt;/h1&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 빠른 초기 로드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 동적 상호작용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- SEO 최적화&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;SSR 언제 어디에서 사용해야 하는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPA 최근에 개발된 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SEO 가 얼마나 중요한가?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;public 데이터에만 필요하다&lt;/li&gt;
&lt;li&gt;비즈니스 아이템에 SEO 가 필수적인가 관점&lt;/li&gt;
&lt;li&gt;고객 타겟팅와 SEO 가 연관성을 지니는가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EX. 네이버 블로그 SSR&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타겟 : 블로그 글을 읽는 네이버 메인 및 검색 사용자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 상황 : 사용자는 빠르게 콘텐츠를 소비한다.&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;anuglar.js 프레임워크로 구현되어있음. - CSR 만 지원한다.&lt;/li&gt;
&lt;li&gt;이로 인해 초기 로드시간이 길어 사용자 경험이 우려된다.&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;Next.js 로 SSR 구성하기를 React 공식 가이드에서 권장하고 있지만&lt;/li&gt;
&lt;li&gt;네이버 블로그 검토 당시 Next.js 는 6.x ver 이었음. 9.x ver 부터 SSR 이 구성이 잘 되어있었음.&lt;/li&gt;
&lt;li&gt;Next.js 는 서비스 플로우 전체 담당하는 프레임워크, 의존도가 높아진다. - 뷰 전용 라이브러리를 사용해야 좋음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 배운점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSR의 딥한 구현방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hydration&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;#참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.react.dev/learn/start-a-new-react-project#production-grade-react-frameworks&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&amp;lt;공식문서&amp;gt; React 의 SSR 구현 시 Next.js 추천&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@hyemin916/%EB%B2%88%EC%97%AD-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%95%B1SSR%EC%9D%98-Hydration-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0&quot;&gt;(번역) 리액트 앱(SSR)의 Hydration 이해하기&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@darren-kk/React%EB%A1%9C-%EA%B0%84%EB%8B%A8%ED%95%9C-SSR-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0#nextjs%EB%8A%94&quot;&gt;10분 만에 React로 SSR 구현하기 (feat. Next.js 소스코드)&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@sunaaank/React-deep-dive&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[React] 네이버 블로그의 Node.js기반 SSR 전환, CSR과 SSR방식을 자세하게 살펴보기&lt;/a&gt;&lt;/p&gt;</description>
      <category>  FrontEnd</category>
      <category>기술면접</category>
      <category>프론트엔드</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/218</guid>
      <comments>https://datdaradanadat.tistory.com/218#entry218comment</comments>
      <pubDate>Mon, 4 Nov 2024 17:07:50 +0900</pubDate>
    </item>
    <item>
      <title>[JS] Lv.2 귤 고르기</title>
      <link>https://datdaradanadat.tistory.com/217</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; 문제설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경화는 과수원에서 귤을 수확했습니다. 경화는 수확한 귤 중 'k'개를 골라 상자 하나에 담아 판매하려고 합니다. 그런데 수확한 귤의 크기가 일정하지 않아 보기에 좋지 않다고 생각한 경화는 귤을 크기별로 분류했을 때 서로 다른 종류의 수를 최소화하고 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 경화가 수확한 귤 8개의 크기가 [1, 3, 2, 5, 4, 5, 2, 3] 이라고 합시다. 경화가 귤 6개를 판매하고 싶다면, 크기가 1, 4인 귤을 제외한 여섯 개의 귤을 상자에 담으면, 귤의 크기의 종류가 2, 3, 5로 총 3가지가 되며 이때가 서로 다른 종류가 최소일 때입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경화가 한 상자에 담으려는 귤의 개수&amp;nbsp;k와 귤의 크기를 담은 배열&amp;nbsp;tangerine이 매개변수로 주어집니다. 경화가 귤 k개를 고를 때 크기가 서로 다른 종류의 수의 최솟값을 return 하도록 solution 함수를 작성해주세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;1388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9SCeO/btsKwyKwPtQ/UKguexlklYlrzaYAFNlz50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9SCeO/btsKwyKwPtQ/UKguexlklYlrzaYAFNlz50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9SCeO/btsKwyKwPtQ/UKguexlklYlrzaYAFNlz50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9SCeO%2FbtsKwyKwPtQ%2FUKguexlklYlrzaYAFNlz50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1510&quot; height=&quot;1388&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;1388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️나의 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종류별 중복개수를 구하는 법??&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;map 을 돌아서 Object 객체 형식에 값, 개수를 삽입한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`dict[val] = (dict[val]||0)+1`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 넣으면, dict의 형식은 `{ '1': 1, '2': 2, '3': 2, '4': 1, '5': 2 }`을 예시로 가진다&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;객체를 정렬해서 많은 순서대로! `[ 2, 2, 2, 1, 1 ]`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남은 귤의 수 k가 현재 선택할 귤의 수 x보다 많거나 같으면 빼서 귤 개수 업데이트 해줌&lt;/p&gt;
&lt;pre id=&quot;code_1702898217840&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function solution(k, tangerine) {
    var ans = 0;
    let dict = {}
    
    tangerine.map((val)=&amp;gt;{
        dict[val] = (dict[val]||0)+1
    });
    
    let arr = Object.values(dict).sort((a,b)=&amp;gt;b-a);
    
    for(const x of arr){
        ans++;
        if(k&amp;gt;x) k-=x;
        else break;
    }
        
    return ans;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체를 사용한다는 생각까지 닿기 어려웠음. 생각해내니 술술 작성됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if 문 조건 중요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열에서 중복값 세는 방법 `dict[val] = (dict[val]||0)+1`&lt;/p&gt;</description>
      <category>코딩테스트/JavaScript</category>
      <category>JavaScript</category>
      <category>Lv.2</category>
      <category>자바스크립트</category>
      <category>프로그래머스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/217</guid>
      <comments>https://datdaradanadat.tistory.com/217#entry217comment</comments>
      <pubDate>Mon, 4 Nov 2024 14:53:33 +0900</pubDate>
    </item>
    <item>
      <title>[JS] Lv.2 카펫</title>
      <link>https://datdaradanadat.tistory.com/216</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; 문제설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Leo는 카펫을 사러 갔다가 아래 그림과 같이 중앙에는 노란색으로 칠해져 있고 테두리 1줄은 갈색으로 칠해져 있는 격자 모양 카펫을 봤습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;843&quot; data-origin-height=&quot;636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TIZfH/btsJ8TuwZrj/jKsJPZOggk5JyHe6I4oko0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TIZfH/btsJ8TuwZrj/jKsJPZOggk5JyHe6I4oko0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TIZfH/btsJ8TuwZrj/jKsJPZOggk5JyHe6I4oko0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTIZfH%2FbtsJ8TuwZrj%2FjKsJPZOggk5JyHe6I4oko0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;843&quot; height=&quot;636&quot; data-origin-width=&quot;843&quot; data-origin-height=&quot;636&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Leo는 집으로 돌아와서 아까 본 카펫의 노란색과 갈색으로 색칠된 격자의 개수는 기억했지만, 전체 카펫의 크기는 기억하지 못했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Leo가 본 카펫에서 갈색 격자의 수 brown, 노란색 격자의 수 yellow가 매개변수로 주어질 때 카펫의 가로, 세로 크기를 순서대로 배열에 담아 return 하도록 solution 함수를 작성해주세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;656&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHzF00/btsJ7NhWR9K/QZTVIFlpsa7wa6bLKk0x1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHzF00/btsJ7NhWR9K/QZTVIFlpsa7wa6bLKk0x1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHzF00/btsJ7NhWR9K/QZTVIFlpsa7wa6bLKk0x1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHzF00%2FbtsJ7NhWR9K%2FQZTVIFlpsa7wa6bLKk0x1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1020&quot; height=&quot;656&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;656&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️나의 풀이&lt;/h2&gt;
&lt;pre id=&quot;code_1702898217840&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function solution(brown, yellow) {
    for(var y=3; y&amp;lt;= (brown+yellow)/y; y++){
        var x = Math.floor((brown+yellow)/y)
        if((x-2) * (y-2) === yellow){
            break;
        }
    }
    return [x,y];
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>코딩테스트/JavaScript</category>
      <category>JavaScript</category>
      <category>Lv.2</category>
      <category>자바스크립트</category>
      <category>프로그래머스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/216</guid>
      <comments>https://datdaradanadat.tistory.com/216#entry216comment</comments>
      <pubDate>Wed, 16 Oct 2024 14:08:06 +0900</pubDate>
    </item>
    <item>
      <title>[JS] Lv.2 짝지어 제거하기</title>
      <link>https://datdaradanadat.tistory.com/215</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; 문제설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짝지어&amp;nbsp;제거하기는,&amp;nbsp;알파벳&amp;nbsp;소문자로&amp;nbsp;이루어진&amp;nbsp;문자열을&amp;nbsp;가지고&amp;nbsp;시작합니다.&amp;nbsp;먼저&amp;nbsp;문자열에서&amp;nbsp;같은&amp;nbsp;알파벳이&amp;nbsp;2개&amp;nbsp;붙어&amp;nbsp;있는&amp;nbsp;짝을&amp;nbsp;찾습니다.&amp;nbsp;그다음,&amp;nbsp;그&amp;nbsp;둘을&amp;nbsp;제거한&amp;nbsp;뒤,&amp;nbsp;앞뒤로&amp;nbsp;문자열을&amp;nbsp;이어&amp;nbsp;붙입니다.&amp;nbsp;이&amp;nbsp;과정을&amp;nbsp;반복해서&amp;nbsp;문자열을&amp;nbsp;모두&amp;nbsp;제거한다면&amp;nbsp;짝지어&amp;nbsp;제거하기가&amp;nbsp;종료됩니다.&amp;nbsp;문자열&amp;nbsp;S가&amp;nbsp;주어졌을&amp;nbsp;때,&amp;nbsp;짝지어&amp;nbsp;제거하기를&amp;nbsp;성공적으로&amp;nbsp;수행할&amp;nbsp;수&amp;nbsp;있는지&amp;nbsp;반환하는&amp;nbsp;함수를&amp;nbsp;완성해&amp;nbsp;주세요.&amp;nbsp;성공적으로&amp;nbsp;수행할&amp;nbsp;수&amp;nbsp;있으면&amp;nbsp;1을,&amp;nbsp;아닐&amp;nbsp;경우&amp;nbsp;0을&amp;nbsp;리턴해주면&amp;nbsp;됩니다.&lt;br /&gt;예를&amp;nbsp;들어,&amp;nbsp;문자열&amp;nbsp;S&amp;nbsp;=&amp;nbsp;baabaa&amp;nbsp;라면&lt;br /&gt;b&amp;nbsp;aa&amp;nbsp;baa&amp;nbsp;&amp;rarr;&amp;nbsp;bb&amp;nbsp;aa&amp;nbsp;&amp;rarr;&amp;nbsp;aa&amp;nbsp;&amp;rarr;&lt;br /&gt;의&amp;nbsp;순서로&amp;nbsp;문자열을&amp;nbsp;모두&amp;nbsp;제거할&amp;nbsp;수&amp;nbsp;있으므로&amp;nbsp;1을&amp;nbsp;반환합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;930&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WZzqi/btsJ2aRXRkQ/7qvqXHEpVct3xckfw3moJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WZzqi/btsJ2aRXRkQ/7qvqXHEpVct3xckfw3moJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WZzqi/btsJ2aRXRkQ/7qvqXHEpVct3xckfw3moJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWZzqi%2FbtsJ2aRXRkQ%2F7qvqXHEpVct3xckfw3moJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;930&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;930&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️나의 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복 문자열을 제거하는 메소드가 있나 생각해보았는데, 붙어있는 짝을 파악하는 메소드는 없다고 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열(스택)을 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;for 문을 돌면서 배열에 문자열을 하나씩 넣고, 이전값 -1 과 cur 값이 같다면 pop 한다. 다르면 push 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1702898217840&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function solution(s)
{
    const stack = [];
    for(let i=0; i&amp;lt;s.length; i++){
        const cur = s.charAt(i);
        if(stack[stack.length -1] === cur){
            stack.pop();
        }else{
            stack.push(cur);
        }
    }
    return stack.length === 0? 1 : 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메소드로만 문제풀려고 하지말고, 메소드를 이용해서 알고리즘 문제를 해결해나가자&lt;/p&gt;</description>
      <category>코딩테스트/JavaScript</category>
      <category>JavaScript</category>
      <category>Lv.2</category>
      <category>자바스크립트</category>
      <category>프로그래머스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/215</guid>
      <comments>https://datdaradanadat.tistory.com/215#entry215comment</comments>
      <pubDate>Fri, 11 Oct 2024 14:44:35 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] Lv.1 두 정수의 합</title>
      <link>https://datdaradanadat.tistory.com/214</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; 문제설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두&amp;nbsp;정수&amp;nbsp;a,&amp;nbsp;b가&amp;nbsp;주어졌을&amp;nbsp;때&amp;nbsp;a와&amp;nbsp;b&amp;nbsp;사이에&amp;nbsp;속한&amp;nbsp;모든&amp;nbsp;정수의&amp;nbsp;합을&amp;nbsp;리턴하는&amp;nbsp;함수,&amp;nbsp;solution을&amp;nbsp;완성하세요.&lt;br /&gt;예를&amp;nbsp;들어&amp;nbsp;a&amp;nbsp;=&amp;nbsp;3,&amp;nbsp;b&amp;nbsp;=&amp;nbsp;5인&amp;nbsp;경우,&amp;nbsp;3&amp;nbsp;+&amp;nbsp;4&amp;nbsp;+&amp;nbsp;5&amp;nbsp;=&amp;nbsp;12이므로&amp;nbsp;12를&amp;nbsp;리턴합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eHdgKP/btsJZKNhj1N/sXciJfkxkHdHxqH5haEltk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eHdgKP/btsJZKNhj1N/sXciJfkxkHdHxqH5haEltk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eHdgKP/btsJZKNhj1N/sXciJfkxkHdHxqH5haEltk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeHdgKP%2FbtsJZKNhj1N%2FsXciJfkxkHdHxqH5haEltk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;910&quot; height=&quot;654&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;654&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️나의 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;a=3, b=5 일 경우, 사이에 속한 모든 정수를 구하는 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;a+1을 b가 될때까지 해서, 그 값을 더한다&lt;/p&gt;
&lt;pre id=&quot;code_1702898217840&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {
    public long solution(int a, int b) {
        long answer = 0;
        
        if(a&amp;lt;b){
            for(int i=a; i &amp;lt;= b; i++){
                answer += i;
            }    
        }else if(b&amp;lt;a){
            for(int i=b; i &amp;lt;= a; i++){
                answer += i;
            }
        }else{
            answer=a;
        }
        
        return answer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바를 오랜만에 사용하니 어색... 색다름....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트의 메소드 보다, 알고리즘과 로직으로 생각하는 방법에 익숙해져야할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜나면 자바스크립트로 풀었을때는 a,b 의 더 작은값, 큰값을 바로 파악하고 for 문 하나로 풀었기 때문이다.&lt;/p&gt;
&lt;pre id=&quot;code_1728546154212&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function solution(a, b) {
    let answer = 0;
    
    for(let i = Math.min(a, b); i &amp;lt;= Math.max(a, b); i++) {
    	answer = answer + i;
    }
    
    return answer;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접근 방법이 많이 다른 것 같다..&lt;/p&gt;</description>
      <category>코딩테스트/Python</category>
      <category>java</category>
      <category>lv.1</category>
      <category>자바</category>
      <category>프로그래머스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/214</guid>
      <comments>https://datdaradanadat.tistory.com/214#entry214comment</comments>
      <pubDate>Thu, 10 Oct 2024 16:38:20 +0900</pubDate>
    </item>
    <item>
      <title>[JS] Lv.2 JadenCase 문자열 만들기</title>
      <link>https://datdaradanadat.tistory.com/213</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; 문제설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JadenCase란&amp;nbsp;모든&amp;nbsp;단어의&amp;nbsp;첫&amp;nbsp;문자가&amp;nbsp;대문자이고,&amp;nbsp;그&amp;nbsp;외의&amp;nbsp;알파벳은&amp;nbsp;소문자인&amp;nbsp;문자열입니다.&amp;nbsp;단,&amp;nbsp;첫&amp;nbsp;문자가&amp;nbsp;알파벳이&amp;nbsp;아닐&amp;nbsp;때에는&amp;nbsp;이어지는&amp;nbsp;알파벳은&amp;nbsp;소문자로&amp;nbsp;쓰면&amp;nbsp;됩니다.&amp;nbsp;(첫&amp;nbsp;번째&amp;nbsp;입출력&amp;nbsp;예&amp;nbsp;참고)&lt;br /&gt;문자열&amp;nbsp;s가&amp;nbsp;주어졌을&amp;nbsp;때,&amp;nbsp;s를&amp;nbsp;JadenCase로&amp;nbsp;바꾼&amp;nbsp;문자열을&amp;nbsp;리턴하는&amp;nbsp;함수,&amp;nbsp;solution을&amp;nbsp;완성해주세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;736&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/exNWQu/btsJZjCkZzV/yPPu1mK4ps8RU5q8srxdCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/exNWQu/btsJZjCkZzV/yPPu1mK4ps8RU5q8srxdCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/exNWQu/btsJZjCkZzV/yPPu1mK4ps8RU5q8srxdCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FexNWQu%2FbtsJZjCkZzV%2FyPPu1mK4ps8RU5q8srxdCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;920&quot; height=&quot;736&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;736&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️나의 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;띄어쓰기를 기준으로 첫문자를 대문자로 바꾼다. 이후 문자는 소문자로 바꾼다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나눈 문장을 합친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`word.charAt(0).toUpperCase()` : 단어의 첫 글자를 대문자로 변환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`word.substring(1).toLowerCase()` : 두 번째 글자부터 끝까지의 글자를 소문자로 변환&lt;/p&gt;
&lt;pre id=&quot;code_1702898217840&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function solution(s) {
    var answer = '';
    return s.split(' ').map(word =&amp;gt; 
    word.charAt(0).toUpperCase() + word.substring(1).toLowerCase()
  ).join(' ')
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자 간의 + 연산으로 이어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;charAt(0) 첫글자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;substring(1) 1 ~ end&lt;/p&gt;</description>
      <category>코딩테스트/JavaScript</category>
      <category>JavaScript</category>
      <category>Lv.2</category>
      <category>자바스크립트</category>
      <category>프로그래머스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/213</guid>
      <comments>https://datdaradanadat.tistory.com/213#entry213comment</comments>
      <pubDate>Thu, 10 Oct 2024 16:23:41 +0900</pubDate>
    </item>
    <item>
      <title>[JS] Lv.1 옹알이(2)</title>
      <link>https://datdaradanadat.tistory.com/212</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; 문제설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머쓱이는 태어난 지 11개월 된 조카를 돌보고 있습니다. 조카는 아직 &quot;aya&quot;, &quot;ye&quot;, &quot;woo&quot;, &quot;ma&quot; 네 가지 발음과 네 가지 발음을 조합해서 만들 수 있는 발음밖에 하지 못하고 연속해서 같은 발음을 하는 것을 어려워합니다. 문자열 배열 `babbling`이 매개변수로 주어질 때, 머쓱이의 조카가 발음할 수 있는 단어의 개수를 return하도록 solution 함수를 완성해주세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1586&quot; data-origin-height=&quot;1562&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MbP3o/btsJW6IDMfU/5VUiGuHm3WzZqffy5G8o21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MbP3o/btsJW6IDMfU/5VUiGuHm3WzZqffy5G8o21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MbP3o/btsJW6IDMfU/5VUiGuHm3WzZqffy5G8o21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMbP3o%2FbtsJW6IDMfU%2F5VUiGuHm3WzZqffy5G8o21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1586&quot; height=&quot;1562&quot; data-origin-width=&quot;1586&quot; data-origin-height=&quot;1562&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️나의 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4가지 발음을 조합한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연속은 불가능하다 - repeat(2) 로 2번 이상 반복 횟수를 끊는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만들 수 있는 모든 경우의 조합을 찾는것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;for 문을 두번 돌면서 파악한다.&lt;/p&gt;
&lt;pre id=&quot;code_1702898217840&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function solution(babbling) {
   const ans = ['aya','ye','woo','ma'];
    let cnt = 0;
    
    for(let i = 0; i &amp;lt; babbling.length; i++){
        let babble = babbling[i];
        
        for(let j = 0; j &amp;lt; ans.length; j++){
            if(babble.includes(ans[j].repeat(2))) break;
            
            babble = babble.split(ans[j]).join(&quot; &quot;);
        }
        if(babble.split(&quot; &quot;).join(&quot;&quot;).length === 0){
            cnt += 1;
        }
    }
    return cnt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;join 시에 공백을 주어서 구분하면, ans 와 매칭하다가 보지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열이 ans 와 문자열이 매칭되는경우 &amp;gt; ans 의 발음으로만 구성되어있는 경우 +1을 계산한다&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;☑️배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로직 설계는 이해되는데, 추후의 가공 및 규칙 적용하는걸 생각하는게 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규식으로 쉽게 풀이한 것을 보고,, 문자열 재참조하는법을 배웠다..!!&lt;/p&gt;</description>
      <category>코딩테스트/JavaScript</category>
      <category>JavaScript</category>
      <category>lv.1</category>
      <category>자바스크립트</category>
      <category>프로그래머스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/212</guid>
      <comments>https://datdaradanadat.tistory.com/212#entry212comment</comments>
      <pubDate>Mon, 7 Oct 2024 15:35:31 +0900</pubDate>
    </item>
    <item>
      <title>[JS] Lv.1 두 개 뽑아서 더하기</title>
      <link>https://datdaradanadat.tistory.com/211</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; 문제설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정수 배열 numbers가 주어집니다. numbers에서 서로 다른 인덱스에 있는 두 개의 수를 뽑아 더해서 만들 수 있는 모든 수를 배열에 오름차순으로 담아 return 하도록 solution 함수를 완성해주세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;1758&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n5MyS/btsJRoW5KQq/tplijVbK1bVTWsYuFuhip1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n5MyS/btsJRoW5KQq/tplijVbK1bVTWsYuFuhip1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n5MyS/btsJRoW5KQq/tplijVbK1bVTWsYuFuhip1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn5MyS%2FbtsJRoW5KQq%2FtplijVbK1bVTWsYuFuhip1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;890&quot; height=&quot;1758&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;1758&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️나의 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;for 문을 두개 돌면서 모든 합의 경우를 temp 배열에 넣는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;for 문을 다 돌고 temp의 정렬, 중복제거를 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복제거 생각하자마자 갑지가 퍼뜩 Set 객체가 생각났다! filter 메서드도 마찬가지로 사용할 수 있을 것 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1702898217840&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function solution(numbers) {
    let temp = [];
    for (let i=0; i&amp;lt;numbers.length; i++){
        for(let j=i+1; j&amp;lt;numbers.length; j++){
            temp.push(numbers[i] + numbers[j])
        }
    }
    const ans = [...new Set(temp)]
    return ans.sort((a,b)=&amp;gt;a-b)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Set 객체를 이용한 중복제거.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;filter 로 바꿔서 문제를 풀어보자면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`return ans.sort((a,b) =&amp;gt; a-b).filter((val,idx) =&amp;gt; val != ans[idx+1]);`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 중복 제거할 수 있다.&lt;/p&gt;</description>
      <category>코딩테스트/JavaScript</category>
      <category>JavaScript</category>
      <category>lv.1</category>
      <category>자바스크립트</category>
      <category>프로그래머스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/211</guid>
      <comments>https://datdaradanadat.tistory.com/211#entry211comment</comments>
      <pubDate>Mon, 30 Sep 2024 15:58:18 +0900</pubDate>
    </item>
    <item>
      <title>[JS] Lv.1 [1차] 다트 게임</title>
      <link>https://datdaradanadat.tistory.com/210</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; 문제설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;다트 게임은 총 3번의 기회로 구성된다.&lt;/li&gt;
&lt;li&gt;각 기회마다 얻을 수 있는 점수는 0점에서 10점까지이다.&lt;/li&gt;
&lt;li&gt;점수와 함께 Single(S), Double(D), Triple(T) 영역이 존재하고 각 영역 당첨 시 점수에서 1제곱, 2제곱, 3제곱 (점수^1 , 점수^2 , 점수^3 )으로 계산된다.&lt;/li&gt;
&lt;li&gt;옵션으로 스타상(`*`) , 아차상(`#`)이 존재하며 스타상(`*`) 당첨 시 해당 점수와 바로 전에 얻은 점수를 각 2배로 만든다. 아차상(`#`) 당첨 시 해당 점수는 마이너스된다.&lt;/li&gt;
&lt;li&gt;스타상(`*`)은 첫 번째 기회에서도 나올 수 있다. 이 경우 첫 번째 스타상(`*`)의 점수만 2배가 된다. (예제 4번 참고)&lt;/li&gt;
&lt;li&gt;스타상(`*`)의 효과는 다른 스타상(`*`)의 효과와 중첩될 수 있다. 이 경우 중첩된 스타상(`*`) 점수는 4배가 된다. (예제 4번 참고)&lt;/li&gt;
&lt;li&gt;스타상(`*`)의 효과는 아차상(`#`)의 효과와 중첩될 수 있다. 이 경우 중첩된 아차상(`#`)의 점수는 -2배가 된다. (예제 5번 참고)&lt;/li&gt;
&lt;li&gt;Single(S), Double(D), Triple(T)은 점수마다 하나씩 존재한다.&lt;/li&gt;
&lt;li&gt;스타상(`*`), 아차상(#)은 점수마다 둘 중 하나만 존재할 수 있으며, 존재하지 않을 수도 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;1412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dk1odK/btsJPGvSpze/DiKYktboerIHXo0AE5Ywck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dk1odK/btsJPGvSpze/DiKYktboerIHXo0AE5Ywck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dk1odK/btsJPGvSpze/DiKYktboerIHXo0AE5Ywck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdk1odK%2FbtsJPGvSpze%2FDiKYktboerIHXo0AE5Ywck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1010&quot; height=&quot;1412&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;1412&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️나의 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 꽤 복잡한데, 문제를 다음과 같이 정리를 해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점수계산&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번의 점수 0~10점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보너스 : S 1제곱 D 2제곱 T 3제곱&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션 : *상 현재점수 *2, 직전점수 *2 (직전점수 없을 수 있고, 없으면 현재점수만)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션 : #상 (-현재점수)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isNaN를 통해 보너스를 기준으로 인식을 한다. 예를들어 1S* 일경우 S 바로 앞의 위치[i]를 파악한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;‼️ 문제 발생 ‼️ 10S*일 경우, S 바로 앞의 위치[i]는 0이된다. 점수가 0점부터 10점까지 있기 때문에, i-1 값이 1이면,10으로 인식해야한다. 아닐경우 해당값 그대로 진행되도록 한다.&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;문자 인식의 경우가 많기 때문에 switch - case 문으로 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규표현식도 많이 사용할 것 같다고 생각했는데, 나는 별로 선호하지않아 고려하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`Math.pow(score, 2)` 코드로 제곱을 적용하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션 스타상에서 직전점수가 있을때, 직점점수 *2 는 경우를 추가했다.&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;각 문자횟수마다 돌면서 push 로 값을 넣어주었고, reduce 메소드로 배열의 누적값을 계산하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1702898217840&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function solution(dartResult) {
    var ans = 0;
    let score = 0;
    let cnt = []
    
    for(let i=0; i&amp;lt;dartResult.length; i++){
    let res = dartResult[i];
        if (!isNaN(res)) {
            score = dartResult[i-1] === '1' ? 10 : Number(res);
        } else {
            switch (res) {
                case &quot;S&quot;:
                    cnt.push(score);
                    break;
                case &quot;D&quot;:
                    cnt.push(Math.pow(score, 2));
                    break;
                case &quot;T&quot;:
                    cnt.push(Math.pow(score, 3));
                    break;
                case &quot;*&quot;:
                    cnt[cnt.length - 1] *= 2;
                    if (cnt.length &amp;gt; 1) {
                        cnt[cnt.length - 2] *= 2;
                    }
                    break;
                case &quot;#&quot;:
                    cnt[cnt.length - 1] *= -1;
                    break;
            }
        }
    }
    return cnt.reduce((acc, cur)=&amp;gt;acc + cur);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;☑️다른 사람 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`let darts = dartResult.match(/\d.?\D/g);`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\d 는 점수 (0~10) &amp;gt; 이때 .?문법을 통해 임의의 하나의 문자가 있을수도 있고 없을수도 있는 옵셔널 &amp;gt; 10을 인식한다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\D 는 숫자가 아닌 문자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\g 는 특수문자&lt;/p&gt;
&lt;pre id=&quot;code_1727419432576&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function solution(dartResult) {
    const bonus = { 'S': 1, 'D': 2, 'T': 3 },
          options = { '*': 2, '#': -1, undefined: 1 };

    let darts = dartResult.match(/\d.?\D/g);

    for (let i = 0; i &amp;lt; darts.length; i++) {
        let split = darts[i].match(/(^\d{1,})(S|D|T)(\*|#)?/),
            score = Math.pow(split[1], bonus[split[2]]) * options[split[3]];

        if (split[3] === '*' &amp;amp;&amp;amp; darts[i - 1]) darts[i - 1] *= options['*'];

        darts[i] = score;
    }

    return darts.reduce((a, b) =&amp;gt; a + b);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;match 를 통해 정규표현식을 파악하고, * 가 있으면 현재 점수 뿐만 아니라, 이전점수까지 계산식 적용한다.&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;☑️배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오 문제 LV.1 이라고 해서 풀었는데,, 정규표현식이라는 어려움을 맞닥드렸다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Math.pow 를 통해서 제곱하는 방법!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;case 로 분기하고, push 를 적절하게 해서, reduce 누적값을 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오테크 블로그에서 후기를 보니 정답률이&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;73.47%, 난이도 하 이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;코테는 잠깐 못푸는 시간이 있으면 실력에 확 나타나는 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;앞으로 좀더 열심히 해보자..&lt;/span&gt;&lt;/p&gt;</description>
      <category>코딩테스트/JavaScript</category>
      <category>JavaScript</category>
      <category>lv.1</category>
      <category>자바스크립트</category>
      <category>프로그래머스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/210</guid>
      <comments>https://datdaradanadat.tistory.com/210#entry210comment</comments>
      <pubDate>Fri, 27 Sep 2024 16:00:17 +0900</pubDate>
    </item>
    <item>
      <title>[JS] Lv.1 숫자 문자열과 영단어</title>
      <link>https://datdaradanadat.tistory.com/209</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; 문제설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네오와 프로도가 숫자놀이를 하고 있습니다. 네오가 프로도에게 숫자를 건넬 때 일부 자릿수를 영단어로 바꾼 카드를 건네주면 프로도는 원래 숫자를 찾는 게임입니다.&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;1478 &amp;rarr; &quot;one4seveneight&quot;&lt;/li&gt;
&lt;li&gt;234567 &amp;rarr; &quot;23four5six7&quot;&lt;/li&gt;
&lt;li&gt;10203 &amp;rarr; &quot;1zerotwozero3&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 숫자의 일부 자릿수가 영단어로 바뀌어졌거나, 혹은 바뀌지 않고 그대로인 문자열&amp;nbsp;s가 매개변수로 주어집니다.&amp;nbsp;s가 의미하는 원래 숫자를 return 하도록 solution 함수를 완성해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 각 숫자에 대응되는 영단어는 다음 표와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자 영단어&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;two&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;three&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;four&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;five&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;six&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;seven&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;eight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;nine&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1374&quot; data-origin-height=&quot;824&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6C6ec/btsJJbqcaQU/OMjRMOGALx9NKMfG53afC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6C6ec/btsJJbqcaQU/OMjRMOGALx9NKMfG53afC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6C6ec/btsJJbqcaQU/OMjRMOGALx9NKMfG53afC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6C6ec%2FbtsJJbqcaQU%2FOMjRMOGALx9NKMfG53afC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1374&quot; height=&quot;824&quot; data-origin-width=&quot;1374&quot; data-origin-height=&quot;824&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️나의 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 문자와 숫자 구분해주기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. split, join 조합 - split으로 나눈다음, 문자열을 숫자와 매칭하는 방법을 못생각해냈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. replace 조합 - 10개 모두 일일히 작성해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;**문제점 발생&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규표현식 /문자/gi 가 아닌, '문자' 로 작성했을때는 실패했다. 왜일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) oneoneonezero 일 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 정규표현식으로 하면 1110 으로 변환되지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 문자열로 작성했을때는 1oneone0으로 변환된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1702898217840&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function solution(s) {
    var ans = 0;
    ans = s.replace(/zero/gi,0)
    .replace(/one/gi,1)
    .replace(/two/gi,2)
    .replace(/three/gi,3)
    .replace(/four/gi,4)
    .replace(/five/gi,5)
    .replace(/six/gi,6)
    .replace(/seven/gi,7)
    .replace(/eight/gi,8)
    .replace(/nine/gi,9)
    
    return parseInt(ans);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;☑️다른 사람의 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영어단어를 배열에 넣으면, index 값과 같다는 것을 보았다..!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;while 문으로 돌면서 배열값을 만나면 split 하고, 배열값의 idx로 바꿔서 join 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1727144411539&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function solution(s) {
    const num = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'];
    
    for(let i=0; i&amp;lt;num.length; i++){
        s = s.split(num[i]).join(i);
    }
    return Number(s);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;for 문 로직&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 숫자 단어 num[i] 를 기준으로 s 를 분리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분리된 요소를 idx 값, 즉 i 로 합친다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) oneoneonezero라면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. num[i]가 zero, i=0 일때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;s.split('zero')는 [&quot;oneoneone', &quot;&quot;], join(0) 으로 &quot;oneoneone0&quot; 이 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. num[i]가 one, i=1 일때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;s.split('one')는 [&quot;&quot;, &quot;&quot;, &quot;&quot;, &quot;0&quot;], join(1) 으로 &quot;1110&quot; 이 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. num[i]가  two, i=2 일때&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;s.split('two')는 1110에서 two가 없으므로 나누고, 합치지 않는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4........&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;val이 0123... 일때는 배열로 생각해보기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;replace() 여러개 풀이법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;replace( 1 | 2 | 3, 0)으로 1, 2, 3 값을 한번에 0으로 바꾸는 작성법도 있다.&lt;/p&gt;</description>
      <category>코딩테스트/JavaScript</category>
      <category>JavaScript</category>
      <category>lv.1</category>
      <category>자바스크립트</category>
      <category>프로그래머스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/209</guid>
      <comments>https://datdaradanadat.tistory.com/209#entry209comment</comments>
      <pubDate>Tue, 24 Sep 2024 11:32:15 +0900</pubDate>
    </item>
    <item>
      <title>Next.js npm run dev, 저장 시에 서버 꺼짐. chunks error</title>
      <link>https://datdaradanadat.tistory.com/208</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아래 github Discusstions의 댓글을 통해 해결했습니다.&lt;br /&gt;&lt;a href=&quot;https://github.com/vercel/next.js/discussions/60185&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/vercel/next.js/discussions/60185&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1725784076186&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;UNKNOWN: unknown error to open .next\static\chunks\app\layout.js on Windows &amp;middot; vercel next.js &amp;middot; Discussion #60185&quot; data-og-description=&quot;Summary I encountered the error below in some events such as clicking Tag or reloading changes in the code. I tested on two different Windows machines and one Macbook, On both Windows machines, I h...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/vercel/next.js/discussions/60185&quot; data-og-url=&quot;https://github.com/vercel/next.js/discussions/60185&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/qqhm9/hyWZd9nlT1/rVYfeZljkxT3EkKQQ9Qmr1/img.png?width=1200&amp;amp;height=600&amp;amp;face=1006_136_1066_201&quot;&gt;&lt;a href=&quot;https://github.com/vercel/next.js/discussions/60185&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/vercel/next.js/discussions/60185&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/qqhm9/hyWZd9nlT1/rVYfeZljkxT3EkKQQ9Qmr1/img.png?width=1200&amp;amp;height=600&amp;amp;face=1006_136_1066_201');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;UNKNOWN: unknown error to open .next\static\chunks\app\layout.js on Windows &amp;middot; vercel next.js &amp;middot; Discussion #60185&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Summary I encountered the error below in some events such as clicking Tag or reloading changes in the code. I tested on two different Windows machines and one Macbook, On both Windows machines, I h...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github.io 포트폴리오 개발 시에, Next.js 를 새롭게 적용하기위해 강의를 하나씩 보면서 사용법을 익히고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vscode 에서 초 간격으로 자동저장이 설정되어있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm run dev 로 서버를 키고, 코드를 수정할때마다 에러가 발생하는것..!&lt;/p&gt;
&lt;pre id=&quot;code_1725784289297&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;⨯ [Error: UNKNOWN: unknown error, open 'C:\dev\test-nextjs\nextjs-init\portfolio-rev\.next\static\chunks\app\layout.js'] {
  errno: -4094,
  code: 'UNKNOWN',
  syscall: 'open',
  path: 'C:\\dev\\test-nextjs\\nextjs-init\\portfolio-rev\\.next\\static\\chunks\\app\\layout.js'
}
[Error: UNKNOWN: unknown error, open 'C:\dev\test-nextjs\nextjs-init\portfolio-rev\.next\static\chunks\app\layout.js'] {
  errno: -4094,
  code: 'UNKNOWN',
  syscall: 'open',
  path: 'C:\\dev\\test-nextjs\\nextjs-init\\portfolio-rev\\.next\\static\\chunks\\app\\layout.js'
}
[Error: UNKNOWN: unknown error, open 'C:\dev\test-nextjs\nextjs-init\portfolio-rev\.next\static\chunks\app\layout.js'] {
  errno: -4094,
  code: 'UNKNOWN',
  syscall: 'open',
  path: 'C:\\dev\\test-nextjs\\nextjs-init\\portfolio-rev\\.next\\static\\chunks\\app\\layout.js'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러메시지가 이해가 안갔다.. 서버를 구동시킬때 npm run dev &amp;gt; next dev 명령어. &amp;gt; `.next` 폴더 생성. 개발자모드에서 개발하면서 실시간으로 빌드파일이 수정되고, 브라우저에 적용되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. `.next` 에 존재하지 않는다? 빌드파일이 제대로 수정되고 있지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결책으로 저장될때마다 `.next` 폴더를 삭재 후 재생성하도록 로직 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. node 버전의 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본인은 이미 전역으로 node 를 설치해서 사용하고 있지 않고, nvm 으로 노드 버전관리를 하면서 프로젝트를 진행하고 있다. 문제가 될 일이 없음.&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;결국 깃허브 Discussions 에서 문제점 파악하고 해결함!&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;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;next 에서 생성한 개발 서버를 내 pc 의 어떤 바이러스 백신 관리앱이 감지할때마다 계속 죽이고 있었던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 글의 경우 Bitdefender 백신 프로그램을 사용하고 있다고 해서, 예외 추가로 개발 파일 디렉터리를 추가하면 되는것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 나는 window 유저고 (오늘 맥북 삼ㅎ) 위 백신 프로그램을 사용하지 않아서 컴퓨터 보안을 싹다 뜯어보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학부생 저학년때 기본으로 설치된 알약....... 이놈을 지운줄 알았는데 보안에서 열심히 돌아가고 있었고... 내 개발서버를 죽이고 있었다....&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;윈도우 제어판 &amp;gt; 프로그램삭제 &amp;gt; 알약 제거 &amp;gt; 재부팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;할 일이 있어서, 재부팅 안하고 2시간정도 다른 작업 후에 다시 확인해보니 삭제가 제대로 또 안되었엇음.... 이때 내 노트북 카톡이랑 노션, 코드, 인터넷 싹다 렉 엄청났어서 심각했는데.. 아니었다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 알약 제거 후 광명 찾았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안 문제가 있을까 찾아보니, window 상위버전(10 이상) 일 경우, 이미 제공되는 보안 서비스로도 충분하기 때문에 문제가 발생하지 않을 것으로 예상된다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국어로 된 문제 해결 글이 없어서 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 문제가 발생했더라도, 블로그에 꼭꼭 기록하는 습관을 들일 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>til</category>
      <category>에러</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/208</guid>
      <comments>https://datdaradanadat.tistory.com/208#entry208comment</comments>
      <pubDate>Sun, 8 Sep 2024 17:42:05 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] Day80 - 응용 구조 설계, 개발 및 배포환경, 계획</title>
      <link>https://datdaradanadat.tistory.com/198</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 응용의 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 개발과정의 설계를 미리 구성하는 방법&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 응용으로 만들어져 UI 에 해당하는 부분을 서비스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BE로 향하는 api 호출은 브라우저의 JS 실행에 의해서 이루어짐 &amp;gt; 이 코드를 FE가 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- FE 에서 call(post, get, put, delete) 하는것처럼 보이지만, 호출은 브라우저의 실행에 의해서 이루어지는것이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Express 응용으로 만들어져 데이터베이스를 이용한 데이터 모델을 서비스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 를 이용한 사용자 인증을 통해 데이터 접근을 보호 : 로그아웃으로 인한 인증정보 해제, 기존 사용자의 정보 접근 제한&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS 정책을 통한 악의적인 접근을 방지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DB&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prgms_notes 라는 이름의 데이터베이스에 두개의 테이블을 포함&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;설정해야 할 것&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BE의 API URL 주소 :&amp;nbsp;코드에 포함하여 endpoint 를 구성할 수 있도록&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;BE&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB(host, database, user, password) : 직접 connector 를 이용하여 데이터베이스 접근을 할 수 있도록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FE의 URL : CORS 정책에 의해 접근을 허용할 수 있도록&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;아키텍처&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Forward Proxy : 클라이언트 대신 프록시 서버가 목적 서버에 통신해주는 구성. 1차로 프록시 서버를 호출하고, 없다면 클라이언트가 웹서버에 직접 액세스 할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reverse Proxy : 웹서버쪽에 위치해 클라이언트의 접근을 최초로 받아 리퀘스트에 해당하는 웹서버에 배분하는 역할&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 하나의 인스턴스 EC2 내부에 구성되어있어서, /api url 이후의 구성하는건 BE로 연결, /*url 이후의 구성은 FE로 포트다이랙트 되도록 클러스터를 구성하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 부분을 개발 이후에 아키텍처 정리하는 과정을 했는데, 설계에서부터 아키텍처를 짜니 프로젝트 전반에 대해 이해가 되는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Production 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;https 연결이 가능하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BE URL 엔드포인드는 프로젝트도메인/api&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로컬 테스트 환경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;staging area 서버 : 배포환경과 동일한 서버 하나를 더 만들어서, 배포서버에 업로드 전에, staging 서버에 업로드 해서 인수테스트를 진행한다. 자원, 인스턴스 타입, 사양으로 똑같이 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비슷한 쿠버네티스 서버 환경을 구성하기는 하지만, 개발 단계에서 검증하기에는..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 웹서버를 사이에 두지 않고, FE,BE 접근 URL 설정에 차이가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 인터넷 domain name 을 통하여 접근하는 것이 불가능하다. : 로컬 테스트 환경이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 데이터베이스 인스턴스도 동일한 쿠버네티스 컬러스터 내에 배치한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- SSL 인증 없다. http 도메인. JWT 쿠키 처리 방식에 대해 차이점이 발생한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개발 환경의 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 변경할 때마다 로컬 테스트 환경에 배포하는 것은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 시간 소요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 환경설정, 디버깅의 번거로움&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;단위 테스트&lt;/b&gt; 작성 및 활용 : CI/CD 를 위해서도 매우 중요한 일&lt;/li&gt;
&lt;li&gt;간단한 사용자 테스트 : &lt;u&gt;npm 을 이용해서 FE, BE 각각을 서로 다른 포트에서 서비스하도록 하고 테스트&lt;/u&gt;&lt;/li&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발이 진행될수록 규모 등이 무거워진다. 시간이 더 많이 소요된다 &amp;gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;테스트 자동화의 중요성!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운용이 배포될때 가지는 순서 및 구조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발, 배포 환경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 구현의 이전에, 다른 부분의 코드 동작을 확인하는 것이 매우 유리하다. 초기일경우 변경이 빈번하기 때문!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트케이스를 먼저 구성하면, 효율을 높일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조, 인터페이스의 요구사항 변경 시 테스트 코드 정비도 어려움. 사용자 인터페이스를 다루는 코드는 단위테스트로 검증하는데 어렵기때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 동작을 눈으로 확인할 수 있는 환경을 구축하는 것이 필요하다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터베이스 준비&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 의존성을 없이 개발하는게 수월하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 순간부터는 코드의 검증을 위해 데이터베이스의 상태가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 or CRUD 등..&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;스키마가 확정이라면, 미리 마련하는 것도 ㄱㅊ. 변경가능성 낮은경우 ok&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;* 프로덕션 DB를 테스트에 활용하는 것은 안된다!!&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;로컬 환경에서 테스트에 이용할 수 있는 DB를 따로 만들어야한다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;계획 수립&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;역할 분담
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기획, 디자인, 코드개발, 테스트, 운영자원 : PM의 역할&lt;/li&gt;
&lt;li&gt;BE,FE의 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;통일된 개발 방법, 팀 사이의 인터페이스 : 결과물에 대한 구체적인 예상으로부터&lt;/li&gt;
&lt;li&gt;프로젝트 일정 및 지연 발생에 대한 대응책&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멘토들과 적극적인 코드리뷰 계획&lt;/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;TDD : 부담스러울 수 있지만, 결과물에 대한 예측이 명확하기에 프로젝트에 연습으로 좋다.&lt;/li&gt;
&lt;li&gt;CI 지속적 통합 : 어느 단계에서부터 도입? FE+BE+DB 가 통합되고, 인수 테스트가 적용되기 시작할 시점&lt;/li&gt;
&lt;li&gt;CD 지속적 배포/인도 : 첫 릴리스 타겟 시점에 파이프라인 구축을 완료하는 계획으로&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;계획 테스트&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;단위 : mock 목 데이터 사용 테스트케이스 철저할수록 도움이 됨&lt;/li&gt;
&lt;li&gt;통합 : 통합이 올바른지, 자동화 방법을 어떻게 할지 &amp;gt; 데이터베이스의 상태 관리 방법에 주의&lt;/li&gt;
&lt;li&gt;인수&lt;/li&gt;
&lt;li&gt;스모크 : 배포상태가 올바른지 검사 &amp;gt; status code 를 체크한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빌드 에이전트 구현 및 적용(node), 파이프라인 설계 및 구현(jenkins)&lt;/li&gt;
&lt;li&gt;통합 인수 테스트를 위한 시자리오 개발 및 이에 적용할 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일정계획&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마일스톤 설정, 작업 병렬화, 도구 활용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서와 시간의 배분은 자율적&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전점검(실현 가능성 검토)가 중요하다. &amp;gt; 멘토&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;for 문서화&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기술적 요소&lt;/li&gt;
&lt;li&gt;일정 계획&lt;/li&gt;
&lt;li&gt;진척상황&lt;/li&gt;
&lt;li&gt;동적 대응&lt;/li&gt;
&lt;li&gt;가이드라인&lt;/li&gt;
&lt;li&gt;합의사항&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>웹 풀 사이클 데브코스</category>
      <category>til</category>
      <category>데브코스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/198</guid>
      <comments>https://datdaradanadat.tistory.com/198#entry198comment</comments>
      <pubDate>Fri, 16 Aug 2024 02:01:35 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] Day79 - 웹 기반 문서편집기 제작 프로젝트 설계</title>
      <link>https://datdaradanadat.tistory.com/197</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;목표&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react+express&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동화 파이프라인으로 구축, 개발 및 통합(빌드, 테스트, 배포)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 개발 프로세스에 더 중점을 두고 진행할 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;테스트 자동화 및 배포 프로세스&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자로써 해야할 역할, 범위, 타팀과 업무에 대한 이해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경구성 자동화를 통해 도구의 지원하는 방식. 환경 구성에 주의해야할 부분&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발환경과 프로덕션 환경의 차이를 염두하고, 전체 싸이클을 경험하는 과정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CICD 파이프라인이 중점이 아닌, E2E 테스트, AWS, 인프라 모니터링 관련된 부분이 추가된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 클라우드 인프라를 이용한 프로덕션 환경 모사.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모니터링과 시각화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;소개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노션과 유사한 상태의 노트, 문서, 메모장과 같은 프로젝트 서비스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 요구사항으로부터 전체 설계를 도출할 수 있는 능력은 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기술스택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BE&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Express.js v4.18&lt;/li&gt;
&lt;li&gt;MariaDB 10.6.16&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FE&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;React.js v18&lt;/li&gt;
&lt;li&gt;Tiptap library for editor component&lt;/li&gt;
&lt;li&gt;JWT&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Production deployment&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS : Kubernetes, Minikube 클러스터 환경&lt;/li&gt;
&lt;li&gt;Jenkins, Terraform, Selenium&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;node.js v18.x&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker desktop으로 구성한 single-node cluster - 테스트용 클러스터 &amp;gt; jenkins 가 설치되어 운용되는 것으로 가정함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Selenium 파이프라인 : 필요할때만 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포환경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 instance 하나에 minikube로 구성한 k8s sluster 서비스 ( 프로젝트에서 제공 )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB : 같은 EC2 인스턴스에 MariaDB 설치 및 데이터베이스 스키마 생성되어서 제공됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;SW 요구사항 명세서 : 결과물 도출물에 대한 설명&lt;/li&gt;
&lt;li&gt;구조 설계서
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BE : api 입/출력 및 동작에 대한 명시(+소스 코드 패키지 구조)&lt;/li&gt;
&lt;li&gt;FE : 컴포넌트 구분 및 게층 구조 명세(+소스 코드 패키지 구조)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;상세 설계서
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BE : 생략&lt;/li&gt;
&lt;li&gt;FE : 각 컴포넌트의 기능 명세 및 와이어프레임&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;/li&gt;
&lt;li&gt;개발환경을 셋업하고, 뼈대만 있는 코드(스켈레톤 코드)로 BE,FE 각각 개발 단계 테스트가 가능함을 확인한다.&lt;/li&gt;
&lt;li&gt;단위테스트를 확보하고 코드를 개발한다.&lt;/li&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CICD 파이프라인을 구축하고, 인수테스트 시나리오를 설계, 적용한다.&lt;/li&gt;
&lt;li&gt;코드 품질을 확보하기 위한 개선 및 기능 추가를 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한번 프로세스를 잘 구축해두면 (CICD) 이후에 유연하고 효육적 개발을 할 수 있다.&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;소프트웨어 요구사항 명세서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*요구사항을 만족하는 소프트웨어를 개발하는것!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경에 따른 업데이트를 자주 적용해야 하고. 테스트 케이스를 작성해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TDD : 요구사항 &amp;gt; 테스트 케이스 &amp;gt; 코드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필수 산출물!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SRS : Software requirement Specification&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;waterfall 모델에서 주로 필수 산출물로 정의하고, 애자일 방법론에서는 산출을 생략하는 경우도 있다.&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;# Requirement-driven development 방식 : 웹개발 맥락에서는 많이 사용하지 않지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;missionscritical system(디바이스 내부 시스템) 등에서는 foramlverification 방법과 조합해 엄격하게 검증하는게 중요시되고 있다.&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;요구사항 ID, 요구사항명, 개요, 입력, 출력 에 대한 상세 내용이 표시된다.&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;☑️ 배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gitaction 을 이용한 CICD 프로젝트에 적용해 보았는데, 클러스터로 파이프라인 구축하여 &lt;b&gt;로컬에서 테스트배포하는&lt;/b&gt; 방법 배울 수 있어서 좋을 것 같다.&lt;/p&gt;</description>
      <category>웹 풀 사이클 데브코스</category>
      <category>til</category>
      <category>데브코스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/197</guid>
      <comments>https://datdaradanadat.tistory.com/197#entry197comment</comments>
      <pubDate>Wed, 14 Aug 2024 16:03:20 +0900</pubDate>
    </item>
    <item>
      <title>[MIL] 개발 블로그 글쓰기 스터디 회고 (4~6월)</title>
      <link>https://datdaradanadat.tistory.com/185</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;뒤늦은 회고,,, 총총...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 애정하고 8기,9기동안 활동하고 있는 IT 연합 동아리 코테이토에서 개발 블로그 글쓰기에 참여했다.&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;a href=&quot;https://datdaradanadat.tistory.com/91&quot;&gt;개발자 진로에 대한 고민&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datdaradanadat.tistory.com/96&quot;&gt;CS 지식 네트워크 보안 공격 기법&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datdaradanadat.tistory.com/115&quot;&gt;알고리즘 풀이 DFS/BFS&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://datdaradanadat.tistory.com/113&quot;&gt;DFS 백준 14888&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datdaradanadat.tistory.com/112&quot;&gt;BFS 백준 18352&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datdaradanadat.tistory.com/135&quot;&gt;node.js로 요구사항에 맞는 api 설계 및 구현&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datdaradanadat.tistory.com/147&quot;&gt;SSR vs CSR 로 프로젝트 구성&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datdaradanadat.tistory.com/162&quot;&gt;개발자 데스크 셋업&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Why?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;처음에는 동아리에서 프로젝트 하나, 스터디 하나 총 두개의 활동에 참여하려고 했는데... 선착순 및 개설 이슈로 3개를 참여하게 되었다. 개블쓰 스터디를 시작하기 전에는 블로그 글이 다양하거나 많지 않았고, 나 또는 학습한 것에 대해 정리하는 과정이 있으면 좋을 것 같아 참여하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 개발자들은 깃허브 잔디심기가 유행이라는데, 나는 스스로 꾸준하고 다양하게 활동하는게 어렵기 때문에... 블로그 활성화를 위해서 개블쓰는 나에게 꼭 맞는 활동이었다. 또 이런데 들어가면 책임감 가지고 잘하려고 한다!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 방학기간동안 개블쓰 2기가 진행되고 있는데 참여하지 못하였다. 데브코스에서 프로젝트를 진행하게 되면서 일주일마다 정신없이 살아가고 있기 때문이다. (프로젝트 3개 진행중...) 너무너무 아쉽지만 지금까지의 노력이 헛되이지 않도록 마무리 회고를 잘 작성하면서 보내주려 한다ㅎㅎ&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;How?&lt;/h2&gt;
&lt;figure id=&quot;og_1722404196862&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - IT-Cotato/9th-Dev-Blog-Study: 9기에 진행하는 '개발 블로그 쓰기' 스터디입니다.&quot; data-og-description=&quot;9기에 진행하는 '개발 블로그 쓰기' 스터디입니다. Contribute to IT-Cotato/9th-Dev-Blog-Study development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/IT-Cotato/9th-Dev-Blog-Study&quot; data-og-url=&quot;https://github.com/IT-Cotato/9th-Dev-Blog-Study&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b5gLTX/hyWGVgOmI6/pt0kdhQjfgGLpHBP4nf5AK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/IT-Cotato/9th-Dev-Blog-Study&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/IT-Cotato/9th-Dev-Blog-Study&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b5gLTX/hyWGVgOmI6/pt0kdhQjfgGLpHBP4nf5AK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - IT-Cotato/9th-Dev-Blog-Study: 9기에 진행하는 '개발 블로그 쓰기' 스터디입니다.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;9기에 진행하는 '개발 블로그 쓰기' 스터디입니다. Contribute to IT-Cotato/9th-Dev-Blog-Study development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그 글쓰기 이지만, 특이하게 블로그와 깃허브 .md 작성을 동시에 진행하였다.&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;스터디원의 PR 을 읽고, 리뷰와 피드백을 작성해 서로 성장하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 세가지를 목표로 하였다.&lt;/p&gt;
&lt;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;li&gt;타인의 글을 읽고 질문하는, 질문 받는 습관&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주제는 큰 틀로 6가지를 준비해주셨는데, 알차고 다양해서 넓게 생각하는 계기가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 나는 작성한 글에 대한 내용은 엄청 특별하거나 신박한 부분에 대해서는 부족하지만, 스터디원의 리뷰와 지식공유가 큰 도움이 되었다. 또 멱등성, 깃플로우 등과 같은 좋은 내용에 대해 읽으면서 지식도 쌓아갈 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원의 글에 작성된 리뷰를 읽으면서 배우고 알아가는 것도 있었다. 또 글을 읽고 궁금한 점을 질문하면, 글 작성자가 더 깊게 공부해서 알게된 점, 해결책을 공유하는 방식이 좋았다. 모르는것, 궁금한 것을 부끄러워하지 않고 솔직하게 공유하면서 동료성장을 한 기분이 많이 들었다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 2기에는 기획, 디자인 파트도 참여하게 되어서 글 주제나 내용이 더 다채로와질 것 같다.&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;What?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1기는 프론트 2명, 백엔드 4명으로 구성되었는데 같은 파트 민재님의 글을 통해 생각과 개발의 깊이를 느낄 수 있게 되어서 좋았다. 또한 백엔드 파트 팀원의 글을 읽으면서 로그인 및 개발 구조와 환경 플로우, 스프링 컨트롤러, 머신러닝 등 다양한 부분에 알게되어서 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론 뿐만 아니라 프로젝트에 개발을 적용하기 위해 무엇이 더 좋은지, 필요한지 &lt;b&gt;개발자로써 깊게 고민하는 방법&lt;/b&gt;을 배웠다. 사소한 부분도 마구잡이로 구현하는게 아닌, 왜 필요한가? 어떻게 적용하는게 좋은가? 먼저 고민하고 설계해서 프로젝트에 적용하는 방법. 그리고 원리부터 이해하는 방법이 중요하다고 느꼈다. 그리고 개블쓰 덕분에 생각을 깊게 할 수 있는 계기가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 데브코스 프로젝트에서 혼자 프론트 설계부터 깃플로우, API 연동, 배포까지 모두 관리하고 구현한 경험이 있다. 이때 멘붕올뻔한거,, 강의 + 개블쓰에서 받은 리뷰와 팀원글의 정보를 모아 잘 마무리하게 된 것 같아 다행이었다.&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;로그인과 기본키, response 데이터 등의 과정에서 백엔드와 설계 및 연동할때 원하는 바를 명확히 소통할 수 있었다. 깃 플로우 전략으로 CICD 를 적용해 문제가 생겨도 바로 해결할 수 있었다. api + hook 조합으로 연동하였다.&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;무작정 axios 부터 작성해서 코드 덩어리였던 작년과 달라졌다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드분리도 정확하게 하고, 깃에 문제생겨도 해결할줄 알고, 백엔드 로직도 파악해서 설계 및 구현할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://datdaradanadat.tistory.com/91&quot;&gt;첫 개블쓰 글&lt;/a&gt;에서&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표를 프론트에 몰입하되,다른 분야까지 전체적으로 흐름을 이해하는 개발자가 되겠다고 생각했다. 프론트로써 보다 다양한 기능을 구현해 내고, 그걸 CS 지식에 적용하여 관리기법을 사용해 보고 싶다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 작성했다. 완벽하지는 않지만, 목표에 가까워졌다는게 느껴졌다. 우하하&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개블쓰와 그 과정을 프로젝트에 녹여내려고 노력하면서 성장하게 된 것 같다!&lt;/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;넓게 고민하고, 깊게 학습하고, 동료랑 성장하는 개블쓰 최고!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스터디 마무리 발표때 작성한 나의 후기이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최고의 리더와 든든한 팀원들과 함께 학습하고 성장할 수 있어서 너무 좋은 기회였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 경험으로 나도 리더역할, 팀원역할을 백퍼 이상 발휘할 수 있는 사람이 되고 싶다! (진행중..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주어진 것에 최선을 다하는 내가 되자!&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 지갑에 항상 가지고다니는 포토카드. 의지박약일때 읽으면 힘나게 하는 말! 아자아자!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;1020&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F31m2/btsIS96H2NG/axIu53k2trUNjK6duItBq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F31m2/btsIS96H2NG/axIu53k2trUNjK6duItBq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F31m2/btsIS96H2NG/axIu53k2trUNjK6duItBq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF31m2%2FbtsIS96H2NG%2FaxIu53k2trUNjK6duItBq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;855&quot; height=&quot;1020&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;1020&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>회고</category>
      <category>mil</category>
      <category>개블쓰</category>
      <category>회고</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/185</guid>
      <comments>https://datdaradanadat.tistory.com/185#entry185comment</comments>
      <pubDate>Wed, 31 Jul 2024 14:20:29 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] Day67 - 리뷰, 베스트, 배너, 모바일 대응</title>
      <link>https://datdaradanadat.tistory.com/184</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;캐로셀&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Slick 라이브러리&lt;/h3&gt;
&lt;figure id=&quot;og_1723047230012&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Home | React Slick&quot; data-og-description=&quot;The Last React Carousel You'll Ever Need!&quot; data-og-host=&quot;react-slick.neostack.com&quot; data-og-source-url=&quot;https://react-slick.neostack.com/&quot; data-og-url=&quot;https://react-slick.neostack.com/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://react-slick.neostack.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://react-slick.neostack.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Home | React Slick&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The Last React Carousel You'll Ever Need!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;react-slick.neostack.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목 데이터로 reviewAll 데이터를 훅으로 생성해서 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 슬라이딩 효과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`npm i react-slick --save` `npm i slick-carousel --save` 타입 설치 `npm i --save-dev @types/react-slick`&lt;/p&gt;
&lt;pre id=&quot;code_1723048132716&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Slider from 'react-slick';

import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';
//..
const sliderSettings = {
    dots: true,
    infinite: true,
    speed: 500,
    slidesToShow: 3,
    slidesToScroll: 3,
	gap: 16,
};
  
  //..
&amp;lt;Slider {...sliderSettings}&amp;gt;
        //..
&amp;lt;/Slider&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리액트 컴포넌트 직접 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입 생성 시 target 항목 필수!&lt;/p&gt;
&lt;pre id=&quot;code_1723187992239&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export interface Banner {
  id: number;
  title: string;
  description: string;
  image: string;
  url: string;
  target: string;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;picsum 에서 목 데이터 이미지를 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;target에서 `_balnk` 새 탭에서 열겠다. `_self` 현재 탭에서 열겠다 옵션!&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;position을 relative, absolute 를 적절하게 주어, 이미지 상단에 콘텐츠title, description 이 들어가도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;슬라이드 구현&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;currentIndex로 슬라이드 순서 구별.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`flex: 0 0 100%;` 으로 화면에 100%로 보이면서, 정렬은 column 으로 되는 효과!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음, 이전 버튼&lt;/p&gt;
&lt;pre id=&quot;code_1723189217445&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const handlePrev = () =&amp;gt; {
    if (currentIndex === 0) return;
    setCurrentIndex(currentIndex - 1);
  };
  const handleNext = () =&amp;gt; {
    if (currentIndex === banners.length - 1) return;
    setCurrentIndex(currentIndex + 1);
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배너 이동하는 transform 값은 인덱스로 이동해야할 만큼 계산하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;css 에서 이동해준다. 애니메이션도 같이!&lt;/p&gt;
&lt;pre id=&quot;code_1723189368535&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const [currentIndex, setCurrentIndex] = useState(0);

  const transFormValue = useMemo(() =&amp;gt; {
    return currentIndex * -100;
  }, [currentIndex]);
//..
const BannerContainerSytle = styled.div&amp;lt;BannerContainerSytleProps&amp;gt;`
  display: flex;
  transform: translateX(${(props) =&amp;gt; props.$transFormValue}%);
  transition: transform 0.5s ease-in-out;
`;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인디케이터 Indecator&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스타일과 클래스명 active로 스타일링을 하고, onClick으로 setCurrentIndex를 index 로 설정해서 이동하도록 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1723190336183&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const handleIndecatorClick = (index: number) =&amp;gt; {
    setCurrentIndex(index);
  };
  //..
  &amp;lt;BannerIndecatorStyle&amp;gt;
        {banners.map((_, index) =&amp;gt; (
          &amp;lt;span
            className={index === currentIndex ? 'active' : ''}
            onClick={() =&amp;gt; handleIndecatorClick(index)}
          &amp;gt;&amp;lt;/span&amp;gt;
        ))}
      &amp;lt;/BannerIndecatorStyle&amp;gt;
      //..
      const BannerIndecatorStyle = styled.div`
  position: absolute;
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);

  span {
    display: inline-block;
    width: 16px;
    height: 16px;
    border-radius: 100px;
    background: #fff;
    margin: 0 4px;
    cursor: pointer;

    &amp;amp;.active {
      background: ${({ theme }) =&amp;gt; theme.color.primary};
    }
  }
`;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;정리&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;overflowhidden 을 가지는 전체 너비를 가지는 뷰포트를&lt;/li&gt;
&lt;li&gt;flex column 정렬 시키고, &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;`flex-grow`&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; `flex-shrink` 를 0으로 설정해서 늘어나지도, 줄어들지도 않게 하기! &lt;b&gt;`flex: 0 0 100%;`&amp;nbsp;&lt;/b&gt;flex-basis를 100%로 좌우를 꽉 차게 구성한다. &amp;gt; 캐러셀에서도 조정할 수 있음&lt;/li&gt;
&lt;li&gt;배너를 이동하게 하는건 배너의 핸들러 transFormValue, currentIndex의 값만 가지고, 값을 반환해서 css 에서 그 값으로 이동하도록 `transform` 옵셕으로 설정하였다.&lt;/li&gt;
&lt;li&gt;모든 기준은 currentIndex로 기준 역할을 한다.&lt;/li&gt;
&lt;li&gt;transFormValue 에서 오른쪽으로가는 버튼을 누르면, 슬라이드는 사실 왼쪽으로 100%만큼씩 이동하게 된다. 반대여서 -가 붙는다.&lt;/li&gt;
&lt;li&gt;좌우 버튼은 index 를 조정하는 역할&lt;/li&gt;
&lt;li&gt;indecator 인디케이터는 각 원에 index 를 직접 넣어서, 같은 state(currentIndex)를 동작하도록 하였다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컴포넌트 재사용 + 일부요소 제외&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 bookItem을 매핑하는 컴포넌트 bookBestItem 컴포넌트 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 css를 export 해서, 선택자 할 요소를 지정한다. &amp;gt; `override`를 하겠다!&lt;/p&gt;
&lt;pre id=&quot;code_1723105032006&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const BookBestItem = ({ book, itemIndex }: Props) =&amp;gt; {
  return (
    &amp;lt;BookBestItemStyle&amp;gt;
      &amp;lt;BookItem book={book} view=&quot;grid&quot; /&amp;gt;
      &amp;lt;div className=&quot;rank&quot;&amp;gt;{itemIndex + 1}&amp;lt;/div&amp;gt;
    &amp;lt;/BookBestItemStyle&amp;gt;
  );
};

const BookBestItemStyle = styled.div`
  ${BookItemStyle} {
    .summary,
    .price,
    .likes {
      display: none;
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모바일 대응 : 반응형 웹&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;viewport
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;index.html 에서 `&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot; /&amp;gt;` 으로 기본적으로 지원해주고 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상대값을 가진 레이아웃
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상대값의 너비는 max-width: 1020px;&lt;/li&gt;
&lt;li&gt;width는 100%로 상태값으로 주고,&amp;nbsp;&lt;/li&gt;
&lt;li&gt;max-width를 주어 고정값을 준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;화면 너비 감지 &lt;b&gt;mediaquery&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;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;너비 감지 훅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`window.matchMedia.matches` 로 현재 뷰포트의 너비를 분석한다.&lt;/p&gt;
&lt;pre id=&quot;code_1723199022936&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const useMediaQuery = () =&amp;gt; {
  const [isMobile, setIsMobile] = useState(
    window.matchMedia(getTheme('light').mediaQuery.mobile).matches,
  );

  useEffect(() =&amp;gt; {
    const isMobileQuery = window.matchMedia(
      getTheme('light').mediaQuery.mobile,
    );

    setIsMobile(isMobileQuery.matches);
  }, []);

  return { isMobile };
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모바일 뷰 일 경우, 다음처럼 조건을 나누어 스타일링을 분리할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1723199073738&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@media screen And ${({ theme }) =&amp;gt; theme.mediaQuery.mobile} {
..//
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isOpen 값을 상태로 두어서, 열고 닫힐때 스타일링을 분기해 표시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`const [isMobileOpen, setIsMobileOpen] = useState(false);`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>웹 풀 사이클 데브코스</category>
      <category>til</category>
      <category>데브코스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/184</guid>
      <comments>https://datdaradanadat.tistory.com/184#entry184comment</comments>
      <pubDate>Tue, 30 Jul 2024 14:15:28 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] Day66 - 드롭다운, 탭, 토스트, 모달, 무한스크롤</title>
      <link>https://datdaradanadat.tistory.com/183</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;모킹 서버 작성 MSW&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트 라이브러리. 업계표준라이브러리. 자바스크립트라면 다른 환경에서도 많이 사용한다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mocking responses 모킹 : 실제로 존재하지 않는 것을 가상으로 구현하는 것. node 서버를 가상으로 띄워논다&lt;/p&gt;
&lt;figure id=&quot;og_1722914232867&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Mocking responses&quot; data-og-description=&quot;Learn how to mock HTTP responses.&quot; data-og-host=&quot;mswjs.io&quot; data-og-source-url=&quot;https://mswjs.io/docs/basics/mocking-responses/&quot; data-og-url=&quot;https://mswjs.io/docs/basics/mocking-responses/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gtB9m/hyWKJz2Ve4/IO9gkiSSDVTy8Hp9mOVpvK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/d9xOjv/hyWKBIK06S/lbwRJP7k9cG3iaRaOoNoj0/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://mswjs.io/docs/basics/mocking-responses/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://mswjs.io/docs/basics/mocking-responses/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gtB9m/hyWKJz2Ve4/IO9gkiSSDVTy8Hp9mOVpvK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/d9xOjv/hyWKBIK06S/lbwRJP7k9cG3iaRaOoNoj0/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Mocking responses&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Learn how to mock HTTP responses.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;mswjs.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Mock Service Worker&lt;/li&gt;
&lt;li&gt;존재하지 않는 API 에 대한 응답을 모킹&lt;/li&gt;
&lt;li&gt;service worker에서 요청을 처리&lt;/li&gt;
&lt;li&gt;chrome 기준 devTool의 Application / service workers의 &quot;Bypass for network&quot;로 일시정지&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리뷰 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목 서비스로 리뷰 생성하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리뷰 모델, 테스트 생성&lt;/p&gt;
&lt;pre id=&quot;code_1722916057056&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export interface BookReviewItem {
  id: number;
  userName: string;
  content: string;
  createAt: string;
  score: number;
}
//..
export const fetchBookReview = async(bookId:string)=&amp;gt;{
    return await requestHandler&amp;lt;BookReviewItem&amp;gt;(&quot;get&quot;,`/reviews/${bookId}`)
}
//..
  const [reviews, setReviews] = useState&amp;lt;BookReviewItem[]&amp;gt;([]);
  fetchBookReview(bookId).then((reviews) =&amp;gt; {
      setReviews(reviews);
    });
//..
const { book, likeToggle, reviews } = useBook(bookId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MSW 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`npm i msw --save-dev`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 생성 `npx msw init public/ --save` public 지정 이유는 spa 서빙하는 파일이 존재하는데, 브라우저에 모킹서버를 직접 동작하겠다는 의미이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`mockServiceWorker.js` 파일 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;browser.ts 에 목 브라우저 불러와서 사용하는 코드&lt;/p&gt;
&lt;pre id=&quot;code_1722916520907&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { setupWorker } from 'msw/browser';

const handlers = [];

export const worker = setupWorker(...handlers);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;review.ts&lt;/p&gt;
&lt;pre id=&quot;code_1722916925981&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { http, HttpResponse } from 'msw';
import { BookReviewItem } from '../models/book.model';

export const reviewsById = http.get(
  'https://localhost:2222/reviews/:bookId',
  () =&amp;gt; {
    const data: BookReviewItem[] = [];
    return HttpResponse.json(data, {
      status: 200,
    });
  },
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 비동기 처리의 타이밍 이슈 해결방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DOM 의 마운트하는 부분을 한번에 묶어서 async 로 요청을 보낸다.&lt;/p&gt;
&lt;pre id=&quot;code_1722955587242&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function mountApp() {
  if (process.env.NODE_ENV === 'development') {
    const { worker } = require('./mock/browser');
    await worker.start(); //이때 시작
  }

  const root = ReactDOM.createRoot(
    document.getElementById('root') as HTMLElement,
  );
  root.render(
    &amp;lt;React.StrictMode&amp;gt;
      &amp;lt;App /&amp;gt;
    &amp;lt;/React.StrictMode&amp;gt;,
  );
}
mountApp();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;faker.js 더미데이터&lt;/h3&gt;
&lt;figure id=&quot;og_1722960421717&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Person | Faker&quot; data-og-description=&quot;&quot; data-og-host=&quot;fakerjs.dev&quot; data-og-source-url=&quot;https://fakerjs.dev/api/person.html#firstname&quot; data-og-url=&quot;https://fakerjs.dev/api/person.html#firstname&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Knebm/hyWKwARp8W/dnKPpkEVFDTmJ3wXwgCw3K/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://fakerjs.dev/api/person.html#firstname&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://fakerjs.dev/api/person.html#firstname&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Knebm/hyWKwARp8W/dnKPpkEVFDTmJ3wXwgCw3K/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Person | Faker&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;fakerjs.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;pre id=&quot;code_1722960745608&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { fakerKO as faker } from '@faker-js/faker';
const mockReviewData: BookReviewItem[] = Array.from({ length: 8 }).map(
  (_, index) =&amp;gt; ({
    id: index,
    userName: `${faker.person.lastName()}${faker.person.firstName()}`,
    content: faker.lorem.paragraph(),
    createAt: faker.date.past().toISOString(),
    score: faker.helpers.rangeToNumber({ min: 1, max: 5 }),
  }),
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;helpers에 range 많이 사용함. 8개의 배열 데이터&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fakerKO 한국어 서비스&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;UI&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;드롭다운&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저아이콘 &amp;gt; 하단 패널 등장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션 모달 생성, 공간을 유용하고, 사용자 상호작용에 유용하다 &amp;lt;&amp;gt; selector 와 다르다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;click outside pattern&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패널 밖모습 클릭하면 창 닫힘&lt;/p&gt;
&lt;pre id=&quot;code_1723018860904&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const dropdownRef = useRef&amp;lt;HTMLDivElement&amp;gt;(null);

  useEffect(() =&amp;gt; {
    function handleOutsideClick(e: MouseEvent) {
      if (
        dropdownRef.current &amp;amp;&amp;amp;
        !dropdownRef.current.contains(e.target as Node)
      ) {
        //외부클릭되었음
        setOpen(false);
      }
    }
    document.addEventListener('mousedown', handleOutsideClick);
    return () =&amp;gt; {
      document.removeEventListener('mousedown', handleOutsideClick);
    };
  }, [dropdownRef]);
//..
&amp;lt;DropdownStyle $open={open} ref={dropdownRef}&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if 문 안의 조건이 많이쓰이는 패턴이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dropdownRef 가 현재 참이고 &amp;amp;&amp;amp; 클릭했을때, 컨테인이 포함하는 부분 외부일 경우! setOpen을 false 로 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;탭&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;Tabs&amp;gt; 라이브러리 없이 구현하기&lt;/p&gt;
&lt;pre id=&quot;code_1723020214238&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//자식탭
interface TabsProps {
  children: React.ReactNode;
}

const Tabs = ({ children }: TabsProps) =&amp;gt; {
  const [activeIndex, setActiveIndex] = useState(0);
  const tabs = React.Children.toArray(
    children,
  ) as React.ReactElement&amp;lt;TabProps&amp;gt;[];

  return (
    &amp;lt;TabsStyle&amp;gt;
      &amp;lt;div className=&quot;tab-header&quot;&amp;gt;
        {tabs.map((tab, index) =&amp;gt; (
          &amp;lt;button
            onClick={() =&amp;gt; setActiveIndex(index)}
            className={activeIndex === index ? 'active' : ''}
          &amp;gt;
            {tab.props.title}
          &amp;lt;/button&amp;gt;
        ))}
      &amp;lt;/div&amp;gt;
      &amp;lt;div className=&quot;tab-content&quot;&amp;gt;{tabs[activeIndex]}&amp;lt;/div&amp;gt;
    &amp;lt;/TabsStyle&amp;gt;
  );
};

//BookDetail.tsx
&amp;lt;div className=&quot;content&quot;&amp;gt;
        &amp;lt;Tabs&amp;gt;
          &amp;lt;Tab title=&quot;상세설명&quot;&amp;gt;
            &amp;lt;Title size=&quot;medium&quot;&amp;gt;상세 설명&amp;lt;/Title&amp;gt;
            &amp;lt;EllipsisBox linelimit={4}&amp;gt;{book.detail}&amp;lt;/EllipsisBox&amp;gt;
          &amp;lt;/Tab&amp;gt;

          &amp;lt;Tab title=&quot;목차&quot;&amp;gt;
            &amp;lt;Title size=&quot;medium&quot;&amp;gt;목차&amp;lt;/Title&amp;gt;
            &amp;lt;p className=&quot;index&quot;&amp;gt;{book.contents}&amp;lt;/p&amp;gt;
          &amp;lt;/Tab&amp;gt;

          &amp;lt;Tab title=&quot;리뷰&quot;&amp;gt;
            &amp;lt;Title size=&quot;medium&quot;&amp;gt;리뷰&amp;lt;/Title&amp;gt;
            &amp;lt;BookReview reviews={reviews} onAdd={addReview} /&amp;gt;
          &amp;lt;/Tab&amp;gt;
        &amp;lt;/Tabs&amp;gt;
      &amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;activeIndex를 이용해서 상태를 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;children을 받아서 React.Children.toArray 메서드로 tab 변수에 할당한다. 그리고 map 으로 순회하면서 상태관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토스트 showAlert&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러개의 토스트를 열릴 수 있도록, zustand 로 전역 상태관리를 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;toastStore.ts&lt;/p&gt;
&lt;pre id=&quot;code_1723021151350&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const useToastStore = create&amp;lt;ToastStoreState&amp;gt;((set) =&amp;gt; ({
  toasts: [],
  addToast: (message, type = 'info') =&amp;gt; {
    set((state) =&amp;gt; ({
      toasts: [...state.toasts, { message, type, id: Date.now() }],
    }));
  },
  removeToast: (id) =&amp;gt; {
    set((state) =&amp;gt; ({
      toasts: state.toasts.filter((toast) =&amp;gt; toast.id !== id),
    }));
  },
}));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hook으로 만들어서 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1723021922816&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const useToast = () =&amp;gt; {
  const showToast = useToastStore((state) =&amp;gt; state.addToast);
  return { showToast };
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Toast 는 사이트 전역에서 표시되어야 하기 때문에,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너를 생성해서 상위App.tsx에 넣어주어야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1723022497421&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const ToastContainer = () =&amp;gt; {
  const toasts = useToastStore((state) =&amp;gt; state.toasts);
  return (
    &amp;lt;ToastContainerStyle&amp;gt;
      {toasts.map((toast) =&amp;gt; (
        &amp;lt;Toast
          key={toast.id}
          id={toast.id}
          message={toast.message}
          type={toast.type}
        /&amp;gt;
      ))}
    &amp;lt;/ToastContainerStyle&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3초 뒤에 fadeout 되면서 사라짐&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`onAnimationEnd`&lt;/p&gt;
&lt;pre id=&quot;code_1723026978522&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
    const timer = setTimeout(() =&amp;gt; {
      //delete
      handleRemoveToast();
    }, TOAST_REMOVE_DELAY);
    return () =&amp;gt; clearTimeout(timer);
  }, []);

  const handleAnimationEnd = () =&amp;gt; {
    if (isFadingOut) {
      removeToast(id);
    }
  };

//..
 @keyframes fade-in {
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }

  @keyframes fade-out {
    from {
      opacity: 1;
    }
    to {
      opacity: 0;
    }
  }

  &amp;amp;.fade-in {
    animation: fade-in 0.3s ease-in-out forwards;
  }

  &amp;amp;.fade-out {
    animation: fade-out 0.3s ease-in-out forwards;
  }
  
  opacity: 0;
  transition: all 0.3s ease-in-out;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드 timeout 을 많이 사용하기 때문에, hook 으로 분리해서 사용하는게 좋다.&lt;/p&gt;
&lt;pre id=&quot;code_1723027219368&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const useTimeout = (callback: () =&amp;gt; void, delay: number) =&amp;gt; {
  useEffect(() =&amp;gt; {
    const timer = setTimeout(callback, delay);
    return () =&amp;gt; clearTimeout(timer);
  }, [callback, delay]);
};

export default useTimeout;
//...
  useTimeout(() =&amp;gt; {
    setIsFadingOut(true);
  }, TOAST_REMOVE_DELAY);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모달&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;modal-body, modal-contents 구조로 나뉘고, contents 내부에 자식요소를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;top, left 를 50%로 지정하면, div의 왼쪽상단 모서리가 기준이기 때문에, `&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;transform: translate(-50%, -50%);` 처럼 div 너비의 절반으로 중앙정렬할 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;x, 외부 클릭시 닫힘, 이미지 클릭시 열림&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ ESC 누르면 닫힘&lt;/p&gt;
&lt;pre id=&quot;code_1723032057347&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; const handleKeydown = (e: KeyboardEvent) =&amp;gt; {
    if (e.key === 'Escape') {
      handleClose();
    }
  };

  useEffect(() =&amp;gt; {
    if (isOpen) {
      window.addEventListener('keydown', handleKeydown);
    } else {
      window.removeEventListener('keydown', handleKeydown);
    }

    return () =&amp;gt; {
      window.removeEventListener('keydown', handleKeydown);
    };
  }, [isOpen]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트리스너 등록하고, key값에 엑세스해서 키에 대한 이벤트 분기하는 부분 체크하기!&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;모달(토스트)의 위치는 header 의 아래가 아닌, 전체 root 의 바로 아래에 위치해야한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;gt; 리액트 create portal 기능(react-dom)&lt;/h3&gt;
&lt;pre id=&quot;code_1723033473511&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { createPortal } from 'react-dom';
//..
return createPortal(
    &amp;lt;ModalStyle
      className={isFadingOut ? 'fade-out' : 'fade-in'}
      onClick={handleOverlayClick}
      onAnimationEnd={handleAnimationEnd}
    &amp;gt;
      &amp;lt;div className=&quot;modal-body&quot; ref={modalRef}&amp;gt;
        &amp;lt;div className=&quot;modal-contents&quot;&amp;gt;{children}&amp;lt;/div&amp;gt;
        &amp;lt;button className=&quot;modal-close&quot; onClick={handleClose}&amp;gt;
          &amp;lt;FaPlus /&amp;gt;
        &amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/ModalStyle&amp;gt;,
    document.body,
  );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;무한 스크롤&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-query의 infinite queries 기능을 제공한다!&lt;/p&gt;
&lt;figure id=&quot;og_1723034014098&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Infinite Queries | TanStack Query React Docs&quot; data-og-description=&quot;Does this replace [Redux, MobX, etc]? react&quot; data-og-host=&quot;tanstack.com&quot; data-og-source-url=&quot;https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries&quot; data-og-url=&quot;https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/jIiGD/hyWKJf3F3H/Ijv45y4M4PgSZpuKCmerb0/img.png?width=3000&amp;amp;height=1704&amp;amp;face=0_0_3000_1704&quot;&gt;&lt;a href=&quot;https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/jIiGD/hyWKJf3F3H/Ijv45y4M4PgSZpuKCmerb0/img.png?width=3000&amp;amp;height=1704&amp;amp;face=0_0_3000_1704');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Infinite Queries | TanStack Query React Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Does this replace [Redux, MobX, etc]? react&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tanstack.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;return의 구조의 변화 `flatMap` 사용&lt;/p&gt;
&lt;pre id=&quot;code_1723035886096&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;결과 = {
	books, pagination
}

-----------------------
결과 = {
	pages: [
    	{books, pagination},
        {books, pagination},
        {books, pagination},
    ]
}

const books = data ? data.pages.flatMap((page) =&amp;gt; page.books) : [];
  const pagination = data ? data.pages[data.pages.length - 1].pagination : {};
  const isEmpty = books.length === 0;

  return {
    books,
    pagination,
    isEmpty,
    isBooksLoading: isFetching,
    fetchNextPage,
    hasNextPage,
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1723035948691&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Array.prototype.flatMap() - JavaScript | MDN&quot; data-og-description=&quot;Array 인스턴스의 flatMap() 메서드는 배열의 각 요소에 주어진 콜백 함수를 적용한 다음 그 결과를 한 단계씩 평탄화하여 형성된 새 배열을 반환합니다. 이 메서드는 map() 뒤에 깊이 1의 flat()을 붙이&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap&quot; data-og-url=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/qXo5D/hyWKw2bARx/PCKYiyTMsFHSdxaFXKu5j0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/qXo5D/hyWKw2bARx/PCKYiyTMsFHSdxaFXKu5j0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Array.prototype.flatMap() - JavaScript | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Array 인스턴스의 flatMap() 메서드는 배열의 각 요소에 주어진 콜백 함수를 적용한 다음 그 결과를 한 단계씩 평탄화하여 형성된 새 배열을 반환합니다. 이 메서드는 map() 뒤에 깊이 1의 flat()을 붙이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;더보기 감지 IntersectionObserver API 방식&lt;/h3&gt;
&lt;figure id=&quot;og_1723039595249&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;IntersectionObserver - Web API | MDN&quot; data-og-description=&quot;Intersection Observer API의 IntersectionObserver 인터페이스는 대상 요소와 상위 요소, 또는 대상 요소와 최상위 문서의 뷰포트가 서로 교차하는 영역이 달라지는 경우 이를 비동기적으로 감지할 수 있는 &quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/API/IntersectionObserver&quot; data-og-url=&quot;https://developer.mozilla.org/ko/docs/Web/API/IntersectionObserver&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/BAESc/hyWKDfXOEu/m39LXIzKDaGtICqLGHYx3K/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/IntersectionObserver&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/API/IntersectionObserver&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/BAESc/hyWKDfXOEu/m39LXIzKDaGtICqLGHYx3K/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;IntersectionObserver - Web API | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Intersection Observer API의 IntersectionObserver 인터페이스는 대상 요소와 상위 요소, 또는 대상 요소와 최상위 문서의 뷰포트가 서로 교차하는 영역이 달라지는 경우 이를 비동기적으로 감지할 수 있는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰포트와 타겟과 교차했을때 특정값을 return 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트에 붙는게 아닌, 지속석으로 reActive 상태처럼 동작한다.&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;특정 돔이 화면에 등장한다면, fetchNext 가 동작하도록!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;more 컨테이너를 관찰하고 있다가, 해당 컨테이너가 보인다면 isIntersecting 한다면 (뷰포트와 타겟의 교차) 사용자 눈 화면에 보인다면?, 다음페이지를 로드한다.&lt;/p&gt;
&lt;pre id=&quot;code_1723042456911&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface ObserverOptions {
  root?: Element | null;
  rootMargin?: string;
  threshold?: number | number[]; //0 or [0,1]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;root : 관찰되는 대상의 기준점. 요소일 수도 있고, 없을수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rootMargin : 교차되는 거리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;threshold : 교차의 비율. 타겟값 요소가 화면에 얼만큼 보일때 할것인지 0에서 ~1까지&lt;/p&gt;
&lt;pre id=&quot;code_1723042927958&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const useIntersectionObserver = (
  callback: Callback,
  options?: ObserverOptions,
) =&amp;gt; {
  const targetRef = useRef(null);

  useEffect(() =&amp;gt; {
    const observer = new IntersectionObserver(callback, options);

    if (targetRef.current) {
      observer.observe(targetRef.current);
    }

    return () =&amp;gt; {
      if (targetRef.current) {
        observer.unobserve(targetRef.current);
      }
    };
  });
  return targetRef;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;return이 꼭 필요함!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;훅 호출 후 사용법&lt;/p&gt;
&lt;pre id=&quot;code_1723042976019&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const moreRef = useIntersectionObserver(([entry]) =&amp;gt; {
    if (entry.isIntersecting) {
      loadMore();
    }
  });

  const loadMore = () =&amp;gt; {
    if (!hasNextPage) return;
    fetchNextPage();
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️ 배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리가 아닌, UI 구현을 직접 할 수 있어서 좋았음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능구현 과제 중에는 라이브러리 아닌 직접구현 한다면 가산점이 생긴다던데,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모달, 탭, 드롭다운의 경우 직접 구현한 바 있어서 복습해서 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토스트는 react-toast 를 썼었는데, 라이브러리 아닌 직접 구현도 생각보다 복잡하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무한스크롤은 와 어려웠다. react-query의 Infinity queries 를 사용하는 방법1, IntersectionObserver 로 스크롤인식하는 방법2&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-query를 이용해서 데이터 가공한다면, 사용해도 좋은 방식, 아니라면 IntersectionObserver를 사용하는것이 좋아보인다.&lt;/p&gt;</description>
      <category>웹 풀 사이클 데브코스</category>
      <category>til</category>
      <category>데브코스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/183</guid>
      <comments>https://datdaradanadat.tistory.com/183#entry183comment</comments>
      <pubDate>Fri, 26 Jul 2024 14:48:01 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] Day65 - useAuth, react-query, 스니펫</title>
      <link>https://datdaradanadat.tistory.com/182</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;중간 회고&lt;/h2&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;/li&gt;
&lt;li&gt;문제 해결&lt;/li&gt;
&lt;li&gt;유연성과 적응성 (애자일)&lt;/li&gt;
&lt;li&gt;퍼포먼스 향상&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;더 잘하려고!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;theme.ts 컬리 키를 사용하는 인터페이스를 선언해서 사이드이펙트, 정의, 활용을 하게 되었다.&lt;/li&gt;
&lt;li&gt;사용하는 데이터 모델을 인터페이스로 정리, export 해서 재사용하였다. 이때 모델끼리도 재사용, 분리, extends 확장으로 상속받아서 동작할 수있도록 확용하였다.&lt;/li&gt;
&lt;li&gt;상태 Props 의 데이터 타입 정의, UI 타입 정의, Pick(선택), Omit(제외) 과 같은 타입스크립트 문법 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;데이터 흐름
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도서 Books.tsx &amp;gt; 도서목록 컴포넌트 BooksList.tsx &amp;gt; useBooks.ts 도서목록 받아오는 훅 &amp;gt; books.api.ts httpClient.ts 를 통해 보내고 받아온 엔드포인트 url 에 따라 데이터 요청을 보내고 받아와 response 를 받음. &amp;gt; useBooks.ts 에서 받아온 데이터 가공 &amp;gt; BooksList.tsx 에서 가공된 데이터 사용 &amp;gt; Books.tsx 화면에 표현
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;훅으로 api 를 사용하면 &amp;gt; 파라미터 관리, 재사용성, setIsEmpty 상태값 활용할 수 있다.&lt;/li&gt;
&lt;li&gt;낙관적 업데이트에&amp;nbsp; 좋다. 별도의 서버요청없이 화면 업데이트 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;컴포넌트 작성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Books.twsx 화면은 isEmpty 상태에 따라 BooksList 컴포넌트와 Pagination 컴포넌트, BooksEmpty 컴포넌트를 렌더링한다.&lt;/li&gt;
&lt;li&gt;삼항 연산식으로도 컴포넌트를 렌더링할 수 있다.&lt;/li&gt;
&lt;li&gt;BookList.tsx 에서 각 도서 아이템은 BookItem 의 반복으로 화면에 렌더링할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;css 스타일링 : 생산성 저하 요소 중 하나..
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;flex, grid 필수!&lt;/li&gt;
&lt;li&gt;래거시 absolute position 알아두기 중요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;커스텀 훅
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;use앞에 붙어야 훅으로써 동작할 수 있다.&lt;/li&gt;
&lt;li&gt;별도의 상태를 가지고 전달한다면, 훅으로 항상 사용될 수 있다.&lt;/li&gt;
&lt;li&gt;작성방법, 기능, 사용처 알아두기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생산성 : VScode Snippet 스니펫&lt;/h3&gt;
&lt;figure id=&quot;og_1722832100080&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Snippets in Visual Studio Code&quot; data-og-description=&quot;It is easy to add code snippets to Visual Studio Code both for your own use or to share with others on the public Extension Marketplace. TextMate .tmSnippets files are supported.&quot; data-og-host=&quot;code.visualstudio.com&quot; data-og-source-url=&quot;https://code.visualstudio.com/docs/editor/userdefinedsnippets&quot; data-og-url=&quot;https://code.visualstudio.com/docs/editor/userdefinedsnippets&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bAM3xd/hyWKIgzpOc/8kyjl0E4tUqo3CPhYpmUy0/img.png?width=1012&amp;amp;height=506&amp;amp;face=0_0_1012_506,https://scrap.kakaocdn.net/dn/cS90xG/hyWKB2OQIW/YsdikK8UBXUyktDULoIsE0/img.png?width=906&amp;amp;height=398&amp;amp;face=0_0_906_398,https://scrap.kakaocdn.net/dn/blzeXA/hyWKySyxTS/M5JBwQ7Xsk8EJtqyKd5s91/img.png?width=916&amp;amp;height=374&amp;amp;face=0_0_916_374&quot;&gt;&lt;a href=&quot;https://code.visualstudio.com/docs/editor/userdefinedsnippets&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://code.visualstudio.com/docs/editor/userdefinedsnippets&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bAM3xd/hyWKIgzpOc/8kyjl0E4tUqo3CPhYpmUy0/img.png?width=1012&amp;amp;height=506&amp;amp;face=0_0_1012_506,https://scrap.kakaocdn.net/dn/cS90xG/hyWKB2OQIW/YsdikK8UBXUyktDULoIsE0/img.png?width=906&amp;amp;height=398&amp;amp;face=0_0_906_398,https://scrap.kakaocdn.net/dn/blzeXA/hyWKySyxTS/M5JBwQ7Xsk8EJtqyKd5s91/img.png?width=916&amp;amp;height=374&amp;amp;face=0_0_916_374');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Snippets in Visual Studio Code&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;It is easy to add code snippets to Visual Studio Code both for your own use or to share with others on the public Extension Marketplace. TextMate .tmSnippets files are supported.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;code.visualstudio.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 설정할 수 있는 코드 더미&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;데이터 모델 정의&lt;/li&gt;
&lt;li&gt;컴포넌트 생성&lt;/li&gt;
&lt;li&gt;배치&lt;/li&gt;
&lt;li&gt;1차 스타일링 (api 요청 이후에 할 시 원치않는 데이터가 표시될 가능성 방지)&lt;/li&gt;
&lt;li&gt;데이터 패치 / 훅&lt;/li&gt;
&lt;li&gt;이벤트 핸들러&lt;/li&gt;
&lt;li&gt;추가 스타일링&lt;/li&gt;
&lt;li&gt;리팩토링&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vscode 확장 프로그램&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Snippet Generator&lt;/p&gt;
&lt;figure id=&quot;og_1722837546586&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Snippet Generator - Visual Studio Marketplace&quot; data-og-description=&quot;Extension for Visual Studio Code - Simple snippet generator for VSCode&quot; data-og-host=&quot;marketplace.visualstudio.com&quot; data-og-source-url=&quot;https://marketplace.visualstudio.com/items?itemName=fiore57.snippet-generator&quot; data-og-url=&quot;https://marketplace.visualstudio.com/items?itemName=fiore57.snippet-generator&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=fiore57.snippet-generator&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://marketplace.visualstudio.com/items?itemName=fiore57.snippet-generator&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Snippet Generator - Visual Studio Marketplace&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Extension for Visual Studio Code - Simple snippet generator for VSCode&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;marketplace.visualstudio.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;KPT 회고&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Keep&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;회원가입~주문까지 경험&lt;/li&gt;
&lt;li&gt;생산성 고려&lt;/li&gt;
&lt;li&gt;데이터 흐름 맞추기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Problem&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;테마스위처 미적용&lt;/li&gt;
&lt;li&gt;import alias 아쉬움&lt;/li&gt;
&lt;li&gt;중복코드&lt;/li&gt;
&lt;li&gt;css 스타일링 정리&lt;/li&gt;
&lt;li&gt;UI 패턴의 다양성&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Try&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;alias&lt;/li&gt;
&lt;li&gt;중복코드 제거&lt;/li&gt;
&lt;li&gt;스니펫&lt;/li&gt;
&lt;li&gt;useAuth 훅&lt;/li&gt;
&lt;li&gt;react-query&lt;/li&gt;
&lt;li&gt;다양한 UI 패턴&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Import alias&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;절대경로 환경설정&lt;/p&gt;
&lt;figure id=&quot;og_1722833911674&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - risen228/craco-alias: A craco plugin for automatic aliases generation for Webpack and Jest&quot; data-og-description=&quot;A craco plugin for automatic aliases generation for Webpack and Jest - risen228/craco-alias&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/risen228/craco-alias&quot; data-og-url=&quot;https://github.com/risen228/craco-alias&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cM4vW5/hyWKEeamQL/YCtHAaTKqGKCD2Goedb6Q0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/risen228/craco-alias&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/risen228/craco-alias&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cM4vW5/hyWKEeamQL/YCtHAaTKqGKCD2Goedb6Q0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - risen228/craco-alias: A craco plugin for automatic aliases generation for Webpack and Jest&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A craco plugin for automatic aliases generation for Webpack and Jest - risen228/craco-alias&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CRACO 라이브러리 : config설정을 CRA에 상속하여 따로 뭘 건드리지 않고 추가적인 설정을 부여할 수 있게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tsconfig.paths.json extends 사용&lt;/p&gt;
&lt;pre id=&quot;code_1722834232344&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//tsconfig.json
&quot;extends&quot;: &quot;./tsconfig.paths.json&quot;,
  &quot;include&quot;: [&quot;src&quot;, &quot;craco.config.js&quot;]
  //tsconfig.paths.json
{
  &quot;compilerOptions&quot;: {
    &quot;baseUrl&quot;: &quot;.&quot;,
    &quot;paths&quot;: {
      &quot;@/*&quot;: [&quot;src/*&quot;]
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;craco.config.js&lt;/p&gt;
&lt;pre id=&quot;code_1722834414221&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const cracoAlias = require('craco-alias');

module.exports = {
  plugin: [
    {
      plugin: cracoAlias,
      options: {
        source: 'tsconfig',
        baseUrl: '.',
        tsConfigPath: 'tsconfig.paths.json',
        debug: false,
      },
    },
  ],
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 원리는 tsconfig의 baseUrl과 paths에 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;route 정리&lt;/h4&gt;
&lt;pre id=&quot;code_1722835177135&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const routeList = [
  {
    path: '/',
    element: &amp;lt;Home /&amp;gt;,
  },
  {
    path: '/books',
    element: &amp;lt;Books /&amp;gt;,
  },
//..
const router = createBrowserRouter(
  routeList.map((item) =&amp;gt; {
    return {
      ...item,
      element: &amp;lt;Layout&amp;gt;{item.element}&amp;lt;/Layout&amp;gt;,
      errorElement: &amp;lt;Error /&amp;gt;,
    };
  }),
);

function App() {
  return (
    &amp;lt;BookStoreThemeProvider&amp;gt;
      &amp;lt;RouterProvider router={router} /&amp;gt;
    &amp;lt;/BookStoreThemeProvider&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React-Query 비동기 관리자&lt;/h2&gt;
&lt;figure id=&quot;og_1722838994006&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Overview | TanStack Query React Docs&quot; data-og-description=&quot;TanStack Query (FKA React Query) is often described as the missing data-fetching library for web applications, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your web applications a breeze. Motivation Mo&quot; data-og-host=&quot;tanstack.com&quot; data-og-source-url=&quot;https://tanstack.com/query/latest/docs/framework/react/overview&quot; data-og-url=&quot;https://tanstack.com/query/latest/docs/framework/react/overview&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://tanstack.com/query/latest/docs/framework/react/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tanstack.com/query/latest/docs/framework/react/overview&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Overview | TanStack Query React Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;TanStack Query (FKA React Query) is often described as the missing data-fetching library for web applications, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your web applications a breeze. Motivation Mo&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tanstack.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 useEffect 로 작성한 코드를, 자동적으로 fetch 하고 캐싱하는 라이브러리.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;query client 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`QueryClient` 사용, 최상위 컴포넌트에 (app.tsx) Provider로 감싸야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1722848925669&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//queryClient.ts
import { QueryClient } from &quot;react-query&quot;;
export const queryClient = new QueryClient();

//App.tsx
function App() {
  return (
    &amp;lt;QueryClientProvider client={queryClient}&amp;gt;
      &amp;lt;BookStoreThemeProvider&amp;gt;
        &amp;lt;RouterProvider router={router} /&amp;gt;
      &amp;lt;/BookStoreThemeProvider&amp;gt;
    &amp;lt;/QueryClientProvider&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 useState, useEffect 코드 작성을 &amp;gt; useQuery(키(배열), )&lt;/p&gt;
&lt;pre id=&quot;code_1722848837637&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const useBooks = () =&amp;gt; {
  const location = useLocation();
  const params = new URLSearchParams(location.search);

  const { data: booksData, isLoading: isBooksLoading } = useQuery(
    ['books', location.search],
    () =&amp;gt;
      fetchBooks({
        category_id: params.get(QUERYSTRING.CATEGORY_ID)
          ? Number(params.get(QUERYSTRING.CATEGORY_ID))
          : undefined,
        news: params.get(QUERYSTRING.NEWS) ? true : undefined,
        currentPage: params.get(QUERYSTRING.PAGE)
          ? Number(params.get(QUERYSTRING.PAGE))
          : 1,
        limit: LIMIT,
      }),
  );

  return {
    books: booksData?.books,
    pagination: booksData?.pagination,
    isEmpty: booksData?.books.length === 0,
    isBooksLoading,
  };
};

//books.tsx
  const { books, pagination, isEmpty, isBooksLoading } = useBooks();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 조건이 있을때 &amp;gt; early return&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722848786250&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  if (isEmpty) return &amp;lt;BooksEmpty /&amp;gt;;

  if (!books || !pagination || isBooksLoading) return &amp;lt;Loading /&amp;gt;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️ 배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스니펫&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-query&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 가끔 스타일링이나 alias 가 적용이 안될까...&lt;/p&gt;</description>
      <category>웹 풀 사이클 데브코스</category>
      <category>til</category>
      <category>데브코스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/182</guid>
      <comments>https://datdaradanadat.tistory.com/182#entry182comment</comments>
      <pubDate>Thu, 25 Jul 2024 15:57:30 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] Day64 - 장바구니 목록, 주문서 작성</title>
      <link>https://datdaradanadat.tistory.com/181</link>
      <description>&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 장바구니에 담은 내역을 확인&lt;/li&gt;
&lt;li&gt;선택한 도서 아이템의 수량과 가격 합계를 표시&lt;/li&gt;
&lt;li&gt;장바구니 담은 도서 아이템을 삭제&lt;/li&gt;
&lt;li&gt;각 도서 아이템을 선택하여 주문서 작성&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;이렇게 필요한 기능을 미리 작성한 뒤 구현하는 방식.. 좋다!&amp;nbsp;&lt;/span&gt;&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;장바구니 내역 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cart api 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 항목 선택해서 수량 추가하는 방법 중.. Input Check타입을 사용하는 방법도 있지만, 아이콘으로 동작하도록 하는 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루프되는 아이템이 추가타입을 넣거나 or &lt;b&gt;체크된 데이터 배열을 (상태) 새로 생성하는 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 배열 아이템 컴포넌트의 해당 id를 가진 데이터가 목록에 있는지 판단해야 한다. checked&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복되는 상태 확인 `useMemo()`&lt;/p&gt;
&lt;pre id=&quot;code_1722577031126&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; //carts.tsx
 const [checkedItems, setCheckedItems] = useState&amp;lt;number[]&amp;gt;([]);

  const handleCheckedItem = (id: number) =&amp;gt; {
    if (checkedItems.includes(id)) {
      //언체크
      setCheckedItems(checkedItems.filter((item) =&amp;gt; item !== id));
    } else {
      //체크
      setCheckedItems([...checkedItems, id]);
      return;
    }
  };
  
 //cartItem.tsx
 const isChecked = useMemo(() =&amp;gt; {
    return checkedItems.includes(cart.id);
  }, [checkedItems, cart.id]);

  const handleChecked = () =&amp;gt; {
    onCheck(cart.id);
  };
  return (
    &amp;lt;CartItemStyle&amp;gt;
      &amp;lt;div className=&quot;info&quot;&amp;gt;
        &amp;lt;CheckIconButton isChecked={isChecked} onCheck={handleChecked} /&amp;gt;

//CheckIconButton.tsx
interface Props {
  isChecked: boolean;
  onCheck: () =&amp;gt; void;
}

const CheckIconButton = ({ isChecked, onCheck }: Props) =&amp;gt; {
  return (
    &amp;lt;CheckIconButtonStyle onClick={onCheck}&amp;gt;
      {isChecked ? &amp;lt;FaRegCheckCircle /&amp;gt; : &amp;lt;FaRegCircle /&amp;gt;}
    &amp;lt;/CheckIconButtonStyle&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;낙관적 업데이트&lt;/h2&gt;
&lt;pre id=&quot;code_1722617627746&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const deleteCartItem = (id: number) =&amp;gt; {
    deleteCart(id).then(() =&amp;gt; {
      setCarts(carts.filter((cart) =&amp;gt; cart.id !== id));
    });
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 api 요청을 보내는게 아닌, 명시적으로 요청을 보내면서 ui 를 변경시킨다. 이후 다시 데이터를 로드하거나 받아오는 행위를 했을때 api 요청을 보내 잘 업데이트 되었는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;alert + confirm&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 , 취소. 메시지와 함수를 함께 넘겨서 동작하게 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1722618472821&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const showConfirm = useCallback((message: string, onConfirm: () =&amp;gt; void) =&amp;gt; {
    if (window.confirm(message)) {
      onConfirm();
    }
  }, []);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`flex: 1;` : 늘어나겠다. 0은 늘리지 않겠다는 동작&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코커톤에서 같은 프론트가 요걸로 나머지를 다 채웠길래.. 난 잘 안써서 왜쓰는지 물어봤다. flex 적용한 나머지 부분에 `width: 100%` 와 같은 효과.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 div 를 잘못 나눠서 내가 다시 수정했다... 안좋은 기억으로 마주친 flex:1 ... 많이 사용하겠지?&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;총 수량, 총 금액&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체크된 아이템의 length 로 할 수 있지만, 같은 도서 여러권일 경우가 안된다. reduce 메서드로 각 아이템을 전체적으로 순회하면서 직접 더해주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 금액의 경우, 각 가격 * 수량을 누적으로 동일하게 더해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1722619688091&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const totalQuantity = useMemo(() =&amp;gt; {
    return carts.reduce((acc, cart) =&amp;gt; {
      if (checkedItems.includes(cart.id)) {
        return acc + cart.quantity;
      }
      return acc;
    }, 0);
  }, [carts, checkedItems]);
  
    const totalPrice = useMemo(() =&amp;gt; {
    return carts.reduce((acc, cart) =&amp;gt; {
      if (checkedItems.includes(cart.id)) {
        return acc + (cart.price*cart.quantity);
      }
      return acc;
    }, 0);
  }, [carts, checkedItems]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 계산 같은 경우에는 백엔드에서 진행하기도 하지만, 프론트에서 동적으로 계산하는 경우가 더 많다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 더 많은 api 호출을 줄이고, 서버 성능을 위해 할 수 있는 처리는 프론트에서 하고싶은게 아닐까..&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;라우트 데이터 전달&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useNavigate 함수 안에 state 항목을 활용해서 정보를 전달한다.&lt;/p&gt;
&lt;pre id=&quot;code_1722621355586&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const handleOrder = () =&amp;gt; {
    if (checkedItems.length === 0) {
      showAlert('주문할 상품을 선택해 주세요.');
      return;
    }
    //주문서 작성으로 데이터 전달
    const orderData: Omit&amp;lt;OrderSheet, 'delivery'&amp;gt; = {
      items: checkedItems,
      totalPrice,
      totalQuantity,
      firstBookTitle: carts[0].title,
    };
    showConfirm('주문하시겠습니까?', () =&amp;gt; {
      navigate('/order', { state: orderData });
    });
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이동된 링크에서는 location 함수로 state 를 받아서 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1722622149479&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const location = useLocation();
const orderDataFromCart = location.state;
const {totalQuantity, totalPrice} = orderDataFromCart;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;# 주문서 작성&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;장바구니로 부터 넘어온 정보를 화면에 표시하고 보존&lt;/li&gt;
&lt;li&gt;주소, 수령인, 전화번호를 입력 및 검증&lt;/li&gt;
&lt;li&gt;데이터를 가공하여 서버에 전달&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;useFrom()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useForm 에는 register, handleSubmit, formState, 타입을 지정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입은 order 모델의 delivery 만떼와서, 상세정보를 추가해서 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레지스터는 데이터의 키값을 찾아서 체크한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;form 외부의 버튼을 눌렀을때, submit 동작하도록 함수처리 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 fieldset 은 입력이 안되었을때 에러를 띄울 수 있다. (formState)&lt;/p&gt;
&lt;pre id=&quot;code_1722625905025&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm&amp;lt;DeliveryForm&amp;gt;();
  
//..
&amp;lt;InputText
	inputType=&quot;text&quot;
	{...register('address', { required: true })}
/&amp;gt;

//...
&amp;lt;Button
	size=&quot;large&quot;
	scheme=&quot;primary&quot;
	onClick={handleSubmit(handlePay)}
&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클릭하면, 주문서 페이지에서 데이터를 뽑아온 다음, 서버로 넘겨준다.&lt;/p&gt;
&lt;pre id=&quot;code_1722626680510&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const handlePay = (data: DeliveryForm) =&amp;gt; {
	const orderData: OrderSheet = {
      ...orderDataFromCart,
      delivery: {
        ...data,
        address: `${data.address} ${data.addressDetail}`,
      },
    };
    //서버로 넘겨준다
    console.log(orderData);
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주소찾기 카카오 우편번호 서비스 (타입스크립트)&lt;/h2&gt;
&lt;figure id=&quot;og_1722626726056&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Daum 우편번호 서비스&quot; data-og-description=&quot;우편번호 검색과 도로명 주소 입력 기능을 너무 간단하게 적용할 수 있는 방법. Daum 우편번호 서비스를 이용해보세요. 어느 사이트에서나 무료로 제약없이 사용 가능하답니다.&quot; data-og-host=&quot;postcode.map.daum.net&quot; data-og-source-url=&quot;https://postcode.map.daum.net/guide&quot; data-og-url=&quot;https://postcode.map.daum.net/guide&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/GMS1D/hyWG3zy5Yh/XQwpq4hOQ3RbLeAWs6plq0/img.png?width=402&amp;amp;height=219&amp;amp;face=0_0_402_219&quot;&gt;&lt;a href=&quot;https://postcode.map.daum.net/guide&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://postcode.map.daum.net/guide&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/GMS1D/hyWG3zy5Yh/XQwpq4hOQ3RbLeAWs6plq0/img.png?width=402&amp;amp;height=219&amp;amp;face=0_0_402_219');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Daum 우편번호 서비스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;우편번호 검색과 도로명 주소 입력 기능을 너무 간단하게 적용할 수 있는 방법. Daum 우편번호 서비스를 이용해보세요. 어느 사이트에서나 무료로 제약없이 사용 가능하답니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;postcode.map.daum.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리엑트 라이브러리 존재.... 하지만!! 다음 제공 api 를 사용해보자!&lt;/p&gt;
&lt;figure id=&quot;og_1722626811914&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - kmsbernard/react-daum-postcode: 리액트 컴포넌트로 만든 Daum 우편번호 검색 서비스입니다.&quot; data-og-description=&quot;리액트 컴포넌트로 만든 Daum 우편번호 검색 서비스입니다. Contribute to kmsbernard/react-daum-postcode development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/kmsbernard/react-daum-postcode&quot; data-og-url=&quot;https://github.com/kmsbernard/react-daum-postcode&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/1z92i/hyWKI1AOM7/cJ35v9hgTlc7fZZQXjapW1/img.png?width=1200&amp;amp;height=600&amp;amp;face=958_151_1040_241&quot;&gt;&lt;a href=&quot;https://github.com/kmsbernard/react-daum-postcode&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/kmsbernard/react-daum-postcode&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/1z92i/hyWKI1AOM7/cJ35v9hgTlc7fZZQXjapW1/img.png?width=1200&amp;amp;height=600&amp;amp;face=958_151_1040_241');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - kmsbernard/react-daum-postcode: 리액트 컴포넌트로 만든 Daum 우편번호 검색 서비스입니다.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;리액트 컴포넌트로 만든 Daum 우편번호 검색 서비스입니다. Contribute to kmsbernard/react-daum-postcode development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스크립트 로드&lt;/li&gt;
&lt;li&gt;핸들러&lt;/li&gt;
&lt;li&gt;입력&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크립트 로드 시 index.html에 사용하기엔 번거로우니, useEffect 에서 스크립트 로딩되도록 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1722627415778&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  useEffect(() =&amp;gt; {
    const script = document.createElement('script'); //&amp;lt;script&amp;gt;&amp;lt;/script&amp;gt;
    script.src = SCRIPT_URL; //&amp;lt;script src=&quot;URL&quot;&amp;gt;&amp;lt;/script&amp;gt;
    script.async = true;
    document.head.appendChild(script); //&amp;lt;head&amp;gt;&amp;lt;script src=&quot;URL&quot;&amp;gt;&amp;lt;/script&amp;gt;&amp;lt;/head&amp;gt;

    return () =&amp;gt; {
      document.head.removeChild(script);
    };
  });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트 고려가 안된 라이브러리를 사용할때 &amp;gt; `window.d.ts` 파일 새로 만들어서 타입 정의&lt;/p&gt;
&lt;pre id=&quot;code_1722627685649&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface Window {
  daum: {
    Postcode: any;
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;form 내부 버튼은 기본으로 submit 이 동작하기 때문에 type 를 버튼으로!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트로 주소창 여는 핸들러&lt;/p&gt;
&lt;pre id=&quot;code_1722627965236&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const FindAddressButton = ({ onCompleted }: Props) =&amp;gt; {
  const handleOpen = () =&amp;gt; {
    new window.daum.Postcode({
      oncomplete: (data: any) =&amp;gt; {
        onCompleted(data.address as string);
      },
    }).open();
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전달된 address 를 useFrom 의 setValue(key, value)&amp;nbsp; 활용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`&amp;lt;FindAddressButton onCompleted={(address) =&amp;gt; { setValue('address', address); }} /&amp;gt;`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버로 넘겨준다&lt;/p&gt;
&lt;pre id=&quot;code_1722628466889&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const order = async (orderData: OrderSheet) =&amp;gt; {
  const response = await httpClient.post('/orders', orderData);
  return response.data;
};

//..
showConfirm('주문을 진행하시겠습니까?', () =&amp;gt; {
      order(orderData).then(() =&amp;gt; {
        showAlert('주문이 처리되었습니다');
        navigate('/orderList');
      });
    });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;#주문 내역&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;저장된 주문 내역을 목록(table)로 표시&lt;/li&gt;
&lt;li&gt;[자세히] 버튼으로 상세 정보 패널을 토글&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 존재하지 않으면, 더이상 요청하지 않도록! &amp;gt; 이거 ,,, 이전프로젝트때 useEffect 에서 렌더링이 많이되어서 어려웠었는데,, 해결방법을 알았다!&lt;/p&gt;
&lt;pre id=&quot;code_1722630668834&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const selectedOrderItem = (orderId: number) =&amp;gt; {
    if (orders.filter((item) =&amp;gt; item.id === orderId)[0].detail) {
      setSelectedItemId(orderId);
      return;
    }
    fetchOrder(orderId).then((orderDetail) =&amp;gt; {
      setSelectedItemId(orderId);
      setOrders(
        orders.map((item) =&amp;gt; {
          if (item.id === orderId) {
            return {
              ...item,
              detail: orderDetail,
            };
          }
          return item;
        }),
      );
    });
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if 문으로 요청방어&lt;/p&gt;</description>
      <category>웹 풀 사이클 데브코스</category>
      <category>til</category>
      <category>데브코스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/181</guid>
      <comments>https://datdaradanadat.tistory.com/181#entry181comment</comments>
      <pubDate>Wed, 24 Jul 2024 01:51:24 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] Day63 - Detail 상세페이지</title>
      <link>https://datdaradanadat.tistory.com/180</link>
      <description>&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;각 도서 상세 정보를 노출&lt;/li&gt;
&lt;li&gt;좋아요 버튼을 클릭시 좋아요 또는 취소 기능&lt;/li&gt;
&lt;li&gt;수량을 입력하여 장바구니 담기&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;null 일수도 있는 경우 타입에러&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;early return 방식을 사용할 수 있다! 배워감&lt;/p&gt;
&lt;pre id=&quot;code_1722494294933&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from 'react';
import { useParams } from 'react-router-dom';
import styled from 'styled-components';
import { useBook } from '../hooks/useBook';

const BookDetail = () =&amp;gt; {
  const { bookId } = useParams();
  const { book } = useBook(bookId);

  if (!book) return null;
  
  return &amp;lt;BookDetailStyle&amp;gt;{book.title}&amp;lt;/BookDetailStyle&amp;gt;;
};

const BookDetailStyle = styled.div``;

export default BookDetail;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도서목록 중복 키배열&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카테고리, 포맷, 등 같은 형식으로 중복되기 때문에 하드코딩이 아닌! 데이터의 키를 가지고 배열을 만들어서 처리하기&lt;/p&gt;
&lt;pre id=&quot;code_1722498320552&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const BookInfoList = [
  {
    label: '카테고리',
    key: 'category_name',
    filter: (book: IBookDetail) =&amp;gt; (
      &amp;lt;Link to={`/books?category_id=${book.category_id}`}&amp;gt;
        {book.category_name}
      &amp;lt;/Link&amp;gt;
    ),
  },
  {
    label: '포맷',
    key: 'form',
  },
  {
    label: '페이지',
    key: 'pages',
  },
  {
    label: 'ISBN',
    key: 'isbn',
  },
  {
    label: '출간일',
    key: 'pubDate',
    filter: (book: IBookDetail) =&amp;gt; {
      return `${formatDate(book.pubDate)}`;
    },
  },
  {
    label: '가격',
    key: 'price',
    filter: (book: IBookDetail) =&amp;gt; {
      return `${formatNumber(book.price)} 원`;
    },
  },
];
//...
{BookInfoList.map((item) =&amp;gt; (
	&amp;lt;dl&amp;gt;
		&amp;lt;dt&amp;gt;{item.label} &amp;lt;/dt&amp;gt;
		&amp;lt;dd&amp;gt;
			{item.filter
			? item.filter(book)
			: book[item.key as keyof IBookDetail]}
		&amp;lt;/dd&amp;gt;
	&amp;lt;/dl&amp;gt;
))}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;날짜 포매팅 Day.js : 가볍고, 많은 기능을 가졌음!&lt;/p&gt;
&lt;figure id=&quot;og_1722495484619&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Day.js &amp;middot; 2kB JavaScript date utility library&quot; data-og-description=&quot;2kB JavaScript date utility library&quot; data-og-host=&quot;day.js.org&quot; data-og-source-url=&quot;https://day.js.org/docs/en/installation/node-js&quot; data-og-url=&quot;https://day.js.org/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://day.js.org/docs/en/installation/node-js&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://day.js.org/docs/en/installation/node-js&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Day.js &amp;middot; 2kB JavaScript date utility library&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;2kB JavaScript date utility library&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;day.js.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;긴 줄글 더보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`overflow: hidden;` 일정 height 이상 넘어가면 가려라는 css 항목&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 대응할 수 없다는 단점이 있다. 높이보다, 줄 개수대로 적용하는게 필요하다.&lt;/p&gt;
&lt;pre id=&quot;code_1722499026018&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: ${({ linelimit }) =&amp;gt; linelimit};;
-webkit-box-orient: vertical;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;overflow 가 생기면 `ellipsis` 점점점으로 표시하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`-webkit-box` 항목으로 지정한다. `line-clamp` 는 4줄까지만 표기하고, `box-orient` 는 방향을 뜻한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;linelimt 을 props 상태로 전달할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토글&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;div는 attribute 로 string 만 받을 수 있고, boolean 은 받을 수 없다. 이때 앞에 $ 를 붙인다.&lt;/p&gt;
&lt;pre id=&quot;code_1722529628175&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const EllipsisBox = ({ children, linelimit }: Props) =&amp;gt; {
  const [expanded, setExpanded] = useState(false);
  return (
    &amp;lt;EllipsisBoxStyle linelimit={linelimit} $expanded={expanded}&amp;gt;
      &amp;lt;p&amp;gt;{children}&amp;lt;/p&amp;gt;
      &amp;lt;div className=&quot;toggle&quot;&amp;gt;
        &amp;lt;Button
          size=&quot;small&quot;
          scheme=&quot;normal&quot;
          onClick={() =&amp;gt; setExpanded(!expanded)}
        &amp;gt;
          {expanded ? '접기' : '펼치기'} &amp;lt;FaAngleDown /&amp;gt;
        &amp;lt;/Button&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/EllipsisBoxStyle&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;좋아요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낙관적 업데이트 진행한다. 화면에서 즉각적으로 좋아요 수가 증가하는 걸 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 fetch 중복 요청을 제거하는 경우가 많다!&lt;/p&gt;
&lt;pre id=&quot;code_1722536645580&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const likeToggle = () =&amp;gt; {
    //권한 확인
    if (!isloggedIn) {
      showAlert('로그인이 필요합니다.');
      return;
    }

    if (!book) return;

    if (book.liked) {
      //라이크 상태 &amp;gt; 언라이크 실행
      unlikeBook(book.id).then(() =&amp;gt; {
        setBook({
          ...book,
          liked: false,
          likes: book.likes - 1,
        });
      });
    } else {
      //언라이크 상태 &amp;gt; 라이크를 실행
      likeBook(book.id).then(() =&amp;gt; {
        //성공처리
        setBook({
          ...book,
          liked: true,
          likes: book.likes + 1,
        });
      });
    }
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;장바구니 list&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setTimeout 으로 클릭하고 3초 뒤 사라지고, css 애니메이션 적용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1722539895627&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const addToCart = () =&amp;gt; {
    addCart({
      book_id: book.id,
      quantity: quantity,
    }).then(() =&amp;gt; {
      setCartAdded(true);
      setTimeout(() =&amp;gt; {
        setCartAdded(false);
      }, 3000);
    });
  };
//...
    opacity: ${({ $added }) =&amp;gt; ($added ? '1' : '0')};
    transition: all 0.5s ease;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️ 배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;</description>
      <category>웹 풀 사이클 데브코스</category>
      <category>til</category>
      <category>데브코스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/180</guid>
      <comments>https://datdaradanadat.tistory.com/180#entry180comment</comments>
      <pubDate>Tue, 23 Jul 2024 21:49:43 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] Day62 - 로그인의 전역상태 Zustand, 도서목록조회 category</title>
      <link>https://datdaradanadat.tistory.com/179</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;비밀번호 초기화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호 까먹었을때 &amp;gt; 초기화 요청하는 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모킹 방법. 실무에서는 토큰을 전달하거나 헤더에 토큰을 심기도 하는 방법을 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1722320716655&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//auth.api
export const resetRequest =async (data: SignupProps) =&amp;gt; {
  const response = await httpClient.post('/users/reset', data); //데이터 바디로 보내준다
  return response.data;
};

export const resetPwd =async (data: SignupProps) =&amp;gt; {
  const response = await httpClient.put('/users/reset', data); //데이터 바디로 보내준다
  return response.data;
};

//ResetPwd.tsx
//...
const [resetRequested, setResetRequested] = useState(false);
  const onSubmit = (data: SignupProps) =&amp;gt; {
    if (resetRequested) {
        resetPwd(data).then(() =&amp;gt; {
            showAlert(&quot;비밀번호가 초기화되었습니다.&quot;);
            navigate(&quot;/login&quot;);
        })
    } else {
      //초기화 요청
      resetRequest(data).then(() =&amp;gt; {
        setResetRequested(true);
      });
    }
  };
  //...
  {resetRequested &amp;amp;&amp;amp; (
            &amp;lt;fieldset&amp;gt;
              &amp;lt;InputText
                placeholder=&quot;비밀번호&quot;
                inputType=&quot;password&quot;
                {...register('password', { required: true })}
              /&amp;gt;
              {errors.password &amp;amp;&amp;amp; (
                &amp;lt;p className=&quot;error-text&quot;&amp;gt;비밀번호를 입력해주세요.&amp;lt;/p&amp;gt;
              )}
            &amp;lt;/fieldset&amp;gt;
          )}
          &amp;lt;fieldset&amp;gt;
            &amp;lt;Button type=&quot;submit&quot; size=&quot;medium&quot; scheme=&quot;primary&quot;&amp;gt;
              {resetRequested ? '비밀번호 초기화' : '초기화 요청'}
            &amp;lt;/Button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그인 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 성공 &amp;gt; 서버에서 주는 토큰 획득 &amp;gt; 로그인 전역 상태 GlobalState를 저장하기 위해 상태관리도구 Zustand를 사용한다. `isLoggedIn:true` + localstorage 토큰 저장 &amp;lt; http client, headers.Authorization&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;context api 컨텍스트 api로 테마 전역 상태관리를 한 바 있는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 정보는 주스탠드 ZUSTAND 를 사용해서 관리해보겠다! (지금 다른 플젝에서는 jotai 사용중인데 비교해보기)&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;토큰을 쿠키에 담아서 헤더가 아닌, body 로 전송하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`return res.status(StatusCodes.OK).json({...results[0], token:token});`&lt;/p&gt;
&lt;pre id=&quot;code_1722335198333&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface LoginResponse {
  token: string;
}

export const login = async (data: SignupProps) =&amp;gt; {
  const response = await httpClient.post&amp;lt;LoginResponse&amp;gt;('/users/login', data); //데이터 바디로 보내준다
  return response.data;
};

//Login.tsx
const onSubmit = (data: SignupProps) =&amp;gt; {
    login(data).then((res)=&amp;gt;{
        console.log(res);
        showAlert(&quot;로그인 완료&quot;);
        navigate('/');
    })
  };
  ///...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지는 로그인의 처음만 동작하는거고, local 에 해당 정보를 저장하고, 전역으로 관리해야만 한다.&lt;/p&gt;
&lt;figure id=&quot;og_1722354210156&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - pmndrs/zustand:   Bear necessities for state management in React&quot; data-og-description=&quot;  Bear necessities for state management in React. Contribute to pmndrs/zustand development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/pmndrs/zustand&quot; data-og-url=&quot;https://github.com/pmndrs/zustand&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/OhUAY/hyWGQ7vJzq/Vdoac3J2kMU2IdIgRYGtV1/img.png?width=800&amp;amp;height=467&amp;amp;face=0_0_800_467&quot;&gt;&lt;a href=&quot;https://github.com/pmndrs/zustand&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/pmndrs/zustand&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/OhUAY/hyWGQ7vJzq/Vdoac3J2kMU2IdIgRYGtV1/img.png?width=800&amp;amp;height=467&amp;amp;face=0_0_800_467');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - pmndrs/zustand:   Bear necessities for state management in React&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  Bear necessities for state management in React. Contribute to pmndrs/zustand development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;store 폴더 &amp;gt; `authStore.ts`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zustand interface 는 상태정보 isloggedIn 상태정보와 액션함수 storeLogin 함수를 같이 저장한다.&lt;/p&gt;
&lt;pre id=&quot;code_1722354615783&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { create } from 'zustand';

interface StoreState {
  isloggedIn: boolean;
  storeLogin: (token: string) =&amp;gt; void;
  storeLogout: () =&amp;gt; void;
}

export const useAuthStore = create&amp;lt;StoreState&amp;gt;((set) =&amp;gt; ({
  isloggedIn: getToken() ? true : false, //초기값
  storeLogin: (token: string) =&amp;gt; {
    set({ isloggedIn: true });
    setToken(token);
  },
  storeLogout: () =&amp;gt; {
    set({ isloggedIn: false });
    removeToken();
  },
}));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zustand 에서 토큰 설정을 해주어야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1722354918073&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const getToken = () =&amp;gt; {
  const token = localStorage.getItem('token');
  return token;
};

const setToken = (token: string) =&amp;gt; {
  localStorage.setItem('token', token);
};

const removeToken = () =&amp;gt; {
  localStorage.removeItem('token');
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰을 localStorage 에 저장하고 지우는 함수.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로고침이나 창닫침에도 로그인 유지하도록 getToken 으로 관리한다.&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;로그인 상태인 isloggedIn 을 헤더에서 사용하는 방법?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zustand 의 useAuthStore 을 가져와서, isloggedIn 체크를 하고, 값이 true 일경우, false 일경우 분기해서 표시한다.&lt;/p&gt;
&lt;pre id=&quot;code_1722358196196&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const { isloggedIn, storeLogout } = useAuthStore();
//...
 {isloggedIn &amp;amp;&amp;amp; (
	&amp;lt;ul&amp;gt;
		&amp;lt;li&amp;gt;장바구니&amp;lt;/li&amp;gt;
	&amp;lt;/ul&amp;gt;
)}
{!isloggedIn&amp;amp;&amp;amp; (
	&amp;lt;ul&amp;gt;
		&amp;lt;li&amp;gt;
//...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그아웃 사용은, 로그아웃 btn 클릭 시 onClick useAuthStore 에서 작성한 함수인 storeLogout 이 호출되도록 하면 된다.&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;api 요청에서 headers 에 인증정보를 함께 요청보내는 방법으로, http.ts 코드의 headers 부분에 추가한다. 토큰 만료 시, 401 에러가 발생하기 때문에, localstorage 의 토큰을 지우고, login 페이지로 이동한다.(navigate를 바로 쓸 수 없다)&lt;/p&gt;
&lt;pre id=&quot;code_1722358750671&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const createClient = (config?: AxiosRequestConfig) =&amp;gt; {
  const axiosInstance = axios.create({
    baseURL: BASE_URL,
    timeout: DEFAULT_TIMEOUT,
    headers: {
      'content-type': 'application/json',
      Authorization: getToken() ? getToken() : '',
    },
    withCredentials: true,
    ...config,
  });

  axiosInstance.interceptors.response.use(
    (response) =&amp;gt; {
      return response;
    },
    (error) =&amp;gt; {
      //로그인 만료 처리
      if (error.response.status === 401) {
        removeToken();
        window.location.href = '/login';
        return;
      }

      //에러처리, 로그인 만료 시 실행안됨
      return Promise.reject(error);
    },
  );
  return axiosInstance;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;339&quot; data-origin-height=&quot;167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kFhaw/btsIR1BlZMu/s27bYPEH3HgJbXqsievagK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kFhaw/btsIR1BlZMu/s27bYPEH3HgJbXqsievagK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kFhaw/btsIR1BlZMu/s27bYPEH3HgJbXqsievagK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkFhaw%2FbtsIR1BlZMu%2Fs27bYPEH3HgJbXqsievagK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;339&quot; height=&quot;167&quot; data-origin-width=&quot;339&quot; data-origin-height=&quot;167&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 틀렸을 경우, 처음에는 401 에러가 발생한다. 권한없음. then 으로 처리해주는게 좋다.&lt;/p&gt;
&lt;pre id=&quot;code_1722359024400&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;login(data).then(
	(res) =&amp;gt; {
		storeLogin(res.token);
        console.log(res.token);
        showAlert('로그인 완료');
        navigate('/');
	},
	(err) =&amp;gt; {
        showAlert('로그인 실패');
	},
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도서 목록 페이지&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;도서의 목록을 fetch 하여 화면에 렌더&lt;/li&gt;
&lt;li&gt;페이지네이션&lt;/li&gt;
&lt;li&gt;검색결과 없을때, 결과 없음 화면 놏ㄹ&lt;/li&gt;
&lt;li&gt;카테고리 및 신간 필터 기능 제공&lt;/li&gt;
&lt;li&gt;목록의 view 는 그리드 형태, 목록 형태로 변경 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Books 페이지에 하위 컴포넌트로 구성한다&lt;/p&gt;
&lt;pre id=&quot;code_1722406731339&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Books = () =&amp;gt; {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Title size=&quot;large&quot;&amp;gt;도서 검색 결과&amp;lt;/Title&amp;gt;
      &amp;lt;BookStyle&amp;gt;
        &amp;lt;BooksFilter /&amp;gt;
        &amp;lt;BooksViewSwitcher /&amp;gt;
        &amp;lt;BooksList /&amp;gt;
        &amp;lt;BooksEmpty /&amp;gt;
        &amp;lt;Pagination /&amp;gt;
      &amp;lt;/BookStyle&amp;gt;
    &amp;lt;/&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 로드할때 매번 url 을 매칭하지 않도록 utils 에서 image.ts 로 매핑해준다. 모듈화!&lt;/p&gt;
&lt;pre id=&quot;code_1722406941828&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//image.ts
export const getImgSrc=(id:number) =&amp;gt;{
return `https://picsum.photos/id/${id}/600/600`
}

//BookItem.tsx
&amp;lt;img src={getImgSrc(book.id)} alt={book.title} /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;css : inline-flex, position: relative, absolute, static&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BookItem.spec.tsx 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경이 많이 일어날 예정인 컴포넌트들은 테스트를 돌리는게 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThemeProvider 가 적용되어있는 코드라면, 필수로 import 해주어야 css 가 잘 적용된다.&lt;/p&gt;
&lt;pre id=&quot;code_1722409303872&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from 'react';
import { render } from '@testing-library/react';
import BookItem from './BookItem';
import { BookStoreThemeProvider } from '../../context/themeContext';

const dummyBook = {
  id: 1,
  title: 'dummy Book',
  img: 5,
  category_id: 1,
  form: 'paperback',
  isbn: 'dummy isbn',
  summary: 'dummy summary',
  detail: 'dummy detail',
  author: 'dummy author',
  pages: 100,
  contents: 'dummy contents',
  price: 10000,
  likes: 1,
  pubDate: '2024-07-31',
};

describe('BookItem', () =&amp;gt; {
  it('렌더 여부', () =&amp;gt; {
    const { getByText, getByAltText } = render(
      &amp;lt;BookStoreThemeProvider&amp;gt;
        &amp;lt;BookItem book={dummyBook} /&amp;gt;
      &amp;lt;/BookStoreThemeProvider&amp;gt;,
    );
    //제목에 화면에 잘 렌더링 되느냐
    expect(getByText(dummyBook.title)).toBeInTheDocument();
    expect(getByText(dummyBook.summary)).toBeInTheDocument();
    expect(getByText(dummyBook.author)).toBeInTheDocument();
    expect(getByText('10,000원')).toBeInTheDocument();
    expect(getByText(dummyBook.likes)).toBeInTheDocument();
    expect(getByAltText(dummyBook.title)).toHaveAttribute(
      'src',
      `https://picsum.photos/id/${dummyBook.img}/600/600`,
    );
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BooksEmpty&lt;/h3&gt;
&lt;pre id=&quot;code_1722409760622&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from 'react';
import { FaSmileWink } from 'react-icons/fa';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import Title from '../common/Title';

const BooksEmpty = () =&amp;gt; {
  return (
    &amp;lt;BooksEmptyStyle&amp;gt;
      &amp;lt;div className=&quot;icon&quot;&amp;gt;
        &amp;lt;FaSmileWink /&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;Title size=&quot;large&quot; color=&quot;secondary&quot;&amp;gt;
        검색 결과가 없습니다.
      &amp;lt;/Title&amp;gt;
      &amp;lt;p&amp;gt;
        &amp;lt;Link to=&quot;/books&quot;&amp;gt;전체 검색 결과로 이동&amp;lt;/Link&amp;gt;
      &amp;lt;/p&amp;gt;
    &amp;lt;/BooksEmptyStyle&amp;gt;
  );
};

const BooksEmptyStyle = styled.div`
  display: flex
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 12px;
  padding: 120px 0;

  .icon{
    svg{
      font-size:4rem;
      fill:#ccc;
    }
}
`;

export default BooksEmpty;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BooksFilter.tsx 카테고리, 신간여부 (쿼리스트링)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카테고리는 useState 가 아닌 쿼리스트링으로 제공되어 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리스트링으로 상태를 유지 및 공유할 수 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재사용성, 검색엔진 최적화 유리, 마케팅의 데이터추적과 분석에 용이하다는 장점을 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/books&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;?category_id=0&lt;/span&gt;&amp;amp;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;news=true&lt;/span&gt;&amp;amp;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;view=grid&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Filter 컴포넌트에서 각 필더의 변경 QS 를 요청한다 &amp;gt; useBooks api훅에서 QS 변경을 감지하고, fetch 를 실행시켜 화면을 갱신한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 필터에서 QS의 상태를 변경하고, QS는 상태를 유지한 채, 훅에서 해당 상태로 갱신하는 api 를 요청한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1722410602124&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;URLSearchParams - Web API | MDN&quot; data-og-description=&quot;URLSearchParams 인터페이스는 URL의 쿼리 문자열을 대상으로 작업할 수 있는 유틸리티 메서드를 정의합니다.&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/API/URLSearchParams&quot; data-og-url=&quot;https://developer.mozilla.org/ko/docs/Web/API/URLSearchParams&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bie72d/hyWGP8M4DF/eRkU4c6LftjUFbZPLO8DtK/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/URLSearchParams&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/API/URLSearchParams&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bie72d/hyWGP8M4DF/eRkU4c6LftjUFbZPLO8DtK/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;URLSearchParams - Web API | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;URLSearchParams 인터페이스는 URL의 쿼리 문자열을 대상으로 작업할 수 있는 유틸리티 메서드를 정의합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유틸리티 메서드 new 를 했을때 인스턴스를 생성하고, 쿼리스트링의 access, set 을 할 수 있는 권한을 부여한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722417260317&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import styled from 'styled-components';
import { useCategory } from '../../hooks/useCategory';
import Button from '../common/Button';
import { useSearchParams } from 'react-router-dom';

const BooksFilter = () =&amp;gt; {
  const { category } = useCategory();
  const [searchParams, setSearchParams] = useSearchParams();

  const handleCategory = (id: number | null) =&amp;gt; {
    const newSearchParams = new URLSearchParams(searchParams);

    if (id === null) {
      newSearchParams.delete('category_id');
    } else {
      newSearchParams.set('category_id', id.toString());
    }
    setSearchParams(newSearchParams);
  };

  const handleNews = () =&amp;gt; {
    const newSearchParams = new URLSearchParams(searchParams);

    if (newSearchParams.get('news')) {
      newSearchParams.delete('news');
    } else {
      newSearchParams.set('news', 'true');
    }

    setSearchParams(newSearchParams);
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;BooksFilterStyle&amp;gt;
        &amp;lt;div className=&quot;category&quot;&amp;gt;
          {category.map((item) =&amp;gt; (
            &amp;lt;Button
              size=&quot;medium&quot;
              scheme={item.isActive ? 'primary' : 'normal'}
              key={item.category_id}
              onClick={() =&amp;gt; handleCategory(item.category_id)}
            &amp;gt;
              {item.category_name}
            &amp;lt;/Button&amp;gt;
          ))}
        &amp;lt;/div&amp;gt;
        &amp;lt;div className=&quot;new&quot;&amp;gt;
          &amp;lt;Button
            size=&quot;medium&quot;
            scheme={searchParams.get('news') ? 'primary' : 'normal'}
            onClick={() =&amp;gt; handleNews()}
          &amp;gt;
            신간
          &amp;lt;/Button&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/BooksFilterStyle&amp;gt;
    &amp;lt;/&amp;gt;
  );
};

const BooksFilterStyle = styled.div`
  display: flex;
  gap: 24px;

  .category {
    display: flex;
    gap: 8px;
  }
`;

export default BooksFilter;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;훅에서 값이 존재하는지, isActive 매칭해준다.&lt;/h4&gt;
&lt;pre id=&quot;code_1722417304983&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const setActive = () =&amp;gt; {
    const params = new URLSearchParams(location.search);
    if (params.get('category_id')) {
      setCategory((prev) =&amp;gt; {
        return prev.map((item) =&amp;gt; {
          return {
            ...item,
            isActive: item.category_id === Number(params.get('category_id')),
          };
        });
      });
    } else {
      setCategory((prev) =&amp;gt; {
        return prev.map((item) =&amp;gt; {
          return {
            ...item,
            isActive: false,
          };
        });
      });
    }
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;gt; 반복되는 상수 constent 폴더로 관리&lt;/p&gt;
&lt;pre id=&quot;code_1722417589092&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const QUERYSTRING = {
  CATEGORY_ID: 'category_id',
  NEWS: 'news',
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;useBooks.tsx 커스텀훅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경된 쿼리스트링을 감지해서 fetch하는 기능&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;api는 params로 가져온다.&lt;/p&gt;
&lt;pre id=&quot;code_1722422145996&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Book } from '../models/book.model';
import { Pagination } from '../models/pagination.model';
import { httpClient } from './http';

interface FetchBooksParams {
  category_id?: number;
  news?: boolean;
  currentPage?: number;
  limit: number;
}

interface FetchBooksResponse {
  books: Book[];
  pagination: Pagination;
}

export const fetchBooks = async (params: FetchBooksParams) =&amp;gt; {
  try {
    const response = await httpClient.get&amp;lt;FetchBooksResponse&amp;gt;('/books', {
      params: params,
    });
    return response.data;
  } catch (error) {
    return {
      books: [],
      pagination: {
        totalCount: 0,
        currentPage: 1,
      },
    };
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fetchBooks로 params 값을 빼서 할당한다. 존재할 경우 set 함수로 새로할당한다.&lt;/p&gt;
&lt;pre id=&quot;code_1722422220393&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Book } from '../models/book.model';
import { Pagination } from '../models/pagination.model';
import { fetchBooks } from '../api/books.api';
import { QUERYSTRING } from '../constants/querystring.';
import { LIMIT } from '../constants/pagination';

export const useBooks = () =&amp;gt; {
  const location = useLocation();

  const [books, setBooks] = useState&amp;lt;Book[]&amp;gt;([]);
  const [pagination, setPagination] = useState&amp;lt;Pagination&amp;gt;({
    totalCount: 0,
    currentPage: 1,
  });

  const [isEmpty, setIsEmpty] = useState(true);

  useEffect(() =&amp;gt; {
    const params = new URLSearchParams(location.search);

    fetchBooks({
      category_id: params.get(QUERYSTRING.CATEGORY_ID)
        ? Number(params.get(QUERYSTRING.CATEGORY_ID))
        : undefined,
      news: params.get(QUERYSTRING.NEWS) ? true : undefined,
      currentPage: params.get(QUERYSTRING.PAGE)
        ? Number(params.get(QUERYSTRING.PAGE))
        : 1,
      limit: LIMIT,
    }).then(({ books, pagination }) =&amp;gt; {
      setBooks(books);
      setPagination(pagination);
      setIsEmpty(books.length === 0);
    });
  }, [location.search]);

  return { books, pagination, isEmpty };
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BookList 컴포넌트에 book 데이터 상태로 넘겨서, map.item.id 로 순회해서 표시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;페이지네이션 pagination&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useBooks 에서 받아온 page 데이터를 pagination 값으로 전달한다. 클릭될 경우, params 에서 QS 페이지 값으로 세팅한다. 클릭 scheme 활성화 비활성화 작업을 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1722424195342&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from 'react';
import { Pagination as IPagination } from '../../models/pagination.model';
import { LIMIT } from 'styled-components/dist/utils/createWarnTooManyClasses';
import Button from '../common/Button';
import { useSearchParams } from 'react-router-dom';
import { QUERYSTRING } from '../../constants/querystring.';
import styled from 'styled-components';

interface Props {
  pagination: IPagination;
}

const Pagination = ({ pagination }: Props) =&amp;gt; {
  const [searchParams, setSearchParams] = useSearchParams();
  const { totalCount, currentPage } = pagination;
  const pages: number = Math.ceil(totalCount / LIMIT);

  const handleClickPage = (page: number) =&amp;gt; {
    const newSearchParams = new URLSearchParams(searchParams);
    newSearchParams.set(QUERYSTRING.PAGE, page.toString());

    setSearchParams(newSearchParams);
  };

  return (
    &amp;lt;PaginationStyle&amp;gt;
      {pages &amp;gt; 0 &amp;amp;&amp;amp; (
        &amp;lt;ol&amp;gt;
          {Array(pages)
            .fill(0)
            .map((_, index) =&amp;gt; (
              &amp;lt;li&amp;gt;
                &amp;lt;Button
                  size=&quot;small&quot;
                  scheme={index + 1 === currentPage ? 'primary' : 'normal'}
                  key={index}
                  onClick={() =&amp;gt; handleClickPage(index + 1)}
                &amp;gt;
                  {index + 1}
                &amp;lt;/Button&amp;gt;
              &amp;lt;/li&amp;gt;
            ))}
        &amp;lt;/ol&amp;gt;
      )}
    &amp;lt;/PaginationStyle&amp;gt;
  );
};

const PaginationStyle = styled.div`
  display: flex;
  justify-content: start;
  align-items: center;
  padding: 24px 0;

  ol {
    list-style: none;
    display: flex;
    gap: 8px;
    padding: 0;
    margin: 0;
  }
`;

export default Pagination;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Grid vs List&lt;/h3&gt;
&lt;pre id=&quot;code_1722428402313&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useEffect } from 'react';
import styled from 'styled-components';
import Button from '../common/Button';
import { FaList, FaTh } from 'react-icons/fa';
import { QUERYSTRING } from '../../constants/querystring.';
import { useSearchParams } from 'react-router-dom';

const viewOptions = [
  {
    value: 'list',
    icon: &amp;lt;FaList /&amp;gt;,
  },
  {
    value: 'grid',
    icon: &amp;lt;FaTh /&amp;gt;,
  },
];

export type ViewMode = 'grid' | 'list';

const BooksViewSwitcher = () =&amp;gt; {
  const [searchParams, setSearchParams] = useSearchParams();

  const handleSwitch = (value: ViewMode) =&amp;gt; {
    const newSearchParams = new URLSearchParams(searchParams);
    newSearchParams.set(QUERYSTRING.VIEW, value);
    setSearchParams(newSearchParams);
  };

  useEffect(() =&amp;gt; {
    if (!searchParams.get(QUERYSTRING.VIEW)) {
      handleSwitch('grid');
    }
  }, []);

  return (
    &amp;lt;BooksViewSwitcherStyle&amp;gt;
      {viewOptions.map((option) =&amp;gt; (
        &amp;lt;Button
          key={option.value}
          size=&quot;medium&quot;
          scheme={
            searchParams.get(QUERYSTRING.VIEW) === option.value
              ? 'primary'
              : 'normal'
          }
          onClick={() =&amp;gt; handleSwitch(option.value as ViewMode)}
        &amp;gt;
          {option.icon}
        &amp;lt;/Button&amp;gt;
      ))}
    &amp;lt;/BooksViewSwitcherStyle&amp;gt;
  );
};

const BooksViewSwitcherStyle = styled.div`
  display: flex;
  gap: 8px;
  svg {
    fill: #fff;
  }
`;

export default BooksViewSwitcher;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pick 타입 유틸리티&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;특정 타입에서 속성 몇 개를 제외한 나머지 속성으로 새로운 타입을 생성할 때 사용하는&amp;nbsp;&lt;/span&gt;유틸리티&lt;span style=&quot;text-align: left;&quot;&gt;&amp;nbsp;타입&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gird 일경우, list 일경우 상태를 저장해, 스타일링에서 분기처리한다.&lt;/p&gt;
&lt;pre id=&quot;code_1722428433567&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from 'react';
import styled from 'styled-components';
import { Book } from '../../models/book.model';
import { getImgSrc } from '../../utils/image';
import { formatNumber } from '../../utils/foramt';
import { FaHeart } from 'react-icons/fa';
import { ViewMode } from './BooksViewSwitcher';

interface Props {
  book: Book;
  view?: ViewMode;
}

const BookItem = ({ book, view }: Props) =&amp;gt; {
  return (
    &amp;lt;BookItemStyle view={view}&amp;gt;
      &amp;lt;div className=&quot;img&quot;&amp;gt;
        &amp;lt;img src={getImgSrc(book.img)} alt={book.title} /&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div className=&quot;content&quot;&amp;gt;
        &amp;lt;h2 className=&quot;title&quot;&amp;gt;{book.title}&amp;lt;/h2&amp;gt;
        &amp;lt;p className=&quot;summary&quot;&amp;gt;{book.summary}&amp;lt;/p&amp;gt;
        &amp;lt;p className=&quot;author&quot;&amp;gt;{book.author}&amp;lt;/p&amp;gt;
        &amp;lt;p className=&quot;price&quot;&amp;gt;{formatNumber(book.price)}원&amp;lt;/p&amp;gt;
        &amp;lt;div className=&quot;likes&quot;&amp;gt;
          &amp;lt;FaHeart /&amp;gt;
          &amp;lt;span&amp;gt;{book.likes}&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/BookItemStyle&amp;gt;
  );
};

const BookItemStyle = styled.div&amp;lt;Pick&amp;lt;Props, 'view'&amp;gt;&amp;gt;`
  display: flex;
  flex-direction: ${({ view }) =&amp;gt; (view === 'grid' ? 'colunm' : 'row')};
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);

  .img {
    border-radius: ${({ theme }) =&amp;gt; theme.borderRadius.default};
    overflow: hidden;
    width: ${({ view }) =&amp;gt; (view === 'grid' ? 'auto' : '160px')};
    img {
      max-width: 100%;
    }
  }

  .content {
    padding: 16px;
    position: relative;
    flex: ${({ view }) =&amp;gt; (view === 'grid' ? 0 : 1)};
  }
//...
`;

export default BookItem;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️ 배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;휘몰아치는 강의정보...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입 유틸리티 Pick&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Button 정의할때 onClick 타입 지정해두지 않으니 함수가 실행되지 않는 문제점 발생함... 타입 컴포넌트로 활용하려면 어려움이 있었다..ㅜ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QS 쿼리스트링으로 상태전달 방식 대신하는게 있었다. 그러니까... 카테고리 컴포넌트에서 클릭하면 QS 를 params에 적용하고, 컨텐츠 컴포넌트에서 URL의 QS 인 params 를 코드로 땡겨와서, 가공하는 방식! showMailer 프로젝트에서는 상태관리와 컴포넌트로 모든 카테고리와 검색기능을 구현했는데(모바일고려) 강사님 설명에서는 쿼리파람스로 관리하는게 검색 및 데이터분석, 코드가공의 문제에서 장점이 있다고 하셨다. &amp;gt; 리팩토링할 부분?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 포트가 자꾸 문제라서 힘들다ㅜ 역시 배포가 답(?)&lt;/p&gt;</description>
      <category>웹 풀 사이클 데브코스</category>
      <category>til</category>
      <category>데브코스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/179</guid>
      <comments>https://datdaradanadat.tistory.com/179#entry179comment</comments>
      <pubDate>Fri, 19 Jul 2024 03:46:43 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] Day61 - 라우트, api 통신, 회원가입</title>
      <link>https://datdaradanadat.tistory.com/178</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;라우트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 및 경로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`npm i react-router-dom @types/react-router-dom --save` React Router 를 사용해서 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 경로는 /~&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도서상세만 /books/{id}&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;route 에서 존재하지 않는 에러페이지가 발생할 경우? `errorElement` 항목으로 예외처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류 예외처리 Error.tsx&lt;/p&gt;
&lt;pre id=&quot;code_1721318604398&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useRouteError } from 'react-router-dom';

interface RouteError {
  statusText?: string;
  message?: string;
}

function Error() {
  const error = useRouteError() as RouteError;
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;오류발생&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;다음과 같은 오류가 발생했습니다&amp;lt;/p&amp;gt;
      &amp;lt;p&amp;gt;{error.statusText || error.message}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default Error;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RouteError 타입, useRouteError 메서드를 react-router-dom 에서 기본으로 제공한다.&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;화면깜빡임은 a 태그가 아닌 Link 태그로 대체하면 제대로 동작한다. 내 프로젝트에서는 window.location.href 를 사용하는데 이것또한 a 태그처럼 화면깜빡임이 일어나는 메서드!! 잘 모르고 js 기반에서 동작하는 함수라고 해서 사용했는데.. 전체적으로 수정하였다! onClick 이나 js 에서 필요할 경우 &lt;b&gt;useNavigate() 메서드&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;app.tsx 에서 route가 어디에 적용되어 감싸졌는지 잘 체크해야 에러를 막을 수 있다. 근데 계속 element 를 layout 으로 감쌀수도 없는 모냥인데,,, 내 프로젝트에서는 route와 무관한 Layout.tsx이어서 route 외부에 두었다. route와 연관된 디자인컴포넌트일경우?&lt;/p&gt;
&lt;pre id=&quot;code_1721321170584&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Layout from './components/layout/Layout';
import Home from './pages/Home';
import { BookStoreThemeProvider } from './context/themeContext';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Error from './components/common/Error';

const router = createBrowserRouter([
  {
    path: '/',
    element: (
      &amp;lt;Layout&amp;gt;
        &amp;lt;Home /&amp;gt;
      &amp;lt;/Layout&amp;gt;
    ),
    errorElement: &amp;lt;Error /&amp;gt;,
  },
  {
    path: '/books',
    element: (
      &amp;lt;Layout&amp;gt;
        &amp;lt;div&amp;gt;도서목록&amp;lt;/div&amp;gt;
      &amp;lt;/Layout&amp;gt;
    ),
  },
]);

function App() {
  return (
    &amp;lt;BookStoreThemeProvider&amp;gt;
      &amp;lt;RouterProvider router={router} /&amp;gt;
    &amp;lt;/BookStoreThemeProvider&amp;gt;
  );
}

export default App;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 모델 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;interface 백엔드 데이터 타입 모델 작성하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 전체 책 조회, 책 상세조회는 타입이 유사하기 때문에 확장하여 코드를 작성하는 것이 좋다.&lt;/p&gt;
&lt;pre id=&quot;code_1721322409919&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export interface Book {
  id: number;
  title: string;
  img: number;
  category_id: number;
  form: string;
  isbn: string;
  summary: string;
  detail: string;
  author: string;
  pages: number;
  contents: string;
  price: number;
  pubDate: string;
}

export interface BookDetail extends Book {
  categoryName: string;
  liked: boolean;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;API 통신 모듈 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http.ts&lt;/p&gt;
&lt;pre id=&quot;code_1721322952477&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import axios, { AxiosRequestConfig } from 'axios';

const BASE_URL = 'http://localhost:2222';
const DEFAULT_TIMEOUT = 30000;

export const createClient = (config?: AxiosRequestConfig) =&amp;gt; {
  const axiosInstance = axios.create({
    baseURL: BASE_URL,
    timeout: DEFAULT_TIMEOUT,
    headers: {
      'content-type': 'application/json',
    },
    withCredentials: true,
    ...config,
  });

  axiosInstance.interceptors.response.use(
    (response) =&amp;gt; {
      return response;
    },
    (error) =&amp;gt; {
      return Promise.reject(error);
    },
  );

  return axiosInstance;
};

export const httpClient = createClient();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카테고리 api 를 연결할때&lt;/p&gt;
&lt;pre id=&quot;code_1721324863381&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Category } from '../models/category.model';
import { httpClient } from './http';

export const fetchCategory = async () =&amp;gt; {
  const response = await httpClient.get&amp;lt;Category[]&amp;gt;('category');
  return response.data;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 모델에서 지정한 모델 타입을 참조해서 서버의 데이터를 await 으로 받아온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청할때마다 useState, useEffect 를 호출하는 방법도 있지만,&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721324939676&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const [category, setCategory] = useState&amp;lt;Category[]&amp;gt;([]);

  useEffect(() =&amp;gt; {
    fetchCategory().then((category) =&amp;gt; {
      setCategory(category);
    });
  }, []);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 hook 을 사용하는 방법도 있다! 리액트 기본 문법으로 데이터를 분리하거나 가공할때 많이 사용된다.&lt;/p&gt;
&lt;pre id=&quot;code_1721325218404&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useEffect, useState } from 'react';
import { Category } from '../models/category.model';
import { fetchCategory } from '../api/category.api';

export const useCategory = () =&amp;gt; {
  const [category, setCategory] = useState&amp;lt;Category[]&amp;gt;([]);

  useEffect(() =&amp;gt; {

    fetchCategory().then((category) =&amp;gt; {
      if (!category) return;

      const categoryWithAll = [
        {
          category_id: null,
          category_name: '전체',
        },
        ...category,
      ];
      setCategory(categoryWithAll);
    });

  }, []);
  
  return { category };
};
/////////Header.tsx
  const { category } = useCategory();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;훅 장점!! useEffect, useState 를 분리해서 재사용성이 높고, 코드의 가독성을 챙길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에 적극 적용해보고 싶다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;회원가입&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 입력창 InputText 만든거 가져와서 스타일링 및 사용하는데, 이때 입력의 타입이 다르기 때문에, email, password 타입 선언하고, `React.InputHTMLAttributes&amp;lt;HTMLInputElement&amp;gt;` 의 타입 항복들을 그대로 사용할 수 있도록 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1721326175699&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;///...
interface Props extends React.InputHTMLAttributes&amp;lt;HTMLInputElement&amp;gt; {
  placeholder?: string;
  inputType?: 'text' | 'email' | 'password' | 'number';
}

const InputText = React.forwardRef(
  ({ placeholder, inputType }: Props, ref: ForwardedRef&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
    //reactr 내장 input 컴포넌트
    return (
      &amp;lt;InputTextStyle placeholder={placeholder} ref={ref} type={inputType} /&amp;gt;
    );
  },
);

const InputTextStyle = styled.input`
///...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`preventDefault()` : form 업데이트를 하면 기본적으로 페이지 이동을 제공하는데, 이동을 막는 메서드이다.&lt;/p&gt;
&lt;pre id=&quot;code_1721326692888&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;///...
const Signup = () =&amp;gt; {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (event: React.FormEvent&amp;lt;HTMLFormElement&amp;gt;) =&amp;gt; {
    event.preventDefault();
    console.log(email, password);
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;Title size=&quot;large&quot;&amp;gt;회원가입&amp;lt;/Title&amp;gt;
      &amp;lt;SignupStyle&amp;gt;
        &amp;lt;form onSubmit={handleSubmit}&amp;gt;
          &amp;lt;fieldset&amp;gt;
            &amp;lt;InputText
              placeholder=&quot;이메일&quot;
              inputType=&quot;email&quot;
              value={email}
              onChange={(e) =&amp;gt; setEmail(e.target.value)}
            /&amp;gt;
          &amp;lt;/fieldset&amp;gt;
          &amp;lt;fieldset&amp;gt;
            &amp;lt;InputText
              placeholder=&quot;비밀번호&quot;
              inputType=&quot;password&quot;
              value={password}
              onChange={(e) =&amp;gt; setPassword(e.target.value)}
            /&amp;gt;
          &amp;lt;/fieldset&amp;gt;
          &amp;lt;fieldset&amp;gt;
            &amp;lt;Button type=&quot;submit&quot; size=&quot;medium&quot; scheme=&quot;primary&quot;&amp;gt;
              회원가입
            &amp;lt;/Button&amp;gt;
          &amp;lt;/fieldset&amp;gt;
///...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;React Hook Form 상태관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 interface가 필요하고, 사용법은 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1721327440347&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//...
interface SignupProps {
  email: string;
  password: string;
}

const Signup = () =&amp;gt; {

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm&amp;lt;SignupProps&amp;gt;();

  const onSubmit = (data: SignupProps) =&amp;gt; {
    console.log(data);
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;Title size=&quot;large&quot;&amp;gt;회원가입&amp;lt;/Title&amp;gt;
      &amp;lt;SignupStyle&amp;gt;
        &amp;lt;form onSubmit={handleSubmit(onSubmit)}&amp;gt;
          &amp;lt;fieldset&amp;gt;
            &amp;lt;InputText
              placeholder=&quot;이메일&quot;
              inputType=&quot;email&quot;
              {...register('email', { required: true })}
            /&amp;gt;
            {errors.email &amp;amp;&amp;amp; (
              &amp;lt;p className=&quot;error-text&quot;&amp;gt;이메일을 입력해주세요.&amp;lt;/p&amp;gt;
            )}
          &amp;lt;/fieldset&amp;gt;
//...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프레드 연산자로 데이터를 받아와서 상태관리 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회원가입 api 요청&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력받은 email과 password 값을 api 연결을 fetch 로 진행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1721327820465&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { SignupProps } from '../pages/Signup';
import { httpClient } from './http';

export const signup = async (userData: SignupProps) =&amp;gt; {
  const response = await httpClient.post('/users/join', userData); //데이터를 바디로 보내준다
  return response.data;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 버튼을 눌렀을때 (onSubmit) 네트워크를 통해 Payload body 안에 이메일과 비밀번호가 보내진다.&lt;/p&gt;
&lt;pre id=&quot;code_1721328033984&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; const onSubmit = (data: SignupProps) =&amp;gt; {
    signup(data).then((res) =&amp;gt; {
      //성공
      window.alert('회원가입이 완료되었습니다.');
      navigate('/login');
    });
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`window.alert` 간편하지만 커스텀 디자인하기에는 어렵다. 커스텀 훅으로 useAlert 를 사용하면 가능하다. 유지보수성에 좋다.&lt;/p&gt;
&lt;pre id=&quot;code_1721328198887&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useCallback } from 'react';

export const useAlert = () =&amp;gt; {
  const showAlert = useCallback((message: string) =&amp;gt; {
    window.alert(message);
  }, []);

  return showAlert;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️ 배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문윤기 강사님 수업이 들을수록 너무 좋다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많이 배웠고, 덕분에 다음주 마감에 있는 플젝에 적용할 수 있을 것 같다.&lt;/p&gt;</description>
      <category>웹 풀 사이클 데브코스</category>
      <category>til</category>
      <category>데브코스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/178</guid>
      <comments>https://datdaradanadat.tistory.com/178#entry178comment</comments>
      <pubDate>Thu, 18 Jul 2024 19:17:05 +0900</pubDate>
    </item>
    <item>
      <title>  Vercel 무료 자동 배포, Github Action으로 CICD 구축</title>
      <link>https://datdaradanadat.tistory.com/177</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Vercel 무료 배포&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;firebase로 로그인, 이메일전송 등의 서비스를 구축하고 있다. 미리 배포한 후 연동하려고 하는데 멘토님께서 firebase 배포가 아닌 vercel 배포를 추천하셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git Organization 에서 FE, BE 레포가 다른데 시간이 없어서 둘다 내가 하기로 했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무료로 하려면 조직(팀) 레포가 아니라 내 계정으로 개인으로 fork 를 해서 vercel 배포를 하고 cicd로 바로 적용될 수 있도록 하는게 포인트!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  무료 배포 방법 설명하기 전에,&lt;br /&gt;cicd 전략을 개선한 글 먼저 본다면 프로젝트의 개선 방법이 더 잘보일 듯 합니다!&lt;br /&gt;코드품질, 태그 기반 배포 전략에 대해 설명했습니다.&lt;/blockquote&gt;
&lt;figure id=&quot;og_1752805350759&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot; ️ 안정성 배포전략 - CI 코드품질 검사, CD 태그 기반 배포&quot; data-og-description=&quot;단순했던 첫 배포 파이프라인Kampus 프로젝트는 초기 Git Flow 를 따르자고 정했다. feature 브랜치에서 develop 으로 PR 을 올렸을 때 &amp;rarr; Squash and merge &amp;rarr; merge 후 remote branch 삭제기능 단위로 개발한 브랜&quot; data-og-host=&quot;datdaradanadat.tistory.com&quot; data-og-source-url=&quot;https://datdaradanadat.tistory.com/247&quot; data-og-url=&quot;https://datdaradanadat.tistory.com/247&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/p3Cdn/hyZjnPOnLF/OzOcjh6zsazelcg8lQDS4k/img.png?width=800&amp;amp;height=491&amp;amp;face=0_0_800_491,https://scrap.kakaocdn.net/dn/nglTR/hyZnnN8E6r/9qoJwwiQph3vHl1DeNPx40/img.png?width=800&amp;amp;height=491&amp;amp;face=0_0_800_491,https://scrap.kakaocdn.net/dn/zLPl7/hyZjv1tcrn/prLnZ0vQokDdECNK6k38Y1/img.png?width=1400&amp;amp;height=860&amp;amp;face=0_0_1400_860&quot;&gt;&lt;a href=&quot;https://datdaradanadat.tistory.com/247&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://datdaradanadat.tistory.com/247&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/p3Cdn/hyZjnPOnLF/OzOcjh6zsazelcg8lQDS4k/img.png?width=800&amp;amp;height=491&amp;amp;face=0_0_800_491,https://scrap.kakaocdn.net/dn/nglTR/hyZnnN8E6r/9qoJwwiQph3vHl1DeNPx40/img.png?width=800&amp;amp;height=491&amp;amp;face=0_0_800_491,https://scrap.kakaocdn.net/dn/zLPl7/hyZjv1tcrn/prLnZ0vQokDdECNK6k38Y1/img.png?width=1400&amp;amp;height=860&amp;amp;face=0_0_1400_860');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt; ️ 안정성 배포전략 - CI 코드품질 검사, CD 태그 기반 배포&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;단순했던 첫 배포 파이프라인Kampus 프로젝트는 초기 Git Flow 를 따르자고 정했다. feature 브랜치에서 develop 으로 PR 을 올렸을 때 &amp;rarr; Squash and merge &amp;rarr; merge 후 remote branch 삭제기능 단위로 개발한 브랜&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;datdaradanadat.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 개인 계정에 public으로 fork 한다.&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. vercel에 project 생성해서 git import 한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 연결하는건 fork 해온 개인 레포지토리 링크이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택된 레포지토리만 vercel에 불러오게 할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;719&quot; data-origin-height=&quot;643&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0x9lD/btsIDlAxktR/Tvp17zslMPKdbUd09XQiK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0x9lD/btsIDlAxktR/Tvp17zslMPKdbUd09XQiK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0x9lD/btsIDlAxktR/Tvp17zslMPKdbUd09XQiK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0x9lD%2FbtsIDlAxktR%2FTvp17zslMPKdbUd09XQiK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;719&quot; height=&quot;643&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;719&quot; data-origin-height=&quot;643&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vite로 생성한 프로젝트는 Framework Preset 에서 Vite 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;node.js+express 는 따로 프레임워크를 제공하지 않아 Other 선택하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Environment Variables 에서 환경변수를 등록해주어야 하는데, 추후에 추가할 수 있다. 개발 도중에 환경변수를 생성하면 필수로 추가해야 한다.(+ 깃에도)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;CICD 자동배포&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 개인 github 설정에서 Personal access tokens &amp;gt; Token을 만들어서 repo 권한을 체크한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/settings/tokens&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;개인 계정 Settings &amp;gt; &amp;lt;&amp;gt;Developer Settings &amp;gt; Personal access tokens &amp;gt; Tokens(classic) &amp;gt; Generate new token(classic)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 토큰을 만든다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Note 토큰에 부여할 이름&lt;/li&gt;
&lt;li&gt;Expiration 토큰 만료 기한 : 1달정도 권장. 기한없음으로 하면 위험하다는 경고가 뜬다.&lt;/li&gt;
&lt;li&gt;Select scopes 해당 토큰에 줄 권한 : repo, workflow 체크&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 `ghp_`로 시작하는 토큰이 발급되는데 다시 볼 수 없으니 꼭꼭 메모해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 팀노션, 개인학습노션, 카톡에 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러개 만들 수 있으니, 프로젝트마다 개인토큰 생성 권장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FE, BE 토큰은 통일했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 팀(Organization) 레포지토리에서 4-1, 4-2 작업을 한다&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. build.sh 파일 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트에 아래 `build.sh` 파일을 생성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1721221158765&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/sh
cd ../
mkdir output
cp -R ./[team-repo-name]/* ./output
cp -R ./output ./[team-repo-name]/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[team-repo-name] 에는 팀의 레포이름(프로젝트 이름)을 입력한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;* vercel.json 파일이 있어야 한다. 없을 경우 이때 추가하기!&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Vite + React + TS&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react DOM 을 가상 돔으로 인식해서, 배포되었을때 url 을 가상으로 인식해 페이지가 없다는 404 에러가 발생했다. 여기에서 routes 의 설정을 작성해주면 해결된다.&lt;/p&gt;
&lt;pre id=&quot;code_1721221363807&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;version&quot;: 2,
    &quot;routes&quot;: [
      {
        &quot;handle&quot;: &quot;filesystem&quot;
      },
      {
        &quot;src&quot;: &quot;/.*&quot;,
        &quot;dest&quot;: &quot;/index.html&quot;
      }
    ]
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;node.js + express&lt;/h4&gt;
&lt;pre id=&quot;code_1721221448211&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;version&quot;: 2,
  &quot;builds&quot;: [
    {
      &quot;src&quot;: &quot;app.js&quot;,			
      &quot;use&quot;: &quot;@vercel/node&quot;
    }
  ],
  &quot;routes&quot;: [
    {
      &quot;src&quot;: &quot;/(.*)&quot;,
      &quot;dest&quot;: &quot;/app.js&quot;
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. secret 변수 등록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 레포 Settings &amp;gt; Secrets and variables &amp;gt; Actions &amp;gt; Repository secrets &amp;gt; New repository secret&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`ghp_`로 시작하는 &lt;a href=&quot;https://datdaradanadat.tistory.com/177#3.%20%EA%B0%9C%EC%9D%B8%20github%20%EC%84%A4%EC%A0%95%EC%97%90%EC%84%9C%20Personal%20access%20tokens%20%3E%20Token%EC%9D%84%20%EB%A7%8C%EB%93%A4%EC%96%B4%EC%84%9C%20repo%20%EA%B6%8C%ED%95%9C%EC%9D%84%20%EC%B2%B4%ED%81%AC%ED%95%9C%EB%8B%A4.-1&quot;&gt;3번에서 발급받은 토큰&lt;/a&gt;을 등록해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ 개인계정의 깃허브 이메일 등록한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;985&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bS8eqT/btsIEmSJMjW/x97mBUQsl5bEClkvtSLdDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bS8eqT/btsIEmSJMjW/x97mBUQsl5bEClkvtSLdDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bS8eqT/btsIEmSJMjW/x97mBUQsl5bEClkvtSLdDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbS8eqT%2FbtsIEmSJMjW%2Fx97mBUQsl5bEClkvtSLdDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;985&quot; height=&quot;230&quot; data-origin-width=&quot;985&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. GitHub Action 활성화 코드를 작성, 워크플로우 생성한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포랑 연결된 개인레포의 계정 환경변수(actions, email)을 작성해주면 되는 것 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1721224218461&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; .github
 ┣  ISSUE_TEMPLATE
 ┃ ┣  ✅-etc-template.md
 ┃ ┣  ✨-feature-template.md
 ┃ ┗  -bug-report-template.md
 ┣  workflows
 ┃ ┗  deploy.yml
 ┗  PULL_REQUEST_TEMPLATE.md&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1721222469431&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Synchronize to forked repo
on:
  push:
    branches:
      - main

jobs:
  sync:
    name: Sync forked repo
    runs-on: ubuntu-latest

    steps:
      - name: Checkout main
        uses: actions/checkout@v4
        with: 
          token: ${{ secrets.AUTO_ACTIONS }}
          fetch-depth: 0
          ref: main

      - name: Add remote-url
        run: |
          git remote add forked-repo https://[your-github-name]:${{ secrets.AUTO_ACTIONS }}@github.com/[your-github-name]/[your-repo-name]
          git config user.name [your-github-name]
          git config user.email ${{ secrets.EMAIL }}
      
      - name: Push changes to forked-repo 
        run: |
          git push -f forked-repo main
      
      - name: Clean up
        run: |
          git remote remove forked-repo&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ISSUE_TEMPLATE, PULL_REQUEST_TEMPLATE 는 이슈생성, PR 날릴때 적는 템플릿을 설정해줬다. 깃허브에서 세팅할 수도 있지만, vscode 에서 작성할 수 있다. 코드에서 세팅해주는게 편리한듯.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 팀 레포 main에 올리면 Actions 탭의 workflow 가 돌아간다. run&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1900&quot; data-origin-height=&quot;355&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rKicf/btsIDwIyb75/l0woK5VCNeYUQ6S4xe5zd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rKicf/btsIDwIyb75/l0woK5VCNeYUQ6S4xe5zd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rKicf/btsIDwIyb75/l0woK5VCNeYUQ6S4xe5zd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrKicf%2FbtsIDwIyb75%2Fl0woK5VCNeYUQ6S4xe5zd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1900&quot; height=&quot;355&quot; data-origin-width=&quot;1900&quot; data-origin-height=&quot;355&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인레포에서도 action이 돌아가고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vercel 프로젝트의 Deployments 탭을 보면 성공적으로 배포된 것을 볼 수 있다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1746&quot; data-origin-height=&quot;459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/77Ofh/btsIDs0x5dn/hKmDfhOiEKBkmGbTD4Tu00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/77Ofh/btsIDs0x5dn/hKmDfhOiEKBkmGbTD4Tu00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/77Ofh/btsIDs0x5dn/hKmDfhOiEKBkmGbTD4Tu00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F77Ofh%2FbtsIDs0x5dn%2FhKmDfhOiEKBkmGbTD4Tu00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1746&quot; height=&quot;459&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1746&quot; data-origin-height=&quot;459&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️ 배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷을 열심히 참고하며, 나만의 vercel 배포 세팅을 가지게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이때 firebase의 Google 소셜 로그인을 구현했는데, 승인된 도메인에 배포된 링크를 추가해주어야 했다.&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;&lt;s&gt;이제 vercel + Github Action 조합을 마스터했다!&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;#참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@dlatlgns00/node.js-Express%EB%A5%BC-vercel%EB%A1%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0#4-%ED%96%A5%ED%9B%84-%EB%AA%A9%ED%91%9C&quot;&gt;[ node.js ] Express를 vercel로 배포하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jjang-j.tistory.com/93&quot;&gt;[Github Action] 깃허브 Organization 레포지토리 vercel 자동 배포&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@rmaomina/organization-vercel-hobby-deploy&quot;&gt;GitHub Organization 프로젝트를 vercel 무료로 연동하기 (+git actions)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@charming-l/Github-action%EC%9C%BC%EB%A1%9C-push-%EB%90%A0-%EB%95%8C%EB%A7%88%EB%8B%A4-%EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C-%EC%B5%9C%EC%8B%A0%ED%99%94%ED%95%98%EA%B8%B0to-forked-repo&quot;&gt;Github action으로 Sync Fork 자동화하기 - push 될 때마다&lt;/a&gt;&lt;/p&gt;</description>
      <category>  BackEnd</category>
      <category>CICD</category>
      <category>Express</category>
      <category>Node.js</category>
      <category>React</category>
      <category>vercel</category>
      <category>vite</category>
      <category>배포</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/177</guid>
      <comments>https://datdaradanadat.tistory.com/177#entry177comment</comments>
      <pubDate>Wed, 17 Jul 2024 22:57:56 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] Day60 - 컴포넌트, 헤더</title>
      <link>https://datdaradanadat.tistory.com/176</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Title 컴포넌트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폰트크기 theme 에 적용하기&lt;/p&gt;
&lt;pre id=&quot;code_1721281821043&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export type Headingsize = 'large' | 'medium' | 'small';

interface Theme {
  name: ThemeName;
  color: {
    [key in ColorKey]: string;
  };
  heading: {
    [key in Headingsize]: {
      fontSize: string;
    };
  };
  
export const light: Theme = {
  name: 'light',
  color: {
    primary: 'brown',
    background: 'lightgray',
    secondary: 'blue',
    third: 'green',
  },
  heading: {
    large: { fontSize: '2rem' },
    medium: { fontSize: '1.5rem' },
    small: { fontSize: '1rem' },
  },
};

export const dark: Theme = {
  ...light,
  name: 'dark',
  color: {
    primary: 'coral',
    background: 'midnightblue',
    secondary: 'darkblue',
    third: 'darkgreen',
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 폰트크기는 테마마다 변할리가 없는데,,, 타입은 둘다 지정해주어야 한다! 따라서 `?` 옵셔널 또는 `...light` 오브젝트 복사로 해결할 수 있다. &amp;gt; undefined 일 경우도 파악해서 경우 작성하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Omit 유틸리티 타입&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`Omit&amp;lt;타입, '키'&amp;gt;` 전달받은 타입에서 키를 제외한 타입을 생성한다.&lt;/p&gt;
&lt;figure id=&quot;og_1721282254772&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Documentation - Utility Types&quot; data-og-description=&quot;Types which are globally included in TypeScript&quot; data-og-host=&quot;www.typescriptlang.org&quot; data-og-source-url=&quot;https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys&quot; data-og-url=&quot;https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Documentation - Utility Types&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Types which are globally included in TypeScript&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.typescriptlang.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;props 관련 테스트는 필수로 진행해야, 프로젝트 규모에 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721288770798&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from 'react';
import { styled } from 'styled-components';
import { ColorKey, Headingsize } from '../../style/theme';

interface Props {
  children: React.ReactNode;
  size: Headingsize;
  color?: ColorKey;
}

const Title = ({ children, size, color }: Props) =&amp;gt; {
  return (
    &amp;lt;TitleStyle size={size} color={color}&amp;gt;
      {children}
    &amp;lt;/TitleStyle&amp;gt;
  );
};

const TitleStyle = styled.h1&amp;lt;Omit&amp;lt;Props, 'children'&amp;gt;&amp;gt;`
  font-size: ${({ theme, size }) =&amp;gt; theme.heading[size].fontSize};
  color: ${({ theme, color }) =&amp;gt;
    color ? theme.color[color] : theme.color.primary};
`;

export default Title;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 잘 렌더링 되는지 &lt;b&gt;Jest&lt;/b&gt; 를 이용해 테스트 `Title.spec.tsx`&lt;/p&gt;
&lt;pre id=&quot;code_1721289076905&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { render, screen } from '@testing-library/react';
import Title from './Title';
import { BookStoreThemeProvider } from '../../context/themeContext';

describe('Title 컴포넌트 테스트', () =&amp;gt; {
  it('렌더를 확인', () =&amp;gt; {
    render(
      &amp;lt;BookStoreThemeProvider&amp;gt;
        &amp;lt;Title size=&quot;large&quot;&amp;gt;제목&amp;lt;/Title&amp;gt;
      &amp;lt;/BookStoreThemeProvider&amp;gt;,
    );
    expect(screen.getByText('제목')).toBeInTheDocument();
  });

  it('size props 적용', () =&amp;gt; {
    const { container } = render(
      &amp;lt;BookStoreThemeProvider&amp;gt;
        &amp;lt;Title size=&quot;large&quot;&amp;gt;제목&amp;lt;/Title&amp;gt;
      &amp;lt;/BookStoreThemeProvider&amp;gt;,
    );
    expect(container?.firstChild).toHaveStyle({ fontSize: '2rem' });
  });

  it('color props 적용', () =&amp;gt; {
    const { container } = render(
      &amp;lt;BookStoreThemeProvider&amp;gt;
        &amp;lt;Title size=&quot;large&quot; color=&quot;primary&quot;&amp;gt;
          제목
        &amp;lt;/Title&amp;gt;
      &amp;lt;/BookStoreThemeProvider&amp;gt;,
    );
    expect(container?.firstChild).toHaveStyle({ color: 'brown' });
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Button 컴포넌트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트로 만들면 유용하게 잘 쓴다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scheme : 버튼의 border, background, text color 정도로 이루어진 프리셋 세트이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pointer-events : none 일 경우 마우스 이벤트(클릭기능) 무시, auto 일 경우 이벤트 실행된다.&lt;/p&gt;
&lt;pre id=&quot;code_1721291232008&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; button: {
    [key in ButtonSize]: {
      fontSize: string;
      padding: string;
    };
  };
  buttonScheme: {
    [key in ButtonScheme]: {
      color: string;
      backgroundColor: string;
    };
  };
  borderRadius: {
    default: string;
  };
////////
import React from 'react';
import { styled } from 'styled-components';
import { ButtonScheme, ButtonSize } from '../../style/theme';

interface Props {
  children: React.ReactNode;
  size: ButtonSize;
  scheme: ButtonScheme;
  disabled?: boolean;
  isLoading?: boolean;
}

const Button = ({ children, size, scheme, disabled, isLoading }: Props) =&amp;gt; {
  return (
    &amp;lt;ButtonStyle
      size={size}
      scheme={scheme}
      disabled={disabled}
      isLoading={isLoading}
    &amp;gt;
      {children}
    &amp;lt;/ButtonStyle&amp;gt;
  );
};

const ButtonStyle = styled.button&amp;lt;Omit&amp;lt;Props, 'children'&amp;gt;&amp;gt;`
  font-size: ${({ theme, size }) =&amp;gt; theme.button[size].fontSize};
  padding: ${({ theme, size }) =&amp;gt; theme.button[size].fontSize};
  color: ${({ theme, scheme }) =&amp;gt; theme.buttonScheme[scheme].color};
  background-color: ${({ theme, scheme }) =&amp;gt;
    theme.buttonScheme[scheme].backgroundColor};
  border: 0;
  border-radius: ${({ theme }) =&amp;gt; theme.borderRadius.default};
  opacity: ${({ disabled }) =&amp;gt; (disabled ? 0.5 : 1)};
  pointer-events: ${({ disabled }) =&amp;gt; (disabled ? 'none' : 'auto')};
  cursor: ${({ disabled }) =&amp;gt; (disabled ? 'none' : 'pointer')};
`;

export default Button;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`Button.spec.tsx` 로 테스트 하기&lt;/p&gt;
&lt;pre id=&quot;code_1721292187837&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { render, screen } from '@testing-library/react';
import { BookStoreThemeProvider } from '../../context/themeContext';
import Button from './Button';

describe('Button 컴포넌트 테스트', () =&amp;gt; {
  it('렌더를 확인', () =&amp;gt; {
    render(
      &amp;lt;BookStoreThemeProvider&amp;gt;
        &amp;lt;Button size=&quot;large&quot; scheme=&quot;primary&quot;&amp;gt;
          버튼
        &amp;lt;/Button&amp;gt;
      &amp;lt;/BookStoreThemeProvider&amp;gt;,
    );
    expect(screen.getByText('버튼')).toBeInTheDocument();
  });
  
  it('size props 적용', () =&amp;gt; {
    render(
      &amp;lt;BookStoreThemeProvider&amp;gt;
        &amp;lt;Button size=&quot;large&quot; scheme=&quot;primary&quot;&amp;gt;제목&amp;lt;/Button&amp;gt;
      &amp;lt;/BookStoreThemeProvider&amp;gt;,
    );
    expect(screen.getByRole(&quot;button&quot;)).toHaveStyle({ fontSize: '1.5rem' });
  });

});

//disable, isLoading&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;expect의 getByRole()로 컴포넌트 아이템 button 로딩한다.&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;h2 data-ke-size=&quot;size26&quot;&gt;Input 컴포넌트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;input 의 타입을 지정하는 `attrs` 어트리뷰트 메서드 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1721293949578&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { ForwardedRef } from 'react';
import styled from 'styled-components';

interface Props {
  placeholder?: string;
}

const InputText = React.forwardRef(
  ({ placeholder }: Props, ref: ForwardedRef&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
    //reactr 내장 input 컴포넌트
    return &amp;lt;InputTextStyle placeholder={placeholder} ref={ref} /&amp;gt;;
  },
);

const InputTextStyle = styled.input.attrs({ type: 'text' })`
  padding: 0.25rem 0.75rem;
  border: 1px solid ${({ theme }) =&amp;gt; theme.color.border};
  border-radius: ${({ theme }) =&amp;gt; theme.borderRadius.default};
  font-size: 1rem;
  line-height: 1.5;
  color: ${({ theme }) =&amp;gt; theme.color.text};
`;

export default InputText;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`InputText.spec.tsx`로 테스트 코드&lt;/p&gt;
&lt;pre id=&quot;code_1721293980443&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { render, screen } from '@testing-library/react';
import { BookStoreThemeProvider } from '../../context/themeContext';
import InputText from './InputText';
import React from 'react';

describe('Input 컴포넌트 테스트', () =&amp;gt; {
  it('렌더를 확인', () =&amp;gt; {
    render(
      &amp;lt;BookStoreThemeProvider&amp;gt;
        &amp;lt;InputText placeholder=&quot;여기에 입력&quot; /&amp;gt;
      &amp;lt;/BookStoreThemeProvider&amp;gt;,
    );
    expect(screen.getByPlaceholderText('여기에 입력')).toBeInTheDocument();
  });

  it('forwardRef 테스트', () =&amp;gt; {
    const ref = React.createRef&amp;lt;HTMLInputElement&amp;gt;();
    render(
      &amp;lt;BookStoreThemeProvider&amp;gt;
        &amp;lt;InputText placeholder=&quot;여기에 입력&quot; ref={ref} /&amp;gt;
      &amp;lt;/BookStoreThemeProvider&amp;gt;,
    );
    expect(ref.current).toBeInstanceOf(HTMLInputElement);
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;헤더, 푸터&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로고영역, 카테고리, 개인화 영역(로그인 여부)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로고, 카피라이트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Layout.tsx 에서 전체 스타일 고려하기&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;☑️ 배운점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트를 하면서 느끼는점은, 단순 react 코드를 짜는게 아닌 설계부터 정확히 체크해가며 예외를 고려해가며 코드를 짜야한다는 사실이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;styled-components 등 다양한 기술을 사용하는 것도 큰 틀을 봐야지만 할 수 있는것!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면 큰틀 보기는 어느정도 익숙해진 것 같다.&lt;/p&gt;</description>
      <category>웹 풀 사이클 데브코스</category>
      <category>til</category>
      <category>데브코스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/176</guid>
      <comments>https://datdaradanadat.tistory.com/176#entry176comment</comments>
      <pubDate>Wed, 17 Jul 2024 21:26:22 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] Day59 - CSS, ThemeProver</title>
      <link>https://datdaradanadat.tistory.com/175</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;레이아웃?&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프로젝트의 기본적인 화면 구조를 잡는다&lt;/li&gt;
&lt;li&gt;반복적으로 들어가야하는 헤더, 푸터 등 매 화면마다 제공한다.&lt;/li&gt;
&lt;li&gt;상황과 필요에 따라 레이아웃이 변경될 수 있도록 대비한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Header, Footer 통일된 레이아웃 컴포넌트&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;각 화면 페이지마다 컴포넌트 호출&lt;/li&gt;
&lt;li&gt;레이아웃&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;pico css 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;index.html `&amp;lt;linkrel=&quot;stylesheet&quot;href=&quot;&lt;a href=&quot;https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css&lt;/a&gt;&quot;/&amp;gt;`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dark 테마&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;각 페이지마다 공통 컴포넌트 호출하는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Home.tsx&lt;/p&gt;
&lt;pre id=&quot;code_1721066361587&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from &quot;react&quot;;
import Header from &quot;../components/common/Header&quot;;
import Footer from &quot;../components/common/Footer&quot;;

const Home = () =&amp;gt; {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Header /&amp;gt;
      &amp;lt;div&amp;gt;home&amp;lt;/div&amp;gt;
      &amp;lt;Footer /&amp;gt;
    &amp;lt;/&amp;gt;
  );
};

export default Home;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Detail.tsx&lt;/p&gt;
&lt;pre id=&quot;code_1721066379208&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from &quot;react&quot;;
import Header from &quot;../components/common/Header&quot;;
import Footer from &quot;../components/common/Footer&quot;;
import Sidebar from &quot;../components/common/Sidebar&quot;;

const Detail = () =&amp;gt; {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Header /&amp;gt;
      &amp;lt;Sidebar /&amp;gt;
      &amp;lt;div&amp;gt;디테일&amp;lt;/div&amp;gt;
      &amp;lt;Footer /&amp;gt;
    &amp;lt;/&amp;gt;
  );
};

export default Detail;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통되는 부분이 많기 때문에, 레이아웃으로 구성하는게 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;레이아웃으로 구성하는 경우&lt;/h3&gt;
&lt;pre id=&quot;code_1721066586500&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from &quot;react&quot;;
import Header from &quot;../common/Header&quot;;
import Footer from &quot;../common/Footer&quot;;

interface LayoutProps {
  children: React.ReactNode;
}

const Layout = ({ children }: LayoutProps) =&amp;gt; {
  return (
    &amp;lt;&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;/&amp;gt;
  );
};

export default Layout;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;children을 이용해서 ReactNode 타입 지정해서, 내용물에서 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReactNode 타입 : 리액트로 만든 모든 컴포넌트들이 배치될 수 있다. 집합 형태로 구분된다. 외부 라이브러리나 다른쪽에서 React Element, JSX Element를 사용한 경우 타입오류가 발생하는 경우도 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;623&quot; data-origin-height=&quot;613&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mN9di/btsIzrBjRbS/C7rGQx1g4XQkBfmZUez6ik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mN9di/btsIzrBjRbS/C7rGQx1g4XQkBfmZUez6ik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mN9di/btsIzrBjRbS/C7rGQx1g4XQkBfmZUez6ik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmN9di%2FbtsIzrBjRbS%2FC7rGQx1g4XQkBfmZUez6ik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;623&quot; height=&quot;613&quot; data-origin-width=&quot;623&quot; data-origin-height=&quot;613&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App.tsx 에서 레이아웃을 호출하면서, props로 children의 Home을 전달한다.&lt;/p&gt;
&lt;pre id=&quot;code_1721066934447&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Layout from &quot;./components/layout/Layout&quot;;
import Home from &quot;./pages/Home&quot;;

function App() {
  return &amp;lt;Layout children={&amp;lt;Home /&amp;gt;} /&amp;gt;;
}

export default App;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;or 일반적인 형태(html)로 사용할 수도 있음&lt;/p&gt;
&lt;pre id=&quot;code_1721067057685&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Layout from &quot;./components/layout/Layout&quot;;
import Home from &quot;./pages/Home&quot;;

function App() {
  return (
    &amp;lt;Layout&amp;gt;
      &amp;lt;Home /&amp;gt;
    &amp;lt;/Layout&amp;gt;
  );
}

export default App;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;inedx.tsx, route에서도 레이아웃을 제공할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;route 경로마다 레이아웃이 변경되는 경우가 있기 때문에 Outlet 을 이용해 레이아웃 지정하는 경우가 많다.&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;Global style&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에 일관된 스타일링을 적용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`user agent stylesheet`로 표시되는 브라우저의 기본 스타일이 차이를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 간의 스타일 차이를 극복하기 위해 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;에릭 마이어 reset css&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역사가 깊고, 아직도 많이 쓰이는 근본 css&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 element 의 스타일을 리셋, 즉 0 으로 만든다.&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1721067496475&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;CSS Tools: Reset CSS&quot; data-og-description=&quot;CSS Tools: Reset CSS The goal of a reset stylesheet is to reduce browser inconsistencies in things like default line heights, margins and font sizes of headings, and so on. The general reasoning behind this was discussed in a May 2007 post, if you're inter&quot; data-og-host=&quot;meyerweb.com&quot; data-og-source-url=&quot;https://meyerweb.com/eric/tools/css/reset/&quot; data-og-url=&quot;https://meyerweb.com/eric/tools/css/reset/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cSoKHY/hyWCNuRzIs/inCxiYK9f1QkKS5CBYN5Kk/img.png?width=1040&amp;amp;height=330&amp;amp;face=0_0_1040_330,https://scrap.kakaocdn.net/dn/e8Cyz/hyWzpPYFZ1/DYID2S72c6MteL0XfVfGD1/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024&quot;&gt;&lt;a href=&quot;https://meyerweb.com/eric/tools/css/reset/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://meyerweb.com/eric/tools/css/reset/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cSoKHY/hyWCNuRzIs/inCxiYK9f1QkKS5CBYN5Kk/img.png?width=1040&amp;amp;height=330&amp;amp;face=0_0_1040_330,https://scrap.kakaocdn.net/dn/e8Cyz/hyWzpPYFZ1/DYID2S72c6MteL0XfVfGD1/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;CSS Tools: Reset CSS&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;CSS Tools: Reset CSS The goal of a reset stylesheet is to reduce browser inconsistencies in things like default line heights, margins and font sizes of headings, and so on. The general reasoning behind this was discussed in a May 2007 post, if you're inter&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;meyerweb.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;normalize.css&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reset css 다음으로 오래되고, 많이 쓰인다. 각각 고유의 속성(h1, h2의 차이)를 유지하면서, 기기간의 차이를 줄이는것을 목적으로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습하기 좋은 코드&lt;/p&gt;
&lt;figure id=&quot;og_1721067610717&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - necolas/normalize.css: A modern alternative to CSS resets&quot; data-og-description=&quot;A modern alternative to CSS resets. Contribute to necolas/normalize.css development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/necolas/normalize.css&quot; data-og-url=&quot;https://github.com/necolas/normalize.css&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bf9FxI/hyWCNhkIRW/Kwp2BPDxkxcTRgxq3Xyo6K/img.png?width=1200&amp;amp;height=600&amp;amp;face=976_161_1056_249&quot;&gt;&lt;a href=&quot;https://github.com/necolas/normalize.css&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/necolas/normalize.css&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bf9FxI/hyWCNhkIRW/Kwp2BPDxkxcTRgxq3Xyo6K/img.png?width=1200&amp;amp;height=600&amp;amp;face=976_161_1056_249');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - necolas/normalize.css: A modern alternative to CSS resets&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A modern alternative to CSS resets. Contribute to necolas/normalize.css development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;sanitize.css&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;normalize 발전된 버전이다. 목적은 같고, selecter 부분에서 개선되어진 부분이다.&lt;/p&gt;
&lt;figure id=&quot;og_1721067716737&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - csstools/sanitize.css: A best-practices CSS foundation&quot; data-og-description=&quot;A best-practices CSS foundation. Contribute to csstools/sanitize.css development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/csstools/sanitize.css/&quot; data-og-url=&quot;https://github.com/csstools/sanitize.css&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/jOZHp/hyWzxmSTOh/qvQpAibc0d6XaN2AqJvSlK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/csstools/sanitize.css/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/csstools/sanitize.css/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/jOZHp/hyWzxmSTOh/qvQpAibc0d6XaN2AqJvSlK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - csstools/sanitize.css: A best-practices CSS foundation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A best-practices CSS foundation. Contribute to csstools/sanitize.css development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;sanitize 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`npm install sanitize.css --save`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;index.tsx 하위에 import 해주면 적용된다.&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;개발자도구에 where 셀렉트를 이용해서 평탄화 작업을 진행한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;447&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DTVLN/btsIAUihJe8/rfxDV8PZxXUAi2KO4eyOY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DTVLN/btsIAUihJe8/rfxDV8PZxXUAi2KO4eyOY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DTVLN/btsIAUihJe8/rfxDV8PZxXUAi2KO4eyOY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDTVLN%2FbtsIAUihJe8%2FrfxDV8PZxXUAi2KO4eyOY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;447&quot; height=&quot;448&quot; data-origin-width=&quot;447&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;where 셀렉트 : css 중복 작성을 줄이기 위한 helper selector. 해당 태그가 어디에 있던지 해당 속성을 적용하겠다.&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;styled component : css in js&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근본 css 로 해결할 수 없는 문제점을 해결하였다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;css 태그 명의 하위 속성값의 명확성 부족하다. 추측하기 어려움.&lt;/li&gt;
&lt;li&gt;전달해야할 상태가 여러개라면 한번에 적용하거나 형태를 적용하기 까다롭다.&lt;/li&gt;
&lt;li&gt;여러개의 css 파일을 import 했을 경우, 무엇이 우선인지 파악하기 어렵다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;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;li&gt;압축&lt;/li&gt;
&lt;li&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 언어의 흐름을 자연스럽게 하기위한 목적으로 styled component 사용한다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjUpkR/btsIBBbiMO0/AP7Ft5LsBboDBrAvFEgNo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjUpkR/btsIBBbiMO0/AP7Ft5LsBboDBrAvFEgNo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjUpkR/btsIBBbiMO0/AP7Ft5LsBboDBrAvFEgNo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjUpkR%2FbtsIBBbiMO0%2FAP7Ft5LsBboDBrAvFEgNo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;512&quot; height=&quot;334&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설치&lt;/h3&gt;
&lt;figure id=&quot;og_1721068428730&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;styled-components: Basics&quot; data-og-description=&quot;Get Started with styled-components basics.&quot; data-og-host=&quot;styled-components.com&quot; data-og-source-url=&quot;https://styled-components.com/docs/basics#installation&quot; data-og-url=&quot;https://styled-components.com/docs/basics#installation&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b4Eaka/hyWzCodnD0/MqogCpoOhEWeo38ShzQkSk/img.png?width=652&amp;amp;height=652&amp;amp;face=0_0_652_652,https://scrap.kakaocdn.net/dn/bF9nDY/hyWztrho8o/Wy7MV8gxGiIWGNPFhUSoM1/img.png?width=652&amp;amp;height=652&amp;amp;face=0_0_652_652&quot;&gt;&lt;a href=&quot;https://styled-components.com/docs/basics#installation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://styled-components.com/docs/basics#installation&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b4Eaka/hyWzCodnD0/MqogCpoOhEWeo38ShzQkSk/img.png?width=652&amp;amp;height=652&amp;amp;face=0_0_652_652,https://scrap.kakaocdn.net/dn/bF9nDY/hyWztrho8o/Wy7MV8gxGiIWGNPFhUSoM1/img.png?width=652&amp;amp;height=652&amp;amp;face=0_0_652_652');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;styled-components: Basics&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Get Started with styled-components basics.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;styled-components.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`npm install styled-components`&lt;/p&gt;
&lt;pre id=&quot;code_1721068565431&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from &quot;react&quot;;
import { styled } from &quot;styled-components&quot;;

const Header = () =&amp;gt; {
  return (
    &amp;lt;HeaderStyle&amp;gt;
      &amp;lt;h1&amp;gt;book store&amp;lt;/h1&amp;gt;
    &amp;lt;/HeaderStyle&amp;gt;
  );
};

const HeaderStyle = styled.header`
  background-color: #333;
  h1 {
    color: white;
  }
`;

export default Header;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tailwind css 와 유사한 사용법!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일을 분리해서 사용할 수 있다. &amp;gt; 추가로 찾아보기!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;313&quot; data-origin-height=&quot;65&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDKl6p/btsIy6dak1g/eg3Xx9kzMNKLJoT30jxFk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDKl6p/btsIy6dak1g/eg3Xx9kzMNKLJoT30jxFk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDKl6p/btsIy6dak1g/eg3Xx9kzMNKLJoT30jxFk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDKl6p%2FbtsIy6dak1g%2Feg3Xx9kzMNKLJoT30jxFk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;313&quot; height=&quot;65&quot; data-origin-width=&quot;313&quot; data-origin-height=&quot;65&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 개발자 도구로 확인하면 난수화 된 클래스 명을 가진 것으로 표시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헤더라는 컴포넌트의 스타일을 캡슐화 하기 위해서, 제한하기 위해서!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 index.tsx 에 적용한 글로벌 스타일을 따로 폴더에 `createGlovalStyle` 메소드로 구성한다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;src &amp;gt; sytle &amp;gt; global.ts&lt;/p&gt;
&lt;pre id=&quot;code_1721068864701&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { createGlobalStyle } from &quot;styled-components&quot;;
import &quot;sanitize.css&quot;;

export const GlobalStyle = createGlobalStyle``;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;index.tsx 에 global 호출해서 적용한다.&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;styled-component 테마 ThemeProver&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;UI / UX의 일관성 유지&lt;/li&gt;
&lt;li&gt;유지보수가 용이&lt;/li&gt;
&lt;li&gt;확장성&lt;/li&gt;
&lt;li&gt;재사용성&lt;/li&gt;
&lt;li&gt;사용자 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;theme provider : 프로젝트를 구성하는 컴포넌트가 배치된다. 컴포넌트보다 상위에 위치한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;theme 1, theme 2로 자유롭게 변경할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테마는 확장성 때문에 자주 수정되는 부분이기 때문에 타입으로 관리하면 좋다.&lt;/p&gt;
&lt;pre id=&quot;code_1721070562410&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface Theme {
  name: string;
  color: {
    primary: string;
    background: string;
  };
}

export const light: Theme = {
  name: 'light',
  color: {
    primary: 'brown',
    background: 'lightgray',
  },
};

export const dark: Theme = {
  name: 'dark',
  color: {
    primary: 'coral',
    background: 'midnightblue',
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 color는 무한정 추가하는 방식은 좀 무리... 배열로 바꿀 수 있다, key 를 제한하는 타입&lt;/p&gt;
&lt;pre id=&quot;code_1721070587559&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type ThemeName = 'light' | 'dark';
type ColorKey = 'primary' | 'background' | 'secondary' | 'third';

interface Theme {
  name: ThemeName;
  color: Record&amp;lt;ColorKey, string&amp;gt;;
  /*{
    [key in ColorKey]: string;  
  };*/
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;theme css 에 적용하는 코드&lt;/p&gt;
&lt;pre id=&quot;code_1721071232510&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const HeaderStyle = styled.header`
  background-color: ${({theme}) =&amp;gt; theme.color.background};
  h1 {
    color: ${({theme}) =&amp;gt; theme.color.primary};
  }
`;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GloblaStyle 코드에 theme 에 따른 변경 코드 적용하기&lt;/p&gt;
&lt;pre id=&quot;code_1721071771035&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { createGlobalStyle } from 'styled-components';
import 'sanitize.css';
import { ThemeName } from './theme';

interface Props {
  themeName: ThemeName;
}

export const GlobalStyle = createGlobalStyle&amp;lt;Props&amp;gt;`
    body {
        padding:0;
        margin:0;
    }
    h1{
        margin:0;
    }
    *{
        color : ${(props) =&amp;gt; (props.themeName === 'light' ? 'black' : 'white')}
    }
`;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Theme Swicher with Context API&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 토글 UI를 통해 웹사이트의 색상 테마를 바꿀수 있다. 이때 색상 테마는 전역상태로 존재하고, 사용자의 선택값을 로컬스토리지에 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 테마는 app.tsx에서 변환해주고 있기 때문에 import는 app.tsx 에서 해주기!&lt;/p&gt;
&lt;pre id=&quot;code_1721105425535&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from 'react';
import { light, ThemeName } from '../../style/theme';

interface Props {
  themeName: ThemeName;
  setThemeName: (themeName: ThemeName) =&amp;gt; void;
}

const ThemeSticher = ({ themeName, setThemeName }: Props) =&amp;gt; {
  const toggleTheme = () =&amp;gt; {
    setThemeName(themeName === 'light' ? 'dark' : 'light');
  };
  return &amp;lt;button onClick={toggleTheme}&amp;gt;{themeName}&amp;lt;/button&amp;gt;;
};

export default ThemeSticher;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThemeSwicher.tsx 에서 ThemeProvider 안에 ThemeSwicher 을 넣어준다. + GlobalStyle 스타일 지정해주기&lt;/p&gt;
&lt;pre id=&quot;code_1721105570906&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//theme.ts
export const getTheme = (themeName: ThemeName): Theme =&amp;gt; {
  switch (themeName) {
    case 'light':
      return light;
    case 'dark':
      return dark;
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;context api 변경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 스위치 토글을 어디에서든 사용할 수 있도록 변경한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App에서 전체적으로 사용하려면, App 보다 상위인 index.tsx 에서 제공해주어야 한다. &amp;gt; 추천하는 방법이 아니다. &amp;gt; 이따가 context에 provider 제공해서 app.tsx 에서 최상위 감싸는 컴포넌트가 될 것!&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;themeContext.tsx&lt;/p&gt;
&lt;pre id=&quot;code_1721106548928&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { createContext, ReactNode } from 'react';
import { ThemeName } from '../style/theme';

interface State {
  themeName: ThemeName;
  setThemeName: (themeName: ThemeName) =&amp;gt; void;
}

export const state = {
  themeName: 'light' as ThemeName,
  setThemeName: (themeName: ThemeName) =&amp;gt; {},
};

export const ThemeContext = createContext&amp;lt;State&amp;gt;(state);

export const BookStoreThemeProvider = ({children}:{children:ReactNode}) =&amp;gt;{
    return(
        &amp;lt;ThemeContext.Provider value={state}&amp;gt;
        {children}
        &amp;lt;/ThemeContext.Provider&amp;gt;
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;themeName 을 'light'로만 작성했을때, 타입이 아닌 스트링으로 인식하기 때문에, `'light' as ThemeName` 으로 작성해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721106622966&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Layout from './components/layout/Layout';
import Home from './pages/Home';
import { GlobalStyle } from './style/global';
import { ThemeProvider } from 'styled-components';
import { dark, getTheme, light, ThemeName } from './style/theme';
import ThemeSwicher from './components/header/ThemeSwicher';
import { useContext, useState } from 'react';
import { BookStoreThemeProvider, ThemeContext } from './context/themeContext';

function App() {
  const { themeName, setThemeName } = useContext(ThemeContext);
  return (
    &amp;lt;BookStoreThemeProvider&amp;gt;
      &amp;lt;ThemeProvider theme={getTheme(themeName)}&amp;gt;
        &amp;lt;GlobalStyle themeName={themeName} /&amp;gt;
        &amp;lt;ThemeSwicher themeName={themeName} setThemeName={setThemeName} /&amp;gt;
        &amp;lt;Layout children={&amp;lt;Home /&amp;gt;} /&amp;gt;
      &amp;lt;/ThemeProvider&amp;gt;
    &amp;lt;/BookStoreThemeProvider&amp;gt;
  );
}

export default App;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최상위로 감싸고, context를 useContext 상태값으로 가져와서 전역으로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토글기능 : state&lt;/h3&gt;
&lt;pre id=&quot;code_1721116422856&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { createContext, ReactNode, useState } from 'react';
import { getTheme, ThemeName } from '../style/theme';
import { ThemeProvider } from 'styled-components';
import { GlobalStyle } from '../style/global';

interface State {
  themeName: ThemeName;
  toggleTheme: () =&amp;gt; void;
}

export const state = {
  themeName: 'light' as ThemeName,
  toggleTheme: () =&amp;gt; {},
};

export const ThemeContext = createContext&amp;lt;State&amp;gt;(state);

export const BookStoreThemeProvider = ({
  children,
}: {
  children: ReactNode;
}) =&amp;gt; {
  const [themeName, setThemeName] = useState&amp;lt;ThemeName&amp;gt;('light');

  const toggleTheme = () =&amp;gt; {
    setThemeName(themeName === 'light' ? 'dark' : 'light');
  };

  return (
    &amp;lt;ThemeContext.Provider value={{ themeName, toggleTheme }}&amp;gt;
      &amp;lt;ThemeProvider theme={getTheme(themeName)}&amp;gt;
        &amp;lt;GlobalStyle themeName={themeName} /&amp;gt;
        {children}
      &amp;lt;/ThemeProvider&amp;gt;
    &amp;lt;/ThemeContext.Provider&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전의 테마 상태들을, 컨텍스트로 싹다 가져옴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로! app.tsx 의 BookStorethemeProvider로 감싸서, 해당 컴포넌트는 themeContext 에서 전역적으로 관리하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 toggleTheme가 파라미터가 필요한데, 코드가 길어지거나 중요하지 않아 추천하지 않는다. themeContext.tsx에서 toggleTheme 타입 생성시 파라미터 값 없앤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;themeSwicher.tsx 가 컨텍스트를 참조하도록 한다.&lt;/h4&gt;
&lt;pre id=&quot;code_1721116682747&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useContext } from 'react';
import { ThemeContext } from '../../context/themeContext';

const ThemeSticher = () =&amp;gt; {
  const { themeName, toggleTheme } = useContext(ThemeContext);
  return &amp;lt;button onClick={() =&amp;gt; toggleTheme}&amp;gt;{themeName}&amp;lt;/button&amp;gt;;
};

export default ThemeSticher;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로고침 시 테마 light로 초기화된다. 전역상태 초기화 &amp;gt; localstorage 에 저장해서 테마값 유지할 수 있도록 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1721117410823&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//themeContext.tsx
const toggleTheme = () =&amp;gt; {
    setThemeName(themeName === 'light' ? 'dark' : 'light');
    localStorage.setItem(
      THEME_LOCALSTORAGE_KEY,
      themeName === 'light' ? 'dark' : 'light',
    );
  };

  useEffect(() =&amp;gt; {
    const savedThemeName = localStorage.getItem(
      THEME_LOCALSTORAGE_KEY,
    ) as ThemeName;

    setThemeName(savedThemeName || DEFAULT_THEME_NAME);
  }, []);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;themeContext.tsx에서 creageContext를 이용해 ThemeContext를 만들고 전역으로 사용하도록 export 해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;BookStoreThemeProvider&amp;gt; 하위의 컴포넌트들이 context를 구독해서 꺼내서 사용한다 `useContext` 에서 생성한 객체를 넣어서 사용한다. 커스텀 context api, provider 컴포넌트에 넣어서 `ThemeContext.Provider`,`ThemeProvider`, `GlobalStyle`모아서 사용하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 테마 관련 모든 css 는 themeContext.tsx의 `BookStoreThemeProvider` 에서 지정할 수 있다.&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;☑️ 배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이아웃, 컨텍스트 api&lt;/p&gt;</description>
      <category>웹 풀 사이클 데브코스</category>
      <category>til</category>
      <category>데브코스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/175</guid>
      <comments>https://datdaradanadat.tistory.com/175#entry175comment</comments>
      <pubDate>Tue, 16 Jul 2024 17:40:45 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] Day58 - Book Store Project</title>
      <link>https://datdaradanadat.tistory.com/174</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;인증 - query string 필터링 목록 - 상세화면 + 장바구니 - 주문서 작성, 목록 - 환경설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨텍스트 api, use~~ 라이브러리로 전역 상태관리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모달, 드롭다운, 포스트. 무한스크롤,. 슬라이드 배너 라이브러리없이 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반응형 웹&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CRA vs Vite&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;CRA&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Vite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;webpack 압축&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;ESBuild 롤업 빌드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;node.js&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Golang&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;express serverq&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;koa server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;source build&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;module build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;process.env.KEY&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;import.meta.env.KEY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;HMR&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;HMR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;a href=&quot;https://create-react-app.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://create-react-app.dev/&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;a href=&quot;https://vitejs.dev/guide/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://vitejs.dev/guide/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*HMR : Hot Module Reading. 개발 코드 변경시 화면에 즉시 적용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트는 HMR 사용안함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vite 빠른 속도 때문에 주목받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 규모에서는 크게 다르지 않다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치 명령어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npx create-react-app book-store-c --template typescript &amp;gt; `npm run start`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm create vite@latest book-store-v -- --template react-ts &amp;gt; npm install &amp;gt; nvm install 노드버전 &amp;gt; `npm run dev`&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;package.json의 구조가 다르다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CRA : testing, types 도구들 설치&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vite : Jest 도구 직접 설치해서 test, types 사용한다. 모두 포함한 보일러 플레이트&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;CRA 로 프로젝트 진행할 것이다! 컴포넌트를 잘 구분하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;폴더구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App.tsx : app에 모든 컴포넌트를 렌더하면 위험함. 첫화면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;index.tsx : 앱 컴포넌트 구조 마운트 하는 역할을 한다. `reportWebVitals` 앱의 상태를 report 해주는 모듈(제거해도됨)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App.test.tsx : `npm run test`&amp;nbsp; 렌더하고자 하는 링크를 수정해주는 파일이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721021479820&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; src
 ┣  api
 ┣  components
 ┃ ┗  common
 ┃ ┃ ┗  Header.tsx
 ┣  hooks
 ┣  models
 ┃ ┗  user.model.ts
 ┣  pages
 ┃ ┗  Home.tsx
 ┣  utils
 ┃ ┗  foramt.ts
 ┣  App.test.tsx
 ┣  App.tsx
 ┣  index.tsx
 ┣  react-app-env.d.ts
 ┗  setupTests.ts&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;pages : 라우트에 대응하는 페이지 컴포넌트(컨테이너)&lt;/li&gt;
&lt;li&gt;components : 공통 컴포넌트, 각 페이지에서 사용되는 컴포넌트&lt;/li&gt;
&lt;li&gt;utils : 유틸리티. helper, format 등&lt;/li&gt;
&lt;li&gt;hooks : 리액트 훅&lt;/li&gt;
&lt;li&gt;model : 모델(타입)&lt;/li&gt;
&lt;li&gt;api : api 연동을 위한 fetcher 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React CLI - package.json&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;start : 코드 localhost에서 실행&lt;/li&gt;
&lt;li&gt;build : build 폴더 생성된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴파일된 index 등 파일 생성된다. 필요한 react 어플리케이션이 마운트 되는데, static 안에 js 로 컴파일, 번들링 되어있다.&lt;/li&gt;
&lt;li&gt;SPA 앱을 바로 배포할 수 있다. 싱글페이지의 장점&lt;/li&gt;
&lt;li&gt;vs next, nuxt SSR 렌더링 사용할 경우 배포를 위해 nodescript 빌드, 서버가 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;test : 테스트 구동한다.&lt;/li&gt;
&lt;li&gt;eject : webpack 세팅 초기화 및 탈출. 별도의 설정이 필요하거나, 라이브러리나 플러그인에서 필요한 경우에만 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&quot;typecheck&quot;: &quot;tsc --noEmit --skipLibCheck&quot; : 타입스크립트 타입체크 tsc&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1721021980992&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm run typecheck

src/index.tsx:6:3 - error TS2345: Argument of type 'HTMLElement | null' is not assignable to parameter of type 'Container'.
  Type 'null' is not assignable to type 'Container'.

6   document.getElementById('root')
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


Found 1 error in src/index.tsx:6&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;tsc : 타입스크립트 컴파일 명령어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;typecheck&quot;: &quot;tsc --noEmit --skipLibCheck&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tsc : 컴파일을 위한 명령어, 발견 및 알리기 때문에 타입스크립트 명령어로 많이 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;noEmit : 자바스크립트 출력파일 비활성화 &amp;gt; 타입체크로만 사용하기 위해 js 로 변환하는 과정 생략하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;skipLibCheck : 외부라이브러리 타입체크 생략. &amp;gt; 외부라이브러리 많아졌을때 시간을 많이 소요하게 된다.&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;☑️ 배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;testing 도구에 있어서는 Vite가 챙겨주어야 할게 많다는 것을 알았다.&lt;/p&gt;</description>
      <category>웹 풀 사이클 데브코스</category>
      <category>til</category>
      <category>데브코스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/174</guid>
      <comments>https://datdaradanadat.tistory.com/174#entry174comment</comments>
      <pubDate>Mon, 15 Jul 2024 14:44:27 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] Day40 - JavaScript 문법 2</title>
      <link>https://datdaradanadat.tistory.com/173</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️ flow control, 제어 흐름, 흐름 제어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 컴퓨터에게 명령하고, 컴퓨터는 이를 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 이벤트(trigger)로 시작해서~ 원하는 결과를 도출하며 종료된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령형 프로그램의 개별 명령문, 명령 또는 함수 호출이 실행되거나 평가되는 순서&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;goto
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;다른 구문에서 시작.&lt;/li&gt;
&lt;li&gt;개발 설계의 오류를 발생시킬 수 있어 사용하지 말것!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;choice
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;일부 조건이 충족되는 경우에만 일련의 명령문 실행&lt;/li&gt;
&lt;li&gt;`if-else`, `switch`&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;loop
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;어떤 조건이 충족될때까지 일련의 명령문을 0회 이상 실행&lt;/li&gt;
&lt;li&gt;Collection loop, General loop &amp;gt; 자료구조에 따라 종류가 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;continue
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;현재 실행 구문에서 떨어진 한 구문의 집합을 실행&lt;/li&gt;
&lt;li&gt;Loop continuation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;break
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;프로그램 실행을 중단&lt;/li&gt;
&lt;li&gt;Loop early exit(루프 중지), 함수 실행 정지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;표현식 expression&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 값으로 이행되는 임의의 유효한 코드 단위&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표현식이 평가되면, 새로운 값을 생성하거나 기존 값을 참조한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값으로 평가될 수 있는 문은 모두 표현식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 리터럴 표현식,, 함수 표현식...&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;☑️ 문 statement&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램을 구성하는 기본 단위, 최소 실행 단위 `== 명령문`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 선언문, 할당문, 제어문, 반복문, 블럭문 ...&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;블럭문 block statement : 명령문들을 그룹을 한쌍의 중괄호로 묶어서 표현한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;제어문 control flow statement&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;choice 조건문 : `if-else` `switch` 일부 조건이 충족되는 경우에만 일련의 명령문을 실행한다.&lt;/li&gt;
&lt;li&gt;loop - loop early exit : `break`&lt;/li&gt;
&lt;li&gt;loop - loop continuation : `continue`&lt;/li&gt;
&lt;li&gt;subroutine exit : `return`&lt;/li&gt;
&lt;li&gt;non local control flow : `try-catch` `throw` `generator` `async`&lt;/li&gt;
&lt;li&gt;Conditional loop : `while`&amp;nbsp; `do-while`&lt;/li&gt;
&lt;li&gt;general loop : `for`&lt;/li&gt;
&lt;li&gt;collection loop : `for-in`&amp;nbsp; `for-of`&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;조건문 if-else, switch&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;if-else&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논리 조건의 참/거짓에 따라 명령문을 실행해야 할 경우 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if 문의 논리 조건에는 true, false 로 평가할 수 있는 표현식을 대입 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참일경우 &amp;gt; if 블럭문&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거짓일경우 &amp;gt; else 블럭문&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;else if절&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;falsy (false, undefined, null, 0, NaN, &quot;&quot;)는 false 로 평가된다.&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;중첩 if문 &amp;gt; 지향&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;switch 문&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;switch에 명시된 표현식을 평가한 후, 평가된 값과 case 라벨 값을 비교하여 일치하는 case 의 명령문을 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- default&amp;nbsp; : 평가된 값에 해당하는 case 문이 없을경우 default의 명령문이 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라벨 별로 return break 등 case를 종료시키는 코드가 없으면 무한 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Loop 반복분 Conditional loop, For loop, break, continue&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;조건부 loop&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;루프가 시작할때 조건을 평가 : `while`
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;시작할때 통과가 안하면, 본문 생략 가능&lt;/li&gt;
&lt;li&gt;`while(표현식){ 본문 };`&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;마지막에 조건을 평가 : `do-while`
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;본문은 항상 한번 이상 실행&lt;/li&gt;
&lt;li&gt;`do{ 본문 } while(표현식);`&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;For loop&lt;/h4&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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;C언어에서 유래&lt;/li&gt;
&lt;li&gt;loop 변수의 비교 및 증감으로 count 관리를 해야 한다&lt;/li&gt;
&lt;li&gt;`for(초기문; 조건문; 증감문;){....}`&lt;/li&gt;
&lt;li&gt;초기문 : loop 변수 초기화&lt;/li&gt;
&lt;li&gt;조건문 : 코드 실행 전, 조건을 평기해 loop 지속할지 판단&lt;/li&gt;
&lt;li&gt;증감문 : 코드 실행 후 실행되는 문장 &amp;gt; 루프변수 증감 용도, 컨트롤 하는 부분&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;집합 Collection loop 형식 - ForEach loop
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;컬렉션 안의 항목들을 횡단하는 제어흐름문&lt;/li&gt;
&lt;li&gt;&amp;nbsp;카운터 루프변수를 관리하지 않는다&lt;/li&gt;
&lt;li&gt;`for ... of`
&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;`for(변수 fo 객체) { ... }`&lt;/li&gt;
&lt;li&gt;Array, Set, String, ...&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;`for ... in`
&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;`for(변수 in object객체 ) { ... }`&lt;/li&gt;
&lt;li&gt;객체의 속성값 key 을 반환할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;break, continue 문&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;break : 제어 흐름 종류증 프로그램 실행 중단의 종류&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복문, switch문 종료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 가까운 반복문을 종료하고, 다음 명령어로 넘어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;continue : 현재 실행 구문에서 떨어진 한 구문의 집합을 실행하는 제어흐름 종류에 속한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;while, do,..while, for문을 다시 시작하기 위해 사용한다. 가장 안쪽의 반복을 종료하고, 다음 반복을 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;while &amp;gt; 조건문으로 이동&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;for &amp;gt; 증감문으로 이동&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;☑️ 예외처리 Exception&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도치 않은 상황을 제어해야 한다. handeling, control&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외상황을 핸들링하지 않는다면, 기능이 동작하지 않거나 어플리케이션이 shut down 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인 : 언어레벨 ~ 외부요인 등&lt;/p&gt;
&lt;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;li&gt;라이브러리 손상&lt;/li&gt;
&lt;li&gt;사용자의 입력 실수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ECMAScript Error , DOMException&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ECMAScript Error : 자바스크립트 언어에서 발행하는 에러 타입
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;RangeError : 값이 집합에 없거나 허용 범위가 아닐때&lt;/li&gt;
&lt;li&gt;ReferenceError : 존재하지 않는 변수 참조 시&lt;/li&gt;
&lt;li&gt;SyntaxError : 문법을 지키지 않았을 때&lt;/li&gt;
&lt;li&gt;TypeError : 값이 기대한 자료형이 아니여서 연산이 불가능할때&lt;/li&gt;
&lt;li&gt;....&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DOMException : web API 레벨에서 발생하는 에러 타입
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;NetworkError : 네트워크 에러 발생 시&lt;/li&gt;
&lt;li&gt;AboutError : 작업이 중단되었을 때&lt;/li&gt;
&lt;li&gt;TimeoutError : 작업 시간이 초과되었을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;그외 에러들
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;개발자도 예상못한 상황&lt;/li&gt;
&lt;li&gt;개발자가 에러를 직접 상황을 예상해서 핸들링해야한다&lt;/li&gt;
&lt;li&gt;Error 객체를 사용해 에러를 정의내리고 핸들링할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;throw, Error 객체로 에러 핸들링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;throw문 : 예외를 발생시킬때 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;catch 블럭에서 에러 객체를 핸들링한다&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;현재 함수의 실행이 중지&lt;/li&gt;
&lt;li&gt;에러객체와 함께 에러가 throw&lt;/li&gt;
&lt;li&gt;제어 흐름은
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;호출자 사이에 catch 블럭이 있으면, catch 블럭으로 전달&lt;/li&gt;
&lt;li&gt;catch 블럭이 없으면, 프로그램 종료&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`throw 표현식;`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후의 명령문은 실행되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Error 객체 : 사용자가 정의힐 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`new Error('에러 메시지);`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`Error.message = ''`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`Error.name = ''`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ECMAScript 표준 내장 오류 유형이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;try-catch 문&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 catch : 에러를 catch해서 프로그램이 종료되지 않도록 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외처리하는 catch 핸들러를 콜스택에서 찾아 올라가야한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*콜 스택 : 자바스크립트 코드가 실행되면서 실행 컨텍스트가 생성되고, 함수 호출할때마가 스택이 쌓이고, 함수의 실행이 종료되면, 콜스택에서 스택을 제거하는 원리이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 에러 throw 되면, 콜스택을 확인하여, 핸들링 하고 있는 catch문이 있는 스택에서 처리한다.&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;try-catch 문 : 블록 내에서 예외가 발생할 경우 예외 처리를 맡을 하나 이상의 반응 명령문을 지정한다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;try 블록의 명령문 중 하나가 실패하면 catch로 제어권이 넘어간다&lt;/li&gt;
&lt;li&gt;try 블록의 명령문 중 하나가 성겅하면 catch로 제어권이 넘어가지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인자로 throw 된 catchId를 참조할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`try { ... } catch(catchId) { ... }`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외가 처리되면, 더이상 예외가 타고 올라오지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;finally 블록 :&amp;nbsp; 예외상황이 발생하지 않더라도 무조건 실행된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`try { ... } catch(catchId) { ... } finally { ... }`&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;모든상황에서 쓰는건 X&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 예측하기 어려운 에러에 사용 O&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에 의존되어있는 로직에 사용 O&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;☑️ 객체, 빌트인 객체&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;객체 Object&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성을 가진 독립적인 개체 entity &amp;gt; 속성으로 구성된 집합&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JS == 객체 기반 프로그래밍 언어&lt;/p&gt;
&lt;pre id=&quot;code_1718089425373&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const 객체명 = {
	&quot;속성&quot; : 값,
    &quot;속성2&quot; : 값
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성 property : key-value 로 구성되어 있는 연결 관계. 변수가 아닌 객체에 속해있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`objectName.propertyName` 마침표 표기법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`objectName[&quot;propertyName&quot;]` 대괄호 표기법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값 뿐만 아니라 함수도 할당 가능하다 &amp;gt; 객체에 속해있는 함수 `method`&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;method : 메서드는 다른 함수와 동일하게 정의하지만, 객체 속성에 할당되어있다. 짧은 구문으로 작성 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1718090535613&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const 객체 = {
	속성 : function () {
    		console.log('a');
        },
    속성2(){
    	console.log('b');
    },
};

객체.속성(); &amp;gt; a
객체.속성2(); &amp;gt; b&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 생성 방법&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리터럴 표기 : `const foo = { name : 'a' };&lt;/li&gt;
&lt;li&gt;생성자 함수 : `new` 인스턴스 생성
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;속성이 동일한 객체를 여러개 탬플릿처럼 생성할 수 있다.&lt;/li&gt;
&lt;li&gt;생성자 함수를 정의후, 객체 인스턴스를 생성해 사용한다.&lt;/li&gt;
&lt;li&gt;함수명은 꼭 대문자!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1718090749873&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function Person(name, age){
	this.name = name;
    this.age = age;
}
new Person('dah',11);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`Object.create(객체)` : 생성자 함수처럼 동일한 속성값을 갖는 객체를 생성할 수 있다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;`const Joy = Object.create(Person);` : Person과 동일한 키값을 가지는 Joy 객체 생성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;object 빌트인, 객체 속성 control&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성자 : new Object()&lt;/li&gt;
&lt;li&gt;정적 method : 객체 자체 method, 객체 속성 method, 객체 속성 값 method&lt;/li&gt;
&lt;li&gt;인스턴스 method : `hasOwnProperty()` `isPrototypeOf()` `propertyIsEnumerable()` `toLocaleString()` `toString()` `valueOf()`&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;정적 method&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 자체 method : 복사, 신규생성, 고정, 상태확인. 배열로 반환, prototype 관한 메서드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 속성 method : 추가, 서술자, 열거&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 속성의 값 method : 비교 is, 열거 values&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;객체 mutable, 참조형태 &amp;gt; 참조된 객체들도 같이 변경되는 문제점 발생&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성 동적생성 &amp;gt; 객체 밖에서 마침표 표기법으로 선언해주어도 생성된다&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;for-in : 모든 열거 가능한 속성을 순회한다(체인의 속성까지)&lt;/li&gt;
&lt;li&gt;Object.keys(객체) : 객체 자신의 열거 가능한 속성 이름을 배열로 반환한다&lt;/li&gt;
&lt;li&gt;Object.getOwnPropertyNames(객체) : 객체 자신만의 모든 속성을 배열로 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성 삭제 : `delete Person.name;` 반환값 boolean&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;객체 비교와 Reference&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체는 Reference type 참조타입을 가진다. 모든 연산이 참조값 즉 메모리 주소로 처리된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체 비교
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;객체 속성과 값이 같은 객체여도, 참조값이 다르므로 서로 다르다.&lt;/li&gt;
&lt;li&gt;자기자신의 비교에서만 true&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;객체 복사 : 동일한 객체를 생성해야할 때&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;다른 변수에 그대로 할당할 경우, 동일한 메모리 주소 address 만 할당된다&lt;/li&gt;
&lt;li&gt;둘중 하나의 객체를 수정하면, 동일한 메모리주소이기 때문에 결과적으로 2개 다 변경된다&lt;/li&gt;
&lt;li&gt;side effect 를 고려해 복사하는 방법&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;얕은 복사 Shallow copy
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;복사된 객체의 속성 중 하나라도 같은 참조를 하고있을 경우&lt;/li&gt;
&lt;li&gt;참조타입이 없는경우 권장되는 방법&lt;/li&gt;
&lt;li&gt;정적 메소드 `Object.assign({}, 복사할 객체)`&lt;/li&gt;
&lt;li&gt;spread 연산자 `{ ...복사할 객체 }` 객체 속성 열거&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;깊은 복사 Deep copy
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;하나라도 같은 참조를 하고 있는 속성이 없다면, 깊은복사로 복사되었다고 한다&lt;/li&gt;
&lt;li&gt;참조타입을 신구로 해야하기 때문에 순회하면서 참조를 확인하는 방법&lt;/li&gt;
&lt;li&gt;재귀함수를 이용한 복사 : 라이브러리 lodash - cloneDeep&lt;/li&gt;
&lt;li&gt;JSON.stringify : 객체 &amp;gt; 문자열로 변환(참조가 끊어진다) &amp;gt; `parse` 객체로 변환 &amp;gt; 연산의 부담이 발생한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;표준 빌트인 객체 ECMAScript
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;기초객체 : Object, Function, Boolean, Symbol
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오류 객체 : Error&lt;/li&gt;
&lt;li&gt;숫자 및 날짜 : Number, BigInt, Math, Date&lt;/li&gt;
&lt;li&gt;텍스트 처리 : Regex(정규표현식), String(문자열)&lt;/li&gt;
&lt;li&gt;Collection
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Index based Collection : Array&lt;/li&gt;
&lt;li&gt;Key based Collection : Map, Set, ...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;구조화된 데이터 : JSON&lt;/li&gt;
&lt;li&gt;제어 추상화 객체 : Promise, Generator, AsyncFunction,...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;호스트 객체(브라우저 기준) DOM API&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;달러&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;($) 기호는 제이쿼리를 의미하고, 제이쿼리에 접근할 수 있게 해주는 식별자이다.&lt;/span&gt;&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;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;☑️ 가계부 구현&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CurrentAsset&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TODO: 숫자에 콤마 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TODO: currentFunds가 없는 경우&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;  $currentAssetValue.textContent = store.currentFunds?.toLocaleString() ?? &quot;-&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;currentFund 숫자를 replace로 쪼개지말고, parameter 값이 비어있을때, 숫자의 천단위에 콤마 , 를 찍어주는 JS 메소드 사용한다.&lt;/p&gt;
&lt;figure id=&quot;og_1720151887316&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Number.prototype.toLocaleString() - JavaScript | MDN&quot; data-og-description=&quot;The toLocaleString() method of Number values returns a string with a language-sensitive representation of this number. In implementations with Intl.NumberFormat API support, this method simply calls Intl.NumberFormat.&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString#using_tolocalestring&quot; data-og-url=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Nhj1S/hyWvT3Vt1k/w4HDPteA1IZUAAJ1WVkc60/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString#using_tolocalestring&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString#using_tolocalestring&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Nhj1S/hyWvT3Vt1k/w4HDPteA1IZUAAJ1WVkc60/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Number.prototype.toLocaleString() - JavaScript | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The toLocaleString() method of Number values returns a string with a language-sensitive representation of this number. In implementations with Intl.NumberFormat API support, this method simply calls Intl.NumberFormat.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;optional 옵셔널 체이닝 으로 currentFunds가 없는 경우 대응하지 않는다를 ? 문법으로 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if 문, 삼항연산자로도 사용가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Validation&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TODO: 값이 존재하는지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;&quot;. null, undefined&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;export function validateRequired({ category, description, price }) {
  return (
    Boolean(category) &amp;amp;&amp;amp; Boolean(description) &amp;amp;&amp;amp; Boolean(price) &amp;amp;&amp;amp; price &amp;gt; 0
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TODO: 금액이 현재 자산보다 이하인지&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;export function validatePrice(currentFunds, currentAmount) {
  return currentFunds &amp;gt;= currentAmount;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;addNewHistory&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TODO&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;store의 detailList 새로 갱신
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;존재할경우 push, 아닐경우 빈객체 갱신&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;store.currentFunds 새로 갱신
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재자산 - amount&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;export function addNewHistory(newHistory) {
  try {
  //store의 detailList 새로 갱신
    if (store.detailList[store.todayId]) {
      store.detailList[store.todayId] = store.detailList[store.todayId].push(newHistory);
    } else {
      store.detailList[store.todayId] = [newHistory];
    }
    
    //store.currentFunds 새로 갱신
    store.currentFunds -= newHistory.amount;

    updateStorage();
    return true;
  } catch (error) {
    alert(error);
    return false;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 매핑&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TODO: 데이터 매핑&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열을 amp 돌리면 &amp;gt; 배열 형태로 반환됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 형태는 문자열이기 때문에 join() 메소드 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값들을 매핑하기 위해서 map 안 요소에 들어가야할 항목들 작성하고, html에 띄워준다&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;$sectionHistory.innerHTML = store.dateList
    .map(({ date, id: dateId }) =&amp;gt; {
      const detail = store.detailList[dateId];
      if (!detail?.length) return &quot;&quot;;

      return `&amp;lt;article class=&quot;history-per-day&quot;&amp;gt;
      &amp;lt;p class=&quot;history-date&quot;&amp;gt;2021년 12월 1일&amp;lt;/p&amp;gt;
      ${detail
        .map(({ description, category, amount, fundsAtTheTime, createAt }) =&amp;gt; {
          return `&amp;lt;section class=&quot;history-item&quot;&amp;gt;
            &amp;lt;section class=&quot;history-item-column&quot;&amp;gt;
              &amp;lt;div class=&quot;create-at&quot;&amp;gt;${createAt}&amp;lt;/div&amp;gt;
              &amp;lt;div class=&quot;history-detail&quot;&amp;gt;
                &amp;lt;div class=&quot;history-detail-row history-detail-title&quot;&amp;gt;
                  &amp;lt;p&amp;gt;${description}&amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;history-detail-row history-detail-subtitle&quot;&amp;gt;
                  &amp;lt;p&amp;gt;${category}&amp;lt;/p&amp;gt;
                  &amp;lt;p&amp;gt;
                    ${amount}
                    &amp;lt;span&amp;gt;원&amp;lt;/span&amp;gt;
                  &amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/div&amp;gt;
              &amp;lt;div class=&quot;delete-section&quot;&amp;gt;
                &amp;lt;button class=&quot;delete-button&quot;&amp;gt; &amp;lt;/button&amp;gt;
              &amp;lt;/div&amp;gt;
            &amp;lt;/section&amp;gt;
            &amp;lt;section class=&quot;history-item-caption&quot;&amp;gt;
              &amp;lt;p&amp;gt;
                &amp;lt;span&amp;gt;남은 자산&amp;lt;/span&amp;gt;
                &amp;lt;span&amp;gt;${fundsAtTheTime}&amp;lt;/span&amp;gt;
                &amp;lt;span&amp;gt;원&amp;lt;/span&amp;gt;
              &amp;lt;/p&amp;gt;
            &amp;lt;/section&amp;gt;
          &amp;lt;/section&amp;gt;`;
        })
        .join(&quot;&quot;)}

      
    &amp;lt;/article&amp;gt;`;
    })
    .join(&quot;&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TODO: 항목의 시간 포맷 변경: HH:mm&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DateISOType 로 작성되어있기 때문에, 예쁘게 띄우려면 데이터 가공이 필요하다!&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;iso 타입을 date 객체로 변환&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;const time = new Date(createAt).toLocaleTimeString(&quot;ko-kr&quot;, {
            timeStyle: &quot;short&quot;,
            hourCycle: &quot;h24&quot;,
          });
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1720151913306&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Date.prototype.toLocaleTimeString() - JavaScript | MDN&quot; data-og-description=&quot;The toLocaleTimeString() method of Date instances returns a string with a language-sensitive representation of the time portion of this date in the local timezone. In implementations with Intl.DateTimeFormat API support, this method simply calls Intl.DateT&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString&quot; data-og-url=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/I6xBl/hyWvLkwy6B/Numh3Uxq28kY1XHadZylm1/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/I6xBl/hyWvLkwy6B/Numh3Uxq28kY1XHadZylm1/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Date.prototype.toLocaleTimeString() - JavaScript | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The toLocaleTimeString() method of Date instances returns a string with a language-sensitive representation of the time portion of this date in the local timezone. In implementations with Intl.DateTimeFormat API support, this method simply calls Intl.DateT&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1720151923478&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;How do I use .toLocaleTimeString() without displaying seconds?&quot; data-og-description=&quot;I'm currently attempting to display the user's time without displaying the seconds. Is there a way I can do this using Javascript's .toLocaleTimeString()? Doing something like this: var date = ne...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/17913681/how-do-i-use-tolocaletimestring-without-displaying-seconds&quot; data-og-url=&quot;https://stackoverflow.com/questions/17913681/how-do-i-use-tolocaletimestring-without-displaying-seconds&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bdAfg0/hyWvMqcG1l/dwUwzfidDoEfCxvCLGKYeK/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/17913681/how-do-i-use-tolocaletimestring-without-displaying-seconds&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/17913681/how-do-i-use-tolocaletimestring-without-displaying-seconds&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bdAfg0/hyWvMqcG1l/dwUwzfidDoEfCxvCLGKYeK/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;How do I use .toLocaleTimeString() without displaying seconds?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I'm currently attempting to display the user's time without displaying the seconds. Is there a way I can do this using Javascript's .toLocaleTimeString()? Doing something like this: var date = ne...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TODO: 오름차순으로 목록 나열&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소비내역 리스트 최신항목이 가장 상단에!&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;${detail
        .sort((a, b) =&amp;gt; b.id - a.id)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TODO: 금액 콤마 포맷 맞추기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;${amount.toLocaleString()}&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;${fundsAtTheTime.toLocaleString()}&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;removeHistory&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TODO:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;store의 detailList 새로 갱신&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 배열 &amp;gt; 삭제할 요소를 제거한 배열 &amp;gt; 재할당&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;store.detailList[dateId] = store.detailList[dateId].filter(({ id }) =&amp;gt; {
      return id !== Number(itemId);
    });
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;store.currentFunds 새로 갱신&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;삭제할 요소일때 &amp;gt; 전체 금액에 더해서 갱신&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;store.detailList[dateId] = store.detailList[dateId].filter(
      ({ id, amount }) =&amp;gt; {
        if (id === Number(itemId)) {
          store.currentFunds += amount;
        }
        return id !== Number(itemId);
      }
    );
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제 버튼 클릭 할때 &amp;gt; 각 요소 id 매핑&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;button class=&quot;delete-button&quot; data-dateid=${dateId} data-itemid=${id}&amp;gt; &amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️ 배운 점&lt;/h2&gt;
&lt;figure id=&quot;og_1720151980437&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - chae-dahee/dev-JScalculator: 프로그래머스 - 2023 자바스크립트 입문 - 가계부 서비스 구현하기&quot; data-og-description=&quot;프로그래머스 - 2023 자바스크립트 입문 - 가계부 서비스 구현하기. Contribute to chae-dahee/dev-JScalculator development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/chae-dahee/dev-JScalculator/tree/dah&quot; data-og-url=&quot;https://github.com/chae-dahee/dev-JScalculator&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/KOdb6/hyWvWzyy6k/rkFtFFp2TRwKskHPgdufK0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/chae-dahee/dev-JScalculator/tree/dah&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/chae-dahee/dev-JScalculator/tree/dah&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/KOdb6/hyWvWzyy6k/rkFtFFp2TRwKskHPgdufK0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - chae-dahee/dev-JScalculator: 프로그래머스 - 2023 자바스크립트 입문 - 가계부 서비스 구현하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스 - 2023 자바스크립트 입문 - 가계부 서비스 구현하기. Contribute to chae-dahee/dev-JScalculator development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>웹 풀 사이클 데브코스</category>
      <category>til</category>
      <category>데브코스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/173</guid>
      <comments>https://datdaradanadat.tistory.com/173#entry173comment</comments>
      <pubDate>Fri, 5 Jul 2024 13:00:28 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] Day57 - 컨트리뷰트 어필, 저작자</title>
      <link>https://datdaradanadat.tistory.com/172</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;채용 어필하는 방법&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;협업 능력 : 코드 대화 가능, 이슈~토론&lt;/li&gt;
&lt;li&gt;프로젝트 문해력 : 기획, 설계, 구현~테스트, .. 유지보수, 리팩토리, 운영...&lt;/li&gt;
&lt;li&gt;코드 분석 : 언어의 특징, 인사이트 &amp;gt; 코드 구현 능력!&lt;/li&gt;
&lt;li&gt;개발 문화 속에서 성장 경험&lt;/li&gt;
&lt;li&gt;꾸준한 노력/개선 *블로그&lt;/li&gt;
&lt;li&gt;문서, 주석, 설계, 테스트, 기능 제안 &amp;gt; 개발자 한정 X&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IT 서비스 기획 : 이 기능이 구현 가능한지 파악할 수 있는 내용&lt;/li&gt;
&lt;li&gt;QA : 사용자의 경험 테스트&lt;/li&gt;
&lt;li&gt;다양한 부분에 큰 경쟁력이 될 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;팀스테이지 발표 70% 프로젝트 실패 &amp;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;/ol&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오픈소스 저작자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저작자 : 오픈소스 프로젝트를 만든 사람 or 조직&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트리뷰트 vs 저작자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용중인 오픈소스 프로젝트에 코드를 구현해서 컨트리뷰트 PR 하는 기능 수정하고자 하는 추가욕심&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나만의 + 다른 사람들도 필요하기 때문에 일단 도전하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반려를 당하면 ? 오픈소스 프로젝트 fork해서 별도의 프로젝트를 내가 직접 운영한다. &amp;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈소스 프로젝트가 되는 코드가 따로 있나? 노노 &amp;gt; 내 프로젝트로 오픈소스를 설정할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm 아주 간단한 라이브러리들도 가능하다. &amp;gt; 한번 만들고 싶은걸!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MIT, ReadMe.md, contributing 문서 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복아 아닌 명확한 ⭐ 프로젝트 이름 명확하게 ⭐&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;Readme
&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;Contributing
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가이드(다른 플젝 참고) or 환영메시지(템플릿 없음)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;LICENSE : 라이선스 전문&lt;/li&gt;
&lt;li&gt;프로젝트 이름 : 상표권 침해를 피하기 위해 가능한 중복 X, 기능이 명확한&lt;/li&gt;
&lt;li&gt;코드
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이해하기 쉬운 코드(*명명법)&lt;/li&gt;
&lt;li&gt;주석 설명 깔끔&lt;/li&gt;
&lt;li&gt;데드코드 정리&lt;/li&gt;
&lt;li&gt;계정 개인 정보 등 민감 정보 제외&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;fork해서 사용할 시 기존 프로젝트 라이선스 신경쓰기&lt;/li&gt;
&lt;li&gt;만약, 회사 프로젝트 : 사내 법무팀, 오픈소스 담당 팀 자문 / 담당자 추가 / 외부에 바로 오픈X (사내 오픈 먼저)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오픈소스 사용 체크리스트 (in 회사)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;금융권 : 시큐어 코팅 by 금융감독원, 금융보안원 &amp;gt; 오픈소스 체크리스트, 활용/관리 안내서를 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 변화가 잦지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;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;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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[개발 중]&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;취약점 최소화 : 보안 정책팀(오픈소스 담당) 협업 &amp;gt; 인증, 주요기능 이중체크로 보안 강화&lt;/li&gt;
&lt;li&gt;대체 수단 확보 : 예외, 법적 이슈 발생 &amp;gt; 대비책 마련(우회)&lt;/li&gt;
&lt;li&gt;자체 대응 및 추가 개발 역량 확부 : 충분한 이해를 바탕으로 기술력 확보하기 &amp;gt; 문제 해결 능력 갖춤&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[개발 후] &amp;gt; 자동 모니터링 체크&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;끊임없는 모니터링 : 오픈소스 현황, 취약점 업데이트&lt;/li&gt;
&lt;li&gt;오픈소스 보안패치 : 확인, 취약점 최소화, 적용&lt;/li&gt;
&lt;li&gt;오픈소스 종속성 검사 : 문제 발생, 최대한 빠른 해결&lt;/li&gt;
&lt;li&gt;기능 및 보안성 테스트 : 테스트 부서, 보안 팀,, 협업을 통해 지속적으로 테스트, 분석, 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☑️ 배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm 오픈소스 만들고자 생각한다!&lt;/p&gt;</description>
      <category>웹 풀 사이클 데브코스</category>
      <category>til</category>
      <category>데브코스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/172</guid>
      <comments>https://datdaradanadat.tistory.com/172#entry172comment</comments>
      <pubDate>Thu, 4 Jul 2024 17:34:06 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] Day56 - 오픈소스 프로젝트, 추천 사이트</title>
      <link>https://datdaradanadat.tistory.com/171</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;오픈소스 찾기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깃허브 Explore &amp;gt; Topics 에 핫한 주제의 오픈소스 및 프로젝트를 확인할 수 있다.&lt;/p&gt;
&lt;figure id=&quot;og_1719936103398&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub: Let&amp;rsquo;s build from here&quot; data-og-description=&quot;GitHub is where over 100 million developers shape the future of software, together. Contribute to the open source community, manage your Git repositories, review code like a pro, track bugs and fea...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/topics&quot; data-og-url=&quot;https://github.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cVcGJE/hyWvMb9HFu/kiADcx0C4rWZpNCP8STB70/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://github.com/topics&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/topics&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cVcGJE/hyWvMb9HFu/kiADcx0C4rWZpNCP8STB70/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub: Let&amp;rsquo;s build from here&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;GitHub is where over 100 million developers shape the future of software, together. Contribute to the open source community, manage your Git repositories, review code like a pro, track bugs and fea...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈소스인 플젝도 있고, 아닌 플젝도 있다 &amp;gt; license 꼭 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;없는 코드라면? 가져와서 수정하면 안됨 &amp;gt; 포크 참고해서 다른 코드 만들거나, 레포 저장하기위한 포크 ok &amp;gt; 오픈소스는 아니다! 리뷰는 못남기지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈소스로 할래? 라고 제안할 수는 있다 insight &amp;gt; Propose &amp;gt; License 종류까지 제시할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제안할때 ?&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;어떤 프레임워크, 모듈이랑 같이 쓰이는지? (ex. node.js + express)&lt;/li&gt;
&lt;li&gt;npm 모듈이 어떤 라이선스에 적용되는지 &amp;gt; 그중 많이 쓰이는 MIT 라이선스 &amp;gt; 크게 고려할 것이 없다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;* 가장 간단하고, 고려할것이 적고, 이해하기 쉽고 '저작자 보호해주세요' &amp;gt; 누구나 사용 하능한 `MIT 라이선스`&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;`Apach` ? 웹 서버 관련 + 기업이 사용하기를 원한다면! : 특허 관련된 내용이 있기 때문이다.&lt;/li&gt;
&lt;li&gt;오픈소스 커뮤니티. 프로젝트 구성완, 컨트리뷰터 등등 히스토리 `GNU (L/A/)GPL v3`&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;라이선스는 언제나 변경 가능하다&lt;/h2&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;gt; 논란 후에 변경되는 경우&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;mongoDB : 급상 데이터베이스&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AGPL &amp;gt; SSPL&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;elasticsearch : 검색엔진. 데이터베이스 select + 네트워크 SPA에서 검색하는 엔징
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Apache &amp;gt; SSPL + Elastics License&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Grafana : 데이터 분석, cpu, 메모리 사용율 시각화하는 웹 어플리케이션
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Apache &amp;gt; AGPL&lt;/li&gt;
&lt;li&gt;규정이 좀 쎈걸로 바뀜. 오픈소스를 이용해 서비스를 마구잡이로 수정 + 비공개되어서 논란이 생김&lt;/li&gt;
&lt;li&gt;AGPL : 오픈소스 장려&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SENTRY : 에러를 추천하고 로딩하는 라이브러리 &amp;gt; 빅테크에서 FE, BE에서 사내시스템이 적용중이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BSD 3 Clause &amp;gt; BUSL&lt;/li&gt;
&lt;li&gt;BUSL : 마리아 DB에서 자체적으로 만듬. 소스 수정, 배포, 컴파일 가능 &amp;gt; 상용 서비스 목적 사용 금지 &amp;gt; 기간이 지나면 Apache로 바꿔서 상용 서비스 가능 &amp;gt; 유료로 사용 금지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AWS vs mongoDB vs elasticsearch&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;aws : 아마존 클라우드 서비스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSPL - Server Side Public License : 몽고DB가 직접 만들었음. 클라우드 서비스에서 상업적으로 사용 금지. 서비스를 기동하는데 필요한 &lt;b&gt;연동된 모든 코드&lt;/b&gt;를 다 공개해라.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OSI에서 인정하지 않았다.. 공정성의 문제가 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elastics License : 호스팅, 클라우드 등의 서비스는 오픈소스로 추가해서 상업적으로 사용 금지.&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;aws에서 mongoDB의 오픈소스를 가져와서 사용자에게 유료로 돈을 받음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라우드 + 서치엔징 최고의 효율을 보여주기에 붙여서 많이 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고디비가 고집하는 이유는? 오픈소스 자유, 상업적 금지.. 다같이 오픈해서 코드개발 크게 하자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RedHat 리눅스 오픈소스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스 공개 &amp;gt; 유료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스 오픈소스로 &amp;gt; 개별리눅스 업체가 가져가서 새로운 OS 만들고 있어서 논란이 됨&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;오픈소스 프로젝트 찾는법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;깃허브&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;star 수가 많을 경우 오픈소스이다!&lt;/li&gt;
&lt;li&gt;알고리즘 천천히 쌓아가기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구글 코드인 - 2019년&lt;/h3&gt;
&lt;figure id=&quot;og_1719939961846&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Google Code-in Archive&quot; data-og-description=&quot;Google's contest introducing 13-17 year old pre-university students to open source software development.&quot; data-og-host=&quot;codein.withgoogle.com&quot; data-og-source-url=&quot;https://codein.withgoogle.com/archive/&quot; data-og-url=&quot;https://codein.withgoogle.com/archive/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bIxmML/hyWvSQY1U9/FepE7WdfMq4d8wvmaxCLV1/img.png?width=192&amp;amp;height=192&amp;amp;face=0_0_192_192&quot;&gt;&lt;a href=&quot;https://codein.withgoogle.com/archive/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://codein.withgoogle.com/archive/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bIxmML/hyWvSQY1U9/FepE7WdfMq4d8wvmaxCLV1/img.png?width=192&amp;amp;height=192&amp;amp;face=0_0_192_192');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Google Code-in Archive&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Google's contest introducing 13-17 year old pre-university students to open source software development.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;codein.withgoogle.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중고등학생 대상 교육목적 오픈소스 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진행중인 task, 목적, 사용자가 할 수 있는 contribute에 대해 설명해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어던 종류의 프로젝트가 오픈소스가 되는지.. 공부하기 위한, 파악하기 위한 사이트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개별 회사 및 조직 - 네이버&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 전체적으로 분석해서 갈아엎기보다는,, 간단한 오탈자, 주석 등을 수정하기&lt;/p&gt;
&lt;figure id=&quot;og_1719940674900&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;NAVER Open Source&quot; data-og-description=&quot;Open, Share and Enjoy - NAVER Open Source&quot; data-og-host=&quot;naver.github.io&quot; data-og-source-url=&quot;https://naver.github.io/&quot; data-og-url=&quot;https://naver.github.io&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/be0wnJ/hyWrObEg1f/rSPkKLHXuqSGddw38G2r80/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400&quot;&gt;&lt;a href=&quot;https://naver.github.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://naver.github.io/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/be0wnJ/hyWrObEg1f/rSPkKLHXuqSGddw38G2r80/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;NAVER Open Source&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Open, Share and Enjoy - NAVER Open Source&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;naver.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1719940790004&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;함께 성장하는 기술, Kakao open source&quot; data-og-description=&quot;#engineering #opensource #software #tech&quot; data-og-host=&quot;www.kakaocorp.com&quot; data-og-source-url=&quot;https://www.kakaocorp.com/page/detail/542&quot; data-og-url=&quot;https://www.kakaocorp.com/page/detail/542&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/kWKki/hyWrL64oOM/3isCbQcZUspK5DhfnVw6pK/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/cFOovl/hyWrTD1Tp3/UUdK8qh4oFjbUbG3UxFja1/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400&quot;&gt;&lt;a href=&quot;https://www.kakaocorp.com/page/detail/542&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.kakaocorp.com/page/detail/542&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/kWKki/hyWrL64oOM/3isCbQcZUspK5DhfnVw6pK/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/cFOovl/hyWrTD1Tp3/UUdK8qh4oFjbUbG3UxFja1/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;함께 성장하는 기술, Kakao open source&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;#engineering #opensource #software #tech&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.kakaocorp.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1719941008182&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - toss/es-hangul: A modern JavaScript library for handling Hangul characters.&quot; data-og-description=&quot;A modern JavaScript library for handling Hangul characters. - toss/es-hangul&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/toss/es-hangul&quot; data-og-url=&quot;https://github.com/toss/es-hangul&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cPfH4U/hyWrSZo7y4/spvK1cEfYqEhnPzYtMIKP0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/toss/es-hangul&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/toss/es-hangul&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cPfH4U/hyWrSZo7y4/spvK1cEfYqEhnPzYtMIKP0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - toss/es-hangul: A modern JavaScript library for handling Hangul characters.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A modern JavaScript library for handling Hangul characters. - toss/es-hangul&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;h3 data-ke-size=&quot;size23&quot;&gt;⭐ codetriage&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응급환자 우선순위 라는 뜻 &amp;gt; 컨트리뷰트가 시급한? 우선순위를 나눈 오픈소스 보여줌 : 이슈가 많이 열려있는 순서&lt;/p&gt;
&lt;figure id=&quot;og_1719941048697&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Get Started Contributing to Open Source Projects | CodeTriage&quot; data-og-description=&quot;&quot; data-og-host=&quot;www.codetriage.com&quot; data-og-source-url=&quot;https://www.codetriage.com/&quot; data-og-url=&quot;https://www.codetriage.com/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.codetriage.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.codetriage.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Get Started Contributing to Open Source Projects | CodeTriage&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.codetriage.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빨 &amp;gt; 주 &amp;gt; 노 &amp;gt; 초&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flutter, vscode 등 오픈소스로 열려있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깃허브의 플젝설명 및 이슈, 언어 등 미리 볼 수 있는 정보를 보여준다. Good!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Triage Inssues : 열려있는 이슈를 이메일로 전송한다.&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;gt; 돈을 주고 깃허브 스타를 받는 이벤트 &amp;gt; 사과문..&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;☑️ 배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;codetriage 사이트를 알게 되어 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈소스 propose 하는 방법!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신입개발자의 나에겐 조금 먼 거리의 내용이라고 편견이 있었는데, 원하는 회사의 오픈소스 코드를 구경할 수 있고, 작은 부분도 기여하는 부분이 생긴다면 정말 뜻깊을 것 같다. 나도 이런 경험 하기 위해 틈틈히, 꼼꼼히 살펴보는 개발자가 되자!&lt;/p&gt;</description>
      <category>웹 풀 사이클 데브코스</category>
      <category>til</category>
      <category>데브코스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/171</guid>
      <comments>https://datdaradanadat.tistory.com/171#entry171comment</comments>
      <pubDate>Wed, 3 Jul 2024 02:37:03 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] Day55 - 오픈소스 컨트리뷰터 Contributor</title>
      <link>https://datdaradanadat.tistory.com/170</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;오픈소스 구성원&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저작자 : 오픈소스 플젝 만든 사람 or 조직&lt;/li&gt;
&lt;li&gt;사용자 : 오픈소스 플젝 사용하는 사람&lt;/li&gt;
&lt;li&gt;Maintainer 메인테이너 : 프로젝트를 관리하는 책임이 있는 사람
&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;없어도 돌아가기는 한다. &amp;gt; 컨트리뷰터가 날린 pr 을 관리하는 커미터 존재&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;contributor 컨트리뷰터 : 컨트리뷰션 활동을 하는 모든 사람&lt;/li&gt;
&lt;li&gt;Commitor 커미터 : 컨트리뷰션을 리뷰하는 사람. 컨트리뷰터 중 하나
&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨트리뷰터의 유형&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에 한정되지 않음 &amp;gt; 소스코드 뿐만 아니라 + 테스트, 배포, 가이드(문서화), 기획, 설계, 분석 등등....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 컨트리뷰터 1000명 이상.....~&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://legacy.reactjs.org/docs/how-to-contribute.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt;에서 가이드를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;380&quot;&gt;&lt;a href=&quot;https://naver.github.io/OpenSourceGuide/book/BetterContribution/why-contribute-to-open-source.html&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/efZgEs/btsIlnZdWD0/3d9AQF1lp0Vz6pHXEKyIT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FefZgEs%2FbtsIlnZdWD0%2F3d9AQF1lp0Vz6pHXEKyIT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;708&quot; height=&quot;380&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/a&gt;&lt;figcaption&gt;네이버 오픈소스 가이드 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오타 수정&lt;/li&gt;
&lt;li&gt;번역 + 제안&lt;/li&gt;
&lt;li&gt;문서 설명 추가&lt;/li&gt;
&lt;li&gt;배너 문구 수정 제안&lt;/li&gt;
&lt;li&gt;UI/UX 제안&lt;/li&gt;
&lt;li&gt;버그 픽스 + 제안&lt;/li&gt;
&lt;li&gt;기능 제안&lt;/li&gt;
&lt;li&gt;문서작업&lt;/li&gt;
&lt;li&gt;기능 추가/수정/삭제&lt;/li&gt;
&lt;li&gt;리팩토링 e &amp;gt; expired&lt;/li&gt;
&lt;li&gt;버전, 외부모둘 변경 제안&lt;/li&gt;
&lt;li&gt;에러 메시지 구체화&lt;/li&gt;
&lt;li&gt;리소스&lt;/li&gt;
&lt;li&gt;테스트 케이스 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;활동 이유?&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;등등 혼자서는 절대 찾을 수 없는 사항들을 찾아줄 듯&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자/컨트리뷰터 입장&lt;/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;코드 분석 &amp;gt; 코드 문해력 성장&lt;/li&gt;
&lt;li&gt;코드 보는 시선, 관점, 시야를 넓힐 수 있다.&lt;/li&gt;
&lt;li&gt;내가 원하는 기능을 추가하고, 코드 짜는데도&amp;nbsp; 편리함 &amp;gt; 객관적인 평가를 받아볼 경험&lt;/li&gt;
&lt;li&gt;전세계 멋진 개발자에게 코드 리뷰를 받을 수 있다.&lt;/li&gt;
&lt;li&gt;이력서, 포트폴리오 &amp;gt; 개발 문화에 진심이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OSS.kr(Open Source Software) 오픈소스 소프트웨어.한국&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스코드는 공개 + 라이선스 = 소프트웨어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 한국 공공기관 : 공개 소프트웨어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세계 오픈소스 담당 협회 OSI : 오픈소스 인증 마크&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국 저작권협회 OLIS : 라이선스 관련 정보&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈소스 소프트웨어 통합지원센터 &amp;nbsp;&amp;nbsp;&lt;a href=&quot;https://www.oss.kr/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.oss.kr/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719924988060&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;공개SW 포털 - 공개SW 포털&quot; data-og-description=&quot;WELCOME There are a million reasons to use XE3. XpressEngine은 자유로운 웹 콘텐츠 발행을 돕는 CMS입니다. Laravel ...&quot; data-og-host=&quot;www.oss.kr&quot; data-og-source-url=&quot;https://www.oss.kr/&quot; data-og-url=&quot;https://www.oss.kr&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.oss.kr/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.oss.kr/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;공개SW 포털 - 공개SW 포털&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;WELCOME There are a million reasons to use XE3. XpressEngine은 자유로운 웹 콘텐츠 발행을 돕는 CMS입니다. Laravel ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.oss.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과학기술정보통신부 등 공공기관에서 관리한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.oss.kr/user_question&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;열린마당의 문의하기&lt;/a&gt;가 활성화되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈소스 해커톤 &amp;gt; 아카데미로 변경됨. 컨트리뷰션 현업자들이 참여하는 멘토링 행사&lt;/p&gt;
&lt;figure id=&quot;og_1719925167351&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;공개SW 포털 - 공개SW 포털&quot; data-og-description=&quot;공개SW 포털&quot; data-og-host=&quot;www.oss.kr&quot; data-og-source-url=&quot;https://www.oss.kr/contribution_academy&quot; data-og-url=&quot;https://www.oss.kr/contribution_academy&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.oss.kr/contribution_academy&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.oss.kr/contribution_academy&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;공개SW 포털 - 공개SW 포털&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;공개SW 포털&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.oss.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트렌드에 민감하게 반응하려고 잘 찾아보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;/li&gt;
&lt;li&gt;소스코드 충돌
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;contribute 하고 싶은 내용을 이미 구현하고 있지는 않은지, 구현하지 않기로 결정한 건 아닌지 체크 필수!&lt;/li&gt;
&lt;li&gt;시작하기 전에 내가 작업을 시작할 것을 알리는 것이 좋다(=이슈 오픈, discusion)&lt;/li&gt;
&lt;li&gt;너무 오래된 open issue &amp;gt; discusion이나 커뮤니티로 직접 질문하기 &amp;gt; stop or 진행됨 or 취소 &amp;gt; 오래된, 작업하지 않은, 정리되지 않은 이슈 정리해라! &amp;gt; 일종의 contribution&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;gt; 구현했는데 방향성이 안맞을 수도 있으니 미리 문의 필수&lt;/li&gt;
&lt;li&gt;내부적으로 함께 기능 개발할 팀 구하기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;PR 던질때 템플릿 가이드 나 문서를 확인하면서 준수할 내용(테스트 유무, 컨벤션 등) 체크한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨트리뷰트 절차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;contributing 문서 등에 절차 가이드를 제공하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국어 버전&lt;/p&gt;
&lt;figure id=&quot;og_1719926676850&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;docs/contributing/how_to_open_a_pull_request-ko.md at master &amp;middot; cloud-barista/docs&quot; data-og-description=&quot;Cloud-Barista Community's materials. Contribute to cloud-barista/docs development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/cloud-barista/docs/blob/master/contributing/how_to_open_a_pull_request-ko.md&quot; data-og-url=&quot;https://github.com/cloud-barista/docs/blob/master/contributing/how_to_open_a_pull_request-ko.md&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bB8s1T/hyWvRxJZJL/buTpzT4wKgb9GW6BgKg6pk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/cloud-barista/docs/blob/master/contributing/how_to_open_a_pull_request-ko.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/cloud-barista/docs/blob/master/contributing/how_to_open_a_pull_request-ko.md&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bB8s1T/hyWvRxJZJL/buTpzT4wKgb9GW6BgKg6pk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;docs/contributing/how_to_open_a_pull_request-ko.md at master &amp;middot; cloud-barista/docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Cloud-Barista Community's materials. Contribute to cloud-barista/docs development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개요, 역할, 참고, 절차를 설명하고 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;오픈소스 프로젝트 Fork &amp;gt; 내 레포&lt;/li&gt;
&lt;li&gt;내 레포 &amp;gt; 내 로컬 pc 로 clone&lt;/li&gt;
&lt;li&gt;코드 컨벤션, 커밋 메시지 등 코드 구현 전에 체크해야 하는 규칙 확인 및 준수하며 작업&lt;/li&gt;
&lt;li&gt;코드 구현, 수정 + 주석 수정 + 명명법 등&lt;/li&gt;
&lt;li&gt;내 레포로 Push&lt;/li&gt;
&lt;li&gt;오픈소스 레포로 Pull Request&lt;/li&gt;
&lt;li&gt;Contributor License Agreement : 오픈소스에 기여한 사람으로써 라이선스에 동의함? 확인&lt;/li&gt;
&lt;li&gt;리뷰어, 커미터, 메인테이너, 저작자 등 검토할 사람에게 리뷰 후 머지 (검토 생략하고 바로 머지되는 경우도 있음)&lt;/li&gt;
&lt;li&gt;merge 되었다 PR 닫혔다 등의 깃허브 이메일이 날라온다.&lt;/li&gt;
&lt;li&gt;오픈소스 깃허브 Contributor 리스트에 내 계정이 추가됨&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1930&quot; data-origin-height=&quot;695&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ewIS5R/btsIkF686Ss/xO1nlV6RKTwJ8SgNjvBepk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ewIS5R/btsIkF686Ss/xO1nlV6RKTwJ8SgNjvBepk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ewIS5R/btsIkF686Ss/xO1nlV6RKTwJ8SgNjvBepk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FewIS5R%2FbtsIkF686Ss%2FxO1nlV6RKTwJ8SgNjvBepk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1930&quot; height=&quot;695&quot; data-origin-width=&quot;1930&quot; data-origin-height=&quot;695&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 내 로컬 과 저작자 레포는 연결되어 있지 않다.. &amp;gt; 내계정에서 PR 만 안날라면 무관한 레포나 다름없음&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;☑️ 배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트폴리오가 된다..... 실무에서 새 기능을 런칭하는 경우도 있겠지만, 기존 코드를 리팩토링하는 경우가 더 많을 수도 있다. 그럴 경우 컨트리뷰트 경험이 큰 도움이 될 것이라고 생각했다.&lt;/p&gt;</description>
      <category>웹 풀 사이클 데브코스</category>
      <category>til</category>
      <category>데브코스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/170</guid>
      <comments>https://datdaradanadat.tistory.com/170#entry170comment</comments>
      <pubDate>Tue, 2 Jul 2024 22:41:14 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] Day54 - 오픈소스, 깃허브</title>
      <link>https://datdaradanadat.tistory.com/169</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;오픈소스 코드 공개시 &amp;gt; 깃허브 가이드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수익을 챙길시 꼼꼼히 챙기기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈소스의 공식 홈페이지 링크를 활용하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에 사용시 &amp;gt; 깃허브 레포지토리 Readme / License.txt 에 명시&lt;/p&gt;
&lt;figure id=&quot;og_1719912071494&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;오픈 소스 가이드&quot; data-og-description=&quot;Learn how to launch and grow your project.&quot; data-og-host=&quot;opensource.guide&quot; data-og-source-url=&quot;https://opensource.guide/ko/&quot; data-og-url=&quot;https://opensource.guide/ko/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/nVpyJ/hyWrZEaMzT/hFueVzSFifIO3h9WCc2Eb0/img.png?width=1000&amp;amp;height=536&amp;amp;face=0_0_1000_536&quot;&gt;&lt;a href=&quot;https://opensource.guide/ko/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://opensource.guide/ko/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/nVpyJ/hyWrZEaMzT/hFueVzSFifIO3h9WCc2Eb0/img.png?width=1000&amp;amp;height=536&amp;amp;face=0_0_1000_536');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;오픈 소스 가이드&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Learn how to launch and grow your project.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;opensource.guide&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;GitHub 프로젝트를 공개하는 것은 프로젝트 라이센싱과 동일하지 않습니다.&lt;br /&gt;&amp;nbsp;공개 프로젝트는 GitHub의 서비스 약관에 명시되어 있으며, 다른 사람들이 프로젝트를 보거나 포크할 수는 있지만, 다른 권한은 없습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈소스 정보 복사로 가져오기만 해도 ㄱㅊㄱㅊ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPL 분유의 라이선스는 출처 외에도 챙겨야할 사항이 많다 &amp;gt; MIT 추천하는 문화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;GitHub에서 새로운 프로젝트를 만들 때, 라이선스를 추가할 것인지 물어봅니다.&lt;/blockquote&gt;
&lt;figure id=&quot;og_1719912266151&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;리포지토리 라이선싱 - GitHub Docs&quot; data-og-description=&quot;GitHub의 퍼블릭 리포지토리는 오픈 소스 소프트웨어를 공유하는 데 자주 사용됩니다. 리포지토리가 진정으로 오픈 소스가 되려면 다른 사용자가 소프트웨어를 자유롭게 사용, 변경 및 배포할 수&quot; data-og-host=&quot;docs.github.com&quot; data-og-source-url=&quot;https://docs.github.com/ko/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/licensing-a-repository&quot; data-og-url=&quot;https://docs.github.com/ko/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/licensing-a-repository&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/X6Ys2/hyWvInepYy/TZniklY7XKegolDqYHvRo0/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200,https://scrap.kakaocdn.net/dn/kczlr/hyWvUnFFgb/dsTW22mAaGzH9pkdZroKy0/img.png?width=958&amp;amp;height=206&amp;amp;face=0_0_958_206&quot;&gt;&lt;a href=&quot;https://docs.github.com/ko/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/licensing-a-repository&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.github.com/ko/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/licensing-a-repository&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/X6Ys2/hyWvInepYy/TZniklY7XKegolDqYHvRo0/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200,https://scrap.kakaocdn.net/dn/kczlr/hyWvUnFFgb/dsTW22mAaGzH9pkdZroKy0/img.png?width=958&amp;amp;height=206&amp;amp;face=0_0_958_206');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;리포지토리 라이선싱 - GitHub Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;GitHub의 퍼블릭 리포지토리는 오픈 소스 소프트웨어를 공유하는 데 자주 사용됩니다. 리포지토리가 진정으로 오픈 소스가 되려면 다른 사용자가 소프트웨어를 자유롭게 사용, 변경 및 배포할 수&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;887&quot; data-origin-height=&quot;1230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csnDFl/btsIjeJmtoa/pIwmbQnUYFk5Wo7jiu9BD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csnDFl/btsIjeJmtoa/pIwmbQnUYFk5Wo7jiu9BD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csnDFl/btsIjeJmtoa/pIwmbQnUYFk5Wo7jiu9BD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsnDFl%2FbtsIjeJmtoa%2FpIwmbQnUYFk5Wo7jiu9BD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;887&quot; height=&quot;1230&quot; data-origin-width=&quot;887&quot; data-origin-height=&quot;1230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레포 생성 후 추후에 라이선스 추가할 수도 있다!&lt;/p&gt;
&lt;figure id=&quot;og_1719912462226&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;리포지토리에 라이선스 추가 - GitHub Docs&quot; data-og-description=&quot;다른 사용자가 더 쉽게 기여할 수 있도록 리포지토리에 오픈 소스 라이선스를 포함할 수 있습니다.&quot; data-og-host=&quot;docs.github.com&quot; data-og-source-url=&quot;https://docs.github.com/ko/communities/setting-up-your-project-for-healthy-contributions/adding-a-license-to-a-repository&quot; data-og-url=&quot;https://docs.github.com/ko/communities/setting-up-your-project-for-healthy-contributions/adding-a-license-to-a-repository&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bxXoH3/hyWvLEhv5j/021YXH4cMDXwUCBgtvvlzk/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200,https://scrap.kakaocdn.net/dn/bRD2Jh/hyWvOueIaF/yLKxnBBltodfBbvEsHtpRk/img.png?width=2146&amp;amp;height=1096&amp;amp;face=0_0_2146_1096,https://scrap.kakaocdn.net/dn/h4xZK/hyWvMwp2xb/Sf1REmh0iJqeJ4JSvZKza0/img.png?width=1286&amp;amp;height=468&amp;amp;face=0_0_1286_468&quot;&gt;&lt;a href=&quot;https://docs.github.com/ko/communities/setting-up-your-project-for-healthy-contributions/adding-a-license-to-a-repository&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.github.com/ko/communities/setting-up-your-project-for-healthy-contributions/adding-a-license-to-a-repository&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bxXoH3/hyWvLEhv5j/021YXH4cMDXwUCBgtvvlzk/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200,https://scrap.kakaocdn.net/dn/bRD2Jh/hyWvOueIaF/yLKxnBBltodfBbvEsHtpRk/img.png?width=2146&amp;amp;height=1096&amp;amp;face=0_0_2146_1096,https://scrap.kakaocdn.net/dn/h4xZK/hyWvMwp2xb/Sf1REmh0iJqeJ4JSvZKza0/img.png?width=1286&amp;amp;height=468&amp;amp;face=0_0_1286_468');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;리포지토리에 라이선스 추가 - GitHub Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;다른 사용자가 더 쉽게 기여할 수 있도록 리포지토리에 오픈 소스 라이선스를 포함할 수 있습니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;license.md (txt) 문서 구조&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LICENSE.md&lt;/li&gt;
&lt;li&gt;LICENSE.txt : 마크대운이 아닌, 텍스트 형식의 파일. md와 같은 내용. 오픈소스 라이선스 전문 명시 문서
&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;README.md : 오픈소스의 사용방법 및 목적 설명하는 문서&lt;/li&gt;
&lt;li&gt;COPYRIGHT.txt : 오픈소스의 저작권자에 대한 설명&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;NOTICE.txt : 오픈소스 라이선스 개요 설명&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;CONTRIBUTING.md : 컨드리뷰터가 되어보자!&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;code of conduct : 오픈소스 프로젝트(커뮤니티)에 참여하는 방법에 대한 표준 = 모든 기여 존중, 서로 존중, 호의적 포용적 환경 + 멤버간 이슈 발생에 대한 문제 해결 방안에 대한 내용도 포함되어있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CONTRIBUTING.md문서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트에 기여하는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트(오픈소스)의 수정에 어떻게 기여할 수 있는지 설명한 문서이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오타 및 수정, 변경, 발전 등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 프로젝트에 기여하는 절차를 안내한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1719913031754&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;hubot/CONTRIBUTING.md at main &amp;middot; hubotio/hubot&quot; data-og-description=&quot;A customizable life embetterment robot. Contribute to hubotio/hubot development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/hubotio/hubot/blob/main/CONTRIBUTING.md&quot; data-og-url=&quot;https://github.com/hubotio/hubot/blob/main/CONTRIBUTING.md&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/caQasY/hyWvMwp7Bc/mzs3VI6Au5OPAjTbXg6l11/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/hubotio/hubot/blob/main/CONTRIBUTING.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/hubotio/hubot/blob/main/CONTRIBUTING.md&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/caQasY/hyWvMwp7Bc/mzs3VI6Au5OPAjTbXg6l11/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;hubot/CONTRIBUTING.md at main &amp;middot; hubotio/hubot&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A customizable life embetterment robot. Contribute to hubotio/hubot development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;api나 기능을 바꾸고 싶다면 &amp;gt; __ 프로세를 확인해 보아야 한다.&lt;/li&gt;
&lt;li&gt;이미 존재하는 이슈인지 아닌지 파악하고&lt;/li&gt;
&lt;li&gt;fork하고 clone해서&lt;/li&gt;
&lt;li&gt;작업을 하고~~ 어쩌구~~&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;We love pull requests. Here's a quick guide:&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;If you're adding a new feature or changing user-facing APIs, check out the&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/hubotio/evolution&quot;&gt;Hubot Evolution&lt;/a&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;process.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;Check for&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/github/hubot/issues&quot;&gt;existing issues&lt;/a&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;for duplicates and confirm that it hasn't been fixed already in the&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/github/hubot/commits/main&quot;&gt;main branch&lt;/a&gt;&lt;br /&gt;Fork the repo, and clone it locally&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;npm link&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;to make your cloned repo available to npm&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;Follow&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/hubotio/hubot/blob/main/docs/index.md&quot;&gt;Getting Started&lt;/a&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;to generate a testbot&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;npm link hubot&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;in your newly created bot to use your hubot fork&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;Create a new branch for your contribution&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;Add&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/hubotio/hubot/blob/main/test&quot;&gt;tests&lt;/a&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;(run with&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;npm test)&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; letter-spacing: 0px;&quot;&gt;Push to your fork and submit a pull request&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브랜치, 풀리궤, 릴리즈 언제 되는지 문서도 작성되어있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;code of conduct 문서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 사용조항. 시행 강령&lt;/p&gt;
&lt;figure id=&quot;og_1719913214283&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;hubot/CODE_OF_CONDUCT.md at main &amp;middot; hubotio/hubot&quot; data-og-description=&quot;A customizable life embetterment robot. Contribute to hubotio/hubot development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/hubotio/hubot/blob/main/CODE_OF_CONDUCT.md&quot; data-og-url=&quot;https://github.com/hubotio/hubot/blob/main/CODE_OF_CONDUCT.md&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b7wbak/hyWrSkJTKg/vjqae0u8JyVTvJaSNkrEKK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/hubotio/hubot/blob/main/CODE_OF_CONDUCT.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/hubotio/hubot/blob/main/CODE_OF_CONDUCT.md&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b7wbak/hyWrSkJTKg/vjqae0u8JyVTvJaSNkrEKK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;hubot/CODE_OF_CONDUCT.md at main &amp;middot; hubotio/hubot&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A customizable life embetterment robot. Contribute to hubotio/hubot development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;contributor 이 코드를 수정해서 기여하려는 사람에서 &amp;gt; 지켜야할 협업예의? 지침과 비슷한 내용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 인종,,, 국가,,, 성별,,, 등 인간적인 이야기를 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 독재자처럼 사용하지 말아라~&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커뮤니티에 참여하기 위한 윤리강령에 호의적인 개발집단이다~ 라고 설명하는 문서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;(오픈 소스 프로젝트=)커뮤니티 상태파일&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LICENSE.md, LICENSE.txt, README.md, COPYRIGHT.txt,&amp;nbsp;NOTICE.txt,&amp;nbsp;CONTRIBUTING.md,&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;code of conduct&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;깃허브는 커뮤니티에 대한 행동 기준! 공정한 방식으로 즐겁게 생산하는 환경!&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;커뮤니티를 빌딩한다&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로필 권장 체크리스트 Insights&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 깃허브에 기여한 사람에 대한 정보, 기간 및 어떤 부분&lt;/p&gt;
&lt;figure id=&quot;og_1719917228082&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - pronist/hello:   당신의 멋진 블로그에 걸맞는 최고의 티스토리 스킨&quot; data-og-description=&quot;  당신의 멋진 블로그에 걸맞는 최고의 티스토리 스킨. Contribute to pronist/hello development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/pronist/hello/community&quot; data-og-url=&quot;https://github.com/pronist/hello&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/baZZoJ/hyWvQMmNSo/otX4shXseKmO9pKqBeHDbK/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640&quot;&gt;&lt;a href=&quot;https://github.com/pronist/hello/community&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/pronist/hello/community&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/baZZoJ/hyWvQMmNSo/otX4shXseKmO9pKqBeHDbK/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - pronist/hello:   당신의 멋진 블로그에 걸맞는 최고의 티스토리 스킨&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  당신의 멋진 블로그에 걸맞는 최고의 티스토리 스킨. Contribute to pronist/hello development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영이 잘 되고 있는지 체크할 수 있다.&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;깃허브 이슈 Issue 탭&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Open, Close&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서 발생할 수 있는 이슈 : 문제, 버그, 개선, 기능추가, 작업 task, 기획 등등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 모든 활동을 뜻함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이슈를 등록해서 사용하는지? todo list 개념, 해야할 task 가 `Open` 되어 있고 or 계획으로도 사용할 수 있다. + 질문, 댓글을 통한 협업&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완료된 이슈를 `Close` 표시, 계획취소 및 중단&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;Issue 로 할일 을 등록하고, 커밋할때 이슈 끌고와서 참조해서 작성할 수 있음.&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;* Jira : 칸반보드 시스템&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈 등록의 템플릿 깃허브에서 메뉴로 제공한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 별도의 문서나 소통창구가 존재할 것이다. 오픈소스의 이슈 많이 활용 vs 실무 이슈 많이 활용하지 않는다.&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;Pull Request&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pull 당기다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main &amp;gt; 추가 branch를 생성해서 개발한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병합, 충돌 시에 분기해서 개발한 기능을 &amp;gt; 기존 브랜치에 병합해도 되는지 물어볼때 &amp;gt; Pull Request 풀리퀘 요청한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Branch가 Branch에게 요청하는 방식 &amp;gt; 컨펌 &lt;b&gt;코드리뷰&lt;/b&gt; 후에 merge 진행함.&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;pr 템플릿을 깃허브에서 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Discussions 토론 게시판&lt;/h3&gt;
&lt;figure id=&quot;og_1719917195817&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;pronist hello &amp;middot; Discussions&quot; data-og-description=&quot;Explore the GitHub Discussions forum for pronist hello. Discuss code, ask questions &amp;amp; collaborate with the developer community.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/pronist/hello/discussions&quot; data-og-url=&quot;https://github.com/pronist/hello/discussions&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cYtGIH/hyWvIU540P/wvwHpFz2nSpekOR5RH0f1k/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640&quot;&gt;&lt;a href=&quot;https://github.com/pronist/hello/discussions&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/pronist/hello/discussions&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cYtGIH/hyWvIU540P/wvwHpFz2nSpekOR5RH0f1k/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;pronist hello &amp;middot; Discussions&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Explore the GitHub Discussions forum for pronist hello. Discuss code, ask questions &amp;amp; collaborate with the developer community.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의견 논의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토픽 + 댓글로 커뮤니티처럼 개선사항, 질문 등을 진행한다. (General ,ideas, QnA 등 카테고리 세분화 분리)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;code of conduct 대신 깃허브 community guideline 도 사용할 수 있다.&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;☑️ 배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 사용하던 깃허브를 활용하는 방법 배울 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;license, cicd, issues, pr, commit 사용하고 있었는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;템플릿 및 규칙을 설정하는 방식을 알 수 있었다. 깊게 알 수 있어서 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깃허브 멤버의 역할도 정할 수 있음.&lt;/p&gt;</description>
      <category>웹 풀 사이클 데브코스</category>
      <category>til</category>
      <category>데브코스</category>
      <author>닿다라다나닷</author>
      <guid isPermaLink="true">https://datdaradanadat.tistory.com/169</guid>
      <comments>https://datdaradanadat.tistory.com/169#entry169comment</comments>
      <pubDate>Mon, 1 Jul 2024 23:52:17 +0900</pubDate>
    </item>
  </channel>
</rss>