개발새발 로그

선언형 프로그래밍 방식을 바닐라 자바스크립트로 배워보자 본문

자바스크립트

선언형 프로그래밍 방식을 바닐라 자바스크립트로 배워보자

이즈흐 2024. 5. 18. 20:48

요새는 리액트만 공부해서 그런지 이런 고민이 생겼다. 

"자바스크립트로 이런 기능 한번 만들어보세요!" 했을 때 어떻게 할 것인가?

기능을 구현할 수야 있겠지만 과연 그 기능을 남들이 봤을 때 "옳은 방식"으로 만들까?

 

다시한번 자바스크립트를 공부하려고 마음을 먹었고, 내가 과거에 배웠던 자바스크립트를 다시 꺼내왔다.

이번에 두 번째로 보는 것인데 다시 복습하면서 중요한 점들을 상기시키려고 한다.

 

 

📖명령형 프로그래밍 방식과 선언형 프로그래밍 방식

명령형 프로그래빙 방식이란?

프로그래밍의 상태와 상태를 변경시키는 구문의 관점에서 연산을 설명하는 프로그래밍 패러다임의 일종이다.

 

자연 언어에서의 명령법이 어떤 동작을 할 것인지를 명령으로 표현하듯이, 명령형 프로그램은 컴퓨터가 수행할 명령들을 순서대로 써 놓은 것이다.

 

쉽게 말해 어떻게(How) 할 건지를 설명하는 프로그래밍 방식이라고 한다.

 

 

선언형 프로그래밍 방식이란?

원하는 결과를 묘사하는 방식으로 코드를 작성하는 프로그래밍 패러다임의 일종이다.

 

쉽게 말해 무엇(What)을 할 건지를 설명하는 프로그래밍 방식이라고 한다.

대표적인 선언형 프로그래밍 방식이 HTML이라고한다.

선언형 프로그래밍 스타일로 코드를 작성하면 전체적인 가독성과 추상화 수준을 높여 개발자가 문제의 본질에 집중할 수 있도록 도와줍니다.
또한 이러한 작업을 통해 재사용성이 높고 병렬처리가 유리한 특징을 가지게 됩니다.

 

 

 

👨‍💻간단한 명령형과 선언형 프로그래밍 방식

function double(arr) {
  let result = []
  for (let i = 0; i < arr.length; i++) {
    result.push(arr[i] * 2)
  }
  return result
}

document.querySelector("body").innerHTML = double([1, 2, 3])

위 코드가 명령형 프로그래밍 방식의 예시다.

코드가 어떻게 흘러가는지 하나 하나 작성한 모습이다.

 

그럼 이걸 선언형 프로그래밍 방식으로 바꾸면 아래와 같다.

function double(arr) {
  return arr.map(number => number * 2)
}

map이라고 정의된 함수를 이용해 과정을 함축시켰다.

무엇을 원하는지에 대한 묘사를 한 것이다.

 

 

그럼 아래와 같은 명령형 코드는 어떻게 선언형으로 바꿀 수 있을까?

function double(arr) {
  let result = []
  for (let i = 0; i < arr.length; i++) {
    if(typeof arr[i] === "number"){ // 타입 검사 추가
      result.push(arr[i] * 2)
    }
  }
  return result
}

타입 검사가 추가된 코드이다.

위 코드는 아래와 같이 선언형으로 바꿀 수 있다.

function double(arr) {
  return arr
    .filter(param => typeof param === 'number')
    .map(number => number * 2)
}

좀 더 코드가 간단 명료해졌다.

 

 


이렇게 선언형프로그래밍과 명령형 프로그래밍을 간단히 알아보았다.

어쨌거나 명령형 프로그래밍 방식은 좋지않은 부분이 존재한다.

그럼 실제로 html요소에 넣을 기능을 만들 때 어떻게 해야할까?

 

나는 과거에 컴포넌트 방식으로 기능을 구현했었다.

이번에 다시 컴포넌트 방식을 정리하면서 복습하려고 한다.

 

👨‍💻컴포넌트 방식이란..

먼저 어떤 기능을 만들 것이냐면

간단하게 버튼을 만들고,

버튼을 화면에 그리고,

클릭하면 취소선이 그려지는 기능을 만들어보려고 한다.

 

먼저 명령형으로 만든다고 하면 어떻게 코드를 작성하게 될까?

// 버튼을 만든다.
const $button1 = document.createElement("button");
$button1.textContent = "Button1"

// 만든 버튼을 화면에 그린다.
const $main = document.querySelector("#app")
$main.appendChild($button1);

// 토글 버튼 함수
const toggleButton = ($button) => {
  if($button.style.textDecoration === "line-through"){
    $button.style.textDecoration = "none"
  }else{
    $button.style.textDecoration = "line-through"
  }
}

// 버튼을 클릭하면 취소선이 그어진다.
$button1.addEventListener("click",()=>{
    toggleButton($button1);
})

 

명령형 프로그래밍은 위 코드처럼 작성해볼 수 있다.

근데 만약 여기서 버튼이 여러개라면?
아마도 위 코드를 여러개 중복해서 만들어야 할 것이다.

그리고 코드가 어떻게 돌아가는지 예측하기 힘들어진다.

버그 또한 찾기 어려울 것이다.

 

참고로 버튼이 많다면 이벤트를 등록하는 부분은 아래와 같이 할 수있다.

(그래도 여전히 기능이 확장되면 가독성이 안좋아짐)

querySelectorAll("button").forEach((button)=>{ ... });

 

 

이걸 toggleButton이라는 함수로 추상화할 것이다.

바로 코드를 보

function ToggleButton({$target, text}){
  const $button = document.createElement("button")// 버튼이 생긴다.
  let isInit = false //render 함수가 밖에서 여러번 호출될 수 있으므로 초기화를 한번만 수행하도록 하기위함

  this.toggle = ()=>{
    if($button1.style.textDecoration === "line-through"){
      $button1.style.textDecoration = "none"
    }else{
      $button1.style.textDecoration = "line-through"
    }
  }

  // 렌더 함수에서 필요한 로직을 수행한다.
  this.render = ()=>{
    $button.textContent = text

    if(!isInit){
      $target.appendChild($button)
      $button.addEventListener("click", ()=>{
        this.toggle()
      })
      isInit = true
    }
  }
  this.render(); // render를 바로 실행한다.
}

const $app = document.querySelector("#app")
const button1 = new ToggleButton({
  target : $app,
  text : "버튼1"
})
const button2 = new ToggleButton({
  target : $app,
  text : "버튼2"
})
const button3 = new ToggleButton({
  target : $app,
  text : "버튼2"
})

아까 명령형 프로그래밍처럼 기능이 여기저기 흩어져있는게 아닌 한 곳에서 모두 처리하고 있다.

그리고 render() 함수를 이용하는데 이를 통해 렌더링 되는 시점이 명확하다.

 

하나의 개념으로 추상화 한 것이다.

이 코드를 흔히 컴포넌트 방식으로 추상화한다고 한다.

이 코드는 나중에 기능을 추가하거나 확장할 때도 용이하다.

 

그럼 예시로 3가지의 기능을 추가하면서 컴포넌트 방식에 대해서 더 자세히 알아보자

🛠️기능 추가하기 1 : 3번 클릭할 때마다 alert 띄우기

3번 클릭할 때마다 alert를 띄운다고 했을 때 아래와 같이 구현하면 되지않을까? 생각할 수 있을 것이다.

function ToggleButton({ $target, text }) {
  const $button = document.createElement('button')
  let isInit = false
  let clickCount = 0; // 클릭카운트 상태

  this.toggle = () => {
    clickCount++ // 카운트 증가
    if ($button.style.textDecoration === 'line-through') {
      $button.style.textDecoration = 'none'
    } else {
      $button.style.textDecoration = 'line-through'
    }

	// alert 실행
    if(clickCount % 3 === 0){
      alert("3번째 클릭!")
    }
  }

  this.render = () => {
    $button.textContent = text

    if (!isInit) {
      $target.appendChild($button)
      $button.addEventListener('click', () => {
        this.toggle()
      })
      isInit = true
    }
  }
  this.render()
}

 

이때 만약 모든 버튼에 해당 기능을 넣는 것이 아닌 특정한 버튼에만 해당 기능을 넣으려면 어떻게 해야할까?

이럴때는 함수를 호출하는 밖에서 그 행위를 주입하면 된다.

function ToggleButton({ $target, text, onclick }) { // onClick을 받아올 수 있도록 추가해줬다.
 
  // ...
  
  let clickCount = 0;

  this.toggle = () => {
    clickCount++ 
    
    //...

    if(onclick){
      onclick(clickCount)
    }
  }

// ...

const button1 = new ToggleButton({
  target: $app,
  text: '버튼',
  onclick: (clickCount)=>{ // 특정한 버튼에서만 alert기능을 수행한다.
    if(clickCount % 3 === 0){
      alert("3번째 클릭!")
    }
  }
})

 

이제 위 코드에서 수정할 부분이 하나 있다.

ToggleButton 컴포넌트의 상태를 만들어야 한다.

 

지금 코드에서 클릭하면 취소선을 나타내는 기능이 그저 text.Decoration이 존재하냐 안하냐로 구분하고 있다.

이런 식으로 하는 것 보다 UI의 상태를 추상화하고, 해당 상태에 따라서 render함수가 따라가도록 해야한다.

 

에를 들어 bold 스타일을 추가하거나 했을 때 if문을 textDecoration처럼 똑같이 추가해줘야 할 것이다.

그럼 if문이 점점 복잡해질 것이다.

 

그래서 아래와 같은 코드를 컴포넌트에 추가해줄 것이다.

  this.state = {
    clickCount: 0,
    toggled: false
  }

  this.setState = nextState => {
    this.state = nextState
    this.render()
  }

리액트를 많이 써봤다면 많이 봤을 코드이다.

실제로 나는 컴포넌트 방식으로 자바스크립트를 구현할 때 render, state, setState를 먼저 만들고 시작한다.

거의 '국룰" 처럼 사용하고 있는 방식이다.

 

그럼 위 코드를 어떻게 사용하는지 다시 코드로 보자

function ToggleButton({ $target, text, onclick }) {
  const $button = document.createElement('button')

  //상태 생성
  this.state = {
    clickCount: 0,
    toggled: false,
    isInit: false
  }

  this.setState = nextState => {
    this.state = nextState
    //상태를 업데이트하고 다시 렌더링을 수행한다.
    this.render()
  }

  this.toggle = () => {
    // 상태로 기능 관리
    if (this.state.toggled) {
      $button.style.textDecoration = this.state.toggled
        ? 'line-through'
        : 'none'
    }
    // 상태 업데이트
    this.setState({
      clickCount: this.state.clickCount + 1,
      toggled: !this.state.toggled
    })

    if (onclick) {
      onclick(this.state.clickCount)
    }
  }

  this.render = () => {
    $button.textContent = text

    if (!this.state.isInit) {
      $target.appendChild($button)
      $button.addEventListener('click', () => {
        this.toggle()
      })
      this.state.isInit = true
    }
  }
  this.render()
}

const $app = document.querySelector('#app')
const button1 = new ToggleButton({
  target: $app,
  text: '버튼',
  onclick: clickCount => {
    if (clickCount % 3 === 0) {
      alert('3번째 클릭!')
    }
  }
})

UI에 접근하는 것이 아니라 상태를 기반으로 렌더링 하는 구조가 되었다.

이 방식이 조금 더 선언적인 방식이되면서 복잡도를 낮추게 되었다.

DOM을 여기저기서 접근하는 것을 최대한 제한 시킨 것이다.

 

🛠️기능 추가하기 2 : ToggleButton 이외에 5초 뒤에 토글되는 버튼 만들기

이제 기존 ToggleButton을 확장한 예시를 보여주려고 한다.

이 부분은 바로 코드로 보면 이해가 빠르게 될 것이다.

function TimerButton({ $target, text, timer = 3000 }) {
  const button = new ToggleButton({
    $target,
    text,
    onClick: () => {
      setTimeout(() => {
        button.setState({
          ...button.state,
          toggled: !button.state.toggled
        })
      }, timer)
    }
  })
}

new TimerButton({
  $target: $app,
  text: '3초뒤에 토글됩니다.'
})

기존의 ToggleButton에서 기능을 확장한 것이다.

 

 

🛠️기능 추가하기 3 : Button Group 만들기

해당 기능 또한 기존의 ToggleButton을 확장해보려고한다.

코드를 보면 빠르게 이해될 것이다.

function ButtonGroup({ $app, 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 })
        }
      })
      $app.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
    }
  ]
})

이 코드도 TimerButton과 같이 ButtonGroup을 사용하는 하단의 코드 부분을 보면 실제로 ButtonGroup이 어떻게 구현되는지는 모른다. 관심사가 아닌 것이다.

 

그리고 ButtonGroupt을 사용할 때 인터페이스에 맞춰서 사용하면 재사용성이 쉬워진다.

또한 외부에 어떤 요인에 의해서 결정되는 것이 아니라 $target과 buttons라는 파라미터로 격리된 값들에 의해서만 결정된다.

 

궁극적으로 이런 개념이 있어야 리액트나 Vue를 사용할 때 함정에 빠지지않는다.

이 과정으로 사고방식을 어떻게 해야할지 감을 잡아보는 것이었다.

 

 

 

📘마무리하며..

일단 자바스크립트로 선언적으로 어떻게 코드를 짜는지 컴포넌트 방식은 무엇인지 알아보았다.

나는 예전에 배웠던 내용인데도 이렇게 정리하고 나서야 제대로 기억할 수 있었다.

이후 더 내용이 있다. 

오늘은 여기까지만 하지만 이후 내용을 더 공부해서 자바스크립트 능력을 다시 올려보려고 한다.

728x90
반응형
LIST