참고서 : 하스켈로 배우는 함수형 프로그래밍(오카와 노리유키 저, 정인식 번)

함수형 프로그래밍에서 함수

함수형 프로그래밍에서 “함수”란 주어진 입력 값만으로 단지 하나의 출력되는 값을 결정하는 규칙이라는 수학적 의미의 함수

1
2
3
4
// C++
void say(const std::string& something) {
std::cout << something << std::endl;
}
  • 이 함수는 외부로 문자 출력을 하기 때문에 외부의 상황에 의존하므로 항상 같은 결과가 나올 수 없고 따라서 함수형 프로그래밍의 함수가 아니다
1
2
3
4
# Ruby
def current # 외부에서 현재 시각을 가져옴 (때에 따라 결과가 다름)
Time.now
end
1
2
3
4
5
std::string show(const int n) {
std::ostringstream oss;
oss << n;
return oss.str();
}
  • show 함수는 주어진 숫자에 대해서만 그 값의 문자열 표현을 얻는다
  • 동일 숫자를 부여하면 언제 어디서나 동일한 문자열 표현을 얻을 수 있으므로 이 함수가 수학적인 의미에서 함수이다
1
2
3
def total(numbers)
numbers.inject(:+)
end
  • total 함수도 같은 숫자의 배열을 부여하면 동일한 합계를 얻을 수 있기 때문에 수학적인 의미의 함수이다
  • 프로그래밍에서 상태를 참조하거나 상태에 변화를 줌으로써 다음 번 이후의 결과에까지 영향을 미치는 효과를 부작용이라 한다
  • 따라서 이 부작용이 있으면 함수형 프로그래밍의 함수라 부를 수 없다

프로그래밍의 패러다임

  • 프로그래밍에서 프로그래밍 패러다임이란 프로그램이나 그것에 대상이 되는 문제 자체를 어떤 식으로 볼 것인가라는 것이다
  • 명령형 프로그래밍의 경우 프로그램은 컴퓨터가 수행해야 할 명령의 나열
  • 객체지향 프로그래밍의 경우 프로그램은 객체와 그것들의 메세징
  • 함수형 프로그래밍의 패러다임은 프로그램은 함수이며 커다란 프로그램은 작은 프로그램의 조합으로 구성된다
  • 이 때 조합의 용이성/부품으로서의 독립성을 모듈화라고 부른다
  • 함수에 입력 가능한 값 전체로 이루어진 집합을 정의역, 출력 가능한 값 전체로 이루어진 집합을 치역이라고 한다
  • 함수형 프로그래밍에서 함수에 해당하지 않는 것은 일반적으로 모듈화에 좋지 않다 (수학적인 의미에서 함수만 사용하기 = 절차를 취급하지 않기의 제약이 있음)

함수형 언어라?

■ 함수형 언어이기 위한 조건

  • 리터럴이 있다 : 람다식에 의한 함수의 리터럴
  • 실행시간에 생성할 수 있다 : 함수 합성에 의해서 혹은 부분 적용, 고차 함수 등에 의해 함수를 실행시간에 생성할 수 있다
  • 변수에 넣어서 취급할 수 있다
  • 절차나 함수에 인수로서 제공할 수 있다 : 정의역이 특정 함수의 집합이 되는 함수를 만들 수 있다
  • 절차나 함수의 결과로서 반환할 수 있다 : 치역이 특정 함수의 집합이 되는 함수를 만들 수 있다

Comment and share

프론트엔드 엔지니어로서 UI/UX를 향상시키는 방법 (2)

  • 로딩화면은 앱이 서버와의 통신이 늦어지거나 데이터를 불러오는 데 텀이 생길 때 활용됨

  • 로딩 스피너: 로딩 화면에서 가장 익숙한 로딩 애니메이션으로 4초 이내의 비교적 짧고 부분적인 로딩이 필요할 때 사용됨

  • 로딩 애니메이션: 흔히 브랜드 마크를 사용하는데 이 때 마크가 무한 반복되면서 유저에게 강한 인상을 줄 수 있으므로 서비스의 핵심 액션을 사용할 때 활용하면 좋음

  • 스켈레톤 스크린: 스켈레톤 UI는 실제 데이터가 렌더링 되기 전에 컨텐츠가 자리잡을 레이아웃을 구조적으로 보여줌으로서 사용자가 로딩 속도를 비교적 짧게 느낄 수 있음

  • 프로그레스 바: 얼마나 진행되고 있는지를 가장 직관적으로 보여줄 수 있는 UI로 진행률을 보여줄 때 사용하는 것이 가장 일반적임 (예: 회원가입, 설문)

Comment and share

HTML5 Canvas 기초 (6)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
<!DOCTYPE html>
<html>
<head>
<title>Canvas</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
canvas {
background: #eee;
}
</style>
</head>

<body>
<h1>Interaction</h1>
<canvas class="canvas" width="600" height="400"></canvas>

<script src="utils.js"></script>
<script src="Box.js"></script>
<script src="Panel.js"></script>
<script>
const canvas = document.querySelector(".canvas");
const context = canvas.getContext("2d");
const boxes = [];
const mousePos = { x: 0, y: 0 };
let panel;
let selectedBox; // 클릭된 box
let oX;
let oY;
let step; // 애플리케이션의 상태(단계)를 저장 1 ~ 4
let rafId;

context.font = "bold 30px sans-serif";

function render() {
context.clearRect(0, 0, canvas.width, canvas.height);

let box;
for (let i = 0; i < boxes.length; i++) {
box = boxes[i];
// box.x += box.speed;
// if (box.x > canvas.width) {
// box.x = -box.width;
// }
box.draw();
}

switch (step) {
case 1:
for (let i = 0; i < boxes.length; i++) {
box = boxes[i];
box.x += box.speed;
if (box.x > canvas.width) {
box.x = -box.width;
}
}
break;

case 2:
// panel.scale += 0.05;
// 현재크기 = 현재크기 + (목표크기 - 현재크기)*0.1
// 감속
panel.scale = panel.scale + (1 - panel.scale) * 0.1;
// 가속
// panel.scale = panel.scale + panel.scale*0.02;
// 각도 = 스케일(0~1) * 720;
panel.angle = panel.scale * 720;
panel.draw();
if (panel.scale >= 0.999) {
panel.scale = 1;
panel.angle = 720;
step = 3;
}
break;

case 3:
panel.draw();
break;
}

// console.log('render!');

rafId = requestAnimationFrame(render);
if (step === 3) {
panel.showContent();
cancelAnimationFrame(rafId);
}
}

let tempX, tempY, tempSpeed;

function init() {
step = 1;
oX = canvas.width / 2;
oY = canvas.height / 2;
for (let i = 0; i < 10; i++) {
tempX = Math.random() * canvas.width * 0.8;
tempY = Math.random() * canvas.height * 0.8;
tempSpeed = Math.random() * 4 + 1;
boxes.push(new Box(i, tempX, tempY, tempSpeed));
}

panel = new Panel();

render();
}

canvas.addEventListener("click", (e) => {
mousePos.x = e.layerX;
mousePos.y = e.layerY;

let box;
for (let i = 0; i < boxes.length; i++) {
box = boxes[i];
if (
mousePos.x > box.x &&
mousePos.x < box.x + box.width &&
mousePos.y > box.y &&
mousePos.y < box.y + box.height
) {
selectedBox = box;
}
}

if (step === 1 && selectedBox) {
// console.log(selectedBox.index);
step = 2;
} else if (step === 3) {
step = 1;
panel.scale = 0.01;
selectedBox = null;
render();
}
});

init();
</script>
</body>
</html>
  • Box.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Box {
constructor(index, x, y, speed) {
this.index = index;
this.x = x;
this.y = y;
this.speed = speed;
this.width = 100;
this.height = 100;
this.draw();
}

draw() {
context.fillStyle = "rgba(0,0,0,0.5)";
context.fillRect(this.x, this.y, 100, 100);
context.fillStyle = "#fff";
context.fillText(this.index, this.x + 30, this.y + 30);
}
}
  • Panel.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Panel {
constructor() {
this.scale = 0.01;
this.angle = 0;
}

draw() {
context.fillStyle = "rgba(255,0,0,0.8)";
// 변환 초기화;
context.resetTransform();
// context.setTransform(1,0,0,1,0,0);
context.translate(oX, oY);
context.scale(this.scale, this.scale);
context.rotate(canUtil.toRadian(this.angle));
context.translate(-oX, -oY);
context.fillRect(oX - 150, oY - 150, 300, 300);
context.resetTransform();
}

showContent() {
console.log("showContent 실행");
context.fillStyle = "#fff";
context.fillText(selectedBox.index, oX, oY);
}
}
  • utils.js
1
2
3
4
5
const canUtil = {
toRadian: function (degree) {
return (degree * Math.PI) / 180;
},
};

Comment and share

HTML5 Canvas 기초 (5)

캔버스라이브강좌

  • 인터렉션
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html>
<head>
<title>Canvas</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
canvas {
background: #eee;
}
</style>
</head>

<body>
<h1>Interaction</h1>
<canvas class="canvas" width="600" height="400"></canvas>

<script>
const canvas = document.querySelector(".canvas");
const context = canvas.getContext("2d");

context.fillRect(200, 200, 100, 100);

function clickHandler(e) {
// console.log(e.clientX, e.clientY);
// console.log(e.layerX, e.layerY);
const x = e.layerX;
const y = e.layerY;

if (x > 200 && x < 200 + 100 && y > 200 && y < 200 + 100) {
console.log("x ok");
}
}

canvas.addEventListener("click", clickHandler);
</script>
</body>
</html>

Comment and share

HTML5 Canvas 기초 (4)

캔버스라이브강좌

  • 캔버스내에서 Translate 사용하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
const canvas = document.querySelector(".canvas");
const ctx = canvas.getContext("2d");
let scaleValue = 1;

function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.translate(250, 250);
ctx.scale(scaleValue, scaleValue);
scaleValue += 0.01;
ctx.strokeRect(-50, -50, 100, 100);
ctx.restore();

requestAnimationFrame(draw);
}

draw();
...

ctx.setTransform(a, b, c, d, e, f)

  • a ~ f의 매개변수는 각각 수평 확장, 수직 왜곡, 수평으로 기울이기, 수직 확장, 수평 이동, 수직이동을 뜻한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const canvas = document.querySelector(".canvas");
const ctx = canvas.getContext("2d");

let scaleValue = 1;
let rotationValue = 0;

function toRadian(degree) {
return (degree * Math.PI) / 180;
}

function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height); // 캔버스 초기화
ctx.save();
// 디폴트 값을 저장 (색상이나 폰트 스타일 등)
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.translate(250, 250); // 중앙으로 위치시킴
ctx.scale(scaleValue, scaleValue);
// scale함수는 도형의 크기를 변화시킴. 여기서는 매 draw 실행마다 0.01씩 가로세로가 커짐.
ctx.rotate(toRadian(rotationValue));
// rotateValue의 radian 값 만큼 회전, 마이너스가 된다면 시계 반대방향으로 회전할 것
ctx.strokeRect(-50, -50, 100, 100);
// 해당 도형 내 기준점(0, 0 이라면 좌측상단이 됨)에서 100, 100 크기의 사각형을 그림
ctx.restore();
// 저장된 디폴트값을 불러옴

scaleValue += 0.01;
rotationValue -= 2;
requestAnimationFrame(draw);
}

draw();


  • clearRect를 실행하지 않았을 때와 실행했을 때의 차이

해당 함수의 MDN 문서 링크

ctx.save()

ctx.restore()

ctx.scale()

ctx.rotate()

ctx.stroke()

ctx.fillRect()

ctx.transform()

ctx.translate()

ctx.strokeRect()

ctx.clearRect()

ctx.setTransform()

Comment and share

HTML5 Canvas 기초 (3)

캔버스라이브강좌

  • 캔버스 영역에 비디오 입히기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html>
<head>
<title>Canvas</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>

<body>
<h1>Video</h1>
<video class="video" src="./horse.mp4" autoplay muted loop></video>
<canvas class="canvas" width="500" height="300"></canvas>

<script>
const canvas = document.querySelector(".canvas");
const ctx = canvas.getContext("2d");
ctx.font = "bold 50px serif";
ctx.fillStyle = "white";

const subtitle = [
{ time: 1, message: "Melody Lane", x: 270, y: 70 },
{ time: 3, message: "Son of Orfevre", x: 270, y: 170 },
{ time: 5, message: "8th May", x: 270, y: 920 },
];

let canPlayState = false;

ctx.textAlign = "center";
ctx.fillText("loading video...", 250, 150);

const videoElem = document.querySelector(".video");
videoElem.addEventListener("canplaythrough", render); // 이미지는 load, 비디오는 canplaythrough 이벤트를 사용

function render() {
ctx.drawImage(videoElem, 0, 0, 540, 960);

for (let i = 0; i < subtitle.length; i++) {
if (videoElem.currentTime > subtitle[i].time) {
// currentTime 변수는 재생 시간을 나타내고 이는 재생시간에 맞춰 자동으로 증가함
ctx.fillText(subtitle[i].message, subtitle[i].x, subtitle[i].y);
}
}

requestAnimationFrame(render); // 영상이 재렌더링 될 때마다 자막을 시간에 맞춰 전부 입힘
}
</script>
</body>
</html>
  • 비디오의 픽셀 조작
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const canvas = document.querySelector(".canvas");
const ctx = canvas.getContext("2d");

const videoElem = document.querySelector(".video");
videoElem.addEventListener("canplaythrough", render);

const btnsElem = document.querySelector(".btns");

let imageData;
const particles = [];
let particle;
let colorValue;
let leng;

function render() {
ctx.drawImage(videoElem, 0, 0, 600, 400);
imageData = ctx.getImageData(0, 0, 600, 400);
leng = imageData.data.length / 4;

for (let i = 0; i < leng; i++) {
switch (colorValue) {
case "red":
imageData.data[i * 4 + 0] = 255;
break;
case "green":
imageData.data[i * 4 + 1] = 255;
break;
case "blue":
imageData.data[i * 4 + 2] = 255;
break;
}
}

ctx.putImageData(imageData, 0, 0);
requestAnimationFrame(render);
}

btnsElem.addEventListener("click", function (e) {
colorValue = e.target.getAttribute("data-color");
});

Comment and share

HTML5 Canvas 기초 (2)

캔버스라이브강좌

1
2
3
4
5
6
7
const ctx = document.querySelector(".canvas").getContext("2d");

const imgElem = new Image();
imgElem.src = "./dog.jpg";
imgElem.addEventListener("load", () => {
ctx.drawImage(imgElem, 50, 50, 200, 200);
});

  • drawImage함수는 세 가지 패턴으로 나뉘고 아래와 같다
1
2
3
4
5
6
7
void ctx.drawImage(image, dx, dy);
// 이미지 객체를 캔버스 내 x, y 좌표에 그림
void ctx.drawImage(image, dx, dy, dWidth, dHeight);
// 이미지 객체를 캔버스 내 x, y 좌표에 dWidth * dHeight로 그림
void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
// 이미지 객체를 (sx, sy) 좌표로부터 sWidth * sHeight만큼 크롭하여
// 캔버스 내 (x, y)에 dWidth * dHeight로 그림
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const canvas = document.querySelector(".canvas");
const ctx = canvas.getContext("2d");
const control = document.querySelector(".control");
let drawingMode = false;
let colorVal = "black";

function moveHandler(e) {
if (!drawingMode) return;

ctx.beginPath();
ctx.arc(e.layerX, e.layerY, 10, 0, Math.PI * 2, false);
ctx.fill();
}

function downHandler(e) {
drawingMode = true;
}

function upHandler(e) {
drawingMode = false;
}

function setColor(e) {
colorVal = e.target.getAttribute("data-color");
ctx.fillStyle = colorVal;
}

canvas.addEventListener("mousedown", downHandler);
canvas.addEventListener("mousemove", moveHandler);
canvas.addEventListener("mouseup", upHandler);
control.addEventListener("click", setColor);

  • 색상 버튼을 이용해 선 색상을 바꾸기
1
2
3
4
5
6
7
8
...
function createIamge() {
const url = canvas.toDataURL("image/png"); // 이미지 타입을 정의하여 매개변수로
const imgElem = new Image(); // 이미지 객체를 새로 생성하고
imgElem.src = url; // 해당 url을 할당
resultImage.appendChild(imgElem); // 자식으로 추가
}
...

Comment and share

HTML5 Canvas 기초 (1)

캔버스라이브강좌

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html>
<head>
<title>Canvas</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body {
margin: 0;
}
.canvas {
width: 500px;
height: 300px;
background: #eee;
}
</style>
</head>

<body>
<h1>캔버스 사이즈 설정</h1>
<canvas class="canvas" width="500" height="300"></canvas>
<canvas class="canvas canvas2" width="1000" height="600"></canvas>

<script>
const canvas = document.querySelector(".canvas");
const canvas2 = document.querySelector(".canvas2");
const context = canvas.getContext("2d");
const context2 = canvas2.getContext("2d");

context.arc(100, 100, 50, 0, Math.PI * 2, false);
context2.arc(100, 100, 50, 0, Math.PI * 2, false);
context.fill();
context2.fill();
</script>
</body>
</html>

  • 캔버스를 지정하고 queryselector로 캔버스 객체를 지정, 특정 함수를 이용함으로써 해당 객체 내에서 도형을 그릴 수 있다
  • 자세한 내용은 MDN 문서를 참고
  • arc(호) 함수는 arc(x, y, radius, startAngle, endAngle, anticlockwise)와 같은 형식으로 사용하며 (x, y)가 원점이고 반지름이 r인 anticlockwise 방향으로 향하는 (기본값은 시계방향 회전) 호를 그리게 된다. 이 원은 startAngle 에서 시작하여 endAngle 에서 끝남
1
2
3
4
5
6
7
8
const canvas = document.querySelector(".canvas");
const context = canvas.getContext("2d");

context.fillRect(50, 50, 100, 100);
context.fillStyle = "red";
context.fillRect(0, 0, 100, 100);
context.clearRect(80, 80, 50, 50);
context.strokeRect(150, 150, 100, 100);

  • fillRect(사각형)함수는 fillRect(x, y, width, height)와 같은 형식으로 사용함 x, y 위치에 width, height에 맞는 사각형을 그린다
  • clearRect는 특정한 부분을 지우는 함수로 받는 인자는 fillRect와 같다. 마찬가지로 strokeRect는 사각형의 외선만 그리는 함수.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function draw() {
var ctx = document.getElementById("canvas").getContext("2d");
for (var i = 0; i < 6; i++) {
for (var j = 0; j < 6; j++) {
ctx.fillStyle =
"rgb(" +
Math.floor(255 - 42.5 * i) +
", " +
Math.floor(255 - 42.5 * j) +
", 0)";
ctx.fillRect(j * 25, i * 25, 25, 25);
}
}
}

  • fillStyle은 한번 설정되면 그 색으로만 도형을 그리기 때문에 각각 다른 색깔로 그리고 싶다면 매번 새로 설정해주어야한다.
  • 위의 draw 함수를 사용한다면 색상 팔레트와 유사한 결과를 얻을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const ctx = document.querySelector(".canvas").getContext("2d");

ctx.beginPath();

ctx.moveTo(100, 100);
ctx.lineTo(300, 200);
ctx.stroke();
ctx.lineTo(100, 200);
ctx.stroke();
ctx.lineTo(100, 100);
ctx.stroke();
ctx.fillStyle = "red";
ctx.fill();

ctx.closePath();
  • 선을 그리고자 할 때엔 (1) path를 시작, (2) 시작점으로 옮기고, (3) 이동경로를 설정하고, (4) 선을 그려준다. 만약 이 때 이 선들이 하나의 면을 만든다면 색을 주고 칠할 수도 있다.
  • beginPath()와 closePath()는 시작과 끝을 알리는 함수이며 moveTo, lineTo는 각각 x좌표와 y좌표를 매개변수로 받는 함수이다. 이 두 함수로 경로를 설정한다면 이후에는 stroke()나 fill()과 같은 함수로 선을 그리거나 도형을 채울 수 있다.

  • closePath()로 한 번 끊어주지 않으면 위 그림처럼 도형들이 이어져 그려지게 된다

Comment and share

올 해에는 고쳐야 할 10가지 타입스크립트 버릇

참고

타입스크립트와 자바스크립트는 작년에도 꾸준히 발전해왔고, 최근 10년 동안 몇몇 습관들이 정착되었다. 몇몇은 전혀 쓸모가 없었다. 여기 우리가 고쳐야 할 10가지 습관들을 적어본다.

  1. strict 모드를 사용하지 않는 것
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// in tsconfig.json
{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs"
}
}

// ↓↓↓ it should look like ↓↓↓
{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs",
"strict": true
}
}
  1. 기본값들을 ||로 정의하지 않기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createBlogPost(text: string, author: string, date?: Date) {
return {
text: text,
author: author,
date: date || new Date(),
};
}

// ↓↓↓ it should look like ↓↓↓
function createBlogPost (text: string, author: string, date: Date = new Date())
return {
text: text,
author: author,
date: date
}
}
  1. any를 사용하지 않기
1
2
3
4
5
6
7
8
9
10
11
12
async function loadProducts(): Promise<Product[]> {
const response = await fetch("https://api.mysite.com/products");
const products: any = await response.json();
return products;
}

// ↓↓↓ it should look like ↓↓↓
async function loadProducts(): Promise<Product[]> {
const response = await fetch("https://api.mysite.com/products");
const products: unknown = await response.json(); // any 대신에 unknown을 사용할 것
return products as Product[]; // 또한, as를 사용하여 리턴값을 반드시 정의하기
}
  1. val as SomeType 대신 val is SomeType을 사용하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 하지만 unknown을 쓰기보다는 타입가드를 적용하자
async function loadProducts(): Promise<Product[]> {
const response = await fetch("https://api.mysite.com/products");
const products: unknown = await response.json();
return products as Product[];
}

// ↓↓↓ it should look like ↓↓↓
function isArrayOfProducts(obj: unknown): obj is Product[] {
return Array.isArray(obj) && obj.every(isProduct);
}

function isProduct(obj: unknown): obj is Product {
return obj != null && typeof (obj as Product).id === "string";
}

async function loadProducts(): Promise<Product[]> {
const response = await fetch("https://api.mysite.com/products");
const products: unknown = await response.json();
if (!isArrayOfProducts(products)) {
throw new TypeError("Received malformed products API response");
}
return products;
}
  • 일반 타입가드, 사용자 정의 타입가드에 관해서는 이 페이지를 참고
  1. as any를 테스트에서 사용하지 않기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
interface User {
id: string
firstName: string
lastName: string
email: string
}

test('createEmailText returns text that greats the user by first name', () => {
const user: User = {
firstName: 'John'
} as any

expect(createEmailText(user)).toContain(user.firstName)
}

// ↓↓↓ it should look like ↓↓↓
interface User {
id: string
firstName: string
lastName: string
email: string
}

class MockUser implements User {
id = 'id'
firstName = 'John'
lastName = 'Doe'
email = 'john@doe.com'
}

test('createEmailText returns text that greats the user by first name', () => {
const user = new MockUser()

expect(createEmailText(user)).toContain(user.firstName)
}
  1. 선택적 프로퍼티
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface Product {
id: string;
type: "digital" | "physical";
weightInKg?: number;
sizeInMb?: number;
}

// ↓↓↓ it should look like ↓↓↓
interface Product {
id: string;
type: "digital" | "physical";
}

// 선택적 프로퍼티가 적용되는 타입에 따라 객체가 각각 다른 구조를 갖는다면 상속을 활용할 것
interface DigitalProduct extends Product {
type: "digital";
sizeInMb: number;
}

interface PhysicalProduct extends Product {
type: "physical";
weightInKg: number;
}
  1. 한 글자의 제네릭을 사용하지 않기
1
2
3
4
5
6
7
8
9
// 제네릭 또한 변수명처럼 사용해야한다. 말인즉슨 알아보기 쉬워야 한다
function head<T>(arr: T[]): T | undefined {
return arr[0];
}

// ↓↓↓ it should look like ↓↓↓
function head<Element>(arr: Element[]): Element | undefined {
return arr[0];
}
  1. 자바스크립트에서 제공하는 non-boolean형 타입추론 체크 대신 자발적인 타입 체크를 사용할 것
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createNewMessagesResponse(countOfNewMessages?: number) {
if (countOfNewMessages) {
return `You have ${countOfNewMessages} new messages`;
}
return "Error: Could not retrieve number of new messages";
}

// ↓↓↓ it should look like ↓↓↓
function createNewMessagesResponse(countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`;
}
return "Error: Could not retrieve number of new messages";
}
  1. Bang Bang 연산자(!!)를 사용하지 않기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createNewMessagesResponse(countOfNewMessages?: number) {
if (!!countOfNewMessages) {
return `You have ${countOfNewMessages} new messages`;
}
return "Error: Could not retrieve number of new messages";
}

// ↓↓↓ it should look like ↓↓↓
function createNewMessagesResponse(countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`;
}
return "Error: Could not retrieve number of new messages";
}
  1. != null은 이제 그만
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createNewMessagesResponse(countOfNewMessages?: number) {
if (countOfNewMessages != null) {
return `You have ${countOfNewMessages} new messages`;
}
return "Error: Could not retrieve number of new messages";
}

// ↓↓↓ it should look like ↓↓↓
function createNewMessagesResponse(countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`;
}
return "Error: Could not retrieve number of new messages";
}

Comment and share

토스 | SLASH 21

예비 프론트 엔드 개발자로서 보면 좋을 것 같은 발표들을 정리

SLASH 페이지

1. Micro-frontend React, 점진적으로 도입하기

열어보기
  • 이 발표에서 소개하는 것
1
2
3
4
5

(1) Django MVC 프로젝트에 점진적으로 React를 도입한 방법
(2) 마이크로 프론트엔드 아키텍처를 도입한 이유와 후기
(3) 프론트엔드 빌드 시간을 효과적으로 단축한 비법

(1) Django MVC 프로젝트에 점진적으로 React를 도입한 방법

토스의 서버사이드에서 HTML 템플릿을 렌더링하는 모놀리식 Django MVC 프로젝트에서 모던 리액트 코드로 어떻게 이전했는지?

  • 시작은 create-react-app을 사용해 빠른 프로젝트를 빌드
  • 복잡한 webpack, Babel, ESLint 등의 설정을 건너뛰게 해 줄 것이라 믿음
  • 완전한 SPA가 아니고 Django와 함께 사용할 때엔 설정의 충돌이 많이 있었음 → 처음부터 설정을 해보자

create-react-app

  • webpack-bundle-tracker(wbt), django-webpack-loader(dwl)를 사용
  • wbt는 빌드 결과물의 chunk 정보를 JSON 파일로 추출해주고 dwl은 이 JSON파일을 토대로 스크립트 태그나 링크 태그를 생성해줌
  • 많은 부분이 리액트로 바뀌고 나니 패키지 관리가 어려워지고 빌드 시간이 길어짐
  • 그래서 Micro-frontend 아키텍처를 사용하기로 함
  • 기존의 거대한 소스 코드를 독립적인 패키지(인프라, 라이브러리, 서비스 패키지)로 각각 분리함
  • 이를 위해 Yarn 2와 Workspace Plugin을 사용 중
  • 이로서 소스 코드부터 빌드 설정까지 완벽하게 격리되고, 의존성 지옥을 탈출할 수 있으며, 빌드 속도도 최적화됨
  • 빌드 시간을 줄이는 가장 좋은 방법은 빌드를 하지 않는 것
  • 소드코드가 바뀐 패키지만 빌드하고 나머지는 기존 빌드 결과물을 재사용
  • Git을 통해 패키지의 변화를 캐치(Git의 해시를 사용)하고 변화가 감지된 패키지만 빌드

2. 프론트엔드 웹 서비스에서 우아하게 비동기 처리하기

열어보기

웹 서비스에서 가장 다루기 어려운 부분은 무엇인가?

  • jQuery의 명령형 프로그래밍에서 React/Vue와 같은 프레임워크의 사용으로 선언형 프로그래밍으로 전환
  • 비동기 프로그래밍은 끊기지 않는 60 프레임의 좋은 사용자 경험을 위해서는 필수
  • 예시 코드 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getBazFromX(x) {
if (x === undefined) {
return undefined;
}

if (x.foo === undefined) {
return undefined;
}

if (x.foo.bar === undefined) {
return undefined;
}

return x.foo.bar.baz;
}
  • 문제점
    → 하는 일은 단순하지만 코드가 너무 복잡함
    → 각 프로퍼티에 접근하는 핵심 기능이 코드로 잘 드러나지 않음

  • 해결책
    → OptionalChaining 문법을 활용

1
2
3
4
5
function getBazFromX(x) {
// if문이 사라져 코드가 간결해짐
// 함수의 역할이 ?를 통해 잘 드러남
return x?.foo?.bar?.baz;
}
  • 예시 코드 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

function fetchAccounts(callback) {
fetchUserEntity(err, user) => {
if (err != null) {
callback(err, null);
return;
}

fetchUserAccounts(user.me, (err, accounts) => {
if (err != null) {
callback(err, null);
return;
}

callback(null, accounts);
})
}
}

  • 문제점
    → 코드가 너무 복잡함
    → 성공 케이스와 실패 케이스가 섞여서 처리됨
    → 매번 에러 유무를 확인해야 함

  • 해결책
    → async-await 문법을 활용하기
    → 비동기처리를 이용해 성공 케이스만을 다루고 실패 케이스는 catch문을 사용해서 처리함으로써 실패 케이스를 외부에 위임할 수 있다

1
2
3
4
5
async function fetchAccounts() {
const user = await fetchUserEntity();
const accounts = await fetchUserAccounts(user.no);
return accounts;
}
  • 예시를 통해 알 수 있는 좋은 코드와 나쁜 코드의 특징

    • 좋은 코드 : 성공, 실패 케이스를 분리해 처리 가능, 비즈니스 로직을 한눈에 파악할 수 있다
    • 나쁜 코드 : 성공, 실패 케이스가 서로 섞임, 비즈니스 로직의 파악이 어려움
  • 비동기를 처리하는 부분을 정의하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// SWR이나 React Query를 사용하여 비동기를 정의
const { data, error } = useAsyncValue(() => {
return fetchSomething();
});

// 컴포넌트에서 로딩과 에러 처리를 동시에 수행함
function Profile() {
const foo = useAsyncValue(() => {
return fetchFoo();
});

if (foo.error) return <div>로딩에 실패했습니다.</div>;
if (!foo.data) return <div>로딩 중입니다...</div>;
return <div>{foo.data.name}님 안녕하세요!</div>;
}

→ 이는 나쁜 코드

  • 비동기 코드가 여러개가 섞이게 되면 비동기 지옥이 됨

  • 2개의 비동기 리소스를 가져올 때 상태가 각각 ‘로딩, 에러, 완료’로 나뉜다면 이 때 상태는 3의 제곱으로 9가지 상태를 가질 수 있음

  • 리액트에서는 더더욱 이 비동기 처리가 어려운데 React팀에서는 React Suspense for Data Fetching도구를 제공함(아직은 실험 버전에서만 사용 할 수 있음)

  • 어떻게 에러 상태와 로딩 상태를 분리하는가?

1
2
3
4
5
6
7
8
9
10
11
12

<!--
1. 컴포넌트를 쓰는 쪽에서 로딩처리와 에러 처리를 한다
2. 로딩 상태는 가장 가까운 Suspense의 Fallback으로 그려진다
3. 에러 상태는 가장 가까운 ErrorBoundary가 componentDidCatch()로 처리
-->
<ErrorBoundary fallback={<MyErrorPage />}>
<Suspense fallback={<Loader />}>
<FooBar />
</Suspense>
</ErrorBoundary>

  • Recoil에서는 Async Selector를, SWR, React Query에서는 { suspense: true}를 정의해주면 된다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getUserName(id) {
var user = JSON.parse(fetchTextSync("/users" + id));
return user.name;
}

function getGreeting(name) {
if (name === "Seb") {
return "Hey";
}
return fetchTextSync("/greeting");
}
function getMessage() {
let name = getUserName(123);
return getGreeting(name) + ", " + name + "!";
}
runPureTask(getMassage).then((message) => console.log(message));
  • Recoil의 비동기 셀렉터
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export const templateSetSelector = selectorFamily({
key: "@messages/template-set",
get: (no: number) => async () => {
return fetchTemplateSet(no);
},
});

export const historiesOfTemplateSetSelector = selectorFamily({
key: "@pages/messenger/template-set/histories",
get: (templateSetNo: number) => async ({ get }) => {
return fetchHistoriesOfTemplateSet(templateSetNo);
},
});

function TemplateSetDetails({ templateSetNo }: Props) {
const templateSet = useRecoilValue(templateSetSelector(templateSetNo));
/* 이 아래에서는 templateSet이 보장됨(타입가드가 필요 없음) */
}
  • React hooks와 suspense의 유사도

  • Hooks에서는 useState, useMemo, useCallback, useEffect와 같은 선언적 코드를 통해 웹 서비스의 코드 복잡도를 낮춰줌

  • 실제 상태 관리, 메모이제이션과 같은 작업은 React 프레임워크가 대신 수행함

  • suspense를 사용할 때에도 컴포넌트에서 비동기적인 리소스를 선언하고 그 값을 읽어온다고 선언하면 컴포넌트를 감싸는 부모 컴포넌트가 대신 수행함

  • try-catch문을 통해 실패할 수 있는 함수는 throw를 통해 부모 함수로 던지고 이 에러 처리를 부모 함수가 수행함

  • 이런 책임 분리 방식을 대수적 효과라고 함

  • 하지 못한 이야기들…
    React Concurrent Mode, useTransition, useDeferredValue
    → React에서 부분적으로 렌더 트리를 완성함으로써 더 나은 사용자 경험 향상 가능

참고자료1 : 데이터를 가져오기 위한 Suspense (실험 단계)
참고자료2 : Algebraic Effects for the Rest of Us


3. JavaScript Bundle Diet

열어보기
  • 사용자는 느린 로딩을 참지 못한다 (5초 초과시 이탈율 38%)
  • API 호출이 너무 많거나 이미지 처리 등 다양한 원인으로 느려질 수 있음
  • 번들 사이즈는 그 중 하나
  • 자바스크립트는 파일을 다운로드하고 파싱한 후에 컴파일을 하고 실행까지 하는 등 같은 용량이더라도 처리 비용이 크다
  • Webpack Analyse는 가장 다양한 정보를 주지만 사용하기 어려움
  • Webpack Visualizer는 깔끔한 시각화를 보여주지만 기능이 부족함
  • 그래서 Webpack Bundle Analyzer를 추천함
  • 번들 용량을 줄일 때 가장 먼저 해야 할 일 : 용량이 큰 라이브러리는 가벼운 라이브러리로, 용도가 겹치는 라이브러리는 하나로 통일
  • 여러 라이브러리가 다른 버전의 라이브러리를 참조하는 경우 Dependency confilict가 일어남
  • npm은 라이브러리를 트리 구조로 저장하기 때문에 node_modules가 과도하게 커지게 됨
  • npm dedupe 명령어와 yarn deduplicate 패키지, yarn 2에선 dedupe 명령어가 생김
  • webpack alias기능을 이용한다면 동일한 라이브러리의 중복을 피할 수 있음
  • lodash는 기능에 비해 용량이 클 수 있다
  • 따라서 가능한 네이티브 함수를 이용하거나 더 가벼운 함수로 구현하여 사용 중임 (참고자료)
  • polyfill도 고려할 것
  • Bundle Phobia를 통해 버전별 번드 용량, 해당 라이브러리의 디펜던시를 분석 할 수 있다
  • 더 가벼운 라이브러리를 만들 때엔 tree-shaking를 고려할 것
  • 하지만 tree-shaking은 side effect가 없을 때에만 가능
  • terser를 사용하면 terser가 /*#__PURE__**/를 발견하면 해당 코드에 side effect가 없다고 판단함 (Babel은 이런 pure annotation을 인식하지만 ts는 인식하지 못함)
  • 무거운 라이브러리의 영향을 줄이기

4. 실무에서 바로 쓰는 Frontend Clean Code

열어보기
  • 이 발표에서 소개하는 것
1
2
3
4
5
6

1. 실무에서 클린 코드의 의의
2. 안일한 코드 추가의 함정
3. 로직을 빠르게 찾을 수 있는 코드
4. 액션 아이템

(1) 클린코드가 의미 있는 이유란?

“그 코드는 안건드리시는 게 좋을거에요. 일단 제가 만질게요.^^;;”

→ 흐름 파악이 어렵고, 도메인 맥락 표현이 안 되어 동료에게 물어봐야 알 수 있는 코드
→ 이런 코드는 개발 시 병목되고 유지보수 시 오래 걸리고 심한 경우 기능 추가가 불가능한 상태가 됨

  • 실무에서 클린 코드의 의의는 유지보수 시간의 단축을 뜻한다.

(2) 안일한 코드 추가의 함정

  • 기존 코드에 기능을 추가하다보면 일어날 수 있는 함정들
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function QP() {
const [popupOpened, setPopupOpened] = useState(false);

async function handleQuestionSubmit() {
const 약관동의 = await 약관동의_받아오기();
if (!약관동의) {
await 약관동의_팝업열기();
}
await 질문전송(questionValue);
alert("질문이 등록되었습니다.");
}

async function handleMyExpertQuestionSubmit() {
await 연결전문가_질문전송(questionValue, 연결전문가.id);
alert(`${연결전문가.name}에게 질문이 등록되었어요.`);
}

return (
<main>
<form>
<textarea placeholder="어떤 내용이 궁금한가요?" />
<Button onclick={handleQuestionSubmit}>질문하기</Button>
</form>
{popupOpened && (
<연결전문가팝업 onSubmit={handleMyExpertQuestionSubmit} />
)}
</main>
);
}

이 코드는…

  • 하나의 목적인 코드가 여러 블럭으로 흩어져 있음
  • 하나의 함수가 여러가지 일을 하고 있음
  • 함수의 세부 구현 단계가 제각각임

그 때는 맞고 지금은 틀리다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

function QP() {
const 연결전문가 = useFetch(연결전문가_받아오기);

// 새 함수
async function handleNewExpertQuestionSubmit() {
await 질문전송(questionValue);
alert("질문이 등록되었어요.");
}

//
async function handleMyExpertQuestionSubmit() {
await 연결전문가_질문전송(questionValue, 연결전문가.id);
alert(`${연결전문가.name}에게 질문이 등록되었어요.`);
}

return (
<main>
<form>
<textarea placeholder="어떤 내용이 궁금한가요?" />
{연결전문가.connected ? (
<PopupTriggerButton
popup={{
<연결전문가팝업
onButtonSubmit={handleMyExpertQuestionSubmit}/>
}}
>
질문하기 </PopupTriggerButton>
) : (
<Button onClick={async () => {
await openPopupToNotAgreedUsers();
await handleMyExpertQuestionSubmit();
}}
>
질문하기
</Button>
)}
</form>
</main>
);
}

async function openPopupToNotAgreedUsers() {
const 약관동의 = await 약관동의_받아오기();
if(!약관동의) {
await 약관동의_팝업열기();
}
}

  • 클린코드란 원하는 로직을 빠르게 찾을 수 있는 코드

(3) 로직을 빠르게 찾을 수 있는 코드

  1. 응집도: 같은 목적의 코드는 뭉쳐두자
  • 무엇을? 당장 몰라도 되는 디테일
  • 코드 파악에 필수적인 핵심 정보를 뭉치면 오히려 답답해짐
  • 어떻게 응집시킬까?
    • 첫번째, 남겨야 할 핵심 데이터와 숨겨야 할 세부 데이터를 나누기
    • 두번째, 핵심 데이터는 밖에서 전달, 나머지는 뭉친다
  • 이를 선언적 프로그래밍이라 한다
  • 그렇다고 선언적인 코드가 무조건 좋은 것은 아니다
  1. 단일책임: 하나의 일을 하는 뚜렷한 이름의 함수를 만들자
  • 일단, 함수 이름을 지어보자 → 함수가 하는 일을 모두 표현할 수 있는 이름을 짓자
  • 한 가지 일만 하는 명확한 이름의 함수로 쪼개기
  • 비슷한 방식으로 한 가지 일만 하는 기능성 컴포넌트 만들기 (React Hooks)
  • 조건이 많아지면 오히려 한글 변수명을 사용하는 게 더 좋을 수도 있다 (마치 주석을 달아놓은 것만 같은 효과도 있음 )
  1. 추상화: 핵심 개념을 뽑아내자
  • 컴포넌트로 추상화하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

// 팝업 코드 제로부터 구현
<div style={팝업스타일}>
<button onClick={async () => {
const res = await 회원가입();
if (res.success) {
프로필로이동();
}
}}>전송</button>
</div>

// 중요 개념만 남기고 추상화
<Popup
onSubmit={회원가입}
onSuccess={프로필로이동}
/>

  • 함수를 추상화하기
1
2
3
4
5
6
// 설계사 라벨을 얻는 함수 구현
const planner = await fetchPlanner(plannerId);
const label = planner.name ? "새로운 상담사" : "연결중인 상담사";

// 중요 개념을 함수 이름에 담아 추상화
const label = await getPlannerLabel(plannerId);

얼마나 추상화할 것인가?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Level 0
<Button onClick={showConfirm}>전송</Button>;
{
isShoConfirm && (
<Confirm
onClick={() => {
showMessage("성공");
}}
/>
);
}

// Level 1
<ConfirmButton
onConfirm={() => {
showMessage("성공");
}}
>
전송
</ConfirmButton>;

// Level 2
<ConfirmButton message="성공">전송</ConfirmButton>;

// Level 3
<ConfirmButton />;
  • 꼭 레벨3가 정답이 아님 → 상황에 따라 다름
  • 추상화 수준이 섞여있다면 코드 파악이 어려우니 주의할 것
  1. 액션 아이템
  • 담대하게 기존 코드 수정하기: 두려워하지 말고 기존 코드를 씹고 뜯고 맛보고 즐기자
  • 큰 그림 보는 연습하기: 그 때는 맞고 지금은 틀리다. 기능 추가 자체는 클린해도 전체적으로는 어지러울 수 있다
  • 팀과 함께 공감대 형성하기: 코드에 정답은 없다. 명시적으로 이야기를 하는 시간이 필요하다
  • 문서로 적어보기: 글로 적어야 명확해진다. 향후 어떤 점에서 위험할 수 있는지, 어떻게 개선할 수 있는지를 정리해두기

느낀점

  • 프론트엔드 엔지니어는 끝없이 더 나은 사용자 경험을 위해 투쟁하는 사람들이다
  • 개발에 필요한 스킬만이 중요한 것이 아니다, 결국은 협업, 팀플레이.
  • 결국 코드를 적는 일도 글쓰기와 다르지 않다. 누군가에게 잘 읽히는 을 써야 하는 것.
  • 실무 레벨에서 필요한 스킬을 배웠다기 보다는 어떤 마음가짐을 배웠다는 게 더 중요했던 강의들이었다.

Comment and share

Harry Kim

author.bio


author.job