<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>mun-jihye.log</title>
        <link>https://velog.io/</link>
        <description>기록을 습관으로</description>
        <lastBuildDate>Sun, 18 Aug 2024 06:47:25 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>mun-jihye.log</title>
            <url>https://velog.velcdn.com/images/mun-jihye/profile/189f1f53-fe7f-4885-9f20-4fa45d295b38/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. mun-jihye.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/mun-jihye" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Node.js]Puppeteer API 사용법]]></title>
            <link>https://velog.io/@mun-jihye/Node.jsPuppeteer-API-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@mun-jihye/Node.jsPuppeteer-API-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Sun, 18 Aug 2024 06:47:25 GMT</pubDate>
            <description><![CDATA[<h2 id="puppeteer란">puppeteer란?</h2>
<p>구글에서 만든 노드 라이브러리로 Headless Chrime 또는 Chrominum을 제어할 수 있다.
백그라운드에서 실행되는 크롬 브라우저 + CDP(Chrome Devtool Protocol)이 베이스로 깔려있어 강력한 UI 테스트 도구로 볼 수 있다. </p>
<h3 id="headless">headless?</h3>
<p>백그라운드에서(CLI) 작동하는 브라우저
때문에 일반 사용자가 브라우저를 사용할 때처럼 화면이 시각적으로 보이지 않는다.
보이는 화면은 없지만,일반적인 브라우저와 같이 웹페이지에 접속하여 HTML, CSS 으로 DOM Tree 와 CSSOM Tree 를 만들고, JS 엔진을 구동한다.</p>
<p>puppeteer에서는 옵션 설정을 통해 headless 모드와 non-headless 모드 둘다 사용할 수 있다.</p>
<pre><code class="language-jsx">const browser = await puppetter.launch({headless: false}) // default = true</code></pre>
<p>유일한 차이점은 만든 화면을 사용자에게 보여주지 않는다는 점이다.
일반 브라우저와 큰 차이가 없기 때문에 보여주는 화면이 없이도, 화면 테스트나 스크린샷을 찍는 것 등 다양한 기능 동작이 가능하며, 사용자가 실제 사용하는 환경과 비슷하게 테스트가 가능하다. </p>
<h3 id="puppetter-기능">puppetter 기능</h3>
<ul>
<li><strong>화면을 스크린샷하거나 pdf를 생성할 수 있다.</strong> (현재 프로젝트 내에서 하고있는 작업)</li>
<li>SPA를 크롤링해서 사전에 콘텐츠를 랜더링할 수 있다.</li>
<li>자동화 된 UI 테스트 실행: Form을 submit, input 입력 등등</li>
<li>최신 버전의 크롬에서 자동화된 테스트 환경을 만들 수 있다.</li>
<li>timeline trace를 통해 퍼포먼스 이슈를 진단할 수 있다.</li>
<li>크롬 확장 프로그램을 테스트할 수 있다.</li>
</ul>
<p><strong>&lt;구조&gt;</strong>
<img src="https://velog.velcdn.com/images/mun-jihye/post/01f1745c-34bf-41de-aaba-83c642d0e990/image.png" alt="">
Puppteer API는 계층적인 구조로 구성되어 있고, 실제 브라우저 구성 요소처럼 되어있다.</p>
<ul>
<li>Puppeteer는 크롬 Devtools protocol을 사용해 브라우저를 컨트롤한다.</li>
<li>Browser 인스턴스는 다수의 browser context를 가질 수 있다.</li>
<li>BrowserContext 인스턴스는 브라우저의 세션을 규정하며 다수의 Page를 가질 수 있다.</li>
<li>Page는 최소 하나의  Frame을 가지고 있어야 한다. (= 메인 Frame)<ul>
<li>추가적으로 iframe이나 frame 태그를 통해 다른 Frame들을 가질 수 있다.</li>
</ul>
</li>
<li>Frame은 최소 하나의 ExecutionContext를 가지고 있어야 한다. 여기서 Frame의 자바스크립트가 실행된다.</li>
<li>Worker는 하나의 ExecutionContext를 가지고 있으며 WebWorkers와 인터렉트 한다. </li>
</ul>
<h2 id="예제">예제</h2>
<pre><code class="language-jsx">(async () =&gt; {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto(&#39;https://example.com&#39;);
  await page.screenshot({path: &#39;example.png&#39;});
  await browser.close();
})();</code></pre>
<p><a href="https://example.com%EB%A1%9C">https://example.com로</a> 이동하여 스크린샷을 example.png로 저장하는 코드
대부분의 메소드들은 Promise를 반환한다.</p>
<p><code>puppeteer.lanch()</code>: browser 생성</p>
<ul>
<li>headless: 기본값은 true, false로 설정하면 브라우저가 실행</li>
<li>defaultViewport: 기본값은 800X600이고 화면이 노출될 사이즈를 지정</li>
<li>devtools: 기본값은 false고, false로 설정하면 브라우저에 Devtools가 열림</li>
</ul>
<p><code>browser.newPage()</code>: 새로운 페이지를 생성, 생성된 페이지로 실제 페이지를 조작 및 제어
<code>page.goto()</code>: 페이지 이동
<code>page.screenshot()</code>: 페이지의 스크린샷을 찍어 특정 path에 저장</p>
<p>이외 브라우저 공식문서 참고하기
<a href="https://pptr.dev/api/puppeteer.browser">https://pptr.dev/api/puppeteer.browser</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Canvas API 개념 및 사용법]]></title>
            <link>https://velog.io/@mun-jihye/Canvas-API-%EA%B0%9C%EB%85%90-%EB%B0%8F-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@mun-jihye/Canvas-API-%EA%B0%9C%EB%85%90-%EB%B0%8F-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Sun, 11 Aug 2024 08:27:00 GMT</pubDate>
            <description><![CDATA[<h1 id="canvas-api-개념">Canvas API 개념</h1>
<p>canvas api를 사용하면 자바스크립트와 html <code>&lt;canvas&gt;</code>를 사용해 그래픽을 그릴 수 있다. 특히 주로 2D 그래픽 요소를 그리기 위해 사용된다. 간단한 도형 뿐 아니라 이미지를 추가하거나 편집하는 용도로도 많이 사용되며, jpg, png, base64 등 다양한 형식으로 출력이 가능하다. 
비슷하게 그래픽을 다루는 api 중에선 3D를 주로 다루는 WebGL이 있다.</p>
<h2 id="webgl-">WebGL ?</h2>
<p>WebGL(Web Graphics Library)은 플러그인을 사용하지 않고 웹 브라우저에서 상호작용 가능한 3D와 2D 그래픽을 표현하기 위한 JavaScript API다. WebGL은 HTML5 canvas 요소에서 사용할 수 있는, OpenGL ES 2.0을 대부분 충족하는 API를 제공한다.</p>
<h2 id="예시">예시</h2>
<p><strong>HTML</strong></p>
<pre><code class="language-jsx">ctx.beginPath();
ctx.arc(x좌표, y좌표, 반지름, 0(시작 각도), 2 * Math.PI(종료 각도));
ctx.fill();</code></pre>
<p>이제 실제로는 어떤 일이 일어날까?</p>
<ol>
<li>beginPath 경로(path)를 생성. (버퍼 생성)</li>
<li>arc 원 혹은 호를 그리기 위해 반지름의 크기만큼 버퍼 영역을 확보. 이 때, Canvas API는 해당 영역에 fill , stroke 중 어떤 것이 호출될지 모르기 때문에 버퍼 영역을 일단 충분히 확보한다.</li>
<li>fill 이 호출되면 해당 버퍼 영역에 drawArray 혹은 drawElements를 호출해서 색을 칠한다.</li>
</ol>
<p>두 번째 원을 그릴 때? 역시 마찬가지로 위와 같은 작업을 동일하게 반복한다.
천 번 만 번 반복을 하더라도 위와 같은 작업은 동일하게 필요한다.</p>
<p><strong>WebGL</strong></p>
<pre><code class="language-jsx">const m4 = twgl.m4;
const gl = document.querySelector(&quot;canvas&quot;).getContext(&quot;webgl&quot;);
const vs = ` attribute vec4 position; uniform mat4 u_matrix; void main() { gl_Position = u_matrix * position; } `;
const fs = ` precision mediump float; uniform vec4 u_color; void main() { gl_FragColor = u_color; } `;
const program = twgl.createProgram(gl, [vs, fs]);
const positionLoc = gl.getAttribLocation(program, &quot;position&quot;);
const colorLoc = gl.getUniformLocation(program, &quot;u_color&quot;);
const matrixLoc = gl.getUniformLocation(program, &quot;u_matrix&quot;);
const positions = [];
const radius = 50;
const numEdgePoints = 64;
for (let i = 0; i &lt; numEdgePoints; ++i) {
  const angle0 = (i) * Math.PI * 2 / numEdgePoints;
  const angle1 = (i + 1) * Math.PI * 2 / numEdgePoints;
  positions.push(0, 0, Math.cos(angle0) * radius, Math.sin(angle0) * radius, Math.cos(angle1) * radius, Math.sin(angle1) * radius);
}
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
gl.useProgram(program);
const projection = m4.ortho(0, gl.canvas.width, 0, gl.canvas.height, -1, 1);

function drawCircle(x, y, color) {
  const mat = m4.translate(projection, [x, y, 0]);
  gl.uniform4fv(colorLoc, color);
  gl.uniformMatrix4fv(matrixLoc, false, mat);
  gl.drawArrays(gl.TRIANGLES, 0, numEdgePoints * 3);
}

drawCircle(50, 75, [1, 0, 0, 1]);
drawCircle(150, 75, [0, 1, 0, 1]);
drawCircle(250, 75, [0, 0, 1, 1]);</code></pre>
<p>WebGL에서는 일종의 인스턴스화를 통해 이미 원을 그려놓거나, 원을 그리는데 필요한 반복 작업들(원의 크기를 계산하고 그 안을 채우고 버퍼 영역을 확보하는 등의 일)을 캐싱할 수 있다.</p>
<h3 id="그럼-canvas-api-에서는-왜-캐싱을-안-할까">그럼 Canvas API 에서는 왜 캐싱을 안 할까?</h3>
<p>그 이유는 Canvas API에서 도형을 모양을 정하고 윤곽을 잡는데 사용되는 호출 함수와, 실제로 그 내부를 채우는 함수가 다르기 때문.</p>
<p>만약 arc 를 호출해서 호를 그렸다고 해도, 그 다음에 사용자가 다음 포인트로 이동을 할 지 moveTo , 다른 포인트로 이동하면서 해당 면적을 채울지 lineTo , 그 안을 채울지 fill, 외곽선을 그릴지 stroke 알 수 없기 때문이다.</p>
<p><strong>뭐가 더 좋은 것일까?</strong>
요점은 WebGL이 Canvas API가 스킵할 수 없는 일부 단계를 스킵하거나, 재사용이 가능하게끔 더 낮은 레벨에서 제어할 수 있다는 점이다.</p>
<p>그러나 위에서 본 바와 같이, Canvas API는 원을 그리는 데 3줄이 필요한 반면 WebGL은 원을 그리는데 60줄의 코드가 필요하다.</p>
<p>따라서 WebGL / Canvas API는 편의성과 성능의 tradeoff가 있고, 상황에 맞게 사용을 하면 된다고 보면 된다.</p>
<h1 id="canvas-api-사용하기">Canvas API 사용하기</h1>
<h2 id="1-도화지-만들기">1. 도화지 만들기</h2>
<p>Canvas API는 Canvas Element가 있어야 하기 때문에, Canvas Element를 하나 넣어준 html 파일을 만들어준다.</p>
<pre><code class="language-jsx">&lt;html&gt;
&lt;head&gt;
&lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&gt;
&lt;title&gt;GAME&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;canvas&gt;&lt;/canvas&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>💡 높이와 너비를 설정하지 않은 canvas의 크기는 기본적으로 w:300px h:150px </p>
<h2 id="2-컨텍스트-가져오기">2. 컨텍스트 가져오기</h2>
<p>캔버스 위에 무엇인가 그리기 위해서는 다음과 같이 드로잉 컨텍스트를 가져와야 한다.
<a href="https://developer.mozilla.org/ko/docs/Web/API/HTMLCanvasElement/getContext">HTMLCanvasElement.getContext() - Web API | MDN</a></p>
<pre><code class="language-jsx">const contextType = &quot;2d&quot; 
// &quot;2d&quot; | &quot;webgl&quot; | &quot;webgl2&quot; | &quot;bitmaprenderer&quot;

const canvas = document.querySelector(&#39;canvas&#39;)
const ctx = canvas.getContext(contextType);</code></pre>
<p>드로잉 컨텍스트 중, 우리는 2차원 렌더링 컨텍스트를 나타내는 <code>CanvasRenderingContext2D</code> 객체를 사용한다.</p>
<h2 id="3-그리기">3. 그리기</h2>
<p>드로잉 컨텍스트의 다양한 메소드를 통해 캔버스에 도형을 그릴 수 있다. </p>
<p><strong>자주 사용되는 메소드</strong></p>
<ul>
<li><code>beginPath</code> 새 경로를 생성<ul>
<li>경로 : 점들의 집합이며 선의 한 부분으로 연결되어 여러가지 도형, 곡선을 이루고 두께와 색상을 나타내는 역할을 한다.</li>
<li>새 령로 생성이기 때문에(이전 경로와의 연결을 끊음), 최초에는 호출하지 않아도 된다.최초의 경로 하나는 이미 존재한다.</li>
</ul>
</li>
<li><code>closePath</code> 경로 닫기. 마지막 경로에 있는 점과 경로의 시작점을 연결한다.</li>
<li><code>stroke</code> 경로의 윤곽선에 선을 그린다.</li>
<li><code>fill</code> 경로의 내부를 채운다</li>
<li><code>moveTo</code> 아무것도 그리지 않고 펜(현재 위치)의 좌표를 옮긴다.</li>
<li><code>lineTo</code> 현재 위치에서 특정 위치까지 직선을 그린다.</li>
<li><code>arc</code> 호/원을 그린다.</li>
<li><code>rect</code> 직사각형을 그린다. </li>
</ul>
<p>** 원과 정사각형 그리기 **</p>
<pre><code class="language-jsx">ctx.rect(5, 5, 40, 40) // x, y, w, h
ctx.arc(100, 15, 20, 0, 2*Math.PI) // x, y, r, 시작 각, 끝 각
ctx.stroke();</code></pre>
<p><img src="https://velog.velcdn.com/images/mun-jihye/post/3a80e385-8a2a-4f44-9143-399cce683745/image.png" alt="">
** 📝 흐린 문제 발생**
흐린 문제는 레티나 디스플레이와 같은 고해상도 디스플레이에서의 추가 픽셀이 필요해서 나타나는 현상으로, window 객체에 있는 <code>devicePixelRatio</code>를 통해 교정할 수 있다. 
<img src="https://velog.velcdn.com/images/mun-jihye/post/a4690e49-d990-4a1e-af87-7db0d42f5cf5/image.png" alt="">
<img src="https://velog.velcdn.com/images/mun-jihye/post/acfa91d2-97fa-4798-93e0-e3688a41de80/image.png" alt=""></p>
<pre><code class="language-jsx">const scale = window.devicePixelRatio;
ctx.scale(scale, scale);
canvas.width = 300 * scale;
canvas.height = 150 * scale;
canvas.style.width = 300 + &quot;px&quot;;
canvas.style.height = 150 + &quot;px&quot;;</code></pre>
<p>*<em>📝 선이 연결된 문제 *</em></p>
<p>해당 코드가 그림을 그리는 과정이라고 생각하면 붓을 떼지 않고 원을 그리러 간 셈이기 때문에 발생한 문제다. <code>moveTo</code>를 통해 붓의 위치를 원으로 옮기자</p>
<pre><code class="language-jsx">ctx.rect(5, 5, 40, 40);
ctx.moveTo(100,25); // 붓의 위치를 옮긴다.
ctx.arc(100, 25, 20, 0, 2*Math.PI);</code></pre>
<p><img src="https://velog.velcdn.com/images/mun-jihye/post/00808fda-f982-49df-a3e5-17a06bb3ac33/image.png" alt="">
++ 여기서 반지름은 x좌표에 원의 반지름을 더해 제거할 수 있지만</p>
<pre><code class="language-jsx">ctx.rect(5, 5, 40, 40);
ctx.stroke();

ctx.beginPath();
ctx.arc(100, 25, 20, 0, 2*Math.PI);
ctx.stroke();</code></pre>
<ul>
<li>정사각형을 정의하고 외곽선을 그린다.</li>
<li>새 경로를 생성하고 새 경로 위에 원을 정의하고 외곽선을 그린다.
<img src="https://velog.velcdn.com/images/mun-jihye/post/832f4592-82fb-430c-991d-9da278d7a715/image.png" alt=""></li>
</ul>
<p>*<em>✨<code>beginPath</code>를 사용해서 새 경롤르 만드는 방법은 각 경로의 스타일을 경리시켜 관리할 때 유용하게 사용된다. *</em></p>
<p>ref) <a href="https://nookpi.tistory.com/133">https://nookpi.tistory.com/133</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[더 가치있는 공통 컴포넌트 만들기]]></title>
            <link>https://velog.io/@mun-jihye/%EB%8D%94-%EA%B0%80%EC%B9%98%EC%9E%88%EB%8A%94-%EA%B3%B5%ED%86%B5-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@mun-jihye/%EB%8D%94-%EA%B0%80%EC%B9%98%EC%9E%88%EB%8A%94-%EA%B3%B5%ED%86%B5-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 18 Jul 2024 01:38:21 GMT</pubDate>
            <description><![CDATA[<p>코드잇 부트캠프를 수료한 사람들끼리 아티클 공유 스터디를 시작했다.
항상 기술 블로그를 읽으면 까먹는 이슈가 발생해서 그 기록을 블로그 글로 남겨 보려고 한다.</p>
<p>프로젝트를 진행하면서 항상 고민이었던 공통 컴포넌트에 대한 아티클을 소개하려고 한다.
글의 분량이 너무 많아 어느 정도만 요약을 해보겠다.
📝원문: <a href="https://fe-developers.kakaoent.com/2024/240116-common-component/">https://fe-developers.kakaoent.com/2024/240116-common-component/</a></p>
<hr>
<h2 id="내가-생각하는-공통-컴포넌트">내가 생각하는 공통 컴포넌트</h2>
<p>공통 컴포넌트는 단지 재사용과 협업을 위한 컴포넌트 정도로 생각했다. 하지만 프로젝트를 하다보면 재사용하기 위해 공통 컴포넌트를 만들었는데도 아주 작은 차이 때문에 그대로 사용하기 어려운 경우가 비일비재하다. 대표적으로 모달의 경우, 버튼의 개수와 스타일에 따라 공통 컴포넌트를 만들었지만, prop을 사용하다 보니 이 컴포넌트가 과연 공통 컴포넌트인지 의문이 들었고 어느 정도의 공통성과 자유도를 가져야 공통 컴포넌트라고 정의할 수 있을 지에 대해 항상 고민했다.  이 블로그는 공통 컴포넌트를 좀 더 쓸모있고 가치 있게 만드는데 필요한 고민들에 집중하므로 내 고민들에 어느정도 답을 줄 수 있을 것 같다.</p>
<h2 id="1-확장-규칙-설계">1. 확장 규칙 설계</h2>
<p>무분별한 prop의 확장은 공통 컴포넌트의 가치를 떨어뜨리므로 프로젝트마다 적절한 공통 컴포넌트 확장 규칙 컨벤션 설정이 필요하다.</p>
<h3 id="1-1-명확한-컴포넌트-역할">1-1 명확한 컴포넌트 역할</h3>
<blockquote>
<p>*<em>공통 컴포넌트 !== 만능 컴포넌트 *</em>
개발하고자 하는 공통 컴포넌트 역할의 경계를 명확히 하자 </p>
</blockquote>
<pre><code class="language-jsx">export default function Button() {
  return &lt;button className=&quot;default-button-class&quot; /&gt;;
}</code></pre>
<p>우리는 공통 컴포넌트를 사용함으로써 개발 시간을 줄이고 유지보수를 편하게 해야한다.
공통 컴포넌트가 많은 역할을 수행하도록 변경할수록 사용하는 쪽에서는 prop을 더 구체적으로 명시해야 하고 유지보수에 많은 시간 소모하게 되는데 이는 본래의 목적을 퇴색하게 된다. </p>
<p>예를 들어 프로젝트 내에서 버튼 컴포넌트를 <em>반복적으로 사용되는 버튼의 기본 형태</em> 라는 책임을 부여했다면 다른 형태의 버튼 디자인을 고려하지말자.</p>
<p>이렇듯 하나의 컴포넌트가 하나의 기능만 수행하도록 설계한 방법론이 <a href="https://fe-developers.kakaoent.com/2023/230330-frontend-solid/">SOLID-Single Responsibility Principle</a>(단일 책임의 원칙)이다.</p>
<h3 id="1-2-인터페이스-정책">1-2. 인터페이스 정책</h3>
<p>인터페이스를 활용해 공통 컴포넌트의 확장성을 확보할 수 있다.</p>
<pre><code class="language-jsx">import cn from &#39;classnames&#39;;
import { ButtonHTMLAttributes } from &#39;react&#39;;

interface Props extends ButtonHTMLAttributes&lt;HTMLButtonElement&gt; {
  variant?: &#39;primary&#39; | &#39;secondary&#39; | &#39;none&#39;;
}

export default function MelonButton({ className, variant, ...rest }: Props) {
  return (
    &lt;button
      className={cn(
        &#39;melon-button-class&#39;,
        {
          &#39;primary-class&#39;: variant === &#39;primary&#39;,
          &#39;secondary-class&#39;: variant === &#39;secondary&#39;,
        },
        className,
      )}
      {...rest}
    /&gt;
  );
}</code></pre>
<p>다양한 상황에서 변수를 prop으로 넘기는 것은 한계가 있기 때문에 인터페이스의 상속과 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters">rest parameters</a>를 사용해 이를 해결할 수 있다.
리액트 타입스크립트의 경우 모든 DOMElement의 attribute를 prop으로 가지고 있는 인터페이스를 제공하기 때문에 이를 활용하여 확장성 있는 설계를 구성할 수 있다. </p>
<blockquote>
<p>이때 주의할 점은 자신이 정한 <strong><em>명확한 컴포넌트 역할</em></strong> 을 해치지 않는 방향에서 prop에 rest parameters를 추가해야한다. </p>
</blockquote>
<h2 id="2-네이티브-요소의--활용">2. 네이티브 요소의  활용</h2>
<p>공통 컴포넌트들의 대부분은 이미 존재하는 네이티브 요소들을 사용할 경우 훨씬 효율적이고 높은 완성도를 가지게 된다.</p>
<h3 id="2-1-네이티브-숨기기">2-1. 네이티브 숨기기</h3>
<pre><code class="language-jsx">import { useState } from &#39;react&#39;;

export default function Checkbox() {
  const [checked, setChecked] = useState(false);

  return (
    &lt;div
      style={{
        width: &#39;fit-content&#39;,
        position: &#39;relative&#39;,
      }}
    &gt;
      {checked ? &lt;CheckedIcon /&gt; : &lt;UncheckedIcon /&gt;}
      &lt;input
        type=&quot;checkbox&quot;
        style={{
          position: &#39;absolute&#39;,
          left: 0,
          top: 0,
          width: &#39;100%&#39;,
          height: &#39;100%&#39;,
          opacity: 0,
        }}
        checked={checked}
        onChange={ev =&gt; setChecked(ev.target.checked)}
      /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>위 코드는 사용자가 체크박스 아이콘을 클릭하면 사실 아이콘이 아닌 네이티브 체크박스를 클릭하도록 구현된 예제다. 이는 ui를 보고 액션을 발생시키는 사람이나 단지 dom 구조를 보고 액션을 발생시키는 브라우저 입장에서 같은 결과값이 발생하도록 유도하게 된다. 또한 input요소가 제공하는 다양한 기능을 활용할 수 있다.</p>
<h3 id="2-2-controlled-vs-uncontrolled">2-2 controlled vs uncontrolled</h3>
<p>폼 요소를 활용해 개발할 때는 제어 방식과 비제어 방식 중 어떤 방향으로 개발할 지 먼저 결정하는 것이 좋다</p>
<blockquote>
<p>폼 요소가 아닌 다른 컴포넌트에서 지속해서 값을 바라보고 리렌더링이 필요하다면 제어 방식이 적합</p>
</blockquote>
<p>👍 제어 방식의 폼 요소는 값을 상태 값으로 관리하기 때문에 상태 값을 추적하여 값이 변경되었는지 확인할 수 있고 이는 상태 값을 기준으로 렌더링 되는 리액트 흐름에 적합한 방식이다.</p>
<p>👎 값이 변경될 때마다 해당 값을 사용하고자 하는 부모 컴포넌트까지 상태를 끌어올려야 하며 해당 부모 요소 하위 모든 요소를 리 렌더링하게 되는 사이드이펙트를 가져온다. 또한 CheckboxGroup과 같은 상위 공통 컴포넌트를 만든다고 하면 불필요한 props drilling이 발생한다.</p>
<blockquote>
<p>상황에 따라 비제어 방식을 활용한다면 과도한 리렌더링을 막을 수 있고, 공통 컴포넌트를 사용한 개발의 DX를 향상시킬 수 있다.</p>
</blockquote>
<p>👎 리액트에서는 사용자의 인터랙션에 따른 이벤트 시스탬이 자체적으로 구축되어있으므로 DOM을 직접 접근해 값을 세팅하는 경우 의도한 액션이 발생하지 않아 제약이 생길 수 있다.</p>
<p>필자의 경우엔 <strong>내부적으로 controlled로 돌아가되 공통 컴포넌트를 가지고 개발하는 사용자 입장에서는 마치 uncontrolled인 것처럼</strong> 사용할 수 있도록 인터페이스를 구성하는 방식을 선호한다.</p>
<pre><code class="language-jsx">// mixed checkbox
import { ChangeEvent, useState } from &#39;react&#39;;

interface Props {
  checked?: boolean;
  onChange?: (checked: boolean) =&gt; void;
}

export default function Checkbox({
  checked: controlledChecked,
  onChange,
}: Props) {
  const isControlled = controlledChecked !== undefined;
  const [checked, setChecked] = useState(false);

  const handleChange = (ev: ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    const checked = ev.target.checked;

    if (!isControlled) {
      setChecked(checked);
    }

    onChange?.(checked);
  };

  return (
    &lt;input
      type=&quot;checkbox&quot;
      onChange={handleChange}
      checked={isControlled ? controlledChecked : checked}
    /&gt;
  );
}
</code></pre>
<ul>
<li>isControlled 변수는 컴포넌트가 제어된 상태인지 여부 (controlledChecked가 undefined가 아닌 경우 컴포넌트는 제어된 상태)</li>
<li>핸들러 함수 내에서 체크박스가 비제어 상태일 경우 내부 상태를 업데이트한다. </li>
<li>checked 속성은 제어된 상태라면 controlledChecked 값을 사용하고, 비제어 상태라면 내부 상태 checked 값을 사용</li>
</ul>
<h4 id="사용-예시">사용 예시</h4>
<ol>
<li>제어된 컴포넌트로 사용<pre><code class="language-jsx">&lt;Checkbox checked={true} onChange={(checked) =&gt; console.log(checked)} /&gt;</code></pre>
</li>
</ol>
<ul>
<li>외부에서 checked 속성을 전달하여 컴포넌트를 제어.</li>
<li>상태가 변경될 때 onChange 콜백 함수가 호출.</li>
</ul>
<ol start="2">
<li>비제어 컴포넌트로 사용<pre><code class="language-jsx">&lt;Checkbox onChange={(checked) =&gt; console.log(checked)} /&gt;</code></pre>
</li>
</ol>
<ul>
<li>checked 속성을 전달하지 않아 비제어 상태로 동작.</li>
<li>내부적으로 상태를 관리하며, 상태가 변경될 때 onChange 콜백 함수가 호출.</li>
</ul>
<h2 id="3-웹-접근성">3. 웹 접근성</h2>
<p>웹 접근성을 훌륭하게 설계해 두었다면 UX의 향상 및 테스트 코드를 구성할 때 강점을 가져갈 수 있고, SEO 적인 측면에서도 도움을 받을 수 있다.</p>
<h3 id="3-1-semantic-tag">3-1. Semantic Tag</h3>
<h3 id="3-2-aria-field">3-2. ARIA Field</h3>
<p>ARIA Field는 웹 콘텐츠를 좀 더 쉽게 접근하기 위해 DOMElement에 부여하는 attribute.</p>
<h4 id="주요-aria-속성">주요 ARIA 속성</h4>
<ol>
<li>aria-label: 요소의 레이블을 설정.</li>
<li>aria-labelledby: 다른 요소의 ID를 참조하여 해당 요소의 텍스트를 레이블로 사용.</li>
<li>aria-describedby: 요소의 설명을 제공하는 다른 요소의 ID를 참조.</li>
<li>role: 요소의 역할을 정의.</li>
</ol>
<h4 id="예시">예시</h4>
<h5 id="1-aria-label">1. aria-label</h5>
<pre><code class="language-jsx">&lt;button aria-label=&quot;Close&quot; onclick=&quot;closeWindow()&quot;&gt;X&lt;/button&gt;</code></pre>
<p>버튼은 화면 판독기에서 &quot;X&quot; 대신 &quot;Close&quot;로 읽힌다.</p>
<h5 id="2-aria-labelledby">2. aria-labelledby</h5>
<pre><code class="language-jsx">&lt;label id=&quot;usernameLabel&quot;&gt;Username:&lt;/label&gt;
&lt;input type=&quot;text&quot; id=&quot;username&quot; aria-labelledby=&quot;usernameLabel&quot;&gt;</code></pre>
<p>input 필드가 usernameLabel 요소에 의해 레이블이 지정</p>
<h5 id="3-aria-describedby">3. aria-describedby</h5>
<pre><code class="language-jsx">&lt;label for=&quot;email&quot;&gt;Email:&lt;/label&gt;
&lt;input type=&quot;text&quot; id=&quot;email&quot; aria-describedby=&quot;emailHelp&quot;&gt;
&lt;span id=&quot;emailHelp&quot;&gt;We&#39;ll never share your email with anyone else.&lt;/span&gt;</code></pre>
<p>input 필드가 emailHelp 요소로 설명</p>
<h5 id="4-role">4. role</h5>
<pre><code class="language-jsx">&lt;nav role=&quot;navigation&quot;&gt;
  &lt;ul&gt;
    &lt;li&gt;&lt;a href=&quot;#home&quot;&gt;Home&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#about&quot;&gt;About&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#contact&quot;&gt;Contact&lt;/a&gt;&lt;/li&gt;
  &lt;/ul&gt;
&lt;/nav&gt;</code></pre>
<p>nav 요소는 role=navigation을 통해 역할을 명시적으로 부여받음</p>
<h2 id="마무리">마무리</h2>
<p>공통 컴포넌트를 좀 더 가치있게 구현하는 방법에 대한 포스트다.
개발자는 확장 규칙을 설계해 재사용될 범위를 명확히 해야하고, 네이티브 요소를 적극적으로 사용해 완성도 있는 컴포넌트를 만들어야 한다. 또한 웹 접근성을 고려해 다양한 사용자와 엔진에 대응해야 한다.
💪 공통 컴포넌트를 가치 있게 만들기 위한 이런 고민과 노력은 웹의 완성도뿐만 아니라 개발자 개인의 개발 역량 향상에도 도움이 된다고 필자는 말한다.
따라서 <a href="https://github.com/mui/material-ui">mui</a>, <a href="https://github.com/react-bootstrap/react-bootstrap">react-bootstrap</a> 같이 유명 라이브러리의 소스 코드를 열어보고 분석해 보는 습관을 기르면, 개발자가 어떤 의도로 코드를 구성했는지 파악하는 과정에서 웹과 사용자를 더 잘 이해하는 개발자로 성장하게 될 것이다.</p>
]]></description>
        </item>
    </channel>
</rss>