<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Singco Dev</title>
    <link>https://singcotive.tistory.com/</link>
    <description>노래하고 코딩하는 저의 즐거운 히스토리를 공유합니다.</description>
    <language>ko</language>
    <pubDate>Mon, 15 Jun 2026 10:40:23 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>씽코</managingEditor>
    <image>
      <title>Singco Dev</title>
      <url>https://tistory1.daumcdn.net/tistory/6408856/attach/f7a40acc0a844e3d8753b8fb738e658d</url>
      <link>https://singcotive.tistory.com</link>
    </image>
    <item>
      <title>LLM 활용 사례</title>
      <link>https://singcotive.tistory.com/entry/LLM-%ED%99%9C%EC%9A%A9-%EC%82%AC%EB%A1%80</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 이마트 프로젝트에서 LLM을 활용하는 사례에 대해 크게 4가지로 구분하여 공유드리고자 합니다. LLM은 개발 생산성 향상, 코드 품질 개선, 유지보수성 강화 등 다양한 측면에서 긍정적인 영향을 미치고 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;e2e 테스트 with Playwright MCP&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Playwright는 멀티 브라우저 환경에서 시각화 테스트를 지원하는 강력한 테스트 라이브러리입니다. 저희는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Playwright MCP(대화형 모드)&lt;/b&gt;&lt;/span&gt;를 활용하여 자연어 시나리오를 기반으로 e2e 테스트 코드를 작성하고, 이를 통해 테스트 유지보수성을 크게 높였습니다.&lt;br /&gt;&lt;br /&gt;이마트앱 와인그랩을 통해 와인을 구매하는 시나리오를 예시로 들어보겠습니다. 시나리오는 다음과 같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;와인그랩 이동 -&amp;gt; 상품 선택 -&amp;gt; 상품 수량 변경 -&amp;gt; 결제페이지 이동 -&amp;gt; 픽업일 선택 -&amp;gt; 결제하기&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;시나리오 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 Cursor와 진행한 실제 대화 내용입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;e2e 테스트를 진행할거야. 시나리오는 아래와 같아. 대화형(MCP)로 진행해.&lt;br /&gt;&lt;br /&gt;1.&amp;nbsp;`이마트도메인`&amp;nbsp;으로&amp;nbsp;이동&lt;br /&gt;2.&amp;nbsp;와인&amp;nbsp;상품&amp;nbsp;하나를&amp;nbsp;선택해서&amp;nbsp;상품&amp;nbsp;상세&amp;nbsp;페이지로&amp;nbsp;이동&lt;br /&gt;3.&amp;nbsp;하단에&amp;nbsp;`바로구매`&amp;nbsp;버튼&amp;nbsp;클릭&lt;br /&gt;4.&amp;nbsp;하단&amp;nbsp;팝업에서&amp;nbsp;`+`&amp;nbsp;버튼&amp;nbsp;클릭해서&amp;nbsp;상품&amp;nbsp;수량을&amp;nbsp;3으로&amp;nbsp;설정&lt;br /&gt;5.&amp;nbsp;환불처리에&amp;nbsp;대한&amp;nbsp;팝업이&amp;nbsp;뜨면&amp;nbsp;`확인`&amp;nbsp;버튼&amp;nbsp;클릭&lt;br /&gt;6.&amp;nbsp;픽업일&amp;nbsp;선택&lt;br /&gt;7.&amp;nbsp;여러&amp;nbsp;픽업일&amp;nbsp;중&amp;nbsp;하나를&amp;nbsp;선택&lt;br /&gt;8. 하단에 결제하기 버튼 클릭&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;br /&gt;&lt;br /&gt;알겠습니다.&amp;nbsp;사용자의&amp;nbsp;요청에&amp;nbsp;따라&amp;nbsp;대화형&amp;nbsp;모드에서&amp;nbsp;시나리오를&amp;nbsp;다시&amp;nbsp;시작하겠습니다.&lt;br /&gt;&lt;br /&gt;1.&amp;nbsp;`이마트&amp;nbsp;와인그랩&amp;nbsp;페이지`&amp;nbsp;페이지로&amp;nbsp;이동합니다.&lt;br /&gt;Called&amp;nbsp;browser_navigate&lt;br /&gt;&lt;br /&gt;2.&amp;nbsp;예상대로&amp;nbsp;로그인&amp;nbsp;페이지로&amp;nbsp;이동되지&amp;nbsp;않고,&amp;nbsp;&quot;로그인&amp;nbsp;후&amp;nbsp;이용할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;서비스입니다&quot;&amp;nbsp;라는&amp;nbsp;문구가&amp;nbsp;표시되었습니다.&amp;nbsp;이&amp;nbsp;대화형&amp;nbsp;모드에서는&amp;nbsp;`auth.json`이&amp;nbsp;적용되지&amp;nbsp;않는&amp;nbsp;것이&amp;nbsp;명확합니다.&lt;br /&gt;이 문제를 해결하고 시나리오를 진행하기 위해, 화면에 보이는 &quot;로그인 하기&quot; 링크를 클릭하여 수동으로 로그인 페이지로 이동하겠습니다.&lt;br /&gt;Called&amp;nbsp;browser_click&lt;br /&gt;&lt;br /&gt;3.&amp;nbsp;로그인&amp;nbsp;페이지로&amp;nbsp;이동했습니다.&amp;nbsp;이제&amp;nbsp;아이디와&amp;nbsp;비밀번호를&amp;nbsp;입력하고&amp;nbsp;로그인&amp;nbsp;버튼을&amp;nbsp;클릭하겠습니다.&amp;nbsp;아이디(`개발기&amp;nbsp;ID`)를&amp;nbsp;입력합니다.&lt;br /&gt;Called&amp;nbsp;browser_type&lt;br /&gt;&lt;br /&gt;4.&amp;nbsp;이제&amp;nbsp;비밀번호(`개발기&amp;nbsp;PW`)를&amp;nbsp;입력합니다.&lt;br /&gt;Called&amp;nbsp;browser_type&lt;br /&gt;&lt;br /&gt;5.&amp;nbsp;로그인&amp;nbsp;버튼을&amp;nbsp;클릭합니다.&lt;br /&gt;Called&amp;nbsp;browser_click&lt;br /&gt;&lt;br /&gt;6. 로그인 후 원래 페이지인 와인 페이지로 돌아왔습니다. 이제 시나리오의 다음 단계인 상품 선택을 진행하겠습니다.&lt;br /&gt;페이지에&amp;nbsp;여러&amp;nbsp;와인&amp;nbsp;상품이&amp;nbsp;보이는데,&amp;nbsp;이&amp;nbsp;중&amp;nbsp;첫&amp;nbsp;번째&amp;nbsp;상품을&amp;nbsp;선택하여&amp;nbsp;상세&amp;nbsp;페이지로&amp;nbsp;이동하겠습니다.&amp;nbsp;스냅샷을&amp;nbsp;확인하여&amp;nbsp;클릭할&amp;nbsp;요소를&amp;nbsp;찾겠습니다.&lt;br /&gt;Called&amp;nbsp;browser_snapshot&lt;br /&gt;&lt;br /&gt;7.&amp;nbsp;스냅샷을&amp;nbsp;확인하니,&amp;nbsp;현재&amp;nbsp;페이지에는&amp;nbsp;상품&amp;nbsp;목록이&amp;nbsp;바로&amp;nbsp;보이지&amp;nbsp;않고&amp;nbsp;&quot;와인&amp;nbsp;클럽&quot;&amp;nbsp;안내&amp;nbsp;가이드가&amp;nbsp;먼저&amp;nbsp;표시되고&amp;nbsp;있습니다.&lt;br /&gt;시나리오 진행을 위해 먼저 &quot;가이드 레이어 닫기&quot; 버튼을 클릭하여 상품 목록을 표시하겠습니다.&lt;br /&gt;Called&amp;nbsp;browser_click&lt;br /&gt;&lt;br /&gt;8.&amp;nbsp;가이드&amp;nbsp;레이어를&amp;nbsp;닫으니&amp;nbsp;이제&amp;nbsp;상품&amp;nbsp;목록이&amp;nbsp;보입니다.&lt;br /&gt;시나리오에 따라, &quot;좋아하실 만한 와인을 추천해 드릴게요&quot; 섹션에 있는 첫 번째 상품인 &quot;코디치 로쏘 스위트&quot;를 클릭하여 상세 페이지로 이동하겠습니다.&lt;br /&gt;Called&amp;nbsp;browser_click&lt;br /&gt;&lt;br /&gt;9. 상품 상세 페이지로 이동했습니다. 이제 시나리오에 따라 하단에 있는 &quot;바로구매&quot; 버튼을 클릭하여 구매 옵션 팝업을 띄우겠습니다.&lt;br /&gt;Called&amp;nbsp;browser_click&lt;br /&gt;&lt;br /&gt;10.&amp;nbsp;구매&amp;nbsp;팝업이&amp;nbsp;성공적으로&amp;nbsp;나타났습니다.&lt;br /&gt;이제 시나리오에 따라, 상품 수량을 3으로 만들기 위해 &quot;+&quot; 버튼(`늘리기` 버튼)을 두 번 클릭하겠습니다. 현재 수량은 1입니다.&lt;br /&gt;첫&amp;nbsp;번째로&amp;nbsp;`늘리기`&amp;nbsp;버튼을&amp;nbsp;클릭합니다.&lt;br /&gt;Called&amp;nbsp;browser_click&lt;br /&gt;&lt;br /&gt;11.&amp;nbsp;수량이&amp;nbsp;2로&amp;nbsp;변경되었습니다.&amp;nbsp;다시&amp;nbsp;한번&amp;nbsp;`늘리기`&amp;nbsp;버튼을&amp;nbsp;클릭하여&amp;nbsp;수량을&amp;nbsp;3으로&amp;nbsp;만들겠습니다.&lt;br /&gt;Called&amp;nbsp;browser_click&lt;br /&gt;&lt;br /&gt;12. 수량이 3으로 변경되었습니다. 이제 팝업창에 있는 &quot;바로구매&quot; 버튼을 클릭하여 주문서 작성 페이지로 이동하겠습니다.&lt;br /&gt;Called&amp;nbsp;browser_click&lt;br /&gt;&lt;br /&gt;13. 주문 확인 팝업이 나타났습니다. &quot;확인&quot; 버튼을 눌러 계속 진행하겠습니다.&lt;br /&gt;Called&amp;nbsp;browser_click&lt;br /&gt;&lt;br /&gt;14.&amp;nbsp;픽업일&amp;nbsp;선택&amp;nbsp;페이지로&amp;nbsp;이동했습니다.&amp;nbsp;시나리오의&amp;nbsp;다음&amp;nbsp;단계는&amp;nbsp;픽업&amp;nbsp;날짜를&amp;nbsp;선택하는&amp;nbsp;것입니다.&lt;br /&gt;&quot;픽업일 선택&quot; 버튼을 클릭하여 날짜 선택 캘린더를 열겠습니다.&lt;br /&gt;Called&amp;nbsp;browser_click&lt;br /&gt;&lt;br /&gt;15. 픽업 날짜 목록이 나타났습니다. 목록에 있는 첫 번째 날짜인 &quot;2025.06.26 목&quot;을 선택하겠습니다.&lt;br /&gt;Called&amp;nbsp;browser_click&lt;br /&gt;&lt;br /&gt;16.&amp;nbsp;픽업&amp;nbsp;날짜가&amp;nbsp;선택되었습니다.&lt;br /&gt;이제 마지막 단계로, 화면 하단에 있는 &quot;결제하기&quot; 버튼을 클릭하여 결제 페이지로 이동하겠습니다.&lt;br /&gt;Called&amp;nbsp;browser_click&lt;br /&gt;&lt;br /&gt;17.&amp;nbsp;&quot;결제하기&quot;&amp;nbsp;버튼을&amp;nbsp;클릭한&amp;nbsp;후&amp;nbsp;페이지가&amp;nbsp;이동하면서&amp;nbsp;오류가&amp;nbsp;발생했습니다.&amp;nbsp;이는&amp;nbsp;페이지가&amp;nbsp;이동하는&amp;nbsp;동안&amp;nbsp;이전&amp;nbsp;페이지의&amp;nbsp;컨텍스트가&amp;nbsp;사라져서&amp;nbsp;발생하는&amp;nbsp;일반적인&amp;nbsp;현상입니다.&lt;br /&gt;현재&amp;nbsp;페이지는&amp;nbsp;결제&amp;nbsp;페이지일&amp;nbsp;것으로&amp;nbsp;예상됩니다.&amp;nbsp;스냅샷을&amp;nbsp;통해&amp;nbsp;현재&amp;nbsp;페이지를&amp;nbsp;확인하고&amp;nbsp;다음&amp;nbsp;단계를&amp;nbsp;진행하겠습니다.&lt;br /&gt;Called&amp;nbsp;browser_snapshot&lt;br /&gt;&lt;br /&gt;18.&amp;nbsp;결제&amp;nbsp;페이지로&amp;nbsp;성공적으로&amp;nbsp;이동했습니다.&lt;br /&gt;&lt;br /&gt;혹시&amp;nbsp;추가적으로&amp;nbsp;필요한&amp;nbsp;작업이&amp;nbsp;있으신가요?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Playwright&amp;nbsp;MCP를&amp;nbsp;통해&amp;nbsp;시나리오를&amp;nbsp;진행한&amp;nbsp;후,&amp;nbsp;Cursor는&amp;nbsp;`@test.mdc`&amp;nbsp;컨벤션에&amp;nbsp;맞춰&amp;nbsp;테스트&amp;nbsp;코드를&amp;nbsp;자동으로&amp;nbsp;생성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1752651010871&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { test, expect } from '@playwright/test';

test.describe('와인 구매', () =&amp;gt; {
    test.use({ storageState: 'test/mocks/auth.json' });

    test('와인 상품을 선택하고 수량을 조절하여 결제 페이지로 이동한다', async ({ page }) =&amp;gt; {
        await test.step('와인 메인 페이지로 이동', async () =&amp;gt; {
            await page.goto('/webapp/wine');
            await expect(page).toHaveURL(/.*\/webapp\/wine/);
        });

        await test.step('상품 상세 페이지로 이동', async () =&amp;gt; {
            await page.getByRole('button', { name: '가이드 레이어 닫기' }).click();

            await page.getByRole('article').first().click();

            await expect(page).toHaveURL(/.*\/wine\/view/);
        });

        await test.step('구매 팝업에서 수량 조절 및 구매하기', async () =&amp;gt; {
            await page.getByRole('button', { name: '바로구매' }).click();

            await page.getByRole('button', { name: '늘리기' }).click();
            await page.getByRole('button', { name: '늘리기' }).click();

            await page.getByRole('dialog').getByRole('button', { name: '바로구매' }).click();

            await page.getByRole('button', { name: '확인' }).click();
        });

        await test.step('결제 페이지에서 픽업일 선택', async () =&amp;gt; {
            await page.getByRole('button', { name: '픽업일 선택' }).click();

            await page.getByRole('dialog').getByRole('button', { name: /\d/ }).first().click();
        });

        await test.step('결제하기 버튼 클릭 및 페이지 이동 확인', async () =&amp;gt; {
            await page.getByRole('button', { name: '결제하기' }).click();

            await expect(page).toHaveURL(/.*\/payment/);
        });
    });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이처럼&amp;nbsp;Playwright&amp;nbsp;MCP를&amp;nbsp;활용하면&amp;nbsp;비교적&amp;nbsp;빠른&amp;nbsp;속도로&amp;nbsp;e2e&amp;nbsp;테스트를&amp;nbsp;브라우저에서&amp;nbsp;직접&amp;nbsp;확인하고,&amp;nbsp;이를&amp;nbsp;테스트&amp;nbsp;코드로&amp;nbsp;자동&amp;nbsp;생성할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 초기 단계부터 사용자 경험을 고려한 시나리오 기반 테스트를 자동화함으로써, 서비스 출시 전 발생할 수 있는 주요 결함을 사전에 발견하고 수정하는 데 기여합니다. 또한, 코드 리팩토링이나 기능 변경 시에도 기존 기능의 회귀를 즉각적으로 감지하여 안정적인 서비스 유지보수를 가능하게 합니다. QA 의존도를 낮추고, 개발팀의 전반적인 생산성을 높이는 효과를 기대할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요구사항 구현 with TDD&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 이번 홈 팝업 배너 작업을 TDD(테스트 주도 개발) 워크플로우로 구현하였습니다. 이는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;e2e 테스트 with Playwright MCP &lt;/b&gt;&lt;/span&gt;와 마찬가지로, 요구사항을 TDD 플로우로 구현하는 자체 Rule을 작성하고 이를 참조하도록 설정한 결과입니다.&lt;br /&gt;&lt;br /&gt;먼저 요구사항을 작성하기 전 &lt;b&gt;홈 배너&lt;/b&gt;에 대한 인터페이스를 미리 구현해두었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;@NewMemberBanner.vue는 @promotion.d.ts 의 NewMemberTopBanner 인터페이스로 구성돼.&lt;br /&gt;&lt;br /&gt;홈&amp;nbsp;상단&amp;nbsp;배너는&amp;nbsp;NewMemberTopBanner의&amp;nbsp;url&amp;nbsp;유무에&amp;nbsp;따라&amp;nbsp;동작이&amp;nbsp;달라.&lt;br /&gt;1.&amp;nbsp;url&amp;nbsp;있을&amp;nbsp;때,&amp;nbsp;링크&amp;nbsp;이동&lt;br /&gt;2. url 없을 때, sheetBannerList로 @LayerPopup.vue , @SlideBanner.vue 를 활용한 하단 팝업을 활성화&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;br /&gt;안녕하세요.&amp;nbsp;`NewMemberBanner.vue`&amp;nbsp;컴포넌트의&amp;nbsp;동작&amp;nbsp;로직&amp;nbsp;구현&amp;nbsp;요청을&amp;nbsp;확인했습니다.&amp;nbsp;`{미리&amp;nbsp;정의한&amp;nbsp;rule}`&amp;nbsp;TDD&amp;nbsp;워크플로우에&amp;nbsp;따라&amp;nbsp;먼저&amp;nbsp;테스트&amp;nbsp;코드를&amp;nbsp;작성하고&amp;nbsp;기능을&amp;nbsp;구현하겠습니다.&lt;br /&gt;&lt;br /&gt;**1단계:&amp;nbsp;요구사항&amp;nbsp;분석&amp;nbsp;및&amp;nbsp;테스트&amp;nbsp;계획**&lt;br /&gt;&lt;br /&gt;**요구사항**&lt;br /&gt;-&amp;nbsp;`NewMemberBanner.vue`&amp;nbsp;컴포넌트는&amp;nbsp;`NewMemberTopBanner`&amp;nbsp;타입의&amp;nbsp;객체를&amp;nbsp;props로&amp;nbsp;받습니다.&lt;br /&gt;-&amp;nbsp;prop으로&amp;nbsp;받은&amp;nbsp;데이터에&amp;nbsp;`url`&amp;nbsp;속성이&amp;nbsp;있으면,&amp;nbsp;배너&amp;nbsp;클릭&amp;nbsp;시&amp;nbsp;해당&amp;nbsp;`url`로&amp;nbsp;이동합니다.&lt;br /&gt;-&amp;nbsp;`url`&amp;nbsp;속성이&amp;nbsp;없으면,&amp;nbsp;배너&amp;nbsp;클릭&amp;nbsp;시&amp;nbsp;`sheetBannerList`&amp;nbsp;데이터를&amp;nbsp;사용하는&amp;nbsp;하단&amp;nbsp;팝업을&amp;nbsp;엽니다.&lt;br /&gt;-&amp;nbsp;팝업은&amp;nbsp;`LayerPopup.vue`와&amp;nbsp;`SlideBanner.vue`를&amp;nbsp;사용해&amp;nbsp;구현합니다.&lt;br /&gt;&lt;br /&gt;**테스트&amp;nbsp;계획**&lt;br /&gt;`test/integration/components/layout/NewMemberBanner.test.ts`&amp;nbsp;파일을&amp;nbsp;생성하고&amp;nbsp;다음&amp;nbsp;시나리오들을&amp;nbsp;테스트하겠습니다.&lt;br /&gt;&lt;br /&gt;1.&amp;nbsp;&amp;nbsp;**링크&amp;nbsp;이동&amp;nbsp;시나리오**:&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;`url`이&amp;nbsp;제공될&amp;nbsp;경우,&amp;nbsp;배너&amp;nbsp;클릭&amp;nbsp;시&amp;nbsp;페이지&amp;nbsp;이동&amp;nbsp;함수가&amp;nbsp;올바른&amp;nbsp;`url`과&amp;nbsp;함께&amp;nbsp;호출되는지&amp;nbsp;검증합니다.&lt;br /&gt;&lt;br /&gt;2.&amp;nbsp;&amp;nbsp;**팝업&amp;nbsp;활성화&amp;nbsp;시나리오**:&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;`url`이&amp;nbsp;제공되지&amp;nbsp;않을&amp;nbsp;경우,&amp;nbsp;배너&amp;nbsp;클릭&amp;nbsp;시&amp;nbsp;`LayerPopup`의&amp;nbsp;`open`&amp;nbsp;함수가&amp;nbsp;호출되는지&amp;nbsp;검증합니다.&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;팝업이&amp;nbsp;열렸을&amp;nbsp;때,&amp;nbsp;`sheetBannerList`&amp;nbsp;데이터의&amp;nbsp;개수만큼&amp;nbsp;`SlideBanner`&amp;nbsp;내부에&amp;nbsp;슬라이드가&amp;nbsp;생성되는지&amp;nbsp;확인합니다.&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;슬라이드&amp;nbsp;내부의&amp;nbsp;이미지와&amp;nbsp;텍스트&amp;nbsp;컨텐츠가&amp;nbsp;`sheetBannerList`&amp;nbsp;데이터와&amp;nbsp;일치하는지&amp;nbsp;검증합니다.&lt;br /&gt;&lt;br /&gt;이&amp;nbsp;계획대로&amp;nbsp;진행해도&amp;nbsp;괜찮을까요?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;승인 후 테스트 코드를 작성하고 실행합니다. (RED 단계)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;테스트를&amp;nbsp;실행했지만,&amp;nbsp;결과가&amp;nbsp;정상적으로&amp;nbsp;출력되지&amp;nbsp;않았습니다.&amp;nbsp;하지만&amp;nbsp;NewMemberBanner.vue&amp;nbsp;파일에&amp;nbsp;아직&amp;nbsp;기능이&amp;nbsp;구현되지&amp;nbsp;않았기&amp;nbsp;때문에,&amp;nbsp;제가&amp;nbsp;작성한&amp;nbsp;3가지&amp;nbsp;테스트&amp;nbsp;케이스(렌더링,&amp;nbsp;URL&amp;nbsp;이동,&amp;nbsp;팝업&amp;nbsp;활성화)는&amp;nbsp;모두&amp;nbsp;실패하는&amp;nbsp;것이&amp;nbsp;당연합니다.&amp;nbsp;이는&amp;nbsp;예상된&amp;nbsp;결과이며,&amp;nbsp;TDD의&amp;nbsp;[RED]&amp;nbsp;단계가&amp;nbsp;성공적으로&amp;nbsp;완료되었음을&amp;nbsp;의미합니다.&lt;br /&gt;이제&amp;nbsp;TDD의&amp;nbsp;다음&amp;nbsp;단계인&amp;nbsp;[GREEN]으로&amp;nbsp;넘어가겠습니다.&amp;nbsp;실패하는&amp;nbsp;테스트를&amp;nbsp;통과시키기&amp;nbsp;위해&amp;nbsp;NewMemberBanner.vue&amp;nbsp;컴포넌트의&amp;nbsp;구현&amp;nbsp;계획을&amp;nbsp;아래와&amp;nbsp;같이&amp;nbsp;제안합니다.&lt;br /&gt;&lt;br /&gt;**구현&amp;nbsp;계획&amp;nbsp;(의사코드)**&lt;br /&gt;NewMemberBanner.vue&amp;nbsp;파일을&amp;nbsp;다음과&amp;nbsp;같이&amp;nbsp;수정하겠습니다.&lt;br /&gt;1.&amp;nbsp;데이터&amp;nbsp;로딩&amp;nbsp;및&amp;nbsp;반복&amp;nbsp;렌더링:&lt;br /&gt;&amp;bull;&amp;nbsp;&amp;lt;script&amp;nbsp;setup&amp;gt;에서&amp;nbsp;usePromotionStore를&amp;nbsp;사용해&amp;nbsp;newMemberBannerList를&amp;nbsp;가져옵니다.&lt;br /&gt;&amp;bull;&amp;nbsp;onMounted&amp;nbsp;훅에서&amp;nbsp;getNewMemberBanner&amp;nbsp;액션을&amp;nbsp;호출하여&amp;nbsp;데이터를&amp;nbsp;불러옵니다.&lt;br /&gt;&amp;bull;&amp;nbsp;&amp;lt;template&amp;gt;의&amp;nbsp;최상위에서&amp;nbsp;v-for를&amp;nbsp;사용해&amp;nbsp;newMemberBannerList&amp;nbsp;배열의&amp;nbsp;각&amp;nbsp;banner&amp;nbsp;객체에&amp;nbsp;대해&amp;nbsp;배너&amp;nbsp;UI&amp;nbsp;전체를&amp;nbsp;반복하여&amp;nbsp;렌더링합니다.&lt;br /&gt;2.&amp;nbsp;동적&amp;nbsp;컨텐츠&amp;nbsp;표시:&lt;br /&gt;&amp;bull;&amp;nbsp;각&amp;nbsp;배너의&amp;nbsp;이미지,&amp;nbsp;제목,&amp;nbsp;부제는&amp;nbsp;v-for로&amp;nbsp;받은&amp;nbsp;banner&amp;nbsp;객체의&amp;nbsp;image,&amp;nbsp;title,&amp;nbsp;subTitle&amp;nbsp;속성을&amp;nbsp;사용해&amp;nbsp;표시합니다.&lt;br /&gt;3.&amp;nbsp;클릭&amp;nbsp;이벤트&amp;nbsp;처리:&lt;br /&gt;&amp;bull;&amp;nbsp;각&amp;nbsp;배너의&amp;nbsp;메인&amp;nbsp;버튼에&amp;nbsp;@click&amp;nbsp;핸들러(handleBannerClick)를&amp;nbsp;추가합니다.&lt;br /&gt;&amp;bull;&amp;nbsp;handleBannerClick&amp;nbsp;함수는&amp;nbsp;배너&amp;nbsp;객체를&amp;nbsp;인자로&amp;nbsp;받습니다.&lt;br /&gt;&amp;bull;&amp;nbsp;만약&amp;nbsp;banner.url&amp;nbsp;속성이&amp;nbsp;존재하면,&amp;nbsp;inject&amp;nbsp;받은&amp;nbsp;$goWithRouter를&amp;nbsp;호출하여&amp;nbsp;해당&amp;nbsp;URL로&amp;nbsp;이동합니다.&lt;br /&gt;&amp;bull;&amp;nbsp;banner.url&amp;nbsp;속성이&amp;nbsp;없다면,&amp;nbsp;해당&amp;nbsp;배너에&amp;nbsp;연결된&amp;nbsp;LayerPopup의&amp;nbsp;open&amp;nbsp;메서드를&amp;nbsp;호출하여&amp;nbsp;팝업을&amp;nbsp;엽니다.&lt;br /&gt;4.&amp;nbsp;팝업&amp;nbsp;구현:&lt;br /&gt;&amp;bull;&amp;nbsp;v-if=&quot;!banner.url&quot;&amp;nbsp;조건을&amp;nbsp;사용해&amp;nbsp;URL이&amp;nbsp;없는&amp;nbsp;배너에&amp;nbsp;대해서만&amp;nbsp;LayerPopup&amp;nbsp;컴포넌트를&amp;nbsp;렌더링합니다.&lt;br /&gt;&amp;bull;&amp;nbsp;ref를&amp;nbsp;배열&amp;nbsp;형태로&amp;nbsp;관리하여&amp;nbsp;각&amp;nbsp;배너가&amp;nbsp;독립적인&amp;nbsp;LayerPopup&amp;nbsp;인스턴스를&amp;nbsp;갖도록&amp;nbsp;합니다.&lt;br /&gt;&amp;bull;&amp;nbsp;팝업의&amp;nbsp;`#layer`&amp;nbsp;슬롯&amp;nbsp;내부에는&amp;nbsp;SlideBanner&amp;nbsp;컴포넌트를&amp;nbsp;호출하고고,&amp;nbsp;banner.sheetBannerList&amp;nbsp;데이터를&amp;nbsp;사용하여&amp;nbsp;슬라이드를&amp;nbsp;구성합니다.&lt;br /&gt;이&amp;nbsp;계획에&amp;nbsp;따라&amp;nbsp;NewMemberBanner.vue를&amp;nbsp;수정하여&amp;nbsp;테스트를&amp;nbsp;통과시키고자&amp;nbsp;합니다.&amp;nbsp;진행해도&amp;nbsp;괜찮을까요?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;승인&amp;nbsp;후&amp;nbsp;테스트를&amp;nbsp;통과하는&amp;nbsp;최소한의&amp;nbsp;코드를&amp;nbsp;작성합니다.&amp;nbsp;(GREEN&amp;nbsp;단계)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이제 TDD의 [GREEN] 단계의 마지막 절차로, 모든 테스트를 다시 실행하여 통과하는지 확인하겠습니다. 이전에 실패했던 테스트들이 이제는 모두 성공적으로 통과해야 합니다.&lt;br /&gt;&lt;br /&gt;테스트가&amp;nbsp;통과했군요!&amp;nbsp;알려주셔서&amp;nbsp;감사합니다.&amp;nbsp;제&amp;nbsp;이전&amp;nbsp;시도들이&amp;nbsp;계속&amp;nbsp;실패해서&amp;nbsp;혼란을&amp;nbsp;드렸는데,&amp;nbsp;결국&amp;nbsp;잘&amp;nbsp;해결되어&amp;nbsp;다행입니다.&lt;br /&gt;이것으로&amp;nbsp;TDD의&amp;nbsp;[GREEN]&amp;nbsp;단계가&amp;nbsp;성공적으로&amp;nbsp;완료되었습니다.&lt;br /&gt;이제&amp;nbsp;TDD&amp;nbsp;워크플로우의&amp;nbsp;마지막&amp;nbsp;단계인&amp;nbsp;[REFACTOR]로&amp;nbsp;넘어가겠습니다.&amp;nbsp;현재&amp;nbsp;테스트가&amp;nbsp;통과하는&amp;nbsp;것을&amp;nbsp;&quot;안전망&quot;으로&amp;nbsp;삼아,&amp;nbsp;코드의&amp;nbsp;가독성과&amp;nbsp;유지보수성을&amp;nbsp;높이기&amp;nbsp;위해&amp;nbsp;NewMemberBanner.vue&amp;nbsp;컴포넌트와&amp;nbsp;관련&amp;nbsp;테스트&amp;nbsp;코드를&amp;nbsp;개선하는&amp;nbsp;작업을&amp;nbsp;제안합니다.&lt;br /&gt;리팩토링을&amp;nbsp;진행하기&amp;nbsp;전에,&amp;nbsp;먼저&amp;nbsp;현재&amp;nbsp;코드를&amp;nbsp;다시&amp;nbsp;한번&amp;nbsp;살펴보겠습니다.&lt;br /&gt;&lt;br /&gt;NewMemberBanner.vue&amp;nbsp;파일의&amp;nbsp;코드를&amp;nbsp;검토했습니다.&amp;nbsp;가독성과&amp;nbsp;유지보수성을&amp;nbsp;높이기&amp;nbsp;위해&amp;nbsp;다음&amp;nbsp;두&amp;nbsp;가지&amp;nbsp;항목에&amp;nbsp;대한&amp;nbsp;리팩토링을&amp;nbsp;제안합니다.&lt;br /&gt;&lt;br /&gt;**리팩토링&amp;nbsp;계획**&lt;br /&gt;1.&amp;nbsp;변수명&amp;nbsp;변경:&amp;nbsp;bannerList라는&amp;nbsp;변수명은&amp;nbsp;일반적인&amp;nbsp;목록을&amp;nbsp;의미하므로,&amp;nbsp;이&amp;nbsp;배너가&amp;nbsp;어떤&amp;nbsp;배너인지&amp;nbsp;명확히&amp;nbsp;드러나도록&amp;nbsp;newMemberBannerList로&amp;nbsp;변경하겠습니다.&amp;nbsp;이는&amp;nbsp;코드의&amp;nbsp;명확성을&amp;nbsp;높여&amp;nbsp;다른&amp;nbsp;개발자가&amp;nbsp;코드를&amp;nbsp;이해하는&amp;nbsp;데&amp;nbsp;도움을&amp;nbsp;줍니다.&lt;br /&gt;2.&amp;nbsp;API&amp;nbsp;호출&amp;nbsp;로직&amp;nbsp;분리:&amp;nbsp;onMounted&amp;nbsp;훅&amp;nbsp;내부에&amp;nbsp;직접&amp;nbsp;API를&amp;nbsp;호출하는&amp;nbsp;로직이&amp;nbsp;포함되어&amp;nbsp;있습니다.&amp;nbsp;이를&amp;nbsp;별도의&amp;nbsp;fetchNewMemberBanners와&amp;nbsp;같은&amp;nbsp;비동기&amp;nbsp;함수로&amp;nbsp;분리하여&amp;nbsp;onMounted에서는&amp;nbsp;이&amp;nbsp;함수를&amp;nbsp;호출하기만&amp;nbsp;하도록&amp;nbsp;구조를&amp;nbsp;개선하겠습니다.&amp;nbsp;이렇게&amp;nbsp;하면&amp;nbsp;onMounted&amp;nbsp;훅이&amp;nbsp;간결해지고,&amp;nbsp;데이터&amp;nbsp;로딩&amp;nbsp;로직을&amp;nbsp;재사용하거나&amp;nbsp;테스트하기&amp;nbsp;더&amp;nbsp;쉬워집니다.&lt;br /&gt;이 계획에 따라 NewMemberBanner.vue를 수정하여 테스트를 통과시키고자 합니다. 진행해도 괜찮을까요?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #000000; text-align: left;&quot;&gt;승인&amp;nbsp;후 리팩토링을&amp;nbsp;수행합니다.&amp;nbsp;(REFACTOR&amp;nbsp;단계)&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;리팩토링을 완료했습니다.&lt;br /&gt;이제&amp;nbsp;TDD&amp;nbsp;워크플로우의&amp;nbsp;마지막&amp;nbsp;절차로,&amp;nbsp;리팩토링&amp;nbsp;후&amp;nbsp;모든&amp;nbsp;테스트를&amp;nbsp;다시&amp;nbsp;실행하여&amp;nbsp;기존&amp;nbsp;기능에&amp;nbsp;영향을&amp;nbsp;주지&amp;nbsp;않았는지&amp;nbsp;최종적으로&amp;nbsp;검증하겠습니다.&lt;br /&gt;&lt;br /&gt;네,&amp;nbsp;테스트&amp;nbsp;통과를&amp;nbsp;확인했습니다!&lt;br /&gt;리팩토링&amp;nbsp;후에도&amp;nbsp;모든&amp;nbsp;테스트가&amp;nbsp;성공적으로&amp;nbsp;통과했으므로,&amp;nbsp;TDD의&amp;nbsp;모든&amp;nbsp;과정(RED&amp;nbsp;-&amp;gt;&amp;nbsp;GREEN&amp;nbsp;-&amp;gt;&amp;nbsp;REFACTOR)이&amp;nbsp;성공적으로&amp;nbsp;완료되었습니다.&lt;br /&gt;요청하신&amp;nbsp;NewMemberBanner.vue&amp;nbsp;컴포넌트의&amp;nbsp;기능&amp;nbsp;구현&amp;nbsp;및&amp;nbsp;리팩토링&amp;nbsp;작업을&amp;nbsp;모두&amp;nbsp;마쳤습니다.&lt;br /&gt;다음 작업은 무엇일까요?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이와 같은 방식으로 TDD를 진행함으로써 요구사항에 대한 명확한 이해를 바탕으로 설계의 견고함을 확보하고, 개발 과정에서의 오류를 최소화합니다. 이는 예측 가능한 개발 프로세스를 통해 프로젝트의 안정성과 신뢰도를 대폭 향상시키며, 미래의 기능 확장이 용이한 고품질의 코드를 생산하는 데 기여합니다. 또한 테스트 코드 구현 속도를 비약적으로 단축시킬 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커밋&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@commit-convention.mdc (커밋 컨벤션 룰)&lt;/b&gt;을 만들고, &lt;b&gt;&quot;커밋&quot; 명령어 실행 시 &quot;git diff --staged&quot; 를 우선 실행하고 변경 이력에 따라 커밋 메세지 구현해줘 &lt;/b&gt;라는 메모리를 미리 저장합니다.&lt;br /&gt;&lt;br /&gt;이후 `커밋`이라는 명령만 던지면 Cursor가 커밋 메시지를 자동으로 생성해줍니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;커밋&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;알겠습니다.&amp;nbsp;스테이징된&amp;nbsp;변경&amp;nbsp;사항들을&amp;nbsp;확인하고&amp;nbsp;커밋&amp;nbsp;메시지를&amp;nbsp;제안하겠습니다.&lt;br /&gt;`git&amp;nbsp;diff&amp;nbsp;--staged`&amp;nbsp;실행&lt;br /&gt;&lt;br /&gt;`&lt;br /&gt;[SCRUM-14442]&amp;nbsp;feat:&amp;nbsp;신규&amp;nbsp;회원&amp;nbsp;탑&amp;nbsp;배너&amp;nbsp;쿠키&amp;nbsp;기반&amp;nbsp;노출&amp;nbsp;제어&amp;nbsp;기능&amp;nbsp;구현&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;쿠키를&amp;nbsp;사용하여&amp;nbsp;신규&amp;nbsp;회원&amp;nbsp;탑&amp;nbsp;배너의&amp;nbsp;노출&amp;nbsp;여부를&amp;nbsp;제어하도록&amp;nbsp;구현했습니다.&amp;nbsp;&lt;br /&gt;-&amp;nbsp;배너&amp;nbsp;닫기&amp;nbsp;버튼&amp;nbsp;클릭&amp;nbsp;시&amp;nbsp;쿠키를&amp;nbsp;설정하여&amp;nbsp;배너를&amp;nbsp;숨기도록&amp;nbsp;처리했습니다.&amp;nbsp;&lt;br /&gt;-&amp;nbsp;onBeforeMount&amp;nbsp;훅에서&amp;nbsp;쿠키&amp;nbsp;상태에&amp;nbsp;따라&amp;nbsp;초기&amp;nbsp;배너&amp;nbsp;노출&amp;nbsp;여부를&amp;nbsp;결정하도록&amp;nbsp;했습니다.&amp;nbsp;&lt;br /&gt;-&amp;nbsp;관련&amp;nbsp;테스트&amp;nbsp;코드를&amp;nbsp;추가하고&amp;nbsp;수정했습니다.&lt;br /&gt;`&lt;br /&gt;&lt;br /&gt;이 커밋 메세지로 진행할까요?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;LLM을&amp;nbsp;활용하여&amp;nbsp;변경&amp;nbsp;사항을&amp;nbsp;직접&amp;nbsp;파악하고&amp;nbsp;커밋&amp;nbsp;메시지를&amp;nbsp;생성하게&amp;nbsp;함으로써,&amp;nbsp;개발자가&amp;nbsp;수동으로&amp;nbsp;변경&amp;nbsp;내역을&amp;nbsp;파악하고&amp;nbsp;메시지를&amp;nbsp;작성하는&amp;nbsp;데&amp;nbsp;소요되는&amp;nbsp;시간을&amp;nbsp;절약할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;또한,&amp;nbsp;프로젝트의&amp;nbsp;일관된&amp;nbsp;커밋&amp;nbsp;메시지&amp;nbsp;컨벤션을&amp;nbsp;자동으로&amp;nbsp;적용하여&amp;nbsp;히스토리의&amp;nbsp;가독성을&amp;nbsp;극대화합니다.&amp;nbsp;이는&amp;nbsp;협업&amp;nbsp;효율성을&amp;nbsp;증진시키고,&amp;nbsp;특정&amp;nbsp;기능의&amp;nbsp;변경&amp;nbsp;이력을&amp;nbsp;추적하는&amp;nbsp;데&amp;nbsp;필요한&amp;nbsp;노력을&amp;nbsp;대폭&amp;nbsp;줄여줍니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PR 템플릿 생성 및 코드리뷰&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 &lt;b&gt;@pull-request.mdc (PR 컨벤션 룰)&lt;/b&gt;을 추가하고, &lt;b&gt;&quot;PR {비교할 브랜치}&quot;를 명령하면 현재 브랜치와 {비교할 브랜치}의 변경점을 파악해서 PR 템플릿을 생성해 &lt;/b&gt;라는 메모리를 저장합니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;release&lt;/b&gt; 브랜치와 현재 브랜치(&lt;b&gt;feature/SCRUM-14442_home-popup-banner&lt;/b&gt;)를 비교하는 PR 본문을 작성하는 예시입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;PR release&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;git&amp;nbsp;branch&amp;nbsp;-a&lt;br /&gt;&lt;br /&gt;`git&amp;nbsp;branch&amp;nbsp;-a`&amp;nbsp;명령어로&amp;nbsp;`release`&amp;nbsp;브랜치가&amp;nbsp;존재하는&amp;nbsp;것을&amp;nbsp;확인했습니다.&amp;nbsp;이제&amp;nbsp;`git&amp;nbsp;diff&amp;nbsp;release&amp;nbsp;--name-status`&amp;nbsp;명령어를&amp;nbsp;다시&amp;nbsp;실행하여&amp;nbsp;현재&amp;nbsp;브랜치와&amp;nbsp;`release`&amp;nbsp;브랜치&amp;nbsp;간의&amp;nbsp;변경&amp;nbsp;사항을&amp;nbsp;확인하겠습니다.&lt;br /&gt;&lt;br /&gt;git diff release --name-status&lt;br /&gt;&lt;br /&gt;`release`&amp;nbsp;브랜치와&amp;nbsp;현재&amp;nbsp;브랜치(`feature/SCRUM-14442_home-popup-banner`)&amp;nbsp;간의&amp;nbsp;변경&amp;nbsp;내용을&amp;nbsp;확인하여&amp;nbsp;Pull&amp;nbsp;Request&amp;nbsp;본문&amp;nbsp;초안을&amp;nbsp;작성했습니다.&lt;br /&gt;&lt;br /&gt;**Pull&amp;nbsp;Request&amp;nbsp;본문&amp;nbsp;초안:**&lt;br /&gt;&lt;br /&gt;```&lt;br /&gt;###&amp;nbsp; &amp;nbsp;관련&amp;nbsp;이슈&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;**Closes**:&amp;nbsp;`#14442`&amp;nbsp;(예상되는&amp;nbsp;Jira&amp;nbsp;또는&amp;nbsp;GitHub&amp;nbsp;이슈&amp;nbsp;번호,&amp;nbsp;정확한&amp;nbsp;이슈&amp;nbsp;번호를&amp;nbsp;확인하여&amp;nbsp;채워주세요.)&lt;br /&gt;&lt;br /&gt;###&amp;nbsp; &amp;nbsp;주요&amp;nbsp;변경&amp;nbsp;사항&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;메인&amp;nbsp;페이지에&amp;nbsp;신규&amp;nbsp;멤버를&amp;nbsp;위한&amp;nbsp;팝업&amp;nbsp;배너&amp;nbsp;기능&amp;nbsp;추가&lt;br /&gt;-&amp;nbsp;상품&amp;nbsp;관련&amp;nbsp;컴포넌트&amp;nbsp;리팩토링&amp;nbsp;및&amp;nbsp;태그&amp;nbsp;컴포넌트&amp;nbsp;구조&amp;nbsp;개선&lt;br /&gt;-&amp;nbsp;테스트&amp;nbsp;코드&amp;nbsp;구조를&amp;nbsp;단위&amp;nbsp;테스트에서&amp;nbsp;통합&amp;nbsp;테스트로&amp;nbsp;전환&amp;nbsp;및&amp;nbsp;관련&amp;nbsp;파일&amp;nbsp;이동&lt;br /&gt;&lt;br /&gt;###&amp;nbsp; &amp;nbsp;구현&amp;nbsp;내용&amp;nbsp;상세&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;**메인&amp;nbsp;페이지&amp;nbsp;신규&amp;nbsp;멤버&amp;nbsp;배너&amp;nbsp;추가**:&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;`NewMemberBanner.vue`&amp;nbsp;컴포넌트를&amp;nbsp;새로&amp;nbsp;추가하여&amp;nbsp;메인&amp;nbsp;페이지에&amp;nbsp;신규&amp;nbsp;멤버를&amp;nbsp;위한&amp;nbsp;팝업&amp;nbsp;배너를&amp;nbsp;표시합니다.&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;관련&amp;nbsp;로직&amp;nbsp;및&amp;nbsp;스타일링이&amp;nbsp;적용되었습니다.&lt;br /&gt;-&amp;nbsp;**상품&amp;nbsp;태그&amp;nbsp;컴포넌트&amp;nbsp;개선**:&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;`ProductTagList.vue`를&amp;nbsp;`ProductTagItem.vue`로&amp;nbsp;이름을&amp;nbsp;변경하고,&amp;nbsp;`UnifiedProductTagItem.vue`를&amp;nbsp;추가하여&amp;nbsp;상품&amp;nbsp;태그&amp;nbsp;표시&amp;nbsp;로직을&amp;nbsp;개선했습니다.&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;관련&amp;nbsp;상품&amp;nbsp;목록&amp;nbsp;컴포넌트(`ProductList.vue`,&amp;nbsp;`UnifiedProductList.vue`&amp;nbsp;등)들이&amp;nbsp;업데이트되었습니다.&lt;br /&gt;-&amp;nbsp;**테스트&amp;nbsp;코드&amp;nbsp;재편**:&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;기존&amp;nbsp;`unit`&amp;nbsp;테스트로&amp;nbsp;분류되었던&amp;nbsp;`CommentBox`,&amp;nbsp;`CommentListItem`,&amp;nbsp;`CommentWriterInfo`,&amp;nbsp;`RewardLinkSection`&amp;nbsp;컴포넌트의&amp;nbsp;테스트를&amp;nbsp;`integration`&amp;nbsp;테스트로&amp;nbsp;전환하고&amp;nbsp;파일&amp;nbsp;경로를&amp;nbsp;변경했습니다.&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;신규&amp;nbsp;`NewMemberBanner`&amp;nbsp;컴포넌트에&amp;nbsp;대한&amp;nbsp;통합&amp;nbsp;테스트(`NewMemberBanner.test.ts`)를&amp;nbsp;추가했습니다.&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;불필요한&amp;nbsp;`SharePopup.test.ts`&amp;nbsp;파일을&amp;nbsp;삭제했습니다.&lt;br /&gt;-&amp;nbsp;**타입&amp;nbsp;정의&amp;nbsp;및&amp;nbsp;Mock&amp;nbsp;데이터&amp;nbsp;업데이트**:&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;`comment.d.ts`,&amp;nbsp;`delivery.d.ts`,&amp;nbsp;`orderpick.d.ts`,&amp;nbsp;`product.d.ts`,&amp;nbsp;`promotion.d.ts`&amp;nbsp;등&amp;nbsp;관련&amp;nbsp;타입&amp;nbsp;정의&amp;nbsp;파일들이&amp;nbsp;최신&amp;nbsp;변경&amp;nbsp;사항을&amp;nbsp;반영하여&amp;nbsp;업데이트되었습니다.&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;`promotion.ts`&amp;nbsp;목업&amp;nbsp;데이터&amp;nbsp;파일이&amp;nbsp;새로&amp;nbsp;추가되었습니다.&lt;br /&gt;&lt;br /&gt;###&amp;nbsp; &amp;nbsp;스크린샷&amp;nbsp;또는&amp;nbsp;동영상&lt;br /&gt;&lt;br /&gt;(UI&amp;nbsp;변경&amp;nbsp;사항이&amp;nbsp;있는&amp;nbsp;경우,&amp;nbsp;변경&amp;nbsp;전후의&amp;nbsp;스크린샷이나&amp;nbsp;주요&amp;nbsp;기능의&amp;nbsp;동작을&amp;nbsp;보여주는&amp;nbsp;GIF/동영상을&amp;nbsp;첨부해주세요.&amp;nbsp;시각&amp;nbsp;자료는&amp;nbsp;리뷰어가&amp;nbsp;변경&amp;nbsp;사항을&amp;nbsp;직관적으로&amp;nbsp;이해하는&amp;nbsp;데&amp;nbsp;큰&amp;nbsp;도움이&amp;nbsp;됩니다.)&lt;br /&gt;&lt;br /&gt;|&amp;nbsp;변경&amp;nbsp;전&amp;nbsp;|&amp;nbsp;변경&amp;nbsp;후&amp;nbsp;|&lt;br /&gt;|:---:|:---:|&lt;br /&gt;|&amp;nbsp;(신규&amp;nbsp;배너가&amp;nbsp;없던&amp;nbsp;화면)&amp;nbsp;|&amp;nbsp;(신규&amp;nbsp;배너가&amp;nbsp;표시되는&amp;nbsp;화면)&amp;nbsp;|&lt;br /&gt;&lt;br /&gt;###&amp;nbsp;✅&amp;nbsp;테스트&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;[x]&amp;nbsp;**통합&amp;nbsp;테스트**:&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;`NewMemberBanner`&amp;nbsp;컴포넌트의&amp;nbsp;기능에&amp;nbsp;대한&amp;nbsp;통합&amp;nbsp;테스트를&amp;nbsp;추가하고&amp;nbsp;통과를&amp;nbsp;확인했습니다.&lt;br /&gt;&amp;nbsp;&amp;nbsp;- 기존 `unit`에서 `integration`으로 변경된 테스트(`CommentBox.test.ts`, `CommentListItem.test.ts`,&amp;nbsp;`CommentWriterInfo.test.ts`,&amp;nbsp;`RewardLinkSection.test.ts`)가&amp;nbsp;모두&amp;nbsp;통과함을&amp;nbsp;확인했습니다.&lt;br /&gt;-&amp;nbsp;[&amp;nbsp;]&amp;nbsp;**유닛&amp;nbsp;테스트**:&amp;nbsp;(추가/수정한&amp;nbsp;테스트&amp;nbsp;케이스&amp;nbsp;설명,&amp;nbsp;필요시&amp;nbsp;작성)&lt;br /&gt;-&amp;nbsp;[&amp;nbsp;]&amp;nbsp;**E2E&amp;nbsp;테스트**:&amp;nbsp;(전체&amp;nbsp;플로우&amp;nbsp;테스트&amp;nbsp;내용,&amp;nbsp;필요시&amp;nbsp;작성)&lt;br /&gt;-&amp;nbsp;[&amp;nbsp;]&amp;nbsp;**수동&amp;nbsp;테스트**:&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;1.&amp;nbsp;메인&amp;nbsp;페이지&amp;nbsp;진입&amp;nbsp;시&amp;nbsp;신규&amp;nbsp;멤버&amp;nbsp;팝업&amp;nbsp;배너가&amp;nbsp;정상적으로&amp;nbsp;표시되는지&amp;nbsp;확인.&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;2.&amp;nbsp;상품&amp;nbsp;상세&amp;nbsp;페이지&amp;nbsp;및&amp;nbsp;목록에서&amp;nbsp;태그&amp;nbsp;표시가&amp;nbsp;올바른지&amp;nbsp;확인.&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;3.&amp;nbsp;댓글&amp;nbsp;작성&amp;nbsp;및&amp;nbsp;보기&amp;nbsp;기능이&amp;nbsp;정상적으로&amp;nbsp;동작하는지&amp;nbsp;확인.&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;4.&amp;nbsp;목업&amp;nbsp;데이터를&amp;nbsp;통해&amp;nbsp;프로모션&amp;nbsp;관련&amp;nbsp;API&amp;nbsp;호출이&amp;nbsp;정상적으로&amp;nbsp;처리되는지&amp;nbsp;확인.&lt;br /&gt;&lt;br /&gt;###&amp;nbsp; &amp;nbsp;리뷰어에게&amp;nbsp;전하는&amp;nbsp;말&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;`NewMemberBanner`&amp;nbsp;컴포넌트의&amp;nbsp;노출&amp;nbsp;조건&amp;nbsp;및&amp;nbsp;디자인에&amp;nbsp;대해&amp;nbsp;검토&amp;nbsp;부탁드립니다.&lt;br /&gt;-&amp;nbsp;`product`&amp;nbsp;관련&amp;nbsp;컴포넌트들의&amp;nbsp;리팩토링이&amp;nbsp;적절히&amp;nbsp;이루어졌는지&amp;nbsp;확인&amp;nbsp;부탁드립니다.&lt;br /&gt;-&amp;nbsp;테스트&amp;nbsp;코드&amp;nbsp;구조&amp;nbsp;변경에&amp;nbsp;대한&amp;nbsp;피드백도&amp;nbsp;환영합니다.&lt;br /&gt;&lt;br /&gt;###&amp;nbsp;☑️&amp;nbsp;PR&amp;nbsp;체크리스트&lt;br /&gt;&lt;br /&gt;PR을&amp;nbsp;올리기&amp;nbsp;전,&amp;nbsp;아래&amp;nbsp;항목들을&amp;nbsp;모두&amp;nbsp;확인했는지&amp;nbsp;체크해주세요.&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;[x]&amp;nbsp;커밋&amp;nbsp;메시지가&amp;nbsp;프로젝트&amp;nbsp;컨벤션을&amp;nbsp;따르는가?&lt;br /&gt;-&amp;nbsp;[x]&amp;nbsp;작업&amp;nbsp;브랜치가&amp;nbsp;올바른가?&amp;nbsp;(`main`&amp;nbsp;브랜치에&amp;nbsp;직접&amp;nbsp;커밋&amp;nbsp;X)&lt;br /&gt;-&amp;nbsp;[x]&amp;nbsp;불필요한&amp;nbsp;`console.log`나&amp;nbsp;주석을&amp;nbsp;제거했는가?&lt;br /&gt;-&amp;nbsp;[x]&amp;nbsp;새로운&amp;nbsp;기능에&amp;nbsp;대한&amp;nbsp;테스트&amp;nbsp;코드를&amp;nbsp;추가했는가?&lt;br /&gt;-&amp;nbsp;[x]&amp;nbsp;빌드가&amp;nbsp;성공적으로&amp;nbsp;완료되었는가?&amp;nbsp;(로컬에서&amp;nbsp;확인)&lt;br /&gt;- [ ] 관련 문서(가이드, README 등)를 업데이트했는가? (필요시)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM을&amp;nbsp;활용하여&amp;nbsp;PR&amp;nbsp;작성&amp;nbsp;시간을&amp;nbsp;단축하고,&amp;nbsp;프로젝트&amp;nbsp;컨벤션에&amp;nbsp;맞는&amp;nbsp;일관되고&amp;nbsp;체계적인&amp;nbsp;PR&amp;nbsp;본문을&amp;nbsp;자동으로&amp;nbsp;생성함으로써&amp;nbsp;코드&amp;nbsp;리뷰의&amp;nbsp;효율성을&amp;nbsp;극대화합니다.&amp;nbsp;이는&amp;nbsp;리뷰어들이&amp;nbsp;변경&amp;nbsp;내용을&amp;nbsp;빠르고&amp;nbsp;정확하게&amp;nbsp;이해하도록&amp;nbsp;돕고,&amp;nbsp;핵심적인&amp;nbsp;피드백에&amp;nbsp;집중할&amp;nbsp;수&amp;nbsp;있게&amp;nbsp;하여&amp;nbsp;전체적인&amp;nbsp;개발&amp;nbsp;프로세스의&amp;nbsp;속도와&amp;nbsp;품질을&amp;nbsp;향상시키는&amp;nbsp;데&amp;nbsp;기여합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;LLM은&amp;nbsp;이마트&amp;nbsp;프로젝트에서&amp;nbsp;e2e&amp;nbsp;테스트&amp;nbsp;자동화,&amp;nbsp;TDD&amp;nbsp;워크플로우&amp;nbsp;적용,&amp;nbsp;커밋&amp;nbsp;메시지&amp;nbsp;및&amp;nbsp;PR&amp;nbsp;템플릿&amp;nbsp;자동&amp;nbsp;생성&amp;nbsp;등&amp;nbsp;다양한&amp;nbsp;분야에&amp;nbsp;걸쳐&amp;nbsp;핵심적인&amp;nbsp;역할을&amp;nbsp;수행하고&amp;nbsp;있습니다.&amp;nbsp;이러한&amp;nbsp;LLM의&amp;nbsp;활용은&amp;nbsp;개발&amp;nbsp;프로세스의&amp;nbsp;효율성을&amp;nbsp;높이고,&amp;nbsp;코드&amp;nbsp;품질을&amp;nbsp;향상시키며,&amp;nbsp;궁극적으로&amp;nbsp;안정적인&amp;nbsp;서비스를&amp;nbsp;제공하는&amp;nbsp;데&amp;nbsp;기여합니다.&amp;nbsp;앞으로도&amp;nbsp;LLM의&amp;nbsp;다양한&amp;nbsp;가능성을&amp;nbsp;탐색하여&amp;nbsp;개발&amp;nbsp;문화를&amp;nbsp;더욱&amp;nbsp;고도화해&amp;nbsp;나갈&amp;nbsp;계획입니다.&lt;/p&gt;</description>
      <category>Web</category>
      <author>씽코</author>
      <guid isPermaLink="true">https://singcotive.tistory.com/16</guid>
      <comments>https://singcotive.tistory.com/entry/LLM-%ED%99%9C%EC%9A%A9-%EC%82%AC%EB%A1%80#entry16comment</comments>
      <pubDate>Wed, 16 Jul 2025 16:43:03 +0900</pubDate>
    </item>
    <item>
      <title>  Playwright MCP로 E2E 테스트 자동화하기 (feat. 이마트 와인 구매)</title>
      <link>https://singcotive.tistory.com/entry/%F0%9F%A4%96-Playwright-MCP%EB%A1%9C-E2E-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%90%EB%8F%99%ED%99%94%ED%95%98%EA%B8%B0-feat-%EC%9D%B4%EB%A7%88%ED%8A%B8-%EC%99%80%EC%9D%B8-%EA%B5%AC%EB%A7%A4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Vue.js 기반의 이마트 앱에서 '와인그랩' 구매 프로세스를 자동화한 E2E(End-to-End) 테스트 구축기를 공유하려고 합니다. 이 포스팅에서는 Playwright를 선택한 이유부터, 반복적인 로그인 과정을 스마트하게 처리하는 법, 그리고 AI 페어 프로그래밍 도구(Cursor)를 활용해 실제 테스트 시나리오를 작성하는 과정까지, 저의 경험을 아낌없이 나눠보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  E2E 테스트, 왜 Playwright일까요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;E2E 테스트는 사용자의 여정을 그대로 흉내 내며 프론트엔드부터 백엔드까지 전체&amp;nbsp;시스템의 통합적인 동작을 검증하는 가장 확실한 방법입니다. 수많은 프레임워크 중 Playwright를 선택한&amp;nbsp;이유는 명확했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-indent=&quot;0&quot;&gt;  멀티 브라우저:&amp;nbsp;코드를 한 번만 작성하면 Chromium, Firefox, WebKit에서 모두 테스트할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-indent=&quot;0&quot;&gt;  자동 대기(Auto-Waits):&amp;nbsp;페이지 요소가 나타날 때까지 지능적으로 기다려주므로,&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;sleep&lt;/span&gt;&lt;/b&gt;이나&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;waitFor&lt;/span&gt;&lt;/b&gt;&amp;nbsp;같은 불안정한 코드를 최소화할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-indent=&quot;0&quot;&gt;  강력한 디버깅:&amp;nbsp;테스트 실패 시 자동으로&amp;nbsp;비디오와 스크린샷을 녹화하고,&amp;nbsp;Trace Viewer를 통해 시간 순서대로 모든 과정을&amp;nbsp;시각적으로 분석할 수 있어 디버깅이 매우 편리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot; data-indent=&quot;0&quot;&gt;  인증 상태 공유:&amp;nbsp;한&amp;nbsp;번의 로그인으로 생성된 인증 상태(쿠키, 스토리지 등)를 모든 테스트에서 재사용할&amp;nbsp;수 있어 테스트 속도와 안정성을 크게 높여줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;STEP 1: 테스트의 첫걸음, 환경 설정 (playwright.config.ts)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 프로젝트의 시작은 환경 설정입니다.&amp;nbsp;playwright.config.ts&amp;nbsp;파일은 테스트의 전반적인 동작을 제어하는 핵심 설정 파일입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750400865291&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { defineConfig } from '@playwright/test';
import path from 'path';

// 실행 환경(development, staging, production)에 따라 baseURL을 설정합니다.
const env = process.env.E2E_ENV || 'dev';
let baseURL = 'https://dev.your-service.com';
if (env === 'stg') baseURL = 'https://stg.your-service.com';
if (env === 'prd') baseURL = 'https://your-service.com';

export default defineConfig({
    // 모든 테스트 실행 전 단 한 번만 실행될 '글로벌 셋업' 파일을 지정합니다.
    globalSetup: require.resolve('./test/global-setup'),

    testDir: path.join(__dirname, 'test', 'e2e'),
    timeout: 30 * 1000, // 각 테스트는 30초의 타임아웃을 가집니다.
    reporter: [['list'], ['html', { open: 'never' }]], // 콘솔과 HTML 리포트를 생성합니다.

    use: {
        baseURL, // 테스트의 기본 URL입니다. page.goto('/path') 형태로 사용됩니다.
        headless: true, // CI/CD 환경을 위해 UI 없이 브라우저를 실행합니다.
        viewport: { width: 1280, height: 800 },
        ignoreHTTPSErrors: true,
        video: 'retain-on-failure', // 테스트 실패 시에만 비디오를 녹화합니다.
        screenshot: 'only-on-failure', // 테스트 실패 시에만 스크린샷을 저장합니다.
        trace: 'on-first-retry', // 첫 재시도 시점에 Trace 파일을 기록합니다.

        // globalSetup에서 생성한 인증 상태를 모든 테스트에서 공유합니다.
        storageState: 'test/mocks/auth.json',
    },
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 가장 중요한&amp;nbsp;설정은&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;globalSetup&lt;/span&gt;&lt;/b&gt;과&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;storageState&lt;/span&gt;&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;globalSetup&lt;/span&gt;&lt;/b&gt;은 모든 테스트가 실행되기 전에 단 한 번 실행되는 스크립트로, 저희는 여기에 '로그인' 로직을 넣을 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;storageState&lt;/span&gt;&lt;/b&gt;는 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;globalSetup&lt;/span&gt;&lt;/b&gt;에서 성공한 로그인 세션 정보(쿠키 등)를 파일로 저장하고, 모든 테스트가 이 파일을 공유하게 해주는 마법 같은 옵션입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;STEP 2: 귀찮은 로그인은 한번만! (global-setup.ts)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;E2E 테스트에서 가장 번거로운 과정은 '인증'입니다. 모든 테스트 케이스마다 로그인하는 것은 비효율의 극치죠.&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;global-setup.ts&lt;/span&gt;&lt;/b&gt;을 이용해 이 문제를 해결했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750401006146&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { chromium, FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
    const { baseURL, storageState } = config.projects[0].use;
    if (!baseURL) {
        throw new Error('baseURL이 설정 파일에 정의되지 않았습니다.');
    }

    // 1. 브라우저를 실행하고 새 페이지를 엽니다.
    const browser = await chromium.launch();
    const page = await browser.newPage();

    // 2. 로그인 페이지로 이동해 테스트 계정으로 로그인합니다.
    await page.goto(`${baseURL}/login`);
    await page.getByLabel('아이디').fill('YOUR_TEST_ID');
    await page.getByLabel('비밀번호').fill('YOUR_TEST_PASSWORD');
    await page.getByRole('button', { name: '로그인' }).click();

    // 3. 특정 페이지로 이동이 완료될 때까지 기다립니다.
    await page.waitForURL('**/main');

    // 4. 현재 브라우저의 인증 상태를 storageState 파일 경로에 저장합니다.
    await page.context().storageState({ path: storageState as string });
    await browser.close();
}

export default globalSetup;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 스크립트는 테스트&amp;nbsp;시작 전, 단 한 번 실행되어 다음 작업을 수행합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal;&quot; data-indent=&quot;0&quot;&gt;실제 브라우저를 실행하여&amp;nbsp;로그인 페이지로 이동합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal;&quot; data-indent=&quot;0&quot;&gt;미리 준비된 테스트 계정으로 로그인을 시도합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal;&quot; data-indent=&quot;0&quot;&gt;로그인 성공 후,&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;page.context().storageState()&lt;/span&gt;&lt;/b&gt;를 호출하여 현재 인증 정보를&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;test/mocks/auth.json&lt;/span&gt;&lt;/b&gt;&amp;nbsp;파일로 저장합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal;&quot; data-indent=&quot;0&quot;&gt;브라우저를 닫고 역할을 마칩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 모든 테스트는&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;auth.json&lt;/span&gt;&lt;/b&gt;&amp;nbsp;파일을 읽어 '로그인된 상태'로 시작할 수 있게&amp;nbsp;되었습니다!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;STEP 3: AI와 함께 시나리오 작성 (wine-purchase.test.ts)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 핵심 시나리오인 '와인 구매' 테스트를 작성할 차례입니다. 저는 이 과정을 AI 페어 프로그래밍 도구인&amp;nbsp;Cursor에게 맡겼습니다. &quot;와인 상품을 선택하고 수량을&amp;nbsp;조절하여 결제 페이지로 이동하는 Playwright 테스트 코드를 작성해줘&quot;라고 요청하자, 놀랍도록&amp;nbsp;정확한 코드를 생성해주었습니다.&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;test.step&amp;nbsp;API&lt;/span&gt;&lt;/b&gt;를 사용해 시나리오를 의미 있는 단위로 나누면, 테스트가 실패했을 때 어느 단계에서 문제가 발생했는지 직관적으로 파악할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750401098526&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;test.describe('와인 구매 시나리오', () =&amp;gt; {
    // 설정 파일에 따라 자동으로 auth.json을 읽어 로그인 상태로 만듭니다.
    test.use({ storageState: 'test/mocks/auth.json' });

    test('상품 선택부터 픽업일 지정 후 결제 페이지까지 이동한다', async ({ page }) =&amp;gt; {

        await test.step('1. 와인 메인 페이지로 이동', async () =&amp;gt; {
            await page.goto('/wine');
            await expect(page).toHaveURL(/.*\/wine/);
        });

        await test.step('2. 첫 번째 상품을 선택해 상세 페이지로 이동', async () =&amp;gt; {
            // NOTE: 실제 서비스에서는 데이터 로딩을 기다리는 더 안정적인 방법을 권장합니다.
            await page.waitForTimeout(1000);
            await page.getByTestId('wine-product-item').first().click();
            await expect(page).toHaveURL(/.*\/wine\/view/);
        });

        await test.step('3. 구매 팝업에서 수량을 3개로 늘리고 구매 버튼 클릭', async () =&amp;gt; {
            await page.getByRole('button', { name: '바로구매' }).click();
            // 수량 늘리기 버튼을 두 번 클릭
            await page.getByRole('button', { name: '늘리기' }).dblclick();
            await page.getByRole('button', { name: '구매하기' }).click();
        });

        await test.step('4. 픽업일을 선택', async () =&amp;gt; {
            await page.getByRole('button', { name: '픽업일 선택' }).click();
            // 선택 가능한 날짜 중 첫 번째 날짜를 클릭
            await page.locator('.calendar-day:not(.disabled)').first().click();
        });

        await test.step('5. 결제하기 버튼 클릭 및 최종 페이지 이동 확인', async () =&amp;gt; {
            await page.getByRole('button', { name: '결제하기' }).click();
            await expect(page).toHaveURL(/.*\/payment/);
        });
    });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Playwright MCP 적용 영상&lt;/h2&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Playwright의 스마트한 기능들과 AI의 만남은 테스트 자동화의 개발 경험을 완전히 바꿔 놓았습니다.&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;globalSetup&lt;/span&gt;&lt;/b&gt;과&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;storageState&lt;/span&gt;&lt;/b&gt;로 인증 과정을 분리하여 테스트의 안정성과 속도를 높였고, 복잡한 시나리오 작성은 AI에게 맡겨 생산성을 극대화할 수 있었습니다. 덕분에 저는 더 중요한 로직 검증에 집중할 수 있었죠.테스트 자동화에 막연한 두려움을 가지고 계셨다면, Playwright와 Cursor 같은 AI 도구를 활용해 첫걸음을 내디뎌 보시는 건 어떨까요? 생각보다 훨씬 더 즐겁고 창의적인 여정이 될 거라 확신합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;여러분들은 MCP를 어떻게 활용하고 계시나요?&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;편하게 댓글이나 메시지로 피드백 주시면 정말 감사하겠습니다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Web</category>
      <author>씽코</author>
      <guid isPermaLink="true">https://singcotive.tistory.com/15</guid>
      <comments>https://singcotive.tistory.com/entry/%F0%9F%A4%96-Playwright-MCP%EB%A1%9C-E2E-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%90%EB%8F%99%ED%99%94%ED%95%98%EA%B8%B0-feat-%EC%9D%B4%EB%A7%88%ED%8A%B8-%EC%99%80%EC%9D%B8-%EA%B5%AC%EB%A7%A4#entry15comment</comments>
      <pubDate>Fri, 20 Jun 2025 16:10:32 +0900</pubDate>
    </item>
    <item>
      <title>Javascript Interface로 앱스킴 동시호출 씹힘 현상 해결하기</title>
      <link>https://singcotive.tistory.com/entry/Javascript-Interface%EB%A1%9C-%EC%95%B1%EC%8A%A4%ED%82%B4-%EB%8F%99%EC%8B%9C%ED%98%B8%EC%B6%9C-%EC%94%B9%ED%9E%98-%ED%98%84%EC%83%81-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;&lt;b&gt;앱 스킴 연속 호출 문제와 JavascriptInterface를 활용한 해결 방법&lt;/b&gt;&lt;/h1&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모바일 앱과 웹이 연동되는 &lt;span&gt;&lt;b&gt;하이브리드 앱 환경&lt;/b&gt;&lt;/span&gt;에서는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹에서 네이티브 기능을 호출하기 위해 앱 스킴을 사용하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 프로젝트에서 앱 스킴 호출 시 여러 가지 문제를 경험했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이를 &lt;/span&gt;&lt;b&gt;JavascriptInterface 방식으로 전환하여 해결&lt;/b&gt;&lt;span&gt;할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 글에서는 &lt;/span&gt;&lt;b&gt;앱 스킴 호출에서 발생한 대표적인 이슈들과,&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이를 어떻게 트러블슈팅했는지&lt;/b&gt;&lt;span&gt; 공유드리고자 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  발생한 이슈&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1) 앱 스킴 동시 호출 시 일부 명령 무시&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 스킴을 연속으로 호출할 경우,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫 번째 명령만 실행되고 이후 명령은 무시되는 현상&lt;/b&gt;&lt;span&gt;이 발생했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;emart.app.cmd('페이지 이동');
emart.app.cmd('페이스 아이디 열기'); // 동작하지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 두 명령을 거의 동시에 실행할 경우,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 호출은 무시되어 &lt;span&gt;&lt;b&gt;사용자 경험에 치명적인 문제&lt;/b&gt;&lt;/span&gt;로 이어졌습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2) 콜백 함수 기반 데이터 처리의 한계&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱에서 데이터를 받아야 할 때,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JavaScript가 직접 값을 반환받을 수 없고 반드시 콜백 함수 방식으로 처리&lt;/b&gt;&lt;span&gt;해야 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 앱 스킴 호출
emart.app.cmd('get_message_count', window.getMessageCount);

// 콜백 함수
window.getMessageCount = (messageCount) =&amp;gt; {
  // 알림 개수 처리
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이런 방식은 다음과 같은 단점이 있습니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로직 흐름이 &lt;span&gt;&lt;b&gt;비동기적&lt;/b&gt;&lt;/span&gt;이어서 가독성이 떨어짐&lt;/li&gt;
&lt;li&gt;&lt;b&gt;콜백 함수 위치 추적이 어려워&lt;/b&gt;&lt;span&gt; 디버깅이 복잡해짐&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;테스트 작성이 까다로움&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3) 전역 함수만 실행 가능&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이티브 측에서는 JavaScript의 &lt;span&gt;&lt;b&gt;window 객체에 등록된 전역 함수만 호출&lt;/b&gt;&lt;/span&gt;이 가능했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 다음과 같은 문제점이 있었습니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;모듈화가 어렵고&lt;/b&gt;&lt;/span&gt;, 함수 간 의존성이 증가&lt;/li&gt;
&lt;li&gt;전역 스코프 오염 우려&lt;/li&gt;
&lt;li&gt;여러 페이지 또는 기능 간 &lt;span&gt;&lt;b&gt;함수 이름 충돌&lt;/b&gt;&lt;/span&gt; 위험&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4) 앱 스킴 실행 여부 확인 불가&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;스킴을 실행했는지 여부를 &lt;/span&gt;&lt;b&gt;확실히 확인할 수 있는 방법이 없어&lt;/b&gt;&lt;span&gt;,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 콜백 함수가 실행되었는지를 가지고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;아마 실행되었겠지&amp;rdquo;라고 &lt;span&gt;&lt;b&gt;추측하는 방식&lt;/b&gt;&lt;/span&gt;으로 판단해야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 예외 처리를 어렵게 만들고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 경험을 정밀하게 제어하는 데 한계를 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  트러블슈팅 및 해결 방법&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ JavascriptInterface를 활용한 직접 호출&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JavascriptInterface&lt;/b&gt;&lt;span&gt;를 활용하면&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript에서 네이티브 함수를 직접 호출할 수 있어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스킴을 통한 명령 충돌 문제를 완전히 해결&lt;/b&gt;&lt;span&gt;할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// TypeScript
window.getMessageCount = (messageCount) =&amp;gt; {
  // 알림 개수 처리 로직
};
Android.getMessageCount(); // 직접 호출&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;// AOS (Java)
class Android {
  @JavascriptInterface
  public void getMessageCount() {
    int count = 10;
    runJavascript(&quot;window.getMessageCount(&quot; + count + &quot;)&quot;);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 방식은 &lt;/span&gt;&lt;b&gt;중간의 앱 스킴 브릿지 없이 직접 호출&lt;/b&gt;&lt;span&gt;되므로,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여러 함수 동시 실행 시에도 누락이 없습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ 직접 값 반환으로 콜백 함수 제거&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;콜백 함수 대신 &lt;/span&gt;&lt;b&gt;함수의 반환값을 직접 활용&lt;/b&gt;&lt;span&gt;하면&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 흐름이 훨씬 간단해지고 추적도 쉬워집니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// TypeScript
const messageCount = Android.getMessageCount(); // 값 직접 반환&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// AOS (Java)
class Android {
  @JavascriptInterface
  public int getMessageCount() {
    return 10;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 이 방식은 JS &amp;harr; Java 간 동기 실행 제약 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UI 처리나 API 콜 등 시간이 오래 걸리는 작업에는 부적합&lt;/b&gt;&lt;span&gt;할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ 동기적 실행으로 흐름 파악 용이&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavascriptInterface는 동기 방식으로 작동하기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JavaScript에서 네이티브 함수 실행이 완료된 시점을 명확히 알 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이로 인해 &lt;/span&gt;&lt;b&gt;의존성 있는 로직의 흐름을 예측 가능하게 만들 수 있고&lt;/b&gt;&lt;span&gt;,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;전체 앱 로직을 &lt;/span&gt;&lt;b&gt;모듈화하고 통제하는 데 큰 도움이 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅ 정리하며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하이브리드 앱 환경에서 흔히 사용하는 &lt;span&gt;&lt;b&gt;앱 스킴 방식은 간편하지만,&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예상치 못한 동작 오류와 디버깅의 복잡성&lt;/b&gt;&lt;span&gt;이 따라옵니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavascriptInterface를 활용하면 아래와 같은 개선이 가능합니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;명령 누락 없이 안정적인 함수 호출&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;콜백 제거로 직관적인 코드 구성&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전역 함수 사용 제한 해소&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동기 흐름 확보로 로직 명확화&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 고민을 하고 계시거나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 방식으로 해결하신 경험이 있다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여러분의 의견이 정말 궁금합니다 :)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자유롭게 댓글이나 피드백 남겨주시면 감사하겠습니다!&lt;/p&gt;</description>
      <category>Trouble Shooting</category>
      <author>씽코</author>
      <guid isPermaLink="true">https://singcotive.tistory.com/14</guid>
      <comments>https://singcotive.tistory.com/entry/Javascript-Interface%EB%A1%9C-%EC%95%B1%EC%8A%A4%ED%82%B4-%EB%8F%99%EC%8B%9C%ED%98%B8%EC%B6%9C-%EC%94%B9%ED%9E%98-%ED%98%84%EC%83%81-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0#entry14comment</comments>
      <pubDate>Thu, 19 Jun 2025 14:51:09 +0900</pubDate>
    </item>
    <item>
      <title>1년 동안 느껴본 LLM의 수준과 발전 가능성 그리고 대체 불가능한 인력이 되려면?</title>
      <link>https://singcotive.tistory.com/entry/5%EC%9D%BC-%EB%8F%99%EC%95%88-%EB%8A%90%EA%BB%B4%EB%B3%B8-LLM%EC%9D%98-%EC%88%98%EC%A4%80%EA%B3%BC-%EB%B0%9C%EC%A0%84-%EA%B0%80%EB%8A%A5%EC%84%B1-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%8C%80%EC%B2%B4-%EB%B6%88%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%9D%B8%EB%A0%A5%EC%9D%B4-%EB%90%98%EB%A0%A4%EB%A9%B4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 1년간 LLM을 밀도 있게 활용하면서 다양한 생각을 하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 단순히 개발자뿐 아니라, &lt;span&gt;&lt;b&gt;비개발자 분들께도 충분히 적용되는 이야기&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;오늘은 &lt;/span&gt;&lt;b&gt;현재 LLM(대형 언어 모델)의 수준과 한계&lt;/b&gt;&lt;span&gt;, 그리고 &lt;/span&gt;&lt;b&gt;앞으로의 발전 가능성&lt;/b&gt;&lt;span&gt;,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;더 나아가 &lt;/span&gt;&lt;b&gt;우리가 어떤 방향의 인재로 성장해야 하는지&lt;/b&gt;&lt;span&gt;에 대해 이야기해보려 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  LLM, 어디까지 와 있을까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 AI는 단순히 &amp;lsquo;보조 도구&amp;rsquo; 수준을 넘어 &lt;span&gt;&lt;b&gt;기능 구현의 주체로도 활용 가능한 단계&lt;/b&gt;&lt;/span&gt;에 와 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, AI 모델이 탑재된 &lt;a href=&quot;https://www.cursor.so/&quot;&gt;Cursor 에디터&lt;/a&gt;는 단순한 규칙만 입력해도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상용 수준의 코드와 테스트까지도 자동으로 생성해줍니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히 기능 요구사항만 전달하면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사람이 직접 코드를 작성하지 않아도 될 정도로 빠르고 정교한 결과물&lt;/b&gt;&lt;span&gt;을 만들어냅니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 정말 이대로 LLM에게 다 맡겨도 되는 걸까요?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;⚠️ 직접 써보며 느낀 LLM의 한계 2가지&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 외부 상황을 고려하지 못한다&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM은 주어진 요구사항에 충실한 결과물을 만들어내지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;외부 맥락이나 전략적 의도&lt;/b&gt;&lt;span&gt;를 파악하진 못합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 블로그 글 작성용 에디터를 만든다고 했을 때,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM은 기능 중심의 코드만 작성해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 회사의 KPI가 &lt;b&gt;&amp;lsquo;외부 바이럴 강화&amp;rsquo;&lt;/b&gt;라면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;실제 개발자는 &lt;/span&gt;&lt;b&gt;템플릿이나 검증 로직을 외부 주입 방식으로 구현해 확장성과 유연성을 확보&lt;/b&gt;&lt;span&gt;하려고 하겠죠.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이처럼, &lt;/span&gt;&lt;b&gt;LLM은 비즈니스 전략이나 조직의 맥락을 고려한 설계에는 한계&lt;/b&gt;&lt;span&gt;가 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 완전무결한 코드는 아니다&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 작성한 코드는 대부분 우수하지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;디버깅이 필요한 순간은 반드시 옵니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;문제는, 코드를 &lt;/span&gt;&lt;b&gt;이해하지 않은 채 사용하는 경우&lt;/b&gt;&lt;span&gt;,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;오류 발생 시 &lt;/span&gt;&lt;b&gt;어디서부터 손대야 할지 모를 수 있다는 점&lt;/b&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  앞으로 LLM은 어떻게 발전할까?&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ 외부 상황도 반영할 수 있을 것이다&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사의 KPI, 트래픽, 사용자 지표(MAU/DAU) 등이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정해진 형식으로 체계화되면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM도 이러한 정보를 기반으로 &lt;span&gt;&lt;b&gt;상황에 맞는 설계를 반영하는 수준&lt;/b&gt;&lt;/span&gt;까지 도달할 수 있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ 디버깅도 줄어들 것&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전히 사라지지는 않겠지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;디버깅이 필요한 빈도는 점점 줄어들 것&lt;/b&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국엔 단순한 수정이 아닌,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;고수준 검토를 할 수 있는 인재만이 필요한 시대&lt;/b&gt;&lt;span&gt;가 올 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  그렇다면 우리는 어떤 사람이 되어야 할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사는 언제나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;더 적은 비용&lt;/b&gt;&lt;span&gt;, &lt;/span&gt;&lt;b&gt;더 빠른 속도&lt;/b&gt;&lt;span&gt;, &lt;/span&gt;&lt;b&gt;더 높은 품질&lt;/b&gt;&lt;span&gt;을 원합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 LLM 시대에 &lt;span&gt;&lt;b&gt;대체되지 않는 인재&lt;/b&gt;&lt;/span&gt;가 되기 위해 우리는 어떻게 준비해야 할까요?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;⏳ 가까운 미래&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직은 LLM이 외부 맥락을 완전히 이해하거나 즉각적으로 오류를 해결하지 못합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다음과 같은 능력이 중요합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;결과물을 &lt;/span&gt;&lt;b&gt;빠르게 이해하고 판단할 수 있는 역량&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;외부 전략과 이슈를 고려해 &lt;/span&gt;&lt;b&gt;LLM을 유도할 수 있는 능력&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LLM 도구에 빠르게 적응하고 활용할 수 있는 실행력&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  중장기 미래 (LLM이 완성형에 가까워졌을 때)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 거의 완전한 도구가 된다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이후의 인재상은 다음과 같을 것입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;폭넓은 지식 기반&lt;/b&gt;&lt;/span&gt;으로 비즈니스적 영향까지 고려할 수 있는 시야&lt;/li&gt;
&lt;li&gt;&lt;span&gt;조직 내외 커뮤니케이션을 통해 &lt;/span&gt;&lt;b&gt;전략을 설계하고 협의할 수 있는 능력&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  켄트 벡(Kent Beck)이 말한 미래의 핵심 역량&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TDD의 선구자인 켄트 벡은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미래에 더 가치가 올라갈 역량으로 다음을 제시했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비전 설정 능력&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이정표 관리 능력&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;복잡성 제어 능력&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국, 우리가 준비해야 할 방향성과 정확히 일치하는 내용이라고 생각합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅ 지금 우리가 할 수 있는 실천 항목들&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 다양한 실제 업무에 LLM과 MCP를 적용해보기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 현재 기술의 가능성과 한계를 직접 체감해보기&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 자신의 분야에서 문제 해결 경험과 깊이를 쌓기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; LLM이 보지 못하는 &lt;span&gt;&lt;b&gt;맥락을 유도하고 교정할 수 있는 힘&lt;/b&gt;&lt;/span&gt;을 기르기&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 직무 경계를 넘나들 준비하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; AI는 직무 간 경계를 무너뜨릴 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인접 분야에 대한 얕은 이해라도 지금부터 쌓아두는 것이 중요합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;맺음말&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM은 곧 우리의 &amp;lsquo;동료&amp;rsquo;가 될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 그 동료와 &lt;/span&gt;&lt;b&gt;어떻게 협업할 수 있는가&lt;/b&gt;&lt;span&gt;가&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;곧 우리의 경쟁력이 될 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 분야에 계시거나, 혹은 전혀 다른 분야에 계신 분들의 생각은 어떠신가요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여러분의 의견이 정말 궁금합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;편하게 댓글이나 메시지로 피드백 주시면 정말 감사하겠습니다  &lt;/p&gt;</description>
      <category>Paradigm</category>
      <author>씽코</author>
      <guid isPermaLink="true">https://singcotive.tistory.com/13</guid>
      <comments>https://singcotive.tistory.com/entry/5%EC%9D%BC-%EB%8F%99%EC%95%88-%EB%8A%90%EA%BB%B4%EB%B3%B8-LLM%EC%9D%98-%EC%88%98%EC%A4%80%EA%B3%BC-%EB%B0%9C%EC%A0%84-%EA%B0%80%EB%8A%A5%EC%84%B1-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%8C%80%EC%B2%B4-%EB%B6%88%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%9D%B8%EB%A0%A5%EC%9D%B4-%EB%90%98%EB%A0%A4%EB%A9%B4#entry13comment</comments>
      <pubDate>Thu, 19 Jun 2025 11:43:33 +0900</pubDate>
    </item>
    <item>
      <title>vue의 컴포넌트 렌더링 순서를 확실히 알아보자!</title>
      <link>https://singcotive.tistory.com/entry/vue%EC%9D%98-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%88%9C%EC%84%9C%EB%A5%BC-%ED%99%95%EC%8B%A4%ED%9E%88-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bllmCY/btslUtNzBp3/AkAIOKpidBjA1I1Eywp7i0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bllmCY/btslUtNzBp3/AkAIOKpidBjA1I1Eywp7i0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bllmCY/btslUtNzBp3/AkAIOKpidBjA1I1Eywp7i0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbllmCY%2FbtslUtNzBp3%2FAkAIOKpidBjA1I1Eywp7i0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4400&quot; height=&quot;2475&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;h2 id=&quot;이슈-정의&quot; data-ke-size=&quot;size26&quot;&gt; 이슈 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 컴포넌트 2개의 데이터를 비동기로 호출하는데 이 때 두 컴포넌트 모두 데이터가 존재하지 않다면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;emit을 통해 데이터 존재 여부를 상위 컴포넌트로 전달해주고, 상위 컴포넌트의 렌더링을 막으려고 했다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;// Parent.vue
&amp;lt;template&amp;gt;
	&amp;lt;div v-if=&quot;isFristData || isSecondData&quot;&amp;gt;
    	&amp;lt;FirstChild @is-first-data=&quot;isFirstData&quot; /&amp;gt;
        &amp;lt;SecondChild @is-second-data=&quot;isSecondData&quot; /&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
export default {
	name: &quot;Parent&quot;,
    methods: {
    	isFirstData(isShow) {
        	return isShow;
        },
        isSecondData(isShow) {
        	return isShow;
        }
    }
}
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// FirstChild.vue
&amp;lt;template&amp;gt;
	&amp;lt;div&amp;gt;FirstChild&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
export default {
	name: &quot;Parent&quot;,
    emits: [&quot;isFirstData&quot;],
    created() {
    	this.getData();
    },
    methods: {
    	async getData() {
        	const res = axios.get('data api 요청');
           	this.$emit('isFristData', res);
        }
    }
}
&amp;lt;/script&amp;gt;

// SecondChild.vue
&amp;lt;template&amp;gt;
	&amp;lt;div&amp;gt;SecondChild&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
export default {
	name: &quot;Parent&quot;,
    emits: [&quot;isSecondData&quot;],
    created() {
    	this.getData();
    },
    methods: {
    	async getData() {
        	const res = axios.get('data api 요청');
           	this.$emit('isSecondData', res);
        }
    }
}
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FirstChild와 SecondChild의 data 존재 여부와 상관없이 Parent는 렌더링되지 않는다. 렌더링에 조건이 걸리지 않는 것으로 보아 동작 순서에서 문제가 있다는 추측을 할 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;vue의-생명주기&quot; data-ke-size=&quot;size23&quot;&gt;vue의 생명주기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 헷갈리는 부분은 beforeUpdate 전까지이므로 거기까지만 알아보겠다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;data, computed, methods, watch 등 정의 &amp;rarr; created -&amp;gt; 인스턴스 부착 &amp;rarr; beforeMount &amp;rarr; 렌더링 &amp;rarr; mounted&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;부모-자식-간-렌더링-순서&quot; data-ke-size=&quot;size23&quot;&gt;부모 자식 간 렌더링 순서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;console.log를 찍어보면 created는 부모 &amp;rarr; 자식 순, mounted는 자식 &amp;rarr; 부모 순인 것을 알 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부모 data 등 정의 &amp;rarr; 부모 created &amp;rarr; 자식 data 등 정의 &amp;rarr; 자식 created &amp;rarr; 자식 렌더링 &amp;rarr; 자식 mounted &amp;rarr; 부모 렌더링 &amp;rarr; 부모 mounted&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;컴포넌트의-동기적-실행&quot; data-ke-size=&quot;size23&quot;&gt;컴포넌트의 동기적 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vue의 생명주기에 따른 함수 실행은 동기적으로 실행된다. 즉, 자식의 created에서 비동기를 실행하여도 부모의 생명주기가 전부 실행된 후, 비동기 로직이 실행 컨텍스트 큐에 도달한다.&lt;/p&gt;
&lt;h2 id=&quot;trouble-shooting&quot; data-ke-size=&quot;size26&quot;&gt; trouble shooting&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 emit으로 data의 유무 전달 시 부모 컴포넌트의 렌더링이 제대로 동작하지 않았던 이유는 isFirstData, isSecondData 모두 undefined가 들어가고 Parent 컴포넌트의 렌더링 조건이 false로 끝나고 나서야 비동기 로직의 emit이 실행되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;emit으로 v-if의 렌더링 조건을 반영하기 위해서는 부모 컴포넌트에 렌더링 조건을 담당하는 data를 선언하고, emit으로 그 data 값을 변경해주면 다시 렌더링이 되면서 v-if에 렌더링 조건이 반영된다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;// Parent.vue
&amp;lt;template&amp;gt;
	&amp;lt;div v-if=&quot;isFristData || isSecondData&quot;&amp;gt;
    	&amp;lt;FirstChild @get-first-data=&quot;getFirstData&quot; /&amp;gt;
        &amp;lt;SecondChild @is-second-data=&quot;isSecondData&quot; /&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
export default {
	name: &quot;Parent&quot;,
    data() {
    	return {
        	isFristData: true,
            isSecondData: true
        }
    }
    methods: {
    	getFirstData(isShow) {
        	this.isFirstData = isShow;
        },
        getSecondData(isShow) {
        	this.isSecondData = isShow;
        }
    }
}
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// FirstChild.vue
&amp;lt;template&amp;gt;
	&amp;lt;div&amp;gt;FirstChild&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
export default {
	name: &quot;Parent&quot;,
    emits: [&quot;getFirstData&quot;],
    created() {
    	this.getData();
    },
    methods: {
    	async getData() {
        	const res = axios.get('data api 요청');
           	this.$emit('getFristData', res);
        }
    }
}
&amp;lt;/script&amp;gt;

// SecondChild.vue
&amp;lt;template&amp;gt;
	&amp;lt;div&amp;gt;SecondChild&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
export default {
	name: &quot;Parent&quot;,
    emits: [&quot;getSecondData&quot;],
    created() {
    	this.getData();
    },
    methods: {
    	async getData() {
        	const res = axios.get('data api 요청');
           	this.$emit('getSecondData', res);
        }
    }
}
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>Framework/Vue</category>
      <author>씽코</author>
      <guid isPermaLink="true">https://singcotive.tistory.com/12</guid>
      <comments>https://singcotive.tistory.com/entry/vue%EC%9D%98-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%88%9C%EC%84%9C%EB%A5%BC-%ED%99%95%EC%8B%A4%ED%9E%88-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90#entry12comment</comments>
      <pubDate>Fri, 30 Jun 2023 10:31:41 +0900</pubDate>
    </item>
    <item>
      <title>react에서 setInterval 사용 시 플래그 설정을 왜 useRef로 적용해야할까?</title>
      <link>https://singcotive.tistory.com/entry/react%EC%97%90%EC%84%9C-setInterval-%EC%82%AC%EC%9A%A9-%EC%8B%9C-%ED%94%8C%EB%9E%98%EA%B7%B8-%EC%84%A4%EC%A0%95%EC%9D%84-%EC%99%9C-useRef%EB%A1%9C-%EC%A0%81%EC%9A%A9%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ybC2I/btslXnF5ZGz/rSmK9bB5rLIF6h3CqEwivk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ybC2I/btslXnF5ZGz/rSmK9bB5rLIF6h3CqEwivk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ybC2I/btslXnF5ZGz/rSmK9bB5rLIF6h3CqEwivk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FybC2I%2FbtslXnF5ZGz%2FrSmK9bB5rLIF6h3CqEwivk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4400&quot; height=&quot;2475&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;문제-정의&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt; 문제 정의&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3, 2, 1, 0 카운트 다운을 진행하는 컴포넌트를 동작시키기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;setInterval을 사용해 3에서 1초마다 countDown을 동작하고, count가 0이 되었을 때,&lt;span&gt;&amp;nbsp;&lt;/span&gt;clearInterval을 사용해 setInterval을 중지하는 로직을 구현 중이었다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;clearInterval 동작 여부를 구분하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;isIntervalStop이라는 식별자를 flag로 사용하였다.&lt;/p&gt;
&lt;h3 id=&quot;일반-변수로-플래그-선언&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;일반 변수로 플래그 선언&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;const [count, setCount] = useState(3);
const [isStartCountDown, setIsStartCountDown] = useState(false);
let isIntervalStop = false;

const buttonClickHandler = () =&amp;gt; {
  setIsStartCountDown(true);
};

useEffect(() =&amp;gt; {
  if (isStartCountDown === true) {
    const countDownInterval = setInterval(() =&amp;gt; {
      console.log(&quot;isIntervalStop:&quot;, isIntervalStop);
      if (!isIntervalStop) {
        setCount((prev) =&amp;gt; prev - 1);
      } else {
        clearInterval(countDownInterval as NodeJS.Timeout);
        setIsStartCountDown(false);
      }
    }, 1000);
  }
}, [isStartCountDown, isIntervalStop]);

useEffect(() =&amp;gt; {
  if (count === 0) {
    isIntervalStop = true;
  }
}, [count]);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;count가 0이 되었을 때,&lt;span&gt;&amp;nbsp;&lt;/span&gt;isIntervalStop을 true로 변경하지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;isIntervalStop은 state가 아니므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;isIntervalStop의 변경을 useEffect에서 감지하지 못한다. 따라서&lt;span&gt;&amp;nbsp;&lt;/span&gt;clearInterval로직이 실행되지 않는다.&lt;/p&gt;
&lt;h3 id=&quot;usestate로-플래그-선언&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;useState로 플래그 선언&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;useEffect에서 flag의 변경을 감지해서&lt;span&gt;&amp;nbsp;&lt;/span&gt;clearInterval로직을 동작할 수 있도록&lt;span&gt;&amp;nbsp;&lt;/span&gt;isIntervalStop을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;state로 선언&lt;/b&gt;하였다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;const [count, setCount] = useState(3);
const [isStartCountDown, setIsStartCountDown] = useState(false);
const [isIntervalStop, setIsIntervalStop] = useState(false);

const buttonClickHandler = () =&amp;gt; {
  setIsStartCountDown(true);
};

useEffect(() =&amp;gt; {
  if (isStartCountDown === true) {
    const countDownInterval = setInterval(() =&amp;gt; {
      console.log(&quot;isIntervalStop:&quot;, isIntervalStop);
      if (!isIntervalStop) {
        setCount((prev) =&amp;gt; prev - 1);
      } else {
        clearInterval(countDownInterval as NodeJS.Timeout);
        setIsStartCountDown(false);
      }
    }, 1000);
  }
}, [isStartCountDown, isIntervalStop]);

useEffect(() =&amp;gt; {
  if (count === 0) {
    setIsIntervalStop(true);
  }
}, [count]);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;setIsIntervalStop(true)가 실행되면, useEffect에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;isIntervalStop의 변경을 감지하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;clearInterval로직을 실행한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;isIntervalStop에 대한 console을 출력하면 true로 변경 되었다가 이내 다시 false로 출력되는 것을 볼 수 있다. 결과적으로 countDown은 멈추지 않고 계속 실행된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;262&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QtD9P/btslQlpVIXn/hu3tjKau9oDWg9kyakrUq1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QtD9P/btslQlpVIXn/hu3tjKau9oDWg9kyakrUq1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QtD9P/btslQlpVIXn/hu3tjKau9oDWg9kyakrUq1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/QtD9P/btslQlpVIXn/hu3tjKau9oDWg9kyakrUq1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;778&quot; height=&quot;262&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;262&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 확실히 알기 위해서는 react에서 state가 변경될 때, 어떤 동작을 수행하는지 알아볼 필요가 있다.&lt;/p&gt;
&lt;h2 id=&quot; state-변경-시-리렌더링&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt; state 변경 시 리렌더링&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;react 컴포넌트의 경우 부모 컴포넌트가 리렌더링될 때, props값이 변경될 때,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;state가 변경될 때&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;리렌더링된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;isIntervalStop을 state로 둘 경우 state를 변경하는 순간 useEffect 내부의&lt;span&gt;&amp;nbsp;&lt;/span&gt;countDownInterval로직이 다시 선언되고 실행된다. 그리고 기존에 실행 중이던 setInterval의 콜백함수는 종료되지 않고, 계속 실행된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 때, 기존에 실행 중이던 setInterval의 콜백함수는 리렌더링되어 새로 생성된&lt;span&gt;&amp;nbsp;&lt;/span&gt;isIntervalStop이 아닌,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;리렌더링되기 전, 종료되기 전의 컴포넌트의&lt;span&gt;&amp;nbsp;&lt;/span&gt;isIntervalStop을 참조&lt;/b&gt;하고 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존 실행 중이던 콜백함수의 실행컨텍스트의 렉시컬 환경은 리렌더링 전의 컴포넌트 함수 스코프의 렉시컬 환경을 참조하고 있고, 이 때의 컴포넌트 함수를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;외부함수&lt;/b&gt;, 그리고 외부함수의&lt;span&gt;&amp;nbsp;&lt;/span&gt;isIntervalStop이라는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;자유변수&lt;/b&gt;를 참조하고 있는 콜백함수는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;클로저&lt;/b&gt;가 된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;isIntervalStop&lt;span&gt;&amp;nbsp;&lt;/span&gt;state를 true로 변경하여도 클로저인 콜백함수는 false로 종료되어진&lt;span&gt;&amp;nbsp;&lt;/span&gt;isIntervalStop&lt;span&gt;&amp;nbsp;&lt;/span&gt;자유변수를 사용하기 때문에 종료되지 않고 계속 실행된다. 그리고 리렌더링 이후 새로 선언된&lt;span&gt;&amp;nbsp;&lt;/span&gt;isIntervalStop에 true가 할당되어, 리렌더링 이후 만들어진&lt;span&gt;&amp;nbsp;&lt;/span&gt;countDownInterval로직은 한번도 실행되지 않고&lt;span&gt;&amp;nbsp;&lt;/span&gt;clearInterval로 종료된다.&lt;/p&gt;
&lt;h2 id=&quot;useref로-플래그-선언&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt; useRef로 플래그 선언&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;useRef 함수는 current 속성을 가지고 있는 객체를 반환하는데, 인자로 넘어온 초기값을 current 속성에 할당한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;current 속성의 값을 변경하여도, 리렌더링되지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;useRef로&lt;span&gt;&amp;nbsp;&lt;/span&gt;isIntervalStop을 선언&lt;/b&gt;하게 되면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;isIntervalStop.current를 true로 변경했을 때, 컴포넌트 함수가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;리렌더링되지 않고&lt;/b&gt;, 기존에&lt;span&gt;&amp;nbsp;&lt;/span&gt;setInterval의 콜백함수도 true로 변경된&lt;span&gt;&amp;nbsp;&lt;/span&gt;isIntervalStop.current를 참조하므로, 문제없이 카운트다운이 종료된다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;const [count, setCount] = useState(3);
const [isStartCountDown, setIsStartCountDown] = useState(false);
const isIntervalStop = useRef(false);

const buttonClickHandler = () =&amp;gt; {
  setIsStartCountDown(true);
};

useEffect(() =&amp;gt; {
  if (isStartCountDown === true) {
    const countDownInterval = setInterval(() =&amp;gt; {
      console.log(&quot;isIntervalStop:&quot;, isIntervalStop);
      if (!isIntervalStop.current) {
        setCount((prev) =&amp;gt; prev - 1);
      } else {
        clearInterval(countDownInterval as NodeJS.Timeout);
        setIsStartCountDown(false);
      }
    }, 1000);
  }
}, [isStartCountDown]);

useEffect(() =&amp;gt; {
  if (count === 0) {
    isIntervalStop.current = true;
  }
}, [count]);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z9bFG/btslUNd7Bwf/KcPAHrLHy1JRe10vxrb0zk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z9bFG/btslUNd7Bwf/KcPAHrLHy1JRe10vxrb0zk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z9bFG/btslUNd7Bwf/KcPAHrLHy1JRe10vxrb0zk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/z9bFG/btslUNd7Bwf/KcPAHrLHy1JRe10vxrb0zk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;814&quot; height=&quot;330&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;♂️useref에-setinterval-할당하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt; &amp;zwj;♂️useRef에 setInterval 할당하기&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;플래그를 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;clearInterval를 처리하는 로직이 한 useEffect에 몰려있어 복잡하다. useRef에&lt;span&gt;&amp;nbsp;&lt;/span&gt;setInterval를 할당하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;setInterval를 시작하는 로직과,&lt;span&gt;&amp;nbsp;&lt;/span&gt;clearInterval를 통해 inteval을 종료하는 로직을 다른 useEffect로 분리하였다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;const [count, setCount] = useState(3);
const [isStartCountDown, setIsStartCountDown] = useState(false);
const countDownInterval = useRef&amp;lt;NodeJS.Timer | null&amp;gt;(null);

const buttonClickHandler = () =&amp;gt; {
  setIsStartCountDown(true);
};

useEffect(() =&amp;gt; {
  if (isStartCountDown === true) {
    countDownInterval.current = setInterval(() =&amp;gt; {
      setCount((prev) =&amp;gt; prev - 1);
    }, 1000);
  }
}, [isStartCountDown]);

useEffect(() =&amp;gt; {
  if (count === 0) {
    clearInterval(countDownInterval.current as NodeJS.Timer);
    countDownInterval.current = null;
    setIsStartCountDown(false);
  }
}, [count]);&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;h3 id=&quot;참조&quot; data-ke-size=&quot;size23&quot;&gt;참조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://www.daleseo.com/react-hooks-use-ref/&quot;&gt;React Hooks: useRef 사용법&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Framework/React</category>
      <author>씽코</author>
      <guid isPermaLink="true">https://singcotive.tistory.com/11</guid>
      <comments>https://singcotive.tistory.com/entry/react%EC%97%90%EC%84%9C-setInterval-%EC%82%AC%EC%9A%A9-%EC%8B%9C-%ED%94%8C%EB%9E%98%EA%B7%B8-%EC%84%A4%EC%A0%95%EC%9D%84-%EC%99%9C-useRef%EB%A1%9C-%EC%A0%81%EC%9A%A9%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C#entry11comment</comments>
      <pubDate>Fri, 30 Jun 2023 10:29:33 +0900</pubDate>
    </item>
    <item>
      <title>라우터 제한 설정 시 어떤 요소의 유무를 확인해야할까? (with JWT)</title>
      <link>https://singcotive.tistory.com/entry/%EB%9D%BC%EC%9A%B0%ED%84%B0-%EC%A0%9C%ED%95%9C-%EC%84%A4%EC%A0%95-%EC%8B%9C-%EC%96%B4%EB%96%A4-%EC%9A%94%EC%86%8C%EC%9D%98-%EC%9C%A0%EB%AC%B4%EB%A5%BC-%ED%99%95%EC%9D%B8%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C-with-JWT</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1GL8P/btslT1wUKFq/niPF6z2eYECk8ncaB5VdP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1GL8P/btslT1wUKFq/niPF6z2eYECk8ncaB5VdP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1GL8P/btslT1wUKFq/niPF6z2eYECk8ncaB5VdP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1GL8P%2FbtslT1wUKFq%2FniPF6z2eYECk8ncaB5VdP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4400&quot; height=&quot;2475&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;️인증-vs-인가&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;인증 vs 인가&lt;/h2&gt;
&lt;h3 id=&quot;인증이란&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;인증이란?&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;유저가 누구인지 확인하는 절차. 회원가입하고 로그인하는 절차를 의미한다.&lt;/p&gt;
&lt;h3 id=&quot;인가란&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;인가란?&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;인증 이후, 유저에 대한 권한을 허락하는 절차. 인증된 사용자가 특정 페이지, 특정 api 정보를 호출할 때 보내줘도 되는지 확인하는 과정을 의미한다.&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt; 접근 제한&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;접근 제한&lt;/span&gt; 은 server에서 하는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;인가&lt;/span&gt; 와는 다르게 사용자가 누구인지 식별할 수는 없기 때문에 사용자마다 페이지를 구별해서 띄워줄 수는 없다. 하지만 JWT 토큰 인증 방식의 경우, refreshToken의 유무, accessToken의 유무, userId의 유무를 통해 사용자의 &lt;b&gt;로그인 여부&lt;/b&gt; 확인할 수 있고 로그인 여부에 따라 페이지 접근 제한을 둘 수 있다.&lt;br /&gt;&lt;br /&gt;예를들어&amp;nbsp;사용자가&amp;nbsp;로그인을&amp;nbsp;해야만&amp;nbsp;들어갈&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;페이지를&amp;nbsp;요청한다면,&amp;nbsp;사용자의&amp;nbsp;로그인&amp;nbsp;여부를&amp;nbsp;확인하고,&amp;nbsp;만약&amp;nbsp;비로그인&amp;nbsp;사용자라면&amp;nbsp;해당&amp;nbsp;페이지를&amp;nbsp;보여주지&amp;nbsp;않고&amp;nbsp;로그인&amp;nbsp;페이지를&amp;nbsp;보여주는&amp;nbsp;식의&amp;nbsp;처리를&amp;nbsp;할&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;server에서 사용자에게 정보를 제공해도 되는지 확인 하는 인가 과정이 있다면, client에서는 사용자에게 페이지&amp;nbsp;&lt;br /&gt;접근 제한&amp;nbsp;을 걸어줄 수 있다. 라우터 제한을 하기 위해 어떠한 요소를 사용하는 것이 이상적일까?&lt;/blockquote&gt;
&lt;div&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;h2 id=&quot;접근-제한-요소로-무엇을-사용할까&quot; data-ke-size=&quot;size26&quot;&gt; 접근 제한 요소로 무엇을 사용할까?&lt;/h2&gt;
&lt;h3 id=&quot;useridstore&quot; data-ke-size=&quot;size23&quot;&gt;userId(store)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자를 식별할 수 있는 userId를 store에 저장했다. userId의 유무에 따라 접근 제한을 걸 수 있지만, userId가 현재도 유효한 userId인지 확인이 불가능하다.&lt;/p&gt;
&lt;h3 id=&quot;refreshtokencookie&quot; data-ke-size=&quot;size23&quot;&gt;refreshToken(cookie)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;refreshToken은 브라우저 저장소인 Cookie에 저장했다. refreshToken은 accessToken 만료 기간이 지난 직후 또는 새로고침 시 토큰을 재발급해줄 때 유효한 사용자인지 식별해주는 역할로 사용된다. 따라서 userId를 접근 제한 요소로 둘 때보다는 조금 더 보증된 사용자라고 할 수 있을 것이다. 하지만 Cookie는 새로고침 시 유지되기 때문에, 토큰 만료 기간을 정하지 않으면, api 요청을 보낼 때까지 유효한 토큰인지 알 수가 없다.&lt;/p&gt;
&lt;h3 id=&quot;accesstokenapi-header&quot; data-ke-size=&quot;size23&quot;&gt;accessToken(API header)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;accessToken은 보안적 이슈를 대비하기 위해 API header에 집어넣는데, API header는 store와 cookie와는 다르게 새로고침 시 지워진다. 따라서 token 재발급을 요청하는 주기가 store와 cookie보다 빠르다. 재발급을 요청하는 주기가 빠를수록 토큰이 유효한 토큰인지 서버에서 확인하는 주기가 빠르다는 것이므로 사용자가 더욱 보증되었다고 생각할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;접근 제한의 요소에 대해 고민하면서 느낀 부분은 api 요청을 하고 server에서 유효한 토큰인지 확인하지 않으면 위에서 다룬 세 가지 모두 존재 유무로 인증된 사용자라고 확신하기에는 부족하다는 것이다. 그럼에도 api 요청 시 사용자 식별에 accessToken이 주로 쓰이고, 토큰 재발급 주기가 나머지 둘보다 빠르기 때문에 client에서도 accessToken을 접근 제한 요소로 두는 것이 이상적이다 생각한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;blockquote style=&quot;color: #000000;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;h3 id=&quot;참조&quot; data-ke-size=&quot;size23&quot;&gt;참조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://velog.io/@aaronddy/%EC%9D%B8%EC%A6%9DAuthentication%EA%B3%BC-%EC%9D%B8%EA%B0%80Authorization&quot;&gt;인증(Authentication)과 인가(Authorization)&lt;/a&gt;&lt;br /&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://cotak.tistory.com/108&quot;&gt;[React.js] 라우터를 이용한 접근 제한 구현 (Access Control &amp;amp; Authentication)&lt;/a&gt;&lt;br /&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://velog.io/@yaytomato/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0&quot;&gt;  프론트에서 안전하게 로그인 처리하기 (ft. React)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Web</category>
      <author>씽코</author>
      <guid isPermaLink="true">https://singcotive.tistory.com/10</guid>
      <comments>https://singcotive.tistory.com/entry/%EB%9D%BC%EC%9A%B0%ED%84%B0-%EC%A0%9C%ED%95%9C-%EC%84%A4%EC%A0%95-%EC%8B%9C-%EC%96%B4%EB%96%A4-%EC%9A%94%EC%86%8C%EC%9D%98-%EC%9C%A0%EB%AC%B4%EB%A5%BC-%ED%99%95%EC%9D%B8%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C-with-JWT#entry10comment</comments>
      <pubDate>Fri, 30 Jun 2023 10:22:51 +0900</pubDate>
    </item>
    <item>
      <title>Oauth Login을 통해 추가정보 입력받아 회원가입하기(feat : React, Spring)</title>
      <link>https://singcotive.tistory.com/entry/Oauth-Login%EC%9D%84-%ED%86%B5%ED%95%B4-%EC%B6%94%EA%B0%80%EC%A0%95%EB%B3%B4-%EC%9E%85%EB%A0%A5%EB%B0%9B%EC%95%84-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85%ED%95%98%EA%B8%B0feat-React-Spring</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/23ppg/btslR2XDwFQ/F1PNR4oCAjGAdzjYW2D5qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/23ppg/btslR2XDwFQ/F1PNR4oCAjGAdzjYW2D5qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/23ppg/btslR2XDwFQ/F1PNR4oCAjGAdzjYW2D5qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F23ppg%2FbtslR2XDwFQ%2FF1PNR4oCAjGAdzjYW2D5qk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4400&quot; height=&quot;2475&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;용어-정리&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;용어 정리&lt;/h2&gt;
&lt;h3 id=&quot;client&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Client&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자사 서비스가 실질적으로 운영되는 사이트.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 React를 통해 Client를 띄워주고 있다.&lt;/p&gt;
&lt;h3 id=&quot;authorization-server&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Authorization Server&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Google, Kakao, Naver 등 인증에 필요한 아이템을 제공해주는 서버.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Client는 ID, PW로 로그인 요청을 해서 Authorization Code를 발급받을 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;resource-server&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Resource Server&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Google, Kakao, Naver 등 사용자의 개인정보를 가지고있는 애플리케이션 서버.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Server는 Authorization Code를 넘겨 개인정보를 응답받을 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;server&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Server&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자사 서비스의 Backend를 담당하는 서버.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Client로부터 받은 Authorization Code를 Resource Server로 전달해 개인정보를 응답받고 DB Server에서 회원중복검사를 한 뒤, 로그인 또는 회원가입 로직을 수행한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 Server로 Spring Boot를 사용한다.&lt;/p&gt;
&lt;h3 id=&quot;db-server&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;DB Server&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자사 서비스의 회원정보를 관리하는 DB 서버.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 DB Server로 MySQL을 사용한다.&lt;/p&gt;
&lt;h2 id=&quot;로그인-flow&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;로그인 flow&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Oauth 로그인을 할 수 있는 서비스는 무수히 많다. 그 중에 Kakao 로그인을 통한 로그인을 설명하겠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;793&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clvCl0/btslR5UsqrG/Ob8zc1lag5ZSoSXnbub0Ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clvCl0/btslR5UsqrG/Ob8zc1lag5ZSoSXnbub0Ak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clvCl0/btslR5UsqrG/Ob8zc1lag5ZSoSXnbub0Ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclvCl0%2FbtslR5UsqrG%2FOb8zc1lag5ZSoSXnbub0Ak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;793&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;793&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;flow-상세설명&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;flow 상세설명&lt;/h2&gt;
&lt;h3 id=&quot;1-로그인-요청&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. 로그인 요청&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서비스 접근을 위해 미리 발급 받은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;client ID&lt;/b&gt;와 Spring Server로 Authorization Code를 전달하는 로직을 담당하는 라우터주소를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Redirect URI&lt;/b&gt;에 담아 요청을 보낸다.&lt;/p&gt;
&lt;h3 id=&quot;2-로그인-페이지-응답&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. 로그인 페이지 응답&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Kakao에서 로그인 페이지를 띄워준다.&lt;/p&gt;
&lt;h3 id=&quot;3-카카오-계정으로-로그인&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3. 카카오 계정으로 로그인&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Kakao 로그인 페이지에서 ID, PW를 입력 후 로그인을 시도한다.&lt;/p&gt;
&lt;h3 id=&quot;4-authorization_code-응답&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4. authorization_code 응답&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Authorization Code를  Query에 담아 Redirect URI로 리다이렉션된다.&lt;/p&gt;
&lt;h3 id=&quot;5-spring-서버로-로그인-요청&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;5. Spring 서버로 로그인 요청&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Authorization Code를 담아 Spring Server로 로그인 요청을 보낸다.&lt;/p&gt;
&lt;h3 id=&quot;6-access-token-및-회원정보-요청&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;6. access token 및 회원정보 요청&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Client에서 받은 Authorization Code를 포함해서 Kakao Resource Server로 access token과 회원정보를 요청한다.&lt;/p&gt;
&lt;h3 id=&quot;7-access-token-발급-및-회원정보-응답&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;7. access token 발급 및 회원정보 응답&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;authorization code가 올바른 code이면 access token과 Server가 요청한 회원정보를 응답한다.&lt;/p&gt;
&lt;h3 id=&quot;8-회원-중복-확인-요청&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;8. 회원 중복 확인 요청&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Kakao Resource Server로부터 전달받은 회원정보를 통해 DB Server에 회원정보가 이미 존재하는지 확인한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래에서는 signIn 과정과 signUp 과정을 구분하여 설명하겠다.&lt;/p&gt;
&lt;h2 id=&quot;로그인&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;로그인&lt;/h2&gt;
&lt;h3 id=&quot;1-jwt-token-발급&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. JWT token 발급&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;회원정보가 이미 존재한다면, 회원가입이 아닌 로그인을 해야하므로, responseType에 &quot;signIn&quot;을 넣어주고 즉시 JWT Token을 발급해준다.&lt;/p&gt;
&lt;h3 id=&quot;2-로그인-처리&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. 로그인 처리&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;응답 받은 refreshToken을 쿠키에 저장하고, accessToken을 API Header에 저장한다.&lt;/p&gt;
&lt;h2 id=&quot;회원가입&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;회원가입&lt;/h2&gt;
&lt;h3 id=&quot;1-회원가입-로직-요청&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. 회원가입 로직 요청&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;회원정보가 존재하지 않으므로 회원가입을 진행해야 한다. 따라서 responseType에 &quot;signUp&quot;을 넣어준다.&lt;/p&gt;
&lt;h3 id=&quot;2-추가정보-입력-처리&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. 추가정보 입력 처리&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;추가정보를 입력하는 페이지로 리다이렉션 해준다. 사용자가 추가정보를 입력한 후 회원가입 완료하기 버튼을 클릭한다.&lt;/p&gt;
&lt;h3 id=&quot;3-회원가입-요청&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3. 회원가입 요청&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Server로 사용자가 입력한 추가정보를 담아 회원가입을 요청한다.&lt;/p&gt;
&lt;h3 id=&quot;4-회원정보-추가&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4. 회원정보 추가&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Server에서 회원정보를 받아 DB Server에 넣어준다.&lt;/p&gt;
&lt;h3 id=&quot;5-jwt-token-발급&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;5. JWT Token 발급&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Server에서 JWT Token을 발급해서 Client에 응답한다.&lt;br /&gt;Client는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;2. 로그인처리&lt;/span&gt;와 같이 JWT Token을 저장한다.&lt;/p&gt;
&lt;h2 id=&quot;고민한-내용&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;고민한 내용&lt;/h2&gt;
&lt;h3 id=&quot;oauth-회원가입-시-회원정보를-자사-server에서-관리하려면-어떻게-해야할까&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Oauth 회원가입 시 회원정보를 자사 Server에서 관리하려면 어떻게 해야할까?&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대부분의 블로그에서는 Client와 Oauth Server와의 관계 속에서 로그인과 회원가입을 처리하는 내용이 담겨있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는 회원정보를 따로 관리할 필요가 있었다. MySQL을 이용해 User Table을 독자적으로 관리하고, Spring Boot에서 JWT Token을 발급해서 인증&amp;amp;인가를 처리한다.&lt;/p&gt;
&lt;h3 id=&quot;oauth-회원가입-시-추가정보를-입력해야할-경우-어떤-flow로-진행이-될까&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Oauth 회원가입 시 추가정보를 입력해야할 경우, 어떤 flow로 진행이 될까?&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;추가정보를 입력할 필요가 없는 경우,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;8. 회원 중복 확인 요청&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 후 로그인 또는 회원가입을 진행하면 된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;추가정보를 입력해야하는 경우,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;8. 회원 중복 확인 요청&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 후 회원가입을 진행하는 과정은 아래와 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;추가정보 입력 페이지로 리다이렉션한다.&lt;/li&gt;
&lt;li&gt;추가정보를 담아 JWT token을 발급해주는 api로 요청을 보낸다.&lt;/li&gt;
&lt;li&gt;DB Server에 회원정보를 저장하고, JWT token을 발급한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;h3 id=&quot;참조&quot; data-ke-size=&quot;size23&quot;&gt;참조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://hwannny.tistory.com/92&quot;&gt;Oauth 2.0 개념 정리&lt;/a&gt;&lt;br /&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://rrecoder.tistory.com/148&quot;&gt;React Oauth 로그인 구현하기&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Web</category>
      <author>씽코</author>
      <guid isPermaLink="true">https://singcotive.tistory.com/9</guid>
      <comments>https://singcotive.tistory.com/entry/Oauth-Login%EC%9D%84-%ED%86%B5%ED%95%B4-%EC%B6%94%EA%B0%80%EC%A0%95%EB%B3%B4-%EC%9E%85%EB%A0%A5%EB%B0%9B%EC%95%84-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85%ED%95%98%EA%B8%B0feat-React-Spring#entry9comment</comments>
      <pubDate>Fri, 30 Jun 2023 10:16:20 +0900</pubDate>
    </item>
    <item>
      <title>CSR vs SSR</title>
      <link>https://singcotive.tistory.com/entry/CSR-vs-SSR</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cY0Hnh/btslRRvrk14/cm6kOjzCob2WWloMiFkYo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cY0Hnh/btslRRvrk14/cm6kOjzCob2WWloMiFkYo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cY0Hnh/btslRRvrk14/cm6kOjzCob2WWloMiFkYo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcY0Hnh%2FbtslRRvrk14%2Fcm6kOjzCob2WWloMiFkYo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4400&quot; height=&quot;2475&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id=&quot;ssrserver-side-rendering&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;SSR(Server Side Rendering)&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;server에서 HTML을 정제해서 브라우저로 전송해 페이지를 띄워주는 방식&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;server에서 HTML을 완성해서 보내주기 때문에 브라우저에서 우클릭 후&lt;span&gt;&amp;nbsp;&lt;/span&gt;페이지 소스 보기&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;완성된 html&lt;/b&gt;을 볼 수 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 완성된 페이지를 볼 수 있는 속도가 CSR에 비해 빠르다.&lt;/li&gt;
&lt;li&gt;완성된 html이므로 검색엔진이 사이트의 내용을 파악할 수 있다. =&amp;gt; 검색에 용이하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자의 요청에 따라 동적으로 페이지를 변경해주는 속도가 CSR에 비해 느리다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;csrclient-side-rendering&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;CSR(Client Side Rendering)&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;server에서 완성되지 않은 html을 브라우저로 전송하고, 사용자의 요청에 의해 브라우저 단에서 js로 나머지 컴포넌트를 렌더링하는 방식&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;완성되지 않은 html을 브라우저로 보내주기 때문에 브라우저에서 우클릭 후&lt;span&gt;&amp;nbsp;&lt;/span&gt;페이지 소스 보기&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;미완성된 html&lt;/b&gt;을 볼 수 있다. (보통 전송된 html을 보면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt;div id=&quot;app&quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;만 있을 확률이 높다.)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자의 요청에 따라 동적으로 페이지를 변경해주는 속도가 SSR에 비해 빠르다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 완성된 페이지를 볼 수 있는 속도가 SSR에 비해 느리다.&lt;/li&gt;
&lt;li&gt;html에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt;div id=&quot;app&quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;밖에 존재하지 않으므로 검색엔진이 사이트를 검색하기 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;csr-or-ssr을-구현하는-방식은-무엇이-있을까&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;CSR or SSR을 구현하는 방식은 무엇이 있을까?&lt;/h2&gt;
&lt;h3 id=&quot;csr&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;CSR&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Ajax를 사용하는 방식&lt;/li&gt;
&lt;li&gt;vanillaJS를 통해 컴포넌트를 rendering 하는 방식&lt;/li&gt;
&lt;li&gt;Angular, React, VueJS 등의 CSR 프레임 워크&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;ssr&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;SSR&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;nodeJS, Spring, Django 등을 통해 완성된 html을 브라우저로 전송하는 방식&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설명드린 방식은 &quot;~~한 방식이면 CSR만 구현할 수 있다&quot;라는 의미가 아니고 &quot; ~~한 방식을 사용하면 CSR을 구현할 수 있다&quot; 라는 의미이다. 예를 들어 nodeJS를 통해 SSR을 하면서 vanillaJS를 통해 CSR을 동시에 할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;의문점&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;의문점&lt;/h2&gt;
&lt;h3 id=&quot;client-server가-따로-존재하면-csr이다&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Client Server가 따로 존재하면 CSR이다?&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;React, VueJS 프레임워크를 사용하면 Client Server를 구동하고 CSR 방식을 사용하게 된다. 다만 nodeJS를 통해 Client Server를 구동하고 SSR 방식을 사용할 수도 있다. 따라서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Client Server의 존재 여부와 CSR, SSR 방식은 상관이 없다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 id=&quot;서비스는-csr-또는-ssr-둘-중-하나만-선택해서-사용할-수-있다&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;서비스는 CSR 또는 SSR 둘 중 하나만 선택해서 사용할 수 있다?&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CSR과 SSR의 장단점이 뚜렷하기에 최근에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;CSR과 SSR을 같이 구현하는 방식을 채택하는 곳도 많다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;React에서는 Next.js, VueJS에서는 Nuxt.js를 사용해 CSR과 더불어 SSR의 기능을 같이 구현하는 방식을 취한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;h3 id=&quot;참조&quot; data-ke-size=&quot;size23&quot;&gt;참조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://zuminternet.github.io/vue-ssr/&quot;&gt;Vue SSR 제대로 적용하기 (feat. Vanilla SSR)&lt;/a&gt;&lt;br /&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://velog.io/@commitnpush/CSR-vs-SSR#3-csrclient-side-rendering%EC%99%80-spasingle-page-application&quot;&gt;CSR vs SSR&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Web</category>
      <author>씽코</author>
      <guid isPermaLink="true">https://singcotive.tistory.com/8</guid>
      <comments>https://singcotive.tistory.com/entry/CSR-vs-SSR#entry8comment</comments>
      <pubDate>Fri, 30 Jun 2023 10:13:39 +0900</pubDate>
    </item>
    <item>
      <title>옵저버패턴(Observer Pattern)</title>
      <link>https://singcotive.tistory.com/entry/%EC%98%B5%EC%A0%80%EB%B2%84%ED%8C%A8%ED%84%B4Observer-Pattern</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bi4UWD/btsj6OzFu5Y/ccv84euqqv6PxjOT65EPU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bi4UWD/btsj6OzFu5Y/ccv84euqqv6PxjOT65EPU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bi4UWD/btsj6OzFu5Y/ccv84euqqv6PxjOT65EPU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbi4UWD%2Fbtsj6OzFu5Y%2Fccv84euqqv6PxjOT65EPU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4400&quot; height=&quot;2475&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;옵저버패턴이란&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;옵저버패턴이란?&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;어떤 객체의 상태가 변할 때 그와 연관된 객체들에게 알림을 보내는 디자인 패턴&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;옵저버 패턴에는 주체 객체와 상태의 변경을 알아야 하는 관찰 객체(Observer Object)가 존재하며 이들의 관계는 1:1이 될 수도 있고 1:N이 될 수도 있다.&lt;/p&gt;
&lt;h2 id=&quot;옵저버-패턴의-장단점&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;옵저버 패턴의 장단점&lt;/h2&gt;
&lt;h3 id=&quot;장점&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;실시간으로 한 객체의 변경사항을 다른 객체에 전파할 수 있다.&lt;/li&gt;
&lt;li&gt;느슨한 결합으로 시스템이 유연하고 객체 간의 의존성을 제거할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;단점&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;너무 많이 사용하게 되면, 상태 관리가 힘들 수 있다.&lt;/li&gt;
&lt;li&gt;데이터 배분에 문제가 생기면 자칫 큰 문제로 이어질 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;옵저버패턴의-간단한-예시&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;옵저버패턴의 간단한 예시&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;크루(Crew)는 코치(Coach)를 observing 하고 있고, 코치가 하는 일을 모두 notify 받아야 한다. Singco라는 코치와 Loopy, Mimmi, Fry 라는 크루들이 있다고 가정해보자.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;코치는 크루들을 등록하고, 해제하고, 크루들에게 행동을 알리는 세가지 행동을 가지고 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 코치와 크루는 아래와 같은 인터페이스를 갖는다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;interface Crew {
  update(msg: String);
}

interface Coach {
  subscribe(crew: Crew);
  unsubscribe(crew: Crew);
  notifyCrew(msg: String);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음은 코치인 Singco 클래스를 만들어보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;class Singco implements Coach {
  private crews = [];

  public eatFood(): void {
    console.log(&quot;Singco가 밥을 먹는다.&quot;);
    this.notifyCrew(&quot;나 밥먹었다.&quot;);
  }

  public runaway(): void {
    console.log(&quot;Singco가 도망간다.&quot;);
    this.notifyCrew(&quot;나 도망갔다.&quot;);
  }

  public sleep(): void {
    console.log(&quot;Singco가 잔다.&quot;);
    this.notifyCrew(&quot;나 잔다.&quot;);
  }

  public subscribe(crew: Crew): void {
    this.crews.push(crew);
  }

  public unsubscribe(crew: Crew): void {
    this.crews = this.crews.filter((member) =&amp;gt; member !== crew);
  }

  public notifyCrew(msg: String): void {
    this.crews.forEach((crew) =&amp;gt; crew.update(msg));
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Singco 클래스는 밥먹기, 도망가기, 잠자기라는 세가지 행동을 갖는다. 그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;크루들의 update 메서드를 각각 호출&lt;/b&gt;한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;옵저버인 Loopy, Mimmi, Fry 크루들은 코치인 Singco가 행동을 관찰하다가 Singco가 행동을 할 때, update 로직을 실행한다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;class Loopy implements Crew {
  public update(msg: String): void {
    console.log(`Loopy 수신 : ${msg}`);
  }
}

class Mimmi implements Crew {
  public update(msg: String): void {
    console.log(`Mimmi 수신 : ${msg}`);
  }
}
class Fry implements Crew {
  public update(msg: String): void {
    console.log(`Fry 수신 : ${msg}`);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;singco가 sleep이라는 행동을 하게되면&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;const singco = new Singco();
const loopy = new Loopy();
const mimmi = new Mimmi();
const fry = new Fry();

singco.subscribe(loopy);
singco.subscribe(mimmi);
singco.subscribe(fry);

singco.sleep();

// Singco가 잔다.
// Loopy 수신 : 나 잔다.
// Mimmi 수신 : 나 잔다.
// Fry 수신 : 나 잔다.&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Loopy 구독을 해지한 뒤에 sleep을 다시 실행하면&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;singco.unsubscribe(loopy);
singco.sleep();

// Singco가 잔다.
// Mimmi 수신 : 나 잔다.
// Fry 수신 : 나 잔다.&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;구독한 크루들만 코치의 행동에 로직을 실행하게 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;h3 id=&quot;참조&quot; data-ke-size=&quot;size23&quot;&gt;참조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://pjh3749.tistory.com/266&quot;&gt;[디자인패턴] 옵저버 패턴 (Observer Pattern) 아주 간단하게 정리해보기&lt;/a&gt;&lt;br /&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://coding-factory.tistory.com/710&quot;&gt;[Design Pattern] 옵저버 패턴(Observer Pattern)에 대하여&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Paradigm</category>
      <author>씽코</author>
      <guid isPermaLink="true">https://singcotive.tistory.com/6</guid>
      <comments>https://singcotive.tistory.com/entry/%EC%98%B5%EC%A0%80%EB%B2%84%ED%8C%A8%ED%84%B4Observer-Pattern#entry6comment</comments>
      <pubDate>Fri, 16 Jun 2023 01:27:36 +0900</pubDate>
    </item>
    <item>
      <title>제너레이터(Generator)</title>
      <link>https://singcotive.tistory.com/entry/%EC%A0%9C%EB%84%88%EB%A0%88%EC%9D%B4%ED%84%B0Generator</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/slxHc/btsj8hg0RSk/yZDM7wau6AOAw9xuIELOxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/slxHc/btsj8hg0RSk/yZDM7wau6AOAw9xuIELOxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/slxHc/btsj8hg0RSk/yZDM7wau6AOAw9xuIELOxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FslxHc%2Fbtsj8hg0RSk%2FyZDM7wau6AOAw9xuIELOxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4400&quot; height=&quot;2475&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;h2 id=&quot;제너레이터란&quot; data-ke-size=&quot;size26&quot;&gt;제너레이터란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 함수는? 하나의 값 만을 반환한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제너레이터를 사용하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;여러 개의 값을 필요에 따라 하나씩 반환할 수 있다.&lt;/li&gt;
&lt;li&gt;제너레이터와 이터러블 객체를 함께 사용하면 손쉽게 데이터 흐름 집합체를 만들 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;제너레이터-함수&quot; data-ke-size=&quot;size26&quot;&gt;제너레이터 함수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;function*&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제너레이터 함수를 호출하면 코드가 실행되지 않고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;제너레이터 객체가 반환된다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function* generatorFunc() {
  yield 1;
  yield 2;
  return 3;
}

let generator = generatorFunc();
console.log(generator); // Object [Generator] {}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;next&quot; data-ke-size=&quot;size23&quot;&gt;next()&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제너레이터 주요 메서드&lt;/li&gt;
&lt;li&gt;가장 가까운&lt;span&gt;&amp;nbsp;&lt;/span&gt;yield &amp;lt;value&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;문을 만날 때까지 실행이 지속&lt;/li&gt;
&lt;li&gt;yield &amp;lt;value&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;문을 만나면 실행이 멈추고 산출하고자 하는 값인&lt;span&gt;&amp;nbsp;&lt;/span&gt;value&lt;span&gt;&amp;nbsp;&lt;/span&gt;가 바깥 코드에 반환&lt;/li&gt;
&lt;li&gt;두 프로퍼티를 가진 객체를 반환
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;value: 산출 값&lt;/li&gt;
&lt;li&gt;done: 함수 코드 실행이 끝났으면 true, 아니라면 false&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;next() 메서드를 입력할 때마다 다음 가장 가까운&lt;span&gt;&amp;nbsp;&lt;/span&gt;yield &amp;lt;value&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;문을 만날 때까지 실행이 지속&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;function* generatorFunc() {
  yield 1;
  yield 2;
  return 3;
}

let generator = generatorFunc();
let one = generator.next();
console.log(one); // { value: 1, done: false }
let two = generator.next();
console.log(two); // { value: 2, done: false }
let three = generator.next();
console.log(three); // { value: 3, done: true }
let four = generator.next();
console.log(four); // { value: undefined, done: true }&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;제너레이터와-이터러블&quot; data-ke-size=&quot;size26&quot;&gt;제너레이터와 이터러블&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제너레이터는&lt;span&gt;&amp;nbsp;&lt;/span&gt;iterable&lt;/li&gt;
&lt;li&gt;for...of 반복문을 사용해 값을 얻을 수 있음&lt;/li&gt;
&lt;li&gt;return 값을 반환할 수 없음&lt;span&gt;&amp;nbsp;&lt;/span&gt;return 3 &amp;rarr; yield 3&lt;/li&gt;
&lt;li&gt;제너레이터 객체를&lt;span&gt;&amp;nbsp;&lt;/span&gt;spread 연산자를 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;iterable array로 바꿀 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function* generatorFunc() {
  yield 1;
  yield 2;
  return 3;
}

let generator = generatorFunc();
for (let value of generator) {
  console.log(value);
}
// 1, 2

let sequence = [...generatorFunc()];
console.log(sequence); // [1, 2]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;이터러블-대신-제너레이터-사용하기&quot; data-ke-size=&quot;size26&quot;&gt;이터러블 대신 제너레이터 사용하기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;iterator 간단하게 알아보기객체는 iterable하지 않다. spread 연산자는 사용할 수 있다.단, iterator 객체를 사용할 경우 next() 메서드를 사용해 간단한 범위 반복자를 형성할 수 있다.&lt;/li&gt;
&lt;li&gt;key: value 로 이루어진 쌍이므로, 출력은 불가능하다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;let&lt;/span&gt; obj &lt;span style=&quot;color: #000000;&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #000000;&quot;&gt;{&lt;/span&gt; one&lt;span style=&quot;color: #000000;&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;color: #000000;&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;,&lt;/span&gt; two&lt;span style=&quot;color: #000000;&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;color: #000000;&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;,&lt;/span&gt; &lt;span style=&quot;color: #000000;&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;;&lt;/span&gt; console&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;...&lt;/span&gt;obj&lt;span style=&quot;color: #000000;&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;;&lt;/span&gt; &lt;span style=&quot;color: #000000;&quot;&gt;// TypeError: Found non-callable @@iterator&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;// iterator
let range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() {
    return {
      current: this.from,
      last: this.to,
      next() {
        if (this.current &amp;lt;= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      },
    };
  },
};

console.log(...range); // 1, 2, 3, 4, 5

// generator
range = {
  from: 1,
  to: 5,

  *[Symbol.iterator]() {
    // [Symbol.iterator]: function*()를 짧게 줄임
    for (let value = this.from; value &amp;lt;= this.to; value++) {
      yield value;
    }
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;제너레이터-컴포지션&quot; data-ke-size=&quot;size26&quot;&gt;제너레이터 컴포지션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제너레이터 안에 제너레이터를 임베딩할 수 있는 기능&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function* generateSequence(start, end) {
  for (let i = start; i &amp;lt;= end; i++) yield i;
}

function* generatePasswordCodes() {
  // 0..9
  yield* generateSequence(48, 57);

  // A..Z
  yield* generateSequence(65, 90);

  // a..z
  yield* generateSequence(97, 122);

  // for (let i = 48; i &amp;lt;= 57; i++) yield i;

  // for (let i = 65; i &amp;lt;= 90; i++) yield i;

  // for (let i = 97; i &amp;lt;= 122; i++) yield i;
}

let str = &quot;&quot;;

for (let code of generatePasswordCodes()) {
  str += String.fromCharCode(code);
}

console.log(str); // 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;yield를-사용해-제너레이터-안밖으로-정보-교환하기&quot; data-ke-size=&quot;size26&quot;&gt;'yield'를 사용해 제너레이터 안.밖으로 정보 교환하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;generator.next(arg)&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 값을 안, 밖으로 전달할 수 있다. arg는 yield의 결과&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function* gen() {
  let ask1 = yield &quot;2 + 2 = ?&quot;;

  console.log(ask1); // 4

  let ask2 = yield &quot;3 * 3 = ?&quot;;

  console.log(ask2); // 9
}

let generator = gen();

console.log(generator.next().value); // **&quot;2 + 2 = ?&quot;** 

console.log(generator.next(4).value); // &quot;3 * 3 = ?&quot;

console.log(generator.next(9).done); // true&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;첫 generator.next() 메서드는 인자의 유무에 상관없이 첫 yield의 값을 받는다.&lt;br /&gt;&quot;2 + 2 = ?&quot;&lt;/li&gt;
&lt;li&gt;두번째&lt;span&gt;&amp;nbsp;&lt;/span&gt;generator.next(4)&lt;span&gt;&amp;nbsp;&lt;/span&gt;의 argument인 4는 바로 전 yield가 들어간 ask1의 위치에 들어간다. 그리고 두번째 yield의 값을 받은 객체를 받는다.&lt;br /&gt;ask1 = 4&lt;br /&gt;{ value:&quot;3 * 3 = ?&quot;, done: false }&lt;/li&gt;
&lt;li&gt;세번째 generator.next(9)의 argument인 9 는 바로 전 yield가 들어간 ask2의 위치에 들어간다. 그리고 다음 yield가 없기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;{ value: undefined, done: true }를 받는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제너레이터와 프로미스를 이용한 비동기 처리!&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;generatorthrow&quot; data-ke-size=&quot;size26&quot;&gt;generator.throw&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부코드가 에러를 만들거나 던질 수 있다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function* gen() {
  try {
    let result = yield &quot;2 + 2 = ?&quot;; // (1)

    console.log(
      &quot;위에서 에러가 던져졌기 때문에 실행 흐름은 여기까지 다다르지 못합니다.&quot;
    );
  } catch (e) {
    console.log(e); // 에러 출력
  }
}

let generator = gen();

let question = generator.next().value;
console.log(question); // 2 + 2 = ?

generator.throw(new Error(&quot;데이터베이스에서 답을 찾지 못했습니다.&quot;)); // (2)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) 에서 제너레이터로 던진 에러는 yield와 함께 (1)로 던져진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;던져진 에러는 catch로 전달된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제너레이터 바깥에서 error를 잡을 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;verilog&quot;&gt;&lt;code&gt;function* generate() {
  let result = yield &quot;2 + 2 = ?&quot;; // Error in this line
}

let generator = generate();

let question = generator.next().value;

try {
  generator.throw(new Error(&quot;데이터베이스에서 답을 찾지 못했습니다.&quot;));
} catch(e) {
  alert(e); // 에러 출력&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>Language/JavaScript</category>
      <author>씽코</author>
      <guid isPermaLink="true">https://singcotive.tistory.com/5</guid>
      <comments>https://singcotive.tistory.com/entry/%EC%A0%9C%EB%84%88%EB%A0%88%EC%9D%B4%ED%84%B0Generator#entry5comment</comments>
      <pubDate>Fri, 16 Jun 2023 01:26:58 +0900</pubDate>
    </item>
    <item>
      <title>MVVM 패턴</title>
      <link>https://singcotive.tistory.com/entry/MVVM-%ED%8C%A8%ED%84%B4</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nAyW1/btsj9CLP841/CfaLLz1O6NPkZretrnkXDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nAyW1/btsj9CLP841/CfaLLz1O6NPkZretrnkXDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nAyW1/btsj9CLP841/CfaLLz1O6NPkZretrnkXDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnAyW1%2Fbtsj9CLP841%2FCfaLLz1O6NPkZretrnkXDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4400&quot; height=&quot;2475&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;nbsp;목차&lt;br /&gt;1. 비즈니스 로직과 프레젠테이션 로직&lt;br /&gt;2. MVVM이란?&lt;br /&gt;3. Recoil을 접목시킨 MVVM 패턴&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번에 collusic-new, about 프로젝트에 mvvm 패턴을 적용하면서 디자인 패턴에 대해 처음 공부하게 되었다. model, view, viewmodel이란 각각의 용어도 생소했지만, 무엇보다 mvvm을 react에 적용시키는 방식이 예제마다 너무 달라서 혼란스러웠다. 또한, 뷰로직과 비즈니스 로직이 정확히 어떤 차이가 있는지 와닿지 않아 프로젝트를 하는 도중, 코드를 분리하면서도 개운치가 못했다. 따라서 오늘 내용은 비즈니스 로직과 프레젠테이션 로직(뷰 로직과 비슷한 개념인 것 같다...)부터 얘기해보려고 한다.&lt;/p&gt;
&lt;h2 id=&quot;1-비즈니스-로직과-프레젠테이션-로직&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;1. 비즈니스 로직과 프레젠테이션 로직&lt;/h2&gt;
&lt;h3 id=&quot;비즈니스-로직&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;비즈니스 로직?&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위키피디아에서 정의하는 비즈니스 로직은 다음과 같다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비지니스 로직(Business Logic) 또는 도메인 로직(Domain Logic)은현실 세계에서 어떻게 데이터를 만들고 저장하고 바꿀 것인지에 대한 비지니스 규칙(Business Rules)을 인코드(Encodes) 한,소프트 웨어 안의 프로그램의 한 부분이다.&lt;br /&gt;&amp;mdash;위키피디아(영문판),&amp;nbsp;Business Logic&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;비즈니스 로직은 사용자가 원하는 요청에 의해 어떠한 데이터를 어떻게 CRUD 하거나, 가공할 것인지를 말한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특정 책에 대한 상세정보를 Read하는 비즈니스 로직을 가진&lt;span&gt;&amp;nbsp;&lt;/span&gt;detailBook(bookId)&lt;span&gt;&amp;nbsp;&lt;/span&gt;만들어보자.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;async detailBook(bookId: number) {
    const { data } = await axios.get(`api/books/${bookId}`);
    return data;
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위와 같이 간단한 api요청을 통해 데이터를 불러오는 비즈니스 로직도 있지만, 받은 데이터들을 사용자의 입맛에 맞게 가공하는 비즈니스 로직도 존재한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 특정 책을 하트를 눌렀을 때, 사용자 좋아하는 책 목록 데이터에 특정 책을 추가하는 비즈니스 로직과 책 목록을 반환하는 비즈니스 로직을 만들어보자.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;public addFavoriteList(bookId: number, bookTitle: string) {
    this.favoriteList.push({ bookId, bookTitle });
}

public getFavoriteList() {
    return this.favoriteList;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;프레젠테이션-로직&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;프레젠테이션 로직?&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위키피디아에서 정의하는 프레젠테이션 로직은 다음과 같다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레젠테이션 로직은 비지니스 오브젝트(비지니스 로직)가 팝업 화면과 드롭 다운 매뉴 중어떤 것을 선택할 것인지와 같이소프트웨어 사용자에게 표시되는 로직을 말한다. 프레젠테이션 로직에서 비지니스 로직을 분리하는 것은소프트웨어 개발 및 프레젠테이션과 컨텐츠를 분리하려하는 사례의(an instance of) 중요한 관심사이다.&lt;br /&gt;&amp;mdash;위키피디아(영문판),&amp;nbsp;Presentation Logic&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프레젠테이션 로직(뷰 로직)은 수많은 비즈니스 로직들을 어떻게 보여줄 것인지에 대한 로직이다. 즉, 사용자 인터페이스(UI)를 어떻게 표시할까에 대한 로직이라고 할 수 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기여작 목록을 보여줄 때 사용자의 프로필 사진이 있으면 그 사진을 프로필로, 아니면 미리 저장해놓은 defaultProfile로 보여주는 뷰 로직을 만들어보자.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;{contributeList.map((project, idx) =&amp;gt; (
  &amp;lt;img
    src={
      project.userProfile !== undefined
        ? project.userProfile
        : `../../assets/defaultProfile/defaultProfile.png`
    }
    alt={project.userEmail}
    className=&quot;profile&quot;
  /&amp;gt;
))}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;2-mvvm이란&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. MVVM이란?&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;본격적으로 MVVM 패턴에 대한 설명을 하려고 한다. 먼저 MVVM 패턴은 비즈니스 로직과 프레젠테이션 로직을 UI로 부터 분리하기 위해 만들어졌다. 비즈니스 로직과 프레젠테이션 로직을 UI로부터 분리하게 되면 유지보수, 재사용, 테스트가 쉬워진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Jrav3/btsj9DKKFbt/QrcWEgimR9EzGeDNtQaCF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Jrav3/btsj9DKKFbt/QrcWEgimR9EzGeDNtQaCF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jrav3/btsj9DKKFbt/QrcWEgimR9EzGeDNtQaCF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJrav3%2Fbtsj9DKKFbt%2FQrcWEgimR9EzGeDNtQaCF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;442&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MVVM은 Model, View, ViewModel의 약자이다. 각각의 역할에 대해 알아보도록 하자.&lt;/p&gt;
&lt;h3 id=&quot;model&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Model&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로그램에서 사용되는 실제 데이터가 들어있고, api를 요청하거나, 데이터를 업데이트하는 비즈니스 로직이 있다. 즉, 사용할 데이터에 관련된 동작과 데이터를 다룬다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;groovy&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;class BookModel {
    constructor() {
      books = [
        {id: 'RCB-123',name: &quot;React Cook Book&quot;, isFavorite: false},
        {id: 'VCB-123',name: &quot;Vue Cook Book&quot;, isFavorite: false},
        {id: 'ACB-123',name: &quot;Angular Cook Book&quot;, isFavorite: false}
      ];
    }
    
    getBooks() {
        return this.books
    }

    toggleFavorite(bookId) {
      const target = this.books.filter(item =&amp;gt; item.id === bookId)[0];
      target.isFavorite = !target.isFavorite
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;view&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;View&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자에게 보여지는 부분으로 UI와 관련된 것을 다루는 부분이다.&lt;/li&gt;
&lt;li&gt;비즈니스 로직은 없고, 뷰 로직만이 존재한다.&lt;/li&gt;
&lt;li&gt;View는 ViewModel를 지속적으로 관찰한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ViewCotroller을 만들어 View에서 뷰 로직을 분리시키는 방법도 있는데, 더 헷갈릴 수 있으므로 아래 예제에서는 분리시키지 않도록 하겠다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;const BookView = ({bookViewModel}) =&amp;gt; { 
    const [isNeverView, setIsNeverView] = useState(false);

    const handleToggleFavorite = useCallback((bookId) =&amp;gt; {
      bookViewModel.toggleFavorite(bookId)
    }, [viewModel]);

    const handleClickNeverView = useCallback(() =&amp;gt; {
      setIsNeverView(!isNeverView);
    }, [isNeverView]);
    
    return (
      &amp;lt;BookList 
        books={bookViewModel.getBooks()} 
        handleToggleFavorite={handleToggleFavorite}
        handleClickNeverView={handleClickNeverView}
      /&amp;gt;
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;viewmodel&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;ViewModel&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;View와 Model을 연결해주는 역할이다.&lt;/li&gt;
&lt;li&gt;Model의 데이터가 업데이트되거나 View에서 Model에 있는 함수를 호출할 경우 그로인한 모든 변경 사항들이 자동으로 업데이트 되어 View에 적용된다.&lt;/li&gt;
&lt;li&gt;View Model은 View 와 1:N의 형태를 이루고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;class BookViewModel {
    constructor(bookStore) {
        this.store = bookStore
    }

    getBooks() {
        return this.store.getBooks()
    }

    toggleFavorite(bookId) {
       this.store.toggleFavorite(bookId)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;view--viewmodel-viewmodel--model-이어주기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;View &amp;harr;&amp;nbsp;ViewModel, ViewModel &amp;harr;&amp;nbsp;Model 이어주기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ViewModel을 인스턴스화하고 필요한 모든 의존성 주입을 담당한다.&lt;/li&gt;
&lt;li&gt;ViewModel의 인스턴스는 props를 통해 View로 전달된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Provider를 만들어 서로를 이어주는 구성체를 형성할 수 있는데 아래 예제에서는 분리시키지 않도록 하겠다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;import React from 'react';

import BookViewModel from './viewModel/BookViewModel'
import BookModel from './model/BookModel'
import BookView from './view/BookView'

function App() {
  const bookModel = new BookModel()
  const bookViewModel = new BookViewModel(bookModel)

  return (
    &amp;lt;&amp;gt;
      &amp;lt;BookView 
        bookViewModel={bookViewModel}
      /&amp;gt;
    &amp;lt;/&amp;gt;
  )
}

export default App;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;3-recoil을-접목시킨-mvvm패턴&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. Recoil을 접목시킨 MVVM패턴&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;recoil은 state를 구독하는 컴포넌트에 한하여 state가 새로운 값으로 리렌더링이 되는 상태관리 라이브러리이다. recoil에 관해서는 다음에 자세히 설명해보도록 하겠다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Recoil로 MVVM을 사용하는 예제는 많지가 않다. 예제를 따라하면서 ViewModel에서 뷰 로직이 들어가면서 View와 ViewModel의 구분이 확실치 않다는 느낌을 받았다. 아래에서는 Recoil이 적용된 Model과 내가 석연치 않게 생각했던 부분에 대해 얘기해보려 한다.&lt;/p&gt;
&lt;h3 id=&quot;model-1&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Model&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;recoil에서는 데이터의 상태를 atom으로 관리하고, atom의 상태값을 이용해 데이터를 가공한 상태를 selector로 관리한다. CRUD와 데이터 가공에 대한 비즈니스 로직 모두 recoil의 atom, selector를 통해 이루어진다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;export const AllCheckProductItemsSelector = selector({
    key: &quot;ProductList/Item/AllCheck&quot;,
    get: ({ get }) =&amp;gt; {
        const productList = get(ProductListAtom)
        if (productList) return false;
        return get(ProductListAtom).every(item =&amp;gt; item.checked)
    },
    set: ({ set }) =&amp;gt; {
        set(ProductListAtom, prevState =&amp;gt; prevState.map(item =&amp;gt; {
                const currentItem = { ...item };
                currentItem.checked = !currentItem.checked;
                return currentItem;
            })
        )
    }
})

export const CheckProductItemSelector = selector({
    key: &quot;ProductList/Item/Check&quot;,
    get: ({ get }) =&amp;gt; { },
    set: ({ get, set }, product_id) =&amp;gt; {
        set(ProductListAtom, prevState =&amp;gt; {
            return prevState.map(item =&amp;gt; {
                 const currentItem = {...item}
                    if (item.id === product_id) {
                        currentItem.checked = !currentItem.checked
                    }
                return currentItem;
            })
        })
    }
})&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;viewmodel에서-뷰로직이&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;ViewModel에서 뷰로직이??&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MVVM예제에서 ViewModel은 의존성 주입을 통해 View와 Model을 연결해주는 역할을 담당한다. 이 구성체에서는 뷰로직도, 비즈니스 로직도 들어가지 않는다. 하지만 타 블로그에서의 ViewModel예제를 보면 뷰 로직이 들어가 있는 것을 알 수 있다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #212529; text-align: start;&quot;&gt;&lt;code&gt;const [deliveryMethodList] = useRecoilState(DeliveryMethodAtom);
const [selectedMethod] = useRecoilState(SelectedMethodAtom);
const [productList] = useRecoilState(ProductListAtom);
const price = useRecoilValue(PriceStatsSelector);
const [, toggleChecked] = useRecoilState(CheckProductItemSelector);
const [allChecked, allCheckedProductItem] = useRecoilState(
  AllCheckProductItemsSelector
);
const [, increaseAmount] = useRecoilState(
  IncreaseProductItemAmount
);
const [, decreaseAmount] = useRecoilState(
  DecreaseProductItemAmount
);
const [, handleDeleteProductItem] =
  useRecoilState(DeleteProductItem);
const [, changeDeliveryMethod] = useRecoilState(
  ChangeDeliveryMethod
);
const { isDropdown, toggleDropdown } = useDropdown();
const toggleAllChecked = () =&amp;gt; {
  allCheckedProductItem(allChecked);
};
const handlechangeDeliveryMethod = (method_id: number) =&amp;gt; {
  changeDeliveryMethod(method_id);
  toggleDropdown();
};
const handleOrder = () =&amp;gt; {
  console.log(&quot;주문 완료 로직&quot;);
};&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞으로 collusic-new 프로젝트 리팩토링을 과정에서 recoil을 사용한 ViewModel과 View를 만들어보려고 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;blockquote style=&quot;color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조&lt;br /&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://nitro04.blogspot.com/2020/08/eassy-technology-it-web.html&quot;&gt;[ Eassy - Technology, IT, Web ] 비지니스 로직(규칙, 층) 과 프레젠테이션 로직(규칙, 층) 이란 무엇인가?&lt;/a&gt;&lt;br /&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://velog.io/@dlrmsghks7/whatismvvmpattern&quot;&gt;React에서 MVVM 패턴 알아보기&lt;/a&gt;&lt;br /&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://2jinishappy.tistory.com/235?category=948597&quot;&gt;로직을 UI로부터 분리하는 MVVM Architecture Pattern&lt;/a&gt;&lt;br /&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://velog.io/@danbii/MVVM-%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4&quot;&gt;MVVM 디자인패턴&lt;/a&gt;&lt;br /&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://velog.io/@rjsdnql123/TILReact%EC%99%80-MVVM%ED%8C%A8%ED%84%B4&quot;&gt;[TIL]React와 MVVM패턴&lt;/a&gt;&lt;br /&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://velog.io/@towel1017/Recoil%EB%A1%9C-MVVM-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-1-MVVM&quot;&gt;Recoil로 MVVM 사용하기 - (1) MVVM&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Paradigm</category>
      <author>씽코</author>
      <guid isPermaLink="true">https://singcotive.tistory.com/4</guid>
      <comments>https://singcotive.tistory.com/entry/MVVM-%ED%8C%A8%ED%84%B4#entry4comment</comments>
      <pubDate>Fri, 16 Jun 2023 01:26:10 +0900</pubDate>
    </item>
    <item>
      <title>익명함수(Anonymous Function)</title>
      <link>https://singcotive.tistory.com/entry/%EC%9D%B5%EB%AA%85%ED%95%A8%EC%88%98Anonymous-Function</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VaztO/btsj5SbPbz2/k8V6UYDpzRwL6Q5P8aZXl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VaztO/btsj5SbPbz2/k8V6UYDpzRwL6Q5P8aZXl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VaztO/btsj5SbPbz2/k8V6UYDpzRwL6Q5P8aZXl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVaztO%2Fbtsj5SbPbz2%2Fk8V6UYDpzRwL6Q5P8aZXl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4400&quot; height=&quot;2475&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;h3 id=&quot;함수&quot; data-ke-size=&quot;size23&quot;&gt;함수?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수이름을 호출하여 재사용이 가능하다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function sum(a, b) {
	let m = a + b;
	return m;
}
let sumanswer = sum(1, 2);
console.log(sumanswer); // 3&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;익명함수란&quot; data-ke-size=&quot;size23&quot;&gt;익명함수란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 리터럴 방식으로 만들어진 이름없는 함수.&lt;/p&gt;
&lt;blockquote style=&quot;color: #000000;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리터럴?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문자 그대로&lt;/li&gt;
&lt;li&gt;데이터를 만들 때 흔히 사용하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;let a = 10;
let name = 'superman';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;익명함수-구조&quot; data-ke-size=&quot;size23&quot;&gt;익명함수 구조&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변수에 함수의 코드를 저장하는 대신 함수명을 사용하지 않음&lt;/li&gt;
&lt;li&gt;변수 명을 함수 명처럼 사용해서 함수를 호출하거나 변수 값을 이동시키는데 사용할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;let hello = function () {
	document.write(&quot;Hello World&quot;);
};
hello(); // Hello World

let hi = function () {
	let string = &quot;Hello World&quot;;
	return string;
}
console.log(hi()); // Hello World&lt;/code&gt;&lt;/pre&gt;
&lt;script src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7357961761204202&quot;&gt;&lt;/script&gt;
&lt;!-- 부분 하단 광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-7357961761204202&quot; data-ad-slot=&quot;7663758943&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;h3 id=&quot;익명함수의-변수명-변경&quot; data-ke-size=&quot;size23&quot;&gt;익명함수의 변수명 변경&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변수값으로 구성된 함수 코드를 다른 변수명에 변수를 대입하듯이 이동 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;let apple = function () {
	document.write(&quot;This is a banana.&quot;);
};

// apple 변수에 저장된 함수를 banana 변수에 대입
let banana = apple;
banana(); // This is a banana.&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;호이스팅-불가&quot; data-ke-size=&quot;size23&quot;&gt;호이스팅 불가&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// hoisting X
orange();
var orange = function () {
document.write(&quot;This is an orange.&quot;);
};
// error

// hoisting O
banana(); // This is an banana.
function banana() {
document.write(&quot;This is an banana.&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote style=&quot;color: #000000;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;호이스팅?&lt;/b&gt;&lt;br /&gt;함수 안에 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;선언&lt;/b&gt;들을 모두 끌어올려서 해당 함수 유효 범위의 최상단에 선언하는 것&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;var 변수&lt;/b&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;함수선언문&lt;/b&gt;에서만 호이스팅 적용&lt;/li&gt;
&lt;li&gt;호이스팅 방지를 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;let, const&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;선언&lt;/b&gt;은 끌어올려지지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;할당&lt;/b&gt;은 끌어올려지지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;console.log(hello); // undefined
var hello = &quot;Hello&quot;;

console.log(hi); // error
let hi = &quot;Hi&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;script src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7357961761204202&quot;&gt;&lt;/script&gt;
&lt;!-- 부분 하단 광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-7357961761204202&quot; data-ad-slot=&quot;7663758943&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;h3 id=&quot;즉시실행함수&quot; data-ke-size=&quot;size23&quot;&gt;즉시실행함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수에 함수 자체를 대입하는 익명함수의 예제와는 달리 함수가 실행된 결과값을 변수에 대입한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 익명함수
let sum = function(a, b) {
	let m = a + b;
	return m;
};
console.log(sum(1, 2)); // 3

// 즉시실행함수
let sum = (function(a, b) {
	let m = a + b;
	return m;
})(1, 2);
console.log(sum); // 3&lt;/code&gt;&lt;/pre&gt;
&lt;script src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7357961761204202&quot;&gt;&lt;/script&gt;
&lt;!-- 부분 하단 광고 --&gt; &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-client=&quot;ca-pub-7357961761204202&quot; data-ad-slot=&quot;7663758943&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;
&lt;script&gt;
     (adsbygoogle = window.adsbygoogle || []).push({});
&lt;/script&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>Language/JavaScript</category>
      <author>씽코</author>
      <guid isPermaLink="true">https://singcotive.tistory.com/3</guid>
      <comments>https://singcotive.tistory.com/entry/%EC%9D%B5%EB%AA%85%ED%95%A8%EC%88%98Anonymous-Function#entry3comment</comments>
      <pubDate>Fri, 16 Jun 2023 01:24:13 +0900</pubDate>
    </item>
    <item>
      <title>CommonJS와 ES Modules은 왜 함께 할 수 없는가?</title>
      <link>https://singcotive.tistory.com/entry/CommonJS%EC%99%80-ES-Modules%EC%9D%80-%EC%99%9C-%ED%95%A8%EA%BB%98-%ED%95%A0-%EC%88%98-%EC%97%86%EB%8A%94%EA%B0%80</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bixEZO/btskaEiti5u/6JgvY1FEe5kpJguYbPsJjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bixEZO/btskaEiti5u/6JgvY1FEe5kpJguYbPsJjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bixEZO/btskaEiti5u/6JgvY1FEe5kpJguYbPsJjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbixEZO%2FbtskaEiti5u%2F6JgvY1FEe5kpJguYbPsJjk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4400&quot; height=&quot;2475&quot; data-filename=&quot;FE 톺아보기.png&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2475&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonJS에 대하여&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NodeJS에서 기본 모듈 시스템으로 채택하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd; color: #666666;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&amp;nbsp;&lt;span style=&quot;color: #f89009;&quot;&gt;require()&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;와 &lt;span style=&quot;color: #f89009;&quot;&gt;module.exports &lt;/span&gt;를 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;script 파일이 global scope 처럼 사용된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;module.exports&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 여러개의 객체를 내보낼 경우, exports 변수의 fields로 할당한다.&lt;/p&gt;
&lt;pre id=&quot;code_1686844550693&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const canadianToUs = function (canadian) {
  return roundTwoDecimals(canadian * exchangeRate);
};

function usToCanadian(us) {
  return roundTwoDecimals(us / exchangeRate);
}

exports.canadianToUs = canadianToUs; // 내보내기 1
exports.usToCanadian = usToCanadian; // 내보내기 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 한개의 객체를 내보낼 경우, module.exports 변수 자체에 할당한다.&lt;/p&gt;
&lt;pre id=&quot;code_1686844593896&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const obj = {};
obj.canadianToUs = function (canadian) {
  return roundTwoDecimals(canadian * exchangeRate);
};
obj.usToCanadian = function (us) {
  return roundTwoDecimals(us / exchangeRate);
};
module.exports = obj;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ES Modules&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES6부터 들어온 모듈 시스템이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ESM은 &lt;span style=&quot;color: #666666;&quot;&gt;import&lt;/span&gt; 와 &lt;span style=&quot;color: #666666;&quot;&gt;export&lt;/span&gt; 를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;function과 variable을 module scope에 넣고 각 function은 function scope를 가진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;export&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;named export&lt;/h4&gt;
&lt;pre id=&quot;code_1686844927080&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const a = 1
export function fn(){}
export class Class{}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1686844971113&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const a = 1
function fn(){}
class Class{}

export { a, fn, Class }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;default export&lt;/h4&gt;
&lt;pre id=&quot;code_1686844992464&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 변수값은 default로 선언, 내보내기가 동시에 되지 않는다
export default const a = 1 // xx

// fn.js
export default function fn(){}

// Class.js
export default class Class{}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonJS와 ES Modules은 함께할 수 없다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;node 14 에서는 옛날 스타일의 CommonJS와 (이하 CJS) 새로운 스타일의 ESM Scripts (이하 MJS) 두개가 공존하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CJS는 &lt;span style=&quot;color: #666666;&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;import&lt;/span&gt;,&lt;/span&gt; &lt;span style=&quot;color: #f89009;&quot;&gt;export&lt;/span&gt; 를 사용할 수 없고 ESM은 &lt;span style=&quot;color: #f89009;&quot;&gt;require()&lt;/span&gt;, &lt;span style=&quot;color: #f89009;&quot;&gt;module.exports&lt;/span&gt; 를 사용할 수 없다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;참조&lt;br /&gt;&lt;a href=&quot;https://yceffort.kr/2020/08/commonjs-esmodules&quot;&gt;CommonJS와 ES Modules는 왜 함께할 수 없는가?&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Language/JavaScript</category>
      <author>씽코</author>
      <guid isPermaLink="true">https://singcotive.tistory.com/2</guid>
      <comments>https://singcotive.tistory.com/entry/CommonJS%EC%99%80-ES-Modules%EC%9D%80-%EC%99%9C-%ED%95%A8%EA%BB%98-%ED%95%A0-%EC%88%98-%EC%97%86%EB%8A%94%EA%B0%80#entry2comment</comments>
      <pubDate>Fri, 16 Jun 2023 01:09:22 +0900</pubDate>
    </item>
  </channel>
</rss>