1122 - 1128
1122
브라우저 캐시
cache: 자주 접근하는 데이터를 복사 해놓는 임시 저장소
Browser Cache: 서버 지연을 줄이기 위해 사용하는 웹 캐시의 일종
웹 캐시 또는 HTTP 캐시는 서버 지연을 줄이기 위해 웹 페이지, 이미지, 기타 유형의 웹 멀티미디어 등의 웹 문서들을 임시 저장하기 위한 정보기술이다.
- 서버와 클라이언트가 서로 통신을 하면 당연하게 지연이 생길 수 밖에 없다.
- 이 문제를 해결하기 위한 기술의 일련을 웹 캐시라고 부른다. 브라우저 캐시는 웹 캐시의 일종으로 브라우저가 웹사이트의 에셋(자산)을 저장하는 것이다.
- 브라우저 캐시에는 일반적으로 정적 자산 ex) 이미지, HTML, CSS, JavaScript을 캐싱한다.
캐시의 생명 주기
- HTTP에서 리소스란 웹 브라우저가 HTTP 요청으로 가져올 수 있는 모든 종류의 파일을 말한다. 대표적으로 HTML, CSS, JS, 이미지, 비디오 파일 등이 리소스에 해당된다.
- 웹 브라우저가 서버에서 지금까지 요청한 적이 없는 리소스를 가져오려고 할 때 서버와 브라우저는 완전한 HTTP 요청/응답을 주고 받는다. 이후 HTTP 응답에 포함된 Cache-Control 헤더에 따라 받은 리소스의 생명 주기가 결정된다.
reference
웹 서비스 캐시 똑똑하게 다루기
알고리즘
Container With Most Water
var maxArea = function (height) {
let area = 0;
let lt = 0;
let rt = height.length - 1;
while (lt < rt) {
area = Math.max(area, Math.min(height[lt], height[rt]) * (rt - lt));
height[lt] > height[rt] ? rt-- : lt++;
}
return area;
};
- 투포인터는 항상 같은 인덱스에서만 시작해서 rt를 lt가 따라간다고 생각했는데 그런 고정관념을 깨부셨다.
- lt와 rt 끝에서 시작하여 lt에 위치한 높이보다 rt에 위치한 높이가 크다면 포인터를 계속 조정하고, 해당 인덱스에서 너비를 계산해 최대값을 구하는 문제였다.
프로그래머스 카카오 Level3 - 징검다리 건너기
const solution = (stones, k) => {
let answer = 0;
let lt = 0;
let rt = 200000000;
const canGo = (mid) => {
let cnt = 0;
for (let i = 0; i < stones.length; i++) {
if (stones[i] - mid <= 0) cnt++;
else cnt = 0;
if (cnt === k) return false;
}
return true;
};
while (lt <= rt) {
let mid = Math.floor((lt + rt) / 2);
if (canGo(mid)) {
lt = mid + 1;
} else {
answer = mid;
rt = mid - 1;
}
}
return answer;
};
- 효율성을 검사하는 문제이고 주어진 값이 2억을 넘어가는 것을 보고 이분탐색을 떠올렸다.
- 확인 함수는 배열에서 특정 값을 뺀 값이 0보다 작거나 같은 횟수가 k번 반복되면 징검다리를 건널 수 없다고 구현하였다.
- 마지막으로 징검다리를 건널 수 없는 특정 값이 최대로 건널 수 있는 니니즈 친구들 수이다.
1123
FE 성능 최적화
프론트엔드 개발자에게 필요한 능력에는 어떤 것들이 있을까?
사용자 관점에서 애플리케이션을 설계하고, 디자이너와 백엔드 개발자 사이에서 일 하기 때문에 커뮤니케이션 능력을 필요로 한다.
물론 위의 두 가지 능력도 굉장히 중요하지만 기술적인 부분이나 성능 개선을 위한 지식을 보유하고 있는 것도 중요하다고 생각한다.
성능 최적화에 관심이 생겨 프론트엔드 개발자가 성능을 최적화하기 위해 적용할 수 있는 기술들에 대해 정리하고 일부 기능은 최근에 진행한 여행 가계부 애플리케이션 ‘Shall we trip?’에 적용해 보고자 한다.
성능 측정은 Lighthouse를 사용했다.
페이지 로드 최적화
블록 차단 리소스 최적화
HTML을 파싱할 때, CSS나 JavaScript 파일을 만나게 되면 파싱을 멈추고 해당 파일을 파싱하거나 다운로드 후 실행하게 되는데, 이처럼 HTML 파싱을 차단하는 요소를 블록 차단 리소스라고 한다.
- CSS의 경우
<head>태그 안에 임포트해야 하며,<script>태그로 실행되는 js는 일반적으로<body>맨 하단에 위치시킨다. - js 파일은
defer나async어트리뷰트를 이용해서 블로킹을 방지할 수 있다. 다만defer의 경우 IE9 이하에서 문제를 발생시키고,async의 경우 DOM 생성 도중 파일을 다운로드 받고 실행까지 시켜서 문제를 발생시킬 수 있으므로 사용에 주의해야 한다.
CLS 최소화하기
Cumulate Layout Shift는 누적 레이아웃 변화의 약자다. 어떤 페이지에 들어갔을 때 갑작스럽게 발생하는 레이아웃 이동의 정도를 합산 이동 거리라는 개념을 도입해서 만들어낸 지표이다. 늦게 로딩되는 리소스에 의해 레이아웃이 변화하는 현상을 최소화하는 것은 중요하다.
레이아웃 변화를 일으키는 요소
- 위치, 사이즈가 지정되어 있지 않은 이미지
- 위치, 사이즈가 없는 광고나 iframe
- 동적으로 변화하는 콘텐츠
- FOUT, FOIT 방식으로 로딩되는 웹폰트
- FOUT(Flash of unstyled text) - 웹폰트가 로드되기 전까지 시스템 기본 글꼴을 보여주고, 로드되면 이후 대체
- FOIT(Flash of invisible font) - 웹폰트가 로드되기 전까지 텍스트 렌더링을 하지 않다가 이후 렌더링
- DOM 업데이트 하기 전, 네트워크 응답을 기다리는 작업
- Layout 변화를 일으키는 CSS 속성들을 사용할 때
@import에서 link 태그 방식으로 바꾸기
여행 가계부 애플리케이션인 ‘Shall We Trip’에 CLS를 낮추기 위한 방법은 고민했다.
가장 먼저 발견한 것은 폰트와 boxicon이 @import 방식으로 css 파일에 들어가 있었다는 것이다.
@import 방식은 성능을 생각한다면 좋은 방법이 아니다. link 태그는 로딩 시 병렬방식으로 다운로드 하는 반면, @import 방식은 직렬 방식으로 다운로드 하기 때문에 로딩시간이 길어지는 문제점이 발생한다.
또한 @import 방식은 edge 브라우저에서 제대로 동작하지 않는다는 문제점이 존재한다.
link 태그는 병렬방식으로 다운로드 하여 로딩속도가 빠르고, 여러개를 link 방식으로 다운로드 하더라도 익스플로러에서 순서가 동일하게 작동한다.
preload
preload는 선언적 fetch이다. 브라우저에게 페이지에서 필요한 자원을 일찍이 fetch 하라는 지침이다.
폰트와 boxicon은 반드시 사용할 자원이기에 preload 속성을 넣어주었다.
그리고 폰트를 가져오는 사이트들에 preconnect 속성을 주어 브라우저가 외부의 도메인과 미리 연결을 할 수 있도록 했다.
리소스 용량 줄이기
리소스의 용량을 줄임으로써 리소스 다운로드 시간을 최적화할 수 있다.
JS 용량 최적화
- 트리 쉐이킹
- 번들 파일의 불필요한 코드를 제거해주는 것을 말한다. 사용하고자 하는 기능만을 임포트 한다면 파일 용량은 더욱 작아질 것이다.
- 불필요한 코드는 제거하고 tab size 2를 사용한다.
- 압축 및 난독화로 용량을 최소화한다.
- webpack 4 버전부터 mode를 production으로 설정해 놓으면 알아서 코드를 최적화해준다.
- 여러 플러그인들을 기본으로 적용해주는데, 그 중 TerserPlugin은 압축(Minify)과 난독화(Uglify)를 기본적으로 적용해준다.
압축 - 들여쓰기와 공백이 제거되고, 전체 코드가 한 줄로 병합된다. 원본 코드에서 들여쓰기, 공백, 콤마 등이 제대로 사용되지 않았다면 문제가 발생할 수 있다.
난독화 - JS 코드 자체를 분석하기 어렵게 만드는 과정. 난독화 단계를 높일수록 코드를 해석하고 실행하는 속도가 느려질 수 있다.
CSS 최적화
- 간결한 선택자 사용
- 브라우저는 셀렉터를 오른쪽에서 왼쪽으로 읽기 때문에 자식에서 부모로 거쳐서 올라가게 된다. 그래서 선택자가 복잡해질수록 비용이 많이 발생하게 된다.
- CSS 파일 압축 - CssMinimizerWebpackPlugin 사용하기
- CSS 파일에서도 불필요한 빈칸, 공백 등을 제거해 압축시켜주는 작업을 진행한다.
이미지, 미디어 최적화
- 이미지는 png보다 jpg, jpeg의 이미지 크기가 더 작으므로 시각적인 품질 차이가 적다면 jpg, jpeg 확장자를 사용한다.
- jpg, jpeg 대신 webp를 사용하면 평균 20~30% 정도 크기 감소를 시킬 수 있다. 다만 webp를 지원하지 않는 구버전 브라우저도 있기 때문에 주의해서 사용해야 한다.
- 애니메이션이 적용된 요소의 경우, gif보다 video 태그로 mp4 파일을 사용하여 적은 용량의 리소스를 요청할 수 있다.
<video src={imageSrc} type="video/mp4" autoPlay muted loop playsInline />
- 위와 같이 작성하면 마치 GIF처럼 보이게 한다.
리소스 요청 개수 줄이기
리소스 요청 개수 자체를 줄여서 사용자에게 더 빠르게 페이지를 보여줄 수 있는 성능 최적화이다.
이미지 스프라이트
여러 개의 이미지를 하나의 이미지로 합쳐서 관리하는 이미지를 의미한다.
웹 페이지에 이미지가 사용될 경우 해당 이미지를 다운받기 위해 브라우저는 서버에 이미지를 요청하게 된다.
이미지 스프라이트를 사용하면 여러 이미지를 다운로드 받아도 서버 요청을 줄일 수 있다.
-> 로딩 시간 단축
스프라이트 이미지를 다운받아 background-position 속성을 조절하여 여러 이미지를 사용할 수 있다.
lazy-loading
웹 페이지를 열면 브라우저가 모든 이미지를 읽고 불러와서 렌더링 할 것이다.
만약 많은 이미지가 한 페이지에 있다면 그 페이지를 여는 시간도 오래 걸릴 것이고 모바일 환경에서 접속한다면 수많은 데이터를 낭비하게 될 것이다.
이러한 상황을 방지하기 위해 스크롤이 하단에 닿았을 때, 동적으로 데이터를 요청하고 화면에 렌더링하도록 구현하였다.
이와 같은 방식으로 구현하면 첫 페이지를 불러올 때 로딩 시간도 단축되고, 사용자가 원할 때만 데이터를 불러올 수 있다.
reference
FE 성능 최적화
프론트엔드 성능 최적화
프로그래머스 카카오 Level3 - 무지의 먹방 라이브
function solution(food_times, k) {
food_times.unshift(0);
const sT = [...food_times];
sT.sort((a, b) => a - b);
let rest = sT.length - 1;
for (let i = 1; i < sT.length; i++) {
if (k < rest * (sT[i] - sT[i - 1])) {
const len = k % rest;
let cnt = 0;
for (let j = 1; j < food_times.length; j++) {
if (food_times[j] >= sT[i]) {
if (cnt === len) return j;
cnt++;
}
}
} else {
k -= rest * (sT[i] - sT[i - 1]);
rest--;
}
}
return -1;
}
- 해설을 여러번 보고 해도 자꾸 틀렸던 아주 어려운 문제.. 주기적으로 다시 풀어봐야겠다.
- 구현 발상은 어렵지 않다. 효율성을 위해 정렬을 한 뒤 가장 작은 수에 남은 개수를 곱해서 k에서 빼고, k가 그렇게 했을 때 0보다 작아지는 경우 남은 횟수만큼 돌려서 답을 구하는 문제였다.
- 하지만 발상을 코드로 옮기는 것이 쉽지 않았다. 무턱대고 구현했다가는 코드가 지저분해지고 조금만 꼬여도 어디까지 진행됐는지 알기 힘든 상황이었다.
- 여기서 몇 가지 기술을 이용했는데, 첫 번째로 배열의 맨 앞에 0을 삽입하여 코드를 작성하기 쉽게 만들었다. 문제 결과 자체도 몇 번째 차례인지 구하는 것이기에 0이 삽입되면 인덱스가 0부터 시작되는 배열 특성상 계산하기 복잡했다.
- 작은 수부터 계산하기 위해 정렬을 하기 위한 배열과, 원본에서 값을 찾기 위한 배열 총 두 개의 배열을 통해 답을 도출하였다. 정렬을 한 뒤 현재 인덱스 값에서 바로 앞 인덱스 값을 뺀 크기만큼 곱하고, 남은 배열의 길이를 변수로 선언하고 그 값을 반복문이 지날 때마다 하나씩 빼주는 방식을 사용했다.
Combination Sum
var combinationSum = function (candidates, target) {
const answer = [];
const tmp = [];
function DFS(s, sum) {
if (sum > target) return;
if (sum === target) {
answer.push([...tmp]);
return;
}
for (let i = s; i < candidates.length; i++) {
tmp.push(candidates[i]);
DFS(i, sum + candidates[i]);
tmp.pop();
}
}
DFS(0, 0);
return answer;
};
- 중복 조합을 구하는 문제
- 공식을 아느냐 모르냐로 갈릴 것 같은 문제였다
- 다른 부분은 조합과 똑같으나 재귀 호출을 할 때 i+1로 호출하면 조합, i로 호출하면 중복조합이다
Combination Sum2
var combinationSum2 = function (candidates, target) {
const answer = [];
const tmp = [];
candidates.sort((a, b) => a - b);
function DFS(s, sum) {
if (sum > target) return;
if (sum === target) {
answer.push([...tmp]);
return;
} else {
for (let i = s; i < candidates.length; i++) {
if (i > s && candidates[i] === candidates[i - 1]) continue;
tmp.push(candidates[i]);
DFS(i + 1, sum + candidates[i]);
tmp.pop();
}
}
}
DFS(0, 0);
return answer;
};
- 일반 조합을 구하는데, 그 중 중복된 경우는 제거하는 문제
- i가 s보다 크면서, i번 째에 존재하는 요소와 i-1번 째에 존재하는 요소가 같다면 다음 포문으로 건너 뛴다
1124
위상 정렬
순서가 정해져있는 작업을 차례로 수행해야 할 때 그 순서를 결정해주기 위해 사용하는 알고리즘
- 위상 정렬은 사이클이 발생하지 않는 방향 그래프에만 적용할 수 있다.
- 시작점이 존재해야만 사용할 수 있기 때문이다.
위상 정렬 알고리즘의 해결책
- 현재 그래프가 위상 정렬이 가능한지
- 위상 정렬이 가능하다면 그 결과는 무엇인지
큐를 이용하여 위상 정렬 수행하기
- 진입 차수가 0인 정점을 큐에 삽입한다.
- 큐에서 원소를 꺼내 연결된 모든 간선을 제거한다.
- 간선 제거 이후에 진입차수가 0이 된 정점을 큐에 삽입한다.
- 큐가 빌 때까지 2~3번 과정을 반복합니다. 모든 원소를 방문하기 전에 큐가 빈다면 사이클이 존재하는 것이고, 모든 원소를 방문했다면 꺼낸 순서가 위상 정렬의 결과입니다.
장난감 조립 문제 풀이
function solution(n, relations) {
const answer = [];
const graph = Array.from(Array(n + 1), () => Array());
const indegrees = Array(n + 1).fill(0);
const dy = Array.from(Array(n + 1), () => Array(n + 1).fill(0));
Q = [];
for (let [a, b, c] of relations) {
graph[b].push([a, c]);
indegrees[a]++;
}
for (let i = 1; i < n; i++) {
if (indegrees[i] === 0) {
Q.push(i);
dy[i][i] = 1;
}
}
while (Q.length) {
cur = Q.shift();
for (let [next, cnt] of graph[cur]) {
for (let i = 1; i < n; i++) {
dy[i][next] += dy[i][cur] * cnt;
}
indegrees[next]--;
if (indegrees[next] === 0) Q.push(next);
}
}
for (let i = 1; i < n; i++) {
if (dy[i][n] > 0) answer.push([i, dy[i][n]]);
}
return answer;
}
- 이번 코딩테스트를 치르며 위상 정렬에 대해 알게 되었다.
- 장난감을 조립하기 위해서는 중간 부품이 기본 부품 몇 개로 이루어졌는지를 알아야하는데, 중간 부품이 또 다른 중간 부품으로 이루어 질 수 있기 때문에 작업을 순서대로 실행해줘야 했다.
- 주어진 관계 배열을 순회하며 기본 및 중간 부품을 완성 부품과 그 완성 부품을 이루기 위한 기본 및 중간 부품 개수 배열을 가르키는 그래프를 만들고, 가리킴을 당하는 완성 부품의 진입 차수를 1 증가시켰다.
- 진입 차수가 0인 부품들을 Q에 삽입하였고, dy[i][j]는 j를 만들기 위해 필요한 i 부품의 개수를 저장하는 배열을 생성하고 초기화했다.
- Q가 빌 때까지, 큐에서 원소를 꺼내 해당 원소가 가리키는 부품을 순회하며 해당 부품을 만들기 위해 필요한 dy 배열을 하위 부품을 만들기 위해 필요한 부품 * 개수로 초기화 해줬다.
- 해당 부품의 진입 차수를 감소시키고, 진입 차수가 0이라면 Q에 삽입하였다.
1125
리액트 수업 - 1일 차
리액트 특징
- 선언형 - 선언형 방식을 주로 쓰지만, 명령형 방식도 혼합하여 사용한다.
- 컴포넌트 기반 - 재사용을 손쉽게 할 수 있다.
- 멀티 플랫폼 지원 - 웹 앱, 모바일 네이티브 앱, 데스크톱 앱 등
제일 중요한 주제는 함수이다. 함수를 잘 만들면 리액트도 잘 다룰 수 있다.
명령형 vs 선언형 프로그래밍 패러다임
명령형(imperative) 프로그래밍은 무언가 어떻게 해야 하는지 설명하고, 반면 선언형(declarative) 프로그래밍은 일부 명령적 구현에 대한 추상을 사용해 원하는 것이 무엇인지를 기술한다.
component driven development
컴포넌트 주도 개발. 컴포넌트를 모듈 단위로 개발하여 UI 구축에 도달하는 개발 및 설계 방법론입니다. 기본적인 컴포넌트 단위부터 시작하여 UI 뷰를 구성하기 위한 점진적 결합 방식의 상향적 성향을 띕니다.
컴포넌트란 상호 교환 가능하고 표준화 된 UI 구성 요소이다. UI 조각의 모양, 기능을 캡슐화한다.
CDD 방식의 장점
- 품질 - 독립적으로 컴포넌트를 분리하여 관련 상태를 정의하여 UI가 다양한 시나리오에서 동작하는지 확인 가능하다.
- 내구성 - 컴포넌트 수준에서 테스트하여 세부 사항까지 버그를 정확하게 찾아낼 수 있다. 테스트보다 작업량이 적다.
- 속도 - 컴포넌트를 재사용하여 UI를 보다 빠르게 조립할 수 있다.
- 효율성 - UI를 개별 컴포넌트로 분해한 다음 서로 다른 팀 구성원 간에 공유하여 개발 및 디자인을 병렬 처리한다.
멀티 플랫폼 지원
웹 앱, 모바일 네이티브 앱, 데스크톱 앱 등
용어
React Hot Loader: 코드가 변경되었을 때 페이지를 새로고침하지 않고 바뀐부분만 빠르게 교체해주는 라이브러리 CDN(Content delivery network): 웹 콘텐츠를 사용자와 가까운 곳에서 전송함으로써 전송 속도를 높인다. 실제 리소스를 다운받지 않고 CDN 되어있는 주소를 써보자
모듈 다시 내보내기
export... from... 문법을 사용하면 가져온 개체를 즉시 ‘다시 내보내기(re-export)’ 할 수 있다. 이름을 바꿔서 다시 내보낼수 있는 것이다.
// 이름으로 내보낸 모듈을 다시 내보낼 때
export * from "./textFormatting.js";
// 기본으로 내보낸 모듈을 다시 내보낼 때
export { default as throwError } from "./throwError.js";
모듈 다시 내보내기 사용 이유
- 진입점 역할을 하는 주요 파일을 통해 기능을 외부에 노출시키는 경우 외부 개발자가 패키지 안의 파일들을 뒤져 내부 구조를 건드리게 하면 안 된다. 그러려면 공개할 것만 내보내기 하고 나머지는 숨기는 것이 좋다.
- 이때 내보낼 기능을 패키지 전반에 분산하여 구현한 후, 진입점에서 이 기능들을 가져오고 이를 다시 내보내면 원하는 바를 어느 정도 달성할 수 있다.
default export 다시 내보내기
기본 내보내기를 다시 내보낼 때는 주의점이 있다.
export * from './user.js'를 사용해 모든 걸 한 번에 다시 내보내면 default export는 무시되고, named export만 다시 내보내진다.- 두 가지를 동시에 다시 내보내고 싶다면 두 문을 동시에 사용해야 한다.
- default export를 다시 내보낼 땐 이런 상황을 인지하고 있다가 처리해줘야 하므로 몇몇 개발자들은 default export를 다시 내보내는 것을 선호하지 않는다.
1126
리액트 수업 - 2일 차
실습 환경 설정
package.json 기본 모듈 타입 module로 변경
서버에서 라이브 서버를 require 방식이 아닌 import 방식으로 받아오기 위해서는 아래와 같이 설정해야 한다.
- 별다른 설정을 하지 않으면 common.js 방식으로 모듈을 불러오기 때문에 package.json에서 type을 module로 설정한다.
- 기본 모듈을 mjs 방식으로 설정하였으므로, server 측 코드를 require이 아닌 import 방식으로 바꾸고 서버의 index.js의 확장자도 mjs로 바꾼다.
env 설정
live 서버를 실행 시킬 때 process.env에서 저장되어 있는 값을 참조하여 기본 값으로 세팅하려고 하는데, env 파일을 생성하지 않고 npm 스크립트에서 환경변수를 세팅하며 실행 시킬 수 있다.
"scripts": {
"start": "cross-env OPEN='/client/public' npm run dev",
"dev": "cross-env PORT=8080 node server/index.mjs",
}
- 위와 같이
키=값형태로 등록하면process.env.키로 환경변수를 참조 가능하다.
HTML 파일 메타 태그
<!-- 인터넷 익스플로러만을 위한 코드 -->
<!-- 인터넷 익스플로러의 가장 마지막 버전. 11 버전으로 렌더링 해달라는 명령 코드 -->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!-- 너비를 각각의 디바이스에 맞추라는 코드 -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
서버 파일 수정 시 자동 변경
- nodemon과 node-dev가 있다.
- 둘다 동작 방식은 비슷한듯 하다
Test 유틸리티
Jest라는 프레임워크와 비슷한 방식으로 동작하는 유틸리티를 가볍게 구현해보았다.
// tests.js
import throwError from "./throwError.js";
// 기대(expect) 값을 검토하는 유틸리티
// 코드 사용 예시)
// expect(전달값).toBe(기대값)
// expect(전달값).not.toBe(기대값)
// expect(전달값).toBeInTheDocument(기대값)
// expect(전달값).not.toBeInTheDocument(기대값)
export function expect(received) {
// 전달 값과 비교할 수 있는 유틸리티 모음 객체 반환
return {
toBe(expected) {
// 전달값과 기대값을 비교해서 같지 않으면 오류 발생
if (received !== expected) {
throwError(`${received}와 ${expected} 값은 동일하지 않습니다.`);
}
},
toBeTruthy() {
if (received !== true) {
throwError(`${received} 값은 true가 아닙니다.`);
}
},
toBeFalsy() {
if (received !== false) {
throwError(`${received} 값은 false가 아닙니다.`);
}
},
toBeInTheDocoument() {
if (!document.body.contains(received)) {
throwError(`${received}는 문서에 포함되지 않습니다.`);
}
},
toHaveClass(expected) {
if (!received.classList.contains(expected)) {
throwError(
`${received} 요소는 ${expected} 클래스 이름을 포함하지 않습니다.`
);
}
},
not: {
toBe(expected) {
if (received === expected) {
throwError(`${received}와 ${expected} 값이 동일합니다.`);
}
},
toBeInTheDocoument() {
if (document.body.contains(received)) {
throwError(`${received}는 문서에 포함되어 있습니다.`);
}
},
},
};
}
// 테스트(test) 유틸리티
// 코드 사용 예시)
// test('1 + 1 = 2', () => expect(1 + 1).not.toBe('12'))
export function test(description, callback) {
// 오류 발생 여부 감지
try {
callback();
console.log(`🟢 테스트 성공: ${description}`);
} catch (error) {
console.groupCollapsed(`🔴 테스트 실패: ${description}`);
console.error(error.message);
console.groupEnd();
}
}
// 기술(describe) 유틸리티
// 코드 사용 예시)
// describe('테스트 리스트 항목을 대변하는 레이블', () => { test(); test(); ... })
export function describe(testLabel, callback) {
console.group(testLabel);
callback();
console.groupEnd();
}
- 야무 강사님이 라이브코딩으로 작성한 이 코드는 내게 큰 충격을 주었다.. 충격을 받은 포인트는 다음과 같다.
- 클로저의 활용 - 모듈을 사용하지 않았을 때 정보은닉을 하는 용도를 제외하고 클로저를 충분히 활용하지 못했던 것 같다. 하지만 expect 함수는 클로저를 아름답게 활용했다. 전달 값을 자유 변수로 두고, 이 자유변수를 참조하는 메서드들로 구성된 객체를 반환하는 것을 보고 ‘아 .. 클로저는 이렇게 사용하는 것이구나’ 깨닳았다.
- 부정 메서드를 중첩 객체를 활용해서 키를 not으로 두고 내부에 메서드를 작성하는 것이 생소하고 신기했다.
- 비교 유틸리티에서 원하는 값을 얻지 못하면 throwError 함수로 에러를 호출하였는데, test 함수에서 콜백 내부에 비교 함수를 작성하고 이에 실패했을 때 내부에서 발생시킨 에러를 사용해 try catch로 에러처리하는 것이 정말 깔끔하다고 느끼게 했다.
console.group 관련 메서드
- console.group console.end 메서드와 함께 사용하면 내부에 있는 콘솔을 그룹핑 해준다.
- console.groupCollapsed 내부에 그룹핑 된 콘솔을 닫은 채로 출력한다. (열 수 있음)
사용 예시
import { describe, test, expect } from "../utils/index.js";
const appNode = document.getElementById("app");
const headlineNode = appNode.querySelector("h1,h2,h3,h4,h5,h6");
describe("DOM 테스트", () => {
test('문서의 제목이 "React 앱 개발환경 구성" 인가?', () => {
expect(document.title).toBe("React 앱 개발환경 구성");
});
test("문서에 #app 요소가 존재하는가?", () => {
expect(appNode).toBeInTheDocoument();
});
test("#app 요소 안에 제목 요소가 포함되어 있나?", () => {
const hasHeadlineElement = !!appNode.querySelector("h1,h2,h3,h4,h5,h6");
expect(hasHeadlineElement).toBeTruthy();
});
test('제목의 내용은 "React 앱 개발"', () => {
expect(headlineNode.textContent).toBe("React 앱 개발");
});
test("제목 요소는 headline 클래스를 포함하는가?", () => {
expect(headlineNode).toHaveClass("headline");
});
});
1127
라인 코딩테스트
1번
function solution(record) {
const answer = [0, 0];
const queue = [];
const stack = [];
for (let i = 0; i < record.length; i++) {
const [act, price, cnt] = record[i].split(" ");
if (act === "S") {
for (let j = 0; j < cnt; j++) {
answer[0] += queue.shift();
answer[1] += stack.pop();
}
} else {
for (let j = 0; j < cnt; j++) {
queue.push(+price);
stack.push(+price);
}
}
}
return answer;
}
console.log(
solution(["P 300 6", "P 500 3", "S 1000 4", "P 600 2", "S 1200 1"])
);
- 물건을 선입선출, 후입선출 방식으로 각각 계산하여 결과를 반환하는 문제이다.
- 큐와 스택으로 해결하였다.
2번
function solution(arr) {
let answer = 0;
let isDown = false;
let lt = 0;
let rt = 0;
for (let i = 1; i < arr.length; i++) {
if (arr[i] > arr[i - 1]) {
if (isDown) {
lt = i - 1;
isDown = false;
}
rt = i;
} else if (arr[i] < arr[i - 1]) {
isDown = true;
answer += rt - lt;
} else {
lt = i + 1;
rt = i + 1;
}
}
return answer % (10 ** 9 + 7);
}
console.log(solution([0, 1, 2, 5, 3, 7]));
- 연속되는 바이토닉 수열의 개수를 구하는 문제였다.
- 투포인터로 해결하였고, 두 수를 비교해서 클 때, 작을 때, 같을 때로 구분하여 처리하였다.
- 같은 경우 이전까지의 수에서는 바이토닉 수열을 구할 수 없으므로 초기화했다.
- 현재 인덱스 값이 이전 인덱스 값보다 작은경우 현재 인덱스 까지의 바이토닉 수열 개수를 구해서 정답에 더했다.
- 핵심은 현재 인덱스 값이 증가하는 경우의 위치를 계속해서 저장해두는 것이었다.
3번
function solution(n, k) {
const queue = [];
const dx = [-1, 0, 1, 0];
const dy = [0, 1, 0, -1];
const dist = Array.from(Array(n + 1), () =>
Array(n + 1).fill(Number.MAX_SAFE_INTEGER)
);
function getMax() {
let coor = [];
let max = 0;
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= n; j++) {
if (
dist[i][j] > max ||
(coor.length && dist[i][j] === max && j < coor[1])
) {
max = dist[i][j];
coor = [i, j];
}
}
}
return coor;
}
function BFS() {
while (queue.length) {
const [x, y] = queue.shift();
for (let i = 0; i < 4; i++) {
const nx = x + dx[i];
const ny = y + dy[i];
if (nx > 0 && nx <= n && ny > 0 && ny <= n) {
if (dist[x][y] + 1 < dist[nx][ny]) {
dist[nx][ny] = dist[x][y] + 1;
queue.push([nx, ny]);
}
}
}
}
}
dist[1][1] = 0;
queue.push([1, 1]);
BFS();
for (let i = 2; i < k; i++) {
const [x, y] = getMax();
dist[x][y] = 0;
queue.push([x, y]);
BFS();
}
return getMax();
}
- BFS에서는 현재 queue에 저장되어 있는 좌표를 기준으로 모든 좌표의 최대 거리를 계산하여 입력하도록 했다.
- 가장 거리가 멀면서 열의 값이 작은 좌표를 반환하는 getMax()함수를 작성하고, 이를 k-1번 반복한 뒤 getMax()를 반환하면 k 번째 들어오는 사람의 좌표 위치를 구할 수 있었다.
4번
function solution(rectangles) {
const answer = [];
const tmp = [];
const arr = rectangles
.map((v, i) => [i, v])
.sort((a, b) => a[1][1] - b[1][1]);
const dy = Array(1000001).fill(0);
const dx = Array(1000001).fill(0);
for (let i = 0; i < arr.length; i++) {
const [x1, y1, x2, y2] = arr[i][1];
const height = y2 - y1;
let baseY = 0;
for (let j = x1; j < x2; j++) {
baseY = Math.max(baseY, dy[j]);
}
for (let j = x1; j < x2; j++) {
dy[j] = baseY + height;
}
tmp.push([arr[i][0], [x1, baseY, x2, baseY + height]]);
}
tmp.sort((a, b) => a[1][0] - b[1][0]);
for (let i = 0; i < tmp.length; i++) {
const [x1, y1, x2, y2] = tmp[i][1];
const width = x2 - x1;
let baseX = 0;
for (let j = y1; j < y2; j++) {
baseX = Math.max(baseX, dx[j]);
}
for (let j = y1; j < y2; j++) {
dx[j] = baseX + width;
}
answer.push([tmp[i][0], [baseX, y1, baseX + width, y2]]);
}
return answer.sort((a, b) => a[0] - b[0]).map((v) => v[1].join(" "));
}
console.log(
solution([
[0, 2, 1, 3],
[1, 2, 2, 5],
[3, 3, 4, 4],
[4, 1, 6, 3],
[1, 6, 5, 7],
[5, 5, 7, 6],
[5, 8, 6, 10],
])
);
- 위에서 내려오는 도형을 계산하는 것과 오른쪽에서 왼쪽으로 미는 로직을 철저히 분리하여 구현하였다.
- 핵심은 현재까지 바닥이나 왼쪽에서부터 쌓여있는 높이를 저장해두는 것이었다.
총평
난이도도 내가 느끼기에는 적절했고, 테스트 케이스가 모두 알 수 없다보니 정답을 맞은지는 모르겠지만 끝까지 다 풀었다는 점에서 뿌듯하다.
이제는 공채 코딩테스트도 어느정도 도전해볼만 하다는 생각이 든다.
결과가 어떻게 나올지는 모르겠고, 경력직을 뽑는 과정이다보니 코딩테스트가 조금 쉽게 나왔을 수도 있지만 그래도 만족스러운 결과를 얻은 것 같다.
1128
우테코 프리코스 1주차
우아한 테크코스 1차를 합격하고 프리코스 주간이 시작되었다.
자바스크립트 연습도 할 겸 도움이 될 것 같다고 판단하여 진행하기로 결정했다.
미션은 기능 요구사항, 프로그래밍 요구사항, 과제 진행 요구사항 세 가지로 구성되어 있었다.
기능 단위로 커밋을 하는 연습을 하려고 했다.
1주차 과제는 이전 우테코 과정과 똑같은 야구 게임이었다. 하지만 처음 접하여 요구사항을 지키면서 하려다보니 생각보다 시간이 걸렸다.
1주차라 난이도는 쉬웠지만, 그 속에서도 배운점이 있었다. 2, 3주차 때도 시간을 아껴서 도전해볼만한 가치가 있다는 생각이 들었다.
느낀 점
기존에도 클린코드나 폴더 구조화에 관심이 많아 신경쓰며 코딩을 진행했지만 이렇게 규칙을 제대로 정해놓고 한 적은 처음이라 좋은 경험이었습니다. 함수 내부 로직을 15줄 이내로 맞추려다보니 자연스럽게 함수가 수행하는 일이 줄어들었고, 다 구현해놓고 보니 생각보다 더 깔끔하게 구조가 잡힌 것 같아 이러한 코딩 컨벤션의 중요성을 다시금 깨닫게 되는 순간이었습니다.
원래부터 커밋 로그를 남길 때 prefix를 달거나 제목을 명사 형식으로 작성하며 진행했지만, 정작 프로젝트를 진행하다보면 기능별로 커밋을 남기기가 쉽지 않고 커밋 메세지 body 부분을 작성하는 것을 소홀히 했던 것 같습니다. 하지만 이번에 이런 부분들을 이전보다 신경쓰며 진행하다 보니 확실히 기록 로그가 깔끔하게 남는다는 것을 몸소 체험했습니다.