Frontend
명령형 프로그래밍과 선언형 프로그래밍
프로그래밍 패러다임
프로그래밍 패러다임은 프로그래머에게 프로그래밍의 관점을 갖게 해 주고, 결정하는 역할을 한다. 소프트웨어 공학의 방법론과 비교할 수 있다. 쉽게 말하자면 프로그래머가 문제를 해결하는데 사용하는 기법이나 방법, 스타일이라고 할 수 있겠다.
예시로 객체 지향 프로그래밍은 프로그램을 상호작용하는 객체들의 집합으로 볼 수 있게 하고, 함수형 프로그래밍은 상태 값을 가지지 않는 함수 값들의 연속으로 프로그램을 정의한다.
크게 두 갈래, 명령형(Imperative) 패러다임과 선언형(Declarative) 패러다임으로 나뉜다. 컴퓨터공학을 공부할 때는 주로 명령형 패러다임의 갈래인 절차 지향과 객체 지향 방식의 프로그래밍을 공부했다. 웹 프론트엔드 개발을 공부하면서 선언형 프로그래밍 방식이 무엇인지, 함수형 프로그래밍의 장점과 단점 등을 익히고 여러 프로그래밍 방식에 대해 익숙해질 필요가 있다.
프론트엔드 개발을 하면서 명령형 프로그래밍 방식으로 개발하는 것과 선언형 프로그래밍 방식으로 개발하는 것의 차이점을 예제를 보면서 이해해보자.
명령형 프로그래밍
컴퓨터 과학에서 명령형 프로그래밍은 선언형 프로그래밍과 반대되는 개념으로, 프로그램이 상태와 상태를 변경시키는 구문의 관점에서 연산을 설명하는 프로그래밍 패러다임의 일종이다. 자연 언어에서의 명령법이 어떤 동작을 할 것인지를 명령으로 표현하듯이, 명령형 프로그래밍은 컴퓨터가 수행할 명령들을 순서대로 써 놓은 것이다. (@wikipedia)
명령형 프로그래밍은 컴퓨터가 수행할 명령들을 순서대로 써 놓은 것이다.
- 대부분의 언어들이 명령을 나열하는 것으로 개발이 진행된다.
- “어떻게 구현하는가”를 디테일하게 기술하는 것에 관점이 있다.
- 처음 프로그래밍을 배울 때 배우는 방식
선언형 프로그래밍
한 정의에 따르면, 프로그램이 어떤 방법으로 해야 하는지를 나타내기보다 무엇과 같은지를 설명하는 경우에 “선언형”이라고 한다. 예를 들어, 웹 페이지는 선언형인데 웹페이지는 제목, 글꼴, 본문, 그림과 같이 “무엇”이 나타나야하는지를 묘사하는 것이지 “어떤 방법으로” 컴퓨터 화면에 페이지를 나타내야 하는지를 묘사하는 것이 아니기 때문이다. 이것은 전통적인 포트란과 C, 자바와 같은 명령형 프로그래밍 언어와는 다른 접근 방식인데, 명령형 프로그래밍 언어는 프로그래머가 실행될 알고리즘을 명시해주어야 하는 것이다. 간단히 말하여, 명령형 프로그램은 알고리즘을 명시하고 목표는 명시하지 않은데 반해 선언형 프로그램은 목표를 명시하고 알고리즘을 명시하지 않는 것이다. (@wikipedia)
대표적인 선언형 프로그래밍 방식
- HTML : 무엇이 나타나야 되는지를 명시한다.
- SQL : 무엇이 나타나야 되는지를 명시한다.
- 화면을 그릴 때 약속된 태그들을 나열한다.
- 실제로 어떻게 렌더링 되느냐는 짜는 사람 입장에서 알 필요 없고, 무엇이 나와야 하는지를 선언하는 것이다.
명령형 vs 선언형
프론트엔드 개발에서 가장 많이 사용되는 자바스크립트 언어는 브라우저의 DOM 컨트롤을 위해 만들어졌다. 선언형 프로그래밍은 개발자가 원하는 결과물을 얻기 위한 명세에만 집중할 수 있게 해준다. 코드의 추상화 수준은 높아지고, 더 상위 레벨에서 프로그램의 흐름을 제어할 수 있다. 자바스크립트의
map
, filter
, reduce
와 같은 고차 함수들은 복잡한 반복문 없이 데이터 변환과 필터링, 축약 등을 선언적으로 표현할 수 있게 한다.선언적 방식으로 UI 컴포넌트 추상화하기
코드 예시
1-1. double 함수 만들기
배열을 받아 배열의 각 요소를 double시키는 함수를 만들자.
명령형
function double(arr) { let results = [] for (let i=0; i<arr.length; i++) { results.push(arr[i] * 2) } return results }
선언형
function double(arr) { arr.forEach((number, i, arr) => arr[i] = number * 2); return arr; } function double(arr) { return arr.map(number => number * 2); }
- forEach는 반환 값이 없다는 점 기억하기
1-2. double 함수 보완하기
명령형
function double(arr) { let results = [] for (let i=0; i<arr.length; i++) { if (typeof arr[i] === 'number') { results.push(arr[i] * 2) } } return results; }
- for문 안에 로직들이 점점 쌓이면 결국 추후에 코드의 맥락을 확인하기가 어려워진다.
선언형
function double(arr) { return arr.filter(item => typeof item === 'number') .map(number => number * 2); }
어떻게 처리하는지에 대해 묘사하면 명령형 프로그래밍 방식이고, 무엇을 원하는지에 대해 묘사하면 선언형 프로그래밍 방식으로 프로그래밍한다고 볼 수 있다.
2. 입력값 필터링하고 정렬하기
더미 데이터
const data = [ { name: '모나' colors: ['black', 'white'], age: 3, ear: 'unfolded' }, // ... etc ];
명령형
// 털 색이 까만색이 포함되어 있으면서 // 귀가 접혀있지 않은 고양이들을 뽑기 function filterCats(cats) { let results = [] for(let i=0; i<cats.length; i++) { const cat = cats[i]; if (cat && cat.colors.includes('black') && cat.ear === 'unfolded') { results.push(cat.name) } } return results; } const filteredCatsName = filterCats(data);
선언형
function filterCats(cats, color) { return cats.filter(cat => cat && cat.colors.includes(color) && cat.ear === 'unfolded') .map(cat => cat.name) } const filteredCatsName = filterCats(data, 'black');
3-1. 토글 버튼 만들기
요구사항
- 화면에 버튼을 3개 넣고, 버튼을 클릭하면 삭선이 그어지도록 만들기
명령형 방식으로 하기
// 버튼을 3개를 만든다. const $button1 = document.createElement('button'); $button1.textContent = 'Button1'; const $button2 = document.createElement('button'); $button2.textContent = 'Button2'; const $button3 = document.createElement('button'); $button3.textContent = 'Button3'; // 만든 버튼을 화면에 그린다. const $main = document.querySelector('main'); $main.appendChild($button1); $main.appendChild($button2); $main.appendChild($button3); // 버튼을 클릭하면 삭선이 그어진다. const toggleButton = ($button) => { if ($button.style.textDecoration === 'line-through') { $button.style.textDecoration = 'none'; } else { $button.style.textDecoration = 'line-through'; } } document.querySelectorAll('button').forEach($button => { $button.addEventListener('click', (e) => { const { target } = e; toggleButton(target); }) })
[컴포넌트 방식] ToggleButton이라는 이름으로 추상화하기
- ToggleButton의 기능, 동작 등을 하나로 묶어 독립적으로 묶는 과정
function ToggleButton({ $target, text }) { const $button = document.createElement('button'); $target.appendChild($button); $button.addEventListener('click', () => { if ($button.style.textDecoration === 'line-through') { $button.style.textDecoration = ''; } else { $button.style.textDecoration = 'line-through'; } }); this.render = () => { $button.textContent = text; }; this.render(); } const $app = document.querySelector('#app'); new ToggleButton({ $target: $app, text: 'Button1' }) new ToggleButton({ $target: $app, text: 'Button2' }) new ToggleButton({ $target: $app, text: 'Button3' })
- 필요한 버튼 하나를 만드는 데 필요한 코드가 ToggleButton에 추상화되어 있는 형태
- 버튼에 새로운 기능을 추가하거나 확장하는 것이 굉장히 편리해진다.
[컴포넌트 방식] 보완하기
function ToggleButton({ $target, text }) { const $button = document.createElement('button'); let isInit = false; this.render = () => { $button.textContent = text; if (!isInit) { $target.appendChild($button); $button.addEventListener('click', () => { if ($button.style.textDecoration === 'line-through') { $button.style.textDecoration = ''; } else { $button.style.textDecoration = 'line-through'; } }); isInit = true } }; this.render(); } const $app = document.querySelector('#app'); const button1 = new ToggleButton({ $target: $app, text: 'Button1' }) const button2 = new ToggleButton({ $target: $app, text: 'Button2' }) const button3 = new ToggleButton({ $target: $app, text: 'Button3' })
- 이전 코드는 render() 함수에 접근하면 render() 함수가 여러번 호출될 수가 있다.
- 만약 코드 밖에서 render() 함수를 통해 버튼을 다시 그릴 수 있게 하기 위해 render() 함수 내부로 이벤트 리스너와 appendChild를 넣어주자.
- 이 때, 플래그 변수 (isInit)을 넣어 초기화되지 않은 버튼과 초기화된 버튼을 판단하여 렌더링할 수 있게 한다.
3-2. 기능 추가하기
추가 요구사항
- 기능1. 3번 클릭할 때마다 alert 띄우기
- 기능2. ToggleButton 외에 5초 뒤에 자동 토글되는 버튼 만들기
- 기능3. ButtonGrouop 만들기
기능 1. 명령형 방식
const $button1 = document.createElement('button'); $button1.textContent = 'Button1'; $button1.className = 'button1'; let button1ToggleCount = 0; const $main = document.querySelector('main'); $main.appendChild($button1); document.querySelectorAll('button').forEach($button => { $button.addEventListener('click', (e) => { const { target } = e; if ($button.style.textDecoration === 'line-through') { $button.style.textDecoration = 'none'; console.log(target.className) if (target.className === 'button1') { button1ToggleCount++ target.textContent = `버튼1 토글 횟수: ${button1ToggleCount}` } } else { $button.style.textDecoration = 'line-through'; } }) })
- 이런 방식으로 작성해도 원하는 대로 결과가 출력되겠지만, 유지보수의 어려움과 버그 발생의 가능성이 높다.
컴포넌트화 시키는 것이 왜 선언적 프로그래밍인지?
기능1. [컴포넌트 방식] alert 띄우기
function ToggleButton({ $target, text }) { const $button = document.createElement('button'); let isInit = false; let clickCount = 0; this.render = () => { $button.textContent = text; if (!isInit) { $target.appendChild($button); $button.addEventListener('click', () => { if ($button.style.textDecoration === 'line-through') { $button.style.textDecoration = ''; } else { $button.style.textDecoration = 'line-through'; } if (clickCount % 3 === 0) { alert('3번째 클릭!'); } }); isInit = true } }; this.render(); } const $app = document.querySelector('#app'); const button1 = new ToggleButton({ $target: $app, text: 'Button1' })
- 3번 누를 때마다 alert 창이 뜬다.
- 그런데, 모든 버튼에 기능이 추가되지 않고 기능을 추가하고 싶은 버튼에만 주입할 순 없을까?
- ToggleButton에 정의하는 것이 아니라 밖에서 행위를 주입할 수 있도록 만들면 된다.
function ToggleButton({ $target, text, onClick }) { const $button = document.createElement('button'); $target.appendChild($button); let clickCount = 0 this.render = () => { $button.textContent = text; } $button.addEventListener('click', () => { clickCount++ if ($button.style.textDecoration === 'line-through') { $button.style.textDecoration = ''; } else { $button.style.textDecoration = 'line-through'; } if (onClick) { onClick(clickCount) } /*if (clickCount % 3 === 0) { alert('3번째 클릭!'); }*/ }); this.render(); } const $app = document.querySelector('#app'); const button1 = new ToggleButton({ $target: $app, text: 'Button1', onClick: (clickCount) => { if (clickCount % 3 === 0) { alert('3번째 클릭!') } } })
- 다른 버튼에는 3번째 클릭마다 alert을 띄우지 않고, onClick 함수를 전달한 인스턴스에만 기능이 전달되는 방식이다.
- 이런 식으로 확장을 해나가는 방법이 있다.
- 다른 버튼에는 다른 기능을 넣어줄 수 있다.
기능2. ToggleButton 외에 5초 뒤에 자동 토글되는 버튼 만들기
지금은 텍스트 데코레이션이 하나의 상태밖에 없지만, 이 상태가 늘어난다면 if문이 굉장히 복잡해질 가능성이 있다. 이 부분을 state와 setState라는 추상화를 통해 해결해보자.
function ToggleButton({ $target, text, onClick }) { const $button = document.createElement('button'); $target.appendChild($button); this.state = { clickCount: 0, toggled: false, } this.setState = (nextState) => { this.state = nextState this.render() } this.render = () => { $button.textContent = text; $button.style.textDecoration = this.state.toggled ? 'line-through' : 'none' } $button.addEventListener('click', () => { this.setState({ clickCount: this.state.clickCount + 1, toggled: !this.state.toggled }) if (onClick) { onClick(this.state.clickCount) } /*if (clickCount % 3 === 0) { alert('3번째 클릭!'); }*/ }); this.render(); } const $app = document.querySelector('#app'); const button1 = new ToggleButton({ $target: $app, text: 'Button1', onClick: (clickCount) => { if (clickCount % 3 === 0) { alert('3번째 클릭!') } } })
- 클릭한 카운트와 토글 여부를 상태로 가지고, setState로 다음에 올 상태를 받아 컴포넌트를 다시 렌더링한다.
- 컴포넌트의 상태를 기준으로 동작(렌더링)하도록 변경되었다.
- 이런식으로 작성하는 것이 조금 더 선언적 프로그래밍이 되면서 복잡도를 낮추게 된다.
타이머 버튼 추가하기 (ToggleButton을 가져와서 onClick 기능만 수정)
function TimerButton({ $target, text, timer = 3000 }) { const button = new ToggleButton({ $target, text, onClick: () => { setTimeout(() => { button.setState({ ...button.state, toggled: !button.state.toggled }) }, timer) } }) } function ToggleButton({ $target, text, onClick }) { const $button = document.createElement('button'); $target.appendChild($button); this.state = { clickCount: 0, toggled: false, } this.setState = (nextState) => { this.state = nextState this.render() } this.render = () => { $button.textContent = text; $button.style.textDecoration = this.state.toggled ? 'line-through' : 'none' } $button.addEventListener('click', () => { this.setState({ clickCount: this.state.clickCount + 1, toggled: !this.state.toggled }) if (onClick) { onClick(this.state.clickCount) } /*if (clickCount % 3 === 0) { alert('3번째 클릭!'); }*/ }); this.render(); } const $app = document.querySelector('#app'); const button1 = new ToggleButton({ $target: $app, text: 'Button1', onClick: (clickCount) => { if (clickCount % 3 === 0) { alert('3번째 클릭!') } } }) new TimerButton({ $target: $app, text: '3초 뒤에 자동으로!', timer: 1000 * 3 })
기능 3. Button Group 만들기
function ButtonGroup({ $target, buttons }) { const $group = document.createElement('div'); let isInit = false this.render = () => { if (!isInit) { buttons.forEach(({ type, ...props}) => { if (type === 'toggle') { new ToggleButton({ $target: $group, ...props }) } else if (type === 'timer') { new TimerButton({ $target: $group, ...props }) } }) $target.appendChild($group) isInit = true } } this.render() } const $app = document.querySelector('#app'); new ButtonGroup({ $target: $app, buttons: [ { type: 'toggle', text: '토글 버튼' }, { type: 'toggle', text: '토글 버튼' }, { type: 'timer', text: '타이머', timer: 1000 } ] })
- 재사용성, 코드 공유가 쉬워진다.
- 외부의 요인에 의해 제어되지 않고, 독립된 컴포넌트로 동작한다.
- 상태를 기반으로 추상화되어 있는 UI를 만드는게 중요하다.
- 이 부분을 제대로 이해한다면 리액트, 앵귤러, 스벨트, 뷰 등의 프레임워크를 너무나 쉽게 이해할 수 있을 것이다.
- UI를 다루는 방식을 선언적인 방식으로 사고방식을 바꾸어야 한다.
- 단순히 바닐라JS만으로도 프레임워크를 대체 가능하다.
프로그래밍 패러다임
- 명령형 프로그래밍과 선언형 프로그래밍 (현재 글)
- 함수형 프로그래밍과 반응형 프로그래밍