nightmare까지 다 완성하긴 했는데 reference랑 비교하니까 바보 같이 기뻐할 때가 아니란 걸 깨달았다...
const calculator = document.querySelector('.calculator');
const buttons = calculator.querySelector('.calculator__buttons');
const firstOperend = document.querySelector('.calculator__operend--left');
const operator = document.querySelector('.calculator__operator');
const secondOperend = document.querySelector('.calculator__operend--right');
const calculatedResult = document.querySelector('.calculator__result');
document.getElementById(id)
: id와 일치하는 dom 요소(Element 객체
)를 반환. 일치하는 요소가 없으면 null
.document.getElementsByClassName(names)
document.getElementsByClassName("test");
document.getElementsByClassName("red test");
: red와 test 클래스를 모두 가진 요소document.getElementById("main").getElementsByClassName("test");
: main 요소 중 test 클래스를 가진 요소document.getElementsByClassName("test")[0];
: test 클래스를 가진 요소 중 가장 첫 번째 요소document.getElementsByName(name)
: name 속성과 일치하는 dom 요소 반환document.getElementsByTagName(name)
: 해당 tag인 dom 요소 반환🌠 checkpoint # querySelector 와 getElementById 차이
🔵 getElementById
element = document.getElementById(id);
- id명과 일치하는 엘리먼트 객체를 반환한다.
🔴 querySelector
element = document.querySelector(selectors);
- 선택자와 일치하는 첫 번째 엘리먼트 객체를 반환한다.
🔵 getElementByClassName
elements = element.getElmentsByClassName(names);
- class명과 일치하는 엘리먼트들의 HTMLCollection을 반환한다.
🔴 querySelectorAll
elementList = parentNode.querySelectorAll(selectors);
- 선택자와 일치하는 엘리먼트들의 NodeList을 반환한다.
<form id="productForm"> <input id="productOne" class="product" type="text" value="Product 1" /> <input id="productTwo" class="product" type="text" value="Product 2" /> <input id="productThree" class="product" type="text" value="Product 3" /> </form>
// getElementByClassName 🔵 const products = document.getElementsByClassName("product"); // querySelectorAll 🔴 const products = document.querySelectorAll("#productForm .product");
🔵 HTMLCollection
- 동적이다. DOM에 새로운 요소(Element)가 추가되면 HTMLCollection은 새로운 요소를 가져온다.
div
span
등과 같은 요소 노드만 포함. 텍스트 노드는 포함하지 않음.document.getElementById("parent").children;
자식 요소 접근 가능.
- 자식 요소 노드 접근)
parentDivp[2]
: 인덱스로 접근
parentDiv.namedItem("id");
: 아이디로 접근
parentDiv.namedItem("name");
: 이름으로 접근- 유사 배열 객체. 반복문으로 요소 접근할 때
for...of
문을 사용.forEach()
사용 불가.🔴 NodeList
- 정적이다. 새로운 요소가 추가되어도 새로운 요소를 가져오지 않는다.
- 요소 노드, 속성 노드, 텍스트 노드 등을 포함.
namedItem()
메서드를 가지고 있지 않아 자식 요소 노드로 접근 불가능.- 유사 배열 객체.
forEach()
메서드 존재.
🎁 HTMLCollection/NodeList 차이점
🎁 getElementById/querySelector 차이점
buttons.addEventListener('click', function(event) {
cosnt target = event.target;
const action = target.classList[0]
const buttonContent = target.textContent;
...
}
addEventListener(event, func, option);
: 이벤트를 등록하는 가장 권장되는 방식(이벤트 리스너). a.addEventListener('click', ()=> { });
dom.removeEventListener(event, func)
: 이벤트 리스너 삭제. 더 이상 이벤트가 필요 없을 경우 메모리 누수를 방지해 반드시 삭제하자.element.classList
: element의 class 속성을 보여준다. css class의 현재 값을 반환한다.elment.classList.toggle(class)
: 0과 1이 반복. 클래스가 존재한다면 클래스를 제거하고, 클래스가 존재하지 않는다면 클래스를 추가함.element.classList.add(class)
: 명시된 클래스를 추가element.classList.remove(class)
: 명시된 클래스를 제거element.textContent
: 해당 요소의 텍스트 노드🌠 자주 쓰는 이벤트
- 포커스 이벤트(focus, blur)
- 폼 이벤트(reset, submit)
- 뷰 이벤트(scroll, resize)
- 키보드 이벤트(keydown, keyup)
- 마우스 이벤트(mouseenter, mouseover, click, dbclick, mouseleave)
- 드래그 앤 드롭 이벤트(dragstart, drag, dragleave, drop)
🌠 textContent / innerHTML의 차이
- textContent : 더 먼저 만들어졌으며 브라우저 호환성이 더 높다. 미세하게 더 가볍기도. 스페이스 공백을 앞,뒤,사이 할 것 없이 다 가져온다.(ie에서. 크롬에선 innerHTML이랑 똑같음)
- innerHTML : 앞뒤 공백을 제거하고 가져옴.
if (target.matches('button')) { // 클릭 된 요소가 버튼 요소가 맞는지 확인
if (action === 'number') {
...
}
}
target
: 클릭 이벤트가 발생한 버튼 요소
dom.matches()
: 요소에 해당 선택자가 있는지 확인하고 true/false 반환
action
: target의 클래스 리스트 중 0번째 클래스
<button class="number">
<button class="operator">
<button class="clear">
<button class="decimal">
html 파일을 보면 모든 버튼 요소들이 종류 별로 클래스가 정해져 있다. action
이 'number'이면 숫자 버튼, 'operator'면 연산 버튼이다. 버튼을 저렇게 정리해놓으니까 깔끔하고 보기 좋아서 앞으로 참고 해야겠다.
그럼 이제 숫자 버튼이 클릭 되었을 때 계산기의 ■ + ■ = □ 안에 채워지게 만들어야 한다.
if (action === 'number') {
if(firstOperend.innerText === '0') {
firstOperend.innerText = buttonContent;
} else
secondOperend.innerText = buttonContent;
}
firstOerend
(첫 번째 숫자 칸)가 '0'(기본값)이면, 아직 첫 번째 숫자가 입력되지 않은 것이므로 클릭한 버튼의 숫자를 firstOperend.innerText
에 넣는다.
'0'이 아닌 경우, 첫 번째 숫자 칸에는 숫자가 입력 되었으므로 secondOperend.innerText
에 클릭한 버튼의 숫자인 buttonContent
를 할당한다.
if (action === 'operator') {
operator.innerHTML = buttonContent;
console.log('연산자 ' + buttonContent + ' 버튼');
}
클릭 한 연산자 버튼의 텍스트 노드를 □ + □ = □ 의 연산자 요소인 operator
의 inner.HTML에 할당한다.
if (action === 'calculate') {
let result = calculate(firstOperend.innerText, operator.innerText, secondOperend.innerText);
calculatedResult.innerText = result;
console.log('계산 버튼');
}
firstNumber
secondNumber
변수에 각각 Number 형변환을 한 첫 번째 값/두 번째 값을 저장한다. 계산은 calculate
함수에서 실행한다.
아래는 calculate
함수이다.
function calculate(n1, operator, n2) {
let result = 0;
if(operator === '+')
result = n1+n2;
else if(operator === '-')
result = n1-n2;
else if(operator === '*')
result = n1*n2;
else if(operator === '/')
result = n1/n2;
return String(result);
}
이미 nightmare test 와 어느정도 뒤섞인 코드다. 지금 보니까 계산만 정상 작동할 뿐 어설픈 부분이 중간중간 보인다. 레퍼런스랑 비교하면서 정리해보고자 한다.
let firstNum=null, operatorForAdvanced, previousKey, previousNum; // 기본 (null 초기화는 내가 함)
// 아래는 내가 추가
let secondNum=null, clickedOperator;
let isOperatorIsOn = false;
let isFirstInput = true;
let isSecondInput = false;
firstNum
: 첫 번째 입력 값
operatorForAdvanced
: 클릭한 연산자 종류
previousKey
: 바로 이전에 클릭한 버튼
previousNum
: 이전 결과값
secondNum
: 두 번째 입력 값
isOperatorIsOn
: operator가 이미 클릭 된 상황인지 판단
isFirstInput
: 첫 번째 값을 입력하는 중인지
isSecondInput
: 두 번째 값을 입력하는 중인지
지금 보니까 내가 추가한 변수들은 전부 없어도 잘만 구현할 수 있었다..
🔵 reference : 버튼 이벤트 리스너 안에 const buttonContainerArray = buttons.children;
문을 추가하였다. 용도는 천천히 코드를 내려가면서 확인하자.
🔴 me
if (action === 'number') {
let result = ''; // 최종 입력 숫자
if(previousKey === 'operator' || isSecondInput){ // 두 번째 값을 입력 할 때라면
isFirstInput = false; // 첫 번째 값 입력은 x
if(secondNum === null && display.textContent !== '.') // 두 번째 값을 아직 아무것도 입력 안 했고 '.' 도 입력 안 했다면
display.textContent = '0'; // display 초기화
} else { // 첫 번째 값을 입력 할 때라면
isFirstInput = true; // 첫 번째 값 입력 허용
firstNum = null; secondNum = null; // 계산의 첫 시작이라는 것이므로, 첫 번째 값/두 번째 값 초기화
}
// 숫자가 아무 것도 입력 안 됐거나 || 이전에 누른 버튼이 Enter 거나 || 이전에 누른 버튼이 AC면
if(display.textContent === '0' || previousKey === 'calculate' || previousKey === 'clear'){
display.textContent = buttonContent; // 화면에 있는 숫자를 모두 지우고 다시 씀
}else // 첫 번째 입력이 아니면('5'3 + '2'1 에서'5'와 '2'의 입력이 아니면)
display.textContent = display.textContent.concat(buttonContent); // 이어 붙이기
previousKey = 'number'; // 이전 키를 number로 변경
result = Number(display.textContent); // 만들어진 숫자를 타입 변환 하여 result에 저장
if(isFirstInput) // 첫 번째 값이라면
firstNum = result; // firstNum에 저장
else secondNum = result; // 두 번째 값이라면 secondNum에 저장
}
아 복잡하다. 출국 전날 급하게 막무가내로 욱여넣은 가방을 펼쳐 보는 기분이다.
🔵 reference
if (action === 'number') {
// 아직 아무것도 입력 안 했거나 || 이전에 누른 버튼이 연산 버튼이거나 || 이전에 누른 버튼이 Enter 이면
if (display.textContent === '0' || previousKey === 'operator' || previousKey === 'calculate') {
display.textContent = buttonContent; // 화면에 완전히 새로 쓴다
} else { // 그게 아니면
display.textContent = display.textContent + buttonContent; // 이어붙인다.
}
previousKey = 'number'; // 이전 키 값을 number로 변경한다.
}
(이렇게 간단할 일인가?)
🔴 me
if (action === 'operator') {
// 연산자 버튼이 이미 한 번 눌렸었고 && 이전에 누른 버튼이 숫자라면 (다항 연산이라면)
if(isOperatorIsOn && previousKey==='number'){
// 앞서 입력한 두 항을 먼저 계산한 다음 firstNum으로 교체시켜 준다. (2+3)+4 = 5+4
firstNum = Number(calculate(firstNum, operatorForAdvanced, secondNum));
secondNum = null; // 계산 된 두 번째 값은 초기
}
isOperatorIsOn = true; // 연산자 한 번 이상 눌렸음을 체크
target.style.backgroundColor = '#00da75'; // 클릭한 연산자 버튼 색상 변경
operatorForAdvanced = buttonContent; // 클릭한 연산자 종류 저장
previousKey = 'operator'; // 이전 키 값 operator로 변경
clickedOperator = target; // 클릭 한 연산자 버튼 요소를 저장
isSecondInput = true; // 이제 두 번째 값을 입력할 차례임을 설정
}
<문제점>
다항 연산을 할 경우 버튼 색상을 변경하지 않았다. 동시에 여러 연산자 버튼의 색상이 다 변하게 되며, Enter 버튼을 눌렀을 때 마지막으로 클릭한 버튼 색상만 검은색으로 돌아온다.
다항 연산을 할 때, 2+2+ 까지 눌렀을 때 앞의 2+2가 계산 된 4가 출력되지 않고, 그냥 계속 입력한 값만 나오게 만들어 버렸다. UI 센스가 없는 건지 수포자라 계산기 안 두드린 티가 나는 건지...
🔵 reference
if (action === 'operator') {
target.classList.add('isPressed'); // 클릭한 연산자 버튼에 isPressed 라는 클래스 추가
// 첫 번째 입력 값이 존재하고 && 연산 버튼도 클릭했고 && 이전에 누른 버튼이 operator가 아니고 && 이전에 누른 버튼이 Enter 가 아니면
if (firstNum && operatorForAdvanced && previousKey !== 'operator' && previousKey !== 'calculate') {
// 다항식에서 앞 두 숫자를 먼저 연산 후 출력 (4+4+ 까지 입력했을 경우 화면에 8 출력)
display.textContent = calculate(firstNum, operatorForAdvanced, display.textContent);
}
firstNum = display.textContent; // 4+ 까지만 입력 하면 firstNum은 그대로인 셈. 다항 연산 했을 경우 변경 된 firstNum을 저장하게 된다.
operatorForAdvanced = buttonContent; // 클릭한 연산자 저장
previousKey = 'operator'; // 이전 키 값 변경
}
firstNum
을 변경하는 부분에서 충격을 받았다. 나는 왜 저걸 굳이 if 조건문 안에 넣으려고 했을까? 화면을 계산 값으로 바꾸면 될 걸.
🔴 me
if (action === 'calculate') {
let result = 0; // 최종 계산 값
isOperatorIsOn = false; // Enter 버튼을 눌렀으니까 연산자 클릭했다는 flag는 초기화
// 3항 이상 다항 연산은 생각도 안 하고 적은 '연산자 버튼 색 검은색으로 바꾸기'
if(clickedOperator) clickedOperator.style.backgroundColor = '#313132';
if(previousKey === 'operator') // 두 번째 값을 입력하지 않고 곧바로 Enter를 눌러버렸을 때
secondNum = firstNum; // 자기 자신에 대해 연산해야 하므로 두 번째 값을 첫 번째 값으로
if(previousKey === 'calculate') // Enter를 여러 번 누르면
// 다중 계산 한다. 1, -, 1, Enter, Enter, Enter 의 결과값은 -2
result = calculate(previousNum, operatorForAdvanced, secondNum);
else // Enter 한 번만 눌렀으면 정상 계산
result = calculate(firstNum, operatorForAdvanced, secondNum);
display.textContent = result; // 결과를 화면에 출력
previousNum = Number(result); // 이전 값에 결과값을 저장해둔다
previousKey = 'calculate'; // 이전 키 값 변경
isSecondInput = false; // 이제 첫 번째 값을 입력해야 하므로 두 번째 값 입력 취소
}
🔵 reference
if (action === 'calculate') {
if (firstNum) { // 첫 번째 값 존재하고
if (previousKey === 'calculate') { // Enter 연속으로 눌렀으면
// 다중 계산한다.
display.textContent = calculate(display.textContent, operatorForAdvanced, previousNum);
} else { // Enter 한 번만 눌렀으면
previousNum = display.textContent; 화면에 출력 되어있는 이전 값을 저장한다
// 정상 계산.
display.textContent = calculate(firstNum, operatorForAdvanced, display.textContent);
}
}
previousKey = 'calculate';
}
display.texContent
값을 calculator()
함수에 넣는 점에서 secondNumber
변수를 따로 지정한 나와 다르다.
(나는 아마 .5만 입력해도 0.5가 되도록 하라. 라는 말을 다르게 오해해서 이상한 변수를 만들었던 것 같다. 아무 숫자도 입력하지 않았을 때('0'일 때) . 버튼을 클릭하면 0이 사라지고 진짜 .만 출력 되게 만드느라..이것저것 조건문의 요건을 충족하려다 보니 쓸모없는 변수가 엄청 많아졌다.)
Enter를 누르기 직전 화면에 출력 되어있는 마지막 값이 곧 두 번째 값이고, 두 번째 값이 입력 되지 않았다더라도(1+Enter) 자기 자신에 의해 연산될 수 있다는 점이 간단하고 마음에 든다.
🔴 me
if (action === 'clear') {
// 연산자 버튼 색 초기화(그래봤자 삼 항 이상 다항연산은 적용도 안 됨)
if(clickedOperator) clickedOperator.style.backgroundColor = '#313132';
firstNum = null; // 첫 번째 숫자 초기화
secondNum = null; // 두 번째 숫자 초기화 (내가 멋대로 초기값 null로 했었음)
operatorForAdvanced = '+'; // 아 이건 bare 난이도 초기화랑 헷갈렸나보다. 거기선 +가 기본상태라...
previousKey = 'clear'; // 이전 키 변경
display.textContent = 0; // 화면 초기화
isSecondInput = false; // 두 번째 값 입력 flag 내림
}
🔵 reference
if (action === 'clear') {
firstNum = undefined; // let firstNum; 이라고 선언했었기 때문에 초기값은 undefined여야 한다.
operatorForAdvanced = undefined; // 위와 마찬가지
previousNum = undefined; // 또찬가지
previousKey = 'clear'; // 이전 키 변경
display.textContent = '0'; // 화면 초기화
}
🔴 me
if (action === 'decimal') {
if(previousKey !== 'decimal'){ // .을 다중 클릭해도 한 개만 출력 되도록
if(previousKey === 'number') // 이전에 숫자 버튼을 눌렀다면
display.textContent = display.textContent.concat('.'); // 이어붙이기
else display.textContent = '.'; // 아니면 기본값 '0' 지우고 . 찍기 (오해의 시작)
}
previousKey = 'decimal'; // 이전 키 값 바꾸기
}
🔵 reference
if (action === 'decimal') {
// 화면에 . 이 없고 && 이전에 클릭한 버튼이 연산자가 아닐 때 (1 + .5 이런 식은 아예 안 되게 막아버리는 구나...)
if (!display.textContent.includes('.') && previousKey !== 'operator') {
display.textContent = display.textContent + '.'; // . 이어붙이기
} else if (previousKey === 'operator') { // 이전에 연산자 버튼 클릭했으면
display.textContent = '0.'; // .5 가 아니라 0.5라고 출력 되도록 0. 붙임
}
previousKey = 'decimal'; // 이전 키 값 수정
}
나는 왜 .5 도 계산 되도록 하라는 말을 .5로 출력하라는 걸로 이해했을까ㅜㅜ
저 부분에서 조건문 맞추느라 이상한 변수 많이 만들었는데...
(사실 조건 맞춘다 한들 이상한 변수 안 만들고도 가능했을지 모르지만)
어찌저찌 나이트메어 테스트 끝까지 성공했다! 기분이 좋았다. 이럴 때가 제일 재밌다. 코딩 공부 자체는 재미 없다고 할 수도 없고 있다고 할 수도 없는 그저 그런 녀석인데. 이렇게 문제를 직접 풀면 그냥 게임처럼 재밌다. 하지만 문제는 나 혼자 작성하고 나 혼자 재밌으면 안 된다는 거다..
페어 분께서 소수점 파트 코드 리뷰를 부탁하셨는데, 말문이 막혔다. 내 눈으로 내 머리로는 이해가 가는데 그걸 말로 서술하려니 뇌가 정지했다. 여러 조건문들이 서로 얽혀있어서 '소수점 파트'만 딱 꼬집어 설명하기 어려웠다. 이건 내가 코드를 예쁘게 작성하지 않은 탓이겠지.
CleanCode 왈, 그냥 위에서 쭉 내려오면서 읽어도 하나의 한글 문서를 읽는 것마냥 자연스럽게 이해가 되어야 한다고 했는데, 내 코드는... 한참 멀었다. 변수 이름도 목적이 모호하게 작성한 것 같다. 다음부터는 꼭 별도의 정리 없이도 코드를 줄줄 리뷰할 수 있을 만큼 자연스러운 코드 흐름을 짜도록 노력해야겠다. 그러기 위해선 새로운 문제를 계속 푸는 것도 좋지만 이미 작성한 코드를 거듭 반복하여 더 나은 코드로 만드는 훈련도 필요한 것 같다. 내 첫 번째 목표는 계산기 코드를 그렇게 수정하는 거다! 오늘 계속 고민해본 다음 아마도 내일이나 주말에 정리하게 될 거 같다.