코드스피치 ES6+ 디자인패턴과 뷰패턴 1회차 정리
2021-02-10
데이터를 활용하여, 테이블로 표현해보자
0. 데이터 분석
- 테이블 명 : title
- column : header
- row : Items (2차원 배열)
{
"title":"TIOBEIndex for June 2017",
"header":["Jun-17","Jun-16","Change","Programming Language","Ratings","Change"],
"items":[
[1,1,"","Java","14.49%","-6.30%"],
[2,2,"","C","6.85%","-5.53%"],
[3,3,"","C++","5.72%","-0.48%"],
[4,4,"","Python","4.33%","0.43%"],
[5,5,"","C#","3.53%","-0.26%"],
[6,9,"","change","Visual Basic .NET","3.11%","0.76%"],
[7,7,"","JavaScript","3.03%","0.44%"],
[8,6,"change","PHP","2.77%","-0.45%"],
[9,8,"change","Perl","2.31%","-0.09%"],
[10,12,"change","Assembly language","2.25%","0.13%"],
[11,10,"change","Ruby","2.22%","-0.11%"],
[12,14,"change","Swift","2.21%","0.38%"],
[13,13,"","Delphi/Object Pascal","2.16%","0.22%"],
[14,16,"change","R","2.15%","0.61%"],
[15,48,"change","Go","2.04%","1.83%"],
[16,11,"change","Visual Basic,2.01%","-0.24%"],
[17,17,"","MATLAB","2.00%","0.55%"],
[18,15,"change","Objective-C","1.96%","0.25%"],
[19,22,"change","Scratch","1.71%","0.76%"],
[20,18,"change","PL/SQL","1.57%","0.22%"]
]
}
1. 코드 구현
-
코드(함수명, 변수명)를 보고 의도를 파악할 수 있어야 한다 → 어떻게 동작할지
- 프로그래밍을 한다는 것은 나만의 단어와 의미체계를 만드는것
- 코드 설명
-
private 변수 때문에 클로져로 구현(class만 알고 있어야 되는 것)
-
클로져 상태의 클래스가 리턴하는 방식으로 하기 위해서 즉시실행함수 구현
//<section id="data"></section> const Tabel = (_ => { return class{ } })(); const table = new Table("#data"); table.load("71_1.json");
-
-
코드를 사용하는 부분(호출하는 부분)을 먼저 작성한다 → 시나리오 작성 (의도를 만들고)
- 호출하는 부분에서 이상이 있다면, 구현도 이상하다
- 테스트 주도 개발과 비슷하다 사용할 코드를 만들고 → 구현
-
그 다음, 함수나 클래스를 구현해야 한다
-
이유 1 - 구현된 클래스나 함수로 내가 안 짤수도 있다.
-
이유 2 - 라이브러리나 사수가 짜줄수도 있기 때문
const Table = (_ => { const Private = Symbol(); return class{ constructor (parent) { } async load (url) { } } })(); const table = new Table("#data"); table.load("71_1.json");
-
-
구현을 하면서, 생각/로직을 확장한다.
-
데이터가 load되면 화면에 render 된다는 프로세스를 알고 있기때문에 render메소드를 구현
-
데이터를 로딩하는것(load) ≠ 그림을 그리는 것(render) → 역할이 다름
- load와 render 메소드를 별도 구현
-
table.load 밑에 render를 호출해주지 않을까?
- load가 언제 완료될지 모르니까(비동기 로직)
const Table = (_ => { const Private = Symbol(); return class{ constructor (parent) { } async load (url) { } render () { } } })(); const table = new Table("#data"); table.load("71_1.json");
-
-
상세구현을 하기전에, 우리는 하고싶은일을 명시적으로 적는 일부터 하자!!
-
주석은 보장해주지 않는다 (오라클도 주석과 코드가 다르다)
- 레거시 코드나 라이브러리를 보고 이걸 만든 의도로 파악해보자
-
코드가 느려질 생각하지도 말고, 무조건 말을 코드로 만드는 연습을 하자
-
부모가 될 셀렉터 문자열을 코드로 표현하면 아래와 같다
constructor (parent) { if (typeof parent != 'string' || !parent) throw 'invalid param'; this[Private] = {parent}; }
-
내장을 보이기 싫다면,Symbol이나 WeakMap을 활용해보자(최대한 노력해보자)
-
누군가 내장을 건드려서 바꿔서 사고가 나도, 잘못을 후회해도 소용없다.
const Private = Symbol(); return class{ constructor (parent) { if (typeof parent != 'string' || !parent) throw 'invalid param'; this[Private] = {parent}; }
함수의 값을 넘기는 방법은 인자와 전역변수(지역참조)를 활용하는 것
-
클래스 안에서의 내부함수(메서드)라면 지역참조를 해야한다(클래스 변수)
-
지역참조 : 컨텍스트 ⇒ 인스턴스의 것만 참조해!!!
- 클래스 안에 있는 메서드는 함수가 아니다.
- 메서드란 인스턴스의 상태를 공유하는 함수의 집합이다.
- _render()함수로 인자로 보내지 않는 이유는 _render는 메서드이기 때문이다.
Object.assign(this[Private], {title, header, items}); this._render();
-
-
외부의 노출되는 함수라면, 외부에서 사용될거라면 인자를 받아서 처리한다.
- 인자를 넘기면 값이 화이트리스트인지 아닌지 모르기 때문에 매번 validation을 해줘야한다.
-
코드를 작성하기전, 한번 중간언어로 고치자(의사코드)
- 우리는 한국말이 편하다 하지만 순서가 이상해도 의미가 전달된다.
- 하지만 컴퓨터는 그렇지 못하다
-
의사코드란, 해야할일이 아니라!!! 순서대로 할일!!
-
부모, 데이터 체크
- 왜 여기서 부모를 체크하나? render할때만 부모를 체크하면 된다
- 부모가 통과하지 못하면 아래를 실행할 필요가 없으니까
_render () { // 부모, 데이터 체크 // table 생성 // 캡션을 title로 // header를 thead로 // items를 tr로 // 부모에 table 삽입 }
-
3. 코드 완성본(1차)
const Table = (_ => {
const Private = Symbol()
return class {
constructor (parent) {
if (typeof parent != 'string' || !parent) throw 'invalid param'
this[Private] = { parent }
}
async load (url) {
const response = await fetch(url)
if (!response.ok) throw 'invalid response' // 데이터 validation
const json = await response.json()
const { title, header, items } = json
if (!items.length) throw 'no items' //데이터 validation
Object.assign(this[Private], { title, header, items })
this._render()
// Promise 기반
// fetch(url).then(response => {
// return response.json();
// }).then(json => {
// this._render();
// })
}
_render () {
// 부모, 데이터 체크
const fields = this[Private], parent = document.querySelector(fields.parent);
if(!parent) throw 'Invalid parent';
if(!fields.items || !fields.items.length){ //이 부분은 데이터가 검증하는 부분과 동일하다!!! 반응하는 로직인데!! 중복이다!!
parent.innerHTML = "no data";
return;
} else parent.innerHTML = '';
// table 생성
// 캡션을 title로
const table = document.createElement('table');
const caption = document.createElement('caption');
caption.innerHTML = fields.title;
table.appendChild(caption)
// header를 thead로
table.appendChild(
fields.header.reduce((thead, data) => {
const th = document.createElement('th');
th.innerHTML = data;
thead.appendChild(th);
return thead;
}, document.createElement('thead'))
)
// items를 tr로
// 부모에 table 삽입
parent.appendChild(
fields.items.reduce((table, row) => {
table.appendChild(
row.reduce((tr, data) => {
const td = document.createElement('td');
td.innerHTML = data;
tr.appendChild(td);
return tr;
}, document.createElement('tr'))
)
return table;
}, table));
}
}
})()
const table = new Table('#data')
table.load('71_1.json')
0. 프로그래밍 세계에서 유일하게 변하지 않는 원칙
모든 프로그램은 변한다
- 이미 작성된 복잡하고 거대한 프로그램을 어떻게 변경할 수 있을 것인가?
- 바꿀 수 없다 → 다시 짠다 → 야근 → 요구사항 변경 → 롤백 → 야근
격리(Isolation) : Something 변화, Something Else 영향 X
- 격리 기본 전
- load 보다는 render가 변화율이 높다!
- render가 변하면 Table클래스가 전체 영향이 받는다.
- 변환율이란 시간적인 대칭성
- 변화의 원인과 주기별로 정리
- load 보다는 render가 변화율이 높다!
실천수칙 : 강한 응집성 & 약한 의존성
- Q. 데이터로딩을 왜 테이블에서 하지?? 강한 응집력이 때문에???
- A. 테이블은 로딩을 가질 역할/책임이 없다.
- A. 덩어리에서 어떻게 코드를 분리할것인가? → 역할을 기준으로 분리하자!!!
- A. 렌더링을 플랫폼 별로 나누어주자
Rendeing을 도메인이라고 정의한다 (중립적인 렌더링) ⇒ 도메인 패턴
1. 코드구현
-
역할 분리
-
Loader 역할 : 데이터를 받아서 json으로 만들어주는 역할
-
Renderer 역할 : json은 데이터를 받아서 그려주는 역할만 한다.
-
Loader와 Renderer는 분리되었기 때문에 인자로 보내는게 정답!! 별도의 객체이기 때문이다.
const loader = new Loader('71_1.json'); loader.load(json => { const renderer = new Renderer(); renderer.setDate(json); renderer.render(); })
-
-
데이터의 화이트리스트(VO ⇒ Value Object)
-
json 은 검증되지 않는 값 (화이트데이터를 보내줘야하는데 블랙데이터를 보내고 있다)
-
정리되고 안정성이 되어 있는 데이터(VO)를 보내준다(value Object)
const data = new JsonData('71_1.json'); const renderer = new Renderer(); renderer.render(data);
-
-
Data Load ⇒ Data Supply로 역할이 변경 되었다.
- 데이터 로드 : 작은역할
- 주요한 역할 : 데이터를 제공
- 렌더러에게 데이터를 제공해주는것이니까 이름을 바꾼다 ⇒ 역할이 바뀐다
-
역할 모델 구현
-
Loader ⇒ JsonData로 바꿨다.
(Data 추상객체 ⇒ (JSON 서브객체)
-
본질적인 로직은 Data 인터페이스(추상객체)가 있고, 파싱로직만 다른 서브 객체 이런게 역할모델이다(이름으로 추정한다)
-
알고리즘이란 제어로직과 변수를 이용하여 원하는 값을 원하는것
-
오브젝트가 메세지를 통해서 문제를 해결한다
-
이러한 메세지를 객체망을 통해 통신한다
-
-
타입체크 및 내부 프로토콜
- JsonData.protype ⇒ Data 이며, instanceof를 통해 해당 type을 체크하면서 유효성 검사를 대신한다.
- 각각의 객체끼리는 타입으로 통신해야 한다(프로토콜 === VO)
- 두 객체가 통신할때, 어떠한 약속을 한다 (프로토콜) ⇒ 어떠한 데이터가 온다
- 객체의 타입이 존재하는 이유는 타입으로 알고리즘을 대체한다.(유효성 검증을 대신한다)
- instanceof(타입체크)를 통해 유효성 검사를 해준다. 타입이 아니면 에러발생
- 강타입을 특성을 활용한 type 프로그래밍 보증한다!!
- getData가 async면 getData를 사용하는곳도 async로 되어있어야 한다(async 전파)
const Data = class { async getData () {throw 'getData mush Overide'} } const JsonData = class extends Data { constructor (data) { super() this._data = data } async getData () { if (typeof this._data === 'string') { const response = await fetch(this._data) return await response.json() } else return this._data } } const Renderer = class { constructor () {}; async render(data) { if(!(data instanceof Data)) throw 'invalid data type'; const json = await data.getData(); console.log(json) } }
- 내부 프로토콜인 info 역할로 유효성 화이트리스트에 대한 책임이 넘어간다.
const Info = class{ constructor (json) { const {title, header, items} = json; if (typeof title != 'string' || !title) throw 'invalid title'; if (!Array.isArray(header) || !header.length) throw 'invalid header'; if(!Array.isArray(items) || !items.length) throw 'invalid items'; items.forEach((v, idx) =>{ if(!Array.isArray(v) || v.length != header.length){ throw 'invalid items : ' + idx; } }); this._private = {title, header, items}; } get title(){return this._private.title;} get header(){return this._private.header;} get items(){return this._private.items;} } const Data = class { async getData () {throw 'getData mush Overide'} } const JsonData = class extends Data { constructor (data) { super() this._data = data } async getData () { let json; if (typeof this._data === 'string') { const response = await fetch(this._data) json = await response.json() } else json = this._data return new Info(json); } } const Renderer = class { constructor () {}; async render(data) { if(!(data instanceof Data)) throw 'invalid data type'; const info = await data.getData(); console.log(info.title, info.header, info.items) } }
- JsonData.protype ⇒ Data 이며, instanceof를 통해 해당 type을 체크하면서 유효성 검사를 대신한다.
-
계약에 대해서(내부/외부)
- Data클래스의 getData()는 info를 보장하지 않는다.(계약사항이 아니다)
- JsonData에서 구현된것이다 ⇒ 권한이 없다
- 역할 재조정
- 부모객체 역할 : 부모는 서브객체의 데이터 결과값을 받아 통신한다
- 서브객체 역할 : 부모의 참고자료만을 만드는 역할
- hook(_getData)을 통해서 서브객체에서 참조데이터 결과를 받아서 Data에서 Info객체를 관리
- 계약이란, 어떤 객체는 어떠한 역할을 해야한다는것을 정의하는것
- 내부계약 Data와 JsonData의 관계 서브객체는 JSON으로 만들어줘야한다
- 외부계약 Render와 Data의 관계에서는 JSON인거 몰라도 된다 그래서 Info를 통해서 애기한다.
- 객체는 인자로 넘기지않는다, 컨텍스트로 넘긴다.(this._data)
const Info = class{...} const Data = class { async getData () { const json = await this._getData() return new Info(json) } // hook async _getData () {throw 'getData mush Overide'} } const JsonData = class extends Data { constructor (data) { super() this._data = data } async _getData () { if (typeof this._data === 'string') { const response = await fetch(this._data) return await response.json() } else return this._data } } const Renderer = class { async render (data) { if (!(data instanceof Data)) throw 'invalid data type' const {_private : {title, header, items}} = await data.getData(); Object.assign(this, {title, header, items}) this._render() } _render () { throw '_render must override' } } const TableRenderer = class extends Renderer { constructor (parent) { if (typeof parent != 'string' || !parent) throw 'invalid param' super() this._parent = parent } // hook _render () {....} } const data = new JsonData('71_1.json') const renderer = new TableRenderer("#data") renderer.render(data)
- Data클래스의 getData()는 info를 보장하지 않는다.(계약사항이 아니다)
2. 요구사항과 수정 변화에 대응하려면 ?
- 어디까지가 경계의 역할을 보고 격리시킬지, 격리되있는것을 객체로 만들어서 협력하게 만들어라
- 템플릿메소드 패턴 : 부모와 자식과 협력
- 훅을 끌어올려 부모쪽에서 대표메소드를 노출하고 자식쪽에서는 그 일부만 구현해서 서로 협력
- 데이터와 렌더러와의 협력 완전히 다른 집안이기 떄문에 프로토콜을 통해서 협력
- Info객체 : 중간에 서로 합의를 본 내용 = 프로토콜
참고사항
-
프로그램을 한다는 나만의 단어와 의미체계를 만든다는 것
-
fetch의 결과는 항상 promise며, then의 결과값도 promise (text를 제외한 모든 타입이)
-
_함수명은 js 3.1부터 내려온 private 함수명의 표현이다
-
코드를 만드는것은 소저와 같다(진흙을 하나하나 붙어서 작품을 만드는것)
에러의 3단계
-
complie 에러 ⇒ 문법이 이상해!! (syntex err)
-
runtime 에러 ⇒ nullpointer ⇒ 해결방법 : throw날려서 그 지점을 찾자! (최후의 보루!!!)
-
context 에러 ⇒ 프로그램이 죽지않는다. 에러가 나오지 않는다 결과값만 다르다 (농부가 되자)