정렬된 요소를 왼쪽으로, 정렬 되지 않은 요소는 오른쪽에 있다고 가정한다. 오른쪽에서 정렬이 되지 않은 요소를 가져와 이미 정렬된 요소들과 비교하여 적절한 위치에 삽입한다. 이중 반복문을 사용하며, 시간 복잡도는 항상 n^2이다. 따라서 배열의 길이가 늘어날수록 시간이 급격히 증가한다.
2. 코드로 살펴보기
function insertionSort(arr) {
const len = arr.length;
for (let i = 1; i < len; i++) {
// 인덱스 0 요소는 이미 왼쪽에서 정렬되어 있다고 가정한다.
const current = arr[i];
// 이번 회차에서 정렬하고차 하는 요소
let j = i - 1;
// 이미 정렬된 요소의 가장 오른쪽부터 비교를 시작한다.
while (j >= 0 && arr[j] > current) {
arr[j + 1] = arr[j];
j--;
// 현재 비교하는 수가 정렬 요소의 수보다 작을 때, 정렬된 요소를 오른쪽으로 한칸 땡긴다.
// 그리고 왼쪽 요소와 비교를 한번 더 진행한다.
}
arr[j + 1] = current;
// 정렬된 요소 중에서 더 작은 수가 없을 경우, 현재 요소를 정렬된 요소의 인덱스로 집어 넣는다.
}
return arr;
}
가장 큰 수, 혹은 가장 작은 수를 선택해서 정렬을 반복하는 과정이다. 이중 반복문을 사용하며, 각 회차마다 정렬이 되지 않은 모든 요소를 조회하여 가장 큰 수(가장 작은 수)를 찾아 제일 오른쪽(왼쪽)으로 위치를 변경한다. 이중 반복문을 사용하므로 시간 복잡도는 항상 n^2으로, 배열의 길이가 늘어날수록 시간이 급격히 늘어난다.
2. 코드로 살펴보기
function selectionSort(arr) {
const len = arr.length;
for (let i = 0; i < len - 1; i++) {
// 전체 요소를 조회하면서 가장 작은 수의 인덱스를 찾는다.
// len - 2 인덱스까지 정렬을 하면, 마지막 인덱스인 len - 1도 자동 정렬이 되기 때문에
// len - 1 인덱스 까지만 반복문을 실행한다.
let minIndex = i;
// 반복문을 시작하는 인덱스가 가장 작은 수의 인덱스라고 가정하고 시작한다.
for (let j = i + 1; j < len; j++) {
// i 다음 인덱스부터 시작해서 남은 모든 요소를 조회한다.
if (arr[minIndex] > arr[j]) {
minIndex = j;
// 만약 현재까지 찾은 가장 작은 수보다 더 작은 수가 나오면,
// 해당 수의 인덱스로 변경한다.
}
}
if (i !== minIndex) {
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
// 조회하면서 찾은 가장 작은 수의 위치를 가장 왼쪽으로 변경한다.
}
}
return arr;
}
물 속에서 물방울이 수면 위로 올라오는 것처럼 가장 큰 수가 계속 오른쪽으로 자리를 옮겨가면서 진행되는 정렬 알고리즘이다. 이중 반복문으로 진행되며, 각 회차마다 인접한 두 요소 (앞, 뒤)를 비교하고 앞의 요소가 뒤 요소보다 클 시 자리를 교환한다. 이중 반복문을 사용하여 시간 복잡도는 항상 n^2으로 배열의 크기가 클수록 실행 시간이 크게 늘어난다.
2. 코드로 살펴보기
function bubbleSort(arr) {
const len = arr.length;
for (let i = 0; i < len - 1; i++) {
// 배열의 전체 요소를 조회합니다. 단, 앞, 뒤 요소를 비교하므로 len - 1 미만으로 범위를 지정합니다.
// 만약 len 미만으로 지정할 시, 맨 마지막 요소는 비교할 대상이 없어 문제가 발생합니다.
for (let j = 0; j < len - 1 - i; j++) {
// j의 범위는 len -1 -i 미만으로 지정합니다. len -1 -i 이상 인덱스 요소들은 이전 회차에서
// 이미 정렬이 된 요소 이므로 다시 비교를 진행하지 않습니다.
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
// 만약 앞의 요소가 뒤의 요소보다 클 시, 위치를 변경합니다.
}
}
}
return arr;
}
자바스크립트는 싱글스레드 언어이다. 따라서 한번에 하나의 작업만 수행한다. 하지만 이 때의 문제는 만약 시간이 오래 걸리는 작업이 있을 시, 전체 서비스가 멈출 수 있다는 것이다. 예를 들어 서버에서 데이터를 모두 받아올 때까지 서비스의 작동이 멈춘다면 어떻게 될까? 이러한 상황을 해결하기 위한 것이 이벤트 루프다.
2. 이벤트 루프는 어떻게 작동할까?
알아야 할 주요 개념은 콜스택, 태스크 큐 2가지다. 콜 스택은 함수가 실행될 때, 콜스택에 추가된다. 콜스택에 담긴 작업은 순차적으로 실행된다. 만약 당장 실행을 끝마치지 않아도 되는 작업 (비동기 작업)일 시 백그라운드로 옮겨 작업을 진행한다. 비동기 작업이 끝난 후 발생한 작업 (콜백 함수, 데이터 전처리 작업 등이 해당)은 태스크 큐에 추가된다. 이후 콜스택이 비었을 때 테스크 큐에 있는 작업을 콜스택으로 옮겨 진행한다.
3. 예시 코드로 살펴보기
function fetchData() {
console.log("fetchData 함수 시작");
fetch("https://example.com").then((response) => {
console.log("받아온 데이터를 json으로 변환");
return response.json();
});
console.log("fetchData 함수 끝");
}
상단의 코드가 실행되는 과정은 다음과 같다.
1. fetchData 함수가 콜스택에 추가된다.
2. 'fetchData 함수 시작' 문자열이 출력된다.
3. 서버에서 데이터를 불러오는 작업을 백그라운드로 옮겨 진행한다.
3 - 1. 데이터를 모두 불러왔으면, '받아온 데이터를 json으로 변환' 문자열을 출력하는 작업을 태스크 큐에 추가한다.
4. 'fetchData 함수 끝' 문자열을 출력하고, 콜스택에 fetchData 함수를 제거한다.
5. 콜스택에 비었으므로, 태스크 큐에서 담긴 '받아온 데이터를 json으로 변환' 작업을 옮겨와 실행한다.
맛집 지도를 구현할 때에 카카오 지도 API를 이용합니다. 이때에 완성된 코드를 그대로 따라치는 것이 아니라 공식 문서에 어떤 부분을 이용하는 지 같이 살펴본다는 점이 좋았다. 카카오 map을 이용할 때에는 Script를 이용해 불러오기 때문에 대부분 컴포넌트 형식으로 외부 모듈을 불러오는 react/ next 프레임워크의 방식과 다소 차이가 있어 이후에도 참조해야 할듯 하다.
nextjs는 프론트엔드 뿐만 아니라 백엔드 기능도 담당하므로 데이터 베이스 작업을 담당하는 ORM도 사용하게 된다. 그중 Prisma ORM은 사용법 간편한 편이기 때문에 자주 사용된다. 개인적으로 Prisma를 사용할 때에 Prisma Studio를 사용해 현재 데이터를 보기가 편하다는 이점이 있다.
Prisma 구현 과정
root 폴더에 prisma 폴더 생성
schema.prisma 파일에 사용하고자 하는 테이블 스키마 작성
seed.ts에 기본 데이터 추가 과정 작성
Prisma studio, npx prisma studio 명령어를 입력하면 데이터 간편히 조회할 수 있다.
이번 강의는 svg를 통해 애니메이션 효과를 적용하는 내용이다. 그동안은 막연하게 이미지에 세부적으로 애니메이션을 적용하려면 조각조각난 이미지를 합쳐서 표현해야 하나 생각했었는데, 이미지를 svg로 변환한다면 각 부위를 css class를 통해 쉽게 접근할 수 있다는 것을 알게 되었다.
1. 구현화면
2. svg를 이용한 애니메이션 효과
svg 파일일 경우 각 요소들의 태그로 접근해서 css를 적용할 수 있다.
svg 형태의 파일일 경우에는 그려진 요소들이 엘리먼트 태그들이다. 그래서 css 적용을 원하는 태그를 찍어서 해당 태그에만 css 애니메이션을 적용함으로써 역동적인 화면을 만들어 낼 수 있다. 이를 활용하여 기존의 이미지 파일을 svg로 변환한 다음 원하는 조작을 할 수 있다.
3. transform-box, transform-origin 속성
transform-box
transform 작업이 일어나는 위치 설정
'border-box': 기본값, 테두리를 포함한 영역의 내부 경계선을 기준으로 작업
'stroke-box': 테두리를 포함한 영역의 외부 경계선을 기준으로 작업
'view-box': 현재 뷰포트를 기준으로 작업
'fill-box': 요소의 content를 기준으로 작업
transform-origin
transform 작업의 중심점 설정
'center', 'top' 등의 키워드 혹은 픽셀값, 백분율 등을 사용하여 어떤 위치를 중심으로 잡을 것인지 선택 가능
이 두가지 속성을 이용해 애니메이션을 좀더 구체적으로 구현할 수 있다. ex) 빙글빙글 도는 애니메이션
4. 정리하기
해당 강의에서 알게된 점은 다음과 같다.
svg를 활용하면 세부적인 애니메이션 적용이 가능하다.
transform-box, transform-origin을 통해 transform의 세밀한 조정이 가능하다.
처음 배웠던 것이 npm이었기에 프로젝트에 모듈을 설치할 때에 언제나 'npm start'를 입력하곤 했다. 하지만 입사한 회사에서는 yarn을 사용하고 있었기에 자동반사로 뛰쳐나오는 'npm install'을 억누르는 중이다. npm 대신 yarn을 쓰게 되면 뭐가 더 좋은 걸까?
npm과 yarn
공통점
자바스크립트의 패키지 매니저
npm
nodejs의 기본 패키지 관리자
'npm install' 사용시 package-lock.json 파일로 의존성 관리
yarn
facebook에서 개발한 javascript 패키지 관리자
'yarn insall' 사용 시 yarn.lock 파일로 의존성 관리
yarn은 뭐가 더 좋은가
빠른 패키지 설치 (병렬 설치)
여러 패키지를 설치할 때에 동시에 진행됨.
npm은 패키지 설치를 순차적으로 진행하므로 yarn을 사용한다면 설치 시간을 단축시킬 수 있음.
오프라인 패키지 설치
yarn은 패키지를 설치할 때에 해당 패키지를 캐시에 저장(.yarn-cache 폴더)
이후 오프라인일 때에도 해당 캐시를 이용해 설치 가능
정리
시간을 단축할 수 있다!
yarn을 사용한다면 패키지를 병렬 설치할 수 있다는 점, 기존 패키지들이 캐시에 저장된다는 점을 통해 설치 속도를 줄일 수 있다.
이전 캐릭터의 그림을 지우기 위해 해당 좌표를 clearRect로 지울 시 배경까지 같이 삭제되어 버림
따라서, 배경, 캐릭터를 레이어로 나누어 구현
배경은 바닥에, 캐릭터는 공중에 뜬 상태로 그려진다고 볼 수 있음
3. 구현
배경 이미지를 넣을 canvas, 캐릭터를 넣을 canvas 2개 생성
배경 canvas와 캐릭터 canvas가 겹쳐져야 하므로 상위 태그에는 relative, canvas에는 absolute 속성 부여
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<!-- 배경 이미지를 넣을 canvas, 캐릭터를 넣을 canvas 2개 생성 -->
<!-- 배경 canvas와 캐릭터 canvas가 겹쳐져야 하므로 상위 태그에는 relative, canvas에는 absolute 속성 부여 -->
<div style="position: relative;">
<canvas id="background" style="position: absolute;"></canvas>
<canvas id="character" style="position: absolute;"></canvas>
</div>
<script src="maple.js"></script>
</body>
</html>
배경 canvas 지정
캐릭터 canvas 지정
배경 제작
배경 경로 불러오기
배경 이미지를 불러오면 배경 canvas에 그리기
이미지를 불러오는 과정에서 비율이 깨지지 않게 이미지의 가로, 세로 길이를 그대로 canvas에 지정
캐릭터 제작
캐릭터 경로 불러오기
캐릭터의 x 좌표 지정
캐릭터 불러오면 캐릭터 canvas에 그리기
캐릭터가 이동할 수 있는 반경은 배경 내부 이므로 배경 이미지의 가로, 세로 길이를 그대로 canvas에 지정
canvas에서 키입력 이벤트 설정
캐릭터의 좌표가 0이하면 더이상 왼쪽으로 이동 못하게 제한
"캐릭터의 x 좌표 값 + 캐릭터의 가로 길이"가 배경의 가로 길이보다 커질 시 더이상 오른쪽으로 이동 못하게 제한
// maple.js
// 배경 canvas 지정
const backgroundCanvas = document.getElementById("background");
const backCtx = backgroundCanvas.getContext("2d");
// 캐릭터 canvas 지정
const characterCanvas = document.getElementById("character");
const chaCtx = characterCanvas.getContext("2d");
// 배경 경로 불러오기
const port = new Image();
port.src =
"https://github.com/ka0824/ka0824.github.io/assets/79782594/e264a9fb-541b-4cd4-845b-947385a19cef";
// 배경 이미지를 불러오면 배경 canvas에 그리기
// 이미지를 불러오는 과정에서 비율이 깨지지 않기 위해 이미지의 가로, 세로 길이를 그대로 canvas에 적용
port.onload = function () {
backgroundCanvas.width = port.width;
backgroundCanvas.height = port.height;
backCtx.drawImage(port, 0, 0);
};
// 캐릭터 경로 불러오기
const mushroom = new Image();
mushroom.src =
"https://github.com/ka0824/ka0824.github.io/assets/79782594/1cac660c-db2c-46d0-af9a-ecfebe31b62d";
// 캐릭터의 x 좌표 지정
let mushroomX = 0;
// 캐릭터 불러오면 캐릭터 canvas에 그리기
// 캐릭터가 이동할 수 있는 반경은 배경 내부 이므로 배경 이미지의 가로, 세로 길이를 그대로 적용
mushroom.onload = function () {
characterCanvas.width = port.width;
characterCanvas.height = port.height;
chaCtx.drawImage(mushroom, mushroomX, 100, 40, 50);
};
// canvas에서 키입력 이벤트 설정
document.addEventListener("keydown", function (event) {
switch (event.key) {
case "ArrowLeft":
// 캐릭터의 좌표가 0 이하면 더이상 왼쪽으로 이동 못하게 제한
if (mushroomX <= 0) {
return;
}
// 이동 속도는 -10, 이동할 때 마다 이전 이미지 지워주기
mushroomX -= 10;
chaCtx.clearRect(0, 0, characterCanvas.width, characterCanvas.height);
chaCtx.drawImage(mushroom, mushroomX, 100, 40, 50);
return;
case "ArrowRight":
// 캐릭터의 가로축이 40이므로, '현재 캐릭터의 x 좌표 값 + 40'이 배경의 가로축보다 커지면 오른쪽으로 이동 못하게 제한
if (mushroomX + 40 >= characterCanvas.width) {
return;
}
mushroomX += 10;
chaCtx.clearRect(0, 0, characterCanvas.width, characterCanvas.height);
chaCtx.drawImage(mushroom, mushroomX, 100, 40, 50);
return;
default:
return;
}
});
clearRect를 사용하지 않으면 이전 화면도 그대로 남아있음. 상황에 따라 사용 여부 결정할 것.
requestAnimationFrame 함수를 이용하자
작성한 애니메이션 함수를 requestAnimationFrame을 이용해 애니메이션으로 구현할 수 있음
2. 직선 이동 애니메이션
애니메이션 시작 좌표 정하기
좌표 증가 값 정하기 (애니메이션의 속도)
애니메이션 함수 작성
좌표 증가 시키기
달라진 좌표에 맞춰 그림 그리기
애니메이션 맞춤 설정
// lineAnimation.js
const canvas1 = document.getElementById("my-canvas1");
const ctx1 = canvas1.getContext("2d");
// 애니메이션 시작 좌표 정하기
let startX1 = 10;
let startY1 = 10;
const dx1 = 10;
function lineAnimation() {
// 캔버스 지우기
// 캔버스의 width, height 값 입력
ctx1.clearRect(0, 0, canvas1.width, canvas1.height);
ctx1.fillStyle = "black";
// 좌표 증가 시키기
startX1 += dx1;
// 달라진 좌표에 맞춰서 그림 그리기
ctx1.fillRect(startX1, startY1, 100, 100);
// 애니메이션 맞춤 설정
requestAnimationFrame(lineAnimation);
}
lineAnimation();
3. 원모양 이동 애니메이션
시작 각도 지정
각도 늘어나는 값 지정 (애니메이션 속도)
애니메이션 함수 작성
원의 중심 좌표 업데이트
코사인(cos), 사인(sin)을 이용해 x 좌표, y 좌표를 반지름에 맞춰 비율 조정
cos(각도) = b / c
sin(각도) = a / c
현재 각도에서 생성된 b와 c의 비율 값을 통해 원 외곽의 x 좌표 구하기
현재 각도에서 생성된 a와 c의 비율 값을 통해 원 외곽의 y 좌표 구하기
생성된 x, y 좌표에 경로의 둘레를 얼마나 크게 할 것 인지 값 설정
원 외곽의 x, y 좌표를 구하는 과정cos(각도) = b / c, 가로 b를 사용하므로 x좌표에 사용
sin(각도) = a / c, 세로 a를 사용하므로 y 좌표에 사용
// circleAnimation.js
const canvas2 = document.getElementById("my-canvas2");
const ctx2 = canvas2.getContext("2d");
let startX2 = 50;
let startY2 = 50;
// 각도 지정
let angle = 0;
// 애니메이션 속도, 각도가 늘어나는 값
const speed = 0.1;
function circleAnimation() {
ctx2.clearRect(0, 0, canvas2.width, canvas2.height);
ctx2.beginPath();
ctx2.arc(startX2, startY2, 10, 0, 2 * Math.PI);
ctx2.fillStyle = "white";
ctx2.fill();
ctx2.strokeStyle = "black";
ctx2.lineWidth = 2;
ctx2.stroke();
angle += speed;
// 원의 중심 좌표 업데이트
// 코사인, 사인을 이용해 x 좌표, y 좌표를 반지름에 맞춰 비율 조정
// 뒤에 곱하는 값이 커질수록 경로의 둘레가 커짐
startX2 = 50 + Math.cos(angle) * 40;
startY2 = 50 + Math.sin(angle) * 40;
// 다음 프레임 요청
requestAnimationFrame(circleAnimation);
}
circleAnimation();
4. 반복되는 애니메이션
직선 이동 애니메이션 재활용
애니메이션 그림이 화면을 벗어나게 되면 좌표 초기화
// infiniteAnimation.js
const canvas3 = document.getElementById("my-canvas3");
const ctx3 = canvas3.getContext("2d");
let startX3 = 10;
let startY3 = 10;
let dx3 = 10;
function infiniteAnimation() {
ctx3.clearRect(0, 0, canvas3.width, canvas3.height);
ctx3.fillStyle = "black";
// 화면을 벗어나게 되면 좌표 초기화
if (startX3 === canvas3.width) {
startX3 = 10;
} else {
startX3 += dx3;
}
ctx3.fillRect(startX3, startY3, 100, 100);
requestAnimationFrame(infiniteAnimation);
}
infiniteAnimation();
5. 왕복 이동 애니메이션
직선 이동 애니메이션 재활용
이동 방향 결정하는 변수 추가
특정 지점에 도달할 때마다 이동 방향 변경
이동 방향에 따라 좌표 증감
// leftRightAnimation.js
const canvas4 = document.getElementById("my-canvas4");
const ctx4 = canvas4.getContext("2d");
let startX4 = 10;
let startY4 = 10;
let dx4 = 10;
// 이동 방향 결정하는 변수 추가
let isMoveRight = true;
function leftRightAnimation() {
ctx4.clearRect(0, 0, canvas4.width, canvas4.height);
ctx4.fillStyle = "black";
// 특정 지점에 도달할 때마다 이동 방향 변경
if (startX4 + 100 === canvas4.width) {
isMoveRight = false;
} else if (startX4 === 10) {
isMoveRight = true;
}
// 이동 방향에 따라 좌표 증감
if (isMoveRight) {
startX4 += dx4;
} else {
startX4 -= dx4;
}
ctx4.fillRect(startX4, startY4, 100, 100);
requestAnimationFrame(leftRightAnimation);
}
leftRightAnimation();
기존 redux에서는 상태 관리 로직에서 불변성 관리가 되지 않아, 기존 값을 따로 복사해야 했음
ex) { ...state, name: 홍길동 }
toolkit을 사용할 시 불변성 유지가 되기 때문에, 변경하고 싶은 값만 변경하면 됨
ex) state.name = "홍길동"
개발자 도구 적용 간편
toolkit은 redux-devtools를 따로 설치하지 않고, 설정만 해도 적용
3. 구현 코드
slice 생성
slice는 reducer와 action을 동시에 관리함
초기 상태 값 설정
slice 내부에 action 생성 (state의 변경 시키고 싶은 값만 수정)
slice를 통해 생성된 action 내보내기
// store/slice/infoSlice.js
import { createSlice } from "@reduxjs/toolkit";
// 초기 상태 값 설정
const initialState = { name: "홍길동", age: 20 };
// slice를 통해 action 생성
const infoSlice = createSlice({
name: "counter",
initialState,
reducers: {
// 액션 생성
// state의 변경 시키고 싶은 값만 수정
changeName: (state, action) => {
state.name = action.payload;
},
changeAge: (state, action) => {
state.age = action.payload;
},
},
});
// reducer에 생성한 action export
export const { changeName, changeAge } = infoSlice.actions;
export default infoSlice.reducer;
store 생성
configure 함수 사용
reducer 등록
개발자 도구 사용 설정
// store/store.js
import { configureStore } from "@reduxjs/toolkit";
import infoReducer from "./slice/infoSlice";
// configureStore 함수 사용
// devTools 따로 설치하지 않고 설정만 해도 사용 가능
const store = configureStore({
// 생성한 reducer 등록
reducer: {
info: infoReducer,
},
// 개발자 도구 사용 설정 ON
devTools: true,
});
export default store;
가장 상위 컴포넌트에 store 등록
// main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { Provider } from "react-redux";
import store from "./store/store";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
{/* 작성한 store 적용 */}
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);