<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>zyekim_front</title>
        <link>https://velog.io/</link>
        <description>주니어 프론트엔드개발자</description>
        <lastBuildDate>Mon, 11 May 2026 13:48:16 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>zyekim_front</title>
            <url>https://images.velog.io/images/k_jihye92/profile/1dcbe239-cc09-4086-a3fd-e088f088b599/KakaoTalk_Image_2022-01-14-16-17-54.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. zyekim_front. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/k_jihye92" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[🔨 Vue `defineProps`에서 넘긴 prop이 `props`가 아니라 `$attrs`로 빠진 문제]]></title>
            <link>https://velog.io/@k_jihye92/Vue-defineProps%EC%97%90%EC%84%9C-%EB%84%98%EA%B8%B4-prop%EC%9D%B4-props%EA%B0%80-%EC%95%84%EB%8B%88%EB%9D%BC-attrs%EB%A1%9C-%EB%B9%A0%EC%A7%84-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@k_jihye92/Vue-defineProps%EC%97%90%EC%84%9C-%EB%84%98%EA%B8%B4-prop%EC%9D%B4-props%EA%B0%80-%EC%95%84%EB%8B%88%EB%9D%BC-attrs%EB%A1%9C-%EB%B9%A0%EC%A7%84-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Mon, 11 May 2026 13:48:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>파일 업로드 컴포넌트에 <code>target</code> 값을 넘겨서 업로드 API 요청 시 <code>targetType</code>으로 사용하려고 했다.
부모 컴포넌트에서는 아래처럼 <code>file-input</code> 컴포넌트에 <code>target</code> prop을 추가로 넘기고 있었다.</p>
</blockquote>
<pre><code class="language-vue">&lt;file-input
  v-model=&quot;salesOrderDetail.fileList&quot;
  :mode=&quot;&#39;edit&#39;&quot;
  :target=&quot;&#39;ORDER&#39;&quot;
/&gt;</code></pre>
<p>공통 컴포넌트인 <code>file-input.vue</code>에서는 props를 이렇게 선언했다.</p>
<pre><code class="language-ts">const props = withDefaults(defineProps&lt;IFileUploadProps&gt;(), {
  module: &quot;&quot;,
  fileCategory: &quot;&quot;,
  target: &quot;&quot;,
  mode: &quot;edit&quot;,
  modelValue: () =&gt; [],
  clearable: false,
  multiple: true,
  accept: &quot;.ppt, .pdf, .hwp, .docx, .xls, .xlsx, .png, .jpg, .jpeg&quot;,
  maxSize: 10,
  error: false,
  errorMessage: &quot;&quot;,
  hintMessage: &quot;&quot;,
  placeholder: &quot;파일첨부&quot;
});</code></pre>
<p>그리고 업로드 요청을 보낼 때 이렇게 사용하고 있었다.</p>
<pre><code class="language-ts">await apiClient.file&lt;IFileResponse&gt;(&quot;/files/upload&quot;, {
  moduleCode: props.module,
  fileType: &quot;FILE&quot;,
  targetType: props.target ?? null,
  file
});</code></pre>
<p>그런데 이상하게도 <code>props.target</code> 값이 보이지 않았다.</p>
<p>❓ 처음에는 부모에서 값을 잘못 넘긴 줄 알았다.
그래서 테스트용으로 <code>fileCategory</code>도 추가해서 넘겨봤다. ❓</p>
<pre><code class="language-vue">&lt;file-input
  v-model=&quot;salesOrderDetail.fileList&quot;
  :mode=&quot;&#39;view&#39;&quot;
  :target=&quot;&#39;ORDER&#39;&quot;
  :fileCategory=&quot;&#39;abc&#39;&quot;
/&gt;</code></pre>
<p>하지만 여전히 원하는 값이 <code>props</code>에서 확인되지 않았다.</p>
<hr>
<h2 id="원인-찾기">원인 찾기</h2>
<p>콘솔로 아무리 찍어봐도 보이지 않길래 일단 상위에 v-bind=&quot;$attrs&quot;선언해 놓아서 혹시... 몰라<code>props</code>와 <code>$attrs</code>를 각각 확인했다.</p>
<p>(언제나 항상 혹시가 문제지..?)</p>
<pre><code class="language-ts">const attrs = useAttrs();

console.log(&quot;props.target:&quot;, props.target);
console.log(&quot;attrs.target:&quot;, attrs.target);
console.log(&quot;attrs:&quot;, attrs);</code></pre>
<p>결과는 다음과 같았다.</p>
<pre><code class="language-ts">props.target: undefined
attrs.target: ORDER
attrs: Proxy(Object) { target: &#39;ORDER&#39; }</code></pre>
<p>여기서 중요한 점은 부모에서 값이 안 넘어온 게 아니라는 것이다.</p>
<p>부모가 넘긴 <code>target=&quot;ORDER&quot;</code>는 실제로 자식까지 도착했다.
다만 Vue가 이 값을 <code>props</code>로 인식하지 못해서 <code>$attrs</code>로 넘겨버린 것이다.</p>
<p>즉 문제는 공통 컴포넌트의 <code>defineProps</code> 처리 쪽에 있었다.</p>
<hr>
<h2 id="타입-선언-확인">타입 선언 확인</h2>
<p>처음에는 <code>IFileUploadProps</code> 타입에 <code>target</code>이 없는 줄 알았다.</p>
<p>하지만 타입 파일을 확인해보니 <code>target</code>은 분명히 존재했다.</p>
<pre><code class="language-ts">interface IBaseUploadProps&lt;T extends IFileResponse = IFileResponse&gt; {
  modelValue: T[];
  mode?: &quot;edit&quot; | &quot;view&quot;;
  required?: boolean;
  hint?: string;
  hintMessage?: string;
  readonly?: boolean;
  disabled?: boolean;
  persistentHint?: boolean;
  clearable?: boolean;
  multiple?: boolean;
  maxSize?: number;
  module: string;
  fileCategory?: string;
  target?: string;
}

export interface IFileUploadProps extends IBaseUploadProps&lt;ISaveFile&gt; {
  placeholder?: string;
  accept?: string;
  target?: string;
}</code></pre>
<p>TypeScript 기준으로는 문제가 없어 보였다.</p>
<p><code>IFileUploadProps</code>는 <code>IBaseUploadProps&lt;ISaveFile&gt;</code>를 상속하고 있고,
<code>target?: string</code>도 존재한다.</p>
<p>그런데 런타임에서는 <code>props.target</code>이 아니라 <code>$attrs.target</code>으로 빠지고 있었다.</p>
<hr>
<h2 id="진짜-원인">진짜 원인</h2>
<p>원인은 <code>defineProps&lt;IFileUploadProps&gt;()</code>에 넘긴 타입 구조가 Vue SFC 컴파일러가 런타임 props로 안정적으로 변환하기에는 복잡했던 것이다.
(뭔가 doc에 있어보여서 이용하면 왜 이런게 터지는거죠..?)</p>
<p>문제가 된 구조는 다음 조합이었다.</p>
<pre><code class="language-ts">interface IBaseUploadProps&lt;T extends IFileResponse = IFileResponse&gt; {
  ...
  target?: string;
}

export interface IFileUploadProps extends IBaseUploadProps&lt;ISaveFile&gt; {
  ...
  target?: string;
}</code></pre>
<p>즉,</p>
<ul>
<li>imported interface</li>
<li>extends</li>
<li>generic</li>
<li>중복 선언된 prop</li>
</ul>
<p>이 조합이 들어가 있었다.</p>
<p>TypeScript는 이 타입을 정상적으로 이해한다.</p>
<p>하지만 Vue의 <code>defineProps&lt;T&gt;()</code>는 완전한 TypeScript 타입 체커처럼 동작하지 않는다.
Vue SFC 컴파일러는 타입 정보를 기반으로 런타임 prop 옵션을 만들어야 하는데, 이 과정은 제한적인 AST 분석에 가깝다.</p>
<p>그래서 TypeScript 입장에서는 <code>target</code>이 존재하지만, Vue 컴파일러가 런타임 props 목록으로 변환하는 과정에서 <code>target</code>을 누락할 수 있다.</p>
<p>그 결과 Vue는 부모에서 전달받은 <code>target</code>을 이렇게 판단한다.</p>
<pre><code class="language-txt">target? 내가 아는 prop 목록에 없네.
그럼 props가 아니라 attrs로 넘긴다.</code></pre>
<p>그래서 최종 결과가 이렇게 된 것이다.</p>
<pre><code class="language-ts">props.target // undefined
attrs.target // &quot;ORDER&quot;</code></pre>
<hr>
<h2 id="해결법">해결법</h2>
<p>가장 확실한 해결법은 <code>defineProps</code>에 사용하는 타입을 flat하게 만드는 것이다.</p>
<p>즉 <code>extends + generic</code> 구조를 피하고, 해당 컴포넌트에서 사용하는 props를 직접 명시한다.</p>
<pre><code class="language-ts">interface FileInputProps {
  modelValue: ISaveFile[];
  mode?: &quot;edit&quot; | &quot;view&quot;;
  required?: boolean;
  hint?: string;
  hintMessage?: string;
  readonly?: boolean;
  disabled?: boolean;
  persistentHint?: boolean;
  clearable?: boolean;
  multiple?: boolean;
  maxSize?: number;
  module: string;
  fileCategory?: string;
  target?: string;
  placeholder?: string;
  accept?: string;
  error?: boolean;
  errorMessage?: string;
}

const props = withDefaults(defineProps&lt;FileInputProps&gt;(), {
  module: &quot;&quot;,
  fileCategory: &quot;&quot;,
  target: &quot;&quot;,
  mode: &quot;edit&quot;,
  modelValue: () =&gt; [],
  clearable: false,
  multiple: true,
  accept: &quot;.ppt, .pdf, .hwp, .docx, .xls, .xlsx, .png, .jpg, .jpeg&quot;,
  maxSize: 10,
  error: false,
  errorMessage: &quot;&quot;,
  hintMessage: &quot;&quot;,
  placeholder: &quot;파일첨부&quot;
});</code></pre>
<p>이렇게 변경하면 Vue가 <code>target</code>을 정상적으로 prop으로 인식한다.</p>
<p>이후 API 요청에서도 정상적으로 사용할 수 있다.</p>
<pre><code class="language-ts">await apiClient.file&lt;IFileResponse&gt;(&quot;/files/upload&quot;, {
  moduleCode: props.module,
  fileType: &quot;FILE&quot;,
  targetType: props.target || null,
  fileCategory: props.fileCategory || null,
  file
});</code></pre>
<p>여기서 <code>?? null</code>보다 <code>|| null</code>을 사용한 이유는 기본값이 빈 문자열일 수 있기 때문이다.</p>
<pre><code class="language-ts">props.target ?? null</code></pre>
<p>이렇게 쓰면 <code>props.target</code>이 <code>&quot;&quot;</code>일 때 그대로 빈 문자열이 들어간다.</p>
<p>반면,</p>
<pre><code class="language-ts">props.target || null</code></pre>
<p>이렇게 쓰면 빈 문자열일 경우 <code>null</code>로 처리할 수 있다.</p>
<hr>
<h2 id="다른-해결-방법에는이런게-있대요">다른 해결 방법에는...이런게 있대요</h2>
<p>공통 타입을 꼭 재사용하고 싶다면 generic을 제거하는 것도 방법이다.</p>
<pre><code class="language-ts">export interface IBaseUploadProps {
  modelValue: ISaveFile[];
  mode?: &quot;edit&quot; | &quot;view&quot;;
  required?: boolean;
  hint?: string;
  hintMessage?: string;
  readonly?: boolean;
  disabled?: boolean;
  persistentHint?: boolean;
  clearable?: boolean;
  multiple?: boolean;
  maxSize?: number;
  module: string;
  fileCategory?: string;
  target?: string;
}

export interface IFileUploadProps extends IBaseUploadProps {
  placeholder?: string;
  accept?: string;
  error?: boolean;
  errorMessage?: string;
}</code></pre>
<p>다만 가장 안전한 방법은 <code>defineProps</code>에 들어가는 타입만큼은 최대한 단순하고 명확하게 유지하는 것이다.</p>
<hr>
<h2 id="임시방편">임시방편</h2>
<p>당장 구조를 바꾸기 어렵다면 <code>$attrs</code>에서 값을 꺼내 사용할 수도 있다.</p>
<pre><code class="language-ts">const attrs = useAttrs();

const uploadTarget = computed(() =&gt; {
  return props.target || String(attrs.target || &quot;&quot;);
});</code></pre>
<p>그리고 업로드 요청에서는 이렇게 사용한다.</p>
<pre><code class="language-ts">await apiClient.file&lt;IFileResponse&gt;(&quot;/files/upload&quot;, {
  moduleCode: props.module,
  fileType: &quot;FILE&quot;,
  targetType: uploadTarget.value || null,
  file
});</code></pre>
<p>하지만 이 방법은 근본적인 해결책은 아니다.</p>
<p><code>target</code>은 명확히 컴포넌트의 prop으로 사용되는 값이기 때문에 <code>$attrs</code>에서 꺼내 쓰는 것보다 <code>defineProps</code>에서 정상적으로 인식되도록 고치는 것이 맞다.</p>
<hr>
<h2 id="다음에-조심할-것">다음에 조심할 것</h2>
<p><code>defineProps&lt;T&gt;()</code>에 타입을 넣을 때는 TypeScript에서 문제가 없다고 해서 Vue 런타임에서도 항상 정상 동작한다고 믿으면 안 된다.</p>
<p>특히 아래와 같은 타입 구조는 조심해야 한다.</p>
<pre><code class="language-ts">interface BaseProps&lt;T&gt; {
  value: T;
}

interface ChildProps extends BaseProps&lt;SomeType&gt; {
  target?: string;
}</code></pre>
<p>이런 구조는 TypeScript에서는 정상이어도 Vue SFC 컴파일러가 런타임 props로 변환하는 과정에서 일부 prop을 누락할 수 있다.</p>
<p>앞으로는 다음 기준을 지키는 것이 좋다.</p>
<h3 id="1-defineprops에-넣는-타입은-최대한-flat하게-작성하기">1. <code>defineProps</code>에 넣는 타입은 최대한 flat하게 작성하기</h3>
<pre><code class="language-ts">interface ComponentProps {
  modelValue: SomeType[];
  target?: string;
  mode?: &quot;edit&quot; | &quot;view&quot;;
}</code></pre>
<h3 id="2-props가-안-잡히면-attrs를-바로-확인하기">2. props가 안 잡히면 <code>$attrs</code>를 바로 확인하기</h3>
<pre><code class="language-ts">const attrs = useAttrs();

console.log(&quot;props.target:&quot;, props.target);
console.log(&quot;attrs.target:&quot;, attrs.target);</code></pre>
<p>만약 <code>props</code>에는 없고 <code>$attrs</code>에 있다면 부모 전달 문제라기보다 자식의 prop 선언 문제일 가능성이 높다.</p>
<h3 id="3-consolelogprops만-믿지-않기">3. <code>console.log(props)</code>만 믿지 않기</h3>
<p>Vue의 <code>props</code>는 reactive proxy이기 때문에 콘솔에서 펼쳐 보는 것만으로는 헷갈릴 수 있다.</p>
<p>가능하면 개별 값을 직접 찍는 것이 좋다.</p>
<pre><code class="language-ts">console.log(&quot;target:&quot;, props.target);
console.log(&quot;module:&quot;, props.module);
console.log(&quot;mode:&quot;, props.mode);</code></pre>
<h3 id="4-공통-타입-재사용보다-컴포넌트-안정성을-우선하기">4. 공통 타입 재사용보다 컴포넌트 안정성을 우선하기</h3>
<p>공통 타입을 재사용하면 코드가 깔끔해 보이지만, <code>defineProps</code>에서는 오히려 런타임 변환 문제가 생길 수 있다.</p>
<p>특히 공통 타입이 generic, extends, imported type을 포함하고 있다면 props 선언에는 flat type을 사용하는 것이 더 안전하다.</p>
<hr>
<h2 id="정리">정리</h2>
<p>이번 문제는 부모 컴포넌트에서 prop을 잘못 넘긴 문제가 아니었다.</p>
<p>부모에서 넘긴 값은 자식까지 정상적으로 도착했다.
다만 Vue가 해당 값을 prop으로 인식하지 못해서 <code>$attrs</code>로 넘겨버린 것이 문제였다.</p>
<p>핵심 원인은 <code>defineProps&lt;IFileUploadProps&gt;()</code>에 사용한 타입이 <code>extends + generic</code> 구조였고, Vue SFC 컴파일러가 이를 런타임 props로 완전히 변환하지 못한 것이었다.</p>
<p>해결은 <code>defineProps</code>에 사용하는 타입을 flat하게 바꾸는 것.</p>
<pre><code class="language-ts">interface FileInputProps {
  modelValue: ISaveFile[];
  module: string;
  target?: string;
  fileCategory?: string;
  mode?: &quot;edit&quot; | &quot;view&quot;;
}</code></pre>
<p>이렇게 단순하게 선언하면 <code>props.target</code>으로 정상 접근할 수 있다.</p>
<p>이번 일을 통해 알게 된 점은 하나다.</p>
<p>TypeScript가 이해하는 타입과 Vue가 런타임 props로 변환할 수 있는 타입은 다를 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[✏️MFA 와 FSD 와 MONO-REPO]]></title>
            <link>https://velog.io/@k_jihye92/MFA-%EC%99%80-FSD-%EC%99%80-MONO-REPO</link>
            <guid>https://velog.io/@k_jihye92/MFA-%EC%99%80-FSD-%EC%99%80-MONO-REPO</guid>
            <pubDate>Mon, 09 Feb 2026 09:22:38 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<blockquote>
<p>서비스 리뉴얼을 요청사항 중, <strong>두 개의 독립적인 B2B 서비스</strong>를 <strong>하나의 프로젝트로 통합</strong>하면서도, 필요에 따라 <strong>특정 모듈을 독립적인 프로젝트로 즉시 분리</strong>할 수 있는 고도의 유연성이 요구되었다. 😅 (2년차인 나에게 또 하나의 도전을..선사..함)
그러면서 알게된 프론트엔드 프로젝트 아키텍처를 간단히 정의하고, 요구사항에 맞는 아키텍처 선택의 이유에 대해 설명하려고 한다.</p>
</blockquote>
<h3 id="1-아키텍처-및-구조-설계-관련-용어-정리">1. 아키텍처 및 구조 설계 관련 용어 정리</h3>
<h4 id="①-모놀리식-monolithic">① 모놀리식 (Monolithic)</h4>
<p>📣 모든 기능이 하나의 코드 베이스와 하나의 빌드 단위로 묶인 구조</p>
<p>😊: 개발 초기 속도가 빠르고 배포 파이프라인이 단순</p>
<p>😡: 서비스 규모가 커지면 코드 간 의존성이 꼬여 수정이 어렵고, 전체를 다시 빌드해야 하므로 배포 효율이 떨어짐</p>
<h4 id="②-모노레포-monorepo">② 모노레포 (Monorepo)</h4>
<p>📣 두 개 이상의 독립적인 프로젝트를 하나의 레포지토리에서 관리하는 방식. 도메인별로 독립적인 패키지 분리가능</p>
<p>😊: 프로젝트 간 코드 공유(Shared 패키지)가 쉬워 의존성 관리 용이, 일관성유지하기 쉬움</p>
<p>😡: 레포지토리 크기가 커지며, 빌드 도구(pnpm, Turborepo 등)에 대한 숙련도가 필요</p>
<p><img src="https://velog.velcdn.com/images/k_jihye92/post/f7025bf0-9185-4211-b6bd-4e9677dcf114/image.png" alt="">이미지 출처: <a href="https://levelup.gitconnected.com/moving-from-multiple-repositories-to-a-lerna-js-mono-repo-faa97aeee35b">Moving from multiple repositories to a lerna-js mono-repo</a></p>
<h4 id="③-mfa-micro-frontends-architecture">③ MFA (Micro Frontends Architecture)</h4>
<p>📣 MSA(Microservices Architecture)의 프론트 버전으로 독립적으로 배포 가능한 작은 단위로 쪼개는 설계 방식</p>
<p>😊: 서비스별 독립 배포, 기술 스택의 자율성, 특정 모듈의 즉각적인 독립 프로젝트화 가능.</p>
<p>😡: 초기 설정이 복잡하며, 런타임 시 의존성 충돌을 관리 필요. 많은 프론트엔드 개발자 필요.</p>
<h4 id="④-fsd-feature-sliced-design">④ FSD (Feature-Sliced Design)</h4>
<p>📣 기능 중심으로 폴더를 계층화하는 프론트엔드 전용 설계 패턴</p>
<p>😊: 구조 명확하고 코드의 응집도가 높고, 특정 기능을 떼어내기 매우 유리, 유지보수성이 향상</p>
<p>😡: 폴더 구조가 깊어지고 모든 작업자가 아키텍처에 대해 학습이 필요</p>
<p><img src="https://velog.velcdn.com/images/k_jihye92/post/beadf365-8082-4364-9f0f-12af1b4d64b9/image.png" alt="">이미지출처: <a href="https://feature-sliced.design/docs/get-started/overview">FSD공식문서</a></p>
<h3 id="2-요구사항에-맞춘-최적의-아키텍처-mfa-기반-모노레포--fsd">2. 요구사항에 맞춘 최적의 아키텍처: &quot;MFA 기반 모노레포 + FSD&quot;</h3>
<blockquote>
<p>요구 사항을 다시 정리 하자면, 🤦‍♂️<strong>&quot;두개의 서비스를 합치나, 언제든 특정 모듈을 뽑아서 별도 프로젝트로 만들 수 있어야 한다&quot;</strong> 
때문에 리서치 한 결과로 생각해보면, <code>모노 레포 + MFA</code>로 한 레포지토리에 두개 도메인을 관리하고 
각 도메인 폴더는 언제든 특정 모듈을 뽑아서 프로젝트를 생성할 수 있게 <code>feature</code>, <code>entities</code> 단위로 구조를 짜려고 한다</p>
</blockquote>
<ul>
<li>거시적(Macro) 관점 - 모노레포 &amp; MFA: 물리적인 프로젝트 분리. SCM과 특송을 별도의 앱으로 격리하여 서로 영향을 주지 않게 함.</li>
<li>미시적(Micro) 관점 - FSD: 프로젝트 내부의 논리적 분리. 특정 기능(모듈)이 자기 완결성을 갖도록 설계하여 복사-붙여넣기만으로 기능 이전이 가능하게 함.</li>
</ul>
<h3 id="3-추천-아키텍처-폴더-구조-예시">3. 추천 아키텍처 폴더 구조 예시</h3>
<h4 id="①-거시적---monorepo-pnpm-workspaces">① 거시적 - Monorepo (pnpm Workspaces)</h4>
<pre><code class="language-Plaintext">
root/
├── apps/
│   ├── shell/                # 메인 포털 (SCM + 특송을 담는 그릇)
│   ├── scm-service/          # 공급망 관리 App
│   └── express-service/      # 국제 특송 관리 App
├── packages/
│   ├── shared-ui/            # 공통 버튼, 입력폼 등 Design System
│   ├── shared-api/           # Axios 인스턴스, 공통 API 호출 로직
│   └── shared-utils/         # 날짜 포맷팅, 권한 체크 등 공통 함수
├── package.json
└── pnpm-workspace.yaml</code></pre>
<h4 id="②-미시적---개별-app-내부-fsd-구조">② 미시적 - 개별 App 내부 (FSD 구조)</h4>
<p>각 모듈이 독립성을 가짐.</p>
<pre><code class="language-Plaintext">src/
├── app/                      # 앱의 진입점 (Provider, Router 설정)
├── pages/                    # 라우트 페이지 (예: 운송장 목록 페이지)
├── widgets/                  # 복잡한 컴포넌트 조합 (예: 상단 네비바 + 검색창)
├── features/                 # 실제 비즈니스 가치 (예: 운송장-출력, 배송-추적)
│   └── ship-tracking/        # 이 폴더만 떼어내면 다른 프로젝트에서도 사용 가능
│       ├── ui/               # 해당 기능 전용 컴포넌트
│       ├── model/            # Pinia 스토어 (특송 서비스 전용 상태)
│       └── api/              # 해당 기능 전용 API 호출
├── entities/                 # 비즈니스 엔티티 (예: 화물, 고객, 기사)
└── shared/                   # 프로젝트 내부 공통 모듈</code></pre>
<h3 id="결론-왜-이-구조인가">결론: 왜 이 구조인가?</h3>
<p>1️⃣재사용성: features/ 하위의 특정 폴더와 packages/shared만 복사하면, 당장 새로운 B2B 서비스를 런칭가능
2️⃣유연성: <code>vite Module Federation</code> 을 통해 필요한 시점에만 모듈을 로드하므로 초기 로딩 속도 최적화에 유리</p>
<hr>
<br/>    

<h2 id="center-but-작업자는-나-혼자임center"><center> BUT 작업자는 나 혼자임</center></h2>
<p><img src="https://velog.velcdn.com/images/k_jihye92/post/5fdcd54d-eb4a-49ae-b5aa-5159e4641767/image.png" width="600" height="350" /><img src="" alt=""></p>
<p>😭MFA를 사용해 독립배포 가능하게 하자니 초기 세팅 및 유지 보수하는데 업무 부담이 클 것 같고,
😭FSD를 구조대로 넣자니 관리 포인트가 너무 많아져 역시 혼자 업무하는데 개발 속도가 나지 않을 것같다...
🔔그래서 b2b 프로젝트에서 작업해봤을 때, 고민이 되었던 부분을 녹여 위에서 조사한 각 장점..?의 일부를 조금씩 더해서 아래와 같이 구조를 설계해 보았다.</p>
<pre><code class="language-plaintext">src/
├── apps/
│   ├── scm-service/           # 공급망 관리 앱
│   │   ├── views/         # 도메인별 페이지
│   │   │   ├── order/     # 주문 도메인
│   │   │   │   ├── components/
│   │   │   │   ├── store.ts
│   │   │   │   └── OrderList.vue
│   │   │   └── product/   # 상품 도메인
│   │   └── main.ts
│   └── express-service/       # 특송 관리 앱 (구조 동일)
│
├── packages/                  # 재사용 가능한 &quot;부품&quot;||&quot;재료&quot;들
│   ├── shared/                # [Pure Shared] 비즈니스 로직 없음
│   │   ├── ui/                # 공통 헤더,푸터, 테이블, 툴팁, 버튼
│   │   ├── api/               # Axios 인스턴스, Interceptor
│   │   └── utils/             # 포맷팅, Swal Wrapper
│   └── auth/                  # [Auth Domain] 로그인, 권한체크, 토큰관리
│       ├── components/
│       └── store/
│
├── layout/                    # 공통 레이아웃
│   ├── DefaultLayout.vue
│   └── AuthLayout.vue</code></pre>
<h3 id="다시-결론--왜-이-구조인가">다시 결론 : 왜 이 구조인가?</h3>
<p>1️⃣  서비스 경계가 명확해서 나중에 서비스가 확장되어 개발자가 늘어나 팀이 커진다면! MFA도입해 각 독립적 배포 가능하게 확장 할수 있지 않을까.
2️⃣ FSD 구조에 맞추면 혼자 개발 생산성에 좋지 않아서 도메인 단위로 views 분리해서 나중에 필요한 도메인만 뽑아 새로운 프로젝트를 생성하기 쉽지 않을까.</p>
<br/>

<hr>
<p><em>이렇게..정리를 해보았고.. 이제 시작이닿</em>
<em>그래도 폴더가 많아지네요 ㅎ..........ㅎ</em>
<img src="https://velog.velcdn.com/images/k_jihye92/post/b2dfa01e-4526-4f05-98a4-69c3468b5509/image.png" width="120" height="120" align='left'/></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[✏️Reflow와 Repaint의 차이점과 성능 최적화 방법]]></title>
            <link>https://velog.io/@k_jihye92/Reflow%EC%99%80-Repaint%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90%EA%B3%BC-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@k_jihye92/Reflow%EC%99%80-Repaint%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90%EA%B3%BC-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Fri, 09 Jan 2026 09:57:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>웹 페이지는 HTML을 단순히 화면에 출력하는 것이 아니라, 브라우저의 <strong>렌더링 파이프라인(Rendering Pipeline)</strong> 을 거쳐 화면에 그려집니다.<br>이 과정에서 성능에 큰 영향을 미치는 개념이 바로 <strong>Reflow</strong>와 <strong>Repaint</strong>입니다.</p>
</blockquote>
<p>이 두 개념을 정확히 이해하면 프론트엔드 성능 최적화의 방향이 명확해집니다.</p>
<p><img src="https://velog.velcdn.com/images/k_jihye92/post/b28f504b-6b40-4ad9-ba3b-f1f6cbea3cbf/image.png" alt=""></p>
<h2 id="브라우저-렌더링-과정-간단-정리">브라우저 렌더링 과정 간단 정리</h2>
<p>브라우저는 다음과 같은 과정을 통해 화면을 렌더링합니다.</p>
<ol>
<li>DOM 생성 (HTML 파싱)</li>
<li>CSSOM 생성 (CSS 파싱)</li>
<li>Render Tree 생성</li>
<li><strong>Layout (Reflow)</strong> – 요소의 위치와 크기 계산</li>
<li><strong>Paint (Repaint)</strong> – 색상, 배경 등 그리기</li>
<li>Composite – 레이어 합성 (GPU 처리)</li>
</ol>
<p>Reflow와 Repaint는 이 중 <strong>Layout과 Paint 단계</strong>에 해당하며, 빈번하게 발생할 경우 성능 저하의 원인이 됩니다.</p>
<hr>
<h2 id="1-reflow란">1. Reflow란?</h2>
<p><strong>Reflow(Layout)</strong> 는 브라우저가 <strong>요소의 위치와 크기를 다시 계산하는 과정</strong>을 의미합니다.</p>
<p>DOM 구조가 변경되거나, 레이아웃에 영향을 주는 CSS 속성이 변경되면 발생합니다.</p>
<h3 id="reflow가-발생하는-대표적인-경우">Reflow가 발생하는 대표적인 경우</h3>
<ul>
<li>DOM 요소 추가 / 삭제</li>
<li><strong>크기관련</strong> : <code>width</code>, <code>height</code>, <code>margin</code>, <code>padding</code> 변경</li>
<li><strong>레이아웃관련</strong>:<code>display</code>, <code>flex</code> 등</li>
<li><strong>폰트크기</strong> : <code>font-size</code> , <code>font-weight</code> 등</li>
<li><strong>위치관련</strong> : <code>position</code>,<code>top</code>,<code>left</code> 등</li>
<li>브라우저 창 크기 변경</li>
<li>레이아웃 정보를 읽는 코드 실행</li>
</ul>
<pre><code class="language-js">const el = document.querySelector(&#39;.box&#39;);
el.style.width = &#39;200px&#39;; // Reflow 발생</code></pre>
<p>Reflow는 해당 요소뿐 아니라 <strong>부모와 자식 요소까지 연쇄적으로 영향을 줄 수 있어 비용이 매우 큰 작업</strong>입니다
(CPU를 사용)</p>
<hr>
<h2 id="2-repaint란">2. Repaint란?</h2>
<p><strong>Repaint(Paint)</strong> 는 <strong>요소의 레이아웃은 그대로 두고 시각적인 스타일만 다시 그리는 과정)</strong>입니다.</p>
<h3 id="repaint-발생하는-대표적인-경우">Repaint 발생하는 대표적인 경우</h3>
<ul>
<li><strong>색상관련</strong> : <code>color</code>, <code>background-color</code> 변경</li>
<li><strong>테두리관련</strong> : <code>border-color</code> , <code>border-radius</code> 변경</li>
</ul>
<pre><code class="language-js">const el = document.querySelector(&#39;.box&#39;);
el.style.backgroundColor = &#39;red&#39;; // Repaint 발생</code></pre>
<p>Repaint는 Reflow보다 비용이 적지만, <strong>자주 발생하면 성능에 영향을 줄 수 있습니다.</strong>
(GPU를 사용)</p>
<hr>
<h2 id="성능-최적화-방법">성능 최적화 방법</h2>
<h3 id="1-reflow를-유발하는-css-속성-변경-최소화">1. Reflow를 유발하는 CSS 속성 변경 최소화</h3>
<p>레이아웃에 영향을 주는 속성은 가능한 한 초기 렌더링 시에만 설정하고,
동적 변경은 최소화하는 것이 좋습니다.</p>
<p>❗<em>reflow 유발 대표 속성</em></p>
<p><code>width</code>, <code>height</code>
<code>margin</code>, <code>padding</code>
<code>border</code>,
<code>top, left, right, bottom</code></p>
<pre><code class="language-js">// ❌ 반복적인 Reflow
for (let i = 0; i &lt; 100; i++) {
  el.style.width = `${i}px`;
}</code></pre>
<h3 id="2-레이아웃-정보-읽기와-쓰기-분리하기">2. 레이아웃 정보 읽기와 쓰기 분리하기</h3>
<p>레이아웃 정보를 읽는 순간, 브라우저는 강제로 최신 레이아웃 계산(Reflow) 을 수행합니다.</p>
<pre><code class="language-js">// ❌ Layout Thrashing
el.style.width = &#39;200px&#39;;
console.log(el.offsetWidth); // 강제 Reflow

// ✅ 읽기와 쓰기 분리
const width = el.offsetWidth;
el.style.width = width + 10 + &#39;px&#39;;</code></pre>
<h3 id="3-애니메이션은-transform과-opacity-사용">3. 애니메이션은 transform과 opacity 사용</h3>
<p><code>transform</code>과 <code>opacity</code>는 Reflow를 발생시키지 않고 Composite 단계에서 처리됩니다.
GPU 가속을 사용할 수 있어 성능상 가장 유리합니다.</p>
<pre><code class="language-js">
/* ❌ */
.box {
  left: 100px;
}

/* ✅ */
.box {
  transform: translateX(100px);
}</code></pre>
<h3 id="4-will-change-속성-활용">4. will-change 속성 활용</h3>
<p><code>will-change</code>는 브라우저에 앞으로 변경될 속성을 미리 알려주는 힌트입니다.</p>
<pre><code class="language-js">.box {
  will-change: transform;
}</code></pre>
<p>❗주의사항
과도한 사용은 메모리 낭비가 발생하므로 필요한 요소에만 적용해야 합니다!
애니메이션 시작 직전에만 적용하고 종료 후 제거 권장</p>
<h3 id="5-display와-visibility-차이-이해하기">5. display와 visibility 차이 이해하기</h3>
<ul>
<li><code>display: none</code> : 레이아웃에서 제거 → Reflow 발생</li>
<li><code>visibility: hidden</code>: 공간 유지 → Repaint만 발생</li>
</ul>
<p>상황에 맞게 선택하는 것이 중요합니다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<ol>
<li><strong>Reflow</strong>는 레이아웃 계산, <strong>Repaint</strong>는 화면 다시 그리기</li>
<li><strong>Reflow</strong>는비용이 크기 때문에 최소화가 핵심</li>
<li>애니메이션과 UI 변경은 <code>transform</code>, <code>opacity</code> 중심으로 설계</li>
<li>브라우저 렌더링 과정을 이해하면 자연스럽게 성능 최적화로 이어진다</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[ ✏️Vue 3 `defineProps`와 `withDefaults` (Vue 2와 차이점)]]></title>
            <link>https://velog.io/@k_jihye92/Vue-3-defineProps%EC%99%80-withDefaults-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-Vue-2%EC%99%80-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
            <guid>https://velog.io/@k_jihye92/Vue-3-defineProps%EC%99%80-withDefaults-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-Vue-2%EC%99%80-%EC%B0%A8%EC%9D%B4%EC%A0%90</guid>
            <pubDate>Thu, 08 May 2025 02:22:15 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>Vue3</strong>에서 <code>&lt;script setup&gt;</code> 구문을 사용할 때, 컴포넌트 props를 정의하는 방식이 완전히 달라졌습니다.
이 글에서는 Vue3의 <code>defineProps</code>와 <code>withDefaults</code> 사용법을 예제와 함께 설명하고, <strong>Vue2 방식과의 차이점</strong>도 짚어보겠습니다.</p>
</blockquote>
<hr>
<h3 id="✅-vue2에서-props-기본값-설정-방법">✅ Vue2에서 Props 기본값 설정 방법</h3>
<p>Vue2에서는 props를 <code>props</code> 옵션으로 정의하고, 객체 형태로 기본값과 타입을 지정</p>
<ul>
<li><code>default</code> : 기본값 지정</li>
<li><code>required</code> : false: 생략 가능 (기본이 false)</li>
</ul>
<pre><code class="language-js">export default {
  props: {
    title: {
      type: String,
      required: false,
      default: &#39;상품 이미지&#39;
    },
    itemName: String,
    itemCode: String
  }
}</code></pre>
<h3 id="✅-vue3-script-setup에서의-props-정의">✅ Vue3: <code>&lt;script Setup&gt;</code>에서의 Props 정의</h3>
<h4 id="1-defineprops로-props-선언">1. defineProps로 Props 선언</h4>
<ul>
<li>타입 기반으로 선언하며, 이때는 기본값 설정이 되지 않습니다.</li>
</ul>
<pre><code class="language-ts">&lt;script setup&gt;
const props = defineProps&lt;{
  title?: string;
  itemName?: string;
  itemImageList?: any[];
  itemCode?: string;
}&gt;();
&lt;/script&gt;</code></pre>
<h4 id="2-withdefaults로-기본값-지정">2. withDefaults()로 기본값 지정</h4>
<ul>
<li><code>withDefaults</code>는 타입 기반 <code>defineProps</code>에만 사용 가능</li>
<li>런타임에 작동하는 유일한 기본값 지정 방법</li>
</ul>
<pre><code class="language-ts">&lt;script setup&gt;
export interface Props {
  title?: string;
  itemName: string;
  itemImageList: Array&lt;any&gt;;
  itemCode: string;
}
let props = withDefaults(defineProps&lt;Props&gt;(), {
  title: &quot;상품 이미지&quot;
});
&lt;/script&gt;
</code></pre>
<blockquote>
<p>❗<strong>vue 3.4</strong> 이하 버전에서 타입 기반 선언 시 기본값을 사용시 해당 컴파일러 매크로를 사용하세요 :)
3.5이상의 버전에서만 Reactive Props 구조 분해가 기본 활성화되어
타입기반 <code>definePops</code> 에서도 기본값 설정이 자연스럽게 동작합니다.</p>
</blockquote>
<h4 id="🙅♀️잘못된-예시-작동하지-않음">🙅‍♀️잘못된 예시 (작동하지 않음)</h4>
<ul>
<li>Vue2 스타일의 default는 <code>&lt;script setup&gt;</code>에서 무시됨!<pre><code class="language-js">// default 속성은 무시됨
const props = defineProps({
title: {
  type: String,
  default: &#39;상품 이미지&#39;
}
});</code></pre>
</li>
</ul>
<hr>
<h3 id="🆚-vue-2-vs-vue-3-차이-요약">🆚 Vue 2 vs Vue 3 차이 요약</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>Vue 2 (Options API)</th>
<th>Vue 3 (<code>&lt;script setup&gt;</code>)</th>
</tr>
</thead>
<tbody><tr>
<td>props 선언</td>
<td><code>props: { ... }</code></td>
<td><code>defineProps</code> 사용</td>
</tr>
<tr>
<td>기본값 지정</td>
<td><code>default: &#39;value&#39;</code></td>
<td><code>withDefaults()</code> 사용</td>
</tr>
<tr>
<td>타입 지정</td>
<td><code>type: String</code> 등</td>
<td>TypeScript 기반 정의 권장</td>
</tr>
<tr>
<td>작동 시점</td>
<td>런타임</td>
<td>컴파일 타임 매크로 (<code>defineProps</code>)</td>
</tr>
</tbody></table>
<hr>
<h4 id="참고">참고</h4>
<ul>
<li><a href="https://vuejs.org/guide/components/props.html">Vue 공식 문서 - Props 정의</a></li>
<li><a href="https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits">Vue 공식 문서 - <code>&lt;script setup&gt;</code> defineProps문법</a></li>
<li><a href="https://vuejs.org/api/sfc-script-setup.html#withdefaults">Vue 공식 문서 - withDefaults</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[✏️쿠키(Cookie)와 세션(Session)의 차이점  ]]></title>
            <link>https://velog.io/@k_jihye92/%EC%BF%A0%ED%82%A4Cookie%EC%99%80-%EC%84%B8%EC%85%98Session%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
            <guid>https://velog.io/@k_jihye92/%EC%BF%A0%ED%82%A4Cookie%EC%99%80-%EC%84%B8%EC%85%98Session%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</guid>
            <pubDate>Fri, 21 Mar 2025 02:43:43 GMT</pubDate>
            <description><![CDATA[<h3 id="✅-쿠키-vs-세션">✅ 쿠키 vs 세션</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>쿠키 (Cookie)</th>
<th>세션 (Session)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>저장 위치</strong></td>
<td>클라이언트(브라우저)</td>
<td>서버</td>
</tr>
<tr>
<td><strong>데이터 보관 기간</strong></td>
<td>유효 기간을 설정하면 브라우저를 꺼도 유지됨</td>
<td>브라우저를 닫으면 일반적으로 삭제됨</td>
</tr>
<tr>
<td><strong>보안</strong></td>
<td>브라우저에서 접근 가능 → 보안이 낮음</td>
<td>서버에서 관리 → 보안이 높음</td>
</tr>
<tr>
<td><strong>용량 제한</strong></td>
<td>브라우저별 약 4KB 제한</td>
<td>서버에 저장되므로 제한이 서버 설정에 따라 다름</td>
</tr>
<tr>
<td><strong>사용 목적</strong></td>
<td>자동 로그인, 방문 기록 저장</td>
<td>로그인 상태 유지, 사용자 정보 저장</td>
</tr>
</tbody></table>
<h3 id="✅-사용-예시">✅ 사용 예시</h3>
<ul>
<li><strong>쿠키</strong>: 로그인 유지, 다크 모드 설정 저장  </li>
<li><strong>세션</strong>: 로그인 인증, 장바구니 정보 저장 (안전한 사용자 정보 보관)  </li>
</ul>
<hr>
<h4 id="📌-한-줄-요약">📌 한 줄 요약</h4>
<p>✔ <strong>쿠키는 브라우저에 저장되고, 세션은 서버에서 관리된다!</strong>    </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[✏️로컬 스토리지(Local Storage)와 세션 스토리지(Session Storage)의 차이점  ]]></title>
            <link>https://velog.io/@k_jihye92/%EB%A1%9C%EC%BB%AC-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80Local-Storage%EC%99%80-%EC%84%B8%EC%85%98-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80Session-Storage%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
            <guid>https://velog.io/@k_jihye92/%EB%A1%9C%EC%BB%AC-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80Local-Storage%EC%99%80-%EC%84%B8%EC%85%98-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80Session-Storage%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</guid>
            <pubDate>Fri, 21 Mar 2025 02:42:46 GMT</pubDate>
            <description><![CDATA[<h3 id="✅-로컬-스토리지-vs-세션-스토리지">✅ 로컬 스토리지 vs 세션 스토리지</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>로컬 스토리지 (Local Storage)</th>
<th>세션 스토리지 (Session Storage)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>데이터 유지 기간</strong></td>
<td>브라우저를 꺼도 유지됨</td>
<td>브라우저 탭을 닫으면 삭제됨</td>
</tr>
<tr>
<td><strong>공유 가능 여부</strong></td>
<td>같은 사이트 내 모든 탭에서 공유 가능</td>
<td>같은 탭에서만 사용 가능</td>
</tr>
<tr>
<td><strong>보안</strong></td>
<td>브라우저에 저장되므로 민감한 정보 저장 ❌</td>
<td>비교적 안전하지만, 민감한 정보 저장 ❌</td>
</tr>
<tr>
<td><strong>용량 제한</strong></td>
<td>약 5~10MB (브라우저마다 다름)</td>
<td>약 5MB (브라우저마다 다름)</td>
</tr>
<tr>
<td><strong>사용 목적</strong></td>
<td>로그인 정보, 테마 설정, 장바구니 데이터 저장</td>
<td>폼 입력값 임시 저장, 페이지 새로고침 시 데이터 유지</td>
</tr>
</tbody></table>
<h3 id="✅-사용-예시">✅ 사용 예시</h3>
<ul>
<li><strong>로컬 스토리지</strong>: 다크 모드 설정 유지, 자동 로그인 토큰 저장  </li>
<li><strong>세션 스토리지</strong>: 입력 폼 데이터 임시 저장, 페이지 이동 시 데이터 유지  </li>
</ul>
<hr>
<h4 id="📌-한-줄-요약">📌 한 줄 요약</h4>
<p>✔ <strong>로컬 스토리지는 브라우저를 꺼도 유지되지만, 세션 스토리지는 탭을 닫으면 삭제된다!</strong> </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[✏️ Moment.js에서 가장 많이 쓰이는 메서드 정리]]></title>
            <link>https://velog.io/@k_jihye92/Moment.js%EC%97%90%EC%84%9C-%EA%B0%80%EC%9E%A5-%EB%A7%8E%EC%9D%B4-%EC%93%B0%EC%9D%B4%EB%8A%94-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@k_jihye92/Moment.js%EC%97%90%EC%84%9C-%EA%B0%80%EC%9E%A5-%EB%A7%8E%EC%9D%B4-%EC%93%B0%EC%9D%B4%EB%8A%94-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Fri, 07 Mar 2025 00:55:27 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>📢 <strong>Moment.js</strong>는 JavaScript에서 날짜와 시간을 쉽게 다룰 수 있도록 도와주는 라이브러리입니다. 
가장 많이 사용되는 <strong>주요 메서드 5</strong>개를 소개하고, 각각의 정의와 예제를 정리해보겠습니다.</p>
</blockquote>
<hr>
<h3 id="1-diff">1. <code>diff()</code></h3>
<h4 id="정의">정의</h4>
<p>두 날짜 간의 차이를 계산하는 메서드로, 연도, 월, 일, 시간 등의 단위로 차이를 구할 수 있습니다.</p>
<h4 id="옵션">옵션</h4>
<ul>
<li><code>diff(moment|String|Number|Date|Array, String, Boolean)</code> : 비교할 날짜와 단위를 설정하고, Boolean 값(true)을 추가하면 소수점까지 반환할 수 있습니다.</li>
</ul>
<h4 id="예제">예제</h4>
<pre><code class="language-javascript">const moment = require(&#39;moment&#39;);

const date1 = moment(&#39;2025-03-07&#39;); // 종료날짜
const date2 = moment(&#39;2025-02-25&#39;); // 시작날짜

console.log(date1.diff(date2, &#39;days&#39;)); // 10 (일 단위 차이)
console.log(date1.diff(date2, &#39;weeks&#39;)); // 1 (주 단위 차이)
console.log(date1.diff(date2, &#39;months&#39;, true)); // 0.322 (소수점 포함)</code></pre>
<hr>
<h3 id="2-add--subtract">2. <code>add()</code> / <code>subtract()</code></h3>
<h4 id="정의-1">정의</h4>
<ul>
<li><code>add()</code>: 특정 단위의 시간을 더할 때 사용합니다.</li>
<li><code>subtract()</code>: 특정 단위의 시간을 뺄 때 사용합니다.</li>
<li><code>add()</code>와 <code>subtract()</code>는 음수를 사용할 수도 있으며, <code>add(-n, &#39;단위&#39;)</code>는 <code>subtract(n, &#39;단위&#39;)</code>와 동일한 효과를 가집니다.</li>
</ul>
<h4 id="옵션-1">옵션</h4>
<ul>
<li><code>add(Number, String)</code> : 숫자와 단위를 입력하여 시간을 더합니다.</li>
<li><code>subtract(Number, String)</code> : 숫자와 단위를 입력하여 시간을 뺍니다.</li>
</ul>
<h4 id="예제-1">예제</h4>
<pre><code class="language-javascript">const date = moment(&#39;2025-03-07&#39;);

console.log(date.add(5, &#39;days&#39;).format(&#39;YYYY-MM-DD&#39;)); // 2025-03-12
console.log(date.subtract(2, &#39;months&#39;).format(&#39;YYYY-MM-DD&#39;)); // 2025-01-07
console.log(date.add(-3, &#39;days&#39;).format(&#39;YYYY-MM-DD&#39;)); // 2025-03-04 (subtract(3, &#39;days&#39;)와 동일)</code></pre>
<hr>
<h3 id="3-format">3. <code>format()</code></h3>
<h4 id="정의-2">정의</h4>
<p>날짜를 특정 형식의 문자열로 변환하는 메서드입니다.</p>
<h4 id="옵션-2">옵션</h4>
<ul>
<li><code>format(String)</code> : 원하는 포맷의 문자열을 입력하여 날짜를 변환합니다.</li>
<li>기본값: YYYY-MM-DDTHH:mm:ssZ</li>
</ul>
<h4 id="예제-2">예제</h4>
<pre><code class="language-javascript">const date = moment();

console.log(date.format(&#39;YYYY-MM-DD HH:mm:ss&#39;)); // 2025-03-07 15:30:00
console.log(date.format(&#39;MMMM Do YYYY, h:mm:ss a&#39;)); // March 7th 2025, 3:30:00 pm</code></pre>
<hr>
<h3 id="4-isbefore--isafter">4. <code>isBefore()</code> / <code>isAfter()</code></h3>
<h4 id="정의-3">정의</h4>
<ul>
<li><code>isBefore()</code>: 특정 날짜보다 이전인지 확인하는 메서드</li>
<li><code>isAfter()</code>: 특정 날짜보다 이후인지 확인하는 메서드</li>
</ul>
<h4 id="옵션-3">옵션</h4>
<ul>
<li><code>isBefore(moment|String|Number|Date|Array, String)</code>: 비교할 날짜와 단위를 설정합니다.</li>
<li><code>isAfter(moment|String|Number|Date|Array, String)</code>: 비교할 날짜와 단위를 설정합니다.</li>
</ul>
<h4 id="예제-3">예제</h4>
<pre><code class="language-javascript">const date1 = moment(&#39;2025-03-07&#39;);
const date2 = moment(&#39;2025-02-25&#39;);

console.log(date1.isAfter(date2)); // true
console.log(date2.isBefore(date1)); // true</code></pre>
<hr>
<h3 id="5-startof--endof">5. <code>startOf()</code> / <code>endOf()</code></h3>
<h4 id="정의-4">정의</h4>
<ul>
<li><code>startOf()</code>: 특정 단위의 시작 시점을 반환하는 메서드</li>
<li><code>endOf()</code>: 특정 단위의 마지막 시점을 반환하는 메서드</li>
</ul>
<h4 id="옵션-4">옵션</h4>
<ul>
<li><code>startOf(String)</code> : 설정한 단위의 시작 시간을 반환합니다.</li>
<li><code>endOf(String)</code> : 설정한 단위의 마지막 시간을 반환합니다.</li>
</ul>
<h4 id="예제-4">예제</h4>
<pre><code class="language-javascript">const date = moment(&#39;2025-03-07&#39;);

console.log(date.startOf(&#39;month&#39;).format(&#39;YYYY-MM-DD HH:mm:ss&#39;)); // 2025-03-01 00:00:00
console.log(date.endOf(&#39;month&#39;).format(&#39;YYYY-MM-DD HH:mm:ss&#39;)); // 2025-03-31 23:59:59</code></pre>
<hr>
<h3 id="결론">결론</h3>
<p>Moment.js는 날짜를 다루는 다양한 기능을 제공하며, 특히 <code>diff()</code>, <code>add()/subtract()</code>, <code>format()</code>, <code>isBefore()/isAfter()</code>, <code>startOf()/endOf()</code> 등의 메서드는 자주 사용됩니다. 프로젝트에서 날짜 계산이 필요할 때 유용하게 활용해 보세요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[✏️ Vue 3: `defineExpose` vs `defineEmits`, 그리고 이벤트 vs 함수 차이]]></title>
            <link>https://velog.io/@k_jihye92/Vue-3-defineExpose-vs-defineEmits-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%9D%B4%EB%B2%A4%ED%8A%B8-vs-%ED%95%A8%EC%88%98-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@k_jihye92/Vue-3-defineExpose-vs-defineEmits-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%9D%B4%EB%B2%A4%ED%8A%B8-vs-%ED%95%A8%EC%88%98-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Mon, 24 Feb 2025 03:16:22 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Vue 3에서는 <code>defineExpose</code>와 <code>defineEmits</code>를 사용해 <strong>컴포넌트 간 데이터 및 기능을 전달</strong>할 수 있다. 이를 이해하려면, 먼저 <strong>이벤트(event)와 함수(function)의 차이</strong>를 알아야 한다.</p>
</blockquote>
<hr>
<h3 id="1️⃣-이벤트event-vs-함수function">1️⃣ 이벤트(event) vs 함수(function)</h3>
<h4 id="🎯-함수란">🎯 함수란?</h4>
<ul>
<li>특정 로직을 실행하는 코드 블록</li>
<li>직접 호출해야 실행됨 (<code>함수명()</code>)</li>
<li>독립적인 동작 수행 가능</li>
</ul>
<h5 id="✅-함수-예제">✅ 함수 예제</h5>
<pre><code class="language-js">function sayHello() {
  console.log(&quot;안녕하세요!&quot;);
}

sayHello(); // 직접 호출</code></pre>
<p>➡️ <code>sayHello()</code>를 호출해야 콘솔에 &quot;안녕하세요!&quot;가 출력됨.</p>
<h4 id="🎯-이벤트란">🎯 이벤트란?</h4>
<ul>
<li>특정 <strong>상황(클릭, 입력 등)</strong>이 발생했을 때 실행됨</li>
<li>브라우저나 사용자의 동작에 의해 자동으로 실행됨</li>
<li>이벤트 리스너를 등록해야 함</li>
</ul>
<h5 id="✅-이벤트-예제">✅ 이벤트 예제</h5>
<pre><code class="language-js">document.getElementById(&quot;btn&quot;).addEventListener(&quot;click&quot;, function () {
  console.log(&quot;버튼이 클릭됨!&quot;);
});</code></pre>
<p>➡️ 버튼을 클릭해야 &quot;버튼이 클릭됨!&quot;이 출력됨. 직접 호출하지 않아도 이벤트가 발생하면 자동 실행됨.</p>
<h4 id="🔥-이벤트와-함수의-연결">🔥 이벤트와 함수의 연결</h4>
<ul>
<li>이벤트는 특정 상황이 발생하면 실행되지만, 그 안에서 실행되는 것은 결국 <strong>함수</strong>이다.</li>
<li>즉, 이벤트는 트리거(Trigger) 역할을 하고, 함수는 실제 동작을 수행하는 역할을 한다.</li>
</ul>
<pre><code class="language-js">function handleClick() {
  console.log(&quot;버튼이 눌렸습니다!&quot;);
}

document.getElementById(&quot;btn&quot;).addEventListener(&quot;click&quot;, handleClick);</code></pre>
<p>➡️ 클릭하면 <code>handleClick()</code> 함수가 실행됨.</p>
<hr>
<h3 id="2️⃣-defineexpose-vs-defineemits">2️⃣ <code>defineExpose</code> vs <code>defineEmits</code></h3>
<h4 id="🎯-defineexpose-부모가-자식의-특정-메서드-직접-호출">🎯 <code>defineExpose</code>: 부모가 자식의 특정 메서드 직접 호출</h4>
<p><code>defineExpose</code>는 <strong>자식 컴포넌트의 메서드(함수)나 데이터(변수)를 부모가 직접 접근</strong>할 수 있도록 한다.</p>
<h5 id="✅-defineexpose-예제">✅ <code>defineExpose</code> 예제</h5>
<h5 id="📌-자식-컴포넌트-childcomponentvue">📌 자식 컴포넌트 (<code>ChildComponent.vue</code>)</h5>
<pre><code class="language-vue">&lt;script setup&gt;
import { defineExpose } from &quot;vue&quot;;

const sayHello = () =&gt; {
  console.log(&quot;안녕하세요!&quot;);
};

defineExpose({
  sayHello,
});
&lt;/script&gt;

&lt;template&gt;
  &lt;p&gt;자식 컴포넌트&lt;/p&gt;
&lt;/template&gt;</code></pre>
<h5 id="📌-부모-컴포넌트-parentvue">📌 부모 컴포넌트 (<code>Parent.vue</code>)</h5>
<pre><code class="language-vue">&lt;template&gt;
  &lt;ChildComponent ref=&quot;childRef&quot; /&gt;
  &lt;button @click=&quot;callChildMethod&quot;&gt;자식 함수 호출&lt;/button&gt;
&lt;/template&gt;

&lt;script setup&gt;
import { ref } from &quot;vue&quot;;
import ChildComponent from &quot;./ChildComponent.vue&quot;;

const childRef = ref(null);

const callChildMethod = () =&gt; {
  if (childRef.value) {
    childRef.value.sayHello(); // ✅ 부모에서 직접 자식 함수 호출
  }
};
&lt;/script&gt;</code></pre>
<p>➡️ 부모가 <code>ref</code>를 이용해 자식의 <code>sayHello()</code> 메서드를 직접 실행할 수 있음.</p>
<hr>
<h4 id="🎯-defineemits-자식이-부모에게-이벤트-전달">🎯 <code>defineEmits</code>: 자식이 부모에게 이벤트 전달</h4>
<p><code>defineEmits</code>는 <strong>자식이 부모에게 데이터를 전달</strong>할 때 사용된다. 부모는 해당 이벤트를 감지해 핸들러를 실행할 수 있다.</p>
<h5 id="✅-defineemits-예제">✅ <code>defineEmits</code> 예제</h5>
<h5 id="📌-자식-컴포넌트-childcomponentvue-1">📌 자식 컴포넌트 (<code>ChildComponent.vue</code>)</h5>
<pre><code class="language-vue">&lt;script setup&gt;
import { defineEmits } from &quot;vue&quot;;

const emit = defineEmits([&quot;customEvent&quot;]);

const notifyParent = () =&gt; {
  emit(&quot;customEvent&quot;, &quot;자식이 보낸 데이터!&quot;);
};
&lt;/script&gt;

&lt;template&gt;
  &lt;button @click=&quot;notifyParent&quot;&gt;부모에게 이벤트 전송&lt;/button&gt;
&lt;/template&gt;</code></pre>
<h5 id="📌-부모-컴포넌트-parentvue-1">📌 부모 컴포넌트 (<code>Parent.vue</code>)</h5>
<pre><code class="language-vue">&lt;template&gt;
  &lt;ChildComponent @customEvent=&quot;handleEvent&quot; /&gt;
&lt;/template&gt;

&lt;script setup&gt;
import ChildComponent from &quot;./ChildComponent.vue&quot;;

const handleEvent = (data) =&gt; {
  console.log(&quot;부모가 받은 데이터:&quot;, data);
};
&lt;/script&gt;</code></pre>
<p>➡️ 자식에서 <code>emit(&quot;customEvent&quot;, 데이터)</code>을 실행하면, 부모는 <code>@customEvent</code>로 데이터를 받을 수 있음.</p>
<hr>
<h3 id="3️⃣-defineexpose-vs-defineemits-정리">3️⃣ <code>defineExpose vs defineEmits</code> 정리</h3>
<table>
<thead>
<tr>
<th>기능</th>
<th><code>defineExpose</code></th>
<th><code>defineEmits</code></th>
</tr>
</thead>
<tbody><tr>
<td>방향</td>
<td><strong>자식 → 부모</strong></td>
<td><strong>부모 → 자식</strong></td>
</tr>
<tr>
<td>역할</td>
<td>자식의 특정 메서드/데이터를 부모가 직접 접근할 수 있도록 함</td>
<td>부모에게 이벤트를 전달하여 특정 동작을 실행하게 함</td>
</tr>
<tr>
<td>사용 방식</td>
<td><code>ref</code>를 통해 부모에서 직접 호출</td>
<td><code>emit()</code>을 통해 부모에게 알림</td>
</tr>
<tr>
<td>사용 예시</td>
<td><code>childRef.value.someMethod()</code></td>
<td><code>emit(&quot;event-name&quot;, data)</code></td>
</tr>
</tbody></table>
<hr>
<h3 id="🎯-언제-defineexpose-vs-defineemits를-사용할까">🎯 언제 <code>defineExpose</code> vs <code>defineEmits</code>를 사용할까?</h3>
<p>✔ <strong>부모가 자식의 특정 메서드를 직접 호출해야 한다면</strong> → <code>defineExpose</code></p>
<pre><code class="language-vue">childRef.value.sayHello();</code></pre>
<p>✔ <strong>자식이 부모에게 데이터를 전달해야 한다면</strong> → <code>defineEmits</code></p>
<pre><code class="language-vue">emit(&quot;customEvent&quot;, data);</code></pre>
<p>👉 <strong>이벤트는 특정 상황에서 실행되며(<code>defineEmits</code>), 함수는 직접 실행하는 코드 블록(<code>defineExpose</code>를 활용하여 호출 가능)</strong></p>
<p>이제 Vue 3에서 <strong>이벤트와 함수의 차이</strong>를 이해하고, <code>defineExpose</code>와 <code>defineEmits</code>를 언제 사용해야 하는지 확실히 알았을 거다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🛠️ 프론트엔드와 백엔드 사이에서 주로 협의하는 내용]]></title>
            <link>https://velog.io/@k_jihye92/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%99%80-%EB%B0%B1%EC%97%94%EB%93%9C-%EC%82%AC%EC%9D%B4%EC%97%90%EC%84%9C-%EC%A3%BC%EB%A1%9C-%ED%98%91%EC%9D%98%ED%95%98%EB%8A%94-%EB%82%B4%EC%9A%A9</link>
            <guid>https://velog.io/@k_jihye92/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%99%80-%EB%B0%B1%EC%97%94%EB%93%9C-%EC%82%AC%EC%9D%B4%EC%97%90%EC%84%9C-%EC%A3%BC%EB%A1%9C-%ED%98%91%EC%9D%98%ED%95%98%EB%8A%94-%EB%82%B4%EC%9A%A9</guid>
            <pubDate>Thu, 20 Feb 2025 05:56:24 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<blockquote>
<p>프론트엔드 개발자로 전향하면서 백단과 프론트단 사이에서 소통은 어떤게 필요할까궁금해서 정리해보았다.</p>
</blockquote>
<hr>
<h3 id="1️⃣-api-설계-및-데이터-구조-관련">1️⃣ API 설계 및 데이터 구조 관련</h3>
<h4 id="✅-1-1-api-요청응답-데이터-구조">✅ 1-1. API 요청/응답 데이터 구조</h4>
<p><strong>📌 프론트 &amp; 백엔드 협의 포인트</strong></p>
<ul>
<li>필요한 데이터 및 형식 결정</li>
<li>필드명 통일 (<code>userId</code> vs <code>user_id</code> vs <code>id</code>)</li>
<li>불필요한 데이터 제거</li>
</ul>
<p><strong>💡 예제: 주문 정보 API 협의</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>프론트 요청</th>
<th>백엔드 응답</th>
</tr>
</thead>
<tbody><tr>
<td>주문 번호</td>
<td><code>order_id</code></td>
<td><code>orderId</code></td>
</tr>
<tr>
<td>주문 상태</td>
<td><code>status</code></td>
<td><code>status</code></td>
</tr>
<tr>
<td>사용자 정보</td>
<td><code>user</code></td>
<td><code>{ userId, name, email }</code></td>
</tr>
</tbody></table>
<hr>
<h4 id="✅-1-2-api-응답-상태-코드--에러-핸들링">✅ 1-2. API 응답 상태 코드 &amp; 에러 핸들링</h4>
<p><strong>📌 프론트 &amp; 백엔드 협의 포인트</strong></p>
<ul>
<li>상태 코드 정의 (<code>200</code>, <code>400</code>, <code>500</code> 등)</li>
<li>에러 메시지 통일 (<code>&quot;USER_NOT_FOUND&quot;</code>, <code>&quot;INVALID_INPUT&quot;</code> 등)</li>
<li>UI에서 처리할 수 있도록 에러 코드 설계</li>
</ul>
<p><strong>💡 예제: 로그인 API 응답 협의</strong></p>
<table>
<thead>
<tr>
<th>HTTP 상태 코드</th>
<th>메시지</th>
<th>프론트 처리 방식</th>
</tr>
</thead>
<tbody><tr>
<td><code>200 OK</code></td>
<td><code>{ &quot;message&quot;: &quot;success&quot;, &quot;userId&quot;: 123 }</code></td>
<td>로그인 성공</td>
</tr>
<tr>
<td><code>400 Bad Request</code></td>
<td><code>{ &quot;error&quot;: &quot;INVALID_CREDENTIALS&quot; }</code></td>
<td>&quot;아이디 또는 비밀번호가 올바르지 않습니다.&quot;</td>
</tr>
<tr>
<td><code>500 Internal Server Error</code></td>
<td><code>{ &quot;error&quot;: &quot;SERVER_ERROR&quot; }</code></td>
<td>&quot;서버 오류 발생, 다시 시도해주세요.&quot;</td>
</tr>
</tbody></table>
<hr>
<h4 id="✅-1-3-페이징--데이터-로딩-방식">✅ 1-3. 페이징 &amp; 데이터 로딩 방식</h4>
<p><strong>📌 프론트 &amp; 백엔드 협의 포인트</strong></p>
<ul>
<li>한 번에 불러올 데이터 개수 (<code>page=1&amp;size=20</code>)</li>
<li>무한 스크롤 vs 페이지네이션 vs 더보기 버튼 방식</li>
<li>정렬 &amp; 필터링 방식 (<code>sort=desc</code>, <code>filter=status:active</code>)</li>
</ul>
<p><strong>💡 예제: 주문 목록 조회 API 협의</strong></p>
<pre><code class="language-bash">GET /api/orders?page=1&amp;size=20&amp;sort=createdAt,desc</code></pre>
<hr>
<h3 id="2️⃣-성능--최적화-관련">2️⃣ 성능 &amp; 최적화 관련</h3>
<h4 id="✅-2-1-데이터-전송-최적화-필요한-데이터만-받기">✅ 2-1. 데이터 전송 최적화 (필요한 데이터만 받기)</h4>
<p><strong>📌 프론트 &amp; 백엔드 협의 포인트</strong></p>
<ul>
<li>불필요한 필드 제거</li>
<li>대용량 데이터 (이미지 등) URL 방식으로 전달</li>
<li>캐싱 가능한 데이터 처리 방식 결정</li>
</ul>
<p><strong>💡 예제: 필요 없는 필드 제거 요청</strong></p>
<pre><code class="language-json">{
  &quot;orderId&quot;: 123,
  &quot;user&quot;: {
    &quot;name&quot;: &quot;홍길동&quot;,
    &quot;email&quot;: &quot;hong@example.com&quot;
  },
  &quot;items&quot;: [
    {
      &quot;name&quot;: &quot;노트북&quot;,
      &quot;price&quot;: 1500000
    }
  ]
}</code></pre>
<hr>
<h4 id="✅-2-2-요청-개수-최소화-api-호출-최적화">✅ 2-2. 요청 개수 최소화 (API 호출 최적화)</h4>
<p><strong>📌 프론트 &amp; 백엔드 협의 포인트</strong></p>
<ul>
<li>여러 개의 API 요청을 합쳐서 최소화</li>
<li>필요한 데이터만 포함하는 API 설계</li>
</ul>
<p><strong>💡 예제: 여러 개의 API를 하나로 합치기</strong></p>
<pre><code class="language-json">{
  &quot;orderId&quot;: 123,
  &quot;user&quot;: { &quot;name&quot;: &quot;홍길동&quot;, &quot;email&quot;: &quot;hong@example.com&quot; },
  &quot;items&quot;: [
    { &quot;name&quot;: &quot;노트북&quot;, &quot;price&quot;: 1500000 },
    { &quot;name&quot;: &quot;마우스&quot;, &quot;price&quot;: 30000 }
  ]
}</code></pre>
<hr>
<h4 id="✅-2-3-프론트-vs-백엔드에서-처리할-로직-분배">✅ 2-3. 프론트 vs 백엔드에서 처리할 로직 분배</h4>
<p><strong>📌 프론트 &amp; 백엔드 협의 포인트</strong></p>
<ul>
<li>데이터 필터링을 프론트에서 할지, 백엔드에서 할지 결정</li>
<li>계산 로직 처리 위치 협의</li>
</ul>
<p><strong>💡 예제: 주문 상태 변환 (프론트 vs 백엔드 협의)</strong></p>
<pre><code class="language-json">// 백엔드 응답 예제
{ &quot;status&quot;: &quot;PENDING_PAYMENT&quot; }</code></pre>
<pre><code class="language-javascript">// 프론트에서 변환 예제
const statusText = {
  &quot;PENDING_PAYMENT&quot;: &quot;결제 대기중&quot;,
  &quot;SHIPPED&quot;: &quot;배송 중&quot;,
  &quot;DELIVERED&quot;: &quot;배송 완료&quot;
};
console.log(statusText[&quot;PENDING_PAYMENT&quot;]); // &quot;결제 대기중&quot;</code></pre>
<hr>
<h3 id="3️⃣-인증--보안-관련">3️⃣ 인증 &amp; 보안 관련</h3>
<h4 id="✅-3-1-인증-방식-협의-jwt-vs-세션-vs-oauth-등">✅ 3-1. 인증 방식 협의 (JWT vs 세션 vs OAuth 등)</h4>
<p><strong>📌 프론트 &amp; 백엔드 협의 포인트</strong></p>
<ul>
<li>로그인 인증 방식 결정 (JWT, 세션, OAuth 등)</li>
<li>API 호출 시 인증 토큰 전달 방식 (<code>Authorization</code> 헤더 사용)</li>
</ul>
<p><strong>💡 예제: JWT 인증 방식</strong></p>
<pre><code class="language-http">GET /api/user
Authorization: Bearer eyJhbGciOiJIUzI1...</code></pre>
<hr>
<h4 id="✅-3-2-api-권한-및-보안-체크">✅ 3-2. API 권한 및 보안 체크</h4>
<p><strong>📌 프론트 &amp; 백엔드 협의 포인트</strong></p>
<ul>
<li>인증이 필요한 API vs 공개 API 구분</li>
<li>권한이 없을 때 응답 코드 (<code>403 Forbidden</code> vs <code>401 Unauthorized</code>)</li>
</ul>
<p><strong>💡 예제: 관리자 권한 체크 API</strong></p>
<pre><code class="language-http">GET /api/admin/orders
403 Forbidden // 관리자가 아니라면 이 응답을 보내기</code></pre>
<hr>
<h3 id="🔹-결론-주요-협의-내용-요약">🔹 결론 (주요 협의 내용 요약)</h3>
<p>✅ API 데이터 구조 &amp; 필드명 맞추기<br>✅ 페이징, 정렬, 필터링 방식 정하기<br>✅ 에러 코드 및 응답 메시지 통일<br>✅ 프론트 vs 백엔드에서 처리할 로직 나누기<br>✅ API 요청 개수 최소화 &amp; 최적화<br>✅ 인증 및 권한 체크 방식 협의  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[✏️ HTTP 상태 코드 모음]]></title>
            <link>https://velog.io/@k_jihye92/HTTP-%EC%83%81%ED%83%9C-%EC%BD%94%EB%93%9C-%EB%AA%A8%EC%9D%8C</link>
            <guid>https://velog.io/@k_jihye92/HTTP-%EC%83%81%ED%83%9C-%EC%BD%94%EB%93%9C-%EB%AA%A8%EC%9D%8C</guid>
            <pubDate>Wed, 20 Nov 2024 03:37:04 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>HTTP 상태 코드는 클라이언트와 서버 간의 통신에서 요청이 성공했는지, 실패했는지, 또는 추가 작업이 필요한지 등을 나타내는 숫자 코드입니다. 총 5가지 범주로 나뉩니다.</p>
</blockquote>
<hr>
<h3 id="1xx-정보-응답-informational-responses">1xx: 정보 응답 (Informational Responses)</h3>
<p>클라이언트 요청이 접수되었으며 처리가 진행 중임을 나타냅니다.</p>
<ul>
<li><strong>100 Continue</strong>: 요청의 일부를 수신했으며 나머지를 계속 보내도 됨.</li>
<li><strong>101 Switching Protocols</strong>: 서버가 프로토콜 변경 요청을 수락함.</li>
<li><strong>103 Early Hints</strong>: 리소스 로딩에 사용할 수 있는 사전 정보 제공.</li>
</ul>
<hr>
<h3 id="2xx-성공-successful-responses">2xx: 성공 (Successful Responses)</h3>
<p>요청이 성공적으로 처리되었음을 나타냅니다.</p>
<ul>
<li><strong>200 OK</strong>: 요청이 성공적으로 처리됨.</li>
<li><strong>201 Created</strong>: 요청이 성공적으로 처리되었으며 새 리소스가 생성됨.</li>
<li><strong>202 Accepted</strong>: 요청이 접수되었지만 아직 처리되지 않음.</li>
<li><strong>204 No Content</strong>: 요청은 성공했으나 응답 본문이 없음.</li>
</ul>
<hr>
<h3 id="3xx-리다이렉션-redirection-responses">3xx: 리다이렉션 (Redirection Responses)</h3>
<p>요청한 리소스가 다른 위치에 있거나 추가 작업이 필요함을 나타냅니다.</p>
<ul>
<li><strong>301 Moved Permanently</strong>: 요청한 리소스가 영구적으로 다른 위치로 이동함.</li>
<li><strong>302 Found</strong>: 요청한 리소스가 임시로 다른 위치에 있음.</li>
<li><strong>304 Not Modified</strong>: 리소스가 수정되지 않았으므로 캐시된 버전을 사용해야 함.</li>
<li><strong>307 Temporary Redirect</strong>: 요청한 리소스가 임시로 다른 URL에 있음 (요청 메서드 유지).</li>
<li><strong>308 Permanent Redirect</strong>: 요청한 리소스가 영구적으로 다른 URL에 있음 (요청 메서드 유지).</li>
</ul>
<hr>
<h3 id="4xx-클라이언트-오류-client-errors">4xx: 클라이언트 오류 (Client Errors)</h3>
<p>클라이언트 요청에 오류가 있을 때 사용됩니다.</p>
<ul>
<li><strong>400 Bad Request</strong>: 잘못된 요청으로 인해 서버가 이해하지 못함.</li>
<li><strong>401 Unauthorized</strong>: 인증이 필요하지만 제공되지 않았거나 유효하지 않음.</li>
<li><strong>403 Forbidden</strong>: 권한이 없어 요청이 거부됨.</li>
<li><strong>404 Not Found</strong>: 요청한 리소스를 찾을 수 없음.</li>
<li><strong>405 Method Not Allowed</strong>: 허용되지 않는 HTTP 메서드 사용.</li>
</ul>
<hr>
<h3 id="5xx-서버-오류-server-errors">5xx: 서버 오류 (Server Errors)</h3>
<p>서버가 요청을 처리하지 못했을 때 사용됩니다.</p>
<ul>
<li><strong>500 Internal Server Error</strong>: 서버 내부 오류로 인해 요청을 처리할 수 없음.</li>
<li><strong>501 Not Implemented</strong>: 요청된 메서드가 서버에서 지원되지 않음.</li>
<li><strong>502 Bad Gateway</strong>: 게이트웨이나 프록시 서버에서 잘못된 응답을 수신함.</li>
<li><strong>503 Service Unavailable</strong>: 서버가 현재 요청을 처리할 수 없음 (과부하 또는 유지 보수).</li>
<li><strong>504 Gateway Timeout</strong>: 게이트웨이나 프록시 서버가 요청을 처리하는 데 시간이 초과됨.</li>
</ul>
<hr>
<h2 id="참고">참고</h2>
<ul>
<li>공식 문서: <a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Status">MDN Web Docs - HTTP 상태 코드</a></li>
<li>관련 RFC 문서: <a href="https://www.rfc-editor.org/rfc/rfc7231">RFC 7231 (HTTP/1.1)</a></li>
</ul>
<hr>
<p><code>HTTP 상태 코드는 웹 개발 및 API 통신에서 중요한 역할을 하므로 각 코드의 의미를 명확히 이해하고 사용하는 것이 중요합니다.</code></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vue3에 Stylelint 설치!]]></title>
            <link>https://velog.io/@k_jihye92/Vue-3-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-Stylelint-%EB%8F%84%EC%9E%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@k_jihye92/Vue-3-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-Stylelint-%EB%8F%84%EC%9E%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 09 Jul 2024 02:54:22 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Vue 3 프로젝트를 진행하면서 SCSS를 사용하게 될 경우, 코드 스타일의 일관성을 유지하고 협업 효율성을 높이기 위해 ESLint와 Prettier 뿐만 아니라 Stylelint를 도입하는 것이 중요합니다. Stylelint를 통해 SCSS 포맷을 통일화하면, 탭 사이즈 등 코드 형식을 일관되게 유지할 수 있어 작성법을 표준화하고 코드 충돌의 위험을 줄일 수 있습니다.</p>
</blockquote>
<h3 id="stylelint-설치-및-설정">Stylelint 설치 및 설정</h3>
<p>먼저, Stylelint를 설치합니다. </p>
<pre><code class="language-bash">npm install stylelint stylelint-scss stylelint-config-standard-scss --save-dev</code></pre>
<p>설치가 완료되면, 프로젝트 루트에 <code>.stylelintrc.json</code> 파일을 생성하고 다음과 같이 설정합니다</p>
<pre><code class="language-bash">{
  &quot;extends&quot;: &quot;stylelint-config-standard-scss&quot;,
  &quot;rules&quot;: {
    &quot;color-hex-case&quot;: &quot;lower&quot;,
  }
}
</code></pre>
<p>필요에 따라 아래와 같이 추가 설정이 가능합니다</p>
<pre><code class="language-bash">{
    &quot;plugins&quot;: [&quot;stylelint-scss&quot;, &quot;stylelint-order&quot;],
    &quot;extends&quot;: [
        &quot;stylelint-config-standard-scss&quot;,
        &quot;stylelint-config-recommended-vue/scss&quot;,
        &quot;stylelint-config-prettier-scss&quot;
    ],
    &quot;customSyntax&quot;: &quot;postcss-html&quot;,
    &quot;rules&quot;: {
        &quot;color-hex-case&quot;: &quot;lower&quot;,
        &quot;order/properties-alphabetical-order&quot;: true // 
    }
}
</code></pre>
<h4 id="plugin">Plugin</h4>
<ul>
<li><code>stylelint-scss</code>: SCSS 문법에 대한 지원을 추가합니다. SCSS를 사용할 때 유용한 다양한 규칙을 제공합니다.</li>
<li><code>stylelint-order</code>: CSS 속성의 순서를 정렬하는 기능을 제공합니다. 예를 들어, 알파벳 순서로 속성을 정렬할 수 있습니다.</li>
</ul>
<h4 id="extends">Extends</h4>
<ul>
<li><code>stylelint-config-standard-scss</code>: 표준 SCSS 규칙을 포함하고 있는 Stylelint 구성입니다. 기본적인 스타일 규칙을 설정하여 코드 일관성을 유지합니다.</li>
<li><code>stylelint-config-recommended-vue/scss</code>: Vue.js와 SCSS를 함께 사용할 때 권장되는 Stylelint 규칙을 포함합니다. Vue 파일 내에서 SCSS 스타일을 검사할 때 유용합니다.</li>
<li><code>stylelint-config-prettier-scss</code>: Prettier와의 호환성을 위해 일부 Stylelint 규칙을 비활성화합니다. 이를 통해 Prettier와 Stylelint 간의 충돌을 방지합니다.</li>
</ul>
<h4 id="추가-설정">추가 설정</h4>
<ul>
<li><code>customSyntax: postcss-html</code>을 사용하여 .vue 파일 내의 스타일을 분석할 수 있도록 설정합니다. 이를 통해 Vue 컴포넌트 파일에서 스타일을 검사할 수 있습니다.</li>
</ul>
<blockquote>
<p>Figma 색상 코드 소문자로 변환
프로젝트를 진행하면서 가장 필요했던 부분은 Figma에서 복사한 색상 코드가 대문자로 표시되는 것을 소문자로 변환하는 룰이었습니다. 이를 위해 <code>color-hex-case</code> 규칙을 &quot;lower&quot;로 설정하여 대문자로 작성된 색상 코드가 자동으로 소문자로 변환되도록 합니다.</p>
</blockquote>
<br/>


<h3 id="vscode-설정">VSCode 설정</h3>
<p>VSCode에서 코드 저장 시 자동으로 스타일이 수정되도록 설정하면 더욱 편리합니다. 이를 위해 VSCode의 settings.json 파일에 다음 설정을 추가하세요</p>
<pre><code class="language-bash">
  &quot;editor.codeActionsOnSave&quot;: {
    &quot;source.fixAll.stylelint&quot;: true
  },</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Todo List (with JS)]]></title>
            <link>https://velog.io/@k_jihye92/Todo-List-with-JS</link>
            <guid>https://velog.io/@k_jihye92/Todo-List-with-JS</guid>
            <pubDate>Thu, 14 Mar 2024 13:59:35 GMT</pubDate>
            <description><![CDATA[<h3 id="코딩테스트-회고">코딩테스트 회고</h3>
<blockquote>
<p> 사진 참고해 todo List html, css, js로 40분안에 구현</p>
</blockquote>
<p>통과여부는...👇
....
...
..
.</p>
<h2 id="탈락">&#39;탈락&#39;</h2>
<p><img src="https://velog.velcdn.com/images/k_jihye92/post/85018f7e-b78d-4620-9f59-543b9dfd7ab9/image.png" alt=""></p>
<p style="text-align:center;margin-top: -20px">
  찾아요.. 쥐구멍...
<p>
기본적인건데, 구현하지 못하다니...(개발자는 무리인가) <br/>
마냥 좌절하구 있을 수 없어, 원인을 파악하고 해결해나가기로 했다. <br/>
또, 블로그에 기록해서 내 머리 속에 더 강력하게 남기기로 함!

<h3 id="원인과-해결">원인과 해결</h3>
<blockquote>
<p>vue와 react 강의통해 todo list를 많이 만들어봤는데,
가장 큰 문제는 혼자 구현해보지 않고, 기록도 안함.
결국 내 머리, 손가락에 남는건 없었던 것이다</p>
</blockquote>
<p><strong>problem 1</strong> : list binding부터 실패 -&gt; 마지막 하나만 나옴
  추가되어도 추가된 마지막 하나만 나온다!
  ul에 li을 생성해서 innerHTML 하였음 되었는데..잘못 타켓팅함</p>
<p><strong>solution 1</strong></p>
<pre><code class="language-jsx">  const todoWrap = document.querySelector(&#39;.todoWrap&#39;);
  let todos = [
  {
    id: 1,
    text: &#39;코테 회고&#39;,
    completed: false,
  },
  {
    id: 2,
    text: &#39;방청소하기&#39;,
    completed: true,
  }
];
  const renderList = () =&gt; {
    todoWrap.innerHTML = &#39;&#39;; // 초기화
    todos.forEach(todo =&gt; {
      // li 만들기
      let item = document.createElement(&#39;li&#39;);
      item.setAttribute(&#39;class&#39;,&#39;todoItem&#39;);

      // start li 내부 요소 만들어 넣기 + 클래스명 + 이벤트리스너 추가
      let checkBtn = document.createElement(&#39;button&#39;);
      checkBtn.setAttribute(&#39;class&#39;,&#39;checkBtn&#39;);
      checkBtn.addEventListener(&#39;click&#39;,() =&gt; checkTodo(todo.id));

      let todoText = document.createElement(&#39;p&#39;);
      todoText.innerText = todo.text;

      let removeBtn = document.createElement(&#39;button&#39;);
      removeBtn.setAttribute(&#39;class&#39;,&#39;removeBtn&#39;);
      removeBtn.innerText = &#39;삭제&#39;;
      removeBtn.addEventListener(&#39;click&#39;, () =&gt; removeTodo(todo.id));

      todo.completed ? item.classList.add(&#39;completed&#39;) :      item.classList.remove(&#39;completed&#39;)

      item.append(checkBtn);
      item.append(todoText);
      item.append(removeBtn);
      // end li 내부 요소 만들어 넣기 + 클래스명 + 이벤트리스너 추가

      // 만든 li ul에 넣기
      todoWrap.appendChild(item); 
    })
 }
  renderList() // 호출하기!</code></pre>
<hr>
<p><strong>problem 2</strong> : <strong>createTodo</strong>
  추가 기능은 문제 없었으니 보완해서 다시 해보기</p>
<p><strong>solution 2</strong> </p>
<pre><code class="language-jsx">  const todoInput = document.querySelector(&#39;.todoInput&#39;);
  const addBtn = document.querySelector(&#39;.addBtn&#39;);

  const createTodo = function(){
    if(todoInput.value === &#39;&#39;) {
      alert(&#39;할 일을 입력하세요&#39;);
    } else {
    // new 객체 만들어 담기
      let newItem = {
        id:  nextId + 1,
        text: todoInput.value,
        complted: false
      };

      // todos = [...todos,newItem];   방법1
      todos = todos.concat(newItem); //방법2

      // reset
      todoInput.value = &#39;&#39;;
      nextId += 1;

      // render list
      renderList();
    }
}

  // 버튼에 클릭이벤트 추가
  addBtn.addEventListener(&#39;click&#39;,(e)=&gt;{
      e.preventDefault();
      createTodo();
});
</code></pre>
<hr>
<p>   <strong>solution3</strong>: <strong>checkTodo + removeTodo</strong></p>
<pre><code class="language-jsx">  const checkTodo = function(id) {
  let newTodos = todos.map(todo =&gt; {
    if(todo.id === id) {
      return {...todo,completed: !todo.completed}
  }else return todo
    });
    todos = newTodos;
    renderList();
}

const removeTodo = function(id) {
    let newTodos = todos.filter(todo =&gt; todo.id !== id);
    todos = newTodos;
    renderList();
}</code></pre>
<hr>
<blockquote>
<p><strong>전체 코드 보기</strong></p>
</blockquote>
<p>  <strong>html</strong></p>
<pre><code class="language-jsx">  &lt;body&gt;
    &lt;div class=&quot;wrap&quot;&gt;

      &lt;h2&gt;TodoList&lt;/h2&gt;
      &lt;form&gt;
        &lt;input type=&quot;text&quot; class=&quot;todoInput&quot; placeholder=&quot;할일을 입력하세요&quot;&gt;
        &lt;button class=&quot;addBtn&quot;&gt;추가&lt;/button&gt;
      &lt;/form&gt;
      &lt;ul class=&quot;todoWrap&quot;&gt;

      &lt;/ul&gt;
    &lt;/div&gt;
&lt;/body&gt;</code></pre>
<p><strong>style.css</strong></p>
<pre><code class="language-css">  *{
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

li {
      list-style: none;
}

body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100dvh;
    background-color: #f8f8f8;
}

.wrap{
    padding: 30px;
    width: 450px;
    min-height: 300px;
    height: fit-content;
    background-color: #fff;
    border-radius: 25px;
}

form {
    margin-top: 20px;
    width: 100%;
    display: flex;
    align-items: center;
    column-gap: 8px;
}

.addBtn {
    position: relative;
    font-size: 0;
    width: 35px;
    height: 35px;
    border: 0;
    background-color: #000;
}

.addBtn::before,
.addBtn::after {
    content: &#39;&#39;;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    background-color: #fff;
    border-radius: 5%;
}

.addBtn::before{
    width: 18px;
    height: 2px;
}
.addBtn::after{
    width: 2px;
    height: 18px;
}


.todoInput {
    flex: auto;
    border: 2px solid #000;
    padding: 8px;
}

.todoWrap {
    margin-top: 20px;
}

.todoWrap .todoItem:not(:first-of-type){
    border-top: 1px solid #eee;
}

.todoItem {
    padding: 10px;
    display: flex;
    column-gap: 8px;
    align-items: center;
}

.todoItem p {
    flex: auto
}

.checkBtn {
    width: 25px;
    height: 25px;
    border-radius: 100%;
    background-color: #fff;
    border: 2px solid #000;
    cursor: pointer;
}

.removeBtn {
    padding: 8px 10px;
    font-size: 16px;
    width: fit-content;
    border: 0;
    border-radius: 5px;
    cursor: pointer;
}

.todoItem.completed {
    background-color: #f8f8f8;
}

.todoItem.completed .checkBtn{
    position: relative;
    background-color: #ddd;
    border-color: #ddd;
}

.todoItem.completed p {
    text-decoration: line-through;
    color: #555;
}

.todoItem.completed .checkBtn:before{
    content:&#39;&#39;;
    position: absolute;
    left: 5%;
    top: 30%;
    transform: rotate(45deg) translate(0%, -50%);
    width: 6px;
    height: 10px;
    border-right: 3px solid #fff;
    border-bottom: 3px solid #fff;
    border-radius: 2px;
}
</code></pre>
<p>  <strong>index.js</strong></p>
<pre><code class="language-jsx">  &#39;use strict&#39;;

const todoInput = document.querySelector(&#39;.todoInput&#39;);
const addBtn = document.querySelector(&#39;.addBtn&#39;);
const todoWrap = document.querySelector(&#39;.todoWrap&#39;);

let todos = [
  {
    id: 1,
    text: &#39;코테 회고&#39;,
    completed: false,
  },
  {
    id: 2,
    text: &#39;방청소하기&#39;,
    completed: true,
  }
];

let nextId = todos.length;



const checkTodo = function(id) {
  let newTodos = todos.map(todo =&gt; {
    if(todo.id === id) {
      return {...todo,completed: !todo.completed}

    }else return todo
  })
  todos = newTodos;
  renderList();
}

const removeTodo = function(id) {
  let newTodos = todos.filter(todo =&gt; todo.id !== id);
  todos = newTodos;
  renderList();
}


// list render..!
const renderList = () =&gt; {
  todoWrap.innerHTML = &#39;&#39;;
  todos.forEach(todo =&gt; {
    let item = document.createElement(&#39;li&#39;);
    item.setAttribute(&#39;class&#39;,&#39;todoItem&#39;);

    let checkBtn = document.createElement(&#39;button&#39;);
    checkBtn.setAttribute(&#39;class&#39;,&#39;checkBtn&#39;);
    checkBtn.addEventListener(&#39;click&#39;,() =&gt; checkTodo(todo.id));

    let todoText = document.createElement(&#39;p&#39;);
    todoText.innerText = todo.text;

    let removeBtn = document.createElement(&#39;button&#39;);
    removeBtn.setAttribute(&#39;class&#39;,&#39;removeBtn&#39;);
    removeBtn.innerText = &#39;삭제&#39;;
    removeBtn.addEventListener(&#39;click&#39;, () =&gt; removeTodo(todo.id));

    todo.completed ? item.classList.add(&#39;completed&#39;) : item.classList.remove(&#39;completed&#39;)

    item.append(checkBtn);
    item.append(todoText);
    item.append(removeBtn);

    todoWrap.appendChild(item);
  });
}

renderList();

const createTodo = function(){
  if(todoInput.value === &#39;&#39;) {
    alert(&#39;할 일을 입력하세요&#39;);
  } else {
    let newItem = {
      id:  nextId + 1,
      text: todoInput.value,
      complted: false
    };

    // todos = [...todos,newItem];
    todos = todos.concat(newItem);

    // reset
    todoInput.value = &#39;&#39;;
    nextId += 1;

    // render list
    renderList();
  }
}


addBtn.addEventListener(&#39;click&#39;,(e)=&gt;{
  e.preventDefault();
  createTodo();
});

</code></pre>
<h2 id="p-styletext-aligncentermargin-bottom--30px✨완성✨p"><p style="text-align:center;margin-bottom: -30px"><strong>✨완성✨</strong></p></h2>
<p><img src="https://velog.velcdn.com/images/k_jihye92/post/db6d984a-c582-4bfc-b8d3-0fcc517a86e7/image.gif" width="150px" height="n%" style=""/><img src="" alt=""></p>
<blockquote>
<p>🤜⚡️🤛 강의만 따라하지 말기!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[⚠️ github page 404 error]]></title>
            <link>https://velog.io/@k_jihye92/github-page-404-error</link>
            <guid>https://velog.io/@k_jihye92/github-page-404-error</guid>
            <pubDate>Wed, 06 Mar 2024 02:52:39 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/k_jihye92/post/67ecd69f-a640-4f51-b984-faacb6b74e04/image.png" alt=""></p>
<h3 id="들어가며">들어가며!</h3>
<blockquote>
<p>vue로 만든 페이지를 github page로 배포
다른 페이지로 이동시 갑자기 404 에러가 떴다 🥲 (왜 또 갑자기?!)</p>
</blockquote>
<hr/>

<h3 id="발생원인">발생원인</h3>
<p>폭풍 구글링해보니 github page는 SPA를 지원안한다고...ㅜㅜ</p>
<p>갑자기 안된 이유는,
<code>/#/</code> 해쉬태그가 보기 싫어서 라우터 모드를 <strong>history</strong>로 바꾼게 원흉이었다.
<del>(업데이트 시간 전에 확인 했었는지 그땐 문제 없었지뭐람)</del></p>
<h3 id="🔨-해결방법">🔨 해결방법</h3>
<p>다시 해시모드로 BACK!하는게 젤 간단명료 했다.
일단 포트폴리오 목적의 사이트기도 했으니, ㅎ_ㅎ</p>
<p>다른 방법으로 해결하고싶다면! 아래 사이트 참고 plz👇</p>
<blockquote>
<p>ex. 
dist한 <code>index.html</code>를 복사해 <code>404.html</code>페이지로 둔갑
|| index.html를 리다이렉트하기...
|| BrowserRouter 에 basename 지정</p>
</blockquote>
<p>참고 사이트:</p>
<ul>
<li><a href="https://www.inflearn.com/questions/842189/%EC%83%88%EB%A1%9C%EA%B3%A0%EC%B9%A8%EC%8B%9C-404-%EB%AC%B8%EC%A0%9C">https://www.inflearn.com/questions/842189/%EC%83%88%EB%A1%9C%EA%B3%A0%EC%B9%A8%EC%8B%9C-404-%EB%AC%B8%EC%A0%9C</a></li>
<li><a href="https://github.com/rafgraph/spa-github-pages">https://github.com/rafgraph/spa-github-pages</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] props]]></title>
            <link>https://velog.io/@k_jihye92/React-props</link>
            <guid>https://velog.io/@k_jihye92/React-props</guid>
            <pubDate>Fri, 23 Feb 2024 03:28:35 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>props 는 properties 의 줄임말
 <strong>컴포넌트에게 어떠한 값을 전달</strong>해줘야 할 때, props 를 사용
 (부모 -&gt; 자식 컴포넌트에게 값을 전달할 때)</p>
</blockquote>
<h3 id="사용법">사용법</h3>
<p> App.js(상위)</p>
<pre><code class="language-jsx">    import React from &#39;react&#39;;
    import Welcome from &#39;./Welcome&#39;;

    function App() {
      return &lt;Welcome name=&quot;React&quot; color=&quot;blue&quot;/&gt;;
    }
</code></pre>
<p> Welocome.js(하위)</p>
<pre><code class="language-jsx">    import React from &#39;react&#39;;

    function Welcome(props) {
      return &lt;h1 style={{color: props.color}}&gt;Hello, {props.name}&lt;/h1&gt;;
    }

    export default Welcome;</code></pre>
<p> 전달된 값은 <code>props</code> 파라미터로 조회할수 있고, 객체형태로 전달된다</p>
<blockquote>
<p>만약, 여러개의 props 라면? 👇</p>
</blockquote>
<h4 id="비구조화할당구조분해할당">비구조화할당(구조분해할당)</h4>
<p>변수로 담아 바로 쓰기</p>
<p> Welocome.js</p>
<pre><code class="language-jsx">    import React from &#39;react&#39;;

    function Welcome({color, name}) {
      return &lt;h1 style={{color}}&gt;Hello, {name}&lt;/h1&gt;; //스타일 color: color -&gt;  color 로 축약 가능
    }

    export default Welcome;</code></pre>
<br/>
<hr/>

<h3 id="defatultprop">defatultProp</h3>
<p>prop의 기본값 정하기</p>
<p> Welocome.js</p>
<pre><code class="language-jsx">    import React from &#39;react&#39;;

    function Welcome({color, name}) {
      return &lt;h1 style={{color}}&gt;Hello, {name}&lt;/h1&gt;;
    }

    Welcome.defaultProps = {
        color: &quot;black&quot;
    }

    export default Welcome;</code></pre>
<br/>

<h3 id="prop-type">Prop-type</h3>
<p>prop의 type 정하기</p>
<blockquote>
<p>npm install --save prop-types</p>
</blockquote>
<p> Welocome.js</p>
<pre><code class="language-jsx">    import React from &#39;react&#39;;
    import PropTypes from &#39;prop-types&#39;;

    function Welcome({color, name}) {
      return &lt;h1 style={{color}}&gt;Hello, {name}&lt;/h1&gt;;
    }

    Welcome.defaultProps = {
        color: &quot;black&quot;
    }

    Welcome.propTypes = {
        color: PropTypes.string,
          name: PropTypes.string.isRequired, // 필수 값 isRequired 추가
    }

    export default Welcome;</code></pre>
<blockquote>
<p>더 자세한 type 지정 방법 
👉<a href="https://ko.legacy.reactjs.org/docs/typechecking-with-proptypes.html">https://ko.legacy.reactjs.org/docs/typechecking-with-proptypes.html</a></p>
</blockquote>
<br/>

<h3 id="propchildren">prop.children</h3>
<p>vue의 슬롯 같은 역할?
감싸고 있는 컴포넌트 값을 조회하기</p>
<p>Popup.js</p>
<pre><code class="language-jsx">    import React from &#39;react&#39;;


    function Popup({ children }) {
      const popup = {
           position: &quot;fixed&quot;,
        left: &#39;50%&#39;,
        top: &#39;50%&#39;,
        transform: &#39;translate(-50%,-50%)&#39;,
        width: &#39;200px&#39;,
        height: &#39;200px&#39;,
        ...
      };
      return (
          &lt;div style={popup}&gt;
          { cildren } // 하위 내용 보이게 prop으로 렌더링
        &lt;/div&gt;
      );
    }

    export default Popup;

</code></pre>
<p>App.js</p>
<pre><code class="language-jsx">    import React from &#39;react&#39;;
    import Welcome from &#39;./Welcome&#39;;
    import Popup from &#39;./Popup&#39;;

    function App() {
      return (
        &lt;Popup&gt;
          &lt;Welcome name=&quot;react&quot; color=&quot;red&quot;/&gt;
        &lt;/Popup&gt;
      );
    }
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[✏️ .insertBefore() method]]></title>
            <link>https://velog.io/@k_jihye92/.insertBefore-method</link>
            <guid>https://velog.io/@k_jihye92/.insertBefore-method</guid>
            <pubDate>Tue, 16 Jan 2024 14:33:49 GMT</pubDate>
            <description><![CDATA[<h2 id="insertbefore-란">.insertBefore() 란?</h2>
<p>참조된 요소 앞에 <strong>새로운 요소를 삽입</strong>하는 메소드이다.</p>
<br>

<h2 id="문법">문법</h2>
<pre><code class="language-java">    부모요소.insertBefore(삽입할요소,기준요소)</code></pre>
<ol>
<li><code>부모요소</code> : 삽입할 요소와 기준요소의 상위요소이다.</li>
<li><code>삽입할요소</code> : 삽입하려는 요소를 넣어준다.</li>
<li><code>기준요소</code> :  <code>삽입할요소</code>가 들어갈 위치의 기준이되는 요소로 <code>삽입할요소</code>는 <code>기준요소</code> <strong>바로 앞에 추가</strong> 된다. </li>
</ol>
<blockquote>
<p>단, 기준요소가 <code>null</code> 값일 때는 <code>삽입할요소</code> 는 부모 요소의 <u>마지막 자식으로 추가</u>된다.</p>
</blockquote>
<h2 id="예제">예제</h2>
<h3 id="📍-코드">📍 코드</h3>
<pre><code class="language-html">&lt;div
      class=&quot;parentElem&quot;
      style=&quot;border: 1px solid #222; width: 400px;&quot;
    &gt;
      &lt;p class=&quot;standardElem&quot;&gt;제 바로 위에 형제요소를 만들어 주세요&lt;/p&gt;

    &lt;script&gt;
      const parentEl = document.querySelector(&quot;.parentElem&quot;);
      const standardEl = document.querySelector(&quot;.standardElem&quot;);
      const newEl = document.createElement(&quot;p&quot;);
      newEl.style.color = &quot;red&quot;;
      newEl.innerText = &quot;안녕하세요 새로 추가된 요소입니다.&quot;;

      parentEl.insertBefore(newEl, standardEl);
    &lt;/script&gt;
</code></pre>
<h3 id="📍-결과화면">📍 결과화면</h3>
<img src="https://velog.velcdn.com/images/k_jihye92/post/b990bf8c-241d-43ce-9fe8-c2820f82a1d1/image.png" width="400" height="400"/>

<blockquote>
<p>아래 링크로 insertBefore() method를 적용한 <strong>drag and drop</strong> 예제를 확인할 수있습니다.
👉 <a href="https://codesandbox.io/p/sandbox/drag-list-7y4zc9?layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522clrenly8x00063b6flhwzi923%2522%252C%2522sizes%2522%253A%255B70%252C30%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522clrenly8x00023b6fiw7mux4v%2522%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522clrenly8x00033b6fnnu0d573%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522clrenly8x00053b6fbsqn02gg%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%252C%2522sizes%2522%253A%255B52.8027918434378%252C47.1972081565622%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522clrenly8x00023b6fiw7mux4v%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clrenly8x00013b6fbiwb3pbw%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252Findex.html%2522%252C%2522state%2522%253A%2522IDLE%2522%252C%2522initialSelections%2522%253A%255B%257B%2522startLineNumber%2522%253A9%252C%2522startColumn%2522%253A42%252C%2522endLineNumber%2522%253A9%252C%2522endColumn%2522%253A42%257D%255D%257D%255D%252C%2522id%2522%253A%2522clrenly8x00023b6fiw7mux4v%2522%252C%2522activeTabId%2522%253A%2522clrenly8x00013b6fbiwb3pbw%2522%257D%252C%2522clrenly8x00053b6fbsqn02gg%2522%253A%257B%2522id%2522%253A%2522clrenly8x00053b6fbsqn02gg%2522%252C%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clrenly8x00043b6fdndrfrja%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522UNASSIGNED_PORT%2522%252C%2522port%2522%253A0%252C%2522path%2522%253A%2522%252F%2522%257D%255D%252C%2522activeTabId%2522%253A%2522clrenly8x00043b6fdndrfrja%2522%257D%252C%2522clrenly8x00033b6fnnu0d573%2522%253A%257B%2522tabs%2522%253A%255B%255D%252C%2522id%2522%253A%2522clrenly8x00033b6fnnu0d573%2522%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Atrue%252C%2522showSidebar%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A15%257D">실전적용예제</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[✏️비동기 작업처리: callback, Promise, async/await 차이점]]></title>
            <link>https://velog.io/@k_jihye92/%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%9E%91%EC%97%85%EC%B2%98%EB%A6%AC-callback-Promise-asyncawait-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
            <guid>https://velog.io/@k_jihye92/%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%9E%91%EC%97%85%EC%B2%98%EB%A6%AC-callback-Promise-asyncawait-%EC%B0%A8%EC%9D%B4%EC%A0%90</guid>
            <pubDate>Sun, 30 Jul 2023 12:19:58 GMT</pubDate>
            <description><![CDATA[<p>비동기 작업을 처리할 때 방법으로 <code>callback</code>, <code>Promise</code>, <code>async/await</code>가 있다.</p>
<h2 id="callback">callback</h2>
<p>우선 <code>callback</code>은 다른 함수가 실행되고 나서 실행되는 함수로, 매개변수를 변수가 아닌 함수로 받는다. 함수 내부에서 실행된다. 간단한 작업에서는 상관없지만 비동기 작업을 많이할 경우 코드가 복잡해진다.(소위 <strong>콜백지옥</strong>👿)</p>
<p>대신, Es6에 문법으로 추가된 <code>promise</code>를 사용할 수있다.</p>
<h2 id="promise">Promise</h2>
<p>resolve(성공)와 reject(실패)를 파라미터로 받아와야한다
내부에 <code>.catch()</code>를 통해 에러도 잡을수 있다.</p>
<pre><code class="language-javascript">
const myPromise = new Promise((resolve, reject) =&gt; {
    setTimeOut(() =&gt; {
        //resolve(&#39;result&#39;);
        reject(new Error());
    },1000)
})

myPromise.then(result =&gt; {
    console.log(result) // result
}).catch(e =&gt; {
    console.error(e);
})</code></pre>
<blockquote>
<p>📌 <strong>callback과 차이점과 단점?</strong>
<code>callback</code>은 중첩된 구조라 코드의 깊이가 깊어지지만 <code>promise</code>는 <code>.then()</code>과 <code>.catch()</code>를 사용해 깊어지지 않고 연달아 작성 가능하다. 비교적 난잡하지 않다.
<strong>하지만</strong>, 에러를 잡을때 어떤 부분에서인지 파악하기 어렵고, 특정 조건에 따라 분기를 나누기 번거롭다. 또, 특정값을 공유해가면서 작업을 연달아하기 번거롭다고 한다.</p>
</blockquote>
<h2 id="asyncawait">async/await</h2>
<p>위 단점을 보완한 async/await을 사용한다.
Es8에 추가된 문법이다.</p>
<pre><code class="language-javascript">function sleep(ms) {
    return new Promise(resolve =&gt; setTimeout(resolve,ms));
}

async function process(){
    console.log(&#39;시작&#39;);
    await sleep(1000);
    console.log(&#39;인사&#39;);
}

process();</code></pre>
<p>실행할 함수 앞에 <strong>async</strong>, 실행할 promise 앞에 <strong>await</strong>를 붙여주면 된다.
위 <code>promise</code> 코드처럼 <code>.then()</code>로 넘어가지 않고 함수내부에서 <code>promise</code>를 기다려서 분기점을 나누기 용이하다. 하지만 <code>.catch()</code>같이 에러를 핸들링하지 못해 error를 함수로 던저 <code>try-catch</code>구조로 잡아야한다.</p>
<pre><code class="language-javascript">function sleep(ms) {
    return new Promise(resolve =&gt; setTimeout(resolve,ms));
}
async function makeError() {
    await sleep(1000);
    cont err = new Error();
    throw error; // 에러 던지기
}
async function process() {
    try {
        await makeError();
    } catch (e) {
        console.error(e)
    }
}

process();
</code></pre>
<h3 id="추가로">+추가로,</h3>
<p>여러개 promise 동시에 처리할 수 있는,
<code>Promise.all()</code>
배열에 프로미스들을 등록해주고 <code>Promise.all</code>로 감싸 실행한다. 
단, 셋중에 하나라도 에러가 발생하면 전체 에러로 나타난다.</p>
<p><code>promise.race()</code>
방법은 동일하지만, 등록한 프로미스 중에 가장 빨리 실행되는것 만 나타나게할 때 사용한다.
단, 가장 빨리 끝난게 에러가 나면 에러지만 나머지에서 에러나면 에러로 간주하지 않는다.</p>
<hr>
<p>참고 : 패스트캠퍼스 강의 참고(벨로퍼트와 함께 하는 모던 자바스크립트 : 자바스크립트에서 비동기 처리 다루기 - 김민준 강사)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[⚠️[vsCode] Extension host terminated unexpectedly]]></title>
            <link>https://velog.io/@k_jihye92/vsCode-Extension-host-terminated-unexpectedly</link>
            <guid>https://velog.io/@k_jihye92/vsCode-Extension-host-terminated-unexpectedly</guid>
            <pubDate>Mon, 05 Dec 2022 04:02:45 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<blockquote>
<p>🚫 Extension host terminated unexpectedly</p>
</blockquote>
<p>위와 같이 토스트 팝업이 뜨면서.. 탭이 안된다.</p>
<p>처음에는 sass를 npm 설치하고 생겨서 sass 때문인가 했는데,아니었고 
emmet 문제도 아니였다. 
검색결과 vscode에 설치한 extension이 문제‼️</p>
<h3 id="🔨-해결방법">🔨 해결방법</h3>
<ol>
<li><p>vscode에서 명령창 열기<img src="https://velog.velcdn.com/images/k_jihye92/post/c39c3ad8-34a0-4026-a58b-c53e83d8763d/image.png" alt=""></p>
<ul>
<li>윈도우라면 <code>ctrl</code>+<code>shift</code>+<code>p</code></li>
<li>맥이라면  <code>command</code>+<code>shift</code>+<code>p</code></li>
</ul>
</li>
<li><p>&#39;Start the Extension Bisect&#39; 를 쳐서 실행한다.</p>
<img src="https://velog.velcdn.com/images/k_jihye92/post/4d4817f4-f879-491e-9840-8773e80e8de8/image.png" width="200" style="display:inline"/>
</li>
<li><p>정상적으로 작동할 경우, 하단에 토스트 팝업 확인한다.
<img src="https://velog.velcdn.com/images/k_jihye92/post/8b98a2cb-95ec-48c3-bd7b-479a0213a969/image.png" alt=""></p>
</li>
</ol>
<ul>
<li>여전히 문제라면 &quot;This is bad&quot;를 </li>
<li>정상으로 작동한다면 &quot;Good Now&quot;를 누르고 &quot;Stop Bisect&quot;를 누른다.<br />

</li>
</ul>
<h3 id="마치며">마치며</h3>
<p>내 경우에는 live sass compiler란 익스텐션과 live server 익스텐션 때문인거였고, live sass compiler를 disabled 하니 되었다</p>
<br />
<br />

<p>참고사이트</p>
<ul>
<li><a href="https://stackoverflow.com/questions/70052961/error-extension-host-terminated-unexpectedly-in-vs-code">https://stackoverflow.com/questions/70052961/error-extension-host-terminated-unexpectedly-in-vs-code</a></li>
<li><a href="https://github.com/microsoft/vscode/issues/139280">https://github.com/microsoft/vscode/issues/139280</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[⚠️Error in render: "TypeError: Cannot read properties of undefined (reading 'matched')]]></title>
            <link>https://velog.io/@k_jihye92/Error-in-render-TypeError-Cannot-read-properties-of-undefined-reading-matched</link>
            <guid>https://velog.io/@k_jihye92/Error-in-render-TypeError-Cannot-read-properties-of-undefined-reading-matched</guid>
            <pubDate>Thu, 06 Oct 2022 03:38:32 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<p>vue 공부를 친구와 같이하는데, 친구도 똑같이 router를 npm으로 설치하고 쓰려는데, 
두둥... 에러가났다. 다 똑같이 했는데,,, 뭐 때문이지 하면서 알아봤는데.. 
여기서? 오류가 발생한다고? 싶었던거라 기록하려한다.</p>
<h3 id="발생-이유">발생 이유</h3>
<p>main.js에서 router를 불러올 때, 글자 그대로 명시하지 않았을 경우</p>
<h3 id="🔨-해결-방법">🔨 해결 방법</h3>
<p>new Vue로 생성할때, router관한 key와 value를 똑같이해주지 않는다면 발생한다.</p>
<blockquote>
<p>친구의 경우는 앞에 대문자로 Router로 명시해서 에러가 발생했다. </p>
</blockquote>
<ol>
<li>router로 똑같이해주기<pre><code class="language-java">import router from &#39;./router&#39;
new Vue({
render: h =&gt; h(App),
router,
}).$mount(&#39;#app&#39;)
</code></pre>
</li>
</ol>
<pre><code>2. router: 
```java
import Router from &#39;./router&#39;
new Vue({
  render: h =&gt; h(App),
  router: Router,
}).$mount(&#39;#app&#39;)
</code></pre><br/>

<p>참고사이트</p>
<ul>
<li><a href="https://wotres.tistory.com/entry/vue-error-%ED%95%B4%EA%B2%B0%EB%B2%95-Error-in-render-TypeError-Cannot-read-property-matched-of-undefined">https://wotres.tistory.com/entry/vue-error-%ED%95%B4%EA%B2%B0%EB%B2%95-Error-in-render-TypeError-Cannot-read-property-matched-of-undefined</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[⚠️fatal: not a git repository (or any of the parent directories): .git]]></title>
            <link>https://velog.io/@k_jihye92/fatal-not-a-git-repository-or-any-of-the-parent-directories-.git</link>
            <guid>https://velog.io/@k_jihye92/fatal-not-a-git-repository-or-any-of-the-parent-directories-.git</guid>
            <pubDate>Tue, 13 Sep 2022 03:32:57 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<p>작업중인 개인프로젝트를 github에서 관리하려했다.
GitHub에 레퍼지토리를 생성하고 remote 하려는데...!</p>
<p><img src="https://velog.velcdn.com/images/k_jihye92/post/8aeae1e6-7762-4ce9-874a-9db9eaf2c152/image.png" alt=""></p>
<p>위와 같은 에러가 발생했다.🥺</p>
<h3 id="발생-이유">발생 이유</h3>
<ol>
<li><p>명령을 실행하려고 했지만 git 리포지토리가 있는 프로젝트 폴더로 이동하지 않았을 경우</p>
</li>
<li><p>프로젝트 디렉터리에 있지만 해당 프로젝트 폴더에 대한 Git 저장소를 초기화하지 않았을 경우</p>
</li>
</ol>
<blockquote>
<p>나의 경우는 두 번째 ㅎ..ㅎ</p>
</blockquote>
<h3 id="🔨-해결-방법">🔨 해결 방법</h3>
<ol>
<li><p>command line에서 <code>cd</code>를 사용해 해당 디렉토리로 이동!</p>
</li>
<li><p><code>git init</code> 명령어 실행으로 프로젝트 디렉토리에 .git 폴더 생성!</p>
</li>
</ol>
<br/>

<p>참고사이트</p>
<ul>
<li><a href="https://komodor.com/blog/solving-fatal-not-a-git-repository-error/">https://komodor.com/blog/solving-fatal-not-a-git-repository-error/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[display flex 에도 gap을?]]></title>
            <link>https://velog.io/@k_jihye92/display-flex-%EC%97%90%EB%8F%84-gap%EC%9D%84</link>
            <guid>https://velog.io/@k_jihye92/display-flex-%EC%97%90%EB%8F%84-gap%EC%9D%84</guid>
            <pubDate>Sun, 11 Sep 2022 15:38:24 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<p>목록화면을 퍼블리싱 작업할 때, 종종 display: grid를 사용한다. 여백을 잡을 때, 특히 gap을 사용해 열과 행 사이의 간격을 컨트롤하기 쉬웠기 때문이다. 근데 display: flex에서도 가능한 줄 몰랐어서 찾아보고 기록 남기려한다.
<br/></p>
<h3 id="사용-방법">사용 방법</h3>
<h4 id="1-사용-조건">1. 사용 조건</h4>
<p>인접한 요소가 있을 때, gap 스타일 속성으로 갭을 만든다.</p>
<pre><code>적용대상 : multi-column elements, flex containers, grid containers</code></pre><p>❓multi-column (다중 칼럼)
column-count 속성을 사용해 몇개의 단으로 레이아웃을 나눌 경우,
그 사이의 간격을 column-gap 뿐만아니라 gap 속성으로도 조정할수있다. </p>
<pre><code class="language-css">column-count: 3;
gap: 20px;</code></pre>
<p><img src="https://velog.velcdn.com/images/k_jihye92/post/99abaaf4-cbd6-4c87-a2b8-2df456c9227b/image.png" alt=""></p>
<h4 id="2-사용-형식">2. 사용 형식</h4>
<pre><code class="language-css">
gap: 4em 10px
/* gap = &lt;&#39;줄간격&#39;&gt; &lt;&#39;열 간격&#39;&gt; */ 
</code></pre>
<br/>

<h3 id="브라우저-호환성">브라우저 호환성</h3>
<p>현재 muluti-column에 대한 gap 스타일 속성이 일부만 지원하는 것 외에 flex-box와 grid은 모든 브라우저에서 지원된다. 자세한 내용은 아래 참고사이트 목록 중 첫 번째 클릭!</p>
<p>참고사이트</p>
<ul>
<li><a href="https://developer.mozilla.org/ko/docs/Web/CSS/gap#%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80_%ED%98%B8%ED%99%98%EC%84%B1">https://developer.mozilla.org/ko/docs/Web/CSS/gap#%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80_%ED%98%B8%ED%99%98%EC%84%B1</a></li>
<li><a href="https://webisfree.com/2020-12-05/%5Bcss%5D-%EC%8A%A4%ED%83%80%EC%9D%BC%EC%86%8D%EC%84%B1-gap%EC%9D%84-flex%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0">https://webisfree.com/2020-12-05/[css]-%EC%8A%A4%ED%83%80%EC%9D%BC%EC%86%8D%EC%84%B1-gap%EC%9D%84-flex%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>