티스토리 뷰
리뷰하기전 쓰고 싶은 말
회사 출퇴근 시간에 코드스피츠 강의를 보고 집에와서 리뷰를 쓰려고 했는데 생각보다 내용이 너무 어려워서 그렇게 하지 못했다. 아침 지옥철을 타면서 강의를 이해하기엔 내 능력이 부족했다. 그래서 보다가 포기하고 집에서 집중해서 영상을 보면서 코드도 같이 작성해야겠다고 생각했다. 그런데 최근데 회사일도 많고 다른 일도 많아서 많이 미루게되었다. 이제 재택근무를 하게 되어서 시간이 좀 더 여유가 생겼다. 그시간에 강의듣고 리뷰를 적어야겠다.
리뷰
이번 강의는 예전처럼 이론 위주의 수업이 아니라 실제 코딩을 하는 수업이였다.html parser를 만드는 수업인데 예전에 비슷한걸 만들어본적이 있어서 그냥 보면 이해가 될 줄 알았다. 근데 생각보다는 좀 난이도가 있었다. 물론 구현하는데는 문제가 없지만 그냥 막 구현하려고 이 수업을 듣는게 아니기 때문에...
모든 문제를 해결할 때는 무작정 들어가는 것이 아니라 생각을 해야한다. 강사님은 먼저 3가지 경우를 나눴다.
특별한 건 없고 열고 닫아서 중간에 body가 들어가는 태그, 바로 닫는 태그, 텍스트 3가지 경우다.
그리고 이걸 바탕으로 먼저 기본 함수의 프레임을 잡아보자. 아직 A,B,C의 경우는 생각하지 않았다. 스택 자료구조를 이용한 첫번째 while문과 인덱스를 비교하는 두번째 while문을 만들어놨다.
const parser = (input) => {
input = input.trim();
const result = { name: 'ROOT', type: 'node', children: [] };
const stack = [{ tag: result }];
let curr,
i = 0,
j = input.length;
while ((curr = stack.pop())) {
while (i < j) {
}
}
return result;
};
그리고 이제 여기에 A,B,C 경우만 넣어보자.
<로 시작되지 않는 경우는 항상 C(text)이다. 그리고 <로 시작되면 A,B의 경우이다.
아 여기서는 cursor라는 값을 따로 만들었는데 i를 다루는건 굉장히 위험하기 때문이다. 그래서 이런경우에 하나의 변수를 생성하는 것이 좋다. 메모리상으로 변수 하나 정도는 영향을 끼치지 않는다. 객체의 깊은 복사, 변수생성 등에 들어가는 비용을 고민하지 말자.
const parser = (input) => {
input = input.trim();
const result = { name: 'ROOT', type: 'node', children: [] };
const stack = [{ tag: result }];
let curr,
i = 0,
j = input.length;
while ((curr = stack.pop())) {
while (i < j) {
const cursor = i;
if (input[cursor] === '<') {
// A,B의 경우
} else {
// C의 경우
}
}
}
return result;
};
여기서 강사님이 중요한 말을 한다. 사실 생각해본적은 없는 것 같지만 무의식중에 내가 하고 있던 행동이다.
코드는 무조건 쉬운것부터 처리한다.
어려운걸 먼저 처리하면 나중에 더 편하지 않을까?? 라고 생각하는 사람이 있을지도 모르지만 이건 취향이 아니다. 쉬운것부터 처리하는 것이 무조건 좋다. 어떤 경우를 먼저 처리하든 다른 경우를 처리하다보면 기존 코드를 수정해야 될 일이 생기기도 하고 재사용할 수 있는 함수를 만들 수도 있다. 이때 쉬운것부터 처리했다면 크게 어려움이 없다. 반면에 어려운것부터 처리를 하고 코드를 수정해야 될 일이 생긴다면 골치아파진다.
또한 쉬운 코드는 독립된 가능성일 확률이 낮다. 그렇기 때문에 먼저 봐야한다.
그래서 C의 경우를 처리하자.
코드는 어렵지 않다. 그다음 태그가 나오는 인덱스를 찾고 현재 인덱스(커서)에서 그다음 태그의 인덱스까지 텍스트를 잘라내면 된다. 그리고 i값을 그다음 태그가 나오는 인덱스로 바꿔준다. 앞으로는 이렇게 쉬운 코드는 설명을 생략하겠다.
const parser = (input) => {
// 생략
while (i < j) {
const cursor = i;
if (input[cursor] === '<') {
} else {
const idx = input.indexOf('<', cursor);
curr.tag.children.push({
type: 'text',
text: input.substring(cursor, idx),
});
i = idx;
}
}
// 생략
};
이제 A와 B의 경우를 살펴봐야 한다. 라고 생각하면 안된다.
역할을 인식했으면 즉시 함수로 바꾼다.
C의 경우를 함수로 뺄 수 있지 않을까?? 위의 코드는 알고리즘 적으로 독립적이다. 역할이 분리됬으니 즉시 함수화시켜야 한다. 나중은 없다. 그냥 이렇게 작성하다보면 나도 모르게 나쁜 코드를 만들게 된다.
또한 쉬운 코드는 역할에 위임하는 코드다.
const textNode = (input, cursor, curr) => {
const idx = input.indexOf('<', cursor);
curr.tag.children.push({
type: 'text',
text: input.substring(cursor, idx),
});
return idx;
};
const parser = (input) => {
input = input.trim();
const result = { name: 'ROOT', type: 'node', children: [] };
const stack = [{ tag: result }];
let curr,
i = 0,
j = input.length;
while ((curr = stack.pop())) {
while (i < j) {
const cursor = i;
if (input[cursor] === '<') {
} else {
i = textNode(input, cursor, curr);
}
}
}
return result;
};
이제 C의 경우를 처리했으니 A,B의 경우를 살펴보자. 여기서 A의 경우는 시작태그와 닫는 태그로 나누어진다. 즉
<div>, </div>, <img/> 와 같이 세가지 경우가 나온다. 그리고 어떠한 경우라도 >태그가 나온 이후로 인덱스를 이동시켜야 한다.
그리고 세가지 경우를 분기처리하면 된다.
while (i < j) {
const cursor = i;
if (input[cursor] === '<') {
const idx = input.indexOf('>', cursor);
i = idx + 1;
if (input[cursor + 1] === '/') {
} else {
if(input[idx-1]==='/'){
} else{
}
}
}
이제 세가지 경우 중 어느곳을 먼저 처리해야 될까?? 가장 쉬운것. 당연히 <img/> 이 경우다. 그런데 이 경우는 <div>, <img/> 이 두개의 경우가 비슷하게 처리된다.(시작태그) 그래서 다음과 같이 처리할 수 있다.
태그를 푸시하는 것까지는 설명이 불필요해서 설명하지 않겠다. 신경써서 봐야할 부부은 isClose 부분이다. 닫는태그가 아니라면 스택에 태그와 back인덱스를 객체에 담아서 푸시하고 현재 실행되는 while문을 멈춰버린다. 이부분은 조금뒤에 살펴보자
while (i < j) {
const cursor = i;
if (input[cursor] === '<') {
const idx = input.indexOf('>', cursor);
i = idx + 1;
if (input[cursor + 1] === '/') {
} else {
let name, isClose;
if(input[idx-1]==='/'){
name = input.substring(cursor+1, idx-1), isClose=true;
} else{
name = input.substring(cursor+1, idx), isClose=false;
}
const tag = {name, type:'node', children:[]};
curr.tag.children.push(tag);
if(!isClose){
stack.push({tag,back:curr});
break;
}
}
}
그리고 방금만든 코드를 함수로 빼자.
const elementNode = (input, cursor, idx, curr, stack) => {
let name, isClose;
if(inpput[idx-1]==='/'){
name=input.substring(cursor+1,idx-1), isClose=true;
}else{
name=input.substring(cursor+1,idx), isClose=false;
}
const tag = {
name,
type: 'node',
children: [],
};
curr.tag.children.push(tag);
if (!isClose) {
stack.push({ tag, back: curr });
return true;
}
return false;
};
const parser = (input) => {
while (i < j) {
const cursor = i;
if (input[cursor] === '<') {
const idx = input.indexOf('>', cursor);
i = idx + 1;
if (input[cursor + 1] === '/') {
} else {
if (elementNode(input, cursor, idx, curr, stack)) break;
}
} else {
i = textNode(input, cursor, curr);
}
}
};
그리고 이제 마지막으로 </div> 인 경우를 살펴보자.
curr = curr.back 이게 끝이다. 위에서 모든 처리를 해놨기 때문에 이런 간단한 코드로 해결할 수 있다.
const parser = (input) => {
input = input.trim();
const result = { name: 'ROOT', type: 'node', children: [] };
const stack = [{ tag: result }];
let curr,
i = 0,
j = input.length;
while ((curr = stack.pop())) {
while (i < j) {
const cursor = i;
if (input[cursor] === '<') {
const idx = input.indexOf('>', cursor);
i = idx + 1;
if (input[cursor + 1] === '/') {
curr = curr.back;
} else {
if (elementNode(input, cursor, idx, curr, stack)) break;
}
} else {
i = textNode(input, cursor, curr);
}
}
}
return result;
};
이제 끝난걸까?? 여기서 더 좋게 개선할 방법은 굉장히 많다.
예를 들어 elementNode 함수를 개선할 수 있다. 이건 단순하게 뺄 부분이 보이지 않기 때문에 좀 생각이 필요하다. 중복된 코드를 줄이는 수준은 개발자의 수준에 달려있다. 중복은 제거하는게 아니라 발견하는 거라고 한다.
코드 수준, 아키텍쳐 수준 등등 내 수준에 따라 중복제거가 보인다. 같은 코드를 계속볼때마다 중복이 보이면 그때마다 내 레벨이 올라간거다. 코드수준, 아키텍쳐수준은 가만히 있는다고 올라가지 않는다. 따로따로 훈련해야한다.
코드수준은 언어에 대한 이해가 올라야한다. 예를들어 es6, 7 8 계속해서 새로운 문법이 나오고있다. 우리는 계속해서 새로운 문법을 배우고 사용해야한다.
역할, 책임관계에 대한 이해는 디자인패턴이나 아키텍쳐 강의에서 알려준다고 했다. 아마 mv로시작하는 패턴이나 다른 디자인 패턴이 나오면서 역할, 책임에 대한 관계가 어떻게 바뀌는지 이런것들을 배울것같다.
const elementNode = (input, cursor, idx, curr, stack) => {
const isClose = input[idx - 1] === '/';
const tag = {
name: input.substring(cursor + 1, idx - (isClose ? 1 : 0)),
type: 'node',
children: [],
};
curr.tag.children.push(tag);
if (!isClose) {
stack.push({ tag, back: curr });
return true;
}
return false;
};
그리고 여기서 과제를 줬다. 안타깝지만 과제의 정답을 확인할 방법은 없다. 추후에 만들어서 블로그 혹은 깃헙에 올리겠다.
과제 내용은 꼬리물기 최적화가 되는 재귀함수로 바꾸기, 스택제거하기, json 파서만들기 등등이 있다.
https://github.com/yoonminsang/code-spitz/tree/main/function-and-oop
'강의 > 코드스피츠' 카테고리의 다른 글
코드스피츠 2rd-3 ES6+ 함수와 OOP 6회차 리뷰 (0) | 2022.05.30 |
---|---|
코드스피츠 2rd-3 ES6+ 함수와 OOP 5회차 리뷰 (0) | 2022.05.21 |
코드스피츠 3rd-3 ES6+ 함수와 OOP 4회차 리뷰 (0) | 2022.04.30 |
코드스피츠 3rd-3 ES6+ 함수와 OOP 2회차 리뷰 (0) | 2022.03.26 |
코드스피츠 3rd-3 ES6+ 함수와 OOP 1회차 리뷰 (0) | 2022.03.24 |