데브코스 프론트엔드 5기/VanillaJS를 통한 자바스크립트 기본 역량 강화 1

231005[Day13] VanillaJS를 통한 자바스크립트 기본 역량 강화 1 (2)

코딩하는 키티 2023. 10. 6. 14:25

 

명령형 프로그래밍과 선언적인 프로그래밍 방식의 이해

명령형 프로그램

  • 컴퓨터가 수행할 명령들을 순서대로 써 놓은 것
  • "어떻게 구현하는가"를 디테일하게 기술하는 것에 관점

 

선언형 프로그램

  • 프로그램이 어떤 방법으로 해야하는 지가 아닌 무엇과 같은 지를 설명하는 경우
  • 웹페이지는 선언형 -> 제목, 글꼴, 본문, 그림과 같이 "무엇"이 나타나야하는지를 묘사하는 것이기 때문
  • HTML, SQL

 

명령형 프로그래밍과 선언적인 프로그래밍 방식: 예시 1-1

명령형 방식

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

 

선언형 방식

  • 코드가 간결하며, 유지보수하기 쉽다.
function double(arr) {
  return arr.map(number => number*2)
}

 

명령형 프로그래밍과 선언적인 프로그래밍 방식: 예시 1-2

만약 배열에 숫자가 아닌 문자열이 들어올 때 추가 보완

 

명령형 방식

  • 조건이 많아질수록 더 복잡해진다
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(ele => typeof ele === number)
            .map(number => number*2)
}

 

명령형 프로그래밍과 선언적인 프로그래밍 방식: 예시 2

명령형 방식

const filterCats = (cats) => {
	let results = []
    for(i=0; i<cats.length; i++){
    	const cat = cats[i]
        if(cat){
        	cat.colors.includes('black') &&
            cat.ear === 'unfolded'){
            
         results.push(value.name)
        }
     }
      
      return results
}

 

선언형 방식

const filterCats = (cats) => {
	return cats.filter(
    			cat => 
        			cat &&
            		cat.colors.include('black') &&
           			cat.ear === 'unfolded'
            ).map(cat => cat.name)
}

 

 

토글 버튼 만들기 : 예시 3-1

명령형 방식

// 버튼 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('body');
$main.appendChild($button1);
$main.appendChild($button2);
$main.appendChild($button3);

// 버튼을 클릭하면 삭선이 그어진다.
const toggleButton = ($button) => {
	if($button.style.textDecoration === ''){
    	$button.style.textDecoration = 'line-through'
    } else{
     $button.style.textDecoration = ''
    }
}    
document.querySelectorAll('button').forEach($button => {
  $button.addEventListener('click', (e) => {
    toggleButton(e.target)
}))

 

선언형 방식(추상화)

선언형 방식은 기능 추가시 ToggleButton 내 작성하면 되기 때문에 확장성 면에서 유리하다. 

// 버튼 3개를 만든다
function ToogleButton({
  $target,
  text
}){
  const $button = document.createElement('button');
  let isInit = false
  
  this.render = () => {
    $button.textContent = text;
  }

 	if(!isInit){
    	$target.appendChild($button)
        
          $button.addEventListener('click', (e) => {
            if(e.target.style.textDecoration === 'line-through'){
              e.target.style.textDecoration = 'none';
            }else {
              e.target.style.textDecoration = 'line-through';
            }
          })
          isInit = true
          }

 	 this.render();
}

const $app = document.querySelector('body');

new ToogleButton({
  $target: $app,
  text: 'Button1'
})

new ToogleButton({
  $target: $app,
  text: 'Button2'
})
new ToogleButton({
  $target: $app,
  text: 'Button3'
})

3번 클릭시 나타나는 이벤트

 

+ 함수내 객체 상태를 지정해 줄수 있다.

function ToggleButton({ $target, text, onClick }) {
  const $button = document.createElement("button");

  let isInit = false;

  // 상태 초기화
  this.state = {
    clickCount: 0,
    toggled: false,
  };

  // 03 상태가 변경되면 리렌더링한다
  this.setState = (nextState) => {
    this.state = nextState; // 상태 변경
    this.render(); // 리렌더
  };

  // 04 렌더 함수는 현재 상태를 기준으로 렌더링을 한다
  this.render = () => {
    $button.textContent = `${text} (${this.state.clickCount})`;

    $button.style.textDecoration = this.state.toggled ? "line-through" : "none";
    $button.style.fontWeight = this.state.toggled ? "" : "700";

    if (!isInit) {
      $target.appendChild($button);

      isInit = true;
    }
  };

  // 01 이벤트가 발생하면
  $button.addEventListener("click", () => {
    // 02 상태를 변경하고
    this.setState({
      clickCount: this.state.clickCount + 1,
      toggled: !this.state.toggled,
    });

    // 개별 행동도 마찬가지로 현재 상태를 기준으로 행동을 한다
    if (onClick) {
    	onClick(this.state.clickCount)
    }    
  });

  this.render();
}

const $app = document.querySelector("#app");

new ToggleButton({
  $target: $app,
  text: "버튼1",
  onClick: (clickCount) => {
    if (clickCount % 2 === 0) {
      alert("2번째 클릭!");
    }
  },
});

new ToggleButton({
  $target: $app,
  text: "버튼2",
  onClick: (clickCount) => {
    if (clickCount % 3 === 0) {
      alert("3번째 클릭!");
    }
  },
});

new ToggleButton({
  $target: $app,
  text: "버튼3",
});
  • 이벤트가 발생하면 UI를 바로 변경하는게 아니라 상태를 변경하게 되었다.
  • 상태가 변경되면 자동으로 리렌더링하게 되었다.
  • UI를 변경하는 토글 함수 코드가 렌더 함수 내부로 이동했다.

즉, 컴포넌트가 “상태가 변경되면 자동으로 리렌더링”하는 일련의 흐름을 가지게 되었다. 코드가 일련의 흐름을 가지면 코드의 유지보수와 확장이 쉬워진다. 원하는 로직이 어디에 있는지 더 쉽게 알 수 있기 때문이다.

이전처럼 DOM을 여기저기서 수정할 필요가 없어졌다. DOM 수정은 UI와 관련된 코드이므로 렌더 함수 안에서 찾고 변경하면 된다.

 

- 새로운 기능을 추가하고 싶다면 해야할 일

  • 이벤트 발생에 따라 상태를 어떻게 변경하면 될지 코드를 작성한다.
  • 상태에 따라 UI를 어떻게 변경하면 될지 코드를 작성한다.

 

5초 뒤에 자동으로 토글되는 버튼을 만드세요.

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");

  let isInit = false;

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

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

  this.render = () => {
    $button.textContent = `${text} (${this.state.clickCount})`;

    $button.style.textDecoration = this.state.toggled ? "line-through" : "none";

    if (!isInit) {
      $target.appendChild($button);

      isInit = true;
    }
  };

  $button.addEventListener("click", () => {
    this.setState({
      clickCount: this.state.clickCount + 1,
      toggled: !this.state.toggled
    });

    if (onClick) {
    	onClick(this.state.clickCount)
    }    
  });

  this.render();
}

const $app = document.querySelector("#app");

new ToggleButton({
  $target: $app,
  text: "버튼1",
  onClick: (clickCount) => {
    if (clickCount % 2 === 0) {
      alert("2번째 클릭!");
    }
  },
});

new ToggleButton({
  $target: $app,
  text: "버튼2",
  onClick: (clickCount) => {
    if (clickCount % 3 === 0) {
      alert("3번째 클릭!");
    }
  },
});

new ToggleButton({
  $target: $app,
  text: "버튼3",
});

new TimerButton({
  $target: $app,
  text: "3초 타이머",
});

new TimerButton({
  $target: $app,
  text: "10초 타이머",
  timer: 10 * 1000,
});

 

인자로 type과 timer 값을 입력하면 자동으로 토글 버튼이나 타이머 버튼을 추가하는 ButtonGroup 컴포넌트

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();
}

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");

  let isInit = false;

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

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

  this.render = () => {
    $button.textContent = `${text} (${this.state.clickCount})`;

    $button.style.textDecoration = this.state.toggled ? "line-through" : "none";

    if (!isInit) {
      $target.appendChild($button);

      isInit = true;
    }
  };

  $button.addEventListener("click", () => {
    this.setState({
      clickCount: this.state.clickCount + 1,
      toggled: !this.state.toggled,
    });

    if (onClick) {
    	onClick(this.state.clickCount)
    }    
  });

  this.render();
}

const $app = document.querySelector("#app");

new ButtonGroup({
  $target: $app,
  buttons: [
    {
      type: "toggle",
      text: "토글 버튼",
    },
    {
      type: "timer",
      text: "타이머 버튼",
      timer: 1000,
    },
  ],
});