<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hoon-devlog.log</title>
        <link>https://velog.io/</link>
        <description>프론트엔드 학습 과정을 기록하고 있습니다. </description>
        <lastBuildDate>Mon, 17 Jul 2023 11:39:39 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hoon-devlog.log</title>
            <url>https://velog.velcdn.com/images/hoon-devlog/profile/bf2c1859-689b-4f77-a1d3-77764904efa0/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hoon-devlog.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hoon-devlog" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[CORS 이슈 해결하기]]></title>
            <link>https://velog.io/@hoon-devlog/CORS-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hoon-devlog/CORS-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 17 Jul 2023 11:39:39 GMT</pubDate>
            <description><![CDATA[<p>서버와 클라이언트가 다른 도메인에서 작동하면서 발생하는 크로스 도메인 이슈는 많은 웹 개발자들이 고민하게 만드는 문제 중 하나이다. 이번에는 프로젝트를 진행하면서 발생한 크로스 도메인 이슈 중에서 CORS(Cross-Origin Resource Sharing) 문제를 해결하는 방법에 대해서 알아보자.</p>
<h2 id="1-문제-상황-및-원인">1. 문제 상황 및 원인</h2>
<p>React.js를 이용하여 프론트엔드를 개발하고, Express.js를 이용하여 백엔드를 개발하던 중, 클라이언트와 서버 간의 통신에 문제가 발생하였다. 클라이언트는 localhost:3000에서 동작하고, 서버는 localhost:8000에서 동작하고 있었ek.</p>
<p>클라이언트에서 서버로 API 요청을 보내면 요청은 정상적으로 전달되었지만, 응답을 받을 때 CORS(Cross-Origin Resource Sharing) 정책에 의해 브라우저에서 응답을 받지 못하는 문제가 발생하였다. 이러한 문제는 클라이언트와 서버가 다른 도메인(여기서는 포트가 다르므로)에서 작동하기 때문에 발생한다.</p>
<p>해당 프로젝트에서는 localhost:3000(클라이언트)와 localhost:8000(서버)이 다른 도메인으로 취급되므로, CORS 정책에 의해 클라이언트에서 서버로의 요청은 제한되었다.</p>
<h2 id="2-문제-해결">2. 문제 해결</h2>
<p>CORS 문제를 해결하기 위해, 서버에서 CORS 미들웨어를 사용하여 적절한 헤더를 설정하였습니다. Node.js와 Express.js를 사용하는 서버에서는 &#39;cors&#39; 라는 라이브러리를 이용하여 CORS 문제를 해결할 수 있습니다.</p>
<p>먼저, &#39;cors&#39; 라이브러리를 설치한다.</p>
<pre><code>npm install cors</code></pre><p>그리고 서버에 CORS 미들웨어를 추가한다.</p>
<pre><code class="language-jsx">const cors = require(&quot;cors&quot;);
// ...
app.use(
  cors({
    origin: &quot;http://localhost:3000&quot;,
    credentials: true,
  })
);</code></pre>
<p>여기서, <code>origin</code>은 클라이언트의 도메인을 설정하는 부분이며, <code>credentials</code>는 쿠키와 같은 자격 증명을 포함한 요청을 허용하도록 설정한다. 이렇게 설정하면, 클라이언트에서 API 요청을 보낼 때 CORS 이슈가 발생하지 않는다.</p>
<h2 id="3-배운점">3. 배운점</h2>
<p>CORS는 보안을 위해 필요한 웹 표준이지만, 때때로 개발을 어렵게 만드는 문제를 발생시킨 다는 것을 깨닫게 되었다. 하지만 적절한 라이브러리와 설정을 통해 이 문제를 간단히 해결할 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드에서 새로고침 시 로그인 상태유지가 되지 않는 문제 해결하기]]></title>
            <link>https://velog.io/@hoon-devlog/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90%EC%84%9C-%EC%83%88%EB%A1%9C%EA%B3%A0%EC%B9%A8-%EC%8B%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%83%81%ED%83%9C%EC%9C%A0%EC%A7%80%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hoon-devlog/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90%EC%84%9C-%EC%83%88%EB%A1%9C%EA%B3%A0%EC%B9%A8-%EC%8B%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%83%81%ED%83%9C%EC%9C%A0%EC%A7%80%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 17 Jul 2023 10:58:00 GMT</pubDate>
            <description><![CDATA[<p>사용자 인증 기능을 구현하면서 가장 고민했던 문제 중 하나는, 프론트엔드에서 페이지를 새로고침하였을 때 로그인 상태가 유지되지 않는 이슈였다. 세션을 이용해 서버에서는 로그인과 로그아웃이 정상적으로 이루어졌지만, 프론트엔드에서는 새로고침을 하면 사용자의 로그인 상태가 초기화되는 문제가 있었는데, 이 문제를 어떻게 해결하였는지 알아 보도록하자.</p>
<h2 id="1-문제-상황">1. 문제 상황</h2>
<p>Next.js와 Express를 이용하여 개발을 진행하던 중, 사용자의 로그인 상태를 세션에 저장하였다. 서버에서 로그인과 로그아웃이 정상적으로 작동하였지만, 프론트엔드에서 페이지를 새로고침하면 로그인 상태가 초기화되는 문제가 발생하였다.</p>
<h2 id="2-문제-원인">2. 문제 원인</h2>
<p>프론트엔드에서 페이지를 새로고침하면, React 컴포넌트는 초기 상태로 다시 렌더링되는데, 이 때 로그인 상태가 Redux 스토어에 저장되어 있다면 이 정보도 초기화되어 버리므로 사용자의 로그인 상태가 사라지게 된다.</p>
<p>로그인 상태를 관리하는 Redux 스토어의 초기 상태는 다음과 같다.</p>
<pre><code class="language-jsx">// src/store/authSlice.js

const authSlice = createSlice({
  name: &#39;auth&#39;,
  initialState: {
    isLoggedIn: false,
    user: null,
  },
  // ...
});</code></pre>
<h2 id="문제-해결">문제 해결</h2>
<p>해당 문제를 해결하기 위해, 페이지를 새로고침할 때마다 서버에 로그인 상태를 체크하는 요청을 보냈다. 이렇게 하면, 페이지를 새로고침할 때마다 서버에서 현재 사용자의 로그인 상태를 받아와서 프론트엔드의 상태를 업데이트할 수 있다.</p>
<p>먼저, 로그인 상태를 체크하는 API를 만든다.</p>
<pre><code class="language-jsx">// controllers/authController.js

exports.checkLoginStatus = (req, res, next) =&gt; {
  if (req.session &amp;&amp; req.session.user) {
    res.status(200).json({
      message: &quot;로그인 상태 입니다.&quot;,
      user: req.session.user,
    });
  } else {
    res.status(401).json({ message: &quot;로그아웃 상태 입니다.&quot; });
  }
};</code></pre>
<p>그리고 이 API를 호출하는 함수를 작성한다.</p>
<pre><code class="language-jsx">// src/api/auth.js

export const checkUserLoginStatus = async () =&gt; {
  try {
    const response = await axiosInstance.get(&#39;/auth/check-login-status&#39;);
    return response;
  } catch (error) {
    console.error(error);
    throw error;
  }
};</code></pre>
<p>마지막으로, 프론트엔드에서 페이지가 렌더링될 때마다 로그인 상태를 체크하는 로직을 <code>_app.jsx</code>에 추가한다. 이때, <code>login</code>과 <code>logout</code>은 Redux 스토어에서 사용자의 로그인 상태를 변경하는 액션이다.</p>
<pre><code class="language-jsx">// src/pages/_app.jsx

const App = ({ Component, pageProps }) =&gt; {
  // ...
  const checkLoginStatus = async () =&gt; {
    try {
      const response = await checkUserLoginStatus();
      if (response.status === 200) {
        dispatch(login(response.data.user));
      } else {
        dispatch(logout());
      }
    } catch (error) {
      console.error(error);
    }
  };

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

  // ...
};

export default wrapper.withRedux(App);</code></pre>
<p>이렇게 하면, 페이지를 새로고침하였을 때마다 서버에 로그인 상태 체크 요청을 보내서, 서버의 응답에 따라 프론트엔드의 로그인 상태를 업데이트하게 된다. 이렇게 하여, 새로고침에도 로그인 상태를 유지할 수 있었다.</p>
<h2 id="배운점">배운점</h2>
<p>이 트러블슈팅 경험을 통해, 페이지를 새로고침하였을 때 프론트엔드의 상태가 초기화되는 문제를 해결하는 방법을 배웠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Multer로 프로필 이미지 업로드 구현하기]]></title>
            <link>https://velog.io/@hoon-devlog/Multer%EB%A1%9C-%ED%94%84%EB%A1%9C%ED%95%84-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hoon-devlog/Multer%EB%A1%9C-%ED%94%84%EB%A1%9C%ED%95%84-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 17 Jul 2023 07:52:49 GMT</pubDate>
            <description><![CDATA[<h2 id="1-multer를-사용한-프로필-이미지-업로드-시스템-설정">1. <strong>Multer를 사용한 프로필 이미지 업로드 시스템 설정</strong></h2>
<p>이 프로젝트에서 회원가입이 성공적으로 완료되었다면 사용자가 자신의 프로필 이미지를 업로드 할 수 있다. 이를 구현하기 위해,  Node.js 환경에서 파일 업로드를 다루는 데 매우 유용한 라이브러리인 Multer를 사용해 보자.</p>
<p>Multer는 multipart/form-data를 처리하기 위한 node.js의 미들웨어로, 이는 주로 사용자가 웹페이지를 통해 서버에 파일을 업로드할 때 사용된다.</p>
<p>다음은 Multer의 기본 설정이 담긴 <code>multerConfig.js</code> 파일로, Multer를 이용해 사용자가 업로드한 이미지를 서버에 저장하는 설정을 하는 모듈이다.</p>
<pre><code class="language-jsx">// multerConfig.js

const multer = require(&quot;multer&quot;);
const path = require(&quot;path&quot;);

// 파일을 저장할 디렉토리 설정
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    let uploadPath;
    if (file.fieldname === &quot;profileImage&quot;) {
      uploadPath = &quot;uploads/profile&quot;; // 프로필 이미지 저장 경로
    } else if (file.fieldname === &quot;cafeImage&quot;) {
      uploadPath = &quot;uploads/cafe&quot;; // 카페 이미지 저장 경로
    } else {
      uploadPath = &quot;uploads/default&quot;; // 기본 이미지 저장 경로
    }
    cb(null, uploadPath);
  },

  filename: function (req, file, cb) {
    cb(null, Date.now() + path.extname(file.originalname)); // 파일 이름 설정
  },
});

// 이미지 파일 확인
const fileFilter = (req, file, cb) =&gt; {
  const allowedTypes = [&quot;image/jpeg&quot;, &quot;image/jpg&quot;, &quot;image/png&quot;];

  if (!allowedTypes.includes(file.mimetype)) {
    const error = new Error(&quot;허용되지 않는 파일 형식입니다&quot;);
    error.code = &quot;INCORRECT_FILETYPE&quot;;
    return cb(error, false);
  }

  cb(null, true);
};

const upload = multer({
  storage: storage,
  fileFilter,
  limits: {
    fileSize: 10000000, // 파일 사이즈 10MB로 제한
  },
});

module.exports = upload;</code></pre>
<p>이제 구체적인 부분들을 하나씩 살펴보도록하자.</p>
<h3 id="1-1-multer와-path-모듈을-import">1-1. Multer와 path 모듈을 import</h3>
<pre><code class="language-jsx">const multer = require(&quot;multer&quot;);
const path = require(&quot;path&quot;);</code></pre>
<p>먼저 Multer와 path 모듈을 import 한다. Multer는 파일 업로드를 위한 모듈이고, path는 파일과 디렉토리 경로를 작업하는데 사용되는 Node.js의 내장 모듈이다.</p>
<h3 id="1-2-파일-경로와-이름-설정">1-2 파일 경로와 이름 설정</h3>
<p>다음으로 Multer의 <code>diskStorage</code> 메서드를 이용해 저장될 파일의 경로(<code>destination</code>)와 이름(<code>filename</code>)을 설정한다.</p>
<pre><code class="language-jsx">const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    let uploadPath;
    if (file.fieldname === &quot;profileImage&quot;) {
      uploadPath = &quot;uploads/profile&quot;; // 프로필 이미지 저장 경로
    } else if (file.fieldname === &quot;cafeImage&quot;) {
      uploadPath = &quot;uploads/cafe&quot;; // 카페 이미지 저장 경로
    } else {
      uploadPath = &quot;uploads/default&quot;; // 기본 이미지 저장 경로
    }
    cb(null, uploadPath);
  },

  filename: function (req, file, cb) {
    cb(null, Date.now() + path.extname(file.originalname)); // 파일 이름 설정
  },
});</code></pre>
<p><code>destination</code>에 전달되는 함수는 파일이 저장될 경로를 결정한다. 해당 프로젝트에서는 파일이 프로필 이미지인지 카페 이미지인지에 따라 다른 경로에 저장되도록 설정하였다. 그 외의 경우는 &#39;uploads/default&#39;라는 경로에 저장되도록 설정되어 있다.</p>
<p><code>filename</code>에 전달되는 함수는 파일의 이름을 결정한다. 이 경우, 파일의 원래 확장자(<code>path.extname(file.originalname)</code>)를 유지하면서, 파일 이름 앞에 현재 시간을 붙여 유일성을 보장하도록 하였다.</p>
<h3 id="1-3-업로드-파일-형식-결정">1-3 업로드 파일 형식 결정</h3>
<pre><code class="language-jsx">const fileFilter = (req, file, cb) =&gt; {
  const allowedTypes = [&quot;image/jpeg&quot;, &quot;image/jpg&quot;, &quot;image/png&quot;];

  if (!allowedTypes.includes(file.mimetype)) {
    const error = new Error(&quot;허용되지 않는 파일 형식입니다&quot;);
    error.code = &quot;INCORRECT_FILETYPE&quot;;
    return cb(error, false);
  }

  cb(null, true);
};</code></pre>
<p><code>fileFilter</code> 함수는 어떤 파일을 허용할 것인지 결정하는 역할을 한다. 허용되는 파일 형식들을 <code>allowedTypes</code> 배열에 지정하고, 업로드 된 파일의 형식(<code>file.mimetype</code>)이 이 배열에 포함되어 있지 않으면 에러를 발생시키며, 포함되어 있으면 파일 업로드를 허용한다.</p>
<h3 id="1-4-파일-크기-제한">1-4 파일 크기 제한</h3>
<pre><code class="language-jsx">const upload = multer({
  storage: storage,
  fileFilter,
  limits: {
    fileSize: 10000000, // 파일 사이즈 10MB로 제한
  },
});

module.exports = upload;</code></pre>
<p>마지막으로 <code>multer</code> 함수에 위에서 정의한 <code>storage</code>, <code>fileFilter</code>와 파일 크기 제한을 옵션으로 넘겨준다. 이렇게 설정된 multer 인스턴스를 <code>upload</code>라는 이름으로 export하여, 다른 모듈에서 이를 사용할 수 있다. 해당 프로젝트에서는 파일 크기를 10MB까지 허용하였다.</p>
<p>이를 통해 사용자가 업로드한 파일의 저장 경로, 파일 이름, 허용되는 파일 형식, 파일 크기 등을 쉽게 제어할 수 있다.</p>
<h2 id="2-프로필-이미지-업로드-시스템-구현">2. <strong>프로필 이미지 업로드 시스템 구현</strong></h2>
<p>그럼 코드의 각 부분을 자세히 설명하겠습니다.</p>
<h3 id="2-1-클라이언트가-프로필-이미지-정보를-제출">2-1 <strong>클라이언트가 프로필 이미지 정보를 제출</strong></h3>
<p>다음은 클라이언트가 사용자 프로필 이미지 정보를 제출하는 <code>AddProfileImg.jsx</code> 컴포넌트이다.</p>
<pre><code class="language-jsx">// src/components/signup/addProfileImg/AddProfileImg.jsx

import React, { useState, useRef } from &#39;react&#39;;
import { useSelector, useDispatch } from &#39;react-redux&#39;;

import BaseModal from &#39;../../common/BaseModal.jsx&#39;;
import {
  addProfileImgModalToSignupModal,
  addProfileImgModalTosignupSuccessModal,
} from &#39;../../../store/modalSlice.js&#39;;
import { updateProfileImage } from &#39;../../../store/authSlice.js&#39;;

import { updateUserProfileImage } from &#39;../../../api/auth.js&#39;;

import {
  AddProfileImgModalContent,
  AddProfileModalText,
  ProfileImgLabel,
  ProfileImgInput,
  UploadProfileImgBtn,
  NoUploadProfileImgBtn,
  SubmitProfileImgBtn,
} from &#39;./AddProgileImgStyle.js&#39;;

import userProfile from &#39;../../../../public/assets/icons/user.svg&#39;;

const AddProfileImg = () =&gt; {
  // 리덕스 스토어로부터 현재 모달의 상태 가져오기
  const isAddProfileImgModalVisible = useSelector(
    state =&gt; state.modal.isAddProfileImgModalVisible
  );
  const dispatch = useDispatch();

  // 프로필 이미지 미리보기와 이미지 업로드 여부를 관리하 상태
  const [preview, setPreview] = useState(null);
  const [isImageUploaded, setIsImageUploaded] = useState(false);

  // input 참조를 저장
  const fileInputRef = useRef(null);

  // 모달을 회원가입 모달로 전환
  const handleAddProfileImgModalToSignupModal = () =&gt; {
    dispatch(addProfileImgModalToSignupModal());
  };

  // 모달을 회원가입 성공 모달로 전환
  const handleAddProfileImgModalTosignupSuccessModal = () =&gt; {
    dispatch(addProfileImgModalTosignupSuccessModal());
  };

  // 프로필 이미지 등록을 처리
  const handleAddProfileImgSubmit = async event =&gt; {
    event.preventDefault();

    try {
      if (isImageUploaded) {
        // 이미지 파일을 formData에 추가
        const file = fileInputRef.current.files[0];
        // formData.append(&#39;profileImage&#39;, file);

        // 서버에 파일을 전송
        const response = await updateUserProfileImage(file)

        console.log(response.data);

        // 서버로부터 받은 사용자 정보로 프로필 이미지 상태 업데이트
        dispatch(updateProfileImage(response.data.profileImage));
      }

      handleAddProfileImgModalTosignupSuccessModal();
    } catch (error) {
      console.log(error);
    }
  };

  // 이미지를 선택하면 해당 이미지를 미리보기로 설정
  const handleImageChange = event =&gt; {
    event.stopPropagation();

    let reader = new FileReader();
    let file = event.target.files[0];

    reader.onloadend = () =&gt; {
      setPreview(reader.result);
      setIsImageUploaded(true);
    };

    if (file) {
      reader.readAsDataURL(file);
    }
  };

  // 파일 선택 대화상자를 열어 사용자가 이미지를 선택
  const handleChooseFile = event =&gt; {
    event.preventDefault();
    fileInputRef.current.click();
  };

  return (
    &lt;BaseModal
      isVisible={isAddProfileImgModalVisible}
      onBack={handleAddProfileImgModalToSignupModal}
      title=&#39;프로필 생성하기&#39;
    &gt;
      &lt;AddProfileImgModalContent onSubmit={handleAddProfileImgSubmit}&gt;
        &lt;AddProfileModalText&gt;
          {isImageUploaded
            ? &#39;좋아요!&#39;
            : `프로필 이미지 또는 업로드 버튼을 클릭해서\n이미지를 업로드 하세요`}
        &lt;/AddProfileModalText&gt;
        &lt;ProfileImgLabel
          htmlFor=&#39;user-img&#39;
          background={preview || userProfile.src}
        &gt;&lt;/ProfileImgLabel&gt;
        &lt;ProfileImgInput
          type=&#39;file&#39;
          id=&#39;user-img&#39;
          name=&#39;user-img&#39;
          onChange={handleImageChange}
          ref={fileInputRef}
        &gt;&lt;/ProfileImgInput&gt;

        {isImageUploaded ? (
          &lt;&gt;
            &lt;SubmitProfileImgBtn onClick={handleAddProfileImgSubmit}&gt;
              완료
            &lt;/SubmitProfileImgBtn&gt;
            &lt;UploadProfileImgBtn
              onClick={event =&gt; handleChooseFile(event)}
              isImageUploaded={isImageUploaded}
            &gt;
              사진 변경하기
            &lt;/UploadProfileImgBtn&gt;
          &lt;/&gt;
        ) : (
          &lt;&gt;
            &lt;UploadProfileImgBtn
              onClick={event =&gt; handleChooseFile(event)}
              isImageUploaded={isImageUploaded}
            &gt;
              사진 업로드하기
            &lt;/UploadProfileImgBtn&gt;
            &lt;NoUploadProfileImgBtn onClick={handleAddProfileImgSubmit}&gt;
              나중에 할게요
            &lt;/NoUploadProfileImgBtn&gt;
          &lt;/&gt;
        )}
      &lt;/AddProfileImgModalContent&gt;
    &lt;/BaseModal&gt;
  );
};

export default AddProfileImg;</code></pre>
<p><strong>2-1-1. 사용자 이미지 업로드 UI 컴포넌트 정의</strong></p>
<p>먼저, 사용자가 이미지를 업로드할 수 있도록 UI를 제공하는 React 컴포넌트를 정의한다.</p>
<pre><code class="language-jsx">const AddProfileImg = () =&gt; {
  // 리덕스 스토어로부터 현재 모달의 상태 가져오기
  const isAddProfileImgModalVisible = useSelector(
    state =&gt; state.modal.isAddProfileImgModalVisible
  );
  const dispatch = useDispatch();

  // 프로필 이미지 미리보기와 이미지 업로드 여부를 관리하 상태
  const [preview, setPreview] = useState(null);
  const [isImageUploaded, setIsImageUploaded] = useState(false);

  // input 참조를 저장
  const fileInputRef = useRef(null);
  //...</code></pre>
<p><code>isAddProfileImgModalVisible</code>는 현재 모달이 보이는지를 확인하고, <code>dispatch</code>는 액션을 Redux에 전달하는 함수이다. <code>preview</code>와 <code>isImageUploaded</code>는 각각 이미지의 미리보기 URL과 이미지가 업로드되었는지를 관리하는 상태이며, <code>fileInputRef</code>는 이미지 파일을 선택하는데 사용하는 file input 요소에 대한 참조이다.</p>
<p><strong>2-1-2. 프로필 이미지 파일 서버에 전송</strong> </p>
<pre><code class="language-jsx">  // 프로필 이미지 등록을 처리
  const handleAddProfileImgSubmit = async event =&gt; {
    event.preventDefault();

    try {
      if (isImageUploaded) {
        // 이미지 파일을 formData에 추가
        const file = fileInputRef.current.files[0];

        // 서버에 파일을 전송
        const response = await updateUserProfileImage(file)

        console.log(response.data);

        // 서버로부터 받은 사용자 정보로 프로필 이미지 상태 업데이트
        dispatch(updateProfileImage(response.data.profileImage));
      }

      handleAddProfileImgModalTosignupSuccessModal();
    } catch (error) {
      console.log(error);
    }
  };</code></pre>
<p><code>handleAddProfileImgSubmit</code> 함수는 사용자가 이미지를 선택하고 &#39;완료&#39; 버튼을 눌렀을 때 호출된다. 이 함수는 선택된 이미지 파일을 서버에 전송하고, 서버로부터 응답을 받아 Redux 스토어의 프로필 이미지를 업데이트한다.</p>
<h3 id="2-2-서버에서-프로필-이미지-파일-처리">2-2. 서버에서 프로필 이미지 파일 처리</h3>
<p>서버에서는 Multer를 사용하여 클라이언트로부터 전송된 파일을 받아서 처리합니다.</p>
<pre><code class="language-jsx">exports.updateProfileImage = [
  upload.single(&quot;profileImage&quot;),
  async (req, res, next) =&gt; {
    const { id } = req.user;

    try {
      if (!req.file) {
        return res.status(400).json({
          message: &quot;프로필 이미지 파일이 전송되지 않았습니다.&quot;,
        });
      }</code></pre>
<p><code>upload.single(&quot;profileImage&quot;)</code>는 클라이언트로부터 전송받은 &#39;profileImage&#39;라는 이름의 파일을 받아서 처리한다.</p>
<h3 id="2-3-데이터베이스의-사용자-프로필-이미지-업데이트">2-3. 데이터베이스의 사용자 프로필 이미지 업데이트</h3>
<p>다음으로 이 파일을 사용하여 데이터베이스의 사용자 프로필 이미지를 업데이트 한다.</p>
<pre><code class="language-jsx">      // 사용자 정보 업데이트
      await User.update({ profileImage: req.file.path }, { where: { id } });

      // 변경된 사용자 정보 검색
      const user = await User.findOne({ where: { id } });

      // 세션에 사용자 정보 업데이트
      req.session.user.profileImage = user.profileImage;

      // 업데이트 성공 메시지 반환
      return res.status(200).json({
        message: &quot;프로필 이미지가 성공적으로 업데이트되었습니다.&quot;,
        profileImage: user.profileImage,
      });
    } catch (error) {
      console.error(error);
      return next(error);
    }
  },
];</code></pre>
<p>이 함수는 클라이언트가 전송한 이미지 파일을 디스크에 저장한 후, 파일의 저장 경로를 데이터베이스의 사용자 프로필 이미지로 설정한다. 그 후에는 이 변경된 사용자 정보를 세션에 업데이트하고, 클라이언트에 업데이트가 성공적으로 이루어졌음을 알려준다.</p>
<p>이 방식을 사용하면 클라이언트가 서버에 이미지 파일을 전송하고, 서버가 이 파일을 받아서 사용자의 프로필 이미지를 업데이트하는 과정을 수행할 수 있다. 이 과정은 클라이언트가 프로필 이미지를 선택하고 업로드 버튼을 누르면 시작되며, 서버가 클라이언트로부터 이미지 파일을 받아서 저장하고 데이터베이스를 업데이트하고, 업데이트가 성공적으로 이루어졌음을 클라이언트에 알리는 방식으로 진행된다.</p>
<p>그런 다음, 서버는 클라이언트에게 이 이미지의 URL을 반환하고, 클라이언트는 이 URL을 사용하여 프로필 이미지를 표시한다. 이렇게 하면 사용자는 자신의 프로필 이미지를 업로드하고 업데이트할 수 있게 된다.</p>
<h3 id="2-4-프로필-이미지-미리보기">2-4 프로필 이미지 미리보기</h3>
<p>마지막으로, 프론트엔드에서는 이 이미지를 미리보기하여 사용자가 업로드할 이미지를 미리 확인할 수 있도록 하는 기능을 추가하였다. 이를 위해 FileReader API를 사용하여 사용자가 선택한 이미지 파일을 읽어 들여 미리보기 이미지를 생성한다.</p>
<pre><code class="language-jsx">  // 이미지를 선택하면 해당 이미지를 미리보기로 설정
  const handleImageChange = event =&gt; {
    event.stopPropagation();

    let reader = new FileReader();
    let file = event.target.files[0];

    reader.onloadend = () =&gt; {
      setPreview(reader.result);
      setIsImageUploaded(true);
    };

    if (file) {
      reader.readAsDataURL(file);
    }
  };</code></pre>
<p><code>handleImageChange</code> 함수는 사용자가 이미지를 선택하면 호출되며, 이 함수는 FileReader 객체를 생성하고 이 객체를 사용하여 이미지 파일을 읽는다. 파일 읽기가 완료되면 <code>reader.onloadend</code> 이벤트 핸들러가 호출되어 프리뷰 이미지를 설정하고, 이미지가 업로드되었음을 나타내는 상태를 업데이트한다.</p>
<h3 id="3-테스트">3. 테스트</h3>
<p>이제 프로필이미지 업로드 기능 구현이 완료되었다면 테스트 해보도록 하자.</p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/c7636054-6034-4aac-b1ce-387b2a9692b8/image.png" alt=""></p>
<p>먼저 회원가입을 진행한다.</p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/be9971a6-9214-4715-a730-3ff409963330/image.png" alt=""></p>
<p>성공적으로 회원가입을 완료하면 프로필이미지 업로드 모달창이 나타난다. 사용자의 편의성을 위해서 ‘나중에 할게요’ 버튼을 클릭하거나 모달의 바깥 영역을 클릭하면 프로필 이미지를 업로드 하지 않더라도 기본 프로필 이미지로 해당 애플리케이션을 이용할 수 있도록 하였다.</p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/da385c2f-96f7-4572-af97-37be4f226528/image.png" alt=""></p>
<p>‘사진 업로드하기 버튼’을 클릭하거나 프로필 이미지 미리보기 영역을 클릭하여 이미지를 업로드 할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/20856845-87cd-47e3-ac18-6441e2798208/image.png" alt=""></p>
<p>‘사진 변경하기’ 버튼을 클릭하거나 프로필 이미지 미리보기 영역을 클릭하여 다른 이미지로 변경할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/487afcc3-3524-4710-9386-74f76328e320/image.png" alt=""></p>
<p>‘완료’ 버튼을 클릭하면  회원가입 완료 모달을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/hoon-devlog/post/52fec33b-9da5-4b28-87e1-2e975c5e6466/image.png" alt=""></p>
<p>프로필 이미지 업로드가 성공적으로 완료된 것을 알 수 있다. </p>
<h3 id="4-보완할점">4. 보완할점</h3>
<p>프로필이미지를 업로드할 때, 이미지 형식에 맞지 않는 파일을 업로드할때 적절한 에러 처리를 프론트 쪽에서 진행하지 못했다. 따라서 사용자는 어떤 부분에서 문제가 있는지 인식하지 못하는 현상이 발생하였다. 보다 세밀한 에러처리를 통해서 사용자의 편의성을 높힐 필요성을 느꼈다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ 로그인 시스템 구현 A-Z[사용자인증3]]]></title>
            <link>https://velog.io/@hoon-devlog/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84-A-Z%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9D%B8%EC%A6%9D3</link>
            <guid>https://velog.io/@hoon-devlog/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84-A-Z%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9D%B8%EC%A6%9D3</guid>
            <pubDate>Sun, 16 Jul 2023 14:43:15 GMT</pubDate>
            <description><![CDATA[<p>이제 사용자가 회원가입을 완료하고 자신의 계정으로 로그인할 수 있도록 로그인 시스템을 구현해보자. 로그인 시스템은 회원가입 시스템과 밀접하게 연결되어 있다. 왜냐하면 사용자가 입력한 로그인 정보를 데이터베이스의 회원 정보와 비교하여, 그 정보가 유효한지 확인해야하기 때문이다. 따라서, 이번 단계에서는 사용자가 로그인 폼에 입력한 이메일과 비밀번호를 검증하고, 해당 정보가 올바르면 세션에 사용자 정보를 저장하는 과정을 살펴보도록 하자.</p>
<h2 id="1-클라이언트에서-로그인-정보-제출">1. <strong>클라이언트에서 로그인 정보 제출</strong></h2>
<p>우선 사용자는 로그인 폼을 통해 이메일과 비밀번호를 입력하고 제출한다. 이 과정에서 제출하는 이메일과 비밀번호가 사용자의 로그인 정보가 된다. 다음은 로그인시 사용자의 정보를 입력하고 제출하는 로직을 담당하는 LoginModal.jsx 파일이다.</p>
<pre><code class="language-jsx">// src/components/login/loginModal/LoginModal.jsx

import React, { useState } from &#39;react&#39;;
import { useSelector, useDispatch } from &#39;react-redux&#39;;

import {
  hideLoginModal,
  loginModalToSignupModal,
} from &#39;../../../store/modalSlice&#39;;
import { login } from &#39;../../../store/authSlice.js&#39;;
import BaseModal from &#39;../../common/BaseModal.jsx&#39;;
import EmailLogin from &#39;../emailLogin/EmailLogin.jsx&#39;;

import {
  LoginModalContent,
  LoginModalText,
  SignupBtn,
  OrText,
} from &#39;./LoginModalStyle&#39;;
import KakaoLoginBtn from &#39;../KakaoLoginBtn.jsx&#39;;

import {
  validateLoginEmail,
  validateLoginPassword,
} from &#39;../../../utils/validation&#39;;
import { loginUser } from &#39;../../../api/auth.js&#39;;

const LoginModal = () =&gt; {
  // 로그인 모달의 가시성 상태
  const isLoginModalVisible = useSelector(
    state =&gt; state.modal.isLoginModalVisible
  );
  const dispatch = useDispatch();

  // 로그인 모달을 숨기기
  const handleHideLoginModal = () =&gt; {
    dispatch(hideLoginModal());
  };

  // 로그인 모달에서 회원가입 모달로 전환
  const handleLoginModalToSignupModal = () =&gt; {
    dispatch(loginModalToSignupModal());
  };

  // 이메일과 비밀번호 입력값을 위한 상태
  const [email, setEmail] = useState(&#39;&#39;);
  const [password, setPassword] = useState(&#39;&#39;);

  // 검증 에러들을 위한 상태
  const [emailErrors, setEmailErrors] = useState([]);
  const [passwordErrors, setPasswordErrors] = useState([]);
  const [serverLoginErrors, setServerLoginErrors] = useState(&#39;&#39;);

  // 이메일 입력값 변경을 처리
  const handleChangeEmail = event =&gt; {
    const emailValidationErrors = validateLoginEmail(event.target.value);
    setEmail(event.target.value);
    setEmailErrors(
      emailValidationErrors.length &gt; 0 ? emailValidationErrors : []
    );
    setServerLoginErrors(&#39;&#39;);
  };

  // 비밀번호 입력값 변경을 처리
  const handleChangePassword = event =&gt; {
    const passwordValidationErrors = validateLoginPassword(event.target.value);
    setPassword(event.target.value);
    setPasswordErrors(
      passwordValidationErrors.length &gt; 0 ? passwordValidationErrors : []
    );
    setServerLoginErrors(&#39;&#39;);
  };

  // 로그인 폼 제출을 처리
  const handleLoginSubmit = async event =&gt; {
    event.preventDefault();

    const emailValidationErrors = validateLoginEmail(email);
    const passwordValidationErrors = validateLoginPassword(password);

    setEmailErrors(
      emailValidationErrors.length &gt; 0 ? emailValidationErrors : []
    );
    setPasswordErrors(
      passwordValidationErrors.length &gt; 0 ? passwordValidationErrors : []
    );

    if (
      emailValidationErrors.length &gt; 0 ||
      passwordValidationErrors.length &gt; 0
    ) {
      return;
    }

    try {
      const response = await loginUser(email, password);
      const user = response.data.user;

      dispatch(login(user)); // 로그인 성공 액션을 디스패치, user 정보를 payload로 전달
      handleHideLoginModal(); // 로그인이 성공적으로 완료되면 모달을 숨김
      console.log(&#39;user&#39;, user)
    } catch (error) {
      const serverErrorMessages = error.response.data.message;
      if (error.response &amp;&amp; error.response.status === 401) {
        // 이메일 또는 비밀번호가 일치하지 않음
        setServerLoginErrors(serverErrorMessages);
        console.log(error.response.data);
        console.log(serverErrorMessages);
      }

      if (error.response.status === 429) {
        // 로그인 요청이 너무 많이 감지됨
        setServerLoginErrors(serverErrorMessages);
        console.log(error.response.data.message);
      }
      console.error(error);
    }
  };

  return (
    &lt;BaseModal
      isVisible={isLoginModalVisible}
      onClose={handleHideLoginModal}
      title=&#39;로그인 또는 회원가입&#39;
    &gt;
      &lt;LoginModalContent onSubmit={handleLoginSubmit}&gt;
        &lt;LoginModalText&gt;☕️ 카페골목에 오신 것을 환영합니다.&lt;/LoginModalText&gt;
        &lt;EmailLogin
          email={email}
          password={password}
          handleChangeEmail={handleChangeEmail}
          handleChangePassword={handleChangePassword}
          handleLoginSubmit={handleLoginSubmit}
          emailErrors={emailErrors}
          passwordErrors={passwordErrors}
          serverLoginErrors={serverLoginErrors}
        /&gt;
        &lt;OrText&gt;또는&lt;/OrText&gt;
        &lt;KakaoLoginBtn /&gt;
        &lt;SignupBtn type=&#39;button&#39; onClick={handleLoginModalToSignupModal}&gt;
          카페골목 회원가입 하기
        &lt;/SignupBtn&gt;
      &lt;/LoginModalContent&gt;
    &lt;/BaseModal&gt;
  );
};

export default LoginModal;</code></pre>
<p>위 코드는 React와 Redux를 이용한 클라이언트 사이드에서의 로그인 정보 제출 과정을 보여준다. 이 코드에서 클라이언트가 로그인 정보를 제출하는 과정은 다음과 같다.</p>
<h3 id="1-1-사용자는-이메일과-비밀번호를-입력">1-1. 사용자는 이메일과 비밀번호를 입력</h3>
<pre><code class="language-jsx">const [email, setEmail] = useState(&#39;&#39;);
const [password, setPassword] = useState(&#39;&#39;);</code></pre>
<p>각 입력값은 React의 <code>useState</code> 훅을 사용하여 상태로 관리된다.</p>
<h3 id="1-2-이메일과-비밀번호-유효성-검사">1-2. 이메일과 비밀번호 유효성 검사</h3>
<pre><code class="language-jsx">const handleChangeEmail = event =&gt; {
  const emailValidationErrors = validateLoginEmail(event.target.value);
  setEmail(event.target.value);
  setEmailErrors(
    emailValidationErrors.length &gt; 0 ? emailValidationErrors : []
  );
  setServerLoginErrors(&#39;&#39;);
};

const handleChangePassword = event =&gt; {
  const passwordValidationErrors = validateLoginPassword(event.target.value);
  setPassword(event.target.value);
  setPasswordErrors(
    passwordValidationErrors.length &gt; 0 ? passwordValidationErrors : []
  );
  setServerLoginErrors(&#39;&#39;);
};</code></pre>
<p>사용자가 이메일 또는 비밀번호를 입력할 때마다 <code>handleChangeEmail</code> 또는 <code>handleChangePassword</code> 함수가 실행되어 이메일 또는 비밀번호 상태가 갱신된다. 이때, 입력값의 유효성을 검사하는 <code>validateLoginEmail</code> 또는 <code>validateLoginPassword</code> 함수가 호출되어 입력값의 유효성 검사를 수행한다.</p>
<h3 id="1-3-로그인-폼-제출하기">1-3. 로그인 폼 제출하기</h3>
<pre><code class="language-jsx">const handleLoginSubmit = async event =&gt; {
  event.preventDefault();

  const emailValidationErrors = validateLoginEmail(email);
  const passwordValidationErrors = validateLoginPassword(password);

  setEmailErrors(
    emailValidationErrors.length &gt; 0 ? emailValidationErrors : []
  );
  setPasswordErrors(
    passwordValidationErrors.length &gt; 0 ? passwordValidationErrors : []
  );

  if (
    emailValidationErrors.length &gt; 0 ||
    passwordValidationErrors.length &gt; 0
  ) {
    return;
  }

  try {
    const response = await loginUser(email, password);
    const user = response.data.user;

    dispatch(login(user)); // 로그인 성공 액션을 디스패치, user 정보를 payload로 전달
    handleHideLoginModal(); // 로그인이 성공적으로 완료되면 모달을 숨김
    console.log(&#39;user&#39;, user)
  } catch (error) {
    // 에러 처리...
  }
};
</code></pre>
<p>사용자가 로그인 폼을 제출하면 <code>handleLoginSubmit</code> 함수가 실행된다. 이 함수는 제출된 이메일과 비밀번호의 유효성을 최종적으로 확인하고, 유효성 검사를 통과한 경우 <code>loginUser</code> 함수를 호출하여 서버에 로그인 요청을 보낸다.</p>
<h3 id="1-4-서버에-로그인-요청-및-응답">1-4. 서버에 로그인 요청 및 응답</h3>
<pre><code class="language-jsx">const response = await loginUser(email, password);
const user = response.data.user;

dispatch(login(user)); // 로그인 성공 액션을 디스패치, user 정보를 payload로 전달
handleHideLoginModal(); // 로그인이 성공적으로 완료되면 모달을 숨김
console.log(&#39;user&#39;, user)</code></pre>
<p><code>loginUser</code> 함수는 서버에 로그인 요청을 보내고 응답을 받아와서 처리한다. 로그인이 성공적으로 이루어진 경우, 사용자 정보를 Redux 스토어에 저장하고, 로그인 모달을 숨긴다.</p>
<p>이렇게 클라이언트 사이드에서 로그인 정보가 제출되면, 해당 정보는 서버로 전달되어 사용자 인증이 이루어집니다.</p>
<h2 id="2-로그인-정보-검증">2. <strong>로그인 정보 검증</strong></h2>
<p>클라이언트 측에서 성공적으로 로그인 요청을 서버에 보냈다면, 서버는 로그인 정보 검증 -&gt; 사용자 정보 확인 -&gt; 세션에 사용자 정보 저장 -&gt; 응답 전송의 단계를 거쳐 로그인이 진행된다. 다음은 이러한 로직을 담당하는 authContoller.js 파일의 로그인 login 함수에 대해서 알아보자.</p>
<p>우선, 사용자가 로그인 폼에서 제출한 이메일과 비밀번호가 올바른 형식인지 검증해야 한다. 이메일은 알맞은 형식에 맞춰져 있는지, 비밀번호는 사용자가 회원가입 때 설정한 비밀번호와 일치하는지 확인한다. 이런 검증은 서버 측에서 이루어져야하며, 이는 보안을 위해 필수적인 과정이다.</p>
<pre><code class="language-jsx">// controllers/authController.js

const bcrypt = require(&quot;bcrypt&quot;);
const passport = require(&quot;passport&quot;);
const { User } = require(&quot;../models&quot;);
const upload = require(&quot;../multerConfig&quot;);

const {
  validateSignupEmail,
  validateSignupPassword,
  validateSignupNickname,
  validateSignupPasswordConfirm,
  validateLoginEmail,
  validateLoginPassword,
} = require(&quot;../validations/validation.js&quot;);

// 로그인 처리
exports.login = async (req, res, next) =&gt; {
  // 로그인 입력 유효성 검사
  const { email, password } = req.body;
  const emailErrors = validateLoginEmail(email);
  const passwordErrors = validateLoginPassword(password);

  if (emailErrors.length &gt; 0 || passwordErrors.length &gt; 0) {
    return res.status(400).json({
      errors: {
        email: emailErrors,
        password: passwordErrors,
      },
    });
  }

  try {
    passport.authenticate(&quot;local&quot;, (err, user, info) =&gt; {
      if (err) {
        console.error(err);
        return next(err);
      }
      if (!user) {
        return res.status(401).json({
          message: &quot;로그인 정보가 올바르지 않습니다. 다시 시도해 주세요.&quot;,
        });
      }
      return req.login(user, async (loginErr) =&gt; {
        if (loginErr) {
          console.error(loginErr);
          return next(loginErr);
        }

        const updatedUser = await User.findOne({ where: { id: user.id } });

        // 세션에 사용자 정보 저장
        req.session.user = {
          id: updatedUser.id,
          email: updatedUser.email,
          nickname: updatedUser.nickname,
          profileImage: updatedUser.profileImage,
        };

        return res.status(200).json({
          message: &quot;로그인이 성공적으로 완료되었습니다.&quot;,
          user: req.session.user,
        });
      });
    })(req, res, next);
  } catch (error) {
    console.error(error);
    return res
      .status(500)
      .json({ message: &quot;서버 내부 오류가 발생했습니다. 다시 시도해 주세요.&quot; });
  }
};</code></pre>
<p>먼저, 사용자가 입력한 이메일과 비밀번호가 올바른 형식인지 서버에서 검증한다. 이메일과 비밀번호는 각각 <code>validateLoginEmail</code>과 <code>validateLoginPassword</code> 함수를 통해 유효성 검사가 이루어진다.</p>
<pre><code class="language-jsx">// 로그인 입력 유효성 검사
const { email, password } = req.body;
const emailErrors = validateLoginEmail(email);
const passwordErrors = validateLoginPassword(password);

if (emailErrors.length &gt; 0 || passwordErrors.length &gt; 0) {
  return res.status(400).json({
    errors: {
      email: emailErrors,
      password: passwordErrors,
    },
  });
}</code></pre>
<p>만약 이메일 또는 비밀번호가 유효하지 않은 형식이라면, 서버는 400 상태 코드와 함께 유효성 검사 오류를 클라이언트에게 응답한다.</p>
<h2 id="3-사용자-정보-확인">3. <strong>사용자 정보 확인</strong></h2>
<p>검증이 완료되면, 데이터베이스에 저장된 사용자 정보와 제출된 로그인 정보를 비교한다. 즉, 이메일과 비밀번호가 모두 일치하는 사용자가 데이터베이스에 있는지 확인하는 로직을 거치는 것이다. 이 과정에서 비밀번호는 해시화되어 저장되어 있으므로, 사용자가 입력한 비밀번호를 같은 방식으로 해시화한 후에 비교해야 한다.</p>
<pre><code class="language-jsx">passport.authenticate(&quot;local&quot;, (err, user, info) =&gt; {
  if (err) {
    console.error(err);
    return next(err);
  }
  if (!user) {
    return res.status(401).json({
      message: &quot;로그인 정보가 올바르지 않습니다. 다시 시도해 주세요.&quot;,
    });
  }</code></pre>
<p><code>passport.authenticate</code> 함수를 사용해 로컬 전략에 따라 사용자 인증을 시도한다. 이 함수는 인증이 성공적으로 이루어졌는지, 실패했는지, 그리고 실패한 경우 왜 실패했는지를 파악하고 이에 대한 적절한 응답을 생성한다.</p>
<h2 id="4-세션에-사용자-정보-저장">4. <strong>세션에 사용자 정보 저장</strong></h2>
<p>먼저, 로그인 시스템을 구현하기 위해서는 사용자 세션을 관리해야 한다. 세션 정보는 사용자가 로그인 상태를 유지할 수 있게 해주는 중요한 부분이기 때문이다.</p>
<p>아래는 프로젝트에서 세션을 설정하는 코드이다.</p>
<pre><code class="language-jsx">// app.js

const session = require(&quot;express-session&quot;);

const MySQLStore = require(&quot;express-mysql-session&quot;)(session);
const sessionStore = new MySQLStore(options);

app.use(
  session({
    resave: false, // 세션을 언제나 저장할지 정하는 옵션
    saveUninitialized: false, // 세션이 저장되기 전에 uninitialized 상태로 미리 만들어서 저장하는지 정하는 옵션
    key: &quot;session_cookie_name&quot;,
    secret: process.env.COOKIE_SECRET,
    store: sessionStore, // 세션 스토어 지정
    cookie: {
      httpOnly: true, // 클라이언트에서 쿠키를 JavaScript로 제어할 수 없도록 설정하는 옵션
      secure: false, // true로 설정하면 https를 통해서만 쿠키가 전송
      maxAge: 30 * 24 * 60 * 60, // 쿠키 유효기간 설정 (30일)
    },
  })
);</code></pre>
<p><code>session</code> 함수는 세션에 대한 설정을 객체 형태로 받으며, <code>resave</code>, <code>saveUninitialized</code>, <code>key</code>, <code>secret</code>, <code>store</code>, <code>cookie</code> 등 다양한 옵션을 설정할 수 있다.</p>
<p><code>store</code> 옵션은 세션 데이터를 저장하는 곳을 설정한다. 해당 프로젝트에서는 MySQL 데이터베이스에 세션 정보를 저장하도록 <code>express-mysql-session</code> 라이브러리를 사용했다.</p>
<p><code>cookie</code> 옵션은 세션 쿠키에 대한 설정을 하며, <code>httpOnly</code>, <code>secure</code>, <code>maxAge</code> 등의 설정을 통해 쿠키의 보안과 유효기간을 관리할 수 있다.</p>
<p>이렇게 세션 설정을 통해 로그인한 사용자의 정보를 서버에서 안전하게 관리할 수 있다. 다음으로, 이 세션을 활용하여 사용자 로그인 시스템을 어떻게 구현하는지 알아보자.</p>
<p>사용자 정보가 일치하는 경우, 서버는 사용자를 로그인 상태로 설정하고, 사용자의 세션에 사용자 정보를 저장한다. 이렇게 하면 사용자가 브라우저를 닫거나, 다른 페이지로 이동해도 로그인 상태가 유지된다. 이후 사용자가 다른 요청을 보낼 때마다, 서버는 세션 정보를 확인하여 사용자의 로그인 상태를 판단한다.</p>
<pre><code class="language-jsx">return req.login(user, async (loginErr) =&gt; {
  if (loginErr) {
    console.error(loginErr);
    return next(loginErr);
  }

  const updatedUser = await User.findOne({ where: { id: user.id } });

  // 세션에 사용자 정보 저장
  req.session.user = {
    id: updatedUser.id,
    email: updatedUser.email,
    nickname: updatedUser.nickname,
    profileImage: updatedUser.profileImage,
  };

  return res.status(200).json({
    message: &quot;로그인이 성공적으로 완료되었습니다.&quot;,
    user: req.session.user,
  });
});</code></pre>
<p>인증에 성공한 경우, 사용자의 세션에 사용자 정보를 저장하고, 로그인에 성공했다는 메시지와 함께 사용자 정보를 클라이언트에게 응답한다.</p>
<h2 id="5-응답-전송">5. <strong>응답 전송</strong></h2>
<p>마지막으로, 로그인이 성공적으로 완료되었음을 클라이언트에게 알려준다. 이 때, 응답에는 세션에 저장된 사용자 정보를 포함시킬 수 있다. 만약 로그인이 실패했을 경우에는, 실패의 이유(예: 잘못된 이메일이나 비밀번호)를 클라이언트에게 알려준다.</p>
<pre><code class="language-jsx">catch (error) {
  console.error(error);
  return res
    .status(500)
    .json({ message: &quot;서버 내부 오류가 발생했습니다. 다시 시도해 주세요.&quot; });
}</code></pre>
<p>만약 이 과정에서 서버 내부 에러가 발생하면, 서버는 500 상태 코드와 함께 클라이언트에게 내부 에러가 발생했다는 메시지를 응답한다.</p>
<p>이렇게 간단한 웹 애플리케이션의 로그인 시스템을 구현할 수 있다. 이제 사용자는 자신의 계정으로 로그인하여 애플리케이션의 기능을 사용할 수 있게 되었다. </p>
<h3 id="6-테스트">6. 테스트</h3>
<p>이제 로그인 시스템을 테스트해보자. </p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/935d1cd4-8c60-4ca1-b3ca-d8cfaa4ad275/image.png" alt=""></p>
<p>비로그인 상태에서는 세션 테이블이 비어있는 것을 확인할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/3e6a2c34-54dc-4412-b2f7-a9314f31d044/image.png" alt=""></p>
<p>기존에 가입했던 ‘<a href="mailto:nojungbock@naver.com">nojungbock@naver.com</a>’로 로그인해보자</p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/1474e438-b2e7-41b1-9647-deb27b089a93/image.png" alt=""></p>
<p>세션 테이블에 세션id, 만료기한, 사용자 data가 저장된 것을 확인할 수 있다. data에는 다음과 같은 정보가 저장되어있다.</p>
<pre><code class="language-jsx">{&quot;cookie&quot;:{&quot;originalMaxAge&quot;:2592000000,&quot;expires&quot;:&quot;2023-08-15T14:34:25.943Z&quot;,&quot;secure&quot;:false,&quot;httpOnly&quot;:true,&quot;path&quot;:&quot;/&quot;},&quot;passport&quot;:{&quot;user&quot;:90},&quot;user&quot;:{&quot;[id&quot;:90,&quot;email&quot;:&quot;nojungbock@naver.com](mailto:id%22:90,%22email%22:%22nojungbock@naver.com)&quot;,&quot;nickname&quot;:&quot;노중복닉네임&quot;,&quot;profileImage&quot;:null}}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[회원가입 시스템 구현 A-Z [사용자인증2]]]></title>
            <link>https://velog.io/@hoon-devlog/%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84-A-Z-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%852</link>
            <guid>https://velog.io/@hoon-devlog/%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84-A-Z-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%852</guid>
            <pubDate>Sun, 16 Jul 2023 13:22:39 GMT</pubDate>
            <description><![CDATA[<h1 id="메모">메모</h1>
<p>지난 포스팅에서는 로그인과 회원가입 시스템이 왜 필요한지, 그리고 이메일 기반 인증이 어떤 것인지 알아보았다.  이번에는 실제로 이 회원가입 시스템을 어떻게 구현하는지 알아보자.</p>
<h2 id="1-클라이언트가-회원가입-정보를-제출">1. <strong>클라이언트가 회원가입 정보를 제출</strong></h2>
<p>사용자가 웹페이지에서 이메일 주소와 비밀번호, 그리고 필요한 다른 정보들을 입력하고 &#39;회원가입&#39; 버튼을 누르면 이 정보들이 서버로 전송되는 단계이다. </p>
<p>먼저, api 폴더의 auth.js에 회원가입에 대한 api 함수를 정의하자.</p>
<pre><code class="language-jsx">// src/api/auth.js

import axiosInstance from &#39;../axios&#39;;

export const signupUser = async (
  nickname,
  email,
  password,
  passwordConfirm
) =&gt; {
  try {
    const response = await axiosInstance.post(&#39;/auth/signup&#39;, {
      nickname,
      email,
      password,
      passwordConfirm,
    });
    return response;
  } catch (error) {
    console.error(error);
    throw error;
  }
};</code></pre>
<p>여기서 <code>axiosInstance</code>는 <code>axios</code> 라이브러리를 활용해 생성된 객체를 의미하며, 서버로 HTTP 요청을 보내기 위해 사용된다. 이 인스턴스는 특정 설정을 가질 수 있으며, 인스턴스를 사용하여 요청을 보낼 때마다 적용된다. <code>axiosInstance</code>를 사용하여 실제 HTTP 요청을 서버로 보내면 이메일, 비밀번호, 비밀번호 확인, 닉네임으로 회원가입을 시도하는 POST 요청을 보낸다. </p>
<p>회원가입에 대한 api 함수 정의가 끝났다면 사용자의 회원가입 정보를 입력 받고 서버에 전송하는 역할을 하는 <code>SignupModal.jsx</code> 컴포넌트에 대해서 알아보자.</p>
<pre><code class="language-jsx">// src/components/SignupModal/SignupModal.jsx

import React, { useState } from &#39;react&#39;;
import { useSelector, useDispatch } from &#39;react-redux&#39;;

import {
  hideSignupModal,
  signupModalToLoginModal,
  signupModalToAddProfileImgModal,
} from &#39;../../../store/modalSlice.js&#39;;
import { login } from &#39;../../../store/authSlice.js&#39;;
import BaseModal from &#39;../../common/BaseModal.jsx&#39;;
import WarningMsg from &#39;../../warningMsg/WarningMsg.jsx&#39;;
import {
  validateSignupPasswordConfirm,
  validateSignupEmail,
  validateSignupNickname,
  validateSignupPassword,
} from &#39;../../../utils/validation.js&#39;;
import { signupUser } from &#39;../../../api/auth.js&#39;;

import {
  SignupModalContent,
  EmailInput,
  PasswordInput,
  SignupBtn,
  ConfirmPasswordInput,
  EmailLabel,
  PasswordLabel,
  ConfirmPasswordLabel,
  NicknameLabel,
  NicknameInput,
} from &#39;./SignupModalStyle&#39;;

const SignupModal = () =&gt; {
  const isSignupModalVisible = useSelector(
    state =&gt; state.modal.isSignupModalVisible
  );

  const dispatch = useDispatch();

  // 액션 디스패치하는 핸들러 함수 정의
  const handleHideSignupModal = () =&gt; {
    dispatch(hideSignupModal());
  };

  const handleSignupModalToLoginModal = () =&gt; {
    dispatch(signupModalToLoginModal());
  };

  const handleSignupModalToAddProfileImgModal = () =&gt; {
    console.log(&#39;모달전환&#39;);
    dispatch(signupModalToAddProfileImgModal());
  };

  // 각 입력창의 상태와 에러 메시지를 관리할 상태 정의
  const [email, setEmail] = useState(&#39;&#39;);
  const [password, setPassword] = useState(&#39;&#39;);
  const [passwordConfirm, setPasswordConfirm] = useState(&#39;&#39;);
  const [nickname, setNickname] = useState(&#39;&#39;);

  const [emailErrors, setEmailErrors] = useState([]);
  const [passwordErrors, setPasswordErrors] = useState(&#39;&#39;);
  const [passwordConfirmErrors, setPasswordConfirmErrors] = useState(&#39;&#39;);
  const [nicknameErrors, setNicknameErrors] = useState([]);
  const [serverEmailError, setServerEmailError] = useState(&#39;&#39;);
  const [serverNicknameError, setServerNicknameError] = useState(&#39;&#39;);

  // 이메일 핸들러
  const handleChangeEmail = event =&gt; {
    const emailValidationErrors = validateSignupEmail(event.target.value);
    setEmail(event.target.value);
    setEmailErrors(
      emailValidationErrors.length &gt; 0 ? emailValidationErrors : []
    );
    setServerEmailError(&#39;&#39;);
  };

  // 비밀번호 핸들러
  const handleChangePassword = event =&gt; {
    const passwordValidationErrors = validateSignupPassword(event.target.value);
    setPassword(event.target.value);
    setPasswordErrors(
      passwordValidationErrors.length &gt; 0 ? passwordValidationErrors : []
    );
  };

  // 비밀번호 확인 핸들러
  const handleChangePasswordConfirm = event =&gt; {
    const passwordConfirmValidationErrors = validateSignupPasswordConfirm(
      password,
      event.target.value
    );
    setPasswordConfirm(event.target.value);
    setPasswordConfirmErrors(
      passwordConfirmValidationErrors.length &gt; 0
        ? passwordConfirmValidationErrors
        : []
    );
  };

  // 닉네임 핸들러
  const handleChangeNickname = event =&gt; {
    const nicknameValidationErrors = validateSignupNickname(event.target.value);
    setNickname(event.target.value);
    setNicknameErrors(
      nicknameValidationErrors.length &gt; 0 ? nicknameValidationErrors : []
    );
    setServerNicknameError(&#39;&#39;);
  };

  // 회원가입 폼 제출 이벤트 핸들러
  const handleSignupSubmit = async event =&gt; {
    event.preventDefault();

    // 모든 입력 값에 대해 유효성 검사 진행 및 검사 결과 상태에 반영
    const emailValidationErrors = validateSignupEmail(email);
    const passwordValidationErrors = validateSignupPassword(password);
    const passwordConfirmValidationErrors = validateSignupPasswordConfirm(
      password,
      passwordConfirm
    );
    const nicknameValidationErrors = validateSignupNickname(nickname);

    setEmailErrors(
      emailValidationErrors.length &gt; 0 ? emailValidationErrors : []
    );
    setPasswordErrors(
      passwordValidationErrors.length &gt; 0 ? passwordValidationErrors : []
    );
    setPasswordConfirmErrors(passwordConfirmValidationErrors);
    setNicknameErrors(
      nicknameValidationErrors.length &gt; 0 ? nicknameValidationErrors : []
    );

    setPasswordConfirmErrors(
      passwordConfirmValidationErrors.length &gt; 0
        ? passwordConfirmValidationErrors
        : &#39;&#39;
    );

    // 모든 입력 값이 유효하지 않으면 함수를 종료
    if (
      emailValidationErrors.length &gt; 0 ||
      passwordValidationErrors.length &gt; 0 ||
      passwordConfirmValidationErrors.length &gt; 0 ||
      nicknameValidationErrors.length &gt; 0
    ) {
      return;
    }

    // 모든 입력 값이 유효하면 서버에 회원가입 요청
    try {
      const response = await signupUser(
        nickname,
        email,
        password,
        passwordConfirm
      );
      const user = response.data.user;
      console.log(&#39;회원가입 성공&#39;, user);

      // 회원가입에 성공하면 바로 로그인 상태로 전환
      dispatch(login(user));

      // 회원가입에 성공하면 프로필 이미지 설정 모달로 전환
      handleSignupModalToAddProfileImgModal();
    } catch (errors) {
      // 서버에서 에러 메시지를 받으면 해당 메시지를 상태에 반영
      if (errors.response &amp;&amp; errors.response.data) {
        console.log(errors.response.data);

        const serverErrorMessages = errors.response.data.errors;

        if (errors.response.status === 409) {
          serverErrorMessages.forEach(errorMessage =&gt; {
            if (errorMessage === &#39;이미 사용 중인 이메일입니다.&#39;) {
              setServerEmailError(errorMessage);
            }
            if (errorMessage === &#39;이미 사용 중인 닉네임입니다.&#39;) {
              setServerNicknameError(errorMessage);
            }
          });
          return;
        }
      }
    }
  };

  return (
    &lt;BaseModal
      isVisible={isSignupModalVisible}
      onClose={handleHideSignupModal}
      onBack={handleSignupModalToLoginModal}
      title=&#39;회원가입 완료하기&#39;
    &gt;
      &lt;SignupModalContent&gt;
        &lt;EmailLabel htmlFor=&#39;user-email&#39;&gt;이메일&lt;/EmailLabel&gt;
        &lt;EmailInput
          type=&#39;text&#39;
          id=&#39;user-email&#39;
          name=&#39;user-email&#39;
          placeholder=&#39;이메일을 입력해주세요.&#39;
          value={email}
          onChange={handleChangeEmail}
          errors={emailErrors.length &gt; 0 ? emailErrors : serverEmailError}
        /&gt;
        &lt;WarningMsg
          show={emailErrors.length &gt; 0}
          messages={emailErrors}
        &gt;&lt;/WarningMsg&gt;
        {serverEmailError &amp;&amp; (
          &lt;WarningMsg show={true} messages={[serverEmailError]} /&gt;
        )}
        &lt;PasswordLabel htmlFor=&#39;user-pw&#39;&gt;비밀번호&lt;/PasswordLabel&gt;
        &lt;PasswordInput
          type=&#39;password&#39;
          id=&#39;user-pw&#39;
          name=&#39;user-pw&#39;
          placeholder=&#39;특수문자 포함 10 ~ 20자 이내로 입력해 주세요.&#39;
          value={password}
          onChange={handleChangePassword}
          errors={passwordErrors}
        /&gt;
        &lt;WarningMsg
          show={passwordErrors.length &gt; 0}
          messages={passwordErrors}
        &gt;&lt;/WarningMsg&gt;
        &lt;ConfirmPasswordLabel htmlFor=&#39;user-pw-check&#39;&gt;
          비밀번호 재확인
        &lt;/ConfirmPasswordLabel&gt;
        &lt;ConfirmPasswordInput
          type=&#39;password&#39;
          id=&#39;user-pw-check&#39;
          name=&#39;user-pw-check&#39;
          placeholder=&#39;비밀번호를 한번 더 입력해주세요.&#39;
          value={passwordConfirm}
          onChange={handleChangePasswordConfirm}
          errors={passwordConfirmErrors}
        /&gt;
        &lt;WarningMsg
          show={passwordConfirmErrors.length &gt; 0}
          messages={passwordConfirmErrors}
        &gt;&lt;/WarningMsg&gt;
        &lt;NicknameLabel htmlFor=&#39;user-nickname&#39;&gt;닉네임&lt;/NicknameLabel&gt;
        &lt;NicknameInput
          type=&#39;text&#39;
          id=&#39;user-nickname&#39;
          name=&#39;user-nickname&#39;
          placeholder=&#39;2 ~ 20자로 입력해 주세요.&#39;
          value={nickname}
          onChange={handleChangeNickname}
          errors={
            nicknameErrors.length &gt; 0 ? nicknameErrors : serverNicknameError
          }
        /&gt;
        &lt;WarningMsg
          show={nicknameErrors.length &gt; 0}
          messages={nicknameErrors}
        &gt;&lt;/WarningMsg&gt;
        {serverNicknameError &amp;&amp; (
          &lt;WarningMsg show={true} messages={[serverNicknameError]} /&gt;
        )}
        &lt;SignupBtn
          type=&#39;submit&#39;
          onClick={handleSignupSubmit}
          disabled={
            emailErrors.length &gt; 0 ||
            passwordErrors.length &gt; 0 ||
            passwordConfirmErrors.length &gt; 0 ||
            nicknameErrors.length &gt; 0
          }
        &gt;
          가입하기
        &lt;/SignupBtn&gt;
      &lt;/SignupModalContent&gt;
    &lt;/BaseModal&gt;
  );
};

export default SignupModal;</code></pre>
<p><code>SignupModal.jsx</code> 는 다음과 같은 단계로 클라이언트에서 서버로 회원가입 요청을 보낸다. </p>
<h3 id="1-1-입력-양식을-채워나가는-과정">1-1. <strong>입력 양식을 채워나가는 과정</strong></h3>
<p>먼저, 사용자는 회원가입 폼에 이메일 주소, 비밀번호, 닉네임 등의 정보를 입력한다. 각각의 입력창은 React의 <code>useState</code>를 이용하여 상태를 관리하며, 각 입력창에는 onChange 이벤트가 설정되어 있다. 사용자가 정보를 입력하면 이 이벤트가 발생하고, 입력값이 해당 상태에 저장된다.</p>
<p>이때, 각 입력 값에는 유효성 검사가 적용된다.. 예를 들어, 이메일은 특정 패턴을 가진 문자열이어야 하고, 비밀번호는 특수 문자를 포함해야 하며, 닉네임은 특정 길이를 충족해야 합니다. 유효하지 않은 값이 입력되면 에러메시지가 출력된다.</p>
<h3 id="1-2-회원가입-버튼을-누르는-순간">1-2. <strong>&#39;회원가입&#39; 버튼을 누르는 순간</strong></h3>
<p>사용자가 모든 정보를 입력한 후 &#39;회원가입&#39; 버튼을 누르면, &#39;handleSignupSubmit&#39; 함수가 호출된다. 이 함수는 이벤트 객체를 받아 form의 기본 제출 이벤트를 막는다(<code>event.preventDefault()</code>).  </p>
<p>그 다음, 사용자가 입력한 각 정보에 대해 다시 한 번 유효성 검사를 진행한다. 이는 사용자가 필수 입력 필드를 누락하거나, 유효하지 않은 값을 입력하였을 때를 대비한 것으로 만약 유효성 검사에서 오류가 발견되면, 함수는 종료되고 사용자에게 오류 메시지가 출력된다. 만약 모든 입력 값이 유효하다면, 사용자의 정보는 서버에 회원가입 요청(<code>signupUser</code>)으로 전송된다.</p>
<h3 id="1-3-서버로-회원가입-요청-보내기">1-3. <strong>서버로 회원가입 요청 보내기</strong></h3>
<p>회원가입 요청이 성공적으로 처리되면, 서버는 회원 정보를 데이터베이스에 저장하고, 해당 회원 정보를 포함한 응답을 클라이언트에 보낸다. 클라이언트는 이 응답을 받아 Redux의 <code>login</code> 액션을 dispatch하여 사용자를 로그인 상태로 전환한다.</p>
<p>이후 프로필 이미지 설정 모달로 전환하는 <code>handleSignupModalToAddProfileImgModal</code> 함수가 호출되며, 이를 통해 사용자는 바로 프로필 이미지를 설정할 수 있다.</p>
<p>만약 서버가 요청을 처리하는 도중 문제가 발생하면 에러 메시지를 포함한 응답을 클라이언트에 보낸다. 이를 통해클라이언트는 이 메시지를 화면에 표시하여 사용자가 문제를 인식하고 수정할 수 있다.</p>
<h2 id="2-서버에서-정보-검증-및-저장">2. <strong>서버에서 정보 검증 및 저장</strong></h2>
<p>이렇게 클라이언트가 회원가입 정보를 성공적으로 제출했다면 서버에서는 클라이언트로부터 받은 정보가 올바른지 확인해야 한다. 이메일 주소 형식이 맞는지, 비밀번호가 충분히 안전한지 등을 검증한다. 정보 검증이 끝나면 이제 사용자 정보를 데이터베이스에 저장하게 된다. 비밀번호는 단순히 텍스트 형태로 저장하지 않고, bcrypt 라이브러리를 통해서 해시 알고리즘을 사용해 암호화된 형태로 저장한다. </p>
<p>먼저 클라이언트의 회원가입 요청에 대한 서버의 정보검증 및 저장에 관한 코드를 살펴보자</p>
<pre><code class="language-jsx">// controllers/authController.js

const bcrypt = require(&quot;bcrypt&quot;);
const passport = require(&quot;passport&quot;);
const { User } = require(&quot;../models&quot;);

const {
  validateSignupEmail,
  validateSignupPassword,
  validateSignupNickname,
  validateSignupPasswordConfirm,
  validateLoginEmail,
  validateLoginPassword,
} = require(&quot;../validations/validation.js&quot;);

// 회원가입 처리
exports.signup = async (req, res, next) =&gt; {
  const { email, nickname, password, passwordConfirm, profileImage } = req.body;

  // 회원가입 입력 유효성 검사
  const emailErrors = validateSignupEmail(email);
  const passwordErrors = validateSignupPassword(password);
  const passwordConfirmErrors = validateSignupPasswordConfirm(
    password,
    passwordConfirm
  );
  const nicknameErrors = validateSignupNickname(nickname);

  if (
    emailErrors.length &gt; 0 ||
    passwordErrors.length &gt; 0 ||
    nicknameErrors.length &gt; 0 ||
    passwordConfirmErrors.length &gt; 0
  ) {
    return res.status(400).json({
      errors: {
        email: emailErrors,
        password: passwordErrors,
        passwordConfirm: passwordConfirmErrors,
        nickname: nicknameErrors,
      },
    });
  }

  try {
    // 이메일과 닉네임이 이미 등록되어 있는지 확인
    const exUserWithEmail = await User.findOne({ where: { email } });
    const exUserWithNickname = await User.findOne({ where: { nickname } });

    // 에러 메시지를 저장할 배열
    const errors = [];

    if (exUserWithEmail) {
      // 이미 등록된 이메일의 경우 에러 메시지 추가
      errors.push(&quot;이미 사용 중인 이메일입니다.&quot;);
    }
    if (exUserWithNickname) {
      // 이미 등록된 닉네임의 경우 에러 메시지 추가
      errors.push(&quot;이미 사용 중인 닉네임입니다.&quot;);
    }

    // 이미 등록된 이메일 또는 닉네임이 있을 경우 에러 메시지 반환
    if (errors.length &gt; 0) {
      return res.status(409).json({ errors });
    }

    // 비밀번호를 해시 처리
    const hash = await bcrypt.hash(password, 12);
    // 새로운 사용자 생성
    const newUser = await User.create({
      email,
      nickname,
      password: hash,
      profileImage,
    });

    // Passport login
    req.login(newUser, (loginErr) =&gt; {
      if (loginErr) {
        console.error(loginErr);
        return next(loginErr);
      }

      // 세션에 사용자 정보 저장
      req.session.user = {
        id: newUser.id,
        email: newUser.email,
        nickname: newUser.nickname,
        profileImage: newUser.profileImage,
      };

      return res.status(201).json({
        message: &quot;회원가입이 성공적으로 완료되었습니다.&quot;,
        user: req.session.user,
      });
    });
  } catch (error) {
    console.error(error);
    return next(error);
  }
};</code></pre>
<p>해당 코드는 길고 복잡하게 보이지만 다음과 같은 단계를 통해서 살펴보면 다소 명확하게 이해할 수 있다. </p>
<h3 id="2-1-먼저-필요한-모듈들을-임포트-한다">2-1. 먼저 필요한 모듈들을 임포트 한다.</h3>
<p>User는 Sequelize 모델로, 데이터베이스와의 상호작용에 사용된다.</p>
<pre><code class="language-jsx">const bcrypt = require(&quot;bcrypt&quot;);
const passport = require(&quot;passport&quot;);
const { User } = require(&quot;../models&quot;);

const {
  validateSignupEmail,
  validateSignupPassword,
  validateSignupNickname,
  validateSignupPasswordConfirm,
  validateLoginEmail,
  validateLoginPassword,
} = require(&quot;../validations/validation.js&quot;);</code></pre>
<h3 id="2-2-클라이언트로부터-전송된-정보를-받아-처리한다">2-2. 클라이언트로부터 전송된 정보를 받아 처리한다.</h3>
<pre><code class="language-jsx">exports.signup = async (req, res, next) =&gt; {
  const { email, nickname, password, passwordConfirm, profileImage } = req.body;</code></pre>
<h3 id="2-3-클라이언트에서-받은-사용자-정보에-대한-유효성-검사를-진행한다">2-3. 클라이언트에서 받은 사용자 정보에 대한 유효성 검사를 진행한다.</h3>
<pre><code class="language-jsx">const emailErrors = validateSignupEmail(email);
const passwordErrors = validateSignupPassword(password);
const passwordConfirmErrors = validateSignupPasswordConfirm(
  password,
  passwordConfirm
);
const nicknameErrors = validateSignupNickname(nickname);</code></pre>
<p>각 검사 함수는 해당 입력 값이 유효한지 확인하고, 유효하지 않은 경우에는 오류 메시지를 반환한다.</p>
<h3 id="2-4-데이터베이스에-이미-존재하는-이메일이나-닉네임이-있는지-검사한다">2-4. 데이터베이스에 이미 존재하는 이메일이나 닉네임이 있는지 검사한다.</h3>
<pre><code class="language-jsx">try {
  const exUserWithEmail = await User.findOne({ where: { email } });
  const exUserWithNickname = await User.findOne({ where: { nickname } });</code></pre>
<p>User 모델의 &#39;findOne&#39; 메서드를 사용하여 검사하고 있다.</p>
<h3 id="2-5-비밀번호를-해싱한다">2-5. 비밀번호를 해싱한다.</h3>
<pre><code class="language-jsx">const hash = await bcrypt.hash(password, 12);</code></pre>
<p>해싱은 원래의 비밀번호를 복구할 수 없는 문자열로 변환하는 과정으로, 이 과정을 통해 사용자의 비밀번호가 데이터베이스에 안전하게 저장된다.</p>
<h3 id="2-6-새로운-사용자를-생성한다">2-6. 새로운 사용자를 생성한다.</h3>
<pre><code class="language-jsx">const newUser = await User.create({
  email,
  nickname,
  password: hash,
  profileImage,
});</code></pre>
<p>User 모델의 &#39;create&#39; 메서드를 사용하여 새로운 사용자를 생성하고 있다. 이러한 단계를 거쳐 클라이언트의 회원가입 요청에 대한 사용자의 정보를 검증하고 검증을 통과하였을 경우 새로운 사용자의 정보를 저장한다.</p>
<h2 id="3-응답-전송">3. <strong>응답 전송</strong></h2>
<h3 id="3-1-응답-성공시">3-1 응답 성공시</h3>
<p>마지막으로, 사용자 정보가 성공적으로 저장되었다는 응답을 클라이언트에게 전송한다. 이제 사용자는 로그인 할 수 있게 된다. </p>
<p>클라이언트에 응답을 전송하는 부분은 주로 다음과 같은 코드에서 이루어진다.</p>
<pre><code class="language-jsx">return res.status(201).json({
  message: &quot;회원가입이 성공적으로 완료되었습니다.&quot;,
  user: req.session.user,
});</code></pre>
<p>위 코드에서, <code>res.status(201)</code>는 HTTP 응답 코드를 설정하는 부분으로, <code>201</code> 은 &quot;Created&quot;를 의미하며, 새로운 리소스(여기서는 새로운 사용자)가 성공적으로 생성되었음을 나타낸다.</p>
<p>그 다음으로, <code>json</code> 메소드를 통해 JSON 형식의 응답을 보낸다. 이 메소드를 사용하여 전송하는 객체는 두 개의 속성을 갖는다.</p>
<ul>
<li><code>message</code>: 이는 사용자에게 전달되는 메시지로, 회원가입이 성공적으로 완료되었음을 알려주는 문자열을 사용하였다.</li>
<li><code>user</code>: 이는 로그인한 사용자의 정보를 담은 객체로, 이 정보는 클라이언트 측에서 사용자가 로그인 상태에 있는지를 판별하는 데 사용될 수 있다.</li>
</ul>
<p>따라서, 이 코드를 통해 서버는 사용자에게 회원가입이 성공적으로 이루어졌음을 알리고, 이에 따른 사용자의 정보를 함께 전달함으로써 클라이언트가 로그인 상태를 유지할 수 있도록 한다. 이제 클라이언트는 이 정보를 바탕으로 사용자에게 로그인이 되었음을 알릴 수 있다.</p>
<h3 id="3-2-응답-실패시">3-2 응답 실패시</h3>
<p>응답이 실패했을 때는 주로 두 가지 경우에 해당한다. 사용자 입력 유효성 검사에서 에러가 발생한 경우와 서버 내부에서 에러가 발생한 경우이다.</p>
<p>먼저, 사용자 유효성 검사에서 에러가 발생한 경우이다.</p>
<pre><code class="language-jsx">if (
    emailErrors.length &gt; 0 ||
    passwordErrors.length &gt; 0 ||
    nicknameErrors.length &gt; 0 ||
    passwordConfirmErrors.length &gt; 0
  ) {
    return res.status(400).json({
      errors: {
        email: emailErrors,
        password: passwordErrors,
        passwordConfirm: passwordConfirmErrors,
        nickname: nicknameErrors,
      },
    });
  }</code></pre>
<p>위 코드에서, 각각의 입력 필드(email, password, nickname, passwordConfirm)에 대한 유효성 
검사를 진행하고 그 결과를 에러 배열에 저장한다. 만약 어느 하나라도 에러가 있다면, HTTP 응답 코드 <code>400</code>으로 클라이언트에게 응답을 보내며, 에러 메시지를 함께 전달한다. (HTTP 상태 코드 <code>400</code>은 클라이언트의 요청이 잘못되었음을 나타낸다.)</p>
<p>두 번째 경우는 서버 내부에서 에러가 발생한 경우이다. </p>
<pre><code class="language-jsx">} catch (error) {
    console.error(error);
    return next(error);
  }</code></pre>
<p>이 코드는 서버에서 에러가 발생했을 때 실행된다. 예를 들어, 데이터베이스에 연결하는 도중 문제가 발생하거나, 필요한 정보를 찾지 못했을 때 이런 에러가 발생할 수 있다. 이 경우, 에러 정보를 콘솔에 출력하고, <code>next(error)</code>를 통해 에러 핸들링 미들웨어로 에러를 전달한다. 이 때 사용하는 <code>next</code>
 함수는 Express에서 제공하는 미들웨어 함수로, 이 함수를 호출하면, Express는 현재의 미들웨어를 종료하고 다음 미들웨어를 실행한다.</p>
<p>이 경우에는 에러를 처리하는 미들웨어로 제어를 넘기게 되며, 에러 핸들링 미들웨어에서는 적절한 HTTP 
응답 코드와 함께 클라이언트에게 에러 메시지를 전달하게 된다.</p>
<h3 id="4-테스트">4. 테스트</h3>
<p>이제 회원가입 시스템을 모두 구현하였으므로 실제 프로젝트에서 테스트 해보도록 하자.</p>
<p>회원가입 모달에서 각 입력창에 입력값을 입력한다.
<img src="https://velog.velcdn.com/images/hoon-devlog/post/2a40508b-2e65-4e46-bfb5-7e19e003cbeb/image.png" alt=""></p>
<p>각 입력창 값에 대한 유효성 검사를 하고 있기 때문에 유효성 검사를 통과하지 못했을 경우 다음과 같이 동적으로 에러메시지를 출력한다.</p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/8de30b37-aec2-4207-846f-b7101889ce96/image.png" alt=""></p>
<p>이제 각 입력 창에 대한 유효성 검사를 모두 통과 했다면 더이상 하단에 에러메시지가 출력되지 않는다. 이제 ‘가입하기’ 버튼을 클릭해보자</p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/0f16a3ef-2c5b-4735-b555-cf8ff46a8080/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/1304c5da-ba07-49b8-961c-798c730e90aa/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/72d6743c-fc2b-49e8-aefc-24368a967af9/image.png" alt=""></p>
<p>하지만 데이터베이스에 ‘<a href="mailto:asdasd@naver.com">asdasd@naver.com</a>&#39; 이라는 이메일을 가진 사용자와 ‘닉네임’이라는 닉네임을 가진 사용자의 정보가 이미 존재하는 것을 알 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/a31c6c1f-8add-4b79-84b2-3381978c046a/image.png" alt=""></p>
<p>이제 중복되지 않는 이메일과 닉네임을 통해서 회원가입을 해보자</p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/578edcf8-d53e-40ab-b0fb-473de0aba0fc/image.png" alt=""></p>
<p>회원가입이 성공적으로 완료되고 다음단계인 프로필 이미지 등록 모달이 나타난 것을 알 수 있다. 이제 데이터베이스에 새로운 사용자에 대한 정보를 찾아보자</p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/c5c9ff1c-549d-43f2-9f57-0edabd523839/image.png" alt=""></p>
<p>해당 이메일과 닉네임을 가진 사용자의 정보가 성공적으로 데이터베이스에 저장된 것을 알 수 있다. 또한, 이메일 옆에 있는 값은 사용자의 비밀번호인데 사용자의 비밀번호가 문자열의 조합으로 성공적으로 암호화가 된 것을 알 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로그인 및 회원가입 시스템 개요[사용자인증1]]]></title>
            <link>https://velog.io/@hoon-devlog/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%B0%8F-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EC%9A%94part-1</link>
            <guid>https://velog.io/@hoon-devlog/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%B0%8F-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EC%9A%94part-1</guid>
            <pubDate>Sun, 16 Jul 2023 09:07:28 GMT</pubDate>
            <description><![CDATA[<h2 id="1-서론">1. 서론</h2>
<p>지난 포스팅에서는 useState로 상태관리를 통해 유효성검사를 진행하고 유효성 검사를 통과하지 못하면 각 입력창 하단에 에러 메시지를 출력하는 부분을 작성하였다. 하지만 이메일과 비밀번호를 사용하여 실제로 로그인 요청을 처리하는 부분이 구현되어 있지 않았다.</p>
<p>따라서 이번 포스팅에서는 백엔드와 연동하여 로그인 요청을 처리하고, 요청 결과에 따라 성공/실패 처리를 하는 로그인 및 회원가입 시스템 개요에 대해서 알아보도록 하자</p>
<p>또한, 이 프로젝트에서는 이메일을 사용한 로그인 및 회원가입 시스템을 Node.js와 Express.js를 사용하여 어떻게 구현하는지 알아보고 이를 통해 클라이언트와 서버 간에 RESTful API를 활용하는 방법, 그리고 쿠키와 세션을 통한 인증 방법을 구현해보자.</p>
<h2 id="2-로그인-및-회원가입-시스템-개요">2. <strong>로그인 및 회원가입 시스템 개요</strong></h2>
<p>로그인과 회원가입 시스템은 웹 애플리케이션에서 가장 중요한 기능 중 하나입니다. 이를 통해 사용자를 식별하고, 사용자별로 개인화된 서비스를 제공할 수 있습니다. 이메일 기반 인증은 사용자가 이미 자주 사용하는 이메일 계정을 활용할 수 있어 편리하며, 이메일 확인을 통해 사용자의 신원을 일정 수준에서 보장할 수 있습니다.</p>
<p>로그인과 회원가입 시스템은 웹 에플리케이션에서 가장 중요한 기능 중 하나이다. 이를 통해 사용자를 식별하고 사용자 별로 개인화된 서비스를 제공할 수 있기 때문이다. 이메일 기반 인증은 사용자가 이미 자주 사용하는 이메일 계정을 활용할 수 있어 편리하며, 이메일 확인을 통해 사용자의 신원을 일정 수준에서 보장할 수 있다.</p>
<h2 id="3-사용-기술-소개"><strong>3. 사용 기술 소개</strong></h2>
<p>이번에는 로그인 및 회원가입에 사용되는 기술에 대해 소개하고 해당 기술을 선택하게 된 이유에 대해서 알아보자.</p>
<p><strong>REST API</strong>
로그인 및 회원가입 기능에서 REST API를 사용하는 이유는 주로 사용자 경험과 관련되어 있다.</p>
<ol>
<li><strong>실시간 피드백</strong>: REST API를 사용하면 클라이언트는 서버로부터 바로 응답을 받을 수 있다. 예를 들어, 사용자가 회원가입 시 이메일이나 닉네임이 이미 사용 중인지 바로 확인하고 피드백을 주는 것이 가능하다. 이는 서버사이드 렌더링에서는 페이지 전체를 다시 렌더링해야 하는 데 반해, REST API는 필요한 부분만 업데이트하므로 더 효율적이다.</li>
<li><strong>동적인 사용자 인터페이스</strong>: 회원가입이나 로그인과 같은 폼을 다룰 때, REST API는 필요에 따라 동적으로 UI를 업데이트하는데 유리하다. 예를 들어, 폼 유효성 검사 결과에 따라 특정 메시지를 표시하거나, 로그인 후 사용자의 이름을 화면 상단에 표시하는 등의 동적인 기능을 쉽게 구현할 수 있다.</li>
<li><strong>비동기 처리</strong>: 로그인 및 회원가입 과정에서 서버로 요청을 보낼 때, 사용자는 그 응답을 기다리는 동안 다른 작업을 할 수 있다. 이는 REST API가 비동기적으로 작동하기 때문에 가능한데, 이를 통해 사용자 경험이 향상될 것으로 기대된다.</li>
</ol>
<p>이러한 이유로 로그인 및 회원가입 과정에서는 REST API를 활용하는 것이 더욱 적합하다. 물론, 초기 페이지 렌더링에 대해서는 서버사이드 렌더링이 더 효과적일 수 있지만, 로그인이나 회원가입 같은 특정 인터랙션에 대해선 REST API를 사용하는 것이 더 효율적일 수 있다.</p>
<p><strong>쿠키와 세션</strong>
웹은 기본적으로 상태를 유지하지 않는(stateless) 구조이다. 따라서 사용자가 로그인을 하더라도 그 정보를 유지하지 못하는데, 이 문제를 해결하기 위해 쿠키와 세션을 사용한다. 쿠키는 클라이언트에 저장되는 작은 데이터 조각이고, 세션은 서버에서 관리하는 사용자 정보이다. 로그인 정보를 쿠키에 담아 클라이언트에 보내고, 이후 클라이언트가 요청을 보낼 때마다 쿠키를 함께 보내서 사용자를 식별하는 방식이다.</p>
<p>JWT(Json Web Token)도 인증 방식 중 하나로, 최근에 많이 사용되고 있다. JWT는 자체적으로 정보를 가지고 있어서 세션처럼 별도의 저장소가 필요 없는 장점이 있지만, 토큰이 탈취되면 그대로 사용될 수 있다는 보안 문제와 한 번 발급된 토큰의 변경이 어렵다는 단점이 있다. 이에 반해 세션은 서버에서 관리하기 때문에 상대적으로 보안이 잘 되어 있고, 필요에 따라 세션을 제어할 수 있다는 장점이 있다.</p>
<p>따라서 이번 프로젝트에서는 보안이 중요한 로그인 및 회원가입 기능에 세션을 사용하여 사용자 인증을 처리하도록 하자.</p>
<p><strong>Passport.js</strong>
Passport.js는 Node.js로 만들어진 강력한 인증 미들웨어로, 간단하게는 사용자 이름과 비밀번호를 이용한 로컬 전략에서부터, OAuth(페이스북, 구글, 트위터 등)를 이용한 다양한 전략(strategy)을 쉽게 적용할 수 있게 해준다.</p>
<p>Passport.js를 사용하면 로그인을 구현하는 코드가 대폭 간소화되며, 로그인 로직을 분리해서 관리할 수 있기 때문에 코드의 가독성과 유지 보수성이 향상된다. 또한, Passport.js는 세션을 사용하여 사용자 로그인을 유지하며, 이는 로그인한 사용자를 추적하고, 로그인 세션을 유지하는데 필요한 기능을 제공한다.</p>
<p>따라서 이번 프로젝트에서는 Passport.js를 활용하여 로그인 요청을 처리하고 세션을 관리하도록 하자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL과 sequelize를 활용하여 데이터베이스 설계하기]]></title>
            <link>https://velog.io/@hoon-devlog/MySQL%EA%B3%BC-sequelize%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hoon-devlog/MySQL%EA%B3%BC-sequelize%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 08 Jun 2023 07:25:03 GMT</pubDate>
            <description><![CDATA[<p>이제 프로젝트의 회원가입과 로그인의 프론트엔드 파트가 어느정도 마무리 되었기 때문에 데이터베이스를 통해서 사용자의 정보를 저장할 필요가 있다. 따라서 이번에는 데이터베이스를 설계하는 방법에 대해서 알아보자 </p>
<p>데이터베이스를 설계하는 과정은 크게 다음과 같다.</p>
<ol>
<li><strong>요구 분석</strong>: 개발하려는 시스템의 데이터 관리 요구사항을 이해하고 분석하는 단계이다. 이 과정에서 시스템이 어떤 데이터를 관리해야 하는지, 그리고 어떤 형태로 데이터를 저장하고 처리해야 하는지를 파악한다.</li>
<li><strong>개념적 설계</strong>: 요구 분석의 결과를 기반으로 데이터베이스의 개념적인 구조를 설계하는 단계이다. 이 과정에서 일반적으로 ER(Entity-Relationship) 다이어그램을 사용하여 개체, 속성, 그리고 관계를 표현한다.</li>
<li><strong>논리적 설계</strong>: 개념적 설계의 결과를 바탕으로 데이터베이스 관리 시스템(DBMS)이 이해할 수 있는 논리적인 데이터 모델로 변환하는 과정이다. 이 과정에서 테이블, 필드, 데이터 타입, 키, 관계 등을 정의하게 된다. 이때, Sequelize와 같은 ORM(Object-Relational Mapping) 도구를 사용하여 모델 정의와 관련된 마이그레이션 파일을 생성하는 것이 일반적이다. </li>
</ol>
<p>이러한 과정을 거치면 데이터베이스는 시스템의 요구사항을 충족하면서도 효율적인 성능을 보장할 수 있는 구조를 가지게 된다. 또한, 이러한 설계 과정을 통해 데이터베이스의 정합성을 보장하고, 미래의 확장성을 고려한 설계를 할 수 있다.</p>
<h2 id="1-요구-분석">1. <strong>요구 분석</strong></h2>
<p>먼저, MySQL로 회원을 관리하기 위한 기본적인 데이터베이스 스키마를 설계해 보자</p>
<p>이를 위해 <code>users</code>라는 테이블을 만들고 일반적으로 회원 정보에는 아이디, 비밀번호, 이메일, 닉네임 등이 포함되므로 이를 고려해서 설계해 보자.</p>
<p>이 테이블은 다음과 같은 내용을 포함한다.</p>
<ul>
<li><code>id</code>: 각 사용자를 유일하게 식별하는 ID이다. 이 필드는 자동으로 증가하는 정수이다.</li>
<li><code>nickname</code>: 사용자의 닉네임으로 이 필드는 20자 이내의 문자열이어야 한다.</li>
<li><code>email</code>: 사용자의 이메일 주소이며, 이 필드는 50자 이내의 문자열이어야 한다. 또한, 이 필드는 테이블 내에서 유일해야 한다.</li>
<li><code>password</code>: 사용자의 비밀번호이며, 이 필드는 해싱된 상태로 저장된다.</li>
<li><code>profileImage</code>: 사용자의 프로필 이미지 URL이다. 이 필드는 필수 항목이 아니므로 NULL 값을 허용한다.</li>
<li><code>createdAt</code>: 사용자가 만들어진 시각이며 기본값으로 생성 시각을 자동으로 저장한다.</li>
<li><code>updatedAt</code>: 사용자 정보가 마지막으로 수정된 시각이며, 기본값으로 수정 시각을 자동으로 업데이트한다.</li>
<li><code>userType</code>: 회원의 권한을 나타내는 필드로, &#39;admin&#39;과 &#39;user&#39; 같은 문자열을 저장하거나, 권한의 복잡성에 따라 더 많은 옵션을 저장할 수 있다.</li>
<li><code>userStatus</code>: 회원의 상태를 나타내는 필드로, &#39;active&#39;와 &#39;inactive&#39;, &#39;deleted&#39;와 같은 상태를 저장하거나, 상황에 따라 더 많은 상태를 저장할 수 있다.</li>
</ul>
<p>따라서 위에서 설명한 추가적인 필드들을 반영하여 테이블 스키마를 업데이트하면 다음과 같다</p>
<pre><code class="language-jsx">// models/User.js

&quot;use strict&quot;;
const { Model, DataTypes } = require(&quot;sequelize&quot;);
module.exports = (sequelize) =&gt; {
  class User extends Model {
    static associate(models) {
      // Define associations here
    }
  }
  User.init(
    {
      id: {
        type: DataTypes.INTEGER,
        primaryKey: true,
        autoIncrement: true,
        allowNull: false,
      },
      nickname: {
        type: DataTypes.STRING(20),
        allowNull: false,
      },
      email: {
        type: DataTypes.STRING(50),
        allowNull: false,
        unique: true,
      },
      password: {
        type: DataTypes.STRING(255),
        allowNull: false,
      },
      profileImage: DataTypes.STRING(255),
      userType: {
        type: DataTypes.ENUM,
        values: [&quot;admin&quot;, &quot;user&quot;],
        defaultValue: &quot;user&quot;,
      },
      userStatus: {
        type: DataTypes.ENUM,
        values: [&quot;active&quot;, &quot;inactive&quot;, &quot;deleted&quot;],
        defaultValue: &quot;active&quot;,
      },
    },
    {
      sequelize,
      modelName: &quot;User&quot;,
      tableName: &quot;users&quot;,
      timestamps: true,
      createdAt: &quot;createdAt&quot;,
      updatedAt: &quot;updatedAt&quot;,
    }
  );
  return User;
};</code></pre>
<h2 id="2-개념적-설계">2. <strong>개념적 설계</strong></h2>
<p>다음으로  MySQL Workbench를 사용해서 ER(Entity-Relationship) 다이어그램을 생성해 보자. ER 다이어 그램 생성 과정은 다음과 같다. </p>
<ol>
<li><strong>MySQL Workbench 열기</strong>: 먼저 MySQL Workbench를 시작한다.</li>
<li><strong>새로운 ER 모델 생성</strong>: 메뉴에서 &quot;File&quot; -&gt; &quot;New Model&quot;을 선택하여 새 ER 모델을 생성한다.</li>
<li><strong>다이어그램 추가</strong>: &quot;Add Diagram&quot; 버튼을 사용하여 추가 다이어그램을 생성할 수 있다.</li>
<li><strong>테이블 추가</strong>: 도구 상자에서 &quot;Table&quot; 아이콘을 선택한 후 다이어그램 영역에 드래그하여 테이블을 추가한다.</li>
<li><strong>테이블 수정</strong>: 테이블을 더블 클릭하여 테이블 구조를 수정한다. 여기에서 테이블 이름, 열(column), 인덱스, 외래키 등을 설정할 수 있다.</li>
<li><strong>관계 설정</strong>: 도구 상자의 &quot;1:1 Association&quot;, &quot;1:n Non-Identifying Relationship&quot;, &quot;1:n Identifying Relationship&quot; 등의 아이콘을 사용하여 테이블 간의 관계를 설정할 수 있다. 관계를 설정하려면 해당 아이콘을 선택한 후, 관계를 설정하려는 두 테이블을 클릭하면 된다.</li>
<li><strong>모델 저장</strong>: 모든 테이블과 관계가 다이어그램에 추가되었다면 &quot;File&quot; -&gt; &quot;Save Model As...&quot;를 선택하여 ER 모델을 저장한다.</li>
<li><strong>데이터베이스로 내보내기/동기화</strong>: 만들어진 ER 모델을 실제 MySQL 데이터베이스로 내보내거나 동기화하려면 &quot;Database&quot; 메뉴에서 &quot;Forward Engineer...&quot; 또는 &quot;Synchronize Model...&quot;를 선택한다.</li>
</ol>
<p>users와 cafes테이블을 각각 만들고 이 테이블들을 연결하는 users_cafes 테이블을 다대다 관계로 연결하였다.</p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/500ee3b7-7c06-46f8-a7f0-92cd2cd7f837/image.png" alt=""></p>
<h2 id="3-논리적-설계---sequelize와-같은-ormobject-relational-mapping-도구를-사용하여-모델-정의와-관련된-마이그레이션-파일을-생성">3. <strong>논리적 설계 -</strong> Sequelize와 같은 ORM(Object-Relational Mapping) 도구를 사용하여 모델 정의와 관련된 마이그레이션 파일을 생성</h2>
<p>Sequelize는 Node.js를 위한 ORM(Object-Relational Mapping)으로 JavaScript 객체와 데이터베이스 간의 관계를 매핑해주는 도구이다. ORM을 사용하는 이유는 코드의 가독성을 높이고 데이터베이스 스키마를 쉽게 변경하거나 업데이트할 수 있기 때문이다.</p>
<p><code>sequelize-cli</code>를 통해 데이터베이스를 관리하는데 필요한 파일들에 대해서 알아보자</p>
<ol>
<li><strong>config</strong>: 이 디렉터리에는 <code>config.json</code> 파일이 생성된다. 이 파일은 Sequelize에게 어떻게 데이터베이스에 연결할지에 대한 정보를 제공한다. 각 개발 환경(개발, 테스트, 배포)에 대한 데이터베이스 연결 설정을 별도로 할 수 있다.</li>
<li><strong>migrations</strong>: 이 디렉터리에는 데이터베이스 마이그레이션 파일들이 저장된다. 마이그레이션 파일은 데이터베이스의 스키마를 변경하는데 사용된다. 예를 들어, 테이블을 생성하거나 수정하거나 삭제하는 작업을 마이그레이션 파일로 작성할 수 있다. </li>
<li><strong>models</strong>: 이 디렉터리에는 Sequelize 모델 파일들이 저장된다. 각 파일은 데이터베이스의 테이블에 대응되며, 테이블의 스키마를 정의한다. </li>
<li><strong>seeders</strong>: 이 디렉터리에는 시더 파일들이 저장되며, 시더 파일은 데이터베이스에 초기 데이터를 삽입하는데 사용된다.</li>
</ol>
<p>앞서 작성한 사용자의 스키마를 바탕으로 애플리케이션 코드를 작성해보자.</p>
<p>애플리케이션 코드는 애플리케이션의 비즈니스 로직을 구현하는 코드로 데이터베이스 모델을 사용하여 CRUD(Create, Read, Update, Delete) 연산을 수행하는 코드를 작성하는 것을 포함한다. controllers/userController.js파일에서 사용자의 CRUD 연산 코드를 작성해 보자.</p>
<pre><code class="language-jsx">// controllers/userController.js

const { User } = require(&quot;../models&quot;);

// 모든 사용자를 가져오는 함수
exports.getAllUsers = async (req, res) =&gt; {
  try {
    const users = await User.findAll();
    res.status(200).json(users);
    console.log(req.body);
  } catch (error) {
    console.error(error);
    res.status(400).json({ message: &quot;사용자를 불러오는 중 오류가 발생했습니다&quot; });
  }
};

// 특정 사용자를 가져오는 함수
exports.getUser = async (req, res) =&gt; {
  try {
    const user = await User.findByPk(req.params.id);
    if (!user) return res.status(404).json({ message: &quot;사용자를 찾을 수 없습니다&quot; });
    res.status(200).json(user);
    console.log(req.body);
  } catch (error) {
    console.error(error);
    res.status(400).json({ message: &quot;사용자를 불러오는 중 오류가 발생했습니다&quot; });
  }
};

// 새로운 사용자를 생성하는 함수
exports.createUser = async (req, res) =&gt; {
  try {
    const newUser = await User.create(req.body);
    res.status(201).json(newUser);
    console.log(req.body);
  } catch (error) {
    console.error(error);
    res.status(400).json({ message: &quot;사용자를 생성하는 중 오류가 발생했습니다&quot; });
  }
};

// 사용자 정보를 수정하는 함수
exports.updateUser = async (req, res) =&gt; {
  try {
    const user = await User.update(req.body, {
      where: { id: req.params.id },
    });
    if (!user) return res.status(404).json({ message: &quot;사용자를 찾을 수 없습니다&quot; });
    res.status(200).json({ message: &quot;사용자가 성공적으로 업데이트되었습니다&quot; });
    console.log(req.body);
  } catch (error) {
    res.status(400).json({ message: &quot;사용자를 업데이트하는 중 오류가 발생했습니다&quot; });
  }
};

// 사용자를 삭제하는 함수
exports.deleteUser = async (req, res) =&gt; {
  try {
    const user = await User.destroy({
      where: { id: req.params.id },
    });
    if (!user) return res.status(404).json({ message: &quot;사용자를 찾을 수 없습니다&quot; });
    res.status(200).json({ message: &quot;사용자가 성공적으로 삭제되었습니다&quot; });
    console.log(req.body);
  } catch (error) {
    console.error(error);
    res.status(400).json({ message: &quot;사용자를 삭제하는 중 오류가 발생했습니다&quot; });
  }
};</code></pre>
<p>해당 코드는 사용자 데이터를 관리하기 위한 Controller의 일부를 나타낸다. 여기서는 User 모델에 대해 CRUD(Create, Read, Update, Delete) 연산을 수행하는 함수들이 정의되어 있다. 각 함수는 HTTP 요청을 받아 알맞은 데이터베이스 작업을 수행하고, 그 결과를 클라이언트에게 응답으로 보내준다.</p>
<p>이런 컨트롤러 함수를 정의한 후, 이 함수들을 Express 라우트와 연결하면 완성된다. 이제 라우팅 및 미들웨어 설정을 해보자</p>
<pre><code class="language-jsx">// routes/userRoutes.js

const express = require(&#39;express&#39;);
const userController = require(&#39;../controllers/userController&#39;);
const router = express.Router();

router
  .route(&#39;/&#39;)
  .get(userController.getAllUsers)
  .post(userController.createUser);

router
  .route(&#39;/:id&#39;)
  .get(userController.getUser)
  .put(userController.updateUser)
  .delete(userController.deleteUser);

module.exports = router;</code></pre>
<p> CRUD 연산을 위한 라우트 정의는 다음과 같다.</p>
<ul>
<li><code>router.route(&#39;/:id&#39;)</code>는 URL에서 <code>:id</code> 부분에 동적인 값을 받을 수 있게 해준다. 예를 들어, <code>http://localhost:3000/users/1</code>와 같이 요청하면, <code>:id</code> 부분에 <code>1</code>이라는 값이 들어가게 되며, 이 값을 <code>req.params.id</code>로 가져올 수 있다.</li>
<li><code>get(userController.getUser)</code>는 특정 사용자를 가져오는 요청을 처리한다.</li>
<li><code>put(userController.updateUser)</code>는 특정 사용자의 정보를 업데이트하는 요청을 처리한다.</li>
<li><code>delete(userController.deleteUser)</code>는 특정 사용자를 삭제하는 요청을 처리한다.</li>
</ul>
<p>이렇게 각 컨트롤러는 해당하는 모델에 대한 CRUD 연산을 정의하고, 각 라우트는 해당 연산을 특정 URL 경로에 바인딩한다. 이렇게 하면 클라이언트가 특정 URL에 HTTP 요청을 보내면, 그 요청이 알맞은 함수에 의해 처리되어 적절한 응답을 보내게 된다.</p>
<p>이렇게 작성한 코드는 서버를 구동하는 app.js 파일에서 불러와 사용해야 한다. 이렇게 서버를 구동하면 사용자와 카페에 대한 API 엔드포인트가 생성되고, 이를 통해 데이터베이스와 상호작용할 수 있다.</p>
<pre><code class="language-jsx">// app.js

const express = require(&quot;express&quot;);
const app = express();
const userRoutes = require(&quot;./routes/userRoutes&quot;);
const cafeRoutes = require(&quot;./routes/cafeRoutes&quot;)
const PORT = process.env.PORT || 8000;

// JSON 데이터 파싱을 위한 미들웨어
app.use(express.json());

// 라우팅 설정
app.use(&quot;/&quot;, cafeRoutes);
app.use(&quot;/users&quot;, userRoutes);

app.listen(PORT, () =&gt; {
  console.log(`서버가 ${PORT} 포트에서 실행 중입니다.`);
});

module.exports = app;</code></pre>
<p>마지막으로 sequelize-cli를 이용하여 앞서 작성한 스키마를 기반으로 실제 테이블을 생성해보자 </p>
<p>마이그레이션 파일을 만들기 위해서는 Sequelize CLI를 사용하고 다음 명령어를 실행하면 된다.</p>
<pre><code>npx sequelize-cli migration:generate --name create_users</code></pre><p>생성된 마이그레이션 파일에 미리 설계한 스키마를 바탕으로 다음과 같이 작성한다.</p>
<pre><code class="language-jsx">&#39;use strict&#39;;
module.exports = {
  up: async (queryInterface, Sequelize) =&gt; {
    await queryInterface.createTable(&#39;Users&#39;, {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      nickname: {
        type: Sequelize.STRING
      },
      email: {
        type: Sequelize.STRING
      },
      password: {
        type: Sequelize.STRING
      },
      profileImage: {
        type: Sequelize.STRING
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      userType: {
        type: Sequelize.ENUM(&#39;admin&#39;, &#39;user&#39;),
        defaultValue: &#39;user&#39;
      },
      userStatus: {
        type: Sequelize.ENUM(&#39;active&#39;, &#39;inactive&#39;, &#39;deleted&#39;),
        defaultValue: &#39;active&#39;
      }
    });
  },
  down: async (queryInterface, Sequelize) =&gt; {
    await queryInterface.dropTable(&#39;Users&#39;);
  }
};</code></pre>
<p>마이그레이션 파일을 작성한 후, 다음 명령어를 실행하여 이 마이그레이션을 데이터베이스에 적용할 수 있다.</p>
<pre><code>npx sequelize-cli db:migrate</code></pre><p>이것으로 데이터베이스에 &#39;Users&#39; 테이블이 생성된다.</p>
<p><code>cafes</code> 테이블과 <code>users_cafes</code> 테이블에 대해서도 동일하게 생성하면 MySQL 워크벤치에 다음과 같이 테이블이 생성된 모습을 볼 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/aacf9d2b-eeac-4437-8cea-751dec6a26f1/image.png" alt=""></p>
<p>마지막으로 postman에서 내가 생성한 users 테이블이 정상적으로 HTTP 요청을 처리하고 있는지 테스트 해보자.</p>
<p>URL에<a href="http://localhost8000/users"><code>http://localhost8000/users</code></a> 로 데이터를 입력하고 POST 요청을 해보자.</p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/4698a169-e4e3-40ca-ae69-f9a01f597bcf/image.png" alt=""></p>
<p>MySQL 워크벤치에서 해당 데이터가 추가된 것을 확인할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/f762d6c7-1450-4f60-999b-59059ac63625/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redux Toolkit을 사용하여 모달 상태 관리하기]]></title>
            <link>https://velog.io/@hoon-devlog/Redux-Toolkit%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EB%AA%A8%EB%8B%AC-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hoon-devlog/Redux-Toolkit%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EB%AA%A8%EB%8B%AC-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 21 May 2023 13:17:06 GMT</pubDate>
            <description><![CDATA[<h2 id="redux-api-대신-redux-toolkit을-사용하게-된-이유">Redux API 대신 Redux ToolKit을 사용하게 된 이유</h2>
<p>기존의 프로젝트에서는 Redux API로 상태를 관리하였다. 하지만 전역 상태관리를 해야할 대상이 점점 증가하면서 Redux API로 모든 전역 상태를 관리하면 복잡성이 크게 증가할 것 같았다. 따라서 Redux의 공식적인 추상화 레이어인Redux ToolKit을 통해서 Redux 코드를 더 간결하게 작성하고 싶었다. </p>
<p>Redux ToolKit은 다음과 같은 기능을 제공한다.</p>
<ol>
<li><p>configureStore(): Redux store를 설정하는 데 필요한 여러 함수 호출을 단일 함수 호출로 감싸준다. (Redux 개발 도구 연동, 미들웨어 설정, Redux Thunk 미들웨어 등을 포함한다)</p>
</li>
<li><p>createSlice(): 액션 생성자와 액션 타입, 리듀서를 한 번에 만들어준다.</p>
</li>
</ol>
<h2 id="redux-toolkit-도입과정">Redux ToolKit 도입과정</h2>
<ol>
<li>먼저, <code>redux-toolkit</code> 패키지를 설치한다.</li>
</ol>
<pre><code>npm install @reduxjs/toolkit</code></pre><ol start="2">
<li>기존에 <code>actions.js</code>와 <code>reducer.js</code>에서 수행하던 작업을 <code>modalState.js</code>에서 한 번에 수행하도록 변경한다. </li>
</ol>
<pre><code class="language-jsx">// /src/store/modalSlice.js:

import { createSlice } from &#39;@reduxjs/toolkit&#39;;

// 초기 상태를 정의
const initialState = {
  isLoginModalVisible: false,
  isSignupModalVisible: false,
  isAddProfileImgModalVisible: false,
};

const modalSlice = createSlice({
  // createSlice 함수를 사용하여 리덕스의 액션 생성자와 리듀서 한번에 정의
  name: &#39;modal&#39;,
  initialState,
  reducers: {
    // 액션에 따라 상태를 어떻게 변경할지 정의하는 객체
    showLoginModal: state =&gt; {
      state.isLoginModalVisible = true;
    },
    hideLoginModal: state =&gt; {
      state.isLoginModalVisible = false;
    },
    showSignupModal: state =&gt; {
      state.isSignupModalVisible = true;
    },
    hideSignupModal: state =&gt; {
      state.isSignupModalVisible = false;
    },
    showAddProfileImgModal: state =&gt; {
      state.isAddProfileImgModalVisible = true;
    },
    hideAddProfileImgModal: state =&gt; {
      state.isAddProfileImgModalVisible = false;
    },
  },
});

export const {
  showLoginModal,
  hideLoginModal,
  showSignupModal,
  hideSignupModal,
  showAddProfileImgModal,
  hideAddProfileImgModal,
} = modalSlice.actions;

export default modalSlice.reducer;</code></pre>
<ol start="3">
<li>그리고 <code>store.js</code>에서는 리덕스 툴킷의 <code>configureStore</code> 함수를 사용하여 스토어를 생성한다.</li>
</ol>
<pre><code class="language-jsx">// /src/store/store.js:
import { configureStore } from &#39;@reduxjs/toolkit&#39;;
import modalReducer from &#39;./modalState&#39;;

export default configureStore({
  reducer: {
    modal: modalReducer,
  },
});</code></pre>
<ol start="4">
<li>이후, 애플리케이션 내에서 리덕스 스토어에 접근하는 방식을 변경한다. 이전에는 <code>connect</code> 함수나 <code>useSelector</code> 및 <code>useDispatch</code> 훅을 사용하여 스토어에 접근하고 액션을 디스패치했지만, 리덕스 툴킷을 사용하면서 액션 타입 문자열 대신 액션 생성자 함수를 직접 사용할 수 있다.</li>
</ol>
<p>이런 과정을 거치면서, 액션 타입을 직접 작성하고 액션 생성자 함수와 리듀서 함수를 별도로 작성하는 등의 반복적인 작업을 줄일 수 있었다. 또한, 추후에 액션 생성자 함수를 직접 사용하면서 액션 타입의 오타 등으로 인한 버그 발생 확률을 줄일 수 있을 것으로 기대된다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 도입시 서버 사이드 렌더링과 클라이언트 사이드 렌더링의 차이로 인한 이슈 해결하기(useEffect 훅 사용하기)]]></title>
            <link>https://velog.io/@hoon-devlog/Next.js-%EB%8F%84%EC%9E%85%EC%8B%9C-%EC%84%9C%EB%B2%84-%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81%EA%B3%BC-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81%EC%9D%98-%EC%B0%A8%EC%9D%B4%EB%A1%9C-%EC%9D%B8%ED%95%9C-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0useEffect-%ED%9B%85-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hoon-devlog/Next.js-%EB%8F%84%EC%9E%85%EC%8B%9C-%EC%84%9C%EB%B2%84-%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81%EA%B3%BC-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81%EC%9D%98-%EC%B0%A8%EC%9D%B4%EB%A1%9C-%EC%9D%B8%ED%95%9C-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0useEffect-%ED%9B%85-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 19 May 2023 07:32:25 GMT</pubDate>
            <description><![CDATA[<p>Next.js는 기본적으로 서버 사이드 렌더링(SSR)을 지원하는데, 이는 초기 페이지 로딩 속도를 향상시키고, SEO 최적화를 돕는 등 여러 가지 장점이 있다.</p>
<p>하지만 이러한 장점들과 함께 문제점도 발생하는데, 기존의 프로젝트에서는 <code>window</code>나 <code>document</code>와 같은 브라우저 전용 객체에 접근하는 과정에서 에러가 생기는 경우가 많았다.</p>
<h3 id="해결-방법">해결 방법</h3>
<p>이 문제를 해결하기 위해 <code>useEffect</code> 훅을 사용하여 클라이언트 사이드에서만 해당 코드가 실행되도록 하였다.</p>
<p><code>useEffect</code>는 컴포넌트가 렌더링을 마친 후에 실행되므로, 이를 이용하면 서버 사이드에서는 실행되지 않고, 클라이언트 사이드에서만 실행되도록 할 수 있다.</p>
<pre><code class="language-jsx">const [isClient, setIsClient] = useState(false);

useEffect(() =&gt; {
  setIsClient(true);
}, []);</code></pre>
<p>위의 코드는 클라이언트 사이드에서만 실행되는 useEffect 훅을 사용하여 isClient라는 상태를 true로 설정한다. 이렇게 하면, 이 컴포넌트가 클라이언트 사이드에서만 렌더링되도록 보장할 수 있다.</p>
<p>또한 useEffect 훅은 모달 컴포넌트가 렌더링될 때 필요한 DOM 요소를 준비하고, 컴포넌트가 언마운트될 때 이를 제거하도록 할 수도 있다.</p>
<pre><code class="language-jsx">useEffect(() =&gt; {
    const el =
      document.getElementById(&#39;modal-root&#39;) || document.createElement(&#39;div&#39;);
    if (!document.getElementById(&#39;modal-root&#39;)) {
      el.id = &#39;modal-root&#39;;
      document.body.appendChild(el);
    }
    setModalRoot(el);

    return () =&gt; {
      if (!document.getElementById(&#39;modal-root&#39;)) {
        document.body.removeChild(el);
      }
    };
  }, []);</code></pre>
<p>이 부분은 모달이 클라이언트 사이드에서 렌더링될 때 돔 요소를 찾거나, 없으면 새로 생성해서 body에 추가하도록 하였다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 도입시 스타일링 이슈 해결하기]]></title>
            <link>https://velog.io/@hoon-devlog/Next.js-%EB%8F%84%EC%9E%85%EC%8B%9C-%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hoon-devlog/Next.js-%EB%8F%84%EC%9E%85%EC%8B%9C-%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 19 May 2023 06:21:57 GMT</pubDate>
            <description><![CDATA[<h3 id="도입-배경">도입 배경</h3>
<p>이전 포스팅에 프로젝트를 진행하면서 Next.js를 도입하게된 이유에 대해서 설명하였다. Next.js는 서버 사이드 렌더링(SSR)을 지원하여 초기 페이지 로딩 속도를 향상시키고, SEO에 유리한 구조를 제공하는 등 많은 장점을 갖춘 프레임워크이다.하지만, Next.js를 도입하면서 가장 큰 문제는 클라이언트 사이드에서만 동작하는 라이브러리와의 호환성이다. 그 중에서도 CSS-in-JS 라이브러리인 styled-components를 사용하면서 발생한 스타일링 이슈를 이 글에서는 자세히 다루어보자.</p>
<h3 id="문제-상황">문제 상황</h3>
<p>기존 프로젝트에서는 React와 styled-components를 이용해 개발했었다. 하지만 Next.js를 도입한 후, 서버 사이드에서 렌더링된 페이지는 스타일이 제대로 적용되지 않는 문제가 발생했다.</p>
<h3 id="문제-원인">문제 원인</h3>
<p>Next.js와 styled-components를 함께 사용할 때, 서버 사이드 렌더링과 클라이언트 사이드 렌더링 간에 스타일 순서 불일치 문제가 발생할 수 있다. 이는 Next.js가 페이지를 서버에서 렌더링할 때 스타일이 제대로 적용되지 않은 채로 HTML을 클라이언트로 전달하기 때문에 발생하는 문제이다.</p>
<h3 id="해결-방법">해결 방법</h3>
<p>이 문제를 해결하기 위해서는 Next.js와 styled-components가 서버 사이드 렌더링 시에도 스타일을 제대로 적용할 수 있도록 설정을 조정해야 한다._document.js 파일을 수정하여, 서버에서 페이지를 렌더링할 때 styled-components의 스타일을 수집하도록 하였다.</p>
<pre><code class="language-jsx">// pages/_document.js
import Document from &#39;next/document&#39;;
import { ServerStyleSheet } from &#39;styled-components&#39;; // styled-components 패키지에서 제공하는 도구로, 서버에서 스타일을 수집

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;

    try {
      ctx.renderPage = () =&gt;
        originalRenderPage({
          enhanceApp: (App) =&gt; (props) =&gt;
            sheet.collectStyles(&lt;App {...props} /&gt;

}),
      });

      const initialProps = await Document.getInitialProps(ctx);
      return {
        ...initialProps,
        styles: (
          &lt;&gt;
            {initialProps.styles}
            {sheet.getStyleElement()}
          &lt;/&gt;
        ),
      };
    } finally {
      sheet.seal();
    }
  }
}</code></pre>
<ol>
<li><p>먼저 ServerStyleSheet를 생성한다. 이것은 styled-components가 서버 사이드에서 스타일을 수집하기 위해 제공하는 클래스이다.</p>
</li>
<li><p>원래의 renderPage 함수를 저장하고, 새로운 renderPage 함수를 제공한다. 이 새로운 함수는 App 컴포넌트를 렌더링하면서 그 안에 있는 모든 styled-components 스타일을 수집한다.</p>
</li>
<li><p>원래의 getInitialProps 함수를 호출하여 기본 문서 속성을 가져온다.</p>
</li>
<li><p>getStyleElement 메소드를 이용해서 수집된 스타일을 HTML 스타일 태그로 변환한다.</p>
</li>
<li><p>이 스타일 태그와 기본 문서 속성을 합친 새로운 문서 속성을 반환한다. 이 반환된 속성은 최종적으로 클라이언트에 전송될 HTML 문서에 적용된다.</p>
</li>
</ol>
<p>마지막으로 seal 메소드를 호출해서 더 이상 스타일을 수집하지 않도록 한다.</p>
<p>이렇게 설정하면, 서버에서 페이지를 렌더링할 때 styled-components의 스타일을 수집하여 초기 HTML과 함께 전달한다. 이를 통해 서버 사이드 렌더링에서도 styled-components의 스타일이 제대로 적용되도록 할 수 있다.</p>
<p>또한, 이 문제를 해결하기 위해 .babelrc 파일에 styled-components 플러그인을 추가한다.</p>
<pre><code class="language-jsx">// .babelrc
{
  &quot;presets&quot;: [&quot;next/babel&quot;],
  &quot;plugins&quot;: [[&quot;styled-components&quot;, { &quot;ssr&quot;: true }]]
}</code></pre>
<p>이렇게 설정하면, Next.js와 styled-components를 함께 사용하여도 서버 사이드 렌더링 시에 스타일이 제대로 적용된다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 프로젝트에 Next.js 프레임워크 도입하기 ]]></title>
            <link>https://velog.io/@hoon-devlog/React-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-Next.js-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EB%8F%84%EC%9E%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hoon-devlog/React-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-Next.js-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EB%8F%84%EC%9E%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 18 May 2023 14:52:31 GMT</pubDate>
            <description><![CDATA[<p>기존의 React 프로젝트에 Next.js 프레임워크를 도입하였다. 이 글에서는 그 도입 과정과 Next.js를 선택한 이유에 대해서 알아보자.</p>
<h2 id="nextjs란">Next.js란?</h2>
<p>Next.js는 서버 사이드 렌더링(SSR)을 지원하는 React 프레임워크이다. 초기 페이지 로딩 시간을 줄이고, 검색 엔진 최적화(SEO)를 개선하기 위해 서버 사이드 렌더링을 사용하는데, 이런 기능을 더욱 쉽게 구현할 수 있게 도와준다. 또한 Next.js는 자동 코드 분할, 파일 시스템 기반의 라우팅, API 라우트 등 다양한 기능을 제공한다.</p>
<h2 id="왜-nextjs를-도입할까">왜 Next.js를 도입할까?</h2>
<ol>
<li><strong>서버 사이드 렌더링 (SSR)</strong>: 이는 웹사이트의 초기 로딩 시간을 줄이고, 웹 크롤러가 사이트의 컨텐츠를 더욱 잘 이해할 수 있도록 도와주어 SEO를 개선한다.</li>
<li><strong>Automatic Code Splitting</strong>: Next.js는 페이지 별로 번들을 분할하므로, 필요한 코드만 로드할 수 있다. 이로써 사용자 경험이 개선되며 성능도 개선된다.</li>
<li><strong>API Routes</strong>: Next.js를 이용하면 서버 사이드에서 실행되는 API 엔드포인트를 쉽게 작성할 수 있다. 이 기능을 활용하면 별도의 서버 설정 없이 프로젝트 내에서 API 로직을 관리할 수 있다.</li>
</ol>
<h2 id="nextjs-도입-과정">Next.js 도입 과정</h2>
<ol>
<li><strong>Next.js 설치</strong>: 먼저, Next.js를 설치합니다. 기존 React 프로젝트에 Next.js를 추가하려면 터미널에 <code>npm install next</code>를 입력한다.</li>
<li><strong>페이지 이동</strong>: Next.js는 파일 시스템 기반의 라우팅을 사용한다. 그래서 기존의 React 컴포넌트를 &#39;pages&#39; 디렉토리로 옮기고, 각 페이지에 해당하는 컴포넌트를 루트 레벨에서 접근 가능하도록 만든다.</li>
<li><strong>_app과 _document 파일 생성</strong>: Next.js는 커스텀 <code>_app</code>와 <code>_document</code> 파일을 제공하여 프로젝트 전체에 공통되는 로직을 관리할 수 있게 한다. <code>_app</code> 파일은 페이지 초기화를 담당하며, 페이지 전환에 대한 상태 유지, 추가 데이터를 페이지에 주입하거나 글로벌 CSS를 추가하는 역할을 한다. <code>_document</code> 파일은 서버에서만 렌더링되며, 일반적으로 서버 사이드 렌더링(SSR)을 위한 초기 HTML 문서 구조를 설정한다.</li>
<li><strong>라우팅 구현</strong>: Next.js는 파일 시스템 기반 라우팅을 제공한다. 이는 <code>pages</code> 디렉토리 내의 파일 구조를 바탕으로 URL 경로를 생성한다. </li>
<li><strong>API 라우트 구현</strong>: Next.js에서는 <code>pages/api</code> 디렉토리 내에서 API 라우트를 생성할 수 있다. 이 기능을 사용하면 별도의 서버 없이도 API 로직을 처리할 수 있다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[카카오 로그인 구현하기]]></title>
            <link>https://velog.io/@hoon-devlog/%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hoon-devlog/%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 08 May 2023 12:45:24 GMT</pubDate>
            <description><![CDATA[<p>이메일 로그인을 구현했으니 이제 카카오 로그인을 구현해보자. 카카오 로그인 구현단계는 다음과 같다.</p>
<ol>
<li>Kakao Developer 사이트 가입 및 앱 생성</li>
</ol>
<ul>
<li>먼저, Kakao Developer 사이트에 가입: <a href="https://developers.kakao.com/">https://developers.kakao.com/</a></li>
<li>로그인 한 후, 내 애플리케이션 페이지에서 새로운 앱을 생성</li>
<li>앱을 생성한 후, 앱 설정에서 플랫폼에 웹을 추가하고, 사이트 도메인과 Redirect URI를 설정</li>
</ul>
<ol start="2">
<li>카카오 로그인 버튼 생성</li>
</ol>
<ul>
<li>로그인 버튼 컴포넌트를 생성하고, 버튼에 이벤트 핸들러를 추가</li>
</ul>
<ol start="3">
<li>카카오 JavaScript SDK 추가</li>
</ol>
<ul>
<li>웹 앱에 카카오 JavaScript SDK를 추가
예시:</li>
<li><code>&lt;script src=&quot;https://developers.kakao.com/sdk/js/kakao.min.js&quot;&gt;&lt;/script&gt;</code></li>
</ul>
<ol start="4">
<li>카카오 SDK 초기화 및 로그인 처리</li>
</ol>
<ul>
<li>카카오 SDK를 초기화하고, 로그인 이벤트 핸들러를 구현
예시:</li>
</ul>
<pre><code class="language-jsx">// src.components/Login/KakaoLogin.jsx

import React from &#39;react&#39;;
import styled from &#39;styled-components&#39;;
import { SharedBtn } from &#39;./LoginModal/LoginModalStyle&#39;;
import { palette } from &#39;../../styles/globalColor&#39;;

const KakaoBtn = styled(SharedBtn)`
  background-color: ${palette.yellowColor};
  font-weight: 500;
  font-size: 16px;
  color: ${palette.blackColor};
`;
const KakaoLoginBtn = () =&gt; {
  const handleKakaoLogin = () =&gt; {
    window.Kakao.Auth.login({
      success: authObj =&gt; {
        console.log(&#39;Kakao login success:&#39;, authObj);

        // 사용자 정보 가져오기
        window.Kakao.API.request({
          url: &#39;/v2/user/me&#39;,
          success: res =&gt; {
            console.log(&#39;User info:&#39;, res);
          },
          fail: error =&gt; {
            console.error(&#39;Failed to get user info:&#39;, error);
          },
        });
      },
      fail: error =&gt; {
        console.error(&#39;Kakao login failed:&#39;, error);
      },
    });
  };

  return (
    &lt;KakaoBtn type=&#39;button&#39; onClick={handleKakaoLogin}&gt;
      카카오톡 계정으로 로그인
    &lt;/KakaoBtn&gt;
  );
};

export default KakaoLoginBtn;
</code></pre>
<p>이 과정을 통해 카카오 로그인을 구현할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[useState를 사용하여 이메일 로그인 구현하기]]></title>
            <link>https://velog.io/@hoon-devlog/useState%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hoon-devlog/useState%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 08 May 2023 12:41:29 GMT</pubDate>
            <description><![CDATA[<p>지금까지 로그인 모달창을 Redux를 사용하여 전역 상태관리 하였다. 이번에는 <code>useState</code>를 활용하여 이메일 로그인을 구현해 보자. 이메일 로그인 로직은 다음과 같다.</p>
<ol>
<li>사용자가 이메일 주소와 비밀번호를 입력한다.</li>
<li>입력 값이 변경될 때마다, <code>handleChangeEmail</code> 및 <code>handleChangePassword</code> 함수가 호출되어 이메일과 비밀번호 상태가 업데이트된다.</li>
<li>이메일 유효성 검사: <code>handleChangeEmail</code> 함수에서 이메일이 비어있지 않고 형식이 잘못된 경우 경고 메시지가 표시된다.</li>
<li>비밀번호 입력 확인: <code>handleChangePassword</code> 함수에서 비밀번호가 비어있는지 확인한다. 비어있는 경우 경고 메시지가 표시된다.</li>
<li>사용자가 로그인 버튼을 클릭한다.</li>
<li><code>handleSubmit</code> 함수가 호출되어 입력된 이메일과 비밀번호를 검사한다.
a. 입력이 올바르지 않은 경우 (비어있거나, 이메일 형식이 잘못된 경우), 경고 메시지를 표시하고 로그인 시도를 중단한다.
b. 입력이 올바른 경우, 로그인 요청을 진행한다. (이 부분은 추후에 백엔드와 연동한다.)</li>
<li>로그인 요청이 성공하면, 사용자를 인증하고 로그인 세션을 시작한다. (이 부분은 추후에 백엔드와 연동한다.)</li>
<li>로그인 요청이 실패하면, <code>isLoginError</code> 상태를 true로 설정하여 경고 메시지를 표시한다.</li>
</ol>
<p>이 과정을 통해 사용자는 이메일 주소와 비밀번호를 입력하여 로그인을 시도할 수 있으며, 입력 값에 문제가 있는 경우 적절한 경고 메시지를 통해 피드백을 받을 수 있다.</p>
<p>실제 구현된 코드를 보자.</p>
<h2 id="예시코드">예시코드</h2>
<pre><code class="language-jsx">// src/components/Login/EmailLogin.jsx

import React from &#39;react&#39;;

import WarningMsg from &#39;../WarningMsg/WarningMsg.jsx&#39;;
import {
  EmailLoginContainer,
  EmailLabel,
  PasswordLabel,
  EmailInput,
  PasswordInput,
  LoginBtn,
} from &#39;./EmailLoginStyle.js&#39;;

const EmailLogin = ({
  email,
  password,
  handleChangeEmail,
  handleChangePassword,
  isEmailEmpty,
  isEmailInvalid,
  isPasswordEmpty,
  isLoginError,
}) =&gt; {
  return (
    &lt;EmailLoginContainer&gt;
      &lt;EmailLabel htmlFor=&#39;user-email&#39;&gt;이메일&lt;/EmailLabel&gt;
      &lt;EmailInput
        type=&#39;text&#39;
        id=&#39;user-email&#39;
        name=&#39;user-email&#39;
        placeholder=&#39;이메일을 입력해주세요.&#39;
        value={email}
        onChange={handleChangeEmail}
        error={isEmailEmpty || isEmailInvalid}
      /&gt;
      &lt;WarningMsg show={isEmailEmpty} message=&#39;이메일을 입력해 주세요.&#39; /&gt;{&#39; &#39;}
      &lt;WarningMsg
        show={isEmailInvalid}
        message=&#39;이메일 형식에 맞게 입력해 주세요.&#39;
      /&gt;{&#39; &#39;}
      &lt;PasswordLabel htmlFor=&#39;user-pw&#39;&gt;비밀번호&lt;/PasswordLabel&gt;
      &lt;PasswordInput
        type=&#39;password&#39;
        id=&#39;user-pw&#39;
        name=&#39;user-pw&#39;
        placeholder=&#39;비밀번호를 입력해주세요.&#39;
        value={password}
        onChange={handleChangePassword}
        error={isPasswordEmpty || isLoginError}
      /&gt;
      &lt;WarningMsg show={isPasswordEmpty} message=&#39;비밀번호를 입력해 주세요.&#39; /&gt;{&#39; &#39;}
      &lt;WarningMsg
        show={isLoginError}
        message=&#39;로그인 정보가 올바르지 않습니다. 다시 시도해 주세요.&#39;
      /&gt;
      &lt;LoginBtn type=&#39;submit&#39;&gt;로그인&lt;/LoginBtn&gt;
    &lt;/EmailLoginContainer&gt;
  );
};

export default EmailLogin;</code></pre>
<pre><code class="language-jsx">// src/components/Login/LoginModal/LoginModal.jsx

import React, { useState } from &#39;react&#39;;
import { useSelector, useDispatch } from &#39;react-redux&#39;;

import { hideLoginModal } from &#39;../../../store/actions&#39;;
import BaseModal from &#39;../../helpers/BaseModal.jsx&#39;;
import EmailLogin from &#39;../EmailLogin.jsx&#39;;

import {
  LoginModalContent,
  LoginModalText,
  SignupBtn,
  // KakaoBtn,
  OrText,
} from &#39;./LoginModalStyle&#39;;
import KakaoLoginBtn from &#39;../KakaoLoginBtn.jsx&#39;;

const LoginModal = () =&gt; {
  const isLoginModalVisible = useSelector(state =&gt; state.isLoginModalVisible);
  const dispatch = useDispatch();

  const handleHideLoginModal = () =&gt; {
    dispatch(hideLoginModal());
  };

  const [email, setEmail] = useState(&#39;&#39;);
  const [password, setPassword] = useState(&#39;&#39;);
  const [isEmailEmpty, setIsEmailEmpty] = useState(false);
  const [isEmailInvalid, setIsEmailInvalid] = useState(false);
  const [isPasswordEmpty, setIsPasswordEmpty] = useState(false);
  const [isLoginError, setIsLoginError] = useState(false);

  const handleChangeEmail = event =&gt; {
    setEmail(event.target.value);
    console.log(event.target.value);

    // 이메일이 비어있지 않고 형식이 잘못된 경우에만 경고 메시지 표시
    if (event.target.value !== &#39;&#39; &amp;&amp; !validateEmail(event.target.value)) {
      setIsEmailInvalid(true);
    } else {
      setIsEmailInvalid(false);
    }

    // 이메일이 비어있는지 여부 확인
    setIsEmailEmpty(event.target.value === &#39;&#39;);
  };

  const validateEmail = email =&gt; {
    const regex = /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/;
    return regex.test(email);
  };

  const handleChangePassword = event =&gt; {
    setPassword(event.target.value);
    console.log(event.target.value);

    // 패스워드가 비어있는지 확인
    setIsPasswordEmpty(event.target.value === &#39;&#39;);
  };

  const handleSubmit = async event =&gt; {
    event.preventDefault();
    if (email === &#39;&#39;) {
      setIsEmailEmpty(true);
    } else {
      setIsEmailEmpty(false);
    }

    if (email !== &#39;&#39; &amp;&amp; !validateEmail(email)) {
      setIsEmailInvalid(true);
    } else {
      setIsEmailInvalid(false);
    }

    if (password === &#39;&#39;) {
      setIsPasswordEmpty(true);
    } else {
      setIsPasswordEmpty(false);
    }

    // 입력이 올바르지 않은 경우에는 로그인 시도를 중단
    if (isEmailEmpty || isEmailInvalid || isPasswordEmpty) {
      return;
    }

    // 이메일과 패스워드가 존재하지 않을떄 로직 추가

    try {
      console.log(email, password);
    } catch (error) {
      setIsLoginError(true);
    }
  };

  return (
    &lt;BaseModal
      isVisible={isLoginModalVisible}
      onClose={handleHideLoginModal}
      title=&#39;로그인 또는 회원가입&#39;
    &gt;
      &lt;LoginModalContent onSubmit={handleSubmit}&gt;
        &lt;LoginModalText&gt;☕️ 카페골목에 오신 것을 환영합니다.&lt;/LoginModalText&gt;
        &lt;EmailLogin
          email={email}
          password={password}
          handleChangeEmail={handleChangeEmail}
          handleChangePassword={handleChangePassword}
          handleSubmit={handleSubmit}
          isEmailEmpty={isEmailEmpty}
          isEmailInvalid={isEmailInvalid}
          isPasswordEmpty={isPasswordEmpty}
          isLoginError={isLoginError}
        /&gt;
        &lt;OrText&gt;또는&lt;/OrText&gt;
        &lt;KakaoLoginBtn /&gt;
        &lt;SignupBtn type=&#39;button&#39;&gt;카페골목 회원가입 하기&lt;/SignupBtn&gt;
      &lt;/LoginModalContent&gt;
    &lt;/BaseModal&gt;
  );
};

export default LoginModal;</code></pre>
<pre><code class="language-jsx">// src/components/WarningMsg/WarningMsg.jsx

import React from &#39;react&#39;;
import styled from &#39;styled-components&#39;;
import { palette } from &#39;../../styles/globalColor&#39;;

export const WarningMsgContainer = styled.strong`
  display: block;
  margin-left: 5px;
  margin-bottom: 10px;
  font-size: 13px;
  color: ${palette.redColor};
  display: ${props =&gt; (props.show ? &#39;block&#39; : &#39;none&#39;)};
`;

const WarningMsg = ({ show, message }) =&gt; (
  &lt;WarningMsgContainer show={show}&gt;{message}&lt;/WarningMsgContainer&gt;
);

export default WarningMsg;</code></pre>
<h2 id="추후에-구현할-내용">추후에 구현할 내용</h2>
<ol>
<li>실제 로그인 처리: 현재 예제 코드에는 이메일과 비밀번호를 사용하여 실제로 로그인 요청을 처리하는 부분이 구현되어 있지 않다. 이 부분은 백엔드와 연동하여 로그인 요청을 처리하고, 요청 결과에 따라 성공/실패 처리를 해야 한다.</li>
<li>로그인 성공 후 처리: 로그인이 성공하면, 사용자 인증 및 로그인 세션을 시작하는 등의 추가 작업이 필요하다. 또한, 사용자에게 로그인 성공을 알리는 메시지를 표시하고, 원하는 페이지로 리다이렉션할 수 있도록 처리해야 한다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Portals를 사용하여 모달과 오버레이를 보다 Semantic한 코드 작성하기]]></title>
            <link>https://velog.io/@hoon-devlog/React-Portals%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%A9%B4-%EB%AA%A8%EB%8B%AC%EA%B3%BC-%EC%98%A4%EB%B2%84%EB%A0%88%EC%9D%B4%EB%A5%BC-%EB%B3%B4%EB%8B%A4-%EC%9D%98%EB%AF%B8%EB%A1%A0%EC%A0%81%EC%9D%B8-%EC%BD%94%EB%93%9C%EB%A1%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hoon-devlog/React-Portals%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%A9%B4-%EB%AA%A8%EB%8B%AC%EA%B3%BC-%EC%98%A4%EB%B2%84%EB%A0%88%EC%9D%B4%EB%A5%BC-%EB%B3%B4%EB%8B%A4-%EC%9D%98%EB%AF%B8%EB%A1%A0%EC%A0%81%EC%9D%B8-%EC%BD%94%EB%93%9C%EB%A1%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 01 May 2023 11:36:52 GMT</pubDate>
            <description><![CDATA[<p>기존의 모달과 오버레이를 보면 app이라는 선택자를 가진 div태그 내부에 footer 컴포넌트 하단에 모달과 오버레이가 존재했다. 하지만 모달과 오버레이는 footer 하단에 존재하는 컴포넌트가 아니기 때문에 의미론적인 코드라고 볼 수 없다. </p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/b940b9c8-e35e-49e1-8af8-da46ff9cf753/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/167f31bd-d870-4248-b0d3-acebba3e8e21/image.png" alt=""></p>
<p>React Portals를 사용하면 모달과 오버레이를 보다 시멘틱한 코드로 작성할 수 있다. Portals는 React 컴포넌트를 다른 DOM 노드에 렌더링할 수 있게 해주는데, 이를 통해 모달과 오버레이를 렌더링하는 동안, 다른 DOM 노드를 통해 의미론적인 계층을 유지할 수 있다.</p>
<p>먼저 <code>public/index.html</code> 파일에 모달을 렌더링할 DOM 노드를 추가한다.</p>
<pre><code class="language-html">&lt;!-- public/index.html --&gt;
&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;!-- ... --&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
    &lt;div id=&quot;modal-root&quot;&gt;&lt;/div&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>다음으로, <code>BaseModal</code>컴포넌트를 수정하여 <code>ReactDOM.createPortal()</code>함수를 사용하여 모달을 <code>modal-root</code>에 렌더링하도록 한다.</p>
<pre><code class="language-jsx">// src/components/helpers/BaseModal.jsx
import React, { useRef, useEffect } from &#39;react&#39;;
import ReactDOM from &#39;react-dom&#39;;
import PropTypes from &#39;prop-types&#39;;
import styled from &#39;styled-components&#39;;
import { palette } from &#39;../../styles/globalColor&#39;;

// ... styled components ...

const BaseModal = ({ isVisible, onClose, title, children }) =&gt; {
  const modalRef = useRef(null);

  // ... handleModalOutsideClick and useEffect ...

  if (!isVisible) {
    return null;
  }

  return ReactDOM.createPortal(
    (
      &lt;ModalContainer ref={modalRef}&gt;
        &lt;ModalTop&gt;
          &lt;CloseBtn
            onClick={onClose}
            xmlns=&#39;http://www.w3.org/2000/svg&#39;
            width=&#39;24&#39;
            height=&#39;24&#39;
            viewBox=&#39;0 0 24 24&#39;
          &gt;
            &lt;path d=&#39;m16.192 6.344-4.243 4.242-4.242-4.242-1.414 1.414L10.535 12l-4.242 4.242 1.414 1.414 4.242-4.242 4.243 4.242 1.414-1.414L13.364 12l4.242-4.242z&#39;&gt;&lt;/path&gt;
          &lt;/CloseBtn&gt;
          &lt;h2&gt;{title}&lt;/h2&gt;
        &lt;/ModalTop&gt;
        {children}
      &lt;/ModalContainer&gt;
    ),
    document.getElementById(&#39;modal-root&#39;)
  );
};

// ... propTypes ...

export default BaseModal;</code></pre>
<p>다음으로 오버레이 역시 동일한 방법으로 코드를 수정한다. </p>
<p>이제 더이상 모달과 오버레이가 app이 아닌 modal-root라는 선택자를 가진 div 태그 내부에 존재한다. </p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/a37a4828-1416-4ae3-98fa-fdc16ff0b4de/image.png" alt=""></p>
<p>이렇게 React Portals를 이용하면 외관상으로는 기존의 모달과 오버레이의 모습과 같지만 모달과 오버레이가 기존 코드보다 더 의미론적인 코드로 변경되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[children prop을 활용하여 컴포넌트 합성하기 ]]></title>
            <link>https://velog.io/@hoon-devlog/children-prop%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%ED%95%A9%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hoon-devlog/children-prop%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%ED%95%A9%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 01 May 2023 11:05:24 GMT</pubDate>
            <description><![CDATA[<p>이번에는 children prop을 활용하여  React 프로젝트에서 공통 모달 컴포넌트를 분리하고 각각의 컴포넌트를 합성해보자.</p>
<p>기존에는 서로 다른 두 개의 모달 컴포넌트, <code>LoginModal</code>과 <code>SignupModal</code> 가 각각 모달에 대한 로직 및 스타일을 내부에 정의하였다.</p>
<p>LoginModal
<img src="https://velog.velcdn.com/images/hoon-devlog/post/8c2e07da-4dbc-4cad-913f-3f25e96d4923/image.png" alt=""></p>
<p>SignupModal
<img src="https://velog.velcdn.com/images/hoon-devlog/post/2577a58f-4b17-449a-9470-178b2afdb3ea/image.png" alt=""></p>
<p>하지만 위의 이미지를 보면 <code>LoginModal</code>과 <code>SignupModal</code>은 중복되는 모달에 관련된 로직과 스타일이 많은 것을 볼 수 있다. 따라서<code>BaseModal</code>로 공통된 컴포넌트를 만는뒤 <code>BaseModal</code> 컴포넌트를 기반으로하여, <code>LoginModal</code>과  <code>SignupModal</code> 컴포넌트를 합성하기로 하였다. </p>
<p>이 과정에서 필요한 것이 바로 <code>children</code> prop이다. children prop은 React에서 일반적으로 사용되는 방법으로, 공통 컴포넌트를 분리하고 재사용하기 위해 사용된다.  <code>BaseModal</code>과 같은 공통 컴포넌트에서 <code>children</code> prop을 사용하면, 이 컴포넌트를 사용하는 곳에서 필요한 내용을 추가하거나 변경할 수 있다.</p>
<p>이제 프로젝트에 적용한 사례를 살펴 보자.</p>
<h2 id="프로젝트-적용-사례">프로젝트 적용 사례</h2>
<ol>
<li><code>BaseModal</code> 컴포넌트 생성</li>
</ol>
<p>먼저, 공통 모달 컴포넌트인 <code>BaseModal</code>을 생성한다. 이 컴포넌트는 공통된 모달의 레이아웃과 기본 기능을 가지며, 다른 모달 컴포넌트들이 이를 확장하여 사용할 수 있다.</p>
<pre><code class="language-jsx">// src/components/helpers/BaseModal.jsx

import React, { useRef, useEffect } from &#39;react&#39;;
import PropTypes from &#39;prop-types&#39;;
import styled from &#39;styled-components&#39;;
import { palette } from &#39;../../styles/globalColor&#39;;

// Styled-components를 사용한 스타일 정의 ...

const BaseModal = ({ isVisible, onClose, title, children }) =&gt; {
  const modalRef = useRef(null);

  const handleModalOutsideClick = event =&gt; {
    if (modalRef.current &amp;&amp; !modalRef.current.contains(event.target)) {
      onClose();
    }
  };

  useEffect(() =&gt; {
    if (isVisible) {
      document.addEventListener(&#39;mousedown&#39;, handleModalOutsideClick);
    } else {
      document.removeEventListener(&#39;mousedown&#39;, handleModalOutsideClick);
    }

    return () =&gt; {
      document.removeEventListener(&#39;mousedown&#39;, handleModalOutsideClick);
    };
  }, [isVisible]);

  if (!isVisible) {
    return null;
  }

  return (
    &lt;ModalContainer ref={modalRef}&gt;
      &lt;ModalTop&gt;
        &lt;CloseBtn onClick={onClose} ... &gt;&lt;/CloseBtn&gt;
        &lt;h2&gt;{title}&lt;/h2&gt;
      &lt;/ModalTop&gt;
      {children}
    &lt;/ModalContainer&gt;
  );
};

BaseModal.propTypes = {
  isVisible: PropTypes.bool.isRequired,
  onClose: PropTypes.func.isRequired,
  title: PropTypes.string.isRequired,
  children: PropTypes.node,
};

export default BaseModal;</code></pre>
<ol>
<li>SignupModal 컴포넌트에서 BaseModal 사용하기
이제  <code>BaseModal</code>의 <code>children</code> prop을 사용하여 <code>SignupModal</code>의 내용을 전달한다.</li>
</ol>
<pre><code class="language-jsx">// src/components/SignupModal/SignupModal.jsx

// import 내역들 ..

const SignupModal = () =&gt; {
  const isSignupModalVisible = useSelector(state =&gt; state.isSignupModalVisible);
  const dispatch = useDispatch();

  const handleHideSignupModal = () =&gt; {
    dispatch(hideSignupModal());
  };

  return (
    &lt;BaseModal
      isVisible={isSignupModalVisible}
      onClose={handleHideSignupModal}
      title=&#39;회원가입 완료하기&#39;
    &gt;
      기존 SignupModalContent 컴포넌트 내역들 ..       
    &lt;/BaseModal&gt;
  );
};

export default SignupModal;</code></pre>
<ol>
<li><code>LoginModal</code> 컴포넌트에서 <code>BaseModal</code> 사용하기</li>
</ol>
<pre><code class="language-jsx">// src/components/LoginModal/LoginModalStyle.jsx

// import 내역들 ..

const LoginModal = () =&gt; {
  const isLoginModalVisible = useSelector(state =&gt; state.isLoginModalVisible);
  const dispatch = useDispatch();

  const handleHideLoginModal = () =&gt; {
    dispatch(hideLoginModal());
  };

  return (
    &lt;BaseModal
      isVisible={isLoginModalVisible}
      onClose={handleHideLoginModal}
      title=&#39;로그인 또는 회원가입&#39;
    &gt;
      기존 LoginModalContent 컴포넌트 ...
       &lt;/BaseModal&gt;
  );
};

export default LoginModal;</code></pre>
<h2 id="결론">결론</h2>
<ol>
<li><code>BaseModal</code>이라는 공통 컴포넌트를 생성하여 모달의 레이아웃과 공통 기능을 추상화했다.</li>
<li><code>children</code> prop을 사용하여 <code>BaseModal</code>에 필요한 내용을 전달하여, 각각의 모달 컴포넌트에서 공통 레이아웃과 기능을 재사용할 수 있도록 했다.</li>
<li>이를 통해 코드의 중복을 줄이고, 유지 보수성을 향상시켰다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redux를 통해서 로그인 모달창 상태관리 하기]]></title>
            <link>https://velog.io/@hoon-devlog/Redux%EB%A5%BC-%ED%86%B5%ED%95%B4%EC%84%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%AA%A8%EB%8B%AC%EC%B0%BD-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hoon-devlog/Redux%EB%A5%BC-%ED%86%B5%ED%95%B4%EC%84%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%AA%A8%EB%8B%AC%EC%B0%BD-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 28 Apr 2023 10:13:09 GMT</pubDate>
            <description><![CDATA[<p>Redux를 통해서 로그인 모달창을 상태관리 하기전에 상태관리에 대해서 먼저 알아보자.</p>
<h2 id="상태관리란">상태관리란?</h2>
<p>어플리케이션에서 상태란, 사용자의 입력, 시스템의 이벤트 등의 외부적인 요소로 인해 변화되는 값들을 말한다. 상태관리는 이러한 값을 저장하고, 관리하며, 이러한 값이 변화될 때마다 어플리케이션의 다른 부분에도 그 변화를 반영한다.</p>
<h2 id="프로젝트에서-redux를-통해서-상태관리하게-된-이유">프로젝트에서 Redux를 통해서 상태관리하게 된 이유</h2>
<p>기존의 프로젝트의 상태관리는 Proxy와 옵저버 패턴을 통해서 진행하였다. 하지만 JavaScript 프로젝트에서 React프로젝트로 전환하고 프로젝트의 규모 역시 커지고 있는 상황이었기 때문에 Redux를 통해서 상태관리를 하기로 결정하였다.</p>
<h2 id="redux란-무엇인가">Redux란 무엇인가?</h2>
<p>Redux는 크로스 컴포넌트 또는 앱 와이드 상태를 위한 상태관리 시스템으로 상태를 변경하고 화면에 표시하는 데이터를 관리하도록 도와준다. </p>
<p>상태는 크게 3가지로 구분을 할 수 있다.</p>
<ol>
<li><p>Local State
로컬 상태는 데이터가 변경되어서 하나의 컴포넌트에 속하는 UI에 영향을 미치는 상태이다. 예를 들어 우리는 사용자 입력을 청취하고 useState를 사용해서 그 입력을 모든 키 입력과 함께 state 변수에 저장한다.보통 useState를 사용해서 컴포넌트 안에서 로컬 상태를 관리한다. </p>
</li>
<li><p>Cross-Component State
바로 크로스 컴포넌트 상태는 하나의 컴포넌트가 아니라 다수의 컴포넌트에 영향을 미치는 상태이다. 예를 들면 모달 오버레이를 열거나 닫는 버튼이 있다면 그런 모달 컴포넌트는 다수의 컴포넌트에 영향을 미칠 수 있다.</p>
</li>
<li><p>App-Wide State
앱 와이드 상태는 앱의 모든 컴포넌트에 영향을 미치는 상태도 이다. 예를 들면 사용자인증이 있다.</p>
</li>
</ol>
<p>리덕스는 앱의 중앙 저장소로, 앱의 모든 상태를 저장한다. 이를 통해 컴포넌트에서 저장소의 데이터를 사용할 수 있다. 예를 들어, 사용자 인증 상태가 변경되면 컴포넌트는 이를 감지하여 UI를 업데이트한다. 컴포넌트는 중앙 저장소와의 구독을 통해 데이터 변경을 인지한다.</p>
<p>데이터를 변경할 때는 컴포넌트가 직접 조작하지 않고, 리듀서라는 개념을 사용한다. 리듀서 함수는 데이터 변형을 처리하며, 입력을 받아 변환하고 축소하는 역할을 한다. </p>
<h2 id="redux를-프로젝트에-적용하기">Redux를 프로젝트에 적용하기</h2>
<ol>
<li>먼저 리덕스를 사용하기 위해 필요한 라이브러리들을 설치한다.</li>
</ol>
<pre><code>npm install redux react-redux redux-thunk</code></pre><ol start="2">
<li>이제 애플리케이션의 상태를 관리할 리덕스 스토어를 설정하고, 모달의 상태를 관리하는 액션 생성자와 리듀서를 작성한다.</li>
</ol>
<pre><code class="language-jsx">//src/store/actions.js:
export const SHOW_MODAL = &#39;SHOW_MODAL&#39;;
export const HIDE_MODAL = &#39;HIDE_MODAL&#39;;

export const showModal = () =&gt; ({
  type: SHOW_MODAL,
});

export const hideModal = () =&gt; ({
  type: HIDE_MODAL,
});
</code></pre>
<pre><code class="language-jsx">//src/store/reducer.js:
import { SHOW_MODAL, HIDE_MODAL } from &#39;./actions&#39;;

const initialState = {
  isModalVisible: false,
};

const reducer = (state = initialState, action) =&gt; {
  switch (action.type) {
    case SHOW_MODAL:
      return { ...state, isModalVisible: true };
    case HIDE_MODAL:
      return { ...state, isModalVisible: false };
    default:
      return state;
  }
};

export default reducer;
</code></pre>
<ol start="3">
<li>리덕스 스토어를 설정했으므로, 이제 <code>Provider</code> 컴포넌트를 사용하여 애플리케이션에 스토어를 연결한다. </li>
</ol>
<pre><code class="language-jsx">//src/store/index.js:
import { createStore, applyMiddleware } from &#39;redux&#39;;
import thunk from &#39;redux-thunk&#39;;
import rootReducer from &#39;./reducer&#39;;

const store = createStore(rootReducer, applyMiddleware(thunk));

export default store;
</code></pre>
<p><code>/src/store/index.js</code> 파일은 리덕스 스토어를 설정하고 생성하는 파일이며, 여기에서 스토어를 생성하고 미들웨어를 적용한 다음, 생성된 스토어를 다른 파일에서 사용할 수 있도록 내보낸다.</p>
<ol start="4">
<li>이제 리덕스 스토어에서 모달 상태를 관리할 수 있다. 이 상태를 사용하여 로그인 모달과 오버레이를 표시하거나 숨기는 로직을 구현할 수 있다.</li>
</ol>
<pre><code class="language-jsx">// src/index.js
import React from &#39;react&#39;;
import ReactDOM from &#39;react-dom&#39;;
import { Provider } from &#39;react-redux&#39;;
import store from &#39;./store&#39;;
import App from &#39;./App&#39;;

ReactDOM.render(
  &lt;React.StrictMode&gt;
    &lt;Provider store={store}&gt;
      &lt;App /&gt;
    &lt;/Provider&gt;
  &lt;/React.StrictMode&gt;,
  document.getElementById(&#39;root&#39;)
);
</code></pre>
<ol start="5">
<li>LoginModal 컴포넌트에서 useSelector와 useDispatch 함수를 사용하여 리덕스 상태를 가져오고, 액션 디스패치를 한다.</li>
</ol>
<pre><code class="language-jsx">// src/components/LoginModal/LoginModal.jsx
import React, { useEffect, useRef } from &#39;react&#39;;
import { useSelector, useDispatch } from &#39;react-redux&#39;;

import { hideModal } from &#39;../../store/actions&#39;;

const LoginModal = () =&gt; {
  const isModalVisible = useSelector(state =&gt; state.isModalVisible);
  const dispatch = useDispatch();
  const loginModalRef = useRef(null);

  const handleHideModal = () =&gt; {
    dispatch(hideModal());
  };

  const handleModalOutsideClick = event =&gt; {
    if (
      loginModalRef.current &amp;&amp;
      !loginModalRef.current.contains(event.target)
    ) {
      handleHideModal();
    }
  };

  useEffect(() =&gt; {
    if (isModalVisible) {
      document.addEventListener(&#39;mousedown&#39;, handleModalOutsideClick);
    } else {
      document.removeEventListener(&#39;mousedown&#39;, handleModalOutsideClick);
    }

    return () =&gt; {
      document.removeEventListener(&#39;mousedown&#39;, handleModalOutsideClick);
    };
  }, [isModalVisible]);

  if (!isModalVisible) {
    return null;
  }

  return (
    &lt;LoginModalContainer ref={loginModalRef}&gt;
      {/* 다른 컴포넌트들  */}
    &lt;/LoginModalContainer&gt;
  );
};

export default LoginModal;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[왜 Sass에서 Styled-components로 전환했을까?]]></title>
            <link>https://velog.io/@hoon-devlog/Sass%EC%97%90%EC%84%9C-Styled-components%EB%A1%9C-%EC%A0%84%ED%99%98%ED%95%9C-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@hoon-devlog/Sass%EC%97%90%EC%84%9C-Styled-components%EB%A1%9C-%EC%A0%84%ED%99%98%ED%95%9C-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Fri, 28 Apr 2023 09:47:55 GMT</pubDate>
            <description><![CDATA[<p>오늘은 프로젝트에서 스타일링 작업을 진행하면서 Sass에서 Styled-components로 전환한 이유에 대해서 알아보자.</p>
<h2 id="sass에서-styled-components로-스타일링-전환한-이유"><strong>Sass에서 Styled-components로 스타일링 전환한 이유</strong></h2>
<h3 id="1-코드-유지보수성">1. 코드 유지보수성</h3>
<p>Sass에서는 클래스명이나 ID명 등의 선택자를 사용하여 스타일을 적용하므로, 불필요한 중첩과 복잡도가 높아지는 단점이 있었다. 프로젝트 규모가 점점 더 복잡해지면서 클래스명이 중복되지 않도록 네이밍 작업을 진행했고 이는 나에게 있어서 매우 까다로운 작업이었다. 반면 Styled-components에서는 컴포넌트에 스타일을 적용하기 때문에 선택자의 복잡도를 줄이고, 컴포넌트의 구조와 일치하는 구조로 스타일을 관리할 수 있었다.</p>
<p>실제 프로젝트에서 styled-components를 통해서 스타일링 한 결과를 보자</p>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/5d5efdc1-dff3-4234-b1aa-f821da1bbefe/image.png" alt=""></p>
<p>각 요소의 클래스가 유니크한 값으로 들어가기 때문에 클래스명이 중복되는 문제를 방지할 수 있다. </p>
<h3 id="2-스타일-재사용성">2. 스타일 재사용성</h3>
<p>Sass에서는 mixin이나 extends 등의 기능을 사용하여 스타일을 재사용할 수 있지만, 재사용성이 떨어진다. 반면 Styled-components에서는 스타일을 컴포넌트 단위로 관리하기 때문에 스타일 재사용성이 높아 졌고 컴포넌트를 보다 적극적으로 활용할 수 있었다.</p>
<p>하지만 모든 기술에는 장단점이 있다. styled-components 사용시 주의해야할 점을 알아보자.</p>
<h2 id="styled-components-사용-시-주의점">Styled-components 사용 시 주의점</h2>
<h3 id="1-변수-사용">1. 변수 사용</h3>
<p>Sass에서는 변수를 사용하여 스타일 속성 값을 지정할 수 있다. 하지만 Styled-components에서는 Sass의 문법을 통해 변수를 사용할 수 없으며 아래의 예제처럼 JavaScript 객체를 사용하여 스타일 변수를 만들고, import해서 가져다 쓰는 방식을 이용해야 한다.</p>
<pre><code class="language-jsx">// theme.js
export const theme = {
  primaryColor: &quot;#3498db&quot;,
  secondaryColor: &quot;#2ecc71&quot;,
};

// MyStyledComponent.js
import styled from &quot;styled-components&quot;;
import { theme } from &quot;./theme&quot;;

const MyStyledComponent = styled.div`
  background-color: ${theme.primaryColor};
  color: ${theme.secondaryColor};
`;

export default MyStyledComponent;</code></pre>
<h3 id="2-중첩-구조">2. 중첩 구조</h3>
<p>Sass에서는 클래스를 중첩 구조로 작성하여 스타일을 적용할 수 있다. 하지만 Styled-components에서는 중첩 구조를 사용하지 않기를 권장하기 때문에 컴포넌트 별로 스타일을 모듈화하고 관리하는 것이 좋다.</p>
<h2 id="결론">결론</h2>
<p>Sass에서 Styled-components로 스타일링을 전환한 이유는 코드 유지보수성과 스타일 재사용성이 더 높아지기 때문었지만 Styled-components 사용 시 주의해야 할 점들도 존재한다. 이를 고려하여 프로젝트의 요구 사항과 코딩 스타일에 맞게 스타일링을 진행하는 것이 중요하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Proxy와 옵저버 패턴을 이용하여 전역상태 관리하기 ]]></title>
            <link>https://velog.io/@hoon-devlog/Proxy%EC%99%80-%EC%98%B5%EC%A0%80%EB%B2%84-%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EC%A0%84%EC%97%AD%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hoon-devlog/Proxy%EC%99%80-%EC%98%B5%EC%A0%80%EB%B2%84-%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EC%A0%84%EC%97%AD%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 28 Mar 2023 11:00:38 GMT</pubDate>
            <description><![CDATA[<h2 id="proxy와-옵저버-패턴이란-">Proxy와 옵저버 패턴이란 ?</h2>
<p>옵저버 패턴과 프록시는 서로 다른 패턴이지만, 종종 같이 사용되기도 한다. 프록시는 객체의 동작을 제어하거나 객체에 접근할 때 추가적인 동작을 수행하기 위해 사용되고, 옵저버 패턴은 객체의 상태 변화를 감지하고 상태 변화에 따른 추가적인 동작을 수행하기 위해 사용된다.</p>
<h2 id="proxy와-옵저버-패턴을-이용하여-모달창과-overlay-상태관리하기">Proxy와 옵저버 패턴을 이용하여 모달창과 overlay 상태관리하기</h2>
<p>이제 모달창과 overlay를 프록시와 옵저버 패턴을 이용해서 전역상태 관리를 진행해 보자.</p>
<p>먼저, 프록시를 이용해서 <code>state</code>라는 전역 상태 객체를 만든다. 이 객체에는 모달창이나 overlay와 같은 컴포넌트들의 상태를 저장한다.</p>
<pre><code class="language-jsx">// store.js

export const state = new Proxy(
  {
    modalVisible: false,
    overlayVisible: false
  },
  {
    // state의 속성값이 변경될 때마다 호출되는 set 메소드
    set(target, key, value) {
      // 속성값 변경
      target[key] = value;
      // 속성값 변경을 감지하는 옵저버들에게 알림
      notifyObservers(key, value);
      // 속성값 변경이 완료된 후 true를 반환
      return true;
    }
  }
);</code></pre>
<p>위 코드에서 <code>state</code>라는 Proxy 객체를 만들었으며, 이 객체에는 <code>modalVisible</code>과 <code>overlayVisible</code>이라는 두 개의 상태를 저장한다.</p>
<p>다음으로 Proxy 객체를 만들 때, <code>set</code> 핸들러를 추가했는데 이 핸들러는 프록시 객체의 속성이 변경될 때마다 호출된다. 이 핸들러에서는 변경된 속성의 이름과 값을 옵저버에게 알리는 <code>notifyObservers</code> 함수를 호출한다.</p>
<p>다음으로, 옵저버 패턴을 구현한다. 옵저버 패턴을 구현하기 위해서는 일반적으로 옵저버를 등록하고 삭제하는 함수와 옵저버에게 알리는 함수가 필요하다.</p>
<pre><code class="language-jsx">const observers = new Map();

function addObserver(key, observer) {
  if (!observers.has(key)) {
    observers.set(key, []);
  }
  observers.get(key).push(observer);
}

// export function removeObserver(key, observer) {
//   // 옵저버 리스트가 존재하는지 확인
//   if (observers.has(key)) {
//     const observerList = observers.get(key);
//     // 해당 옵저버가 존재하는 인덱스를 찾아 제거
//     const index = observerList.indexOf(observer);
//     if (index !== -1) {
//       observerList.splice(index, 1);
//     }
//   }
// }

function notifyObservers(key, value) {
  if (observers.has(key)) {
    const observerList = observers.get(key);
    observerList.forEach(observer =&gt; observer(value));
  }
}</code></pre>
<p>위 코드에서는 <code>observers</code>라는 맵을 만들어서 옵저버를 저장한다. <code>addObserver</code> 함수는 <code>observers</code> 맵에 옵저버를 등록하는 함수이다. 만약 해당 속성에 등록된 옵저버가 없다면, 새로운 배열을 만들어서 옵저버를 추가한다.</p>
<p> <code>removeObserver</code> 함수는 <code>observers</code> 맵에서 옵저버를 삭제하는 함수이다. <code>notifyObservers</code> 함수는 <code>set</code> 핸들러에서 호출되는 함수로, 해당 속성에 등록된 옵저버에게 변경된 값을 알려준다.</p>
<p>하지만 <code>removeObserver</code>는 React와 같은 라이브러리나 프레임워크에서 사용되는 개념이기 때문에, 바닐라 자바스크립트에서는 필요하지 않기 때문에 삭제 하도록 한다.</p>
<p>결과적으로 <code>store.js</code> 파일에서 프록시와 옵저버 패턴을 이용해서 전역 상태 관리를 구현하고, <code>Overlay.js</code>, <code>LoginModal.js</code>, <code>Header.js</code> 파일에서 해당 상태를 사용하도록 구현할 수 있다.</p>
<h2 id="각-컴포넌트에서-proxy와-옵저버-패턴-활용하기">각 컴포넌트에서 Proxy와 옵저버 패턴 활용하기</h2>
<ol>
<li>app.js에서 상태 변경 감지</li>
</ol>
<p>우선 <code>app.js</code>에 옵저버 함수가 관찰하는 <code>state</code>의 객체 중에서 <code>modalVisible</code>와 <code>overlayVisible</code>가 true일때는 모달창과 <code>overlay</code>가 보이도록, <code>false</code>일 때는 보이지 않는 로직을 구현한다. <code>app.js</code>에서 구현하는 이유는 모달창과 overlay는 프로젝트 전역에 걸쳐서 사용될 가능성이 크기 때문이다. 이렇게 하면 <code>state</code>가 변경될때마다 그에 맞는 로직이 프로젝트 전반에 걸쳐 실행된다. </p>
<pre><code class="language-jsx">// src/components/app.js
...

import { addObserver } from &#39;../../store&#39;;

const App = () =&gt; {
  const router = Router();

  const renderApp = () =&gt; {

...

    const observer = value =&gt; {
      if (value) {
        loginModal.classList.remove(&#39;hidden&#39;);
        overlay.classList.remove(&#39;hidden&#39;);
        document.body.classList.add(&#39;no-scroll&#39;);
      } else {
        loginModal.classList.add(&#39;hidden&#39;);
        overlay.classList.add(&#39;hidden&#39;);
        document.body.classList.remove(&#39;no-scroll&#39;);
      }
    };

    addObserver(&#39;modalVisible&#39;, observer);
    addObserver(&#39;overlayVisible&#39;, observer);
  };

  const onPopState = () =&gt; {
    renderApp();
  };

...

  const app = { renderApp };

  return app;
};

export default App;
</code></pre>
<ol start="2">
<li>Header.js에서 상태 변경 해주기</li>
</ol>
<pre><code class="language-jsx">
import &#39;./Header.scss&#39;;

...
import { state } from &#39;../../../store&#39;;

const Header = () =&gt; {
 ...
 ...

  profileContainer.addEventListener(&#39;click&#39;, () =&gt; {
    moreInfo.classList.toggle(&#39;hidden&#39;);
  });

  loginTab.addEventListener(&#39;click&#39;, () =&gt; {
    state.modalVisible = true;
    state.overlayVisible = true;
  });

  signupTab.addEventListener(&#39;click&#39;, () =&gt; {
    state.modalVisible = true;
    state.overlayVisible = true;
  });

  return header;
};

export default Header;</code></pre>
<ol start="3">
<li>LoginModal.js에서 상태 변경해주기</li>
</ol>
<pre><code class="language-jsx">import &#39;./LoginModal.scss&#39;;
import { state } from &#39;../../../store&#39;;

const LoginModal = () =&gt; {
  ...
  window.addEventListener(&#39;click&#39;, event =&gt; {
    if (
      event.target === overlay ||
      event.target.closest(&#39;.container-login-modal&#39;) === null
    ) {
      state.modalVisible = false;
      state.overlayVisible = false;
    }
  });

  return loginModal;
};

export default LoginModal;</code></pre>
<ol start="4">
<li>Overlay.js에서 상태 변경해주기</li>
</ol>
<pre><code class="language-jsx">import &#39;./Overlay.scss&#39;;
import { state } from &#39;../../../store&#39;;

const Overlay = () =&gt; {
  const app = document.getElementById(&#39;app&#39;);

  const overlay = document.createElement(&#39;div&#39;);
  overlay.classList.add(&#39;overlay&#39;, &#39;hidden&#39;);
  app.appendChild(overlay);

  overlay.addEventListener(&#39;click&#39;, () =&gt; {
    state.modalVisible = false;
    state.overlayVisible = false;
  });

  return overlay;
};

export default Overlay;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[mermaid를 활용해서 flow chart 만들기]]></title>
            <link>https://velog.io/@hoon-devlog/mermaid%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%B4%EC%84%9C-flow-chart-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@hoon-devlog/mermaid%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%B4%EC%84%9C-flow-chart-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sun, 26 Mar 2023 10:47:29 GMT</pubDate>
            <description><![CDATA[<p>해당 프로젝트에서는 mermaid로 flow chart를 작성하기로 하였다. 그 이유는 meramaid의 간단하고 직관적인 문법을 바탕으로 쉽게 작성할 수 있고 그림으로서 시각적인 효과를 가지고 있어서 프로젝트가 어떤 흐름으로 구성되는지 한눈에 파악할 수 있기 때문이다.</p>
<p>또한, 내가 mermaid로 flow chart를 작성하계된 가장 큰 계기는 바로 유지보수의 용이성과 확장가능성 때문이다. Power Point 등을 활용하여 flow chart를 직접 작성 했을때는 로직이 변경되거나 확장될때 기존의 flow chart를 대거 수정해야하는 경우가 빈번히 발생하였다 반면, mermaid를 활용했을 때는 문법에 맞게 작성한다면 그에 맞는 flow chart를 그림 형태로 직접만들어 주기 때문에 flow chart를 수정하거나 새로운 로직을 추가할 경우에도 해당 부분만 간단히 수정하면 flow chart가 완성 되었기 때문이다.</p>
<p>다음은 이번 프로젝트에서 사용할 flow chart이다.</p>
<pre><code class="language-mermaid">graph TD
A[시작] --&gt; B[홈 화면]
B --&gt; C[카페 목록]
B -- 로그인/회원가입 탭 --&gt; D[로그인 또는 회원가입 모달]
C --&gt; E[카페 작성]
C --&gt; F[카페 상세페이지]
C --&gt; G[로그아웃]
F --&gt; H[카페 수정]
F --&gt; I[카페 삭제]
D --&gt; J[로그인]
D --&gt; K[회원가입]
J --&gt; L{로그인 성공 여부}
K --&gt; M{회원가입 성공 여부}
L -- 성공 --&gt; C
L -- 실패 --&gt; J
M -- 성공 --&gt; J
M -- 실패 --&gt; K
G --&gt; B</code></pre>
<p><img src="https://velog.velcdn.com/images/hoon-devlog/post/02178937-6ed6-497e-ac17-a1ffdbceeb3d/image.png" alt=""></p>
<p>위 flow chart는 카페 목록을 보여주는 홈 화면에서 로그인/회원가입 탭을 누르면 나타나는 로그인 또는 회원가입 모달을 통해 로그인 또는 회원가입을 할 수 있도록 구성되어 있다.</p>
<p>로그인 성공 여부와 회원가입 성공 여부에 따라 로그인 또는 회원가입 모달을 띄우며, 로그인에 성공하면 카페 목록 화면으로 이동하게 되고, 로그인에 실패하면 다시 로그인 모달이 띄워진다. 회원가입에 성공하면 로그인 모달이 띄워지며, 회원가입에 실패하면 다시 회원가입 모달이 띄워진다.</p>
<p>홈화면에서는 카페를 작성할 수 있으며, 각 카페에 대한 상세 페이지를 볼 수 있습니다. 또한 로그아웃 버튼을 통해 로그아웃을 할 수 있다. 카페 상세 페이지에서는 해당 카페를 수정하거나 삭제할 수 있고 로그아웃 후 다시 홈 화면으로 돌아간다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SPA 초기 설계 리팩터링]]></title>
            <link>https://velog.io/@hoon-devlog/SPA-%EC%B4%88%EA%B8%B0-%EC%84%A4%EA%B3%84-%EB%A6%AC%ED%8C%A9%ED%84%B0%EB%A7%81</link>
            <guid>https://velog.io/@hoon-devlog/SPA-%EC%B4%88%EA%B8%B0-%EC%84%A4%EA%B3%84-%EB%A6%AC%ED%8C%A9%ED%84%B0%EB%A7%81</guid>
            <pubDate>Wed, 22 Mar 2023 03:33:53 GMT</pubDate>
            <description><![CDATA[<h2 id="1-불필요한-import-제거">1. 불필요한 import 제거</h2>
<p>변경전</p>
<pre><code class="language-jsx">// src/app.js

import Router from &#39;./router.js&#39;;
import App from &#39;./components/app.js&#39;;

// 라우터 초기화

Router();

export default App;</code></pre>
<p>변경후</p>
<pre><code class="language-jsx">// src/app.js

import App from &#39;./components/app.js&#39;;

export default App;</code></pre>
<p>위 코드에서 Router.js 파일에서 이미 Router 객체를 export하고 있으므로, App.js에서 다시 import하는 것은 불필요한 작업이다.</p>
<p>이 작업을 수행하면, Router.js 파일에서 이미 만들어진 Router 객체를 다시 만들게 되어 메모리 사용량이 증가하고, 불필요한 리소스 낭비가 발생한다.</p>
<p>따라서, Router.js 파일에서 이미 만들어진 Router 객체를 사용하는 것이 바람직하며, 이를 위해서는 App.js에서 Router를 import하는 코드를 제거하였고. 결과적으로 코드를 보다 간결하게 유지할 수 있고, 성능 문제를 예방할 수 있었다.</p>
<h2 id="2-불필요한-이벤트-핸들러-제거와-변수명-함수명-변경">2. 불필요한 이벤트 핸들러 제거와 변수명, 함수명 변경</h2>
<p>변경전</p>
<pre><code class="language-jsx">// src/components/app.js

import Router from &#39;../router&#39;;
import Header from &#39;./Header/Header&#39;;
import Footer from &#39;./Footer/Footer&#39;;
import routes from &#39;../routes&#39;; 

const App = () =&gt; {
  const router = new Router();

  const render = () =&gt; {
    const appElement = document.getElementById(&#39;app&#39;);

    appElement.innerHTML = `
      ${Header()}
      ${router.getComponent()()} 
      ${Footer()}
    `;
  };

  window.addEventListener(&#39;load&#39;, render);
  window.addEventListener(&#39;popstate&#39;, render);
  // router.init();

  const app = {
    render
  };

  return app;
};

export default App;

// 라우터 초기화
// const router = new Router();
// router.init();</code></pre>
<p>변경후</p>
<pre><code class="language-jsx">// src/components/app.js

import Router from &#39;../router&#39;;
import Header from &#39;./Header/Header&#39;;
import Footer from &#39;./Footer/Footer&#39;;
import routes from &#39;../routes&#39;;

const App = () =&gt; {
  const router = new Router();

  const renderApp = () =&gt; {
    const appElement = document.getElementById(&#39;app&#39;);

    appElement.innerHTML = `
      ${Header()}
      ${router.getComponent()()} 
      ${Footer()}
    `;
  };

  const onPopState = () =&gt; {
    render();
  };

  window.addEventListener(&#39;load&#39;, renderApp);
  window.addEventListener(&#39;popstate&#39;, onPopState);

  const app = { renderApp };

  return app;
};

export default App;</code></pre>
<ol>
<li>변수명, 함수명 변경</li>
</ol>
<p>App.js에서 render() 함수는 App 객체 내에 존재한다. 이때, 함수 이름이 의미를 명확하게 전달하지 못할 경우, 코드를 이해하거나 유지보수하기 어려워질 수 있다.</p>
<p>예를 들어, 현재 코드에서 render() 함수는 App 객체에 종속되어 있으며, 함수 이름만으로는 그 역할이 명확하지 않기 때문에, 함수 이름을 보다 명확한 이름으로 변경하여 코드를 이해하기 쉽게 만들어야 한다.</p>
<ol start="2">
<li>불필요한 이벤트 핸들러 제거</li>
</ol>
<p>app.js에서 <strong><code>load</code></strong> 이벤트와 <strong><code>popstate</code></strong> 이벤트에 모두 <strong><code>render()</code></strong> 함수를 등록하면, 같은 작업이 중복으로 실행될 수 있다.</p>
<p>따라서, <strong><code>load</code></strong> 이벤트와 <strong><code>popstate</code></strong> 이벤트에 각각 다른 함수를 등록하여, 필요한 작업을 수행할 수 있도록 해야한다.</p>
<p><strong><code>onPopState</code></strong> 함수에서는 <strong><code>popstate</code></strong> 이벤트가 발생할 때 <strong><code>render()</code></strong> 함수를 호출한단. 이 때 <strong><code>render()</code></strong> 함수를 바로 호출하지 않고, <strong><code>onPopState</code></strong> 함수를 중간에 두는 것은, 나중에 <strong><code>popstate</code></strong> 이벤트가 발생했을 때 추가적인 작업이 필요할 경우 <strong><code>onPopState</code></strong> 함수에서 처리할 수 있도록 하기 위해서이다.</p>
<p>즉, <strong><code>render()</code></strong> 함수를 호출하는 것은 <strong><code>onPopState</code></strong> 함수와 <strong><code>load</code></strong> 이벤트에서 모두 수행되지만, 각각의 이벤트에서 추가적인 작업이 필요한 경우에는 <strong><code>onPopState</code></strong> 함수에서 처리하면 된다. 이를 통해 중복 코드를 제거하고, 가독성을 높일 수 있다.</p>
]]></description>
        </item>
    </channel>
</rss>