<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>vanesa_kim.log</title>
        <link>https://velog.io/</link>
        <description>이건 대체 어떻게 만든 거지?</description>
        <lastBuildDate>Sun, 04 Jan 2026 08:28:38 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>vanesa_kim.log</title>
            <url>https://velog.velcdn.com/images/vanesa_kim/profile/552118d3-3eda-4c63-8e73-f02fdb9b4fc5/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. vanesa_kim.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/vanesa_kim" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[클린 아키텍처 적용하기]]></title>
            <link>https://velog.io/@vanesa_kim/%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@vanesa_kim/%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 04 Jan 2026 08:28:38 GMT</pubDate>
            <description><![CDATA[<p>원래는 클라이언트 코드를 만졌었는데 어쩌다 보니 어드민쪽을 계속 다루게 되어서 어드민쪽에 먼저 적용했다. (클라쪽에 적용하려고 낑낑대며 쓰던 글은 임시글 어딘가에..) 
물론 코드 봐주는 사람이 없어서 나 혼자 낑낑대면서 한거라 제대로 한지는 모르겠다😥</p>
<p>클린아키텍쳐란?
클린 아키텍처는 소프트웨어 설계 원칙 중 하나로, 의존성 방향을 안쪽(비즈니스 로직)으로 향하게 강제하여 유지보수성, 확장성, 테스트 용이성을 높이는 구조이다.</p>
<p>클린 아키텍쳐는 다음을 지켜야 한다.</p>
<ol>
<li>의존성 방향 제어</li>
</ol>
<ul>
<li>UI는 도메인을 알 수 있지만, 도메인은 UI를 몰라야 한다.</li>
</ul>
<ol start="2">
<li>관심사의 분리 (Separation of Concerns)</li>
</ol>
<ul>
<li>상태 관리, 유효성 검사, API 호출, UI는 모두 서로 다른 계층에 위치</li>
</ul>
<ol start="3">
<li>확장성과 테스트 용이성 확보</li>
</ol>
<ul>
<li>특정 도메인의 UI만 바꾸거나 API만 바꿀 때, 다른 부분은 영향을 받지 않음.</li>
<li>테스트 시에도 각각의 훅, 유효성 검사기, 서비스 함수 등을 단위 테스트하기 쉬움.</li>
</ul>
<p>클린 아키텍쳐는 보통 네 가지 계층으로 나뉜다.</p>
<ol>
<li>Entities / Models: 순수한 비즈니스 규칙 / 데이터 구조</li>
</ol>
<ul>
<li>어플리케이션의 핵심 데이터 구조</li>
<li>외부 시스템 (API, DB, UI) 에 전혀 의존하지 않는 순수한 도메인 객체</li>
</ul>
<ol start="2">
<li>Use Cases / Business Logic: 도메인 로직 / 유즈케이스 수행 단위</li>
</ol>
<ul>
<li>사용자의 의도를 수행하기 위한 절차와 규칙
예: 공고 작성 로직, 유효성 검사, 폼 상태 관리 등</li>
</ul>
<ol start="3">
<li>Interface Adapters: UI, API 호출, Presenter, Formatter 등</li>
</ol>
<ul>
<li>외부와 내부 도메인 계층을 이어주는 다리 역할</li>
<li>React 컴포넌트, 훅, API 서비스, 포맷터, 상태 어댑터 등이 포함됨</li>
</ul>
<ol start="4">
<li>Frameworks &amp; Drivers: 프레임워크, 라이브러리, DB, Next.js 등</li>
</ol>
<ul>
<li>실제 애플리케이션을 구동하는 외부 기술 요소</li>
<li>도메인 로직이 직접적으로 의존하지 않도록 해야 함</li>
</ul>
<p>나는 어떻게 적용했는가</p>
<ol>
<li>Entities / Models (도메인 모델)</li>
</ol>
<p>-&gt; /src/model/dto
클린 아키텍처에서 가장 안쪽 계층은 비즈니스 핵심 데이터 구조이다.
보통 백엔드에서는 Entity, Value Object, DTO, ViewModel 등으로 구분하지만,
프론트엔드에서는 DB와 직접 연결된 Entity보다는 API 요청/응답 시 사용하는 DTO 위주로 다루게 된다고 한다. 그래서 일단 폴더명을 model이라고 했고, 안에 dto라는 폴더를 두었다. 아직 다른 폴더를 둘 필요성은 못 느껴서 이렇게만 해두었는데 다른 부분에도 클린 아키텍쳐 적용하다 보면 다른 폴더도 추가될 수도 있다.</p>
<pre><code class="language-javascript">export interface PostRecruitmentRequest {
  paginationRequestDto: {
    page: number
    count: number
  }
  sort?: string
  ...</code></pre>
<p>cf.
entities: DB와 직접 매핑되는 객체 (백엔드 중심)
dto: 데이터 전송 객체, API 요청/응답 스펙 정의 (프론트 중심)
viewModel: 뷰에 맞게 가공된 데이터 (ex: 날짜 포맷팅 등 포함)
mappers: dto ↔ viewModel 간 변환 함수</p>
<ol start="2">
<li>Use Cases / Business Logic (도메인 로직)</li>
</ol>
<p>-&gt; /src/domains
처음에는 use-cases라고 했었는데 domains로 바꾸게 되었다. use-cases는 processOrder, calculateTax 등 단일 기능을 수행하는 단일 책임 함수를 담당하는 폴더기 때문이다. 
그런데 지금 클린 아키텍쳐를 가장 먼저 도입한 페이지에는 단일 책임 함수를 만들 만한 것이 없는 것 같아서 domain으로 했다.  그래서 그냥 그 안에 career를 두고 그 안에 hooks랑 validator를 넣어서 사용자가 입력한 폼의 상태가 올바른지 확인하는 로직을 넣었다.</p>
<pre><code class="language-javascript">export const validateCareerDates = (start: string, end: string) =&gt; {
  ...
  return new Date(start) &lt; new Date(end)
}</code></pre>
<ol start="3">
<li>Interface Adapters (UI/Hook/서비스/포맷터 등)</li>
</ol>
<p>-&gt; /src/components, /src/services, /src/hooks
/src/components: ui컴포넌트들
/src/services: api 호출 함수들, 백엔드 연동 함수들
/src/hooks: 여기저기서 공통적으로 사용될 수 있는 api훅들, 상태 관리들
이 계층은 외부 시스템과 내부 로직을 연결해주는 다리 역할을 하며, 가장 많이 바뀌고, 테스트하기 쉬운 계층이다.</p>
<ol start="4">
<li>Frameworks &amp; Drivers (Next.js, DB, 라이브러리 등)</li>
</ol>
<p>-&gt; /pages 등
라우팅 등을 하는 곳이다.</p>
<p>제대로 하고 있는 것이 맞길 바란다.</p>
<hr>
<p>🧼 클린 아키텍처란?</p>
<p>클린 아키텍처는 소프트웨어 설계 원칙 중 하나로, 의존성의 방향을 안쪽으로 향하게 강제하여
유지보수성, 확장성, 테스트 용이성을 높이는 구조입니다.</p>
<p>핵심 원칙: 외부 계층은 내부 계층을 참조할 수 있지만, 내부 계층은 외부에 의존해서는 안 됩니다.</p>
<p>🔍 주요 계층 구조</p>
<p>Frameworks &amp; Drivers      ➡    /pages, next.js
     ↑
Interface Adapters        ➡    /components, /services
     ↑
Use Cases / Domains       ➡    /domains/*
     ↑
Entities / Models         ➡    /model/dto</p>
<ol>
<li>Entities (=Models)</li>
</ol>
<p>어플리케이션의 핵심 개념과 데이터를 정의하는 계층입니다.</p>
<p>순수한 비즈니스 규칙과 핵심 데이터 구조</p>
<p>외부 시스템 (API, DB, UI) 에 전혀 의존하지 않는 순수한 도메인 객체</p>
<p>순수 JavaScript로 동작 가능한 형태로 작성</p>
<ol start="2">
<li>Use Cases (=Business Logic)</li>
</ol>
<p>실제로 사용자의 요청이나 행동을 처리하는 로직을 담당합니다.</p>
<p>ex. 공고 작성, 날짜 유효성 검사 등</p>
<p>Entities를 이용하여 실제 동작을 구현</p>
<ol start="3">
<li>Interface Adapters</li>
</ol>
<p>UI, API 호출, Formatter 등</p>
<p>외부(프레임워크, UI, API 등)와 내부 도메인 계층을 이어주는 다리 역할</p>
<p>UI/UX에 따라 자주 바뀔 수 있으므로, 도메인 로직과의 결합도를 낮추는 것이 중요함</p>
<ol start="4">
<li>Frameworks &amp; Drivers</li>
</ol>
<p>프레임워크, 라이브러리, DB, Next.js 등</p>
<p>ex. /pages, _app.tsx, _document.tsx 등</p>
<p>실제 애플리케이션을 구동하는 외부 기술 요소</p>
<p>가장 바깥에 위치하며, 내부 도메인에 영향을 주지 않아야 함</p>
<p>✅ Admin에 적용한 클린 아키텍처</p>
<p>1️⃣ Entities / Models (도메인 모델 계층)</p>
<p>📁/src/model/dto</p>
<p>보통 백엔드에서는 Entity, Value Object, DTO, ViewModel 등으로 구분하지만, 
프론트엔드에서는 DB와 직접 연결된 Entity보다는 API 요청/응답 시 사용하는 DTO 위주로 다루게 됩니다.</p>
<p>이에 model이라는 이름을 사용했고, 그 하위에 dto 폴더만 구성했습니다.</p>
<p>model 안에는 viewModels, mapper 등이 들어갈 수도 있다고 합니다. 필요 시 확장 가능합니다.</p>
<p>/src/model/dto/career/CareerDto.ts
export interface PostRecruitmentRequest {
  paginationRequestDto: {
    page: number
    count: number
  }
  sort?: string
  ...
}</p>
<p>2️⃣ Use Cases / Business Logic (도메인 로직 계층)</p>
<p>📁 /src/domains</p>
<p>처음에는 /use-cases라는 폴더명을 고민했지만, 단일 책임 함수가 아니라 도메인 단위로 로직을 관리하고 싶어서 /domains로 이름을 지었습니다..</p>
<p>예를 들어 career 도메인 안에는 사용자 입력 폼에 대한 상태 관리 훅, 날짜 유효성 검사기 등이 들어 있습니다.</p>
<p>/src/domains/career/hooks/useNewCareerWriteForm.hook.ts
const [formData, setFormData] = useState({ ... })
const handleChange = (key, value) =&gt; {
  setFormData(prev =&gt; ({ ...prev, [key]: value }))
}</p>
<p>/src/domains/career/validators/validateCareerDates.ts
export const validateCareerDates = (start: string, end: string) =&gt; {
  return new Date(start) &lt; new Date(end)
}</p>
<p>3️⃣ Interface Adapters (UI/API 서비스 등)</p>
<p>📁 /src/components, /src/services, /src/hooks</p>
<p>/components: 순수 UI 컴포넌트, 입력 필드, 테이블 등</p>
<p>...
const SelectedJobTags = ({ selected, onRemove, options }: Props) =&gt; {
  if (!selected.length) return null</p>
<p>  return (
    <Box display="flex" flexWrap="wrap" mt={1}>
      {selected.map((uuid) =&gt; (
        &lt;Chip
          key={uuid}
          label={findJobLabel(uuid, options)}
          onDelete={() =&gt; onRemove(uuid)}
          color=&quot;primary&quot;
        /&gt;
      ))}
    </Box>
  )
}
...</p>
<p>/services: API 호출 모듈, 백엔드 연동 함수</p>
<p>/src/service/career/getWriteNewCareerSSRData.ts
...생략
  const fetchRegisterOptions = async () =&gt; {
    const fetchPromises = CHECKBOX_FIELDS.map((field) =&gt; {
      const type = fieldToTypeMapping[field]
      return getRegisterOptions(accessToken, type)
        .then((res) =&gt; ({ field, options: res?.[type] || [] }))
        .catch(() =&gt; ({ field, options: [] }))
    })</p>
<pre><code>const results = await Promise.all(fetchPromises)
return results.reduce(
  (acc, { field, options }) =&gt; {
    acc[field] = options
    return acc
  },
  { jobUuids: [], educationLevels: [], region: [], deadlineType: [] },
)</code></pre><p>  }</p>
<p>  const fetchCompanyList = async () =&gt; {
    const res = await getRecruitmentCompany(accessToken)
    return res?.length ? res : []
  }</p>
<p>  const [registerOptions, companyList] = await Promise.all([
    fetchRegisterOptions(),
    fetchCompanyList(),
  ])
...생략</p>
<p>/hooks: 여러 도메인에서 재사용 가능한 hook들. /domain 내의 hooks는 그 도메인 내에서만 사용됨</p>
<p>도메인 로직에 직접적으로 의존하지 않도록 합니다. 추후에 단위테스트를 진행할 경우 테스트의 대상이 될 가능성이 높습니다.</p>
<p>4️⃣ Frameworks &amp; Drivers (Next.js, 외부 라이브러리)</p>
<p>📁 /pages, 외부 패키지</p>
<p>Next.js의 페이지 라우팅 구조 (/pages, _app.tsx, _document.tsx)</p>
<p>SSR 처리, 쿠키/세션, 전역 레이아웃 등 프레임워크 중심 처리</p>
<p>외부 라이브러리 (/lib, canvasjs, slick-carousel 등)도 해당 계층에 포함</p>
<p>도메인 로직에는 영향을 주지 않도록 합니다.</p>
<p>현재 Admin 일부에만 위와 같이 클린 아키텍처가 적용되었습니다. 구조적으로 완벽하지 않거나 일관성이 떨어지는 부분도 있으나 점차적으로 개선해 나갈 예정입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[컴포넌트 파일명 index.tsx와 index.styles.ts 파일명 이름 문제]]></title>
            <link>https://velog.io/@vanesa_kim/%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%ED%8C%8C%EC%9D%BC%EB%AA%85-index.tsx%EC%99%80-index.styles.ts-%ED%8C%8C%EC%9D%BC%EB%AA%85-%EC%9D%B4%EB%A6%84-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@vanesa_kim/%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%ED%8C%8C%EC%9D%BC%EB%AA%85-index.tsx%EC%99%80-index.styles.ts-%ED%8C%8C%EC%9D%BC%EB%AA%85-%EC%9D%B4%EB%A6%84-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Sun, 04 Jan 2026 08:28:08 GMT</pubDate>
            <description><![CDATA[<p>컴포넌트 폴더 하에 index.tsx랑 index.styles.ts을 두었는데 이는 좋은 파일 이름이 아니라는 피드백을 받았다.</p>
<p>난 이렇게 살아왔는데
<img src="https://velog.velcdn.com/images/vanesa_kim/post/1a3064c7-b157-487f-8ed1-762f960371df/image.png" alt=""></p>
<p>역시 실무자의 피드백은 소중하다.</p>
<p>안 좋은 이유</p>
<ol>
<li>명확하지 않은 파일명으로 인한 혼동 
만약 여러 개의 컴포넌트를 index.tsx로 두면 컴포넌트와 스타일 파일을 import할 때 이름 충돌이 발생할 수도 있다. 나는 그 동안 이런 문제가 한 번도 발생하지 않아서 이게 무슨 말인지 이해도 안 간다. 
gpt의 힘을 빌려서 이해해보았다.</li>
</ol>
<pre><code>다른 사람은 index.tsx가 {컴포넌트명}라는 컴포넌트를 대표한다는 것을 바로 알기 어려울 수 있다.</code></pre><pre><code>여러 컴포넌트를 포함하는 폴더에서 사용할 경우, 어떤 컴포넌트를 지칭하는지 명확하지 않을 수 있어 혼동을 야기할 수 있다. 따라서 파일명에 컴포넌트 이름을 명시하는 것이 좋다.</code></pre><p>예를 들어 아래와 같은 경우</p>
<pre><code class="language-javascript">/components
   /user
      /userInput
           /index.tsx
      /userList
           /index.tsx</code></pre>
<p>이런 구조에서 /userInput/index.tsx와 /userList/index.tsx는 각각 다른 컴포넌트를 담당하는 파일이다. 그런데 이렇게 index.tsx로 파일명을 정하면, 나중에 파일명만 보아서는 이 파일들이 무엇을 하는지 즉시 이해할 수 없다. </p>
<p>나는 맨날 아래와 같이 만들어서 파일명 말고 컴포넌트 폴더명을 확인하는 것이 습관이 되어서 뭐가 불편한지 이해가 안 갔는데 이젠 알겠다.</p>
<pre><code class="language-javascript">components/
  UserInfoArea/
    index.tsx
    index.styles.ts
  UserInput/
    index.tsx
    index.styles.ts</code></pre>
<p>gpt도 이렇게 하면 혼동 가능성이 거의 없다고는 한다. 그래도 현직자분이 이런 경우는 본 적이 없다고 하셔서 일반적인 컨벤션을 따르기로 했다. 나도 처음에 이렇게 만들고 나서 이게 맞나 생각한 적이 있기 때문..</p>
<ol start="2">
<li>확장성이 떨어짐
복잡한 컴포넌트가 많아질수록 모든 컴포넌트 파일을 index.tsx와 index.styles.ts로 묶어두면 폴더가 많아지고 복잡해지며 확장성이 떨어질 수 있다.
ex. 각 폴더에 다양한 파일(테스트 파일, 유틸리티 파일 등)이 추가되면, index.tsx와 index.styles.ts 외에도 다른 파일들이 함께 존재할 때 그 파일들이 어떤 역할을 하는지 명확히 구분하기 어려울 수 있다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[무작정 gpt를 쓰면 안되는 이유]]></title>
            <link>https://velog.io/@vanesa_kim/%EB%AC%B4%EC%9E%91%EC%A0%95-gpt%EB%A5%BC-%EC%93%B0%EB%A9%B4-%EC%95%88%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@vanesa_kim/%EB%AC%B4%EC%9E%91%EC%A0%95-gpt%EB%A5%BC-%EC%93%B0%EB%A9%B4-%EC%95%88%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sun, 04 Jan 2026 08:25:55 GMT</pubDate>
            <description><![CDATA[<p>Type &#39;() =&gt; JSX.Element&#39; is not assignable to type &#39;ReactNode&#39;
에러가 40군데 가량에서 남</p>
<p>타입스크립트 설정 (jsx: react-jsx 설정)
React 타입 정의 (@types/react 최신 버전으로 설치)
MUI 관련 패키지 (@mui/material, @mui/system 업데이트)
react-scripts 및 관련 패키지 업데이트
캐시 삭제 및 VSCode 재시작
을 하라 함 근데 1번 했는데도 해결 안됨
뭔가 간단한 거 하나만 하면 해결될거 같아서 구글링했음</p>
<p><img src="https://velog.velcdn.com/images/vanesa_kim/post/5471ee72-6cc4-4261-ad72-5a2e60c77503/image.png" alt="">
이거 하나로 해결됨</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[입력값 매우 많은 페이지 리팩토링& 성능개선 ]]></title>
            <link>https://velog.io/@vanesa_kim/%EC%9E%85%EB%A0%A5%EA%B0%92-%EB%A7%A4%EC%9A%B0-%EB%A7%8E%EC%9D%80-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%84%B1%EB%8A%A5%EA%B0%9C%EC%84%A0-pt1SSR-getServerSideProps</link>
            <guid>https://velog.io/@vanesa_kim/%EC%9E%85%EB%A0%A5%EA%B0%92-%EB%A7%A4%EC%9A%B0-%EB%A7%8E%EC%9D%80-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%84%B1%EB%8A%A5%EA%B0%9C%EC%84%A0-pt1SSR-getServerSideProps</guid>
            <pubDate>Sun, 04 Jan 2026 08:24:54 GMT</pubDate>
            <description><![CDATA[<p>초기 상태
<img src="https://velog.velcdn.com/images/vanesa_kim/post/8d940ab4-dde0-4871-a085-27e75f757880/image.png" alt="">
<img src="https://velog.velcdn.com/images/vanesa_kim/post/a3e08ba4-bf0b-4cc5-9bd0-13ea14d8a4bb/image.png" alt=""></p>
<p>컴폰언트 안에서 관리하던 state값을 밖으로 뺌... 그리고 state 관리 방식도 바꿈.
렌더링 수를 줄임.
그리고 mui accordion 추가하니까 너무 느려져서 뺌
<img src="https://velog.velcdn.com/images/vanesa_kim/post/858dc98e-b497-45ac-9bb3-8b2a37479cdd/image.png" alt=""></p>
<p>체감은 훨씬 빨라졌는데 성능 점수는 낮아졌다. 알아보니까 mui accordion은 이미 최적화가 잘 되어 있어서 성능 점수는 더 높게 나올 수도 있대.</p>
<p>이제 다른 방식으로 성능개선해야 함.
우선 이 페이지는 여러 api를 동시에 호출해야 하는 것이 큰 단점(?)임. 이걸 잘 해야 하는데 지금은 그냥 아래처럼 되어 있다</p>
<pre><code class="language-javascript">...생략
  const CHECKBOX_FIELDS = [
    &#39;jobUuids&#39;,
    &#39;educationLevels&#39;,
    &#39;employmentTypes&#39;,
    &#39;region&#39;,
    &#39;deadlineType&#39;,
  ]
...생략
  useEffect(() =&gt; {
    const fetchData = async () =&gt; {
      CHECKBOX_FIELDS.forEach((field) =&gt; {
        const mappedType = fieldToTypeMapping[field]
        if (mappedType) {
          getRegisterOptions(mappedType).then((response) =&gt; {
            if (response) {
              const options = response[mappedType]
              if (options) {
                setRegisterOptions((prevOptions) =&gt; ({
                  ...prevOptions,
                  [field]: options,
                }))
              }
            }
          })
        }
      })
      try {
        const response = await getRecruitmentCompany()
        if (response &amp;&amp; response.length &gt; 0) {
          setCompanyList(response)
        }
      } catch (error) {
        console.error(error)
      }
    }
    fetchData()
  }, [])</code></pre>
<p><img src="https://velog.velcdn.com/images/vanesa_kim/post/b8e093b3-558f-47f3-ba55-b77dbea88ad6/image.png" alt="">
그래도 딱 한 번 호출되긴 함</p>
<p>문제점
근데 문제점이 <code>CHECKBOX_FIELDS</code> 항목에 대해 Api를 순차적으로 호출하면 시간이 오래 걸릴 수도 있다고 한다. 그리고 getRecruitmentCompany 이거도 순차적으로 호출하고 있다.</p>
<p>해결 방법
api를 병렬로 호출하면 효율성이 높아질 수 있다 한다. 이건 Promise.all을 쓰면 된다.</p>
<pre><code class="language-javascript">  const fetchOptionsData = async () =&gt; {
    try {
      const fetchPromises = CHECKBOX_FIELDS.map((field) =&gt; {
        const mappedType = fieldToTypeMapping[field]
        if (mappedType) {
          return getRegisterOptions(mappedType).then((response) =&gt; {
            if (response) {
              const options = response[mappedType]
              if (options) {
                return {
                  field,
                  options,
                }
              }
            }
            return null
          })
        }
        return null
      })

      const companyPromise = getRecruitmentCompany().then((response) =&gt; {
        if (response &amp;&amp; response.length &gt; 0) {
          setCompanyList(response)
        }
      })

      const [results] = await Promise.all([
        Promise.all(fetchPromises),
        companyPromise,
      ])

      const newRegisterOptions = {
        jobUuids: [] as string[],
        educationLevels: [] as string[],
        region: [] as string[],
        deadlineType: [] as string[],
      }

      results.forEach((result) =&gt; {
        if (result &amp;&amp; result.options) {
          newRegisterOptions[result.field] = result.options
        }
      })

      setRegisterOptions(newRegisterOptions)
    } catch (error) {
      console.error(error)
    }
  }

  useEffect(() =&gt; {
    fetchOptionsData()
  }, [])</code></pre>
<p>근데 역시 성능에는 변화가 별로 없다. 중복 호출 문제가 없었기 때문인 것 같다.
<img src="https://velog.velcdn.com/images/vanesa_kim/post/1416b947-63a3-4911-bf54-d3719e7fc5d5/image.png" alt=""></p>
<p>CLS(Cumulative Lazy Loading) 줄이기</p>
<pre><code class="language-javascript">const PreviewImage = styled.img`
  width: 00px;
  max-width: 400px;
  height: auto;
  margin: 20px 0;
  will-change: opacity, transform;
  transition: opacity 0.3s ease-in-out;
  loading: lazy;
`</code></pre>
<p>그래서</p>
<pre><code class="language-javascript">const PreviewImage = styled.img`
  width: 400px;
  height: 400px;
  object-fit: contain;
  margin: 20px 0;
  will-change: opacity, transform;
  transition: opacity 0.3s ease-in-out;
  loading: lazy;
`</code></pre>
<p>이렇게 해줬는데도 성능 개선 효관 없었다. img가 하나여서 그런듯</p>
<p>어쩔 수 없다. ssr로 필요한 데이터를 미리 받아와야겠다.
ssr 처음 써보는데 이거 cookie로 accesstoken 가져오는게 좀 별로네.
페이지 라우팅, next.js13 쓰는 중인데...
페이지 index.tsx에서 getServerSideProps 함수 만들었고 이 안에서  getRegisterOptions이런 걸 쓰고 있다. getRegisterOptions이건 fetchData라는 api 호출 함수를 사용 중인데, 원래 fetchData에서 토큰을 
그냥</p>
<pre><code class="language-javascript">const getCookie = (name: string): string | null =&gt; {
  const matches = document.cookie.match(new RegExp(&#39;(^| )&#39; + name + &#39;=([^;]+)&#39;))
  return matches ? matches[2] : null
}</code></pre>
<p>이렇게 쓰고 있었다. 근데 ssr에서는 document를 사용 못한다고 해서... fetchData에서 토크을 바로 가져올 수 있으면 좋은데 안되더라.
페이지의 index.tsx 에서 context: GetServerSidePropsContext로</p>
<pre><code class="language-javascript">    const cookies: Record&lt;string, string&gt; = Object.fromEntries(
      Object.entries(context.req.cookies).filter(
        ([key, value]) =&gt; value !== undefined,
      ),
    ) as Record&lt;string, string&gt;
    const accessToken = cookies.accessToken || null</code></pre>
<p>이렇게 사용해서 props로 넘겨줄 수밖에 없었다.</p>
<p>그래도 성능이 거의 두 배 향상되긴 했다.
<img src="https://velog.velcdn.com/images/vanesa_kim/post/6354462b-4cac-43d3-a632-56a3d094f413/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[렌더링 속도 느림]]></title>
            <link>https://velog.io/@vanesa_kim/%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%86%8D%EB%8F%84-%EB%8A%90%EB%A6%BC</link>
            <guid>https://velog.io/@vanesa_kim/%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%86%8D%EB%8F%84-%EB%8A%90%EB%A6%BC</guid>
            <pubDate>Sun, 04 Jan 2026 08:24:30 GMT</pubDate>
            <description><![CDATA[<p>아니 페이지가 로딩이 완료되었는데 시간표 목록이 안 뜬다.
<img src="https://velog.velcdn.com/images/vanesa_kim/post/46183692-a789-4a93-96f9-be9be9d4b3e1/image.png" alt="">
계속 누르면서 하염없이 기다리니 결국 뜨긴 하더라.</p>
<p><img src="https://velog.velcdn.com/images/vanesa_kim/post/f103ded1-5ec1-4776-890d-22bf7bb5f076/image.png" alt="">
배포 사이트 lighthouse 성능 점수는 위와 같고</p>
<p>대체 뭐가 문제지 싶어서 개발자도구 확인해봤는데
<img src="https://velog.velcdn.com/images/vanesa_kim/post/f47bc1d9-c47f-4c99-ba5a-3a58ccb0a59f/image.png" alt=""></p>
<p>서버 응답 기다리는 시간이 무려 4.44초나 된다. ms도 아니고 s....</p>
<p>그러니까 백엔드가 느린게 맞는거 같다. 하지만 지금 백엔드는 다른 할 일이 많기에 이거 수정해달라 할 수는 없어서 최대한 프론트에서 해결해보기로 했다.</p>
<p>근데 이상한 건 시간표 페이지에서 다른 페이지 갔다가 다시 시간표로 돌아오면 시간표 목록 렌더링 속도가 꽤 괜찮다. 문제는 시간표 페이지에 처음 진입할 때, 새로고침할 때다. Apollo에서 어느 정도 캐싱을 하는 건가...
아무튼 새로고침을 하면 브라우저가 JS context(mobx store, Apollo cache, memory cache 등)을 모두 초기화시켜서 api를 다시 호출해야 한다.</p>
<p>그럼.. 캐싱한 것이 날아가지 않도록 바꿔보는 것부터 해야겠다.
일단 시간표는 앱에서만 수정이 가능하다. 웹에서는 바꿀 수 없으므로 캐싱을 해보기로 했다.</p>
<p>그래서 일단... 로컬스토리지에 넣어서 로컬스토리지에 데이터가 없을 때만 api 호출하도록 했음.</p>
<p>그래서 새로고침했을 때는 이제 괜찮았는데 그래도 여전히 서버 응답 속도가 느려서 graphql에서 필요 없는 값 날렸다. 그랬더니 1초... 빨라졌다..
<img src="https://velog.velcdn.com/images/vanesa_kim/post/14e561a3-121f-482e-b795-eab70d59323f/image.png" alt=""></p>
<p>일단 다른 작업도 해야 해서 skeleton ui 적용해두었다.</p>
<p>성능점수도 꽤 높아졌는데 솔직히 skeleton ui 때문인 것 같아서 썩 그리 맘에 들지는 않는다
<img src="https://velog.velcdn.com/images/vanesa_kim/post/04ba3e32-b9ee-49fd-af92-825b28c4b476/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[변경사항 있어도 바로바로 적용 안되는 이슈]]></title>
            <link>https://velog.io/@vanesa_kim/%EB%B3%80%EA%B2%BD%EC%82%AC%ED%95%AD-%EC%9E%88%EC%96%B4%EB%8F%84-%EB%B0%94%EB%A1%9C%EB%B0%94%EB%A1%9C-%EC%A0%81%EC%9A%A9-%EC%95%88%EB%90%98%EB%8A%94-%EC%9D%B4%EC%8A%88</link>
            <guid>https://velog.io/@vanesa_kim/%EB%B3%80%EA%B2%BD%EC%82%AC%ED%95%AD-%EC%9E%88%EC%96%B4%EB%8F%84-%EB%B0%94%EB%A1%9C%EB%B0%94%EB%A1%9C-%EC%A0%81%EC%9A%A9-%EC%95%88%EB%90%98%EB%8A%94-%EC%9D%B4%EC%8A%88</guid>
            <pubDate>Sun, 04 Jan 2026 08:24:00 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/vanesa_kim/post/4ada3a32-14fc-4559-8d2c-a9602b76e203/image.png" alt=""></p>
<p>🛠️ 변경사항이 반영되지 않는 Apollo 캐시 이슈 해결기
🔍 문제 상황
&quot;학적변경 어드민에서 유저 학적변경을 해도 안 사라집니다. 다른 페이지 갔다 와도 그대로 있고 꼭 새로고침해야 반영돼요.&quot;
팀원으로부터 이런 피드백을 받았다.</p>
<p>실제로 확인해보니 /admin/school-verification 페이지에서 학적 변경을 승인하거나 거부해도 모달만 닫힐 뿐,</p>
<p>학적 변경 목록이 갱신되지 않았고</p>
<p>체크박스도 그대로 유지되어 있었다.</p>
<p>즉, UI는 바뀌지 않았고, 실제 데이터는 변경된 상태였다.
새로고침을 하면 그제서야 올바르게 반영된 UI가 보였다.</p>
<p>이건 단순 상태관리 문제가 아니라 쿼리 결과가 최신으로 갱신되지 않는 문제라는 판단이 들었다.</p>
<p>🧭 트러블슈팅 과정
쿼리 실행 후 데이터를 갱신하는 로직은 존재했다
useEditUsersMutation 성공 이후, editUsersData를 통해 store.replaceRows(...)가 잘 호출되고 있었다.
하지만 replaceRows에도 불구하고 화면엔 변경 사항이 반영되지 않았다.</p>
<p>모달에서 데이터 요청이 없어도, 목록 페이지에서 변경 사항을 감지할 수 있어야 했다
처음엔 modal이 닫힐 때 getUsersByPaging()을 다시 호출해야 하나? 싶었지만,
실제로는 editUsersMutation 이후 데이터가 갱신되어야 맞다.</p>
<p>Apollo의 캐시 문제라는 직감
동일한 문제가 다른 페이지에서도 재현되었다. 이쯤 되니 전역적인 설정이 의심됐다.</p>
<p>_app.js에서 Apollo Client 설정 확인</p>
<pre><code class="language-javascript">const defaultOptions = {
  watchQuery: {
    fetchPolicy: &#39;cache-first&#39;,
    errorPolicy: &#39;all&#39;,
  },
  query: {
    fetchPolicy: &#39;network-only&#39;,
    errorPolicy: &#39;all&#39;,
  },
  mutate: {
    errorPolicy: &#39;all&#39;,
  },
}
</code></pre>
<p>여기서 watchQuery.fetchPolicy가 cache-first로 되어 있었다.</p>
<p>→ cache-first는 말 그대로 캐시에 값이 있으면 네트워크 요청 없이 캐시만 사용한다.
그래서 변경된 내용을 반영하지 못한 것.</p>
<p>cache-and-network로 변경</p>
<pre><code class="language-javascript">const defaultOptions = {
  watchQuery: {
    fetchPolicy: &#39;cache-and-network&#39;,
    errorPolicy: &#39;all&#39;,
  },
  ...
}
</code></pre>
<p>이렇게 수정하니 기존 캐시를 먼저 보여주되, 네트워크 요청도 병행해서 최신 데이터를 갱신하도록 작동했고,
문제는 해결됐다.</p>
<p>💡 배운 점
Apollo Client의 fetchPolicy는 전역 설정으로, 모든 API 쿼리에 영향을 준다.
watchQuery.fetchPolicy를 cache-first로 설정하면, 네트워크 요청 없이 캐시된 데이터를 반환하므로
데이터가 변경되어도 화면에 반영되지 않을 수 있다.</p>
<p>cache-and-network는 초기 응답 속도를 보장하면서도 데이터 일관성을 유지하는 좋은 방법이다.</p>
<p>쿼리마다 별도로 fetchPolicy를 지정할 수도 있다.
전역 설정은 기본 동작을 정해주는 것이고, 특정 쿼리에서는 아래처럼 오버라이딩할 수 있다:</p>
<pre><code class="language-javascript">useQuery(MY_QUERY, {
  fetchPolicy: &#39;network-only&#39;,
})

</code></pre>
<p>실제 서비스에서는 성능과 신뢰성의 균형을 고려해 전역 fetchPolicy 설정을 신중히 해야 한다.</p>
<p>✍️ 마무리
이번 이슈는 단순 상태관리나 쿼리 실행의 문제가 아니라, Apollo 캐시의 기본 동작을 정확히 이해하고 있어야만 해결 가능한 문제였다.
작은 설정 하나가 앱 전체 데이터 흐름에 영향을 줄 수 있음을 몸소 배운 계기였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[react quill 텍스트 에디터 이미지 업로드 느림]]></title>
            <link>https://velog.io/@vanesa_kim/react-quill-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EC%97%90%EB%94%94%ED%84%B0-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-%EB%8A%90%EB%A6%BC</link>
            <guid>https://velog.io/@vanesa_kim/react-quill-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EC%97%90%EB%94%94%ED%84%B0-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-%EB%8A%90%EB%A6%BC</guid>
            <pubDate>Sun, 04 Jan 2026 08:23:46 GMT</pubDate>
            <description><![CDATA[<p>텍스트 에디터에 이미지 뜨는 속도가 느리다는 이야기를 듣고...
직접 해보니 (당연하지만) 큰 이미지 올리면 속도가 느려지더라.
그러면 뭐 이미지 압축하고, 유저에게는 그 동안 이미지를 어떻게든 미리 보여줘야겠지?</p>
<p>참고로 지금 문제가 되는 이미지 크기는 9.15MB다. 크긴 커~</p>
<p>일단 지금은
사용자가 이미지를 선택하면
useFileUpload 훅이 이미지를 FormData로 API에 올린다.
서버가 응답으로 이미지의 url을 반환해주는데 이 값을 프론트에서 img 태그에 넣어서 보여준다.</p>
<p>그러니까 서버에 업로드가 끝나야 url을 받을 수 있는거고, 근데 사용자 입장에선 지금 내가 업로드한 사진을 바로 보고 싶어서 문제가 생긴거다.</p>
<p>내가 알기로는 Canvas를 쓰면 라이브러리 없이 이미지를 압축할 수 있는데, 나는 라이브러리 안 쓰고 싶어서... 그니까 원리를 조금이라도 살펴보고 싶어서 한 번 canvas로 먼저 써보기로 했다. 별로면 라이브러리로 바꾸고~~</p>
<p>그래서
canvas를 만들고, 최대 너비를 제한하고, 그에 맞춰 이미지 크기를 줄여서 압축된 이미지를 반환하도록 했다.</p>
<pre><code class="language-javascript">...생략
      const img = new Image();
      const reader = new FileReader();

      reader.onload = e =&gt; {
        img.src = e.target?.result as string;
      };

      img.onload = () =&gt; {
        const canvas = document.createElement(&#39;canvas&#39;);
        const MAX_WIDTH = 1024;

        let width = img.width;
        let height = img.height;

        if (width &gt; MAX_WIDTH) {
          height *= MAX_WIDTH / width;
          width = MAX_WIDTH;
        }

        canvas.width = width;
        canvas.height = height;

        const ctx = canvas.getContext(&#39;2d&#39;);
        ctx?.drawImage(img, 0, 0, width, height);

        canvas.toBlob(
          blob =&gt; {
            const compressedFile = new File([blob!], file.name, {
              type: &#39;image/jpeg&#39;,
              lastModified: Date.now()
            });
            resolve(compressedFile);
          },
          &#39;image/jpeg&#39;,
          0.7 

...생략</code></pre>
<p>꽤나 빨라졌다.
근데 이미지 복붙해서 넣는 경우는 여전히 느려서 복붙하는 경우를 따로 처리를 해주기로 했다.</p>
<pre><code class="language-javascript">  React.useEffect(() =&gt; {
    const handlePaste = async (event: ClipboardEvent) =&gt; {

      const items = event.clipboardData?.items;
      if (!items) return;

      let handled = false;

      for (const item of items) {
        if (item.kind === &#39;file&#39; &amp;&amp; item.type.startsWith(&#39;image/&#39;)) {
          const file = item.getAsFile();
          if (file &amp;&amp; !handled) {
            handled = true;

            const compressed = await compressImageWithCanvas(file);
            setFileObj(compressed);
            event.preventDefault();
          }
        }
      }
    };
    window.addEventListener(&#39;paste&#39;, handlePaste);

    return () =&gt; {
      window.removeEventListener(&#39;paste&#39;, handlePaste);
    };
  }, []);
</code></pre>
<p>아 근데 이미지 하나 복붙해도 두 개가 뜨는 문제가 발생했다. (텍스트 에디터에서 이미지 업로드할 때는 정상 동작함)</p>
<p>이미지 복붙 테스트하면서 알게된건데
컴퓨터에서 이미지 파일을 열어서 이미지 우클릭해서 복붙하는거랑 인터넷에서 이미지 복붙해서 넣는거랑 다르다고 한다...</p>
<p>그래서 그런지 전자는 같은 이미지가 두 개 뜨지만
<img src="https://velog.velcdn.com/images/vanesa_kim/post/9f239f49-4743-4d2e-ae85-74e5d24900a2/image.png" alt=""></p>
<p>후자는 위쪽 이미지가 아이콘으로 뜬다.
<img src="https://velog.velcdn.com/images/vanesa_kim/post/ee090a42-4a33-4dda-9934-d0532deecfb5/image.png" alt=""></p>
<p>이미지 두 개 들어가는 건     event.preventDefault();
를 넣어주면 된다.
왜냐면 복붙한거 그대로 붙이는 것(default)이랑 압축한거랑 둘 다 붙여넣어져서 default를 방지해야 하기 때문이다.</p>
<pre><code class="language-javascript">...생략
  React.useEffect(() =&gt; {
    const handlePaste = async (event: ClipboardEvent) =&gt; {
      event.preventDefault();
...생략</code></pre>
<p>그래서.. 이렇게 문제 상황은 해결 되었고
정확히 얼마나 빨라졌는지 performance.now()로 측정해보았다.</p>
<p>아래처럼 파일 선택 시점 시간 저장하고
<img src="https://velog.velcdn.com/images/vanesa_kim/post/936965b6-d7e1-43a3-9e6a-2fe182b306a6/image.png" alt="">
업로드가 끝나는 시간 저장해서 계산했다.
<img src="https://velog.velcdn.com/images/vanesa_kim/post/e7c0092e-9fee-4c38-b256-279eaeef343d/image.png" alt="">
무려 12529.800000071526 ms... 12.53초다...
<img src="https://velog.velcdn.com/images/vanesa_kim/post/06f1c5ca-6eed-4af6-abdb-3e0f196f66cf/image.png" alt=""></p>
<p>개선 결과</p>
<ol>
<li>텍스트 에디터에 이미지 파일 업로드 속도</li>
<li>899999976158 ms, 그러니까 2.4초</li>
</ol>
<p><img src="https://velog.velcdn.com/images/vanesa_kim/post/456bf202-6748-46d0-9f54-4f3a5a433537/image.png" alt=""></p>
<ol start="2">
<li><p>인터넷에서 이미지 복붙하는 속도</p>
</li>
<li><p>8000000715256 ms, 그러니까 1.61초
<img src="https://velog.velcdn.com/images/vanesa_kim/post/a987028b-b0fa-4407-bc95-befe9a5e7949/image.png" alt=""></p>
</li>
<li><p>파일탐색기에서 이미지 파일 열어서 복붙하는 속도</p>
</li>
<li><p>5 ms, 그러니까 2.56초</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/vanesa_kim/post/c5db2af1-adda-4c94-8c19-aae6c44ab57b/image.png" alt=""></p>
<p>더 줄일라면 줄일 수 있겠지만, 다른 작업들도 해야 해서 여기까지.
아주 만족스럽다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jenkins]]></title>
            <link>https://velog.io/@vanesa_kim/Jenkins</link>
            <guid>https://velog.io/@vanesa_kim/Jenkins</guid>
            <pubDate>Sun, 04 Jan 2026 08:23:15 GMT</pubDate>
            <description><![CDATA[<p>원래 배포는 클라우드타입으로 했다. 근데 아무리 봐도 너무 비싼 것... 그래서 다른 방법을 찾아보다가 포폴에라도 도움이 되게 직접 배포를 하기로 했다. aws, github actions, jenkins 모두 써보고 싶어서 aws+github actions+Jenkins 이렇게 하려 했는데 아무리 봐도 이건 좀 아닌 것 같아서 aws+gihub actions, aws+jenkins 이렇게 둘 다 해보기로 했다.
그리고 우선은 더 유명한 jenkins 먼저 써볼 것임. 이렇게 하면 안되겠지. jenkins 장단점 찾아봐야겠지. 근데 어디선가 봤다. jenkins는 플젝 단위가 커지면 커스텀하기 좋다고.(그렇게 규모 큰 회사 들어가면 좋겠다)</p>
<p>계획: GitHub → Jenkins 트리거 → Docker 이미지 빌드 → ECR 푸시 → ECS 배포</p>
<h2 id="awsgithub-actionsjenkins기존-계획">aws+github actions+jenkins(기존 계획)</h2>
<ul>
<li>여기서 나는 github actions로 플젝 빌드하고 jenkins 트리거하는 것까지 끝냈었다. 그런데 이제 github actions를 안 쓰기로 했으니... 만들어둔 jenkins로 다시 해야 한다.</li>
</ul>
<h2 id="awsjenkins">aws+jenkins</h2>
<p>목표: GitHub에서 푸시하면 Jenkins가 알아서 실행되도록 만드는 것.</p>
<ol>
<li><p>만들어둔 jenkins 실행</p>
<pre><code>docker start jenkins</code></pre></li>
<li><p>jenkins 접속
나는 localhost:8080으로 접속했다.</p>
</li>
<li><p>Dashboard에서 등록해둔 아이템(프로젝트)을 누르면 configure 페이지가 나온다. 
여기서 설명 쓰고, GitHub project에 깃허브 레포 url을 넣는다.</p>
</li>
<li><p>Triggers에서 GitHub hook trigger for GITScm polling을 체크한다.</p>
</li>
</ol>
<p>&lt;여기서 드는 의문&gt;
aws 시크릿 키 같은 건 어떻게 해야 하는가? 깃허브 액션스만 쓰면 거기에 키값을 넣어두면 되는데 젠킨스에선 어떻게 하지?
Jenkins 관리 - Credentials 에 키값 모두 저장. 나는 시크릿 텍스트 형식으로 했다.
-&gt; 프로젝트에 들어가서 구성 -&gt; Environment의 Binding에 저장한 키값 바인딩 -&gt; 아래의 Build Steps의 Execute shell에 <code>export 키값변수=$키값변수</code> 추가해주면 된다.</p>
<p>어어
<img src="https://velog.velcdn.com/images/vanesa_kim/post/4f30ec0c-a7be-4950-8114-e5f9e0da231f/image.png" alt=""></p>
<p>콘솔을 보니 뭔가 이상하다. 뭐가 뭔지 몰라도 저 두번째, 세번째 줄이 이상한 건 알겠다. 나는 master를 안 쓰는데..? 뭔가 브랜치가 제대로 설정이 안 되어 있는 것 같다.</p>
<pre><code>16:14:22 Avoid second fetch
16:14:22  &gt; git rev-parse refs/remotes/origin/master^{commit} # timeout=10
16:14:22  &gt; git rev-parse origin/master^{commit} # timeout=10
16:14:22 ERROR: Couldn&#39;t find any revision to build. Verify the repository and branch configuration for this job.
16:14:23 Finished: FAILURE</code></pre><p><img src="https://velog.velcdn.com/images/vanesa_kim/post/02519d1f-558d-466d-a810-69dfe22bddc4/image.png" alt="">
아니나다를까 역시...우리는 main을 쓰고 있기 때문에 main으로 바꿔주었다.</p>
<p>어 그래도 안된다.
보니까 도커가 실행이 안된다..</p>
<pre><code>16:38:31 + docker --version
16:38:31 /tmp/jenkins~~~~.sh: 10: docker: not found
16:38:31 Build step &#39;Execute shell&#39; marked build as failure
16:38:31 Finished: FAILURE</code></pre><p>왜 도커가 없는거지...?
나는 젠킨스를 도커 컨테이너로 실행하고 있는데.
찾아보니까 컨테이너 안에서 도커 명령을 쓰려면 다음과 같은 두 가지 조건이 충족되어야 한다고 한다.</p>
<ol>
<li>Docker CLI가 Jenkins 컨테이너 안에 설치되어 있어야 한다.</li>
<li>호스트의 Docker 데몬과 통신할 수 있어야 한다.(소켓 공유를 해야 한다)</li>
</ol>
<p>좀 더 알아보아야겠지만 우선 해결 방안은 다음과 같다고 한다.
jenkins/jenkins:lts 이미지에는 docker CLI가 없기 때문에 docker CLI가 설치된 Jenkins 이미지를 써야 한다.</p>
<p>다음과 같이 하면 된다.</p>
<ol>
<li><p>지금 실행 중인 Jenkins 컨테이너를 멈추고 삭제한다.</p>
<pre><code>docker stop jenkins
docker rm jenkins</code></pre></li>
<li><p>Docker CLI가 포함된 이미지로 Jenkins를 실행한다.
원래는 jenkins/jenkins:lts+Docker가 설치된 커스텀 이미지를 써야 하는데 빨리 빌드해야 해서 많이들 사용하는 jenkinsci/blueocean을 쓰기로 했다.</p>
<pre><code>docker run -d \
--name jenkins \
-p 8080:8080 -p 50000:50000 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v jenkins_home:/var/jenkins_home \
jenkinsci/blueocean
</code></pre></li>
</ol>
<pre><code>젠킨스를 새롭게 실행하고 다시 8080에 접속해 주었더니
![](https://velog.velcdn.com/images/vanesa_kim/post/d142fc17-1389-458f-a46d-e4b5bc19982d/image.png)
오... 뭔가 달라진 것 같다. 메뉴는 비슷한거 같은데 UI가 다르다. 이미지란 이런 것인가...


이제 Docker는 실행이 된다.
문제는 빌드를 또 실패한 것. 근데 이번에는 이유가 좀 확실하다.</code></pre><p>Got permission denied while trying to connect to the Docker daemon socket at</p>
<pre><code>
오케이... 권한 없음.. 근데 무슨 권한..?
Jenkins가 도커 데몬에 접근할 권한이 없어서 그렇다고 한다.
내가 지금 Jenkins를 Docker 컨테이너로 실행하고 있는데 도커 빌드를 하려면 호스트 머신의 Docker 데몬에 접근해야 한다고 한다. 이거 때문에 `-v /var/run/docker.sock:/var/run/docker.sock`으로 소켓을 공유했지만, 컨테이너 안에서 도커 명령을 실행하는 사용자(jenkins user)가 그 소켓에 접근할 권한이 없어서 에러가 생긴다.
그러니까 컨테이너 안의 jenkins 유저가 도커 데몬에 접근하기 위해서는 docker 그룹에 넣어줘야 한다. 또 다시..도커를...

1. 도커 정지하고 삭제</code></pre><p>docker stop jenkins
docker rm jenkins</p>
<pre><code>
2. 도커 다시 실행
이번에는 그룹에 추가해주는 속성이 있다.</code></pre><p>docker run -d ^
  --name jenkins ^
  -p 8080:8080 -p 50000:50000 ^
  -v //var/run/docker.sock:/var/run/docker.sock ^
  -v jenkins_home:/var/jenkins_home ^
  jenkinsci/blueocean</p>
<pre><code>
아니 근데 또 실패했다. 권한 문제가 해결이 안된 것이다. 알아보니까 Window에서는 권한 조정이 어렵다고 한다. 그래서 보통 리눅스를 쓰는데 나는 윈도우를 쓰고 싶은걸...

결국은 우분투를 쓰기로 했다.
나 우분투 없는 줄 알았는데 있더라...?...

버추얼박스로 돌리니까 잘 됨. 버추얼박스로 젠킨스 돌려서 젠킨스 잡에 파이프라인 스크립트로 백엔드랑 인프라 레포 클론해와서 실행함.</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[첫 API 만들기: 선호 향수 노트 Top3 조회]]></title>
            <link>https://velog.io/@vanesa_kim/%EC%B2%AB-API-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EC%84%A0%ED%98%B8-%ED%96%A5%EC%88%98-%EB%85%B8%ED%8A%B8-Top3-%EC%A1%B0%ED%9A%8C</link>
            <guid>https://velog.io/@vanesa_kim/%EC%B2%AB-API-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EC%84%A0%ED%98%B8-%ED%96%A5%EC%88%98-%EB%85%B8%ED%8A%B8-Top3-%EC%A1%B0%ED%9A%8C</guid>
            <pubDate>Sun, 04 Jan 2026 08:22:49 GMT</pubDate>
            <description><![CDATA[<p>만들게 된 계기</p>
<ul>
<li>사용자 보유 데이터 기반 통계 분석 API를 만들며, 데이터 집계와 성능을 고려한 경험</li>
<li>사용자들은 자신에 대한 통계를 보는 것을 꽤나 좋아함.</li>
</ul>
<p>계획</p>
<ol>
<li>사용자 JWT 토큰 기반으로 사용자가 보유한 향수 목록 조회</li>
<li>향수의 탑/미들/베이스 노트 중에서 자주 등장하는 노트를 각각 집계</li>
<li>가장 많이 나온 노트 상위 3개씩을 노트 타입별(Top, Middle, Base)로 반환하는 API만들기.</li>
</ol>
<p>예상되는 제약 사항
보유 향수가 많을 경우, 향수에 노트가 많을 경우, 응답 속도가 늦을 수 있다.
(밑에 나오지만 그냥 나의 무지성 조인이 더 문제.)
🔸 Lazy Loading vs Eager Loading
향수 → 노트 관계에서 @ManyToMany(fetch = FetchType.LAZY) 설정 여부
불필요한 N+1 쿼리 발생 방지 위해 fetch join 사용 가능</p>
<pre><code class="language-java">@Query(&quot;SELECT up FROM UserPerfume up JOIN FETCH up.perfume p JOIN FETCH p.topNotes tn ...&quot;)</code></pre>
<p>🔸 데이터 처리 위치
전체 데이터를 불러와서 자바 코드에서 처리하면 CPU 부하가 커질 수 있음.
가능하면 쿼리 레벨에서 집계하거나, 필요한 필드만 가져오도록 projection 고려</p>
<p>예: Redis, Spring Cache 등
🔸 응답 속도 및 테스트</p>
<p>작성 코드</p>
<pre><code class="language-java">    // 가장 선호하는 노트 Top3(보유 향수 기반) 조회
    @Transactional(readOnly = true)
    public MyTopNotesResponseDto getMyTopNotes(){
        Member currentMember = getCurrentMember();
        // 사용자 보유 향수 목록 가져오기
        List&lt;PerfumeCollected&gt; collected = perfumeCollectedRepository.findByMemberId(currentMember.getId());

        // 탑,미들,베이스 노트 집계용 맵
        Map&lt;String, Map&lt;NoteValueDto, Integer&gt;&gt; noteCountMap = new HashMap&lt;&gt;();
        noteCountMap.put(&quot;top&quot;, new HashMap&lt;&gt;());
        noteCountMap.put(&quot;middle&quot;, new HashMap&lt;&gt;());
        noteCountMap.put(&quot;base&quot;, new HashMap&lt;&gt;());

        for (PerfumeCollected p: collected){
            Perfume perfume=p.getPerfume();
            List&lt;Perfumescent&gt; notes = perfume.getPerfumescentList();

            // 각 노트에서 집계하기
            for (Perfumescent ps: notes){
                Perfumenote note = ps.getPerfumenote();
                if (note == null) {
                    System.out.println(&quot;노트가 없습니다 -&quot; + ps.getScentKr());
                    continue;
                }

                // top, middle, base
                String scentType = note.getNoteName();
                String scentKr = ps.getScentKr();
                String scent = ps.getScent();

                NoteValueDto noteDto = new NoteValueDto(scentKr, scent);

                if (noteCountMap.containsKey(scentType)) {
                    Map&lt;NoteValueDto, Integer&gt; countMap = noteCountMap.get(scentType);
                    countMap.put(noteDto, countMap.getOrDefault(noteDto, 0) + 1);
                }

            }
        }
        List&lt;NoteWithCountDto&gt; topNotes = getTop3Notes(noteCountMap.get(&quot;top&quot;));
        List&lt;NoteWithCountDto&gt; middleNotes = getTop3Notes(noteCountMap.get(&quot;middle&quot;));
        List&lt;NoteWithCountDto&gt; baseNotes = getTop3Notes(noteCountMap.get(&quot;base&quot;));

        return new MyTopNotesResponseDto(topNotes, middleNotes, baseNotes);


    }


    private List&lt;NoteWithCountDto&gt; getTop3Notes(Map&lt;NoteValueDto, Integer&gt; noteMap) {
        return noteMap.entrySet()
                .stream()
                .sorted((e1, e2) -&gt; e2.getValue().compareTo(e1.getValue()))
                .limit(3)
                .map(entry -&gt; new NoteWithCountDto(
                        entry.getKey().getScent(),
                        entry.getKey().getScentKr(),
                        entry.getValue()
                ))
                .toList();
    }
</code></pre>
<p>결과
잘 나온다. 포스트맨 기준으로 응답 속도는 1.48초.
※참고로 원래 count를 넣어줄 생각은 없었는데 제대로 된 결과가 나온 것인지 확인하기 위해 넣어주기로 했다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/vanesa_kim/post/2238d5b1-c0fe-4f21-8629-ac562755df05/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/vanesa_kim/post/f830ee03-fafe-47ba-9514-d3d24ea32591/image.png" alt=""></th>
</tr>
</thead>
</table>
<p>개선할 점
사실 1.48초면 괜찮은거 아닌가 했는데 아닌거 같다.</p>
<p>로딩 속도는 페이지 이탈과 아주 상관 관계가 높다. 이에 대한 자료도 많다.
구글은 모바일 웹 로딩 시간이 3초 이상 걸리면 53% 이상의 사용자가 이탈한다는 자료를 공개한 적이 있다.
지금 api는 거의 1.5초 걸리는데 기타 상황(프론트엔드, 네트워크 상태 등)을 고려하면 페이지 로딩 시간이 3초에 육박할 가능성이 아주 높다. 솔직히 뭐 엄청 대단한 작업을 처리하는 api도 아니라서 줄여야 할 것 같긴 하다. 근데 얼마나 줄여야 할지 감이 안 와서 찾아봤더니 또 기준이라 할 수 있는 시간들이 정리되어 있더라.</p>
<pre><code>0.1 seconds: Ideal response time, perceived as instantaneous by users.
0.1 to 1 second: Good response time, users notice a slight delay but remain uninterrupted.
1 to 2 seconds: Acceptable for most applications, but may impact user experience in real-time systems.
2 to 5 seconds: Tolerable for non-critical operations, but users may become impatient.
5+ seconds: Generally unacceptable, likely to result in user frustration and abandonment.</code></pre><p>출처: <a href="https://odown.com/blog/what-is-a-good-api-response-time/#acceptable-api-response-times">https://odown.com/blog/what-is-a-good-api-response-time/#acceptable-api-response-times</a></p>
<p>근데 또 검색하다보니 웹사이트 전체의 로딩 속도가 중요하지, api 하나하나의 속도는 그리 중요하지 않다, 왕초보가 성능 개선을 하는 건 위험하다?(&#39;premature optimization is the devil&#39;s volleyball&#39;)라고 하는 사람들도 꽤 있다.
근데 지금은 이 간단한 걸 만들면서도 성능 문제가 생겼다는거니까 1초 정도 더 줄이면서 공부를 하기로 했다.</p>
<p>개선점 분석
분명히 응답 속도가 느린 원인은 모든 데이터를 가져와서 자바에서 집계하기 때문일 것이다.
어쩐지 이상하더라. 다들 이렇게 쿼리문을 쓰는데 나는 또 자바로... 편하게(?) 집계함수를 만들어버린 것이다.
<img src="https://velog.velcdn.com/images/vanesa_kim/post/d2614a2f-045e-46dc-bf6c-6033760672d2/image.png" alt="">
위에 사진 보면 TTFB(Time To First Byte) 시간이 제일 문제인데, 이 원인이 바로 자바에서 직접 집계하고 있던 것이었다.
조금 더 자세히 보면 현재 코드는</p>
<ol>
<li>DB에서 PerfumeCollected, Perfume, Perfumescent, Perfumenote 전체를 가져옴</li>
<li>이 많은 걸 자바가 받아서 for문을 돌리면서 Map에 집계</li>
<li>집계 결과를 다시 DTO로 가공해서 반환
하고 있다.
1~2단계에서 시간이 오래 걸린다는 건 유치원생도 알 수 있을 것이다.</li>
</ol>
<p>개선 방법
따라서! 나도 쿼리를 써야 한다. DB에서 직접 집계해서 결과만 자바에 전송하도록 해야 한다. </p>
<p>의문점
근데 왜 DB에서 집계하는게 더 빠르지?
찾아보니 DB는 집계에 최적화된 구조와 기능을 가진 전문 도구이기 때문이라고 한다.애초에 CPU, 메모리 효율이 훨씬 좋다고 한다.
사실 DB에는 인덱싱이 다 되어 있다는 점에서 그냥 끝난 이야기 같기도 하다.
반면에 자바는 모든 데이터를 네 트 워 크로 전달받아서(느림) 자바 힙 메모리에 다 올려서 Map 처리(느림)를 한다. 그리고 정렬(느림), DTO 변환(느림) 까지 다 한다.
심한 경우에는 OOM(Out Of Memory) 위험도 있다고 한다.
속도를 직접 재볼 수는 없어서 지피티한테 물어보니까
10만 개의 데이터 기준, DB 집계는 100<del>300ms, 자바 집계는 1</del>2초 이상 걸릴 것ㅇ로 예상된다고 한다.
물론 DB집계가 항상 빠른 건 아니지만 데이터가 크고 집계 대상이 명확할 때(DB에서 쿼리문으로 처리 가능한 경우)는 DB에서 처리하는 것이 대부분 빠르다고 한다.</p>
<p>개선 과정
쿼리문을 작성하려고 보니, 쿼리문 작성 방법이 두 가지가 있따고 한다. JPQL과 네이티브 쿼리. 지금 기존 코드는 네이티브 쿼리로 되어 있어서 네이티브 쿼리로 작성할 것 같긴 한데, JPQL이 뭔지, 둘의 장단점을 알아보아야겠다는 생각이 든다.</p>
<p>JPA(Java Persistence API)란?
자바 객체(ex. User, Perfume)를 DB와 연결하고 불러오게 해주는 표준 인터페이스
즉, 자바로 DB를 다룰 수 있게 하는 표준 API다.
이 JPA의 일부로 등장한 것이 JPQL이다.</p>
<p>JPQL(Java Persistence Query Language)이란?
DB와 소통을 해야 하는데, 전통 SQL로 하면 불편한 점이 많아서 JPA를 사용하는 개발자가 자바 객체 기반으로 쿼리를 짤 수 있도록 만들어진 언어다.</p>
<table>
<thead>
<tr>
<th align="center">전통 SQL(네이티브쿼리)</th>
<th align="center">JPQL</th>
</tr>
</thead>
<tbody><tr>
<td align="center">테이블 중심</td>
<td align="center">객체(엔티티) 중심</td>
</tr>
<tr>
<td align="center">테이블 이름, 컬럼 이름 적어야 함</td>
<td align="center">엔티티 이름, 필드 이름만 알면 됨</td>
</tr>
<tr>
<td align="center">DB에 따라 문법 달라짐(DB종속적)</td>
<td align="center">자바 코드처럼 일관된 문법(DB독립적, 다른 DB로 쉽게 이전 가능)</td>
</tr>
<tr>
<td align="center">결과도 테이블 형식</td>
<td align="center">엔티티나 DTO로 바로 매핑 가능</td>
</tr>
<tr>
<td align="center">복잡한 쿼리까지 자유롭게 사용 가능</td>
<td align="center">일반적으로 비슷하나 복잡한 쿼리는 제약 존재</td>
</tr>
</tbody></table>
<p>개선 코드</p>
<pre><code class="language-java">    @Query(value= &quot;&quot;&quot;
    SELECT ps.scent, ps.scent_kr, COUNT(*) as count
    FROM perfumecollected pc
    JOIN perfume p ON pc.perfume_id=p.id
    JOIN perfumescent ps ON ps.perfume_id=p.id
    JOIN perfumenote pn ON ps.note_id=pn.id
    WHERE pc.user_id = :memberId AND pn.note_name=:noteType
    GROUP BY ps.scent, ps.scent_kr
    ORDER BY count DESC
    LIMIT 3
&quot;&quot;&quot;, nativeQuery = true)
    List&lt;Object[]&gt; findMyTopNotes(@Param(&quot;memberId&quot;) Long memberId, @Param(&quot;noteType&quot;) String noteType);</code></pre>
<p>무지성 조인 같지만 이거 말고는 방법이 없는걸..
자바를 잘 모르므로 하나하나 뜯어보면
@Query 어노테이션</p>
<pre><code class="language-java">@Query(value = &quot;...&quot;, nativeQuery = true)</code></pre>
<p>스프링 데이터 JPA에서 SQL/JPQL을 직접 작성하고 싶을 때 사용하는 기능.
value 의 쌍따옴표 안에는 SQL 쿼리가 들어간다. nativeQuery=true는 네이티브 쿼리를 사용할 때 쓰고, 기본값은 false라서 JPQL을 사용할 때는 생략이 가능한 부분이다.</p>
<p>쌍따옴표 세 개를 쓰는 이유??
Java 15부터 지원되는 텍스트 블록(Text Block) 문법이다. 여러 줄 문자열을개행 포함해서 편하게 쓰기 위해 등장했다.
원래는 개행을 하려면 줄바꿈 문자를 넣고 양쪽에 따옴표 열고 닫고 +로 이어줘야 했다. 가독성이 나빴다는 것이다.</p>
<p>쿼리문 다 쓴 다음에 맨 마지막 줄은 또 왜 쓰는거야?
우선 List&lt;Object[]&gt;는 쿼리 결과가 행 단위로 여러 개 오고, 각 행은 Object 배열로 구성되어 있다는 것을 알려주는 것이다. 지금 결과로 나오는 것이 scent, scent_kr, count인데 이걸 한 줄씩 Object[]로 묶어서 리스트로 리턴한다.
그리고 @Param(&quot;memberId&quot;)는, 쿼리 안의 :memberId 자리에 메서드 인자 값인 Long memberId를 집어넣어주는 것이다.</p>
<p>개선 시도 결과
2.16초요???
<img src="https://velog.velcdn.com/images/vanesa_kim/post/aac41515-aa7e-4f0e-aae5-2b47738f5fb6/image.png" alt="">
두 번째부터는 31ms~60ms 밖에 안 걸리는데, 첫 시도 때의 응답 속도가 이렇게 느린 건 문제가 있따.
<img src="https://velog.velcdn.com/images/vanesa_kim/post/499e8124-f156-445a-bca6-a70b71af4b0e/image.png" alt=""></p>
<p>인덱스를 붙였떠니 응답 시간이 줄긴 했다.
1.78초다.
<img src="https://velog.velcdn.com/images/vanesa_kim/post/d863607c-4c8e-45d1-955a-c59832f8a07d/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Longest Consecutive Sequence]]></title>
            <link>https://velog.io/@vanesa_kim/Longest-Consecutive-Sequence</link>
            <guid>https://velog.io/@vanesa_kim/Longest-Consecutive-Sequence</guid>
            <pubDate>Sun, 03 Aug 2025 07:20:47 GMT</pubDate>
            <description><![CDATA[<h2 id="내-풀이">내 풀이</h2>
<pre><code class="language-javascript">class Solution {
    /**
     * @param {number[]} nums
     * @return {number}
     */
    longestConsecutive(nums) {
        let sortedNums=nums.sort((a,b)=&gt;a-b);
        let prevNum=sortedNums[0];
        let max=1;
        let count=1;
        if (nums.length===0){
            return 0
        }
        for (let i=1;i&lt;sortedNums.length;i++){
            // 이어진다면
            if (sortedNums[i]===prevNum){
                continue;
            }else if (sortedNums[i]===prevNum+1){
                count+=1;
                prevNum=sortedNums[i];
            // 안 이어진다면
            }else{
                if (count&gt;max){
                    max=count;
                }
                count=1;
                prevNum=sortedNums[i];
            }
        }
        if (max&lt;count){
            max=count;
        }
        return max;

    }
}</code></pre>
<p>같은 숫자가 있어도 consecutive할 수 있음을 모르고 풀어서 if문이...</p>
<h2 id="다른-풀이-hash-set">다른 풀이: Hash Set</h2>
<pre><code class="language-javascript">class Solution {
    /**
     * @param {number[]} nums
     * @return {number}
     */
    longestConsecutive(nums) {
        const numSet = new Set(nums);
        let longest = 0;

        for (let num of numSet) {
            if (!numSet.has(num - 1)) {
                let length = 1;
                while (numSet.has(num + length)) {
                    length++;
                }
                longest = Math.max(longest, length);
            }
        }
        return longest;
    }
}</code></pre>
<p>세상 깔끔한 코드다. set으로 중복 제거한 후 각 원소에서 시작하는 경우의 가장 긴 consecutive한 배열 길이를 갱신하는 것. <code>if (!numSet.has(num-1)){</code> 이 코드로 자칫 비효율적인 코드가 될 뻔한 것도 막아주었다.</p>
<pre><code class="language-javascript">class Solution {
    /**
     * @param {number[]} nums
     * @return {number}
     */
    longestConsecutive(nums) {
        const numSet=new Set(nums);
        let longest=0;
        for (let num of numSet){
            if (!numSet.has(num-1)){
                let length=1;
                while(numSet.has(num+length)){
                    length+=1;
                }
                longest=Math.max(longest, length);
            }
        }
        return longest;
    }
}</code></pre>
<h2 id="다른-풀이-hash-map">다른 풀이: Hash Map</h2>
<pre><code class="language-javascript">class Solution {
    /**
     * @param {number[]} nums
     * @return {number}
     */
    longestConsecutive(nums) {
        const mp = new Map();
        let res = 0;

        for (let num of nums) {
          // 중복 방지용
            if (!mp.has(num)) {
                // mp.get(num - 1) || 0) --&gt; 왼쪽 수열 길이
                // mp.get(num + 1) || 0) --&gt; 오른쪽 수열 길이

                  // 현재 num을 기준으로 새로운 연속 수열 길이를 계산하여
                // 현재 숫자에 저장.
                mp.set(
                    num,
                    (mp.get(num - 1) || 0) + (mp.get(num + 1) || 0) + 1,
                );
                  // 수열의 시작점에 길이 업데이트
                mp.set(num - (mp.get(num - 1) || 0), mp.get(num));
                // 수열의 끝점에 길이 업데이트
                mp.set(num + (mp.get(num + 1) || 0), mp.get(num));
                  // 최대 길이 갱신
                res = Math.max(res, mp.get(num));
            }
        }
        return res;
    }
}</code></pre>
<p>어렵다.
핵심 아이디어: 연속된 수열의 &#39;양 끝 값&#39;에 수열의 길이만 기록하는 것
자세한 건 주석</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Top K Frequent Elements]]></title>
            <link>https://velog.io/@vanesa_kim/Top-K-Frequent-Elements</link>
            <guid>https://velog.io/@vanesa_kim/Top-K-Frequent-Elements</guid>
            <pubDate>Fri, 01 Aug 2025 08:47:26 GMT</pubDate>
            <description><![CDATA[<h2 id="내-풀이">내 풀이</h2>
<pre><code class="language-javascript">class Solution {
    /**
     * @param {number[]} nums
     * @param {number} k
     * @return {number[]}
     */
    topKFrequent(nums, k) {
        let answer=[];
        let count={};
        for (let i=0;i&lt;nums.length;i++){
            if (!count[nums[i]]){
                count[nums[i]]=1;
            }else{
                count[nums[i]]+=1;
            }
        }
        let pairs = Object.entries(count);
        pairs=pairs.sort((a,b)=&gt;b[1]-a[1]);
        while(answer.length&lt;k){
            let [num, numCount]=pairs.shift();
            answer.push(num)
        }
        return answer;

    }
}</code></pre>
<p>시간복잡도: O(nlogn)
흐름설명</p>
<ol>
<li>객체를 만들어서 그 안에 숫자:그 숫자 갯수 넣음</li>
<li>Object, entries로 [숫자, 그 숫자 갯수]로 이루어진 배열 만듦.</li>
<li>이 배열을 그 숫자 갯수 내림차순으로 sort함.</li>
<li>while문 내에서 answer 길이가 k가 될 때까지 배열에서 앞에거 빼서 집어넣음.</li>
</ol>
<h2 id="다른-풀이1-sorting">다른 풀이1: Sorting</h2>
<p>사실상 나랑 똑같은 코드인데 훨씬 깔끔하다.
그리고 아래 코드 보면서 알게 된건데, 만약 TypeScript였으면 내 코드는 틀렸을 것이다. 자바스크립트는 느슨한 언어라서 parseInt를 안 해줘도 괜찮은데 만약 타입에 엄격한 언어였으면 틀렸다.</p>
<pre><code class="language-javascript">class Solution {
    /**
     * @param {number[]} nums
     * @param {number} k
     * @return {number[]}
     */
    topKFrequent(nums, k) {
      const count={};
      for (const num of nums){
          count[num]=(count[num] || 0)+1 // 이거 진짜 굉장하다.
      }
      cosnt arr=Object.entries(count).map(([num, freq])=&gt;[freq, parseInt(num)]);
      arr.sort((a,b)=&gt;b[0]-a[0]);
      return arr.slice(0,k).map(pair=&gt;pair[1]);
    }
}
</code></pre>
<h2 id="다른-풀이2-min-heap">다른 풀이2: Min-Heap</h2>
<p>프로그래머스는 자바스크립트 우선순위큐를 왜 지원을 안할까^^ 자바스크립트로 코테 하는 사람들에게 아주 불리한 자료구조...^^ 하... MinPriorityQueue 좀 지원해주세요 리트코드는 지원해주는데 왜 백준이랑 프로그래머스는 안해주시는건데요... (미국과 한국에서의 자바스크립트의 위상이 다르기 때문입니다.)</p>
<pre><code class="language-javascript">class MinHeap{
  constructor(){
    this.heap=[];
  }
  size(){
    return this.heap.length;
  }
  peek(){
    return this.heap[0];
  }
  swap(i,j){
    [this.heap[i], this.heap[j]]=[this.heap[j], this.heap[i]];
  }
  enqueue(value){
    this.heap.push(value);
    this.bubbleUp();
  }
  bubbleUp(){
    let index=this.heap.length-1;
    while(index&gt;0){
      let parentIndex=Math.floor((index-1)/2);
      if (this.heap[parentIndex][1]&lt;=this.heap[index][1]){
        break;
      }
      this.swap(parentIndex, index);
      index=parentIndex;
    }
  }
  dequeue(){
    if (this.heap.length===1){
      return this.heap.pop();
    }
    const min this.heap[0];
    this.heap[0]=this.heap.pop();
    this.bubbleDown();
    return min;
  }
  bubbleDown(){
    let index=0;
    const length=this.heap.length;
    while(true){
      let left=index*2 +1;
      let right=index * 2 +2;
      let smallest=index;

      if (left &lt; length &amp;&amp; this.heap[left][1] &lt; this.heap[smallest][1]) {
        smallest = left;
      }
      if (right &lt; length &amp;&amp; this.heap[right][1] &lt; this.heap[smallest][1]) {
        smallest = right;
      }
      if (smallest === index) break;

      this.swap(index, smallest);
      index = smallest;
    }
  }

}


class Solution {
    /**
     * @param {number[]} nums
     * @param {number} k
     * @return {number[]}
     */
    topKFrequent(nums, k) {
      const count={};
      for (const num of nums){
        count[num]=(count[num]||0)+1;
      }
      const heap = new MinHeap();

      for (const [num, freq] of Object.entries(count)) {
        heap.enqueue([num, freq]);
        if (heap.size() &gt; k) {
          heap.dequeue();
        }
      }

      const res = [];
      while (heap.size() &gt; 0) {
        res.push(heap.dequeue()[0]);
      }

      return res.reverse();
  }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Group Anagrams]]></title>
            <link>https://velog.io/@vanesa_kim/Group-Anagrams</link>
            <guid>https://velog.io/@vanesa_kim/Group-Anagrams</guid>
            <pubDate>Fri, 01 Aug 2025 05:20:47 GMT</pubDate>
            <description><![CDATA[<p>코테에서 매번 망하고 진짜 진짜 이제는 코테를 열심히 할 때가 되었다 싶어서 새로운 마음으로 백준 말고 새로운 사이트를 찾아서 시작함. <del>이젠 정말 물러날 곳도 없음</del></p>
<h2 id="내-답안">내 답안</h2>
<pre><code class="language-javascript">class Solution {
    /**
     * @param {string[]} strs
     * @return {string[][]}
     */
    groupAnagrams(strs) {
       // 비교할 때 sort해서 비교
       let strsCopy=strs.slice().map((el)=&gt;el.split(&#39;&#39;).sort().join(&#39;&#39;));
       let strsSet=new Set(strsCopy);
       let strsSetArray=Array.from(strsSet);
       let answer=Array.from({length:strsSetArray.length}, ()=&gt;[]);
       for (let i=0;i&lt;strs.length;i++){
        let strSort=strs[i].split(&#39;&#39;).sort().join(&#39;&#39;);
        if (strsSetArray.includes(strSort)){
            answer[strsSetArray.indexOf(strSort)].push(strs[i]);
        }
       }
           return answer;

    }
}</code></pre>
<h3 id="단점">단점</h3>
<ul>
<li>미개하다. 저 수많은 변수를 보소.</li>
<li>자바스크립트는 sort가 느리다. NlogN임. 자바스크립트는 대체;; 앱/웹 프론트/백 모두 다 할 수 있는 장점을 가진 굉장한 언어지만 속도가 왜 저럴까(싱글스레드, 인터프리터, 표준 라이브러리 부족(진짜 화남. 우선순위큐도 없음. 파이썬은 이분탐색도 bisect로 한다는데))</li>
</ul>
<h2 id="다른-풀이1-해시테이블">다른 풀이1. 해시테이블</h2>
<pre><code class="language-javascript">class Solution {
    /**
     * @param {string[]} strs
     * @return {string[][]}
     */
    groupAnagrams(strs) {
        const res = {};
        for (let s of strs) {
            const count = new Array(26).fill(0);
            for (let c of s) {
                count[c.charCodeAt(0) - &#39;a&#39;.charCodeAt(0)] += 1;
            }
            const key = count.join(&#39;,&#39;);
            if (!res[key]) {
                res[key] = [];
            }
            res[key].push(s);
        }
        return Object.values(res);
    }
}</code></pre>
<p>흐름</p>
<ol>
<li>각 문자열을 순회하면서 알파벳 개수를 기록한(애너그램이기 때문) <code>count</code>배열을 만든다.</li>
<li><code>count.join(&#39;&#39;)</code> 또는 <code>count.join(&#39;,&#39;)</code>를 고유한 key로 삼는다.</li>
<li>같은 애너그램 문자열은 같은 key를 가지게 된다.</li>
<li>key를 기준으로 <code>res</code> 객체에 문자열을 push한다.</li>
</ol>
<h3 id="장점">장점</h3>
<p>성능 최적화에 아주 좋다. 시간복잡도가 <code>O(n)</code>다.</p>
<h3 id="단점-1">단점</h3>
<p>내 머리로 생각하기 어렵다.</p>
<p>근데 굳이 객체로 안하고 Map을 써도 될 것 같다. 아래와 같이 쓰면 되는 것.</p>
<pre><code class="language-javascript">class Solution {
    /**
     * @param {string[]} strs
     * @return {string[][]}
     */
    groupAnagrams(strs) {
      const res=new Map();
      for (let s of strs){
        const count=new Array(26).fill(0);
           for (let c of s){
         count[c.charCodeAt(0) - &#39;a&#39;.charCodeAt(0)]++; 
        }
        const key=count.join(&#39;,&#39;); // ,로 안해도 되고 그냥 붙여도 됨

        // 만약 해당 키값을 가진게 res에 없다면
        if (!res.has(key)){
         res.set(key, []); 
        }
        res.get(key).push(s);
      }
      return Array.from(res.values());
    }
}</code></pre>
<h3 id="여기서-확인하고-가는-객체와-map의-차이점">여기서 확인하고 가는 객체와 Map의 차이점</h3>
<table>
<thead>
<tr>
<th></th>
<th>객체{}</th>
<th>Map</th>
</tr>
</thead>
<tbody><tr>
<td>key 타입</td>
<td>문자열/심볼만 가능</td>
<td>모든 타입 가능(ex.배열)</td>
</tr>
<tr>
<td>key 순서 보장</td>
<td>보장X(ES6이후는 거의 됨)</td>
<td>입력 순서 보장됨</td>
</tr>
<tr>
<td>내장 메서드</td>
<td>적음(Object.keys 등)</td>
<td>풍부함(get, set, has, forEach, entreis 등)</td>
</tr>
<tr>
<td>성능</td>
<td>작은 규모에서는 비슷</td>
<td>큰 데이터셋에서는 Map이 우세</td>
</tr>
</tbody></table>
<h3 id="객체-메소드">객체 메소드</h3>
<table>
<thead>
<tr>
<th>메소드 이름</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Object.keys(obj)</td>
<td>key리스트(문자열 배열) 반환</td>
</tr>
<tr>
<td>Object.values(obj)</td>
<td>value 리스트 반환</td>
</tr>
<tr>
<td>Object.entries(obj)</td>
<td>[key, value]쌍의 배열 반환</td>
</tr>
<tr>
<td>Object.hasOwnProperty()</td>
<td>객체에 해당 key가 직접 존재하는지 확인</td>
</tr>
<tr>
<td>Object.assign(target, source)</td>
<td>객체 복사</td>
</tr>
<tr>
<td>Object.freeze(obj)</td>
<td>객체를 수정 불가로 만듦</td>
</tr>
<tr>
<td>Object.fromEntries()</td>
<td>entries 배열 --&gt; 객체로 변환</td>
</tr>
<tr>
<td>```javascript</td>
<td></td>
</tr>
<tr>
<td>const obj = { a: 1, b: 2 };</td>
<td></td>
</tr>
<tr>
<td>console.log(Object.keys(obj)); // [&#39;a&#39;, &#39;b&#39;]</td>
<td></td>
</tr>
<tr>
<td>console.log(Object.entries(obj)); // [[&#39;a&#39;, 1], [&#39;b&#39;, 2]]</td>
<td></td>
</tr>
<tr>
<td>```</td>
<td></td>
</tr>
</tbody></table>
<h3 id="map-메소드">Map 메소드</h3>
<table>
<thead>
<tr>
<th>메소드 이름</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>set(key, value)</td>
<td>키-값 추가</td>
</tr>
<tr>
<td>get(key)</td>
<td>키에 해당하는 값 반환</td>
</tr>
<tr>
<td>has(key)</td>
<td>키 존재 여부 반환(true/false)</td>
</tr>
<tr>
<td>delete(key)</td>
<td>키 삭제</td>
</tr>
<tr>
<td>clear()</td>
<td>전체 삭제</td>
</tr>
<tr>
<td>keys()</td>
<td>모든 key 반복자 반환</td>
</tr>
<tr>
<td>values()</td>
<td>모든 value 반복자 반환</td>
</tr>
<tr>
<td>entries()</td>
<td>[key, value] 반복자 반환</td>
</tr>
<tr>
<td>forEach(callback)</td>
<td>모든 요소에 콜백 적용</td>
</tr>
</tbody></table>
<pre><code class="language-javascript">const map = new Map();

map.set(&#39;name&#39;, &#39;Alice&#39;);
map.set(42, &#39;age&#39;);
console.log(map.get(&#39;name&#39;)); // &#39;Alice&#39;
console.log(map.has(42));     // true
console.log([...map.keys()]); // [&#39;name&#39;, 42]</code></pre>
<h2 id="다른-풀이2-정렬">다른 풀이2. 정렬</h2>
<pre><code class="language-javascript">class Solution {
    /**
     * @param {string[]} strs
     * @return {string[][]}
     */
    groupAnagrams(strs) {
        const res = {};
        for (let s of strs) {
            const sortedS = s.split(&#39;&#39;).sort().join(&#39;&#39;);
            if (!res[sortedS]) {
                res[sortedS] = [];
            }
            res[sortedS].push(s);
        }
        return Object.values(res);
    }
}</code></pre>
<p>나랑 비슷하긴 한데 로직이 훨씬 간결하다.</p>
<h3 id="장점-1">장점</h3>
<ul>
<li>객체를 사용해서 간단하다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[숨바꼭질 13913 시간초과&답안 차이]]></title>
            <link>https://velog.io/@vanesa_kim/%EC%88%A8%EB%B0%94%EA%BC%AD%EC%A7%88-13913-%EC%8B%9C%EA%B0%84%EC%B4%88%EA%B3%BC%EB%8B%B5%EC%95%88-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@vanesa_kim/%EC%88%A8%EB%B0%94%EA%BC%AD%EC%A7%88-13913-%EC%8B%9C%EA%B0%84%EC%B4%88%EA%B3%BC%EB%8B%B5%EC%95%88-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Sat, 26 Apr 2025 08:34:04 GMT</pubDate>
            <description><![CDATA[<h2 id="1-문제-상황-설명">1. 문제 상황 설명</h2>
<p>처음엔 isDone 변수를 이용해서 동생을 찾으면 while문을 탈출하려고 했다.
하지만 예상과 달리 시간 초과가 발생했다.</p>
<h3 id="2-원인-분석">2. 원인 분석</h3>
<p>isDone을 true로 만들어도 현재 shift한 current에 대해 current+1, current-1, current*2, 세 방향 전부 다 for문으로 돌고 난 다음에야 while문 조건을 검사할 수 있다.</p>
<p>즉, 동생을 찾았다고 바로 탈출하는 게 아니라, current의 다른 방향 탐색도 모두 끝내야 했다.
이 과정에서 queue에 불필요한 값들이 계속 추가되었고, 이로 인해 shift 연산(O(N)) 비용이 누적되어 시간초과가 났다.</p>
<h3 id="시간초과-코드">시간초과 코드</h3>
<pre><code class="language-javascript">const { start } = require(&quot;repl&quot;);
const filePath = process.platform === &quot;linux&quot; ? &quot;/dev/stdin&quot; : &quot;./예제.txt&quot;;
let input = require(&quot;fs&quot;).readFileSync(filePath).toString().trim().split(&quot;\n&quot;);

const [N, K]=input.shift().split(&#39; &#39;).map((el)=&gt;+el);

// 가장 빠른 시간
let visited=new Array(100001).fill(-1);
// 어떻게 이동해야 하는지. 2차원 배열은 안됨 way 배열 두고 거기에 위치 저장..? 이동 경로용...
let way=new Array(100001).fill(-1);


let queue=[];
queue.push(N);
visited[N]=0;

let isDone=false;

while(isDone===false){
    let current=queue.shift();

    for (let next of [current+1, current-1, current*2]){
        if (next&lt;0||next&gt;=100001){
            continue;
        }

        // 아직 방문 안한 곳이면
        if (visited[next]===-1){
            visited[next]=visited[current]+1;
            way[next]=current;
            queue.push(next)
             // 동생이 있는 곳이라면
            if (next===K){
                isDone=true;
            }
        } 
    }
}

let path=[]
let temp=K;

while(temp!==-1){
    path.push(temp)
    temp=way[temp]

}
console.log(visited[K])
console.log(path.reverse().join(&#39; &#39;).trim())</code></pre>
<h3 id="3-해결-방법">3. 해결 방법</h3>
<p>해결 방법은, current를 꺼냈을 때 즉시 검사하는 것이다.</p>
<p>즉, current === K인지 먼저 체크하고, 맞으면 바로 while문을 끊는다.
더 이상 current의 이동(next)들을 만들 필요가 없다.</p>
<p>for문 돌기 전에 current === K 여부를 먼저 검사하고, 맞으면 바로 isDone = true 하고 break하여 불필요한 next push 없이 탈출</p>
<h3 id="정답-코드">정답 코드</h3>
<pre><code class="language-javascript">const { start } = require(&quot;repl&quot;);
const filePath = process.platform === &quot;linux&quot; ? &quot;/dev/stdin&quot; : &quot;./예제.txt&quot;;
let input = require(&quot;fs&quot;).readFileSync(filePath).toString().trim().split(&quot;\n&quot;);

const [N, K]=input.shift().split(&#39; &#39;).map((el)=&gt;+el);

let visited=new Array(100001).fill(-1);
let way=new Array(100001).fill(-1);

let queue=[];
queue.push(N);
visited[N]=0;

let isDone=false;

while(isDone===false){
    let current=queue.shift();
     if (current===K){
                isDone=true;
     }

    for (let next of [current+1, current-1, current*2]){
        if (next&lt;0||next&gt;=100001){
            continue;
        }

        if (visited[next]===-1){
            visited[next]=visited[current]+1;
            way[next]=current;
            queue.push(next)         
        } 
    }
}

let path=[]
let temp=K;

while(temp!==-1){
    path.push(temp)
    temp=way[temp]

}
console.log(visited[K])
console.log(path.reverse().join(&#39; &#39;).trim())</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ESLint] ESLint 개념, 적용 방법]]></title>
            <link>https://velog.io/@vanesa_kim/ESLint-ESLint-%EA%B0%9C%EB%85%90-%EC%A0%81%EC%9A%A9-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@vanesa_kim/ESLint-ESLint-%EA%B0%9C%EB%85%90-%EC%A0%81%EC%9A%A9-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Thu, 10 Oct 2024 08:20:19 GMT</pubDate>
            <description><![CDATA[<h2 id="eslint란">ESLint란?</h2>
<p><img src="https://velog.velcdn.com/images/vanesa_kim/post/b0899066-6158-439f-9ac0-5859d978a039/image.png" alt="">
ESLint 공식 홈페이지에서 캡쳐해왔다. 굉장히 인상적인 소개 방법이다.</p>
<p>Find and fix problems in your JavaScript code
ESLint statically analyzes your code to quickly find problems. It is built into most text editors and you can run ESLint as part of your continuous integration pipeline.</p>
<p>Many problems ESLint finds can be automatically fixed. ESLint fixes are syntax-aware so you won&#39;t experience errors introduced by traditional find-and-replace algorithms.</p>
<p>Preprocess code, use custom parsers, and write your own rules that work alongside ESLint&#39;s built-in rules. Customize ESLint to work exactly the way you need it for your project.</p>
<p>공홈 설명은 이러하다. <a href="https://f-lab.kr/insight/why-use-eslint-and-prettier-together">f-Lab</a>는 &#39;자바스크립트 코드에서 발견될 수 있는 문제점을 정적 분석을 통해 찾아내고, 코드 품질과 관련된 문제를 보고하는 도구입니다. ESLint는 코드의 버그를 예방하고, 코드 스타일 가이드를 강제하는 데 사용됩니다.&#39;라고 설명한다.</p>
<p>협업할 때 코드 일관성을 유지하는 데에 큰 도움이 될 것으로 보인다.</p>
<p>궁금해져서 찾아봤는데 ESLint의 ES는 ECMAScript에서 따온 것이라고 한다.</p>
<h2 id="eslint-적용-시작quick-start">ESLint 적용 시작(Quick start)</h2>
<p>** 📢 주의사항 **</p>
<ul>
<li>Node.js(^18.18.0, ^20.9.0, or &gt;=21.1.0) 필요</li>
<li>built with SSL support(Node.js에서 공식으로 배포한 걸 사용하고 있다면 SSL은 자동으로 들어가 있다.)</li>
</ul>
<h3 id="1-eslint-설치">1. ESLint 설치</h3>
<pre><code class="language-javascript">npm init @eslint/config@latest
yarn create @eslint/config
pnpm create @eslint/config@latest```
셋 중 하나를 돌린다.

만약 공유해야 하는&amp;공유 가능한&amp;npm에서 호스트중인 config가 있다면 아래를 돌린다.
📢** npm init @eslint/config를 사용하려면 package.json이 있어야 한다. 없다면 npm init이나 yarn init을 먼저 돌려야 한다. **
```javascript
# use `eslint-config-standard` shared config
# npm 7+
npm init @eslint/config@latest -- --config eslint-config-standard</code></pre>
<p>이게 완료되면</p>
<pre><code class="language-javascript">npx eslint yourfile.js
yarn run eslint yourfile.js</code></pre>
<p>둘 중 하나를 돌리면 된다.</p>
<p>나는 없어서 스킵했다.</p>
<h3 id="2-eslint-설정">2. ESLint 설정</h3>
<p>📢** 9.0.0 이하의 버전을 사용중이면 ESLint 사이트에서 <a href="https://eslint.org/docs/latest/use/configure/migration-guide">migration guide</a>를 먼저 보라고 한다 **</p>
<p><code>npm init @eslint/config</code>를 돌리면 <code>eslint.config.js</code> 혹은 <code>eslint.config.mjs</code>가 생긴다. 이 파일을 열어보면</p>
<pre><code class="language-javascript">// eslint.config.js
export default [
    {
        rules: {
            &quot;no-unused-vars&quot;: &quot;error&quot;,
            &quot;no-undef&quot;: &quot;error&quot;
        }
    }
];</code></pre>
<p>이런 코드가 있다.
no-unused-vars랑 no-undef는 ESLint 규칙 중 일부다. 이 규칙들은 코드에서 사용되지 않는 변수나 정의되지 않은 변수에 대한 검사를 수행한다.</p>
<p>해당 규칙의 에러 레벨을 통해 경고(warning)나 에러(error)로 동작하게 할 수 있다.</p>
<pre><code class="language-javascript">&quot;off&quot; 혹은 0: 해당 규칙을 끄는 설정. ESLint가 이 규칙을 적용하지 않는다.
&quot;warn&quot; 혹은 1: 경고로 규칙을 활성화. 규칙 위반 시 경고 메시지를 출력하지만, exit code에는 영향을 미치지 않는다.
&quot;error&quot; 혹은 2: 에러로 규칙을 활성화. 규칙 위반 시 에러 메시지를 출력하며, exit code는 1이 된다.</code></pre>
<p>그러므로 위 <code>eslint.config.js</code>코드는 코드에서 사용되지 않는 변수가 있으면 에러로 처리하고, 코드에서 정의되지 않은 변수를 사용하면 에러로 처리하도록 하는 것이다.</p>
<hr>
<p>일단 나는 eslint를 처음 사용하는 것이기 때문에 가장 많이 사용하는 Airbnb ESLint 규칙을 따르기로 했다. 그리고 Prettier과 통합하기로 했다.
사실 나는 그 동안 VsCode의 beautify를 사용하여 코드 포맷팅을 했는데 어느 순간부터 Prettier이 표준이 되어 버린 것 같아서 이번 기회에 써보려고 한다.</p>
<pre><code class="language-javascript">npx install-peerdeps --dev eslint-config-airbnb</code></pre>
<pre><code class="language-javascript">npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier</code></pre>
<p>Prettier를 설치했으면 루트 디렉토리에 .prettierrc를 만들어서 코드 포맷을 적어주면 된다.</p>
<pre><code class="language-javascript">{
  &quot;semi&quot;: true,           // 세미콜론을 사용
  &quot;singleQuote&quot;: true,     // 작은따옴표를 사용
  &quot;printWidth&quot;: 80,        // 한 줄의 최대 길이 80자
  &quot;tabWidth&quot;: 2,           // 들여쓰기 2칸
  &quot;trailingComma&quot;: &quot;all&quot;,  // 후행 콤마 사용
  &quot;arrowParens&quot;: &quot;always&quot;  // 화살표 함수에서 항상 괄호 사용
}</code></pre>
<p>나의 경우에는 이렇게 했다.</p>
<p>그리고 eslint.config.mjs파일을 설정해주면 된다.</p>
<pre><code class="language-javascript">import globals from &#39;globals&#39;;
import pluginJs from &#39;@eslint/js&#39;;
import tseslint from &#39;typescript-eslint&#39;;
import pluginReact from &#39;eslint-plugin-react&#39;;
import prettierConfig from &#39;eslint-config-prettier&#39;;
import prettierPlugin from &#39;eslint-plugin-prettier&#39;;
import airbnb from &#39;eslint-config-airbnb&#39;;

export default [
  { files: [&#39;**/*.{js,mjs,cjs,ts,jsx,tsx}&#39;] },
  { languageOptions: { globals: { ...globals.browser, ...globals.node } } },
  pluginJs.configs.recommended,
  ...tseslint.configs.recommended,
  pluginReact.configs.flat.recommended,
  airbnb,
  {
    plugins: {
      prettier: prettierPlugin,
    },
    extends: [
      &#39;airbnb&#39;, // Airbnb 스타일 가이드 적용
      pluginJs.configs.recommended,
      ...tseslint.configs.recommended,
      pluginReact.configs.flat.recommended,
      &#39;prettier&#39;, // Prettier 규칙을 비활성화
      &#39;plugin:prettier/recommended&#39;, // Prettier를 ESLint 규칙으로 통합
    ],
    rules: {
      &#39;prettier/prettier&#39;: [
        &#39;error&#39;,
        {
          endOfLine: &#39;auto&#39;,
        },
      ],
      &#39;no-console&#39;: &#39;warn&#39;,
      &#39;react/react-in-jsx-scope&#39;: &#39;off&#39;,
      &#39;@typescript-eslint/no-unused-vars&#39;: &#39;warn&#39;,
      &#39;import/prefer-default-export&#39;: &#39;off&#39;,
    },
  },
];</code></pre>
<p>우선은 이렇게 했다.</p>
<h3 id="3-설정-확인">3. 설정 확인</h3>
<p>eslint가 설정한대로 제대로 작동하는지 확인하려면
package.json에서
scripts에     </p>
<pre><code class="language-javascript">&quot;lint&quot;: &quot;eslint . --ext .js,.jsx,.ts,.tsx&quot;,
&quot;format&quot;: &quot;prettier --write .&quot;,</code></pre>
<p> 를 추가하면 된다.
 이걸 추가하면 npm run lint로 eslint 적용 상태를 확인할 수 있다.
 그리고 만약 이걸 돌리고 나서 자동으로 수정할 수 있는 문제들이 있다고 뜨면 npx eslint --fix를 돌려주면 된다. 나머지는 수동 수정해야 한다.</p>
<p> 아 그리고 prettier 확인하려면
 npx prettier --write .
 이걸 돌려주면 된다.</p>
<hr/>
<br/>

<p>eslint 검색하다가 <a href="https://techblog.woowahan.com/15903/">배민에서 airbnb 규칙이 너무 엄격하다고 eslint를 수정하는 글</a>을 보았는데. 나는 일단 eslint 초보니까 airbnb 먼저 써보려고 한다. 쓰다가 너무 제약이 많다 싶으면 하나씩 조정해 나갈 예정이다.</p>
<p>참고자료
<a href="https://f-lab.kr/insight/why-use-eslint-and-prettier-together">https://f-lab.kr/insight/why-use-eslint-and-prettier-together</a>
ESLint 공식 홈페이지</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React.js, TypeScript] 무한스크롤 page 누락 문제 해결하기]]></title>
            <link>https://velog.io/@vanesa_kim/React.js-TypeScript-%EB%AC%B4%ED%95%9C%EC%8A%A4%ED%81%AC%EB%A1%A4-page-%EB%88%84%EB%9D%BD-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@vanesa_kim/React.js-TypeScript-%EB%AC%B4%ED%95%9C%EC%8A%A4%ED%81%AC%EB%A1%A4-page-%EB%88%84%EB%9D%BD-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 10 Oct 2024 08:19:06 GMT</pubDate>
            <description><![CDATA[<h2 id="🚨🚨-문제-상황-🚨🚨">🚨🚨 문제 상황 🚨🚨</h2>
<p>무한스크롤 시 page가 1씩 늘어나야 하는데, 사진과 같이 누락되는 page값이 발생하였다.
<img src="https://velog.velcdn.com/images/vanesa_kim/post/b7c4f063-f633-457e-a757-31b6849473a7/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/vanesa_kim/post/668837c9-80ae-46e5-9e34-109ca650c506/image.png" alt=""></p>
<p>page가 1 이상인 상태에서 검색어를 변경하면 page가 1부터 불러와진다. 0부터 시작해야 하는데</p>
<p>일단 내 코드를 보면</p>
<pre><code class="language-javascript">  useEffect(() =&gt; {
    setPerfumePage(0);
    setPerfumeSearchResultList([]);
    const debounce = setTimeout(() =&gt; {
      if (perfumeSearchKeyword.length &gt; 1) {
        getPerfumeSearchResult();
      } else {
        setPerfumeSearchResultList([]);
        setPerfumeListToggle(false);
      }
    }, 200);
    return () =&gt; {
      clearTimeout(debounce);
    };
  }, [perfumeSearchKeyword]);

  useEffect(() =&gt; {
    const handleScroll = () =&gt; {
      if (brandListRef.current) {
        const { scrollTop, scrollHeight, clientHeight } = brandListRef.current;
        const threshold = 20;

        if (scrollTop + clientHeight + threshold &gt;= scrollHeight) {
          if (
            perfumeSearchResultTotal &amp;&amp;
            perfumePage * 10 &lt; perfumeSearchResultTotal
          ) {
            setPerfumePage((prevPage) =&gt; prevPage + 1);
          }
        }
      }
    };

    const refCurrent = brandListRef.current;
    refCurrent?.addEventListener(&quot;scroll&quot;, handleScroll);

    return () =&gt; {
      refCurrent?.removeEventListener(&quot;scroll&quot;, handleScroll);
    };
  }, [perfumePage, perfumeSearchResultTotal, perfumeSearchResultList]);</code></pre>
<p>이렇다. setTimeout을 200으로 두고 두 번째 useEffect에서 무한스크롤 threshold를 20으로 두었는데, 저 문제가 발생하는 것이다.</p>
<h2 id="🚑🚑-문제-해결-🚑🚑">🚑🚑 문제 해결 🚑🚑</h2>
<p>문제 해결 시도 중 setTimeout은 300, threshold는 10
<img src="https://velog.velcdn.com/images/vanesa_kim/post/274e6281-1777-42b1-a0b4-d4e804ad5896/image.png" alt=""></p>
<p>빠진 페이지 없이 무한 스크롤은 잘 된다.
근데!
<img src="https://velog.velcdn.com/images/vanesa_kim/post/acc5d5aa-5cb6-4f9d-b71b-b9b9a90fc5dd/image.png" alt="">
검색어를 바꿨으면 page가 0에서 시작해야지 왜 자꾸 1에서 시작하는거지???
혹시 page가 setPage((prevPage)=&gt;prevPage+1) 속도가 api 호출 속도보다 더 빠른가?
--&gt; threshold를 줄여서 api를 좀 천천히? 호출해보자.
(근데 나는... 바닥에 닿기 전에 미리 호출해서 사용자가 보기에는 물 흐르듯 쭉쭉 보여주고 싶은데)</p>
<h2 id="🚑🚑-문제-해결-🚑🚑-1">🚑🚑 문제 해결 🚑🚑</h2>
<p>문제 해결 시도 중.... setTimeout은 300, threshold는 5
<img src="https://velog.velcdn.com/images/vanesa_kim/post/75973cfc-2a1a-40ee-914b-9ea1b9c43cb5/image.png" alt=""></p>
<p>드디어 되었다.</p>
<p>문제는 해결이 되었지만 UX는 별로인 것 같다. 이 부분은 좀 더 방법을 찾아봐야겠다.
... to be continued</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MySQL] MySQL Workbench has encountered a problem.]]></title>
            <link>https://velog.io/@vanesa_kim/MySQL-MySQL-Workbench-has-encountered-a-problem</link>
            <guid>https://velog.io/@vanesa_kim/MySQL-MySQL-Workbench-has-encountered-a-problem</guid>
            <pubDate>Thu, 10 Oct 2024 08:18:11 GMT</pubDate>
            <description><![CDATA[<h2 id="🚒-문제-상황-🚒">🚒 문제 상황 🚒</h2>
<p>아무것도 하지 않았는데 MySQL 워크벤치에 들어가서 접속하려 하니</p>
<pre><code class="language-javascript">MySQL Workbench has encountered a problem.
외부 구성 요소에서 예외를 Throw 했습니다.

We are sorry~~~~~.
bug report 해줘라~~~~.</code></pre>
<p>이런 에러가 떴다.
분명 전날까지만 해도 잘 썼는데 갑자기????</p>
<p>별거 아닐거라 생각하고 컴퓨터를 껐다가 켰는데도 안되었다...!</p>
<h2 id="🛴-해결을-위한-노력-🛴">🛴 해결을 위한 노력 🛴</h2>
<ol>
<li>지난번에 mySQL 서비스가 종료되어서 실행이 되지 않았던 적이 있어서 &#39;서비스&#39;를 들어가서 MySQL80을 껐다가 다시 켜주었다.
<img src="https://velog.velcdn.com/images/vanesa_kim/post/c198666d-8b30-4b24-a67b-b504d2e0c44b/image.png" alt="">
근데 실패</li>
</ol>
<ol start="2">
<li><p>구글링해보니 뭐 Program Files에 들어가서 어떤 파일을 덮어쓰라고 했는데 데이터 날아갈까봐 무서워서 다른 방법을 먼저 시도해보기로 했다.
계속 구글링하다 보니 workbench를 재설치하면 된다고 해서
<a href="https://velog.io/@0woy_/MySQL-1-WorkBench-has-encountered-a-problem-%EB%A7%A4%EA%B0%9C%EB%B3%80%EC%88%98%EA%B0%80-%EC%9E%98%EB%AA%BB%EB%90%98%EC%97%88%EC%8A%B5%EB%8B%88%EB%8B%A4">https://velog.io/@0woy_/MySQL-1-WorkBench-has-encountered-a-problem-%EB%A7%A4%EA%B0%9C%EB%B3%80%EC%88%98%EA%B0%80-%EC%9E%98%EB%AA%BB%EB%90%98%EC%97%88%EC%8A%B5%EB%8B%88%EB%8B%A4</a>
이 글을 보고 그대로 따라했다.
분명 Repair를 했는데도 안되더라.</p>
</li>
<li><p>그래서 결국 mySQL 버그  목록을 들어가서 검색을 해보았는데 버전 문제일 수도 있다고 해서 MySQL Installer - Community 에 들어가서 워크벤치를 최신 버전으로 업데이트해주었다.
<img src="https://velog.velcdn.com/images/vanesa_kim/post/df73a50b-a4f9-4b6a-be4c-21fd17ff8f7f/image.png" alt="">
하지만 역시나 안되더라...</p>
</li>
</ol>
<h2 id="🚑-해결-방법-🚑">🚑 해결 방법 🚑</h2>
<p>혹시!!! mariadb가 켜져 있나 싶어서 cmd에 net stop mariadb를 입력했다.
분명 나는 mariadb를 켠 적이 없는데 얘가 실행중이었다.
바로 cmd창을 켜서 netstat -ano를 입력했다.
<img src="https://velog.velcdn.com/images/vanesa_kim/post/9212b4fa-038d-4b75-9f4d-949c60c3579c/image.png" alt="">
3306번 포트를 사용 중인 서비스의 PID를 확인한 다음에
작업관리자(ctrl+alt+delete) - 세부 정보를 들어가서
아까 확인한 PID값을 찾아서 그 mySQL 프로그램을 죽여주었다.</p>
<p>얘를 죽여주니까 워크벤치 db에 접속할 수 있었다~!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[html 태그 적재적소에 사용하기]]></title>
            <link>https://velog.io/@vanesa_kim/html-%ED%83%9C%EA%B7%B8-%EC%A0%81%EC%9E%AC%EC%A0%81%EC%86%8C%EC%97%90-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@vanesa_kim/html-%ED%83%9C%EA%B7%B8-%EC%A0%81%EC%9E%AC%EC%A0%81%EC%86%8C%EC%97%90-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 15 Sep 2024 06:09:28 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/vanesa_kim/post/f6b08d80-67ae-421b-9edb-acb9455116fd/image.png" alt=""></p>
<p><code>&lt;NoteTitle&gt;</code>을 div로 만드는 것.
잘못된 것이지.
인턴을 하기 전에는 <code>&lt;button&gt;</code> <code>&lt;nav&gt;</code> <code>&lt;article&gt;</code> 이런 태그 모두 잘 사용했다. 이런 태그를 사용하는 이유는 모르고 그냥 그렇게 가르쳤으니까 그렇게 썼다.</p>
<p>근데 인턴을 하면서 내가 <code>&lt;button&gt;</code> 태그를 사용하는 걸 보고 몇몇 분들께서 나보고 왜 <code>&lt;div&gt;</code>를 사용하지 않냐고 <code>&lt;div&gt;</code>가 훨씬 편하다고 하셨다. 그래서 나는 아 현업에서는 그냥 디자인대로 빠르게 만들어내는 것이 중요하니까 웬만해선 <code>&lt;div&gt;</code>를 쓰는구나... 라고 잘못된 인식을 가지게 된다.</p>
<p>이후로 나는 웬만해서는 <code>&lt;div&gt;</code>로 모든 걸 만들어내는 무지성<code>&lt;div&gt;</code>러가 되었다.</p>
<p>하지만 내가 정말 잘 만들어서 상용화하는 프로젝트를 리팩토링하기 위해 고민을 하던 중 이게 맞나?라는 생각이 들기 시작했다.</p>
<p>모든 것이 <code>&lt;div&gt;</code> 하나로 쉽게 만들어질 수 있다면 우리가 html을 공부할 때 왜 그렇게 많은 태그를 배우는 것일까?</p>
<p>그래서 많이 늦었지만...^^ 찾아보기 시작했다.
그리고 아 이대로 가면 큰일나겠구나를 깨달았다.
html 태그를 적재적소에 사용하면 바로 seo를 강화한다고 한다.
생각해보니 당연하다. 검색 결과 리스트 정렬을 방문자수 하나로 정렬할 수는 없지 않은가.</p>
<p>인턴 때 담당한 프로젝트는 워낙 MAU 수치가 높기 때문에 그 업계에선 이미 널리 알려진 사이트였기 때문에 별 상관이 없었을 수도 있다. 하지만 내가 지금 사용하는 프로젝트는 그렇지 않다!! 실사용자 0에서 시작해야 하기 때문에 무조건 상위 노출이 필요한다.</p>
<p>그래서 이제 나는 리팩토링을 하면서 seo 최적화를 위해 html 태그부터 정확시 사용할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[basic CSS] width가 내가 정한 것보다 크게 나온다 (ft. box-sizing: border-box)]]></title>
            <link>https://velog.io/@vanesa_kim/input-%ED%81%AC%EA%B8%B0%EA%B0%80-%EB%82%B4%EA%B0%80-%EC%9B%90%ED%95%98%EB%8A%94-%EA%B2%83%EB%B3%B4%EB%8B%A4-%ED%81%AC%EA%B2%8C-%EB%82%98%EC%98%A8%EB%8B%A4</link>
            <guid>https://velog.io/@vanesa_kim/input-%ED%81%AC%EA%B8%B0%EA%B0%80-%EB%82%B4%EA%B0%80-%EC%9B%90%ED%95%98%EB%8A%94-%EA%B2%83%EB%B3%B4%EB%8B%A4-%ED%81%AC%EA%B2%8C-%EB%82%98%EC%98%A8%EB%8B%A4</guid>
            <pubDate>Sun, 15 Sep 2024 05:14:25 GMT</pubDate>
            <description><![CDATA[<h3 id="🚨🚨-문제-상황-🚨🚨">🚨🚨 문제 상황 🚨🚨</h3>
<p><img src="https://velog.velcdn.com/images/vanesa_kim/post/c15a25de-1ff6-4c09-8a65-e138013af58d/image.png" alt=""></p>
<p>난 width를 220px으로 주었는데 227.56px이 되어서 나온다</p>
<h3 id="🚑🚑-해결-방법-🚑🚑">🚑🚑 해결 방법 🚑🚑</h3>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/vanesa_kim/post/df11a00b-67fc-4e54-b281-84fa21380187/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/vanesa_kim/post/d5d750b8-148e-4f26-9b6e-2dc75645ef43/image.png" alt=""></th>
</tr>
</thead>
</table>
<p>(style 파일에는 250px이라고 되어 있으나 인라인으로 220px 설정해줌)</p>
<p><code>box-sizing: border-box;</code></p>
<h3 id="🚑🚑-원인-🚑🚑">🚑🚑 원인 🚑🚑</h3>
<p>원인은 다음과 같았다. CSS를 지금까지 어떻게 해온건지 부끄럽고 자괴감이 들지만 일단 적어둔다.</p>
<p>기본적으로 width는 요소의 padding과 border를 포함시킨다. box-sizing 속성의 기본값은 content-box인데, 이것은 지정한 width가 컨텐츠 영역만을 의미한다. 그러니까 padding과 border는 컨텐츠 영역과 별개의 크기로 계산되는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/vanesa_kim/post/db348734-cd7b-4fa4-875c-e2f57f0a7bad/image.png" alt="">
(예전에 이 사진을 보며 공부할 때는 이 당연한 걸 왜 이렇게 강조하나 했다.)</p>
<p>해결 방법에 있는 첫 번째 사진을 보면 오른쪽, 왼쪽 padding이 2px이다. 220+2+2+(border값) 대충 계산하면 227.56이 나올 만하다. 따라서 width를 padding, border값 포함한 값으로 사용하고 싶으면 <code>box-sizing:border-box;</code> 설정을 꼭 해주자.</p>
<p>지금까지 이것도 모르고 padding이 있을 경우에는 디자인상 width값 - padding값 을 해서 width에 지정을 해주었는데. 아주 부끄럽다. 예전에 CSS 공부할 때 이건 정말 쓸모 없어 보인다 생각했는데. 전혀 이해를 하지 못해서 그런 것이었다. 아이 부끄럽군.</p>
<h4 id="📚참고">📚참고</h4>
<p><a href="https://juicyjerry.tistory.com/entry/css%EC%97%90%EC%84%9C-box-sizing-%EC%9D%84-border-box%EB%A1%9C-%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0">https://juicyjerry.tistory.com/entry/css%EC%97%90%EC%84%9C-box-sizing-%EC%9D%84-border-box%EB%A1%9C-%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</a>
<a href="https://velog.io/@soryeongk/CSSBasic">https://velog.io/@soryeongk/CSSBasic</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_box_model/Introduction_to_the_CSS_box_model">https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_box_model/Introduction_to_the_CSS_box_model</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Query did not return a unique result: 2 results were returned && 431 (Request Header Fields Too Large)]]></title>
            <link>https://velog.io/@vanesa_kim/500%EB%B2%88-%EC%97%90%EB%9F%AC-org.springframework.dao.IncorrectResultSizeDataAccessException-Query-did-not-return-a-unique-result-2-results-were-returned-431-Request-Header-Fields-Too-Large</link>
            <guid>https://velog.io/@vanesa_kim/500%EB%B2%88-%EC%97%90%EB%9F%AC-org.springframework.dao.IncorrectResultSizeDataAccessException-Query-did-not-return-a-unique-result-2-results-were-returned-431-Request-Header-Fields-Too-Large</guid>
            <pubDate>Sun, 15 Sep 2024 03:13:10 GMT</pubDate>
            <description><![CDATA[<p>🚨🚨 문제 상황 🚨🚨
500번 에러 org.springframework.dao.IncorrectResultSizeDataAccessException: Query did not return a unique result: 2 results were returned &amp;&amp; 431 (Request Header Fields Too Large)</p>
<p>🚑🚑 원인 &amp; 해결 방법 🚑🚑</p>
<p>2 results were returned
리턴값이 하나만 나와야하는데 db에 같은 generationNum을 가진 데이터가 두 개 있었던 거 같네요
--&gt; 그냥 db밀어버림</p>
<p>++놀랍게도 주석 처리를 해도 에러가 계속되는... 주석도 지워야 에러가 안 난다.
<img src="https://velog.velcdn.com/images/vanesa_kim/post/77db9d11-b860-4060-b917-a0a98d6e4ee9/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js 14] 엔터 키, 버튼 클릭 시 동일 이벤트 발생하도록 해보기]]></title>
            <link>https://velog.io/@vanesa_kim/Next.js-14-%EC%97%94%ED%84%B0-%ED%82%A4-%EB%B2%84%ED%8A%BC-%ED%81%B4%EB%A6%AD-%EC%8B%9C-%EB%8F%99%EC%9D%BC-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8F%84%EB%A1%9D-%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@vanesa_kim/Next.js-14-%EC%97%94%ED%84%B0-%ED%82%A4-%EB%B2%84%ED%8A%BC-%ED%81%B4%EB%A6%AD-%EC%8B%9C-%EB%8F%99%EC%9D%BC-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8F%84%EB%A1%9D-%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 17 Aug 2024 08:52:18 GMT</pubDate>
            <description><![CDATA[<h2 id="🚨🚨-문제-상황-🚨🚨">🚨🚨 문제 상황 🚨🚨</h2>
<p><code>&lt;InputText/&gt;</code> 에서 엔터 키를 누를 때, <code>&lt;CommonButton/&gt;</code>을 누를 때, handleSignup을 호출하고 싶음.</p>
<p>그런데, 엔터 키 이벤트가 정상 동작하면 클릭 이벤트가 동작하지 않고, 클릭 이벤트가 정상 동작하며 엔터 키 이벤트가 동작하지 않는다.</p>
<p>다음은 문제가 되었던 코드의 일부분이다.</p>
<h4 id="아래-코드-설명">아래 코드 설명</h4>
<p>page.tsx에서 onSubmit을 form 태그에 직접 설정해서, 버튼 클릭 시, 엔터 입력 시 폼이 제출되도록 했다.
버튼 클릭 시 폼이 자동으로 제출되도록 CommonButton의<code>&lt;button&gt;</code>태그 type을 따로 설정하지 않았다.(button 태그의 기본 type은 submit이므로)
handleSignup함수는 e.preventDefault()로 폼의 기본 동작(새로고침)을 방지하였다.</p>
<h4 id="pagetsx">page.tsx</h4>
<pre><code class="language-javascript">const handleSignup = async (e: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; {
    e.preventDefault(); // 폼의 기본 동작(새로고침) 방지

    if (!data.email || !data.password || data.password !== data.password2) {
      alert(&#39;입력값을 확인해주세요&#39;);
      return;
    }
    try {
      setLoading(true);
      const result = await signup(data);
      router.push(&#39;/login&#39;);
    } catch (err) {
      console.error(&#39;Signup error:&#39;, err);
    } finally {
      setLoading(false);
    }
  };

 return (
      &lt;form className=&quot;w-full flex flex-col items-center&quot; 
   onSubmit={handleSignup}&gt;
        &lt;InputArea data={data} setData={setData} /&gt;
        &lt;CommonButton/&gt;
      &lt;/form&gt;
  );
};</code></pre>
<h4 id="inputareatsx">InputArea.tsx</h4>
<pre><code class="language-javascript">interface InputAreaProps {
  data: SignupData;
  setData: React.Dispatch&lt;React.SetStateAction&lt;SignupData&gt;&gt;;
}
const InputArea = ({ data, setData }: InputAreaProps) =&gt; {
  const handleChange = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    const { name, value } = e.target;
    setData((prev) =&gt; ({ ...prev, [name]: value }));
  };

  return (
    &lt;form&gt;
      &lt;InputText 
            onChange={handleChange}
      /&gt;
    &lt;/form&gt;
  );
};

export default InputArea;
</code></pre>
<h4 id="commonbuttontsx">CommonButton.tsx</h4>
<pre><code class="language-javascript">const CommonButton = ({
}: ButtonProps) =&gt; {
  return (
    &lt;button
    &gt;
      {text}
    &lt;/button&gt;
  );
};

export default CommonButton;</code></pre>
<p>이렇게 하면 CommonButton을 클릭해도 아무 일이 일어나지 않는다.
(엔터 키를 누르면 handleSignup 정상 동작)</p>
<h2 id="🚑🚑-원인--해결-방법-🚑🚑">🚑🚑 원인 &amp; 해결 방법 🚑🚑</h2>
<p><code>page.tsx</code> </p>
<ol>
<li><code>&lt;form&gt;</code>에서 <code>&lt;CommonButton&gt;</code>을 빼버렸다. </li>
<li><code>handleSignup</code> 함수의 파라미터 타입을 두 개 넣어주었다.(마우스 이벤트와 폼 이벤트를 모두 받을 수 있도록)</li>
</ol>
<p><code>InputArea.tsx</code></p>
<ol>
<li><code>&lt;form&gt;</code>이 중복되고 있어서 빼 주었다.</li>
</ol>
<p><code>CommonButton.tsx</code></p>
<ol>
<li>button 태그의 type을 button으로 설정했다.(form 태그에서 벗어났기 때문)</li>
<li>onClick 이벤트를 따로 추가해주었다.</li>
</ol>
<h4 id="pagetsx-1">page.tsx</h4>
<pre><code class="language-javascript">  const handleSignup = async (
    e?: React.FormEvent&lt;HTMLFormElement&gt; | React.MouseEvent&lt;HTMLButtonElement&gt;,
  ) =&gt; {
    if (e &amp;&amp; (e as React.FormEvent&lt;HTMLFormElement&gt;).preventDefault) {
      (e as React.FormEvent&lt;HTMLFormElement&gt;).preventDefault(); 
    }
    if (
      !data.email ||
      emailRegEx.test(data.email) === false ||
      !data.password ||
      passwordRegEx.test(data.password) === false ||
      data.password !== data.password2
    ) {
      alert(&#39;입력값을 확인해주세요&#39;);
      return;
    }

    try {
      setLoading(true);
      const result = await signup(data);
      router.push(&#39;/login&#39;);
    } catch (err) {
      console.error(&#39;Signup error:&#39;, err);
    } finally {
      setLoading(false);
    }
  };



    &lt;form
        className=&quot;w-full flex flex-col items-center&quot;
        onSubmit={handleSignup}
     &gt;
        &lt;InputArea data={data} setData={setData} /&gt;
    &lt;/form&gt;
    &lt;CommonButton
       text=&quot;회원가입&quot;
       onClickEvent={handleSignup}
    /&gt;
</code></pre>
<h4 id="inputareatsx-1">InputArea.tsx</h4>
<pre><code class="language-javascript">interface InputAreaProps {
  data: SignupData;
  setData: React.Dispatch&lt;React.SetStateAction&lt;SignupData&gt;&gt;;
}
const InputArea = ({ data, setData }: InputAreaProps) =&gt; {
  const handleChange = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    const { name, value } = e.target;
    setData((prev) =&gt; ({ ...prev, [name]: value }));
  };

  return (
    &lt;&gt;
      &lt;InputText 
        onChange={handleChange}
      /&gt;
    &lt;/&gt;
  );
};

export default InputArea;
</code></pre>
<h4 id="commonbuttontsx-1">CommonButton.tsx</h4>
<pre><code class="language-javascript">interface CommonButtonProps {
  onClickEvent?: (e: React.MouseEvent&lt;HTMLButtonElement&gt;) =&gt; void; 
}

const CommonButton = ({
  onClickEvent,
}: CommonButtonProps) =&gt; {
  return (
    &lt;button
      type=&quot;button&quot;
      onClick={onClickEvent}
    &gt;
      {text}
    &lt;/button&gt;
  );
};
</code></pre>
<h4 id="예상-원인">예상 원인</h4>
<p>확실하진 않은데 추측해보자면
CommonButton의 버튼 type이 &#39;submit&#39;인데 onClickEvent를 사용했기 때문에 혼란이 야기되었기 때문이다.
CommonButton에 onClickEvent를 따로 내려주지 않았지만 공통 컴포넌트에 onClickEvent 속성이 있어서 문제가 된 듯하다.
<img src="https://velog.velcdn.com/images/vanesa_kim/post/66827f61-7bf1-4df3-bc06-3a96ad2483eb/image.png" alt="">
<img src="https://velog.velcdn.com/images/vanesa_kim/post/c2d06d22-66d2-40dc-b2c0-d6e5ed0ae009/image.png" alt="">
맞는 것 같다. 왜냐면 캡쳐화면처럼 하면 두 이벤트 모두 정상동작하기 때문이다.</p>
]]></description>
        </item>
    </channel>
</rss>