데이터를 활용하여, 테이블로 표현해보자
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 에러 ⇒ 프로그램이 죽지않는다. 에러가 나오지 않는다 결과값만 다르다 (농부가 되자)