Jun 개발노트

코드스피치 강의 ES6+OOP 3회차 정리

2020-09-20

HTML PARSER

BeFore Make HTML Parser

  1. DATA ANANYSYS(데이터 분석)
    • 어떠한 현상(선박안전, 재고관리 등)을 눈으로, 머리로 구조적인 해석을 해야한다.
    • 단순한 원리의 조합으로 그 현상에 대해서 설명 할 수 있어야 한다.
    • 여러 현상/케이스를 줄여서 최소한의 케이스로 줄여서 설명 할 수 있어야 한다.
      • 케이스가 장황하고 많다면 그에 맞게 알고리즘을 짜야한다.(초급개발자의 벽)
      • 수많은 IF로 상황을 구현한다 그러면 안되
      • 개발은 케이스가 언제나 확정적이지 않다. 재귀적이며 복합적인 상황이기때문에 연습 필요
    • 어떠한 현상을 보고 구조적이고, 재귀적인 형태의 데이터 구조를 만들어야 한다.
  2. 프로그램 언어를 정의하는 방법 == 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단계 - 기본 함수 구현

  1. 함수를 디자인할때, 아큐먼트와 리턴값을 통해 확실한 시그니처를 구현해야한다.
    • 어떤 아큐먼트를 받아서 어떻게 처리해서 어떤 리턴값을 만들것인지
    • input이라는 텍스트를 구조적인 객체로 만들어서 리턴하고 싶다. (시그니처)
  2. 동적루프 = 스택구조를 통해 루프를 결정하는 요인을 변경
    • 스택구조를 통해 대상(태그)를 왔다갔다(자식을 처리하고 다시 부모로 가서 처리) 하기 위해
    • 스택구조를 밖에 위치하고(첫번째 while) 알고리즘을 안으로 들어왔다(두번째 while)
    • 두번째 while문 안에서 스택을 추가하면 루프가 더 돈다(동적루프)
    • 루프를 결정하는 요인이 루프를 돌다가 변할 수 있다.
  3. 현재 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 구현

  1. 스캐너

    • 루프를 전체돌면서 요소를 확인하는 것을 스캐너라고 한다.
    • i가 바뀌면 위험하고 조회용으로 사용하기 위해 cursor로 대입
    • i를 바뀔때만 직접적으로 i를 변경한다(i = idx)
  2. 문자열의 시작이 '<'로 태그인지, 텍스트 노드인지 판단한다.

    • 텍스트 노드는 다음 '<' 태그가 오기전까지 텍스트 노드로 인식한다.
  3. 텍스트 노드로 찾아 현재 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;
    };
    
  4. 역할을 인식하는 순간 그 즉시 함수로 만들어 뺀다

    • 나중에 하면 함수로 만들 부분이 변수의 오염으로 인해 어려워진다.
    • 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(케이스 : 시작태그, 닫는태그, 싱글태그)

  1. '<'는 HTML에서 파서를 하기위한 트리거이다(토큰이라고 부른다)

    • 태그가 아닐때, '<'를 쓸때는 항상 이스케이프를 해야한다
    • '>'는 트리거가 아니기때문에 상관없다.
  2. 왜 textNode부터 처리했을까?

    • 코드를 짤때는 무조건 쉬운것부터 처리한다.
    • 쉬운것의 특징은 의존성의 낮다, 독립된 기능일 가능성이 높다.
    • 나중에 쉬운코드의 의존하는 코드를 짜기 편한다.
    • 복잡한것은 공유하는 부분도 크고 중복도 크기떄문에 함수로 뺴기 힘들기 때문
    • 이렇게 해야지 더 견고하고 의존성이 낮은 모듈로 부터 의존성이 높은 모듈을 짜 나갈 수 있다.
  3. 케이스가 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;
      };
      
  4. 시작태그와 싱글태그의 공통점과 차이점을 파악하여 로직 추가

    • 데이터 모델링 / 분석을 잘하면 코드는 매칭 과정일 뿐이다.

    • 공통점은 시작하는 태그이기때문에 이름을 가진다

    • 차이점은 '/>' 닫히냐 안 닫히냐를 통해 구분한다.(동일한 코드를 이지선다로 작성)

    • 공통점과 차이점을 통해 코드로 표현한다.

      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;
      	}
      
  5. 차이점을 처리했다면, 이제 더 이상 차이를 케이스로 인식 안해도된다.

    • 경우의 수가 케이스 ⇒ 케이스를 값으로 바꿀수 잇다 ⇒ 케이스는 값으로 환원
    • 메모리와 연산은 교환된다.
    • 케이스 밑에 연산한 메모리를 가르키는 하나의 연산만 기술하면 된다
    • 차이를 일으키는 연산을 메모리로 흡수한 다음, 그 메모리를 이용한 하나의 로직만 기술(초급의 벽)
    • 이렇게 안하면 케이스마다 알고리즘이 달라져 유지보수가 어렵고 이해하기도 어렵다
  6. 공통 처리 사항 == 화이트리스트로 만든 데이터를 이용하여 처리한다

    • 함수의 인자를 필터링해서 안정적인 조건을 만드는 것(화이트리스트)
    • 서버에서 내려온 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);
  1. 차이점을 이용하여 새로운 스택에 추가한다

    • 스택에 추가하기전 현재 스택의 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;
	}
}
  1. 역할을 인식하는 순간 그 즉시 함수로 만들어 뺀다

    • 단지 브레이크는 외부제어 통제이기 때문에 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;
};
  1. 닫는 태그는 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
  • 알고리즘, 수학적 함수, 연산 이런거는 무조건 어렵다.
    • 왜? 컴퓨터가 연산하는걸 우리가 머리로 생각해야 하니까
  • 쉬운코드 === 역할에게 위임하는 코드
    • 함수명을 통해 의도를 이해하고
    • 함수 안 로직을 처리하는데 신경을 쓰지 않지만
    • 함수명을 통해 어떠한 작업을 할 수 있는지 확인

리더블한 코드란?

적절한 역할 모델로 위임되서 그들간의 통신과 협업을 볼 수 있는 코

중복은 제거하는게 아니라 발견하는거다

 개발자의 기량에 따라 코드, 아키텍쳐, 데이터의 중복을 판단하여 제거 할 수 있다.
 똑같은 코드를 봤는데 중복된게 갑자기 보인다 그럼 실력이 올라간거다
  1. 코드에 대한 중복

    • 언어에 대한 바른 이해와 문법적으로 해박한 사용법으로 코드 중복을 없앨수 잇다.
  2. 아키텍처에 대한 중복

    • 역할 관계를 인식하고 책임이 어디까지 들어가는지 얼마나 확장 가능성 있는지
    • 어디가 중복이고 어디가 레이어인지 알 수 있으면 중복 제거 가능
  3. 데이터에 대한 중복

    • 전통적인 RDB의 정규화랑 안정적인 로직이 있다.