<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>line_jeong32.log</title>
        <link>https://velog.io/</link>
        <description>starter</description>
        <lastBuildDate>Sun, 22 Oct 2023 09:01:30 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>line_jeong32.log</title>
            <url>https://velog.velcdn.com/images/line_jeong32/profile/a6d6b96c-542d-4deb-9d45-791af187d24d/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. line_jeong32.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/line_jeong32" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[섹션 5: 문제 해결 패턴]]></title>
            <link>https://velog.io/@line_jeong32/%EC%84%B9%EC%85%98-5-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@line_jeong32/%EC%84%B9%EC%85%98-5-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Sun, 22 Oct 2023 09:01:30 GMT</pubDate>
            <description><![CDATA[<p>빈도수 세기 패턴</p>
<p>naive solution</p>
<pre><code class="language-js">function same(arr1, arr2) {
  if(arr1.length !== arr2.length) {
    return false;
  }
  for(let i=0; i&lt;arr1.length; i++) {
    let correctIndex = arr2.indexOf(arr1[i]**2);
    if(correctIndex === -1) {
      return false;
    }
    arr2.splice(correctIndex, 1)
  }
  return true;
}

same([1, 2, 3], [4, 1, 9]);</code></pre>
<br />

<p>refactored solution</p>
<pre><code class="language-js">function same(arr1, arr2) {
  if(arr1.length !== arr2.length) {
    return false;
  }
  let frequencyCounter1 = {}
  let frequencyCounter2 = {}
  for(let val of arr1) {
    frequencyCounter1[val] = (frequencyCounter1[val] || 0) + 1
  }
  for(let val of arr2) {
    frequencyCounter2[val] = (frequencyCounter2[val] || 0) + 1
  }
  for(let key in frequencyCounter1) {
    if(!(key**2 in frequencyCounter2)) {
      return false
    }
    if(frequencyCounter2[key**2] !== frequencyCounter1[key]) {
      return false
    }
  }
  return true
}

same([1, 2, 3, 2], [9, 1, 4, 4]);</code></pre>
<br>


<p>anagram</p>
<pre><code class="language-js">function validAnagram(str1, str2) {
  if(str1.length !== str2.length) {
    return false
  }
  const frequencyCounter1 = {}
  const frequencyCounter2 = {}
  for(let val of str1) {
    frequencyCounter1[val] = ++frequencyCounter1[val] || 1
  }
  for(let val of str2) {
    frequencyCounter2[val] = ++frequencyCounter2[val] || 1
  }
  for(let key in frequencyCounter1) {
    if(!(key in frequencyCounter2)) {
      return false
    }
    if(frequencyCounter2[key] !== frequencyCounter1[key]) {
      return false
    }
  }
  return true
}

validAnagram(&#39;anagram&#39;, &#39;nagaram&#39;)</code></pre>
<pre><code class="language-js">function validAnagram(first, second) {
  if(first.length !== second.length) {
    return false
  }
  let lookup = {};
  for(let i=0; i&lt;first.length; i++) {
    let letter = first[i];
    lookup[letter] ? lookup[letter] += 1 : lookup[letter] = 1;
  }
  for(let i=0; i&lt;second.length; i++) {
    let letter = second[i];
    if(!lookup[letter]) {
      return false;
      } else {
        lookup[letter] -= 1;
      }
  }
  return true
}

validAnagram(&#39;anagram&#39;, &#39;nagaram&#39;)</code></pre>
<br>

<hr>
<p>MULTIPLE POINTERS</p>
<p>NAIVE SOLUTION</p>
<pre><code class="language-js">function sumZero(arr) {
  for(let i=0; i&lt;arr.length; i++) {
    for(let j=i+1; j&lt;arr.length; j++) {
      if(arr[i]+arr[j] === 0) {
        return [arr[i], arr[j]];
      }
    }
  }
}

sumZero([-3,-2,-1,0,1,2,3]) // [-3,3]</code></pre>
<p>REFACTOR</p>
<pre><code class="language-js">function sumZero(arr) {
  let left = 0;
  let right = arr.length - 1;
  while(left &lt; right) {
    let sum = arr[left] + arr[right];
    if(sum === 0) {
      return [arr[left], arr[right]];
    } else if(sum &gt; 0) {
      right--;
    } else {
      left++;
    }
  }
}

sumZero([-4,-3,-1,0,1,2,5])</code></pre>
<br>

<p>고유값 세기 솔루션</p>
<pre><code class="language-js">function countUnuqieValues(arr) {
    if(arr.length === 0) return 0;
    let i = 0;
    for(let j=1; j&lt;arr.length; j++) {
        if(arr[i] !== arr[j]) {
            i++;
            arr[i] = arr[j];
        }
    }
    return i+1;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[섹션 4: 문제 해결 접근법]]></title>
            <link>https://velog.io/@line_jeong32/%EC%84%B9%EC%85%98-4-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EC%A0%91%EA%B7%BC%EB%B2%95</link>
            <guid>https://velog.io/@line_jeong32/%EC%84%B9%EC%85%98-4-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EC%A0%91%EA%B7%BC%EB%B2%95</guid>
            <pubDate>Sun, 22 Oct 2023 04:15:49 GMT</pubDate>
            <description><![CDATA[<ol>
<li>UnderStand the Problem</li>
<li>Explore Concrete Examples</li>
<li>Break It Down</li>
<li>Solve/Simplify</li>
<li>Look Back and Refactor</li>
</ol>
<br />

<p>regular expression (1)</p>
<pre><code class="language-js">function charCount(str) {
  const obj = {};
  for(let char of str) {
    char = char.toLowerCase();
    if(/[a-z0-9]/.test(char)) {
      if(obj[char] &gt; 0) {
        obj[char]++;
      } else {
        obj[char] = 1;
      }
    }
  }
  return obj;
}
</code></pre>
<br />


<p>regular expression (2) - simpler version</p>
<pre><code class="language-js">function charCount(str) {
  const obj = {};
  for(let char of str) {
    char = char.toLowerCase();
    if(/[a-z0-9]/.test(char)) {
      obj[char] = ++obj[char] || 1;
    }
  }
  return obj;
}</code></pre>
<br/>

<p>charAtCode() - instead of regular expression</p>
<pre><code class="language-js">function charCount(str) {
  const obj = {};
  for(let char of str) {
    if(isAlphaNumeirc(char)) {
      char = char.toLowerCase();
      obj[char] = ++obj[char] || 1;
    }
  }
  return obj;
}

function isAlphaNumeirc(char) {
  let code = char.charCodeAt(0);
  if(!(code&gt;47 &amp;&amp; code&lt;58) &amp;&amp; !(code&gt;64 &amp;&amp; code&lt;91) &amp;&amp; !(code&gt;94 &amp;&amp; code&lt;123)) {
    return false;
  }
  return true;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[번개 모임 웹 어플리케이션 - 회원 탈퇴]]></title>
            <link>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%ED%9A%8C%EC%9B%90-%ED%83%88%ED%87%B4</link>
            <guid>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%ED%9A%8C%EC%9B%90-%ED%83%88%ED%87%B4</guid>
            <pubDate>Wed, 24 May 2023 13:08:06 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[번개 모임 웹 어플리케이션 - 로그아웃]]></title>
            <link>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83</link>
            <guid>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83</guid>
            <pubDate>Tue, 23 May 2023 13:10:19 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[번개 모임 웹 어플리케이션 - 로그인]]></title>
            <link>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%A1%9C%EA%B7%B8%EC%9D%B8</link>
            <guid>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%A1%9C%EA%B7%B8%EC%9D%B8</guid>
            <pubDate>Mon, 22 May 2023 13:45:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[번개 모임 웹 어플리케이션 - 회원 가입 기능 구현 (2)]]></title>
            <link>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%ED%9A%8C%EC%9B%90-%EA%B0%80%EC%9E%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-2</link>
            <guid>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%ED%9A%8C%EC%9B%90-%EA%B0%80%EC%9E%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-2</guid>
            <pubDate>Sun, 21 May 2023 14:17:41 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[번개 모임 웹 어플리케이션 - 회원 가입 기능 구현]]></title>
            <link>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%ED%9A%8C%EC%9B%90-%EA%B0%80%EC%9E%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%ED%9A%8C%EC%9B%90-%EA%B0%80%EC%9E%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sat, 20 May 2023 13:51:15 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[번개 모임 웹 어플리케이션 - 번개 상세 페이지 마크업]]></title>
            <link>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B2%88%EA%B0%9C-%EC%83%81%EC%84%B8-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%88%ED%81%AC%EC%97%85</link>
            <guid>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B2%88%EA%B0%9C-%EC%83%81%EC%84%B8-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%88%ED%81%AC%EC%97%85</guid>
            <pubDate>Fri, 19 May 2023 09:22:41 GMT</pubDate>
            <description><![CDATA[<h2 id="파일구조">파일구조</h2>
<ul>
<li><p><code>pages/BungaeDetail.js</code>
: 번개 카드를 클릭했을 때 해당하는 글 상세 내용이 보이는 페이지</p>
</li>
<li><p><code>components/BungaeDetail/</code></p>
<ul>
<li><code>BungaeDetail.js</code> : <code>&lt;BungaeDetailPage&gt;</code> 컴포넌트의 자식 컴포넌트로, 대부분의 마크업 코드를 포함한다.</li>
</ul>
</li>
</ul>
<br />

<hr>
<h2 id="코드">코드</h2>
<p>모임 장소, 모임 시간, 작성 시간은 서버에서 받은 데이터를 그대로 사용하는 것이 아니라 원하는 형식으로 변경해야 하므로 함수를 사용하는데, 해당 로직을 담은 함수들은 @utils 폴더로 분리해서 가독성을 높였다. </p>
<p>그리고 수정 페이지로 이동할 때는 useNavigate로 처리하는데 이를 이용해 우선 기존 글 내용을 받을 때, state를 전달하는 방법을 택했다. 이렇게 되면 수정 버튼을 눌렀을 때만 state가 전달되므로 상세 페이지에서 수정 버튼을 눌렀을 때만 정상적으로 기존 글 내용을 받아오게 된다. url을 통해 직접 접근할 때는 useLocation으로 받아온 state가 null 값으로 뜬다. 아마 추후 서버에서 받아오는 방법으로 변경해야 할 것 같다.</p>
<br />

<p><img src="https://velog.velcdn.com/images/line_jeong32/post/0ba30597-6f81-4573-b8a2-3841ab99de02/image.png" alt=""></p>
<br />

<p><code>pages/BungaeDetail.js</code></p>
<pre><code class="language-js">import { useEffect, useState } from &quot;react&quot;;

import { dummyBungaeDetail } from &quot;../@constants/dummy&quot;;
import BungaeDetail from &quot;../components/BungaeDetail/BungaeDetail&quot;;
import RootPageContent from &quot;../components/PageContent/RootPageContent&quot;;

function BungaeDetailPage() {
  const [bungaeDetail, setBungaeDetail] = useState({});
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() =&gt; {
    setBungaeDetail(dummyBungaeDetail);
    setIsLoading(false);
  }, []);

  if (isLoading) return;

  return (
    &lt;RootPageContent maxWidth=&quot;md&quot;&gt;
      &lt;BungaeDetail isLoading={isLoading} bungaeDetail={bungaeDetail} /&gt;
    &lt;/RootPageContent&gt;
  );
}

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

<p><code>components/BungaeDetail/BungaeDetail.js</code></p>
<pre><code class="language-js">import { useNavigate } from &quot;react-router-dom&quot;;

import styled from &quot;styled-components&quot;;

import * as bungaeInfoUtil from &quot;../../@utils/bungaeInfo&quot;;
import Button from &quot;../UI/Button&quot;;

const StyledBungaeHeader = styled.section`
  width: 100%;
  margin-bottom: 30px;

  &gt; .bungae-title {
    font-size: ${({ theme }) =&gt; theme.fontSize[&quot;2xl&quot;]};
    font-weight: ${({ theme }) =&gt; theme.fontWeight.bold};
    margin-bottom: 24px;
    line-height: 1.3;
  }
`;

const StyledUnderscoreContainer = styled.div`
  display: flex;
  justify-content: space-between;
  padding-bottom: 24px;
  border-bottom: 2px solid ${({ theme }) =&gt; theme.palette.gray2};

  &gt; .bungae-owner-and-ago {
    display: flex;
    align-items: center;
    gap: 16px;
    color: ${({ theme }) =&gt; theme.palette.gray5};

    &gt; .bungae-owner {
      padding-right: 16px;
      border-right: 2px solid ${({ theme }) =&gt; theme.palette.gray2};
      color: ${({ theme }) =&gt; theme.palette.black};
    }
  }

  &gt; .bungae-edit-and-delete {
    display: flex;
    align-items: center;
    gap: 10px;
    font-weight: ${({ theme }) =&gt; theme.fontWeight.semiBold};
    color: ${({ theme }) =&gt; theme.palette.mainNavy};
  }
`;

const StyledBungaeInfoContainer = styled.ul`
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 20px;
  font-size: ${({ theme }) =&gt; theme.fontSize.md};
`;

const StyledBungaeInfoContentWrapper = styled.li`
  display: flex;
  gap: 20px;
  font-weight: ${({ theme }) =&gt; theme.fontWeight.semiBold};

  &gt; .bungae-content-title {
    color: ${({ theme }) =&gt; theme.palette.gray5};
  }

  &gt; a {
    color: ${({ theme }) =&gt; theme.palette.mainNavy};
    text-decoration: underline;
  }
`;

const StyledButtonWrapper = styled.div`
  align-self: flex-start;
  margin-top: 20px;
`;

const StyledIntroductionContent = styled.section`
  width: 100%;
  margin-top: 40px;

  &gt; .bungae-introduction-title {
    font-size: ${({ theme }) =&gt; theme.fontSize.lg};
    font-weight: ${({ theme }) =&gt; theme.fontWeight.bold};
    padding-bottom: 24px;
    border-bottom: 2px solid ${({ theme }) =&gt; theme.palette.gray2};
    margin-bottom: 30px;
  }
  &gt; .bungae-introduction-description {
    line-height: 1.3;
  }
`;

function BungaeDetail({ bungaeDetail }) {
  const navigate = useNavigate();

  const {
    id,
    title,
    owner,
    location,
    createdAt,
    meetingAt,
    openChat,
    numberOfParticipants,
    numberOfRecruits,
    description
  } = bungaeDetail;

  const meetingLoacation = bungaeInfoUtil.getMeetingLocation(location);
  const meetingTime = bungaeInfoUtil.getMeetingTime(meetingAt);
  const createdDate = bungaeInfoUtil.getCreatedDate(createdAt);

  const handleClickEdit = () =&gt; {
    navigate(`/bungae/${id}/edit`, { state: { ...bungaeDetail } });
  };

  return (
    &lt;&gt;
      &lt;StyledBungaeHeader&gt;
        &lt;div className=&quot;bungae-title&quot;&gt;{title}&lt;/div&gt;
        &lt;StyledUnderscoreContainer&gt;
          &lt;div className=&quot;bungae-owner-and-ago&quot;&gt;
            &lt;div className=&quot;bungae-owner&quot;&gt;
              {owner.emoji} {owner.nickname}
            &lt;/div&gt;
            &lt;div&gt;{createdDate}&lt;/div&gt;
          &lt;/div&gt;
          &lt;div className=&quot;bungae-edit-and-delete&quot;&gt;
            &lt;Button color=&quot;mainNavy&quot; basic onClick={handleClickEdit}&gt;
              수정
            &lt;/Button&gt;
            &lt;Button color=&quot;mainNavy&quot; basic&gt;
              삭제
            &lt;/Button&gt;
          &lt;/div&gt;
        &lt;/StyledUnderscoreContainer&gt;
      &lt;/StyledBungaeHeader&gt;
      &lt;StyledBungaeInfoContainer&gt;
        &lt;StyledBungaeInfoContentWrapper&gt;
          &lt;div className=&quot;bungae-content-title&quot;&gt;모임 장소&lt;/div&gt;
          &lt;div&gt;{meetingLoacation}&lt;/div&gt;
        &lt;/StyledBungaeInfoContentWrapper&gt;
        &lt;StyledBungaeInfoContentWrapper&gt;
          &lt;div className=&quot;bungae-content-title&quot;&gt;모임 시간&lt;/div&gt;
          &lt;div&gt;{meetingTime}&lt;/div&gt;
        &lt;/StyledBungaeInfoContentWrapper&gt;
        &lt;StyledBungaeInfoContentWrapper&gt;
          &lt;div className=&quot;bungae-content-title&quot;&gt;연락 방법&lt;/div&gt;
          &lt;a href={openChat} target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;
            카카오톡 오픈채팅
          &lt;/a&gt;
        &lt;/StyledBungaeInfoContentWrapper&gt;
        &lt;StyledBungaeInfoContentWrapper&gt;
          &lt;div className=&quot;bungae-content-title&quot;&gt;모집 현황&lt;/div&gt;
          &lt;div&gt;{`${numberOfParticipants} / ${numberOfRecruits}`}&lt;/div&gt;
        &lt;/StyledBungaeInfoContentWrapper&gt;
      &lt;/StyledBungaeInfoContainer&gt;
      &lt;StyledButtonWrapper&gt;
        &lt;Button background=&quot;mainViolet&quot; color=&quot;white&quot; size=&quot;md&quot;&gt;
          번개 참가하기
        &lt;/Button&gt;
      &lt;/StyledButtonWrapper&gt;
      &lt;StyledIntroductionContent&gt;
        &lt;div className=&quot;bungae-introduction-title&quot;&gt;[ 번개 소개 ]&lt;/div&gt;
        &lt;div className=&quot;bungae-introduction-description&quot;&gt;{description}&lt;/div&gt;
      &lt;/StyledIntroductionContent&gt;
    &lt;/&gt;
  );
}

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

<p><code>@utils/bungaeInfo.js</code></p>
<pre><code class="language-js">// ...

const padStartWithZero = (number) =&gt; {
  return String(number).padStart(2, &quot;0&quot;);
};

export const getMeetingLocation = (location) =&gt; {
  return `${location.city} ${location.state} ${location.street} ${location.zipCode} ${location.detail}`;
};

export const getMeetingTime = (meetingAt) =&gt; {
  const meetingDate = new Date(meetingAt);
  const hours = padStartWithZero(meetingDate.getHours());
  const minutes = padStartWithZero(meetingDate.getMinutes());
  return `${hours}:${minutes}`;
};

export const getCreatedDate = (createdAt) =&gt; {
  const createdDate = new Date(createdAt);
  const year = createdDate.getFullYear();
  const month = padStartWithZero(createdDate.getMonth() + 1);
  const date = padStartWithZero(createdDate.getDate());
  return `${year}.${month}.${date}`;
};

// ...</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[번개 모임 웹 어플리케이션 - 번개 작성 및 수정 페이지 마크업 (컴포넌트 및 로직 공유)]]></title>
            <link>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B2%88%EA%B0%9C-%EC%9E%91%EC%84%B1-%EB%B0%8F-%EC%88%98%EC%A0%95-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%88%ED%81%AC%EC%97%85-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%B0%8F-%EB%A1%9C%EC%A7%81-%EA%B3%B5%EC%9C%A0</link>
            <guid>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B2%88%EA%B0%9C-%EC%9E%91%EC%84%B1-%EB%B0%8F-%EC%88%98%EC%A0%95-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%88%ED%81%AC%EC%97%85-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%B0%8F-%EB%A1%9C%EC%A7%81-%EA%B3%B5%EC%9C%A0</guid>
            <pubDate>Thu, 18 May 2023 11:22:44 GMT</pubDate>
            <description><![CDATA[<h2 id="파일구조">파일구조</h2>
<ul>
<li><code>pages/CreateBungae.js</code>
: 번개 작성 페이지</li>
<li><code>pages/EditBungae.js</code>
: 번개 수정 페이지, 상세 페이지의 내용을 초기 값으로 가짐</li>
</ul>
<ul>
<li><code>components/CreateBungae/</code><ul>
<li><code>CreateBungaeForm.js</code> : <code>&lt;CreateBungaePage&gt;</code> 및 <code>&lt;EditBungaePage&gt;</code>가 공통으로 갖는 자식 컴포넌트로 공통된 데이터 및 마크업을 포함</li>
<li><code>MeetingLocation.js</code> : <code>&lt;CreateBungaeForm&gt;</code> 내의 모임 장소 박스, css 코드량이 많아서 따로 분리</li>
</ul>
</li>
</ul>
<br />

<hr>
<h2 id="코드">코드</h2>
<p>폼과 다루는 데이터가 동일한 작성 페이지와 수정 페이지를 하나의 공통 컴포넌트를 사용함으로써 코드가 중복되지 않도록 코드를 만들었다. 또한 분리할 수 있는 상태 및 함수는 분리해서 @hooks 폴더 내에 커스텀 훅 또는 @util 폴더에서 함수로 만든 다음 컴포넌트에서 import 해서 사용함으로서 코드의 가독성 및 재사용성을 높인다.</p>
<p><code>&lt;CreateBungaeForm&gt;</code> 폼 컴포넌트를 자식 컴포넌트로 사용할 때, 번개 상세 페이지의 객체 데이터를 받는 props(bungaeDetail)의 유무에 따라 작성/수정 페이지의 state 초기값이 다르게 들어오도록 구별한다.</p>
<br />

<p><img src="https://velog.velcdn.com/images/line_jeong32/post/5ee1c26b-20d0-4118-8dd8-668cf100c131/image.gif" alt=""></p>
<br />

<p><code>pages/CreateBungae.js</code></p>
<pre><code class="language-js">import CreateBungaeForm from &quot;../components/CreateBungae/CreateBungaeForm&quot;;
import HeadingPageContent from &quot;../components/PageContent/HeadingPageContent&quot;;
import RootPageContent from &quot;../components/PageContent/RootPageContent&quot;;

function CreateBungaePage() {
  return (
    &lt;RootPageContent maxWidth=&quot;md&quot;&gt;
      &lt;HeadingPageContent heading=&quot;번개 모임 만들기&quot;&gt;
        &lt;CreateBungaeForm /&gt;
      &lt;/HeadingPageContent&gt;
    &lt;/RootPageContent&gt;
  );
}

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

<p><code>pages/EditBungae.js</code></p>
<pre><code class="language-js">import { dummyBungaeDetail } from &quot;../@constants/dummy&quot;;
import CreateBungaeForm from &quot;../components/CreateBungae/CreateBungaeForm&quot;;
import HeadingPageContent from &quot;../components/PageContent/HeadingPageContent&quot;;
import RootPageContent from &quot;../components/PageContent/RootPageContent&quot;;

function EditBungaePage() {
  return (
    &lt;RootPageContent maxWidth=&quot;md&quot;&gt;
      &lt;HeadingPageContent heading=&quot;번개 모임 만들기&quot;&gt;
        &lt;CreateBungaeForm bungaeDetail={dummyBungaeDetail} /&gt;
      &lt;/HeadingPageContent&gt;
    &lt;/RootPageContent&gt;
  );
}

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

<p><code>components/CreateBungae/CreateBungaeForm.js</code></p>
<pre><code class="language-js">import { useState } from &quot;react&quot;;

import styled from &quot;styled-components&quot;;

import MeetingLocation from &quot;./MeetingLocation&quot;;
import { numberOptionList, timeOptionList } from &quot;../../@constants/dropdown&quot;;
import useDropdown from &quot;../../@hooks/useDropdown&quot;;
import useInput from &quot;../../@hooks/useInput&quot;;
import { getInitialBungaeState } from &quot;../../@utils/bungaeInfo&quot;;
import Button from &quot;../UI/Button&quot;;
import Dropdown from &quot;../UI/Dropdown&quot;;
import InputWithLabel from &quot;../UI/InputWithLabel&quot;;
import Textarea from &quot;../UI/Textarea&quot;;

const StyledMarginWrapper = styled.div`
  width: 100%;
  margin-bottom: 20px;
`;
const StyledDropdownContainer = styled(StyledMarginWrapper)`
  display: flex;
  gap: 20px;
`;
const StyledNarrowMarginWrapper = styled(StyledMarginWrapper)`
  margin-bottom: 14px;
`;

const StyledButtonContainer = styled.div`
  align-self: flex-end;
  display: flex;
  gap: 10px;
`;

function CreateBungaeForm({ bungaeDetail, onSubmit }) {
  // bungaeDetail에 따라 state 초기값으로 세팅될 값을 반환하는 함수 (getInitialBungaeState)
  const {
    initialNumberOfRecruits,
    initialMeetingTime,
    initialMeetingLocation,
    initialOpenChat,
    initialIntroduction
  } = getInitialBungaeState(bungaeDetail);

  // 드롭다운 상태 및 로직을 관리하는 커스텀 훅 (useDropdown)
  const {
    ref: numberDropdownRef,
    isOpen: numberDropdownIsOpen,
    selected: selectedNumberOption,
    onToggle: onToggleNumberDropdown,
    onSelect: onSelectNumberOption
  } = useDropdown(initialNumberOfRecruits);
  const {
    ref: timeDropdownRef,
    isOpen: timeDropdownIsOpen,
    selected: selectedTimeOption,
    onToggle: onToggleTimeDropdown,
    onSelect: onSelectTimeOption
  } = useDropdown(initialMeetingTime);

  const [meetingLocation] = useState(initialMeetingLocation);

  // input value 및 change 이벤트를 관리하는 커스텀 훅 (useInput)
  const { value: openChat, onChange: onChangeOpenChat } =
    useInput(initialOpenChat);
  const { value: introduction, onChange: onChangeIntroduction } =
    useInput(initialIntroduction);

  const bungaeSubmitHandler = () =&gt; {
    // 서버로 post(작성) 혹은 patch(수정) 요청 
    // onSubmit();
    // 후에 detailPage로 이동
  };

  return (
    &lt;&gt;
      &lt;StyledDropdownContainer&gt;
        &lt;Dropdown
          label=&quot;모집 인원&quot;
          ref={numberDropdownRef}
          isOpen={numberDropdownIsOpen}
          selected={selectedNumberOption}
          onToggle={onToggleNumberDropdown}
          onSelect={onSelectNumberOption}
          options={numberOptionList}
        /&gt;
        &lt;Dropdown
          label=&quot;모임 시간&quot;
          ref={timeDropdownRef}
          isOpen={timeDropdownIsOpen}
          selected={selectedTimeOption}
          onToggle={onToggleTimeDropdown}
          onSelect={onSelectTimeOption}
          options={timeOptionList}
        /&gt;
      &lt;/StyledDropdownContainer&gt;
      &lt;StyledMarginWrapper&gt;
        &lt;MeetingLocation meetingLocation={meetingLocation} /&gt;
      &lt;/StyledMarginWrapper&gt;
      &lt;StyledMarginWrapper&gt;
        &lt;InputWithLabel
          label=&quot;카카오톡 오픈채팅&quot;
          id=&quot;kakao-link&quot;
          placeholder=&quot;오픈 카톡방 링크&quot;
          fontSize=&quot;base&quot;
          height=&quot;46px&quot;
          value={openChat}
          onChange={onChangeOpenChat}
        /&gt;
      &lt;/StyledMarginWrapper&gt;
      &lt;StyledNarrowMarginWrapper&gt;
        &lt;InputWithLabel
          label=&quot;제목&quot;
          id=&quot;create-bungae-title&quot;
          name=&quot;title&quot;
          placeholder=&quot;제목을 입력해주세요&quot;
          fontSize=&quot;base&quot;
          height=&quot;46px&quot;
          value={introduction.title}
          onChange={onChangeIntroduction}
        /&gt;
      &lt;/StyledNarrowMarginWrapper&gt;
      &lt;StyledNarrowMarginWrapper&gt;
        &lt;Textarea
          name=&quot;description&quot;
          placeholder=&quot;번개 모임에 대해 소개해주세요&quot;
          height=&quot;340px&quot;
          value={introduction.description}
          onChange={onChangeIntroduction}
        /&gt;
      &lt;/StyledNarrowMarginWrapper&gt;
      &lt;StyledButtonContainer&gt;
        &lt;Button background=&quot;white&quot; outline&gt;
          취소
        &lt;/Button&gt;
        &lt;Button
          background=&quot;mainViolet&quot;
          color=&quot;white&quot;
          onClick={bungaeSubmitHandler}
        &gt;
          번개 등록
        &lt;/Button&gt;
      &lt;/StyledButtonContainer&gt;
    &lt;/&gt;
  );
}

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

<p><code>bungaeDetail 더미 데이터 구조</code></p>
<pre><code class="language-js">export const dummyBungaeDetail = {
  id: 1,
  owner: {
    id: 1,
    email: &quot;test@test.com&quot;,
    emoji: &quot;😶‍🌫️&quot;, // 필요
    nickname: &quot;닉네임입니다&quot;
  },
  title: &quot;오늘 7시 성수역 클라이밍 하실 분 계신가요!!...&quot;,
  description: &quot;설명&quot;,
  location: {
    country: &quot;한국&quot;,
    city: &quot;서울&quot;,
    state: &quot;성동구&quot;,
    street: &quot;성수일로&quot;,
    zipCode: &quot;1234&quot;,
    detail: &quot;303호&quot;
  },
  openChat: &quot;http://localhost:3000/&quot;, // 필요
  createdAt: &quot;2023-05-17T00:00:00&quot;,
  meetingAt: &quot;2023-05-17T18:00:00&quot;,
  numberOfParticipants: 2,
  numberOfRecruits: 4
};</code></pre>
<br />

<p><code>@utils/bungaeInfo</code>, <code>getInitialBungaeState</code> 함수</p>
<ul>
<li>함수에 전달되는 인수에 따라 다른 초깃값을 리턴한다.</li>
</ul>
<pre><code class="language-js">export const getMeetingTime = (meetingAt) =&gt; {
  const meetingDate = new Date(meetingAt);
  const hours = String(meetingDate.getHours()).padStart(2, &quot;0&quot;);
  const minutes = String(meetingDate.getMinutes()).padStart(2, &quot;0&quot;);
  return `${hours}:${minutes}`;
};

export const getInitialBungaeState = (bungaeDetail) =&gt; {
  let initialNumberOfRecruits = { name: &quot;1명 ~ 10명&quot;, value: null };
  let initialMeetingTime = { name: &quot;00:30 ~ 23:30&quot;, value: null };
  let initialMeetingLocation = null;
  let initialOpenChat = &quot;&quot;;
  let initialIntroduction = { title: &quot;&quot;, description: &quot;&quot; };

  if (bungaeDetail) {
    initialNumberOfRecruits = numberOptionList.find(
      ({ value }) =&gt; value === bungaeDetail.numberOfRecruits
    );
    initialMeetingTime = timeOptionList.find(
      ({ value }) =&gt; value === getMeetingTime(bungaeDetail.meetingAt)
    );
    initialMeetingLocation = `${bungaeDetail.location.city} ${bungaeDetail.location.state} ${bungaeDetail.location.street} ${bungaeDetail.location.zipCode} ${bungaeDetail.location.detail}`;
    initialOpenChat = bungaeDetail.openChat;
    initialIntroduction = {
      title: bungaeDetail.title,
      description: bungaeDetail.description
    };
  }

  return {
    initialNumberOfRecruits,
    initialMeetingTime,
    initialMeetingLocation,
    initialOpenChat,
    initialIntroduction
  };
};</code></pre>
<br />

<p><code>/@constants/dropdown</code>, <code>numberOptionList</code>, <code>timeOptionList</code></p>
<pre><code class="language-js">export const numberOptionList = [
  { name: &quot;1명&quot;, value: 1 },
  { name: &quot;2명&quot;, value: 2 },
  { name: &quot;3명&quot;, value: 3 },
  { name: &quot;4명&quot;, value: 4 },
  { name: &quot;5명&quot;, value: 5 },
  { name: &quot;6명&quot;, value: 6 },
  { name: &quot;7명&quot;, value: 7 },
  { name: &quot;8명&quot;, value: 8 },
  { name: &quot;9명&quot;, value: 9 },
  { name: &quot;10명&quot;, value: 10 }
];

export const timeOptionList = [
  { name: &quot;00:30&quot;, value: &quot;00:30&quot; },
  { name: &quot;01:00&quot;, value: &quot;01:00&quot; },
  { name: &quot;01:30&quot;, value: &quot;01:30&quot; },
  { name: &quot;02:00&quot;, value: &quot;02:00&quot; },
  { name: &quot;02:30&quot;, value: &quot;02:30&quot; },
  { name: &quot;03:00&quot;, value: &quot;03:00&quot; },
  { name: &quot;03:30&quot;, value: &quot;03:30&quot; },
  { name: &quot;04:00&quot;, value: &quot;04:00&quot; },
  { name: &quot;04:30&quot;, value: &quot;04:30&quot; },
  { name: &quot;05:00&quot;, value: &quot;05:00&quot; },
  { name: &quot;05:30&quot;, value: &quot;05:30&quot; },
  { name: &quot;06:00&quot;, value: &quot;06:00&quot; },
  { name: &quot;06:30&quot;, value: &quot;06:30&quot; },
  { name: &quot;07:00&quot;, value: &quot;07:00&quot; },
  { name: &quot;07:30&quot;, value: &quot;07:30&quot; },
  { name: &quot;08:00&quot;, value: &quot;08:00&quot; },
  { name: &quot;08:30&quot;, value: &quot;08:30&quot; },
  { name: &quot;09:00&quot;, value: &quot;09:00&quot; },
  { name: &quot;09:30&quot;, value: &quot;09:30&quot; },
  { name: &quot;10:00&quot;, value: &quot;10:00&quot; },
  { name: &quot;10:30&quot;, value: &quot;10:30&quot; },
  { name: &quot;11:00&quot;, value: &quot;11:00&quot; },
  { name: &quot;11:30&quot;, value: &quot;11:30&quot; },
  { name: &quot;12:00&quot;, value: &quot;12:00&quot; },
  { name: &quot;12:30&quot;, value: &quot;12:30&quot; },
  { name: &quot;13:00&quot;, value: &quot;13:00&quot; },
  { name: &quot;13:30&quot;, value: &quot;13:30&quot; },
  { name: &quot;14:00&quot;, value: &quot;14:00&quot; },
  { name: &quot;14:30&quot;, value: &quot;14:30&quot; },
  { name: &quot;15:00&quot;, value: &quot;15:00&quot; },
  { name: &quot;15:30&quot;, value: &quot;15:30&quot; },
  { name: &quot;16:00&quot;, value: &quot;16:00&quot; },
  { name: &quot;16:30&quot;, value: &quot;16:30&quot; },
  { name: &quot;17:00&quot;, value: &quot;17:00&quot; },
  { name: &quot;17:30&quot;, value: &quot;17:30&quot; },
  { name: &quot;18:00&quot;, value: &quot;18:00&quot; },
  { name: &quot;18:30&quot;, value: &quot;18:30&quot; },
  { name: &quot;19:00&quot;, value: &quot;19:00&quot; },
  { name: &quot;19:30&quot;, value: &quot;19:30&quot; },
  { name: &quot;20:00&quot;, value: &quot;20:00&quot; },
  { name: &quot;20:30&quot;, value: &quot;20:30&quot; },
  { name: &quot;21:00&quot;, value: &quot;21:00&quot; },
  { name: &quot;21:30&quot;, value: &quot;21:30&quot; },
  { name: &quot;22:00&quot;, value: &quot;22:00&quot; },
  { name: &quot;22:30&quot;, value: &quot;22:30&quot; },
  { name: &quot;23:00&quot;, value: &quot;23:00&quot; },
  { name: &quot;23:30&quot;, value: &quot;23:30&quot; }
];
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[번개 모임 웹 어플리케이션 - sortTab 컴포넌트 관련 에러 수정 및 리팩토링]]></title>
            <link>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-sortTab-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EA%B4%80%EB%A0%A8-%EC%97%90%EB%9F%AC-%EC%88%98%EC%A0%95-%EB%B0%8F-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</link>
            <guid>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-sortTab-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EA%B4%80%EB%A0%A8-%EC%97%90%EB%9F%AC-%EC%88%98%EC%A0%95-%EB%B0%8F-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</guid>
            <pubDate>Tue, 16 May 2023 13:43:35 GMT</pubDate>
            <description><![CDATA[<h2 id="sorttab-컴포넌트-관련-에러">sortTab 컴포넌트 관련 에러</h2>
<p><img src="https://velog.velcdn.com/images/line_jeong32/post/c59a0a5b-ea54-4257-b873-bbc7534e1c8a/image.png" alt=""> <img src="https://velog.velcdn.com/images/line_jeong32/post/f4190f7c-76a3-48bd-bc13-2c1fc3f5aaa0/image.png" alt=""> <img src="https://velog.velcdn.com/images/line_jeong32/post/b18db4c6-165f-4a56-922b-b3a3cc4ca4ad/image.png" alt=""></p>
<p><code>&lt;sortTab&gt;</code> 컴포넌트는 위 이미지와 같이 정렬의 기준을 설정하는 탭 메뉴 성격의 컴포넌트이다. 원하는 정렬 기준을 클릭하면 path (혹은 query string)이 변경된다. 그리고 해당 정렬 기준에 맞는 정보를 보여주고 활성화된 탭에는 css 강조 효과를 보여 주는 역할을 한다.</p>
<p>클릭으로 path를 전환하고 그에 맞는 css 효과를 적용하는 것은 어렵지 않았지만, 맨 처음 해당 페이지로 이동하고 정렬 기준을 설정하지 않았을 때에도 둘 중 하나의 기준을 디폴트로 정해 정렬하고, 해당 기준의 탭에도 css 강조 효과를 주는 부분에서 좀 헤맸었다.</p>
<p>먼저는 <code>useState</code>를 사용해서 초기값을 디폴트 정렬로 지정하고 싶은 path로 지정하고 정렬 기준을 클릭할 때마다 state를 업데이트하여 전환하고 Link로 해당 경로로 이동하는 방법을 사용했었다. 
이 방법은 얼핏 잘 작동하는 것처럼 보였으나 문제가 있었다. 뒤로 가기를 할 때는 path를 추적을 하지 못하는 것이었다.. 실제 url 경로를 따르지 않고 클릭할 때만 변환되는 state를 추적하니 생기는 문제였다.</p>
<p>그래서 state 대신 실제 url 경로를 따르는 방법으로 변경해야 했다. 또한 하나의 <code>&lt;SortTab&gt;</code>을 재활용 해서 path parameter 뿐만 아니라 query string을 기준으로도 동일한 기능이 작동하게 만들고 싶었다.</p>
<p>결론적으로, 아래와 같이 작동하도록 코드를 변경했다.</p>
<ol>
<li>뒤로 가기를 할 때도 url이 정상적으로 변경되고 그에 따라 css 효과가 동적으로 변경될 것</li>
<li>탭 메뉴를 선택하기 전, 기준이 되는 path가 없을 때는 디폴트가 되는 정렬 기준을 정해 활성화 할 것</li>
<li><code>&lt;SortTab&gt;</code>를 동일하게 사용해 path parameter 뿐만 아니라 query string을 기준으로도 같은 기능을 할 수 있도록 할 것</li>
</ol>
<br />

<hr>
<h2 id="profilepage-컴포넌트">ProfilePage 컴포넌트</h2>
<ul>
<li>tabMenu 객체 수정<ul>
<li>객체의 key 변경 (SortTab에서 path parameter 뿐만 아니라 query string도 props로 받을 것이기 때문에 보다 일반적인 명칭으로 변경)</li>
<li>경로 추적을 위한 sortBy 속성과 경로 이동을 위한 linkTo 분리</li>
</ul>
</li>
<li>useState 대신 url useLocation를 사용해 경로 추적<ul>
<li>useState 대신  pathname 추적</li>
<li>페이지 전환을 위해 useNavigate 사용 (SortTab에서 Link를 사용하지 않을 것이기 때문에)</li>
</ul>
</li>
</ul>
<br />

<h3 id="--변경-전">- 변경 전</h3>
<pre><code class="language-js">// ...

function ProfilePage() {
  // ...

  const [sortPathname, setSortPathname] = useState(&quot;/profile/created&quot;);

    const tabMenu = [
    {
      name: &quot;내가 만든 번개&quot;,
      pathname: &quot;/profile/created&quot;
    },
    {
      name: &quot;내가 참여한 번개&quot;,
      pathname: &quot;/profile/participated&quot;
    }
  ];

  // tabMenu의 name을 클릭할 때마다 state 변경
   const switchTab = (selected) =&gt; {
    setSortPathname(selected);
  };

  // ...
  return (
    &lt;RootPageContent&gt;
      &lt;UserInfo emoji=&quot;😶‍🌫️&quot; nickname={nickname} email=&quot;test@test.com&quot; /&gt;
      &lt;UserBungaeList
        sortPathname={sortPathname}
        switchTab={switchTab}
        tabMenu={tabMenu}
        bungaeList={bungaeList}
      /&gt;
    &lt;/RootPageContent&gt;
  );
}

// ...</code></pre>
<br />

<h3 id="--변경-후">- 변경 후</h3>
<pre><code class="language-js">// ...

function ProfilePage() {
  // ...

  const navigate = useNavigate();
  const { pathname } = useLocation();

  const tabMenu = [
    {
      name: &quot;내가 만든 번개&quot;,
      sortBy: [&quot;/profile&quot;, &quot;/profile/created&quot;], // css 활성화 경로
      linkTo: &quot;/profile/created&quot; // 클릭 시, 이동 경로
    },
    {
      name: &quot;내가 참여한 번개&quot;,
      sortBy: [&quot;/profile/participated&quot;], // css 활성화 경로
      linkTo: &quot;/profile/participated&quot; // 클릭 시, 이동 경로로
    }
  ];

  // tabMenu의 name을 클릭할 때마다 linkTo 경로로 이동
  const switchTabHandler = (selected) =&gt; {
    navigate(selected);
  };

  // ...
  return (
    &lt;RootPageContent&gt;
      &lt;UserInfo emoji=&quot;😶‍🌫️&quot; nickname={nickname} email=&quot;test@test.com&quot; /&gt;
      &lt;UserBungaeList
        sortBy={pathname}
        onSwitchTab={switchTabHandler}
        tabMenu={tabMenu}
        bungaeList={bungaeList}
      /&gt;
    &lt;/RootPageContent&gt;
}

// ...</code></pre>
<br />

<hr>
<h2 id="sorttab-컴포넌트">SortTab 컴포넌트</h2>
<ul>
<li>props명 변경 (path parameter 뿐만 아니라 query string도 props로 받을 것이기 때문에 보다 일반적인 명칭으로 변경)</li>
<li>경로 추적으로 css 효과를 주기 위해 tabMenu의 sortBy 배열을 확인(includes 메서드 사용)</li>
<li>Link 컴포넌트(a 태그) 대신 li 태그 사용<ul>
<li>path parameter 뿐만 아니라 query string을 기준으로 정렬할 때를 고려</li>
</ul>
</li>
</ul>
<br />

<h3 id="--변경-전-1">- 변경 전</h3>
<pre><code class="language-js">// ...

function SortTab({ sortPathname, switchTab, tabMenu }) {
  return (
    &lt;StyledSortTab&gt;
      {tabMenu.map((menu) =&gt; (
        &lt;Link
          to={menu.pathname}
          key={menu.name}
          onClick={() =&gt; switchTab(menu.pathname)}
        &gt;
          &lt;div className=&quot;user-tab&quot;&gt;
            &lt;div&gt;{menu.name}&lt;/div&gt;
          &lt;/div&gt;
          {menu.pathname === sortPathname &amp;&amp; &lt;div className=&quot;underscore&quot;&gt;&lt;/div&gt;}
        &lt;/Link&gt;
      ))}
    &lt;/StyledSortTab&gt;
  );
}

// ...</code></pre>
<br />

<h3 id="--변경-후-1">- 변경 후</h3>
<pre><code class="language-js">function SortTab({ sortBy, onSwitch, tabMenu }) {
  return (
    &lt;StyledSortTab&gt;
      {tabMenu.map((menu) =&gt; (
        &lt;li
          role=&quot;menuitem&quot;
          key={menu.name}
          onClick={() =&gt; onSwitch(menu.linkTo)}
        &gt;
          &lt;div className=&quot;tab-menu&quot;&gt;
            &lt;div&gt;{menu.name}&lt;/div&gt;
          &lt;/div&gt;
          {menu.sortBy.includes(sortBy) &amp;&amp; &lt;div className=&quot;underscore&quot;&gt;&lt;/div&gt;}
        &lt;/li&gt;
      ))}
    &lt;/StyledSortTab&gt;
  );
}</code></pre>
<br />

<h2 id="bungaesearchpage">BungaeSearchPage</h2>
<ul>
<li>query string를 기준으로 탭 전환</li>
<li>react-router의 <code>useSearchParams</code> 이용</li>
</ul>
<br />

<p><code>pages/BungaeSearch.js</code></p>
<pre><code class="language-js">// ...

function BungaeSearchPage() {
  // ...

  const [searchParams, setSearchParams] = useSearchParams();
  const sort = searchParams.get(&quot;sort&quot;);

  export const tabMenu = [
  {
    name: &quot;최신순&quot;,
    sortBy: [null, &quot;newest&quot;],
    linkTo: &quot;newest&quot;
  },
  {
    name: &quot;마감임박순&quot;,
    sortBy: [&quot;last-minute&quot;],
    linkTo: &quot;last-minute&quot;
  }
];

  // ...

  const switchTabHandler = (selected) =&gt; {
    setSearchParams({ sort: selected });
  };

  // ...

  return (
    &lt;RootPageContent&gt;
    // ...

      &lt;StyledSection&gt;
        &lt;SearchedBungaeList
          count={bungaeList.length}
          sortBy={sort}
          onSwitchTab={switchTabHandler}
          tabMenu={tabMenu}
          bungaeList={bungaeList}
        /&gt;
      &lt;/StyledSection&gt;
    &lt;/RootPageContent&gt;
  );
}

// ...</code></pre>
<br />

<p><code>components/BungaeSearch/SearchedBungaeList.js</code></p>
<pre><code class="language-js">function SearchedBungaeList({
  count,
  sortBy,
  onSwitchTab,
  tabMenu,
  bungaeList
}) {
  return (
    &lt;section&gt;
      &lt;StyledHeadingWrapper&gt;
        &lt;h1&gt;{`번개 검색 결과 (${count})`}&lt;/h1&gt;
        &lt;SortTab sortBy={sortBy} onSwitch={onSwitchTab} tabMenu={tabMenu} /&gt;
      &lt;/StyledHeadingWrapper&gt;
      &lt;BungaeListContent bungaeList={bungaeList} /&gt;
    &lt;/section&gt;
  );
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[번개 모임 웹 어플리케이션 - 검색 페이지 마크업 (+ jsx-a11y 에러 해결)]]></title>
            <link>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B2%80%EC%83%89-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%88%ED%81%AC%EC%97%85</link>
            <guid>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B2%80%EC%83%89-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%88%ED%81%AC%EC%97%85</guid>
            <pubDate>Mon, 15 May 2023 13:07:13 GMT</pubDate>
            <description><![CDATA[<h2 id="파일구조">파일구조</h2>
<ul>
<li><p><code>pages/BungaeSearch.js</code>
: 아래의 컴포넌트로 각 섹션 별로 분리하여 마크업하고 import 한다.</p>
</li>
<li><p><code>components/BungaeSearch/</code></p>
<ul>
<li><code>LocalOptions.js</code> : 시/도, 시/구/군 지역을 선택할 수 있는 옵션을 가진 컴포넌트 (모달)</li>
<li><code>SearchForm.js</code> : 키워드 및 지역을 검색할 수 있는 검색창 컴포넌트</li>
<li><code>SearchedBungaeList.js</code> : 검색 결과를 보여주는 컴포넌트</li>
</ul>
</li>
</ul>
<br />

<hr>
<h2 id="코드">코드</h2>
<ul>
<li>지역 선택은 한번에 하나의 시/도, 시/구/군까지만 선택할 수 있도록 할 것 (다중 선택 불가)</li>
<li>모달창에서 시/도 및 시/군/구를 선택했을 때마다 페이지 내 검색창에 선택된 항목이 동적으로 보이도록 state로 관리할 것</li>
<li>최신순, 마감임박순 정렬 전환은 url의 query string으로 구별해줄 것</li>
<li>정렬에 해당하는 query string이 없으면 최신순으로 정렬할 것</li>
<li>정렬 전환은 <code>&lt;ProfilePage&gt;</code>를 만들 때 사용했던 <code>&lt;SortTab&gt;</code> 컴포넌트를 재사용할 것</li>
<li>검색 결과로 보여지는 번개 카드들은 <code>&lt;BungaeMainPage&gt;</code> 및 <code>&lt;ProfilePage&gt;</code>를 만들 때 사용했던 <code>&lt;BungaeListContent&gt;</code> 컴포넌트를 재사용 할 것</li>
</ul>
<br />

<p><img src="https://velog.velcdn.com/images/line_jeong32/post/dd327092-d00c-4394-96a7-d70c30e6e983/image.gif" alt=""></p>
<p><code>pages/BungaeSearch.js</code></p>
<pre><code class="language-js">import { useEffect, useState } from &quot;react&quot;;
import { useSearchParams } from &quot;react-router-dom&quot;;

import styled from &quot;styled-components&quot;;

import { searchPageTabMenu as tabMenu } from &quot;../@constants/constants&quot;;
import { dummyBungaeList } from &quot;../@constants/dummy&quot;;
import LocalOptions from &quot;../components/BungaeSearch/LocalOptions&quot;;
import SearchedBungaeList from &quot;../components/BungaeSearch/SearchedBungaeList&quot;;
import SearchForm from &quot;../components/BungaeSearch/SearchForm&quot;;
import RootPageContent from &quot;../components/PageContent/RootPageContent&quot;;

const StyledSection = styled.section`
  width: 100%;
  &amp; + &amp; {
    margin-top: 40px;
  }
`;

function BungaeSearchPage() {
  const [selectedLocal, setSelectedLocal] = useState({
    sido: &quot;&quot;,
    sigugun: &quot;&quot;
  });
  const [localOptionsIsOpen, setLocalOptionsIsOpen] = useState(false);
  const [currentSido, setCurrentSido] = useState(0);
  const [currentSigugun, setCurrentSigugun] = useState(null);

  const [bungaeList, setBungaeList] = useState([]);
  const [searchParams, setSearchParams] = useSearchParams();
  const sort = searchParams.get(&quot;sort&quot;);

  useEffect(() =&gt; {
    setBungaeList(dummyBungaeList);
  }, []);

  // 최신순, 마감임박순 클릭 시, 그에 맞는 query string으로 변경
  const switchTabHandler = (selected) =&gt; {
    setSearchParams({ sort: selected });
  };

  // 지역 선택 모달창 open &lt;-&gt; close 전환
  const toggleLocalOptionsHandler = () =&gt; {
    setLocalOptionsIsOpen((prev) =&gt; !prev);
  };

  // 시/도 클릭 이벤트 핸들러
  const selectSidoHandler = (idx, text) =&gt; {
    setCurrentSido(idx);
    setSelectedLocal({ sido: text, sigugun: &quot;&quot; });
    setCurrentSigugun((prev) =&gt; ({ ...prev, sigugun: null }));
  };
  // 시/구/군 클릭 이벤트 핸들러
  const selectSigugunHandler = (idx, text) =&gt; {
    setCurrentSigugun(idx);
    setSelectedLocal((prev) =&gt; ({ ...prev, sigugun: text }));
  };

  // 지역 선택 초기화 클릭 이벤트 핸들러
  const resetSelectionHandler = () =&gt; {
    setCurrentSido(0);
    setCurrentSigugun(null);
    setSelectedLocal({
      sido: &quot;&quot;,
      sigugun: &quot;&quot;
    });
  };

  return (
    &lt;RootPageContent&gt;
      &lt;StyledSection&gt;
        &lt;SearchForm
          onOpen={toggleLocalOptionsHandler}
          selectedLocal={selectedLocal}
        /&gt;
        &lt;LocalOptions
          isOpen={localOptionsIsOpen}
          onClose={toggleLocalOptionsHandler}
          currentSido={currentSido}
          onSelectSido={selectSidoHandler}
          currentSigugun={currentSigugun}
          onSelectSigugun={selectSigugunHandler}
          onReset={resetSelectionHandler}
        /&gt;
      &lt;/StyledSection&gt;
      &lt;StyledSection&gt;
        &lt;SearchedBungaeList
          count={bungaeList.length}
          sortBy={sort}
          onSwitchTab={switchTabHandler}
          tabMenu={tabMenu}
          bungaeList={bungaeList}
        /&gt;
      &lt;/StyledSection&gt;
    &lt;/RootPageContent&gt;
  );
}

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

<p><code>componenets/BungaeSearch/SearchForm.js</code></p>
<pre><code class="language-js">import styled from &quot;styled-components&quot;;

const StyledSearchForm = styled.form`
  width: 100%;
  display: flex;
  border-radius: 5px;
  height: 62px;
`;

const KeywordSearchWrapper = styled.div`
  flex-grow: 1;
  display: flex;
  align-items: center;
  padding: 0 14px;
  border: 1px solid black;
  border-radius: 5px 0px 0px 5px;

  .image-wrapper {
    width: 14px;
    height: 14px;
    margin-right: 8px;
  }
`;

const LocalSearchWrapper = styled(KeywordSearchWrapper).attrs(
  ({ onClick }) =&gt; ({ onClick })
)`
  border-radius: 0px;
  border-left: 0px;

  &gt; p {
    font-size: ${({ theme }) =&gt; theme.fontSize.sm};
    color: ${({ theme }) =&gt; theme.palette.gray5};
    min-width: 120px;
  }
  &gt; p.selected {
    color: ${({ theme }) =&gt; theme.palette.black};
  }
`;

const StyledKeywordInput = styled.input.attrs(() =&gt; ({
  placeholder: &quot;키워드를 입력해주세요&quot;
}))`
  width: 100%;
  outline: none;
  border: none;
  font-size: 14px;
  padding: 0px;
`;

const StyledSearchButton = styled.button`
  outline: none;
  border: 1px solid black;
  border-left: 0px;
  border-top-right-radius: 5px;
  border-bottom-right-radius: 5px;
  width: 62px;
  background: ${({ theme }) =&gt; theme.palette.mainMauve};
  font-weight: ${({ theme }) =&gt; theme.fontWeight.semiBold};
`;

function SearchForm({ onOpen, selectedLocal }) {
  const { sido, sigugun } = selectedLocal;
  const localIsSelected = sido !== &quot;&quot; &amp;&amp; sigugun !== &quot;&quot;;

  return (
    &lt;StyledSearchForm&gt;
      &lt;KeywordSearchWrapper&gt;
        &lt;div className=&quot;image-wrapper&quot;&gt;
          &lt;img src=&quot;/images/search.svg&quot; alt=&quot;keyword search&quot; /&gt;
        &lt;/div&gt;
        &lt;StyledKeywordInput /&gt;
      &lt;/KeywordSearchWrapper&gt;
      &lt;LocalSearchWrapper onClick={onOpen}&gt;
        &lt;div className=&quot;image-wrapper&quot;&gt;
          &lt;img src=&quot;/images/map.svg&quot; alt=&quot;map marker&quot; /&gt;
        &lt;/div&gt;
        {localIsSelected ? (
          &lt;p className=&quot;selected&quot;&gt;{`${sido} ${sigugun}`}&lt;/p&gt;
        ) : (
          &lt;p&gt;지역을 선택해주세요&lt;/p&gt;
        )}
      &lt;/LocalSearchWrapper&gt;
      &lt;StyledSearchButton&gt;검색&lt;/StyledSearchButton&gt;
    &lt;/StyledSearchForm&gt;
  );
}

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

<p><code>componenets/BungaeSearch/LocalOptions.js</code></p>
<pre><code class="language-js">import styled from &quot;styled-components&quot;;

import { localList } from &quot;../../@constants/constants&quot;;
import Button from &quot;../UI/Button&quot;;
import Modal from &quot;../UI/Modal&quot;;

const StyledHeader = styled.h1`
  font-size: ${({ theme }) =&gt; theme.fontSize.lg};
  font-weight: ${({ theme }) =&gt; theme.fontWeight.bold};
  margin-bottom: 24px;
`;

const SyledLocalListWrapper = styled.div`
  display: flex;

  .list-title {
    text-align: left;
    margin-bottom: 6px;
    font-size: ${({ theme }) =&gt; theme.fontSize.xs};
  }
`;

const StyledSidoList = styled.ul`
  width: 160px;
  height: 140px;
  overflow-y: scroll;
  border: 1px solid black;
  font-size: ${({ theme }) =&gt; theme.fontSize.sm};
  cursor: pointer;

  &gt; li {
    padding: 8px 20px;
  }
  &gt; li.active {
    background: ${({ theme }) =&gt; theme.palette.mainMauve};
    font-weight: ${({ theme }) =&gt; theme.fontWeight.semiBold};
  }
`;

const SyledSigugunList = styled(StyledSidoList)`
  border-left: 0px;
`;

const StyledButtonContainer = styled.div`
  display: flex;
  gap: 10px;
  margin-top: 24px;
`;

function LocalOptions({
  isOpen,
  onClose,
  currentSido,
  onSelectSido,
  currentSigugun,
  onSelectSigugun,
  onReset
}) {
  if (!isOpen) return null;

  return (
    &lt;Modal isOpen={isOpen} onClose={onClose}&gt;
      &lt;&gt;
        &lt;StyledHeader&gt;지역 선택&lt;/StyledHeader&gt;
        &lt;SyledLocalListWrapper&gt;
          &lt;div&gt;
            &lt;div className=&quot;list-title&quot;&gt;시·도&lt;/div&gt;
            &lt;StyledSidoList&gt;
              {localList.map(({ sido }, idx) =&gt; (
                &lt;li
                  key={idx}
                  role=&quot;menuitem&quot;
                  className={idx === currentSido ? &quot;active&quot; : &quot;&quot;}
                  onClick={() =&gt; onSelectSido(idx, sido)}
                &gt;
                  {sido}
                &lt;/li&gt;
              ))}
            &lt;/StyledSidoList&gt;
          &lt;/div&gt;
          &lt;div&gt;
            &lt;div className=&quot;list-title&quot;&gt;시·구·군&lt;/div&gt;
            &lt;SyledSigugunList&gt;
              {localList[currentSido].sigugun.map((el, idx) =&gt; (
                &lt;li
                  key={idx}
                  role=&quot;menuitem&quot;
                  className={idx === currentSigugun ? &quot;active&quot; : &quot;&quot;}
                  onClick={() =&gt; onSelectSigugun(idx, el)}
                &gt;
                  {el}
                &lt;/li&gt;
              ))}
            &lt;/SyledSigugunList&gt;
          &lt;/div&gt;
        &lt;/SyledLocalListWrapper&gt;
        &lt;StyledButtonContainer&gt;
          &lt;Button background=&quot;gray3&quot; color=&quot;white&quot; fullWidth onClick={onReset}&gt;
            초기화
          &lt;/Button&gt;
          &lt;Button
            background=&quot;mainViolet&quot;
            color=&quot;white&quot;
            fullWidth
            onClick={onClose}
          &gt;
            확인
          &lt;/Button&gt;
        &lt;/StyledButtonContainer&gt;
      &lt;/&gt;
    &lt;/Modal&gt;
  );
}

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

<p><code>components/BungaeSearch/SearchedBungaeList.js</code></p>
<pre><code class="language-js">import styled from &quot;styled-components&quot;;

import BungaeListContent from &quot;../PageContent/BungaeListContent&quot;;
import SortTab from &quot;../UI/SortTab&quot;;

const StyledHeadingWrapper = styled.div`
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 24px;

  &gt; h1 {
    font-size: ${({ theme }) =&gt; theme.fontSize[&quot;2xl&quot;]};
    font-weight: ${({ theme }) =&gt; theme.fontWeight.bold};
  }
`;

function SearchedBungaeList({
  count,
  sortBy,
  onSwitchTab,
  tabMenu,
  bungaeList
}) {
  return (
    &lt;section&gt;
      &lt;StyledHeadingWrapper&gt;
        &lt;h1&gt;{`번개 검색 결과 (${count})`}&lt;/h1&gt;
        &lt;SortTab sortBy={sortBy} onSwitch={onSwitchTab} tabMenu={tabMenu} /&gt;
      &lt;/StyledHeadingWrapper&gt;
      &lt;BungaeListContent bungaeList={bungaeList} /&gt;
    &lt;/section&gt;
  );
}

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

<hr>
<h2 id="li-태그-onclick-이벤트-eslint-에러-발생-해결-jsx-a11y">li 태그 onClick 이벤트 eslint 에러 발생 해결 (jsx-a11y)</h2>
<p><code>&lt;LocalOptions&gt;</code> 컴포넌트에서 li 태그에 onClick 이벤트 핸들러를 할당했을 때, eslint의 리액트 접근성과 관련된 검사를 하는 jsx-a11y에서 다음과 같은 에러가 발생했다.
<code>Non-interactive elements should not be assigned mouse or keyboard event listeners  jsx-a11y/no-noninteractive-element-interactions</code>
이는 li 태그가 button이나 a 태그와 같이 상호작용하는 요소가 아님에도 onClick 이벤트를 할당했기 때문에 발생한 에러였다. </p>
<p>찾아본 해결 방법은 여러가지가 있었다.</p>
<br />

<ol>
<li><code>eslint-disable-line</code> 추가 - eslint 무시하기<pre><code class="language-js">&lt;li // eslint-disable-line jsx-a11y/no-noninteractive-element-interactions
key={idx}
role=&quot;menuitem&quot;
className={idx === currentSido ? &quot;active&quot; : &quot;&quot;}
onClick={() =&gt; onSelectSido(idx, sido)}
&gt;
{sido}
&lt;/li&gt;</code></pre>
</li>
</ol>
<br />

<ol start="2">
<li>li 태그 대신 li 태그 내부에 button과 같은 interactive elements를 추가해 해당 요소에 onClick 이벤트 등록하기<pre><code class="language-js">&lt;li
key={idx}
role=&quot;menuitem&quot;
className={idx === currentSido ? &quot;active&quot; : &quot;&quot;}
&gt;
&lt;button onClick={() =&gt; onSelectSido(idx, sido)}&gt;
{sido}
&lt;/button&gt;
&lt;/li&gt;</code></pre>
</li>
</ol>
<br />

<ol start="3">
<li><code>role</code> 속성 값 추가하기<pre><code class="language-js">&lt;li
key={idx}
role=&quot;menuitem&quot;
className={idx === currentSido ? &quot;active&quot; : &quot;&quot;}
onClick={() =&gt; onSelectSido(idx, sido)}
&gt;
{sido}
&lt;/li&gt;</code></pre>
</li>
</ol>
<p>이외에도 방법이 더 있었지만, 그 중에서도 role을 추가하는 방식이 가장 마음에 들었다. 그 중에서도 <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/menuitem_role" target="_blank">role=&quot;menuitem&quot;</a>을 속성값으로 사용했다. 여러 지역 리스트 중 하나를 선택해야 하는 일종의 메뉴의 역할을 하고 있기 때문에 해당 해결 방식이 적절하다고 생각해 차용했다.</p>
<blockquote>
<p>참고</p>
</blockquote>
<ul>
<li><a href="https://stackoverflow.com/questions/43337275/how-to-work-around-jsx-a11y-no-static-element-interactions-restriction">https://stackoverflow.com/questions/43337275/how-to-work-around-jsx-a11y-no-static-element-interactions-restriction</a></li>
<li><a href="https://nuli.navercorp.com/community/article/1132961">https://nuli.navercorp.com/community/article/1132961</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[번개 모임 웹 어플리케이션 - 메인 페이지 마크업]]></title>
            <link>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%A9%94%EC%9D%B8-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%88%ED%81%AC%EC%97%85</link>
            <guid>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%A9%94%EC%9D%B8-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%88%ED%81%AC%EC%97%85</guid>
            <pubDate>Sun, 14 May 2023 13:23:32 GMT</pubDate>
            <description><![CDATA[<p><code>피그마로 만든 메인 페이지</code> (1920 * 1080)
<img src="https://velog.velcdn.com/images/line_jeong32/post/5d19e267-5b6d-4cd4-bbc6-30141dd7e957/image.png" alt=""></p>
<hr>
<p><code>src/components/BunageMain/MainBungaeList.js</code></p>
<pre><code class="language-js">import styled from &quot;styled-components&quot;;

import * as bungaeCardUtil from &quot;../../@utils/bungaeCard&quot;;
import BungaeCard from &quot;../BungaeCard&quot;;
import BungaeListContent from &quot;../PageContent/BungaeListContent&quot;;
import SimplePagination from &quot;../UI/SimplePagination&quot;;

const StyledUpperContent = styled.div`
  display: flex;
  justify-content: space-between;
  margin-bottom: 30px;

  .en-heading {
    font-size: ${({ theme }) =&gt; theme.fontSize.md};
    font-weight: ${({ theme }) =&gt; theme.fontWeight.bold};
    color: ${({ theme }) =&gt; theme.palette.mainNavy};
    margin-bottom: 12px;
  }
  .ko-heading {
    font-size: ${({ theme }) =&gt; theme.fontSize[&quot;2xl&quot;]};
    font-weight: ${({ theme }) =&gt; theme.fontWeight.bold};
  }
`;

function NewBungaeList({ enHeading, koHeading, bungaeList }) {
  return (
    &lt;&gt;
      &lt;StyledUpperContent&gt;
        &lt;div&gt;
          &lt;div className=&quot;en-heading&quot;&gt;{enHeading}&lt;/div&gt;
          &lt;div className=&quot;ko-heading&quot;&gt;{koHeading}&lt;/div&gt;
        &lt;/div&gt;
        &lt;SimplePagination /&gt;
      &lt;/StyledUpperContent&gt;
      &lt;BungaeListContent&gt;
        {bungaeList.map(
          ({
            id,
            owner,
            title,
            location,
            createdAt,
            meetingAt,
            numberOfParticipants,
            numberOfRecruits
          }) =&gt; {
            const status = bungaeCardUtil.getBungaeStatus(createdAt, meetingAt);
            const place = location.city + &quot; &quot; + location.state;
            const time = bungaeCardUtil.getMeetingTime(meetingAt);
            const duration = bungaeCardUtil.getBungaeDuration(meetingAt);
            return (
              &lt;BungaeCard
                key={id}
                id={id}
                status={status}
                place={place}
                time={time}
                title={title}
                emoji={owner.emoji}
                nickname={owner.nickname}
                numberOfParticipants={numberOfParticipants}
                numberOfRecruits={numberOfRecruits}
                duration={duration}
              /&gt;
            );
          }
        )}
      &lt;/BungaeListContent&gt;
    &lt;/&gt;
  );
}

export default NewBungaeList;</code></pre>
<p><img src="https://velog.velcdn.com/images/line_jeong32/post/3d93b65c-6332-4b6f-85f0-caf2107b28e8/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[번개 모임 웹 어플리케이션 - 회원정보 수정 페이지 마크업 작업]]></title>
            <link>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%ED%9A%8C%EC%9B%90%EC%A0%95%EB%B3%B4-%EC%88%98%EC%A0%95-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%88%ED%81%AC%EC%97%85-%EC%9E%91%EC%97%85</link>
            <guid>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%ED%9A%8C%EC%9B%90%EC%A0%95%EB%B3%B4-%EC%88%98%EC%A0%95-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%88%ED%81%AC%EC%97%85-%EC%9E%91%EC%97%85</guid>
            <pubDate>Sat, 13 May 2023 13:48:10 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[번개 모임 웹 어플리케이션 - Profile 페이지 마크업]]></title>
            <link>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-Profile-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%88%ED%81%AC%EC%97%85</link>
            <guid>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-Profile-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A7%88%ED%81%AC%EC%97%85</guid>
            <pubDate>Fri, 12 May 2023 14:47:49 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[번개 모임 웹 어플리케이션 - pages 마크업, Input 컴포넌트 리팩토링]]></title>
            <link>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-fnt8l1mf</link>
            <guid>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-fnt8l1mf</guid>
            <pubDate>Thu, 11 May 2023 13:51:45 GMT</pubDate>
            <description><![CDATA[<p>&quot;jsx-a11y/click-events-have-key-events&quot;: 0,</p>
<p><code>src/pages/profile.js</code></p>
<pre><code class="language-js">import styled from &quot;styled-components&quot;;

import { bungaeStatus } from &quot;../@constants/constants&quot;;
import BungaeCard from &quot;../components/BungaeCard&quot;;
import Button from &quot;../components/UI/Button&quot;;

const UserInfoSection = styled.section`
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: 40px;

  &gt; .imoji {
    height: 120px;
    width: 120px;
    border: 1px solid black;
    border-radius: 50%;
    font-size: 40px;
    margin-bottom: 12px;
    display: flex;
    justify-content: center;
    align-items: center;
  }
  &gt; .nickname {
    font-size: ${({ theme }) =&gt; theme.fontSize[&quot;3xl&quot;]};
    font-weight: ${({ theme }) =&gt; theme.fontWeight.semiBold};
    margin-bottom: 16px;
  }
  &gt; .email {
    font-size: ${({ theme }) =&gt; theme.fontSize.base};
    color: ${({ theme }) =&gt; theme.palette.gray4};
    margin-bottom: 24px;
  }
`;

const TabSection = styled.section`
  display: flex;
  margin-bottom: 24px;

  .profile-tab {
    padding: 0.5rem;
    font-weight: ${({ theme }) =&gt; theme.fontWeight.semiBold};
  }
  .active {
    height: 3px;
    border: 1.5px solid black;
    margin: 0rem 0.5rem;
  }
`;

const UserBungaeSection = styled.section`
  width: 100%;
  &gt; ul {
    margin: 0 auto;
    display: flex;
    flex-wrap: wrap;
    gap: 20px;

    @media only screen and (max-width: 1254px) {
      max-width: 895px;
    }
    @media only screen and (max-width: 949px) {
      max-width: 590px;
    }
    @media only screen and (max-width: 644px) {
      max-width: 285px;
    }
  }
`;

function ProfilePage() {
  return (
    &lt;&gt;
      &lt;UserInfoSection&gt;
        &lt;div className=&quot;imoji&quot;&gt;😶‍🌫️&lt;/div&gt;
        &lt;p className=&quot;nickname&quot;&gt;닉네임입니다&lt;/p&gt;
        &lt;p className=&quot;email&quot;&gt;test@test.com&lt;/p&gt;
        &lt;Button background=&quot;mainMauve&quot;&gt;프로필 수정&lt;/Button&gt;
      &lt;/UserInfoSection&gt;
      &lt;TabSection&gt;
        &lt;div&gt;
          &lt;div className=&quot;profile-tab&quot;&gt;
            &lt;div&gt;내가 만든 번개&lt;/div&gt;
          &lt;/div&gt;
          &lt;div className=&quot;active&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div className=&quot;profile-tab&quot;&gt;
            &lt;div&gt;내가 참여한 번개&lt;/div&gt;
          &lt;/div&gt;
          &lt;div&gt;&lt;/div&gt;
        &lt;/div&gt;
      &lt;/TabSection&gt;
      &lt;UserBungaeSection&gt;
        &lt;ul&gt;
          &lt;BungaeCard
            status={bungaeStatus.recruiting}
            place=&quot;성수동 OO 클라이밍 센터&quot;
            time=&quot;19:00&quot;
            title=&quot;오늘 7시 성수역 클라이밍 하실 분 계신가요!!...&quot;
            imoji=&quot;😶‍🌫️&quot;
            nickname=&quot;닉네임입니다&quot;
            numberOfParticipants={2}
            numberOfRecruits={4}
            duration=&quot;00:30:23&quot;
          &gt;&lt;/BungaeCard&gt;
          &lt;BungaeCard status={bungaeStatus.recruiting}&gt;&lt;/BungaeCard&gt;
          &lt;BungaeCard status={bungaeStatus.recruiting}&gt;&lt;/BungaeCard&gt;
          &lt;BungaeCard status={bungaeStatus.recruiting}&gt;&lt;/BungaeCard&gt;
          &lt;BungaeCard status={bungaeStatus.recruiting}&gt;&lt;/BungaeCard&gt;
          &lt;BungaeCard status={bungaeStatus.recruiting}&gt;&lt;/BungaeCard&gt;
        &lt;/ul&gt;
      &lt;/UserBungaeSection&gt;
    &lt;/&gt;
  );
}

export default ProfilePage;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[번개 모임 웹 어플리케이션 - 버튼, 메인 네비게이션 리팩토링 (img 태그 관련)]]></title>
            <link>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-img-%ED%83%9C%EA%B7%B8-div%EB%A1%9C-%EA%B0%90%EC%8B%B8%EA%B8%B0-refactor</link>
            <guid>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-img-%ED%83%9C%EA%B7%B8-div%EB%A1%9C-%EA%B0%90%EC%8B%B8%EA%B8%B0-refactor</guid>
            <pubDate>Wed, 10 May 2023 07:22:03 GMT</pubDate>
            <description><![CDATA[<p>새로고침 할 때마다 잠깐동안 <code>&lt;MainNavigation&gt;</code> 컴포넌트의 레이아웃이 변경됐다가 돌아오는 현상이 있었는데, 자세히 보니 이미지가 뒤늦게 불러와지고 있었다. 그래서 이미지 소스를 가져오기 전에도 레이아웃을 유지하기 위해 <code>&lt;img&gt;</code> 태그의 부모 요소로 <code>&lt;div&gt;</code> 태그를 추가해 width 속성을 추가했다. 이를 위해 <code>&lt;Button&gt;</code> 컴포넌트와 <code>&lt;MainNavigation&gt;</code> 컴포넌트를 수정했다.</p>
<br />

<hr>
<h2 id="버튼-컴포넌트-리팩토링">버튼 컴포넌트 리팩토링</h2>
<p><code>&lt;Button&gt;</code> 컴포넌트에는 2개의 props가 더 추가됐다.</p>
<ul>
<li><code>iconWidth</code> : img 태그를 감싸는 div 요소에 적용될 width</li>
<li><code>iconWithText</code> : 이미지 + 텍스트 조합의 버튼, 하위 태그 클래스(.icon-with-text)로 적용하던 속성을 props로 변경함</li>
</ul>
<br />

<p><code>src/components/ui/Button.js</code></p>
<pre><code class="language-js">import styled, { css } from &quot;styled-components&quot;;

const colorStyles = css`
  ${({ theme, background, color }) =&gt; {
    const selectedBg = theme.palette[background];
    const selectedColor = theme.palette[color];
    return css`
      background: ${selectedBg};
      color: ${selectedColor};
    `;
  }}
`;

const sizes = {
  large: {
    height: &quot;3rem&quot;,
    fontSize: &quot;1.25rem&quot;
  },
  medium: {
    height: &quot;2.875rem&quot;,
    fontSize: &quot;1rem&quot;
  },
  small: {
    height: &quot;1.75rem&quot;,
    fontSize: &quot;0.875rem&quot;
  }
};

const sizeStyles = css`
  ${({ size }) =&gt; css`
    height: ${sizes[size].height};
    font-size: ${sizes[size].fontSize};
  `}
`;

const fullWidthStyle = css`
  ${({ fullWidth }) =&gt;
    fullWidth &amp;&amp;
    css`
      width: 100%;
      justify-content: center;
    `}
`;

const StyledButton = styled.button`
  display: inline-flex;
  align-items: center;
  font-weight: ${({ theme }) =&gt; theme.fontWeight.semiBold};
  outline: none;
  border: none;
  border-radius: 5px;
  ${({ radius }) =&gt;
    radius === &quot;bottom&quot; &amp;&amp;
    css`
      border-top-left-radius: 0px;
      border-top-right-radius: 0px;
    `}

  padding-right: 0.875rem;
  padding-left: 0.875rem;
  ${({ marginTop }) =&gt;
    marginTop &amp;&amp;
    css`
      margin-top: ${marginTop};
    `}

  ${colorStyles}

  ${sizeStyles}

  ${({ outline }) =&gt;
    outline &amp;&amp;
    css`
      border: 1px solid black;
    `}

  ${({ noPadding }) =&gt;
    noPadding &amp;&amp;
    css`
      padding: 0rem;
    `}

  ${fullWidthStyle}

  &gt; div:first-child {
    display: flex;
    ${({ iconWidth }) =&gt;
      iconWidth &amp;&amp;
      css`
        width: ${iconWidth};
      `}

    ${({ iconWithText }) =&gt;
      iconWithText &amp;&amp;
      css`
        margin-right: 6px;
      `}
  }
`;

function Button({
  children,
  background,
  color,
  size = &quot;medium&quot;,
  outline,
  noPadding,
  fullWidth,
  marginTop,
  radius,
  iconWidth,
  iconWithText
}) {
  return (
    &lt;StyledButton
      background={background}
      color={color}
      size={size}
      outline={outline}
      noPadding={noPadding}
      fullWidth={fullWidth}
      marginTop={marginTop}
      radius={radius}
      iconWidth={iconWidth}
      iconWithText={iconWithText}
    &gt;
      {children}
    &lt;/StyledButton&gt;
  );
}

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

<hr>
<h2 id="메인-네비게이션-컴포넌트-리팩토링">메인 네비게이션 컴포넌트 리팩토링</h2>
<p>어플리케이션 로고 등을 가져오기 위해 사용되는 <code>&lt;img&gt;</code> 태그를 <code>&lt;div&gt;</code> 태그로 감싸고 width를 지정해줬다.</p>
<br />

<p><code>수정 후</code></p>
<pre><code class="language-js">import styled, { css } from &quot;styled-components&quot;;

import Button from &quot;./UI/Button&quot;;

const Header = styled.header`
  position: sticky;
  top: 0;
  width: 100%;
  height: 72px;
  display: flex;
  align-items: center;
  padding-right: 1.5rem;
  padding-left: 1.5rem;
  border-bottom: 1px solid black;
  background: white;

  &gt; .ddip-logo {
    width: 94px;
    display: flex;
  }

  &gt; .search-box {
    height: 46px;
    flex-grow: 1;
    display: flex;
    align-items: center;
    padding-right: 0.875rem;
    padding-left: 0.875rem;
    margin-right: 1.5rem;
    margin-left: 1.5rem;
    border: 1px solid black;
    border-radius: 5px;
    font-size: 0.875rem;

    &gt; .search-icon {
      width: 14px;
      margin-right: 6px;
    }
  }

  &gt; .right-buttons {
    display: flex;
    &gt; .auth-buttons {
      display: flex;
      align-items: center;
      margin-left: 16px;

      &gt; .user-icon {
        width: 46px;
        display: flex;
      }

      &gt; hr {
        width: 1px;
        height: 12px;
        margin: 8px;
      }
    }
  }

  ${({ logoOnly }) =&gt;
    logoOnly &amp;&amp;
    css`
      justify-content: center;
      background: inherit;
    `}

  ${({ noSearchBox }) =&gt;
    noSearchBox &amp;&amp;
    css`
      justify-content: space-between;
    `}
`;

function MainNavigation({ logoOnly, noSearchBox, loggedIn }) {
  if (logoOnly) {
    return (
      &lt;Header logoOnly={logoOnly}&gt;
        &lt;div className=&quot;ddip-logo&quot;&gt;
          &lt;img className=&quot;logo&quot; src=&quot;/images/logo.svg&quot; alt=&quot;ddip-logo&quot; /&gt;
        &lt;/div&gt;
      &lt;/Header&gt;
    );
  }

  return (
    &lt;Header noSearchBox={noSearchBox}&gt;
      &lt;div className=&quot;ddip-logo&quot;&gt;
        &lt;img src=&quot;/images/logo.svg&quot; alt=&quot;ddip-logo&quot; /&gt;
      &lt;/div&gt;
      {!noSearchBox &amp;&amp; (
        &lt;div className=&quot;search-box&quot;&gt;
          &lt;div className=&quot;search-icon&quot;&gt;
            &lt;img src=&quot;/images/search.svg&quot; alt=&quot;search&quot; /&gt;
          &lt;/div&gt;
          &lt;span&gt;어떤 번개를 찾으시나요?&lt;/span&gt;
        &lt;/div&gt;
      )}
      &lt;div className=&quot;right-buttons&quot;&gt;
        &lt;Button
          background=&quot;mainViolet&quot;
          color=&quot;white&quot;
          iconWidth=&quot;16px&quot;
          iconWithText
        &gt;
          &lt;div&gt;
            &lt;img src=&quot;/images/thunder.svg&quot; alt=&quot;thunder&quot; /&gt;
          &lt;/div&gt;
          &lt;span&gt;번개 만들기&lt;/span&gt;
        &lt;/Button&gt;
        &lt;div className=&quot;auth-buttons&quot;&gt;
          {loggedIn ? (
            &lt;div className=&quot;user-icon&quot;&gt;
              &lt;img src=&quot;/images/user.svg&quot; alt=&quot;user&quot; /&gt;
            &lt;/div&gt;
          ) : (
            &lt;&gt;
              &lt;Button noPadding&gt;회원가입&lt;/Button&gt;
              &lt;hr /&gt;
              &lt;Button noPadding&gt;로그인&lt;/Button&gt;
            &lt;/&gt;
          )}
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/Header&gt;
  );
}

export default MainNavigation;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[번개 모임 웹 어플리케이션 - 번개 카드, 인풋 마크업]]></title>
            <link>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B2%88%EA%B0%9C-%EC%B9%B4%EB%93%9C-%EB%A7%88%ED%81%AC%EC%97%85</link>
            <guid>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B2%88%EA%B0%9C-%EC%B9%B4%EB%93%9C-%EB%A7%88%ED%81%AC%EC%97%85</guid>
            <pubDate>Tue, 09 May 2023 07:51:18 GMT</pubDate>
            <description><![CDATA[<h2 id="번개-카드-컴포넌트-마크업">번개 카드 컴포넌트 마크업</h2>
<p>번개 카드는 모집 현황에 따라 4개의 다른 UI를 갖는다.</p>
<pre><code class="language-js">const bungaeStatus = {
  // 번개 시간까지 1시간 이내
  imminent: {
    color: &quot;statusRed&quot;,
    text: &quot;마감임박&quot;
  },
  // 번개 게시 1시간 이내
  new: {
    color: &quot;statusYellow&quot;,
    text: &quot;NEW&quot;
  },
  // imminent나 new 상태는 아니지만 모집중
  recruiting: {
    color: &quot;statusGreen&quot;,
    text: &quot;모집중&quot;
  },
  // 번개 시간이 지나 모집마감
  closed: {
    color: &quot;statusGray&quot;,
    text: &quot;모집마감&quot;
  }
}</code></pre>
<br />

<p><code>피그마로 만든 번개 카드 컴포넌트</code>
<img src="https://velog.velcdn.com/images/line_jeong32/post/1d0e7953-f47a-473b-8023-7c25731424a5/image.png" alt=""></p>
<p><code>&lt;BungaeCard&gt;</code> 컴포넌트는 재사용성을 위해 아래와 아래와 같은 props를 가진다.</p>
<ul>
<li><code>status</code> : 모집 상태 객체</li>
<li><code>place</code> : 번개 장소</li>
<li><code>time</code> : 번개 시간</li>
<li><code>title</code> : 글 제목</li>
<li><code>imoji</code> : 유저 이모지</li>
<li><code>nickname</code> : 유저 닉네임</li>
<li><code>numberOfParticipants</code> : 참여자 수</li>
<li><code>numberOfRecruits</code> : 모집자 수</li>
<li><code>duration</code> : 번개 마감까지 기한</li>
</ul>
<br />

<p><code>src/components/BungaeCard.js</code></p>
<pre><code class="language-js">import { Link } from &quot;react-router-dom&quot;;
import styled, { css } from &quot;styled-components&quot;;

const StyledBungaeCard = styled.li`
  width: 285px;
  height: 292px;
  display: inline-flex;
  flex-direction: column;
  justify-content: space-between;
  background: {(props) =&gt; props.theme.palette.white};
  border: 1px solid black;
  border-radius: 5px;
  padding: 26px 20px;

  ${(props) =&gt;
    props.statusText === &quot;모집마감&quot; &amp;&amp;
    css`
      background: ${(props) =&gt; props.theme.palette.gray2};
    `}

  p {
    display: inline-box;
  }

  &gt; .status {
    color: ${({ theme, statusColor }) =&gt; theme.palette[statusColor]};
    font-size: 0.875rem;
    font-weight: ${(props) =&gt; props.theme.fontWeight.bold};
    margin-bottom: 14px;
  }

  &gt; .place, .time {
    color:  ${(props) =&gt; props.theme.palette.gray4};
    font-size: 1rem;
    font-weight: ${(props) =&gt; props.theme.fontWeight.semiBold};
    p:first-child {
      margin-right: 8px;
    }
  }
  &gt; .place {
    margin-bottom: 8px;
  }
  &gt; .time {
    margin-bottom: 14px;
  }

  &gt; h1 {
    font-size: 1.125rem;
    font-weight: ${(props) =&gt; props.theme.fontWeight.bold};
    margin-bottom: 12px;
    line-height: 1.375rem;
    height: 44px;
    ${(props) =&gt;
      props.statusText === &quot;모집마감&quot; &amp;&amp;
      css`
        color: ${({ theme, statusColor }) =&gt; theme.palette[statusColor]};
      `}
  }

  &gt; .bungaeInfo-nickname-numbers {
    display: flex;
    justify-content: space-between;
    margin-bottom: 20px;
    font-size: 0.875rem;

    &gt; .nickname, .numbers {
      span:last-child {
        margin-left: 4px;
      }
    }
    &gt; .numbers {
     display: inline-flex;
     align-items: center; 
    }
    &gt; .nickname {
    color:  ${(props) =&gt; props.theme.palette.gray4};
  }
}

  &gt; .duration {
    font-weight: ${(props) =&gt; props.theme.fontWeight.semiBold};
    text-align: center;
    &gt; div {
      color: ${(props) =&gt; props.theme.palette.mainViolet};
      font-size: ${(props) =&gt; props.theme.fontSize[&quot;4xl&quot;]};
      font-weight: ${(props) =&gt; props.theme.fontWeight.bold};
      margin-top: 10px;
      ${(props) =&gt;
        props.statusText === &quot;모집마감&quot; &amp;&amp;
        css`
          color: ${({ theme, statusColor }) =&gt; theme.palette[statusColor]};
        `}
    }
  }
`;

function BungaeCard({
  status,
  place,
  time,
  title,
  imoji,
  nickname,
  numberOfParticipants,
  numberOfRecruits,
  duration
}) {
  return (
    &lt;Link&gt;
      &lt;StyledBungaeCard statusColor={status.color} statusText={status.text}&gt;
        &lt;div className=&quot;status&quot;&gt;{status.text}&lt;/div&gt;
        &lt;div className=&quot;place&quot;&gt;
          &lt;p&gt;장소 |&lt;/p&gt;
          &lt;p&gt;{place}&lt;/p&gt;
        &lt;/div&gt;
        &lt;div className=&quot;time&quot;&gt;
          &lt;p&gt;시간 |&lt;/p&gt;
          &lt;p&gt;{time}&lt;/p&gt;
        &lt;/div&gt;
        &lt;h1&gt;{title}&lt;/h1&gt;
        &lt;div className=&quot;bungaeInfo-nickname-numbers&quot;&gt;
          &lt;div className=&quot;nickname&quot;&gt;
            &lt;span&gt;{imoji}&lt;/span&gt;
            &lt;span&gt;{nickname}&lt;/span&gt;
          &lt;/div&gt;
          &lt;div className=&quot;numbers&quot;&gt;
            &lt;img src=&quot;/images/recruit.svg&quot; alt=&quot;recruit&quot; /&gt;
            &lt;span&gt;{`${numberOfParticipants}/${numberOfRecruits}`}&lt;/span&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div className=&quot;duration&quot;&gt;
          &lt;p&gt;번개 마감까지&lt;/p&gt;
          &lt;div&gt;{duration}&lt;/div&gt;
        &lt;/div&gt;
      &lt;/StyledBungaeCard&gt;
    &lt;/Link&gt;
  );
}

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

<p>번개 카드 컴포넌트를 사용해 메인 페이지에서 아래와 같이 출력 테스트를 해보았다. 추후 데이터들은 서버에서 받아와 렌더링하고, 작성 시간과 현재 시간을 계산해서 <code>status</code>와 <code>duration</code>의 상태는 프론트엔드 단에서 관리할 예정이다.</p>
<p><img src="https://velog.velcdn.com/images/line_jeong32/post/98bb9c9a-0616-4806-906a-ba0ba67154d5/image.png" alt=""></p>
<p><code>src/pages/BungaeMainPage.js</code></p>
<pre><code class="language-js">import BungaeCard from &quot;../components/BungaeCard&quot;;

const bungaeStatus = {
  imminent: {
    color: &quot;statusRed&quot;,
    text: &quot;마감임박&quot;
  },
  new: {
    color: &quot;statusYellow&quot;,
    text: &quot;NEW&quot;
  },
  recruiting: {
    color: &quot;statusGreen&quot;,
    text: &quot;모집중&quot;
  },
  closed: {
    color: &quot;statusGray&quot;,
    text: &quot;모집마감&quot;
  }
};

function BungaeMainPage() {
  return (
    &lt;div style={{ padding: &quot;40px&quot; }}&gt;
      &lt;ul style={{ display: &quot;flex&quot;, gridGap: &quot;20px&quot; }}&gt;
        &lt;BungaeCard
          status={bungaeStatus.imminent}
          place=&quot;성수동 OO 클라이밍 센터&quot;
          time=&quot;19:00&quot;
          title=&quot;오늘 7시 성수역 클라이밍 하실 분 계신가요!!...&quot;
          imoji=&quot;😶‍🌫️&quot;
          nickname=&quot;닉네임입니다&quot;
          numberOfParticipants={2}
          numberOfRecruits={4}
          duration=&quot;00:30:23&quot;
        /&gt;
        &lt;BungaeCard
          status={bungaeStatus.new}
          place=&quot;성수동 OO 클라이밍 센터&quot;
          time=&quot;19:00&quot;
          title=&quot;오늘 7시 성수역 클라이밍 하실 분 계신가요!!...&quot;
          imoji=&quot;😶‍🌫️&quot;
          nickname=&quot;닉네임입니다&quot;
          numberOfParticipants={2}
          numberOfRecruits={4}
          duration=&quot;00:30:23&quot;
        /&gt;
        &lt;BungaeCard
          status={bungaeStatus.recruiting}
          place=&quot;성수동 OO 클라이밍 센터&quot;
          time=&quot;19:00&quot;
          title=&quot;오늘 7시 성수역 클라이밍 하실 분 계신가요!!...&quot;
          imoji=&quot;😶‍🌫️&quot;
          nickname=&quot;닉네임입니다&quot;
          numberOfParticipants={2}
          numberOfRecruits={4}
          duration=&quot;00:30:23&quot;
        /&gt;
        &lt;BungaeCard
          status={bungaeStatus.closed}
          place=&quot;성수동 OO 클라이밍 센터&quot;
          time=&quot;19:00&quot;
          title=&quot;오늘 7시 성수역 클라이밍 하실 분 계신가요!!...&quot;
          imoji=&quot;😶‍🌫️&quot;
          nickname=&quot;닉네임입니다&quot;
          numberOfParticipants={2}
          numberOfRecruits={4}
          duration=&quot;00:30:23&quot;
        /&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
  );
}

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

<hr>
<h2 id="인풋-컴포넌트-마크업">인풋 컴포넌트 마크업</h2>
<p>로그인, 회원가입, 이메일 인증 및 회원 정보 수정, 회원 탈퇴 페이지에서 일관된 스타일을 지닌 input 요소를 인풋 컴포넌트를 만들었다.
대체로 일관성 있는 디자인이지만 label 요소가 있을 때도, 없을 때도 있고 input 요소 2, 3개 붙어있거나 버튼과 함꼐 결합되는 디자인도 있어서 이를 props 및 조건부를 통해 필요에 따라 적절하게 활용할 수 있도록 작업했다.</p>
<br />

<p><code>피그마로 만든 인풋 디자인</code>
<img src="https://velog.velcdn.com/images/line_jeong32/post/6e9756c4-f6e4-4d1b-b25c-59f5265abc75/image.png" alt=""></p>
<p><code>&lt;Input&gt;</code> 컴포넌트는 재사용성을 위해 아래와 아래와 같은 props를 가진다.</p>
<ul>
<li><code>id</code> : input id</li>
<li><code>label</code> : input label</li>
<li><code>name</code> : input name</li>
<li><code>type</code> : input type (default=&quot;text&quot;)</li>
<li><code>placeholder</code> : input placeholder</li>
<li><code>value</code> : input value</li>
<li><code>onChange</code> : change 이벤트 핸들러</li>
<li><code>onBlur</code> : blur 이벤트 핸들러</li>
<li><code>disabled</code> : input disabled</li>
<li><code>radius</code> : input border-radius (default=&quot;default&quot;, &quot;top&quot;, &quot;bottom&quot;, &quot;none&quot;)</li>
</ul>
<br />

<p><code>src/components/UI/input.js</code></p>
<pre><code class="language-js">import styled, { css } from &quot;styled-components&quot;;

const StyledInput = styled.div`
  width: 100%;

  &gt; .label-wrapper {
    font-size: ${(props) =&gt; props.theme.fontSize.xs};
    margin-bottom: 6px;
  }

  &gt; input {
    width: 100%;
    min-height: 42px;
    padding: 0px 18px;
    font-size: ${(props) =&gt; props.theme.fontSize.sm};
    border: 1px solid black;
    border-radius: 5px 5px 5px 5px;

    ${({ radius }) =&gt; {
      if (radius === &quot;none&quot;) {
        return css`
          border-radius: 0px;
          border-bottom: 0px;
        `;
      }
      if (radius === &quot;top&quot;) {
        return css`
          border-bottom-right-radius: 0px;
          border-bottom-left-radius: 0px;
          border-bottom: 0px;
        `;
      }
      if (radius === &quot;bottom&quot;) {
        return css`
          border-top-left-radius: 0px;
          border-top-right-radius: 0px;
        `;
      }
    }}
  }
`;

function Input({
  id,
  label,
  name,
  type = &quot;text&quot;,
  placeholder,
  value,
  onChange,
  onBlur,
  disabled,
  radius = &quot;default&quot;
}) {
  return (
    &lt;StyledInput radius={radius}&gt;
      {label &amp;&amp; (
        &lt;div className=&quot;label-wrapper&quot;&gt;
          &lt;label htmlFor={id}&gt;{label}&lt;/label&gt;
        &lt;/div&gt;
      )}
      &lt;input
        id={id}
        name={name}
        type={type}
        placeholder={placeholder}
        value={value}
        onChange={onChange}
        onBlur={onBlur}
        disabled={disabled}
      /&gt;
    &lt;/StyledInput&gt;
  );
}

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

<p>인풋 컴포넌트를 사용해 메인 페이지에서 아래와 같이 출력 테스트를 해보았다.</p>
<p><img src="https://velog.velcdn.com/images/line_jeong32/post/d87485a8-0dd1-450d-8b6a-04caed4548f2/image.png" alt=""></p>
<p><code>src/pages/BungaeMain.js</code></p>
<pre><code class="language-js">import styled from &quot;styled-components&quot;;

import Button from &quot;../components/UI/Button&quot;;
import Input from &quot;../components/UI/Input&quot;;

const Wrapper = styled.div`
  padding-top: 40px;
  max-width: 386px;
  margin: 0 auto;
`;

function BungaeMainPage() {
  return (
    &lt;Wrapper&gt;
      &lt;div style={{ marginBottom: &quot;30px&quot; }}&gt;
        &lt;Input id=&quot;이메일&quot; label=&quot;이메일&quot; placeholder=&quot;이메일&quot; /&gt;
      &lt;/div&gt;
      &lt;div style={{ marginBottom: &quot;30px&quot; }}&gt;
        &lt;Input value=&quot;test@test.com&quot; radius=&quot;top&quot; disabled /&gt;
        &lt;Input radius=&quot;bottom&quot; /&gt;
      &lt;/div&gt;
      &lt;div style={{ marginBottom: &quot;30px&quot; }}&gt;
        &lt;Input id=&quot;닉네임&quot; label=&quot;닉네임&quot; value=&quot;닉네임입니다&quot; radius=&quot;top&quot; /&gt;
        &lt;Button outline fullWidth radius=&quot;bottom&quot;&gt;
          닉네임 중복 검사
        &lt;/Button&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;Input
          id=&quot;비밀번호&quot;
          label=&quot;비밀번호&quot;
          placeholder=&quot;현재 비밀번호&quot;
          radius=&quot;top&quot;
        /&gt;
        &lt;Input placeholder=&quot;새 비밀번호&quot; radius=&quot;none&quot; /&gt;
        &lt;Input placeholder=&quot;새 비밀번호 확인&quot; radius=&quot;bottom&quot; /&gt;
      &lt;/div&gt;
    &lt;/Wrapper&gt;
  );
}

export default BungaeMainPage;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[번개 모임 웹 어플리케이션 - 피그마 디자인]]></title>
            <link>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%ED%94%BC%EA%B7%B8%EB%A7%88-%EB%94%94%EC%9E%90%EC%9D%B8</link>
            <guid>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%ED%94%BC%EA%B7%B8%EB%A7%88-%EB%94%94%EC%9E%90%EC%9D%B8</guid>
            <pubDate>Mon, 08 May 2023 13:31:15 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[번개 모임 웹 어플리케이션 - 버튼, 메인 네비게이션 마크업]]></title>
            <link>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B2%84%ED%8A%BC-%EB%A9%94%EC%9D%B8-%EB%84%A4%EB%B9%84%EA%B2%8C%EC%9D%B4%EC%85%98-%EB%A7%88%ED%81%AC%EC%97%85</link>
            <guid>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B2%84%ED%8A%BC-%EB%A9%94%EC%9D%B8-%EB%84%A4%EB%B9%84%EA%B2%8C%EC%9D%B4%EC%85%98-%EB%A7%88%ED%81%AC%EC%97%85</guid>
            <pubDate>Sun, 07 May 2023 12:51:12 GMT</pubDate>
            <description><![CDATA[<h2 id="버튼-컴포넌트-마크업">버튼 컴포넌트 마크업</h2>
<p>styled-component의 <code>createGlobalStyle</code>을 이용한 전역 스타일 설정에서 버튼 요소는 아래와 같은 스타일을 기본으로 한다.</p>
<p><code>src/styles/GlobalStyles.js</code></p>
<pre><code class="language-css">button {
    background: transparent;
    cursor: pointer;
    }</code></pre>
<br />

<p>추가로 styled-components의 <code>ThemeProvider</code>를 활용해 공통 스타일을 적용하여 코드의 통일성을 높이고 유지 보수를 효율적으로 할 수 있도록 관리하고자 한다. 우선 당장 필요한 기본적인 값들을 설정하고 필요할 때마다 점차 추가/수정해 나갈 것이다.</p>
<p><code>src/styles/theme.js</code></p>
<pre><code class="language-js">const fontSize = {
  xs: &quot;0.75rem&quot;, // 12px
  sm: &quot;0.875rem&quot;, // 14px
  base: &quot;1rem&quot;, // 16px
  md: &quot;1.125&quot;, // 18px
  lg: &quot;1.25rem&quot; // 20px
};

const fontWeight = {
  semiBold: 600,
  bold: &quot;bold&quot;
};

const palette = {
  mainViolet: &quot;#5F32E4&quot;,
  mainMauve: &quot;#E5E2F6&quot;,
  black: &quot;black&quot;,
  white: &quot;white&quot;
};

const theme = { fontSize, fontWeight, palette };

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

<p><code>&lt;Button&gt;</code> 컴포넌트는 재사용성을 위해 아래와 아래와 같은 props를 가진다.</p>
<ul>
<li><code>children</code></li>
<li><code>background</code> : 배경색 (<code>theme.js</code> 파일 참조)</li>
<li><code>color</code> : 폰트색 (<code>theme.js</code> 파일 참조)</li>
<li><code>size</code> : large, medium (default), small</li>
<li><code>outline</code> : border (1px solid black)</li>
<li><code>noPadding</code> : padding 0</li>
<li><code>fullWidth</code> : width 100%</li>
<li><code>marginTop</code> : marginTop 조정 @ 23/05/09 추가</li>
<li><code>radius</code> : radius 조정 @ 23/05/09 추가</li>
<li><code>iconWidth</code> : img 태그를 감싸는 div 요소에 적용될 width @ 23/05/10 추가</li>
<li><code>iconWithText</code> : 이미지 + 텍스트 조합의 버튼 (margin-right) @ 23/05/10 추가</li>
</ul>
<br />

<p><code>src/components/ui/Button.js</code></p>
<pre><code class="language-js">import styled, { css } from &quot;styled-components&quot;;

// 버튼 background, color
const colorStyles = css`
  ${({ theme, background, color }) =&gt; {
    const selectedBg = theme.palette[background];
    const selectedColor = theme.palette[color];
    return css`
      background: ${selectedBg};
      color: ${selectedColor};
    `;
  }}
`;

const sizes = {
  large: {
    height: &quot;3rem&quot;,
    fontSize: &quot;1.25rem&quot;
  },
  medium: {
    height: &quot;2.875rem&quot;,
    fontSize: &quot;1rem&quot;
  },
  small: {
    height: &quot;1.75rem&quot;,
    fontSize: &quot;0.875rem&quot;
  }
};


// 버튼 height, font-size에 따라 large, medium, small 3개로 분류
const sizeStyles = css`
  ${({ size }) =&gt; css`
    height: ${sizes[size].height};
    font-size: ${sizes[size].fontSize};
  `}
`;

// 버튼 width 100%
const fullWidthStyle = css`
  ${(props) =&gt;
    props.fullWidth &amp;&amp;
    css`
      width: 100%;
      justify-content: center;
    `}
`;

const StyledButton = styled.button`
  /* 버튼 공통 스타일 */
  display: inline-flex;
  align-items: center;
  font-weight: ${(props) =&gt; props.theme.fontWeight.semiBold};
  outline: none;
  border: none;
  border-radius: 5px;
  ${({ radius }) =&gt;
    radius === &quot;bottom&quot; &amp;&amp;
    css`
      border-top-left-radius: 0px;
      border-top-right-radius: 0px;
    `}

  padding-right: 0.875rem;
  padding-left: 0.875rem;
  ${({ marginTop }) =&gt;
    marginTop &amp;&amp;
    css`
      margin-top: ${marginTop};
    `}


/* props에 따라 다른 스타일 적용 */

  ${colorStyles}

  ${sizeStyles}

  ${(props) =&gt;
    props.outline &amp;&amp;
    css`
      border: 1px solid black;
    `}

  ${(props) =&gt;
    props.noPadding &amp;&amp;
    css`
      padding: 0rem;
    `}

  ${fullWidthStyle}

  &gt; .icon-with-text {
    margin-right: 6px;
  }
`;

function Button({
  children,
  background,
  color,
  size = &quot;medium&quot;,
  outline,
  noPadding,
  fullWidth,
  marginTop,
  radius
}) {
  return (
    &lt;StyledButton
      background={background}
      color={color}
      size={size}
      outline={outline}
      noPadding={noPadding}
      fullWidth={fullWidth}
      marginTop={marginTop}
      radius={radius}
    &gt;
      {children}
    &lt;/StyledButton&gt;
  );
}

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

<hr>
<h2 id="메인-네비게이션-컴포넌트-마크업">메인 네비게이션 컴포넌트 마크업</h2>
<p>메인 네비게이션은 다음 3가지와 관련된 props를 받아서 페이지와 상태에 따라 동적으로 UI가 변경돼야 한다.</p>
<ul>
<li>로그인 상태인지 아닌지</li>
<li>검색바가 필요한지 필요 없는지</li>
<li>로고만 보여줄 것인지 아닌지</li>
</ul>
<br />

<p><code>피그마로 만든 메인 네비게이션 컴포넌트</code> 
<img src="https://velog.velcdn.com/images/line_jeong32/post/aff512d9-da78-41c0-a07c-7180dcae4883/image.png" alt=""></p>
<br />

<p><code>&lt;MainNavigation&gt;</code> 컴포넌트는 재사용성을 위해 아래와 아래와 같은 props를 가진다.</p>
<ul>
<li><code>logoOnly</code> : 메인 네비게이션 중앙에 로고만 있는 레이아웃 (로그인, 회원 가입, 이메일 인증 페이지)</li>
<li><code>noSearchBox</code> : 메인 네비게이션에 검색바가 없는 레이아웃 (검색 페이지)</li>
<li><code>loggedIn</code> : 로그인 했을 때 레이아웃</li>
</ul>
<br />

<p><code>src/componenets/MainNavigation.js</code></p>
<pre><code class="language-js">import styled, { css } from &quot;styled-components&quot;;

import Button from &quot;./UI/Button&quot;;

const Header = styled.header`
  position: sticky;
  top: 0;
  width: 100%;
  height: 72px;
  display: flex;
  align-items: center;
  padding-right: 1.5rem;
  padding-left: 1.5rem;
  border-bottom: 1px solid black;
  background: white;

  &gt; .search-box {
    height: 46px;
    flex-grow: 1;
    display: flex;
    align-items: center;
    padding-right: 0.875rem;
    padding-left: 0.875rem;
    margin-right: 1.5rem;
    margin-left: 1.5rem;
    border: 1px solid black;
    border-radius: 5px;
    font-size: 0.875rem;

    &gt; img {
      margin-right: 8px;
      margin-bottom: 2px;
    }
  }

  &gt; .right-buttons {
    display: flex;
    &gt; .auth-buttons {
      display: flex;
      align-items: center;
      padding: 2px;
      margin-left: 0.7rem;

      &gt; hr {
        width: 1px;
        height: 12px;
        margin: 8px;
      }
    }
  }

  ${(props) =&gt;
    props.logoOnly &amp;&amp;
    css`
      justify-content: center;
      background: ${props.theme.palette.mainMauve};
    `}

  ${(props) =&gt;
    props.noSearchBox &amp;&amp;
    css`
      justify-content: space-between;
    `}
`;

function MainNavigation({ logoOnly, noSearchBox, loggedIn }) {
  if (logoOnly) {
    return (
      &lt;Header logoOnly={logoOnly}&gt;
        &lt;img src=&quot;/images/logo.svg&quot; alt=&quot;logo&quot; /&gt;
      &lt;/Header&gt;
    );
  }

  return (
    &lt;Header noSearchBox={noSearchBox}&gt;
      &lt;img src=&quot;/images/logo.svg&quot; alt=&quot;logo&quot; /&gt;
      {!noSearchBox &amp;&amp; (
        &lt;div className=&quot;search-box&quot;&gt;
          &lt;img src=&quot;/images/search.svg&quot; alt=&quot;search&quot; /&gt;
          &lt;span&gt;어떤 번개를 찾으시나요?&lt;/span&gt;
        &lt;/div&gt;
      )}
      &lt;div className=&quot;right-buttons&quot;&gt;
        &lt;Button background=&quot;mainViolet&quot; color=&quot;white&quot;&gt;
          &lt;img
            className=&quot;icon-with-text&quot;
            src=&quot;/images/thunder.svg&quot;
            alt=&quot;thunder&quot;
          /&gt;
          &lt;span&gt;번개 만들기&lt;/span&gt;
        &lt;/Button&gt;
        &lt;div className=&quot;auth-buttons&quot;&gt;
          {loggedIn ? (
            &lt;img src=&quot;/images/user.svg&quot; alt=&quot;user&quot; /&gt;
          ) : (
            &lt;&gt;
              &lt;Button noPadding&gt;회원가입&lt;/Button&gt;
              &lt;hr /&gt;
              &lt;Button noPadding&gt;로그인&lt;/Button&gt;
            &lt;/&gt;
          )}
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/Header&gt;
  );
}

export default MainNavigation;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[번개 모임 웹 어플리케이션 - proxy 설치 및 설정]]></title>
            <link>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-proxy-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@line_jeong32/%EB%B2%88%EA%B0%9C-%EB%AA%A8%EC%9E%84-%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-proxy-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Fri, 05 May 2023 13:07:51 GMT</pubDate>
            <description><![CDATA[<p>로컬 개발 환경에서 API 요청 시 발생하는 CORS 에러를 해결하기 위해 프록시 설정을 했다. 가장 간단한 방법으로는 package.json에 proxy 값을 설정하는 방법이 있다. 하지만 package.json 파일은 원격 레포지토리에 커밋하고 있기 때문에 해당 방법을 사용하면 서버 주소까지 원격 레포지토리에 올라가게 된다. 그래서 <code>http-proxy-middleware</code> 라이브러리를 사용하는 방법을 택했다.</p>
<br />

<hr>
<h2 id="proxy-설치-및-설정">proxy 설치 및 설정</h2>
<br />

<h3 id="1-http-proxy-middleware-설치">1. http-proxy-middleware 설치</h3>
<pre><code class="language-bash">npm i -D http-proxy-middleware</code></pre>
<br />

<h3 id="2-setupproxyjs-작성">2. setupProxy.js 작성</h3>
<p><code>src/setupProxy.js</code></p>
<pre><code class="language-js">const { createProxyMiddleware } = require(&quot;http-proxy-middleware&quot;);

module.exports = (app) =&gt; {
  app.use(
    [&quot;/auth&quot;, &quot;/users&quot;, &quot;/meeting&quot;],
    createProxyMiddleware({
      target: &quot;서버 주소&quot;,
      changeOrigin: true
    })
  );
};</code></pre>
<br />

<h3 id="3-gitignore-파일에-setupproxyjs-추가">3. .gitignore 파일에 setupProxy.js 추가</h3>
<pre><code># See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

# proxy
setupProxy.js</code></pre>]]></description>
        </item>
    </channel>
</rss>