1206 - 1212
1206
리액트 수업 5일 차
Web App 개발에 적합하지 않은 웹 환경
- 클라이언트 요청에 의해 서버로부터 응답 받은 HTML 파일을 해석하는 과정에서 React 앱을 구성하는 수많은 모듈 파일들을 다시 서버에 호출, 응답, 런타임 중 해석하는 프로세스는 효율적이지 않다.
적합하지 않은 이유
- 브라우저 호환성 문제. 브라우저 환경에서 정식으로 모듈을 사용할 수 있게 되었지만, 모든 브라우저에서 사용할 수 있는 것은 아니다.
- 앱을 빌드하는 환경이 기본적으로 제공되지 않는다.
모듈 번들링
- 앱에 많은 기능이 필요해질수록 모듈 종속성의 올바른 순서를 추적하고 로드하는 번거로움이 더 커지게 된다.
- 복잡성이 증가함에 따라 코드베이스의 기능이 혼란스럽게 뒤섞이고 흩어지는 결과가 발생해 개발이 어려워지는 문제가 발생한다.
- 문제 해결을 위해 코드베이스를 모듈로 분할 관리하고, 묶어줘야 한다.
트리 쉐이킹
- 사용되지 않은 코드 조각은 번들 크기를 불필요하게 키우므로 번들 과정에서 제거해야 한다.
- 트리 쉐이킹은 최종 번들에서 사용하지 않을 코드를 제거하여 JS 번들 크기를 줄이고 앱의 실행 속도를 향상시킨다.
코드 스플리팅
- 앱이 커지면 덩달아 번들 또한 커진다. 특히 크기가 큰 3rd-party 라이브러리를 추가할 때 실수로 앱이 커져서 로드 시간이 길어질 수 있다. 번들이 거대해지는 것을 방지하기 위한 좋은 해결 방법 중 하나는 번들을 나누는 것이다.
- 코드 분할은 런타임 중 나뉘어진 번들을 동적으로 불러오는 것을 말한다.
코드 최적화, 소스맵
- 수 많은 모듈을 번들하는 동안 수행해야 하는 코드 최적화도 필요하다. 코드 최적화를 위해 주석, 공백 또는 긴 변수, 함수 일므 등을 모두 축소 또는 제거하여 파일의 크기를 크게 줄일 수 있다.
- 이 코드는 압축되었기에 사람이 읽기 매우 어렵고, 디버깅도 쉽지 않아 코드를 추적 가능한 소스맵이 필요하다.
- 소스맵은 디코더처럼 작동하며 축소된 코드를 구문 분석한다. 소스맵에는 원본 파일의 행에 대한 참조도 포함되어 있어 버그를 추적하기 용이하게 한다.
React 프로젝트 구성 - 자동편
npx create-react-app 프로젝트이름 --template 템플릿이름
- create-react-app을 설치하지 않고 패키지를 실행해서 프로젝트를 생성하고 (소문자로 -이나 _로 공백없이 프로젝트명 작성) 템플릿은 일종의 프리셋이라고 보면 된다.
- 템플릿을 기반으로 프로젝트를 구성한다. 예를 들어 typescript를 사용한다면 cra-template-typescript를 사용할 수 있다.
- 기본으로 진행한다면 템플릿 없이 진행해도 된다. cra-template이 기본 템플릿
가장 큰 번들 파일을 메인 번들, 작은 조각의 덩어리들을 청크라고 한다.
React 프로젝트 구성 - 개발 환경 메뉴얼 구성
Initialization Git & NPM
{
"private": true,
"name": "config-manual-react-dev-env",
"version": "1.0.0",
"description": "React 앱 개발 환경 메뉴얼 구성",
"main": "index.js",
"scripts": {
"start": "",
"serve": "",
"bundle": "",
"watch": "",
"build": "",
"clear": ""
},
"keywords": ["react", "webpack", "babel", "sass", "postcss"],
"author": "",
"license": "ISC"
}
Basic Setup
npm i -D webpack webpack-cli webpack-dev-server
- 웹팩 설정에 기본적으로 필요한 두 패키지와 dev-server를 설치한다.
- 웹팩 하는 일 자체는 번들밖에 못하기에, 필요한 것들을 더 구성해줘야 한다.
.gitignore 생성
node_modules
coverage
build
dist
.env
.vscode
# Mac
.DS_Store
src는 엔트리 파일, dist는 출력 파일로 생성. dist에 index.html 파일 생성.
webpack 번들 설정
"scripts": {
"start": "",
"serve": "",
"bundle": "webpack --target web --mode development",
"watch": "npm run bundle -- --watch",
"build": "webpack build --target web --mode production",
"clear": ""
},
- webpack 설정을 따로 하지 않으면 기본 값이 src 폴더를 엔드리로 해서 dist를 아웃풋으로 내보낸다.
- mode를 development로 하면 개발용, production으로 하면 배포용. 자동으로 압축된다. 기본값을 production
- target 옵션을 설정해 줄 수 있다. 기본값은 web인데, 오류가 날 수도 있으니 알아두자.
dev server 설정
"scripts": {
"serve": "webpack serve --port 3000 --static dist --entry src/index.js --target web --mode development",
}
- –entry src/index.js는 기본 값이라 작성하지 않아도 된다.
- port번호를 3000으로 하고 dist 폴더를 정적파일로 실행하겠다는 의미이다.
webpack configuration
-
웹팩은 노드 환경에서 실행되므로 common.js 방식으로 모듈을 불러오고 내보내야 한다.
-
기존의 webpack.config.js 가 아닌 webpack 폴더에 config.*.js 방식으로 생성해봤다.
"scripts": {
"serve": "webpack serve --config webpack/config.server.js",
"bundle": "webpack --config webpack/config.dev.js",
"build": "webpack build --config webpack/config.build.js",
}
- 기존의 옵션들을 config 파일로 빼서 구성하였다. 이제 명령어를 실행하면 해당 경로를 찾아서 옵션을 확인하고 webpack 번들링을 실행한다.
// webpack/config.server.js
const serverConfig = {
target: "web",
mode: "development",
devtool: "source-map",
devServer: {
port: 3000,
static: ["dist"],
},
};
module.exports = serverConfig;
- 예를들어 config.server.js의 경우에는 위와 같이 구성한다.
- devtool은 디버깅 과정 향상을 위해 source mapping 스타일을 선택한다. 이 값은 빌드 및 리빌드 속도에 큰 영향을 미칠 수 있다.
webpack-merge
- 중복되는 설정은 합치고, 그 외의 설정만 추가로 정의하여 함께 재사용할 수 있도록 도와주는 패키지
const { merge } = require("webpack-merge");
const devConfig = require("./config.dev");
const serverConfig = merge(devConfig, {
devServer: {
port: 3000,
static: ["dist"],
},
});
module.exports = serverConfig;
- 중복되는 설정은 config.dev에서 가져와서 사용하고, serverConfig에서 추가적으로 필요한 옵션만 따로 빼와서 사용한다.
React 설치
npm i react react-dom
- react 웹 앱을 개발하기 위해 기본 패키지인 react와 react-dom을 설치한다.
- 이후 정상적으로 컴포넌트 생성 및 react 메서드 사용 가능
prettier 설정
npm i -D prettier
- prettier 생성 후 .prettierrc.js 파일 생성 및 설정
- .prettierignore 파일 생성해서 dist, build, coverage 삽입
- 번거로운 설정이 귀찮을 경우 맥에서는 홈 디렉토리에 미리 설정파일을 보이지 않게 집어넣어 두고, cp 명령어로 복사해서 사용 가능하다.
eslint 설정
npx eslint --init
npm i -D eslint-plugin-prettier eslint-plugin-jsx-a11y eslint-config-prettier
- eslint-plugin-prettier는 prettier를 eslint 규칙으로 실행하고 차이점을 개별 eslint 문제로 보고한다.
- eslint-config-prettier는 필요하지 않거나 prettier와 충돌할 수 있는 모든 규칙을 끈다.
- 마찬가지로 .eslintignore 파일 생성해서 dist, build, coverage 삽입
- .eslintrc 파일에 플러그인이나 extends를 설정해 줌
// .eslintrc.js
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:jsx-a11y/recommended",
"prettier",
],
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["react", "jsx-a11y", "prettier"],
rules: {
"react/prop-types": "warn",
"prettier/prettier": "error",
"arrow-body-style": "off",
"prefer-arrow-callback": "off",
},
};
Webpack babel 설정
npm i -D babel-loader @babel/{core,preset-env,preset-react}
- babel-loader는 웹팩에 바벨을 붙이기 위해 필요
- @babel/core는 필수, @babel/preset-env는 ES6를 컴파일, @babel/preset-react는 리액트 개발에 필요
- 여기서 JSX 문법으로 컴포넌트를 수정하면 에러가 발생한다. loader 설정을 안했기 때문!
// .babelrc.js
module.exports = {
presets: ["@babel/preset-env", "@babel/preset-react"],
};
// config.dev.js
const devConfig = {
target: ["web"],
mode: "development",
devtool: "source-map",
// 모듈 > 규칙(들) > (개별) 로더 구성 (어떤 파일? 제외 항목? 어떤 로더 사용? 옵션은?)
module: {
rules: [
// babel-loader 구성
{
test: /\.jsx?$/i,
exclude: /(node_modules|dist)/,
use: "babel-loader",
},
],
},
};
module.exports = devConfig;
- babel-loader를 사용해서 확장자명이 .js나 .jsx인 파일들을 모두 컴파일하고 번들링하겠다는 뜻. 단 node_modules나 dist파일은 제외하고
css 팁
button svg {
pointer-events: none;
}
- 버튼 안에 svg가 있는 경우 이벤트 전파가 안되게 막아준다.
window.requestAnimationFrame()
- 브라우저에게 수행하기를 원하는 애니메이션을 알리고 다음 리페인트가 진행되기 전에 해당 애니메이션을 업데이트하는 함수를 호출하게 한다.
- 특이한 비동기 함수인데, 모니터의 주사율에 맞게 함수를 실행하게 되어있다.
- 스스로를 반복 호출하지 않기 때문에, 반복하려면 재귀적으로 다시 호출해주어야 한다.
JSX inline style
export const RandomCountUpApp = ({ count, isComplete }) => {
const completeStyle = !isComplete ? null : { animationName: "none" };
return (
<div className="randomCountUp">
<output style={completeStyle}>{count}</output>
</div>
);
};
- 보간법을 사용해 객체로 inline style을 삽입하였다.
1207
최근에 웹 컴포넌트라는 개념을 처음 듣고 관심이 생기게 되었다 😀
새로 알게 된 기술을 학습할 때 공식 문서를 참조하여 간단하게 작은 것이라도 만들어 보고자 하는 편인데, 더 좋은 방법은 기존에 진행했던 프로젝트를 발전시키는 데에 적용하는 것이라고 생각한다
최근에 진행한 프로젝트 중 게임을 같이 할 파티원들을 모집하는 ‘POT’ 프로젝트를 리액트나 뷰와 같은 프레임워크 없이 진행하게 되었다.
문제는 멀티 페이지 애플리케이션이다 보니, 거의 모든 페이지에서 사용되는 헤더에 작성되는 html 코드가 중복되어 한 페이지에서 코드를 수정하게 된다면 다른 모든 페이지에서도 코드를 일일히 수정해줘야 한다는 것이다.
이를 재사용하기 쉽게 커스텀 태그로 변환하고, 관리하기 용이하게 리팩토링 해보고자 한다 🙂
웹 컴포넌트란?
재사용 가능한 커스텀 엘리먼트를 생성하고 웹 앱에서 활용할 수 있도록 해주는 다양한 기술들의 모음
세 가지 주요 기술들로 구성되며, 재사용을 원하는 어느 곳이든 코드 충돌에 대한 걱정이 없는 캡슐화된 기능을 갖춘 다용도의 커스텀 엘리먼트를 생성하기 위해 함께 사용될 수 있다.
구현 과정
커스텀 엘리먼트 생성
// components/Header.js
import setHeader from "../utils/header";
class Header extends HTMLElement {
constructor() {
super();
this.innerHTML = `
<header class="header">
<div class="header__wrapper">
<h1>
<a class="header__logo-link" href="/">
<img class="header__logo" src="/images/logo.png" alt="logo">
</a>
</h1>
<nav class="header__nav">
<a class="header__nav-link" href="javascript:void(0);"><span class="header__nav-create-pot">POT 생성</span></a>
<span class="header__nav-link login-info"></span>
</nav>
</div>
</header> `;
}
connectedCallback() {
setHeader();
}
}
export default function defineMainHeader() {
customElements.define("main-header", Header);
}
- html 파일에서 공통으로 사용하던 header를 컴포넌트로 분리했다.
- setHeader() 함수는 로그인 상태에 따라 로그인 버튼을 렌더링할지, 유저 이름을 렌더링할지 결정하는 기존 로직이다.
customElements.define()
- 페이지에 커스텀 엘리먼트를 등록하는 메서드
- 첫 번째 인자로 html에서 사용할 이름을, 두 번째 인자로 엘리먼트의 행위가 정의된 클래스를 전달한다
- 이 메서드를 실행하는 함수를 모듈로 내보내 DOMContentLoaded 이벤트가 실행되면 defineMainHeader를 실행하도록 했다
lifecycle callback
커스텀 엘리먼트의 클래스 내에서 라이프사이클 콜백을 지정할 수 있다
- connectedCallback - 사용자 정의 요소가 문서의 DOM에 처음 연결될 때 호출
- disconnectedCallback - 사용자 정의 요소가 문서의 DOM에서 연결 해제될 때 호출
- adoptedCallback - 사용자 정의 요소가 새 문서로 이동할 때 호출
- attributeChangedCallback - 사용자 정의 요소의 어트리뷰트 중 하나가 추가, 제거 또는 변경될 때 호출
observedAttributes
static get observedAttributes() {
return ['something'];
}
- attributeChangedCallback 함수를 실행시키려면 observedAttributes에 관찰할 어트리뷰트를 명시해야 한다.
- observedAttributes에 명시한 어트리뷰트 값이 변경되면 attributeChangedCallback를 실행한다.
기존 코드

커스텀 엘리먼트 적용 후


정상적으로 렌더링되는 것을 확인할 수 있고, 더 이상 헤더를 고칠때마다 매번 모든 페이지의 헤더를 고치는 것이 아니라 컴포넌트 단위로 수정하여 재사용성, 관리의 용이성을 높였다
❓HTML 모듈화
위의 실습을 통해 커스텀 엘리먼트를 생성해보았다 그렇다면 스타일까지 모두 지정해 컴포넌트만으로 의미가 있는 하나의 모듈처럼 사용할 수는 없을까?
결론은 “있다”
쉬운 방법으로는 위의 예시에 innerHTML 내부에 <style> 태그를 사용해서 그 안에 스타일을 지정해주면 제대로 동작한다.
하지만 css는 전역적으로 적용된다는 점 때문에 컴포넌트가 여러개 생기고 각각 스타일을 지정하다보면 충돌이 발생할 수도 있다
이를 Shadow DOM을 통해 해결할 수 있다
Shadow DOM
Shadow DOM은 결코 새로운 것이 아니다. 흔히들 알고 있는 <video> 태그나 <input type="range" />는 Shadow DOM 내부에 요소들이 포함되어 있다.
Shadow DOM을 사용하면 숨겨진 DOM 트리를 일반 DOM 트리의 요소에 첨부할 수 있다 Shadow DOM 트리는 모든 요소에 첨부할 수 있는 shadow 루트로 시작한다

- Shadow host - shadow DOM이 연결된 일반 DOM 노드
- Shadow tree - shadow DOM 내부의 DOM 트리
- Shadow boundary - shadow DOM이 끝나고, DOM이 시작되는 곳
- Shadow root - shadow tree의 루트 노드
특징
- 노드와 같은 방식으로 Shadow DOM 노드에 영향을 줄 수 있다.
- 차이점은 Shadow DOM 내부의 어떤 코드도 외부에 영향을 줄 수 없으므로 캡슐화를 허용한다는 것
- 이러한 특징을 이용하여 Web Component의 완성도를 더 높일 수 있다.
Element.attachShadow()
const shadow = this.attachShadow({mode: 'open'});
- Shadow root를 연결하는 메서드

위 사진과 같이 shadow-root가 연결되어 하위에 요소를 첨부할 수 있다 다만, 기존 프로젝트 스타일 적용 방식이 Sass를 컴파일 해서 적용하는 방식으로 사용했으므로 일관성을 위해 Shadow DOM은 학습 차원에서 마무리 지었다
느낀점
웹 컴포넌트를 학습하며 html, css, js가 모두 한 파일에 있는 것이 Vue.js의 .vue 파일과 상당히 유사하다는 느낌을 받았다 Vue나 React 같은 라이브러리나 프레임워크가 없던 시절에는 웹 컴포넌트를 사용했다고 한다 실제로 github에서는 프레임워크 없이 웹 컴포넌트를 사용해 사이트를 구축했다 특정 라이브러리나 프레임워크가 영원할 수 없기에 순수 자바스크립트로 컴포넌트화를 구현해보는 것은 의미있는 일이라고 생각한다.
프로젝트가 이미 마무리된 시점에서 억지로 웹 컴포넌트를 적용하려고 하니 쉽지 않았고, 그렇기에 제한되는 조건이 많아서 아쉬움이 남는다. 하지만 이번 경험을 발판 삼아 다음에 다시 사용해 볼 기회가 생긴다면 더 잘해볼 수 있을 것 같다고 느낀다❗️
reference
웹 컴포넌트 공식문서
코딩 애플 유튜브
1208
모의 코딩테스트 8회 회고
코딩테스트 문제
오랜만에 모든 문제를 풀어서 자신감을 높일 수 있었다.
하지만 푼 데에서 그치지 않고 더 좋은 방법들이 있는지 찾아보고 학습하고자 한다.
K-diff Pairs in an Array
var findPairs = function (nums, k) {
let answer = 0;
const nH = new Map();
nums.forEach((num) => {
nH.set(num, nH.get(num) + 1 || 1);
});
const arr = [...nH];
arr.forEach(([key, value]) => {
if (k === 0) {
answer = nH.get(key) > 1 ? answer + 1 : answer;
} else {
answer = nH.has(key + k) ? answer + 1 : answer;
}
});
return answer;
};
- k만큼 차이가 나는 쌍의 개수를 구하는 문제. 단, 쌍은 유일해야 함.
- 처음에는 문제를 투포인터로 해결했으나, 굳이 그럴 필요가 없었다.
- map에 숫자 개수를 저장해두고, k가 0일 때는 해당 숫자의 개수가 2 이상이면 answer를 증가시키고, 그 외에는 해당 숫자 + k가 맵에 존재하면 answer를 1 증가시키는 방법이 존재했다.
Minimum Deletions to Make Character Frequencies Unique
var minDeletions = function (s) {
let answer = 0;
let arr = Array(26).fill(0);
for (let i = 0; i < s.length; i++) {
const index = s.charCodeAt(i) - "a".charCodeAt();
arr[index] += 1;
}
arr.sort((a, b) => b - a);
for (let i = 1; i < 26; i++) {
while (arr[i] && arr[i] >= arr[i - 1]) {
arr[i] -= 1;
answer += 1;
}
}
return answer;
};
- 소문자의 개수만 구하면 되는 경우이니 26개의 배열을 생성하는 것이 생각하지 못한 점이었다.
- 아스키 코드 관련 메서드인
charCodeAt()을 기억해둬야 겠다. - 내림차순으로 정렬하고, 앞의 빈도수보다 크거나 같다면 1씩 감소하여 문제를 해결했다.
Find the City With the Smallest Number of Neighbors at a Threshold Distance
function solution(n, edges, distanceThreshold) {
const graph = Array.from(Array(n), () => Array(n).fill(100000));
edges.forEach((edge) => {
const [to, from, weight] = edge;
graph[to][from] = weight;
graph[from][to] = weight;
});
for (let k = 0; k < n; k++) {
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (i === j) graph[i][j] = 0;
graph[i][j] = Math.min(graph[i][j], graph[i][k] + graph[k][j]);
}
}
}
const tmp = [];
for (let i = 0; i < n; i++) {
let cnt = 0;
for (let j = 0; j < n; j++) {
if (i !== j && graph[i][j] <= distanceThreshold) cnt += 1;
}
tmp.push(cnt);
}
return tmp.lastIndexOf(Math.min(...tmp));
}
- 입력값이 100 이하였기에, 플로이드 와샬로 모든 지점부터 모든 지점까지의 최솟값을 구한 뒤 문제를 해결했다.
추가 수업
NHN 3번 문제
const solution = (info) => {
let answer = 0;
const ch = Array(9).fill(0);
const graph = Array.from(Array(9), () => Array(9).fill(0));
for (const [x, y] of info) {
graph[x][y] = 1;
graph[y][x] = 1;
}
function DFS(L, cur) {
if (L === 8) answer++;
else {
for (let i = 1; i <= 8; i++) {
if (ch[i] === 0 && graph[cur][i] === 0) {
ch[i] = 1;
DFS(L + 1, i);
ch[i] = 0;
}
}
}
}
DFS(0, 0);
return answer;
};
- 특정 학생을 인접해서 줄 세우지 않고, 총 8명의 학생들을 줄 세우는 문제였다.
- 체크 배열과 인접하지 말아야할 정보를 가지고 인접행렬을 만들어서 순열을 구해 문제를 해결했다.
Rotting Oranges
var orangesRotting = function (grid) {
let answer = -1;
const queue = [];
const ch = Array.from(Array(grid.length), () =>
Array(grid[0].length).fill(0)
);
const dx = [-1, 0, 1, 0];
const dy = [0, 1, 0, -1];
let flag = true;
for (let i = 0; i < grid.length; i++) {
for (let j = 0; j < grid[i].length; j++) {
if (grid[i][j] === 2) queue.push([i, j]);
if (grid[i][j] === 1) flag = false;
}
}
if (flag) return 0;
function BFS() {
while (queue.length) {
answer += 1;
const len = queue.length;
for (let i = 0; i < len; i++) {
const [x, y] = queue.shift();
for (let j = 0; j < 4; j++) {
const nx = x + dx[j];
const ny = y + dy[j];
if (
nx >= 0 &&
ny >= 0 &&
nx < grid.length &&
ny < grid[0].length &&
ch[nx][ny] === 0 &&
grid[nx][ny] === 1
) {
ch[nx][ny] = 1;
grid[nx][ny] = 2;
queue.push([nx, ny]);
}
}
}
}
}
BFS();
for (let i = 0; i < grid.length; i++) {
for (let j = 0; j < grid[i].length; j++) {
if (grid[i][j] === 1) answer = -1;
}
}
return answer;
};
- 레벨 탐색을 활용한 문제. 자주 풀어본 유형이라 어렵지 않게 풀 수 있었다.
Longest Palindromic Subsequence
var longestPalindromeSubseq = function (s) {
const len = s.length;
const dy = Array.from(Array(len), () => Array(len).fill(0));
for (let i = 0; i < len; i++) dy[i][i] = 1;
for (let i = 1; i < len; i++) dy[i - 1][i] = s[i - 1] === s[i] ? 2 : 1;
for (let i = 2; i < len; i++) {
for (let j = i; j < len; j++) {
dy[j - i][j] =
s[j - i] === s[j]
? dy[j - i + 1][j - 1] + 2
: Math.max(dy[j - i + 1][j], dy[j - i][j - 1]);
}
}
return dy[0][len - 1];
};
- 한 문자열이 주어졌을 때, 최소한의 문자를 제거하여 팰린드롬을 만드는 문제
- 쉽게 푸는 방법은 문자를 뒤집어서 LCS로 푸는 방법인데, 이번에는 정말 자주 쓰인다는 새로운 방법을 배워봤다.
- dy는 x 번째부터 y 번째까지의 최대 팰린드롬 길이다.
- 그렇기에 대각선부터 초기화하여 우측으로 증가하는 식으로 구현했다.
- 우선 n 번째부터 n 번째까지의 최대 팰린드롬 길이는 1이므로 초기화한다.
- 그리고 n 번째부터 n + 1 번째까지 최대 팰린드롬 길이는 두 수가 같으면 2, 아니면 1로 초기화한다.
- 마지막으로 대각선으로 증가시키며 첫 번째 문자와 마지막 문자가 같다면 좌하에서 + 2, 다르다면 좌, 하의 수를 비교해 큰 수를 할당하면 문제가 해결된다.
1209
리액트 수업 6일 차
degit
npx degit yamoo9/cra 프로젝트이름
- degit 명령을 사용하면 특정 Git 저장소의 최신 커밋을 찾아 다운로드 한다.
- clone 명령어와의 차이점은 .git 디렉토리를 제외하고 다운로드 할 수 있어 더욱 빠른 다운로드가 가능하다.
Audio API
오디오같은 자원을 자동 재생하고 싶은 경우, autoplay 어트리뷰트를 넣어줘도 크롬과 같은 브라우저 엔진 자체에서 자동 재생을 제한하기에 다른 코드를 넣어줘야 한다.
navigator.mediaDevices API
- 카메라, 마이크, 화면 공유와 같이 현재 연결된 미디어 입력 장치에 접근할 수 있는 MediaDevices 객체를 반환한다.
MediaDevices.getUserMedia()
- 사용자에게 미디어 입력 장치 사용 권한을 요청하며, 사용자가 수락하면 요청한 미디어 종류의 트랙을 포함한 MediaStream을 반환한다.
- 반환하는 값은 MediaStream 객체로 이행하는 Promise이다.
/* index.html */
<audio id="bgm" src="/assets/bgm-count.mp3"></audio>
<script>
window.navigator.mediaDevices
.getUserMedia({
audio: true,
})
// fulfilled
.then((stream) => {
console.log("미디어 재생 가능");
const bgmNode = document.getElementById("bgm");
bgmNode.play();
})
// rejected
.catch(({ message }) => console.error(message));
</script>
- 사용자가 음향 접근을 허용하겠냐는 질문에 확인을 누르면 then 메서드가 실행되어 음악이 정상적으로 동작한다.
JS로 모듈화
// index.js
let bgmNode = null;
settings.autoPlaySound({
src: '/assets/bgm-count.mp3' /* required */,
resolve: (audioNode) => {
bgmNode = audioNode;
bgmNode.loop = true;
bgmNode.play();
}
});
// setting.js
autoPlaySound({
src,
id = 'bgm',
resolve,
reject = (errorLog) => console.error(errorLog),
} = {}) {
document.body.insertAdjacentHTML(
'beforeend',
`<audio id=${id} src=${src} loop></audio>`
);
window.navigator.mediaDevices
.getUserMedia({
audio: true,
})
.then(() => {
resolve(document.getElementById(id));
})
.catch(({ message }) => reject(message));
- autoPlaySound의 매개변수로 받을 목록들을 순서 상관없이 받기 위해서 객체로 전달받도록 구현했다.
- body의 맨 뒤에 audio 태그를 삽입하고, 유저에게 오디오 접근을 허용할 것인지 물어봐서 허용한다면 resolve 콜백함수를 실행시키도록 한다.
- resolve 함수는 실행시킬 오디오 요소를 가지고 있는 노드 객체를 전달받아 루프로 실행시킨다.
- bgmNode를 애니메이션이 동작할 때만 실행시키기 위해 외부 스코프에 null을 할당해두고 접근할 수 있도록 했다.
insertAdjacentHTML
- 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입한다.
- 첫 번째 인수로 전달한 위치에 삽입하여 DOM에 반영한다.
- beforebegin, beforeend, afterbegin, afterend를 전달할 수 있다.
- 기존 요소에는 영향을 주지 않으므로 innerHTML 프로퍼티보다 효율적이고 빠르다.
조건부 렌더링
- React에서 조건부 렌더링은 JS의 조건 처리와 같이 동작한다. 상태에 따라서 컴포넌트 중 몇 개만을 렌더링 할 수 있다.
- JSX는 주로 문보다는 표현식을 위주로 사용한다.
StrictMode
- 디버깅을 할 때 유용한 컴포넌트. 리액트에 종속되어 있는 컴포넌트로 애플리케이션 내의 잠재적인 문제를 알아내기 위한 도구
Fragment와 같이 UI를 렌더링하지 않으며, 자손들에 대한 부가적인 검사와 경로를 활성화함.- 컴파운드 컴포넌트 - 빌드할때는 컴파일 되지 않고, 개발할 때만 사용
<StrictMode></StrictMode>
삼항식을 이용한 조건부 렌더링
let error = null;
error = {
message: "이런!! 네트워크 장애가 발생했습니다. ㅠㅡㅠ",
log() {
console.error(this.message);
},
};
const ConditionalRendering = () => {
// nullish ??
error ?? console.log("현재 앱에는 오류(error)가 발생하지 않았습니다.");
// optional chaining ?.
error?.log?.();
return (
<div className="container">
<h1 className="headline">
{!error ? "React 조건부 렌더링" : <EmojiOops height={200} />}
</h1>
<p className={error && "error-message"}>
{!error
? "오류가 존재하면 렌더링 되도록 코드를 작성합니다."
: error.message}
</p>
</div>
);
};
- ConditionalRendering 컴포넌트를 사용할 때 error가 없다면 정상적으로 문자열을, error가 있다면 웁스 이모티콘과 에러 메시지를 렌더링한다.
error?.log?.()는 에러가 있다면 log 메서드를, log 메서드가 실행 가능하다면 실행하는 코딩 방식이다.- if 문은 && 연산자로, if else문은 삼항 연산자로 대체 가능하다. 상황에 따라 가독성에 더 좋은 방법을 사용하자
컴포넌트가 렌더링하는 것을 막기
- 컴포넌트에 의해 렌더링될 때 컴포넌트 자체를 숨기고 싶을 때가 있을 수 있다.
- 이때는 렌더링 결과를 출력하는 대신 null을 반환하면 해결할 수 있다.
- 컴포넌트의 render 메서드로부터 null을 반환하는 것은 생명주기 메서드 호출에 영향을 주지 않는다.
function WarningBanner(props) {
if (!props.warn) {
return null;
}
return <div className="warning">Warning!</div>;
}
Vue2 vs Vue3
Vue3는 IE를 지원하지 않는다는 제약 사항 때문에 현업에서 가장 많이 쓰이고 있는 라이브러리 버전은 Vue2이다.
리스트 렌더링
기본 리스트 컴포넌트
const NumberList = (props) => {
const numbers = props.numbers;
const listItems = numbers.map((number) => <li>{number}</li>);
return <ul>{listItems}</ul>;
};
render(
<StrictMode>
<NumberList numbers={[1, 2, 3, 4, 5]} />
</StrictMode>,
document.getElementById("root")
);
- 일반적으로 컴포넌트 안에서 리스트를 렌더링한다.
- 배열을 ul 엘리먼트 안에 포함하고 DOM에 렌더링한다.
- 이 코드를 실행하면 리스트의 각 항목에 key를 넣어야 한다는 경고가 표시된다. key는 엘리먼트 리스트를 만들 때 포함해야 하는 특수한 문자열 어트리뷰트이다.
Key
- Key는 리액트가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕는다.
- Key는 엘리먼트에 안정적인 고유성을 부여하기 위해 배열 내부의 엘리먼트에 지정해야 한다.
- Key를 선택하는 가장 좋은 방법은 리스트의 다른 항목들 사이에서 해당 항목을 고유하게 식별할 수 있는 문자열을 사용하는 것이다.
- 대부분의 경우 데이터의 ID를 키로 사용한다.
- 안정적인 ID가 없다면 최후의 수단으로 항목의 인덱스를 키로 사용할 수 있지만, 순서가 바뀔수 있는 경우 키에 인덱스를 사용하는 것은 좋지 않다.
- 이로 인해 성능이 저하되거나 컴포넌트 state와 관련된 문제가 발생할 수 있다.
- 리스트 항목에 명시적으로 키를 지정하지 않으면 리액트는 기본적으로 인덱스를 키로 사용한다.
const todoItems = todos.map((todo) => <li key={todo.id}>{todo.text}</li>);
Key 핵심 3가지
- Key는 목록의 index가 아닌 목록의 고유한 id를 사용할 것. 셔플링을 하게 되면 매핑을 보장하지 않는다.
- Key 값이 변하면 컴포넌트를 새로 그리는 데 이를 활용할 수 있다.
- 재조정 알고리즘에서 자식들이 Key를 가지고 있다면, 리액트는 Key를 통해 기존 트리와 이후 트리의 자식들이 일치하는지 확인한다. 즉, 재조정 알고리즘의 속도를 향상시키는 데 도움이 된다.
Key 특징
- Key는 주변 배열의 context에서만 의미가 있다.
- 경험상 map() 함수 내부에 있는 엘리먼트에 Key를 넣어 주는 게 좋다.
- Key는 형제 사이에서만 고유한 값이어야 한다.
- 전체 범위에서 고유할 필요는 없다.
- Key는 힌트를 제공하지만 컴포넌트로 전달하지는 않는다. 즉, props.key는 읽을 수 없다.
- 컴포넌트에서 key와 동일한 값이 필요하면 다른 이름의 prop으로 명시적으로 전달한다.
- JSX 안에 보간법을 사용해 map 함수의 결과를 인라인으로 처리할 수 있다.
- 가독성을 위해 변수로 추출해야 할지 인라인으로 넣을지는 개발자가 직접 판단해야 한다.
- map() 함수가 너무 중첩된다면 컴포넌트로 추출하는 것이 좋다.
컴파운드 컴포넌트 패턴
function List(props) {
return <ul>{props.children}</ul>;
}
List.Item = function ListItem(props) {
return (
<li>
<a href={props.link}>{props.text}</a>
</li>
);
};
// List.Item.displayName = 'ListItem';
<List>
{list.map((item, index) => (
<List.Item key={index} link={item.link} text={item.text} />
))}
</List>;
- 컴포넌트가 컴포넌트를 포함하는 패턴
- 하위 컴포넌트에 화살표 함수를 사용할 경우 익명 함수이기 때문에 displayName prop을 사용해 React 개발 도구에 표시될 컴포넌트 이름을 지정할 수있다.
- 반면, 함수 표현식을 사용하면 이름을 명시할 수 있어 별도로 설정하지 않아도 컴포넌트 이름을 지정할 수 있다.
객체 렌더링
import React, { Fragment as Template } from "react";
{
Object.entries(db).map(([key, value]) => {
return (
<Template key={key}>
<dt>{key}</dt>
<dd>
{isObject(value) || isArray(value) ? (
<PrettyPrintCode code={value} />
) : (
value
)}
</dd>
</Template>
);
});
}
- Object.entries() 메서드를 사용해 [키, 값] 배열로 변환하고 map 메서드를 사용했다.
- dl태그 내부에 dt와 dd외의 요소는 웹 표준을 준수하기 위해 넣지 않아야 하므로 Fragment를 활용하였다.
- 참고로 Key는 fragment에 전달할 수 있는 유일한 어트리뷰트이다.
- key가 있다면
문법으로 명시적으로 선언해야 한다. - Fragment의 단축 문법은 key 어트리뷰트를 사용하지 않을 때만 사용가능하다.
컴포넌트 별 CSS 파일 관리
지금까지는 하나의 CSS 파일로 관리했다면, 이제는 관심사의 분리를 위해 컴포넌트 별로 CSS 파일을 관리하는 형태로 바꿔보고자 한다.
컴포넌트 JS 파일에서 CSS 파일을 불러오게 되면 에러가 발생한다. 이유는 JS 파일에서는 css 문법을 읽어들일 수 없기 때문이다.
style-loader
npm i -D {style,css}-loader
- style-loader는 css-loader와 함께 사용하는 것이 권장된다.
- css-loader에 의해 css가 해석되고, style-loader에 의해 html에 스타일을 주입받는다.
- webpack 설정에서 로더를 읽어오도록 설정해야 한다.
const devConfig = {
target: ["web"],
mode: "development",
devtool: "source-map",
module: {
rules: [
{
test: /\.jsx?$/i,
exclude: /(node_modules|dist)/,
use: "babel-loader",
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
},
};
module.exports = devConfig;
MiniCssExtractPlugin
npm i -D mini-css-extract-plugin
- css 파일을 추출해주는 플러그인. 빌드할 때만 필요하다.
- css 파일도 빌드되어 추출되기를 원하는 사람이 있을 수도 있다. ex) 퍼블리셔
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const devConfig = require('./config.dev');
const filteredRules = devConfig.module.rules.filter(
({ test: regExp }) => !regExp.test('.css')
);
const buildConfig = merge(devConfig, {
mode: 'production',
devtool: false,
module: {
rules: devConfig.module.rules.map((rule) => {
const { test: regExp } = rule;
if (regExp.test('.css')) {
return {
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
};
}
return rule;
}),
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].css',
}),
],
}
- 빌드 시에는 style-loader를 적용할 필요 없으니, 제외하고 새로 적용한다.
로더 vs 플러그인
- 플러그인은 웹팩의 기본적인 동작에 추가적인 기능을 제공하는 속성
- 로더는 파일을 해석하고 변환하는 과정에 관여하는 반면, 플러그인은 결과물의 형태를 바꾸는 역할을 한다.
1210
리액트 수업 7일 차
파일 압축
CssMinimizerPlugin
npm i -D css-minimizer-webpack-plugin
CSS 파일을 압축해주는 플러그인
- 이 플러그인은 웹팩의 plugins에 추가하는 것이 아니라 optimization에 추가해줘야 한다.
optimization: {
minimizer: [new CssMinimizerPlugin()],
},
TerserWebpackPlugin
자바스크립트 파일을 압축해주는 플러그인
npm i -D terser-webpack-plugin
- CssMinimizerPlugin과 마찬가지로 optimization에 추가해줘야 한다.
optimization: {
minimizer: [new CssMinimizerPlugin(), new TerserPlugin()],
},
번들 파일명 바꿔서 내보내기
const path = require('path');
const __root = process.cwd(); // 현재 루트 디렉토리
*output*:{
// 반드시 절대경로
path: path.resolve(__root,'dist'),
filename: 'js/[name].js',
},
- filename에 지정한 값이 빌드된 파일 이름이 된다.
- 파일로 들어오는 이름에 따라 수정하고 싶은 경우 [name].js 처럼 붙이면 파일이름 그대로 js가 번들되어 나온다.
리액트에서 테스트 환경 구성하기
npm i -D jest
npm i -D @testing-library/{react,jest-dom}
npx jest --init로 jest.config.js 파일 생성
// jest.config.js
module.exports = {
clearMocks: true,
collectCoverage: true,
coverageDirectory: "coverage",
roots: ["<rootDir>/src"],
setupFilesAfterEnv: ["./setupTest.js"],
testEnvironment: "jsdom",
testPathIgnorePatterns: ["/node_modules/", "/dist/"],
};
setupTest.js 파일
import "@testing-library/jest-dom/extend-expect";
- jest에서 DOM 관련 추가 메서드들을 사용할 수 있게 해줌
eslint jest 연동
npm i -D eslint-plugin-jest
- jest 함수를 알아보기 위해서 설치해줘야 한다.
module.exports = {
env: {
'jest/globals': true,
},
extends: [
'plugin:jest/recommended',
],
plugins: ['react', 'jsx-a11y', 'prettier', 'jest'],
...
};
- env, plugins, extends에 jest 설정 추가
컴포넌트 CSS 불러오도록 설정
npm i identity-obj-proxy
- 컴포넌트는 JS이므로 css를 불러왔을 때 읽을 수 없다. 즉, TDD는 css를 확인할 수 없다.
- import하는 css를 가져와서 가짜 객체를 만들어주는 플러그인. 테스트 전용 플러그인
// jest.config.js
moduleNameMapper: {
'\\.css$': 'identity-obj-proxy',
},
테스트
테스트 관련 메서드 URL
- 메서드를 모두 외울수는 없으니 필요할 때마다 찾아서 사용하자 GitHub - testing-library/jest-dom: Custom jest matchers to test the state of the DOM
// RandomCountup.test.js
import React from "react";
import { render } from "@testing-library/react";
import { RandomCountUp } from "./RandomCountUp";
// 컴포넌트 props
let count = 101;
let isComplete = true;
describe("RandomCountUp 컴포넌트", () => {
test("RandomCountUp 컴포넌트는 container 요소가 문서에 존재합니다.", () => {
// 컴포넌트 render
const { getByTestId } = render(
<RandomCountUp count={count} isComplete={isComplete} />
);
// test
expect(getByTestId("container")).toBeInTheDocument();
});
test(`RandomCountUp 컴포넌트가 출력하는 카운트 값은 ${count}입니다.`, () => {
// 컴포넌트 Render
const { getByText } = render(
<RandomCountUp count={count} isComplete={true} />
);
const countNode = getByText(count);
// 테스트
expect(countNode).toHaveTextContent(count.toString());
expect(countNode).toHaveStyle("animation-name: none");
});
});
- 테스트를 진행할 컴포넌트와 리액트 테스트 라이브러리의 render 함수를 받아온다.
- render함수는 몇 가지 프로퍼티를 가진 객체를 반환한다.
- getByTestId는 컴포넌트의 data-testid와 매칭된다.
클래스 컴포넌트
- 웹 표준 컴포넌트 기술 사양과 흡사
// 클래스 컴포넌트
export class RandomCountUpClass extends React.Component {
constructor(props) {
super(props);
}
getCompleteStyle() {
return !this.props.isComplete ? null : { animationName: "none" };
}
render() {
return (
<div className="randomCountUp">
<output style={this.getCompleteStyle()}>{this.props.count}</output>
</div>
);
}
}
- class 컴포넌트는 기본적으로 React.Component를 상속받아야 한다.
- 함수 컴포넌트는 this를 사용하지 않고, 클래스 컴포넌트는 this를 사용한다.
- 함수는 몸체 자체가 render 역할을 하고, 클래스는 돌아갈 때마다 render가 호출된다.
프레젠테이셔널 컴포넌트 vs 컨테이너 컴포넌트
프레젠테이셔널 컴포넌트
- 시각적으로 표시하는 데 중점을 둔 컴포넌트
- 상태가 없고, props를 전달받아 화면에 표시해주는 컴포넌트
- 주로 함수 컴포넌트로 많이 구현함.
컨테이너 컴포넌트
- 상태가 있고, props를 공급해주는 컴포넌트
이모지 프레젠테이셔널 컴포넌트
// Emoji.js
export function Emoji({ source, label, className, ...restProps }) {
return (
<figure
className={classNames("emoji", className)}
data-testid="wrapper"
{...restProps}
>
<img src={source} alt={label} title={label} />
</figure>
);
}
Emoji.defaultProps = {
className: "",
};
// App.js
import React from "react";
import { Emoji } from "../../components";
const props = {
id: "emoji-id",
"aria-label": "emoji-label",
source: "/assets/emoji/oops.png",
label: "웁스!!",
};
export default function App({ greetingMessage }) {
return (
<div className="app">
<h1>{greetingMessage}</h1>
<Emoji {...props} />
</div>
);
}
- 컴포넌트의 props로 객체를
{...props}형태로 보내는 문법을 지원한다. - props를 받을 때
...restProps로 rest 파라미터처럼 받을 수 있다.
Object.fromEntries()
- 키값 쌍 목록의 배열을 다시 객체로 변환하는 메서드
1211
모의 코딩테스트 9회 회고
코딩테스트 문제
Sum of Subarray Minimums
var sumSubarrayMins = function (arr) {
let answer = 0;
for (let i = 0; i < arr.length; i++) {
let min = 100000;
for (let j = i; j < arr.length; j++) {
min = Math.min(min, arr[j]);
answer += min;
}
}
return answer % (10 ** 9 + 7);
};
- 모든 부분 수열의 최솟값을 구하는 문제
- 이중 포문을 돌면서 최솟값을 갱신해나가며 해결했다.
pseudo palindrome
function solution(s) {
const answer = [];
s.forEach((str) => {
let lt = 0;
let rt = str.length - 1;
let palindromeTestResult = 0;
while (lt < rt && palindromeTestResult < 2) {
if (str[lt] === str[rt]) {
lt++;
rt--;
} else {
if (str[lt + 1] === str[rt]) lt++;
else if (str[lt] === str[rt - 1]) rt--;
else palindromeTestResult++;
palindromeTestResult++;
}
}
answer.push(palindromeTestResult);
});
return answer;
}
- 모든 단어를 순회하면서 단어의 첫 단어와 끝 단어부터 비교하여 서로 같다면 lt를 증가시키고 rt를 감소시켜 서로 다를 때의 개수를 셌다.
- 다를 경우에는 앞단어를 확인하여 그마저도 다르다면 해당 문자는 일반 문자열 처리하여 문제를 해결했다.
shortest-distance-from-all-buildings
function solution(grid) {
let answer = 1000;
const m = grid.length;
const n = grid[0].length;
const dx = [-1, 0, 1, 0];
const dy = [0, 1, 0, -1];
let L = 0;
let distance = 0;
let foundbuilding = 0;
let queue;
let ch;
const countOfbuilding = grid.reduce(
(acc, cur) => acc + cur.reduce((acc, cur) => acc + (cur === 1 ? 1 : 0), 0),
0
);
function BFS() {
while (queue.length) {
L += 1;
const len = queue.length;
for (let i = 0; i < len; i++) {
const [x, y] = queue.shift();
for (let j = 0; j < 4; j++) {
const nx = x + dx[j];
const ny = y + dy[j];
if (
nx >= 0 &&
ny >= 0 &&
nx < m &&
ny < n &&
ch[nx][ny] === 0 &&
grid[nx][ny] !== 2
) {
ch[nx][ny] = 1;
if (grid[nx][ny] === 1) {
distance += L;
foundbuilding += 1;
} else {
queue.push([nx, ny]);
}
if (foundbuilding === countOfbuilding) {
return distance;
}
}
}
}
}
return 1000;
}
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (grid[i][j] === 0) {
ch = Array.from(Array(m), () => Array(n).fill(0));
ch[i][j] = 1;
queue = [[i, j]];
distance = 0;
L = 0;
foundbuilding = 0;
answer = Math.min(answer, BFS());
}
}
}
return answer;
}
- 레벨 탐색을 이용해서 해결한 문제. 문제 풀이는 그렇게 어렵지 않았지만 코드가 길어질 수 밖에 없는 문제였다.
- 처음에 장애물에 막혀서 BFS가 멈추는 경우에 리턴값을 크게 잡고 처리를 해줬어야 했는데, 그걸 까먹어서 디버깅이 곤란했다.
- 이렇게 코드가 길어지는 문제들은 디버깅을 위해서라도 한 번에 잘 풀어야 할 거 같다.