HTML PARSER
BeFore Make HTML Parser
- DATA ANANYSYS(데이터 분석)
- 어떠한 현상(선박안전, 재고관리 등)을 눈으로, 머리로 구조적인 해석을 해야한다.
- 단순한 원리의 조합으로 그 현상에 대해서 설명 할 수 있어야 한다.
- 여러 현상/케이스를 줄여서 최소한의 케이스로 줄여서 설명 할 수 있어야 한다.
- 케이스가 장황하고 많다면 그에 맞게 알고리즘을 짜야한다.(초급개발자의 벽)
- 수많은 IF로 상황을 구현한다 그러면 안되
- 개발은 케이스가 언제나 확정적이지 않다. 재귀적이며 복합적인 상황이기때문에 연습 필요
- 어떠한 현상을 보고 구조적이고, 재귀적인 형태의 데이터 구조를 만들어야 한다.
- 프로그램 언어를 정의하는 방법
==
BNF 정의 방식을 활용- 내부 구성 요소로부터 응용 구성요소로 확장하는 것
- 언어의 구성요소를 정의하는 여러 문법들이 있다(lex, yak)
A = <TAG>BODY</TAG>
B = <TAG/>
C = TEXT
BODY = (A | B | C)N // A의 BODY는 A, B, C의 재귀
INPUT AND FUNC
1단계 - 기본 함수 구현
- 함수를 디자인할때, 아큐먼트와 리턴값을 통해 확실한 시그니처를 구현해야한다.
- 어떤 아큐먼트를 받아서 어떻게 처리해서 어떤 리턴값을 만들것인지
- input이라는 텍스트를 구조적인 객체로 만들어서 리턴하고 싶다. (시그니처)
- 동적루프 = 스택구조를 통해 루프를 결정하는 요인을 변경
- 스택구조를 통해 대상(태그)를 왔다갔다(자식을 처리하고 다시 부모로 가서 처리) 하기 위해
- 스택구조를 밖에 위치하고(첫번째 while) 알고리즘을 안으로 들어왔다(두번째 while)
- 두번째 while문 안에서 스택을 추가하면 루프가 더 돈다(동적루프)
- 루프를 결정하는 요인이 루프를 돌다가 변할 수 있다.
- 현재 parser라는 함수는 result 객체를 반환한다.
- result객체 안에 children을 채우는 것
let input = '<div>a<a>b</a>c<img/>d</div>'
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 스택을 소비하는 역할
while ((curr = stack.pop())) {
// 두번째 while input을 스캐닝하는 역할
while (i < j) {}
}
return result
}
2단계 - TEXTNODE 구현
-
스캐너
- 루프를 전체돌면서 요소를 확인하는 것을 스캐너라고 한다.
- i가 바뀌면 위험하고 조회용으로 사용하기 위해 cursor로 대입
- i를 바뀔때만 직접적으로 i를 변경한다(i = idx)
-
문자열의 시작이 '<'로 태그인지, 텍스트 노드인지 판단한다.
- 텍스트 노드는 다음 '<' 태그가 오기전까지 텍스트 노드로 인식한다.
-
텍스트 노드로 찾아 현재 stack에 children으로 넣어준다.
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) { // i가 바뀌면 위험하고 조회용으로 사용하기 위해 cursor 대입 const cursor = i if (input[cursor] === '<') { // A, B의 경우 } else { // C의 경우 const idx = input.indexOf('<', cursor) curr.tag.children.push({ type: 'text', text: input.substring(cursor, idx), }) i = idx } } } return result }
-
역할을 인식하는 순간 그 즉시 함수로 만들어 뺀다
- 나중에 하면 함수로 만들 부분이 변수의 오염으로 인해 어려워진다.
- curr은 ref 타입인데 왜 인자로 넣을까?? 그렇게 하지말라고 배웠는데
- 코드는 응집성을 높이고 의존성을 낮혀야하는게 기본 원칙이지만,
- 현실에서는 밸런스를 맞춰야한다.
- 이 코드는 응집성을 최대한으로 높여서 만든거다(밸런스를 맞춘것)
- 좋은 디자인이란 정답이 없다.
- 상황에 맞춰 응집성을 높일것인지, 의존도를 낮출건인지 개발자가 판단(고급)
const textNode = (input, cursor, curr) => { const idx = input.indexOf('<', cursor) curr.tag.children.push({ type: 'text', text: input.substring(cursor, idx), }) return idx }
3단계 - htmNode(케이스 : 시작태그, 닫는태그, 싱글태그)
-
'<'는 HTML에서 파서를 하기위한 트리거이다(토큰이라고 부른다)
- 태그가 아닐때, '<'를 쓸때는 항상 이스케이프를 해야한다
- '>'는 트리거가 아니기때문에 상관없다.
-
왜 textNode부터 처리했을까?
- 코드를 짤때는 무조건 쉬운것부터 처리한다.
- 쉬운것의 특징은 의존성의 낮다, 독립된 기능일 가능성이 높다.
- 나중에 쉬운코드의 의존하는 코드를 짜기 편한다.
- 복잡한것은 공유하는 부분도 크고 중복도 크기떄문에 함수로 뺴기 힘들기 때문
- 이렇게 해야지 더 견고하고 의존성이 낮은 모듈로 부터 의존성이 높은 모듈을 짜 나갈 수 있다.
-
케이스가 3개가 있지만 공통점에 대한 것을 먼저 처리 해준다. (코드 중복 방지)
-
태그의 특징(공통점을 찾는다) '<'시작하고, '>'로 끝난다.
-
공통점을 찾는(추상화적인것)을 눈으로 찾는 훈련이 필요하다.
-
코드를 파악할떄는 최대한 중복과 공통점이 있을거라고 생각하고 찾는다.
-
사물 보고 데이터를 분석할때 추상화된 공통점을 발견하고 재귀적인 로직을 발견하는 것이 개발자의 몫
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] === '/') { } else { } } else i = textNode(input, cursor, curr) } } return result }
-
-
시작태그와 싱글태그의 공통점과 차이점을 파악하여 로직 추가
-
데이터 모델링 / 분석을 잘하면 코드는 매칭 과정일 뿐이다.
-
공통점은 시작하는 태그이기때문에 이름을 가진다
-
차이점은 '/>' 닫히냐 안 닫히냐를 통해 구분한다.(동일한 코드를 이지선다로 작성)
-
공통점과 차이점을 통해 코드로 표현한다.
let name. isClose; // 공통 준비 사항 if(input[idx - 1] === '/'){ //<img/> name = input.substring(cursor + 1, idx - 1), isClose = ture; }else{ //<div> name = input.substring(cursor + 1, idx), isClose = false; }
-
-
차이점을 처리했다면, 이제 더 이상 차이를 케이스로 인식 안해도된다.
- 경우의 수가 케이스 ⇒ 케이스를 값으로 바꿀수 잇다 ⇒ 케이스는 값으로 환원
- 메모리와 연산은 교환된다.
- 케이스 밑에 연산한 메모리를 가르키는 하나의 연산만 기술하면 된다
- 차이를 일으키는 연산을 메모리로 흡수한 다음, 그 메모리를 이용한 하나의 로직만 기술(초급의 벽)
- 이렇게 안하면 케이스마다 알고리즘이 달라져 유지보수가 어렵고 이해하기도 어렵다
-
공통 처리 사항 == 화이트리스트로 만든 데이터를 이용하여 처리한다
- 함수의 인자를 필터링해서 안정적인 조건을 만드는 것(화이트리스트)
- 서버에서 내려온 JSON을 뷰가 소비하게 좋게 바꾸는것(화이트리스트)
- 복잡성을 제거하고 안정성있게 데이터리스트를 만드는 것을 화이트 리스트
- 화이트리스트를 집중하고 알고리즘을 짜는게 더 효율
- 알고리즘을 어렵게 짜려고 하지말고 화이트리스트를 만드는 훈련이 더 중요
let name, isClose // 공통 준비 사항
// 다른점을 기술하는 부분을 메모리로 흡수하는 측
// 차이를 일으키는 연산
if (input[idx - 1] === '/') {
//<img/>
;(name = input.substring(cursor + 1, idx - 1)), (isClose = ture)
} else {
//<div>
;(name = input.substring(cursor + 1, idx)), (isClose = false)
}
// 공통 처리 사항
const tag = { name, type: 'node', children: [] }
curr.tag.children.push(tag)
-
차이점을 이용하여 새로운 스택에 추가한다
- 스택에 추가하기전 현재 스택의 index를 기억한다(함수의 리턴 포인트)
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 = ture;
}else{ //<div>
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;
}
}
- 역할을 인식하는 순간 그 즉시 함수로 만들어 뺀다
- 단지 브레이크는 외부제어 통제이기 때문에 boolean 값을 리턴한다.
- 동적 루프 및 응집성을 높이기 위한 밸런스 처리
const elementNode = (input, cursor, idx, curr, stack) => {
let name, isClose
if (input[idx - 1] === '/') {
;(name = input.substring(cursor + 1, idx - 1)), (isClose = ture)
} 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 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
}
- 닫는 태그는 break할때 저장한 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())) {
// 브래이크를 통해 빠져 나오면 새로운 curr이 생긴다.
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, cursorr, curr)
}
}
return result
}
좋은 개발론
좋은 코드를 짜는 방법
- 데이터를 이해하고
- 재귀적인 로직을 찾아내거나
- 추상화된 공통점을 찾아내거나
- 역할을 이해하거나
코드의 가독성은 어떻게 확보되는것일까요?
- 변수명이 이쁘면? 길면? 코드가 읽기 쉬어지나? NoNo
- 알고리즘, 수학적 함수, 연산 이런거는 무조건 어렵다.
- 왜? 컴퓨터가 연산하는걸 우리가 머리로 생각해야 하니까
- 쉬운코드 === 역할에게 위임하는 코드
- 함수명을 통해 의도를 이해하고
- 함수 안 로직을 처리하는데 신경을 쓰지 않지만
- 함수명을 통해 어떠한 작업을 할 수 있는지 확인
리더블한 코드란?
적절한 역할 모델로 위임되서 그들간의 통신과 협업을 볼 수 있는 코
중복은 제거하는게 아니라 발견하는거다
개발자의 기량에 따라 코드, 아키텍쳐, 데이터의 중복을 판단하여 제거 할 수 있다.
똑같은 코드를 봤는데 중복된게 갑자기 보인다 그럼 실력이 올라간거다
-
코드에 대한 중복
- 언어에 대한 바른 이해와 문법적으로 해박한 사용법으로 코드 중복을 없앨수 잇다.
-
아키텍처에 대한 중복
- 역할 관계를 인식하고 책임이 어디까지 들어가는지 얼마나 확장 가능성 있는지
- 어디가 중복이고 어디가 레이어인지 알 수 있으면 중복 제거 가능
-
데이터에 대한 중복
- 전통적인 RDB의 정규화랑 안정적인 로직이 있다.