간단히 말하자면, 우리가 작성한 HTML / CSS / JS를 브라우저가 화면에 그리는 과정을 말한다.
쉽게 이해해보기 위해서 요리하는 과정에 비유하여 설명해보자면 다음과 같다.
📄 HTML: 요리 레시피 🎨 CSS: 플레이팅 / 장식 ⚙️ JavaScript: 자동 조리 기계 (동작 / 인터랙션) 🧑🏻🍳 브라우저: 요리사 (렌더링 엔진)
그럼 이제 단계별로 렌더링 과정을 알아보자!
1단계: HTML 파싱 ➡️ DOM 트리 생성
먼저 HTML 문서를 파싱하여 태그들을 DOM(Document Object Model) 트리 구조로 변환해준다.
[💡 개념 알고가기 !] 1. 파싱(Parsing)이란? ➡️ "문자 덩어리를 구조로 바꾸는 과정" - 브라우저는 처음 HTML 문서를 처음 받으면 그냥 글자들로 보여진다. 그렇기 때문에 화면에 보여주기 위해서는 구조를 알아야하는데, 예를 들어 "<h1>은 제목이고, <p>는 단락이구나."와 같이 의미를 해석하는 작업이 필요한데 이를 파싱이라고 한다.
2. DOM(Document Object Model)이란? ➡️ "HTML을 트리 구조로 표현한 것" - 만약 HTML 문서에 <h1>안녕하세요</h1>라고 저장되어있다면, 내부적으로는 아래와 같은 구조로 바뀌게 된다. Document > h1 > "안녕하세요" DOM은 웹 페이지의 뼈대 구조라고도 할 수 있다.
DOM 트리와 마찬가지로 style 태그나 .css 파일을 파싱하여 CSSOM(CSS Object Model) 트리를 구축한다.
[💡 개념 알고가기!] 1. CSSOM(CSS Object Model)이란? ➡️ "CSS를 트리 구조로 정리한 것" - CSS도 처음에는 그냥 텍스트이기 때문에 브라우저가 파싱해서 구조화해야하는데, 이를 CSSOM이라고 한다. 만약 HTML에서 <h1>를 빨간색 폰트로 지정했다면 "h1이라는 요소는 빨간색 글자"라고 저장해놓는 객체 구조이다.
DOM 트리와 CSSOM 트리를 결합하여 Render 트리를 구축하는데, Render 트리는 화면에 표시될 요소들만 포함하여 생성한다.
위에서 보여줬던 예시 DOM 트리와 CSSOM 트리를 결합하여 Render 트리를 생성하게 된다면 아래와 같은 트리가 완성된다.
* display: none 스타일이 적용되어있는 <p>DOM 트리 구축</p> 부분은 실제 화면에 보여지지 않기 때문에 제외된다.
4단계: Layout (Reflow)
Layout 단계에서는 각 요소의 위치와 크기를 계산한다.
예를 들어, "버튼은 화면의 왼쪽 여백이 20px이고, 너비는 100px이다."와 같이 계산하게 된다.
5단계: Painting (Repaint)
Painting 단계에서는 Layout 단계에서 계산된 요소의 위치와 크기를 픽셀 단위로 화면에 그린다.
[💡 개념 알고가기 !] 1. Reflow란? ➡️ "요소의 위치나 크기 변경되었을 때 발생" 예를 들어, 요소의 크기(width, height)나 위치(margin, position, display 등)가 변경되면 전체 레이아웃을 다시 계산해야하기 때문에 리플로우가 발생한다.
리플로우는 비용이 큰 작업이며, 발생 시 변경된 요소 뿐만 아닌 연관된 다른 요소도 다시 계산될 수 있다. 또한, 리플로우가 발생하면 자동적으로 리페인트로 발생하기 때문에 DOM 구조나 스타일을 자주 바꾸는 것은 성능 저하의 원인이 될 수 있다는 점을 명시해야한다.
2. Repaint란? ➡️ "요소의 시각적인 스타일만 변경되었을 때 발생" 배경색을 변경한다던지 글자색을 변경하는 등의 시각적인 스타일만 변경될 경우, 브라우저는 위치나 크기를 다시 계산하지 않고 화면에 그리는 작업만 다시 수행하는데, 이를 리페인트라고 한다.
리플로우에 비해 상대적으로 가벼운 작업이지만 이 또한 빈번하게 발생하면 성능에 영향을 줄 수 있다.
문제: 이차원 정수 배열 arr이 매개변수로 주어집니다. arr의 행의 수가 더 많다면 열의 수가 행의 수와 같아지도록 각 행의 끝에 0을 추가하고, 열의 수가 더 많다면 행의 수가 열의 수와 같아지도록 각 열의 끝에 0을 추가한 이차원 배열을 return 하는 solution 함수를 작성해 주세요.
문제: 머쓱이는 프로그래머스에 로그인하려고 합니다. 머쓱이가 입력한 아이디와 패스워드가 담긴 배열 id_pw와 회원들의 정보가 담긴 2차원 배열 db가 주어질 때, 다음과 같이 로그인 성공, 실패에 따른 메시지를 return하도록 solution 함수를 완성해주세요. 아이디와 비밀번호가 모두 일치하는 회원정보가 있으면 "login"을 return합니다. 로그인이 실패했을 때 아이디가 일치하는 회원이 없다면 “fail”를, 아이디는 일치하지만 비밀번호가 일치하는 회원이 없다면 “wrong pw”를 return 합니다.
제한사항 - 회원들의 아이디는 문자열입니다. - 회원들의 아이디는 알파벳 소문자와 숫자로만 이루어져 있습니다. - 회원들의 패스워드는 숫자로 구성된 문자열입니다. - 회원들의 비밀번호는 같을 수 있지만 아이디는 같을 수 없습니다. - id_pw의 길이는 2입니다. - id_pw와 db의 원소는 [아이디, 패스워드] 형태입니다. - 1 ≤ 아이디의 길이 ≤ 15 - 1 ≤ 비밀번호의 길이 ≤ 6 - 1 ≤ db의 길이 ≤ 10db의 원소의 길이는 2입니다.
라면으로 예시를 들어보자면, 라면을 먹기 위해 물을 끓이기 시작하고, 물이 다 끓으면 면을 넣고 끓인 뒤 라면을 먹는다.
코드로 표현하자면 다음과 같다.
function boilWater() {
console.log('1. 물 끓이기 시작');
for (let i = 0; i < 3; i++) {
console.log('2. 물 끓이는 중 ...');
}
console.log('3. 물을 다 끓였어요!');
}
function makeRamen() {
boilWater();
console.log('4. 면 넣고 끓이기');
console.log('5. 라면 완성!');
}
makeRamen();
코드의 흐름을 보면, makeRamen이 호출되면서 먼저 boilWater 함수가 실행된다.
boilWater 함수에서 1번이 출력된 후, 2번이 3번 출력되고 나서 3번이 출력된다.
이 과정이 끝날 때까지 다음 코드는 실행되지 않으며, 작업이 끝나면 함수를 빠져나와 그 이후의 작업인 4번과 5번이 순서대로 출력된다.
// 출력
1. 물 끓이기 시작
2. 물 끓이는 중 ...
2. 물 끓이는 중 ...
2. 물 끓이는 중 ...
3. 물을 다 끓였어요!
4. 면 넣고 끓이기
5. 라면 완성!
그렇다면 비동기에서의 라면은 어떻게 끓일까?
비동기란(Asynchronous)? : 일이 끝나길 기다리지 않고 다른 일을 먼저 처리하는 방식
일단 라면을 먹기 위해 물을 끓일 것이다.
하지만 이전과 다른 점은, 라면을 끓일동안 게임을 하다 올 것이다.
게임을 하다가 물이 끓으면 타이머가 울리고, 그때 면을 넣고 끓인 후 라면을 먹으면 된다.
코드로 표현하자면 다음과 같다.
function boilWaterAsync() {
console.log("1. 물 끓이기 시작");
setTimeout(() => {
console.log("3. 물 끓었음");
console.log("4. 면 넣고 끓이기");
console.log("5. 라면 완성!");
}, 3000);
}
function doSomethingElse() {
console.log("2. 게임하기 🎮");
}
boilWaterAsync();
doSomethingElse();
먼저 boilWaterAsync가 호출되고 1번이 출력된다.
그 후 setTimeout으로 3초 뒤 실행하게끔 3, 4, 5번이 출력되게끔하고,
그 사이에 doSomethingElse가 호출되면서 2번이 출력된다.
그리고 3초 후 3, 4, 5번이 순서대로 출력된다.
1. 물 끓이기 시작
2. 게임하기
(3초 후 ...)
3. 물 끓었음
4. 면 넣고 끓이기
5. 라면 완성!
그럼 이제 비동기에서 중요한 개념들인 콜백, Promise, async/await은 뭘까?
콜백 함수란? : 콜백은 “나중에 실행할 함수”를 인자로 전달해서, 어떤 일이 끝난 뒤 실행되게 하는 방식
문제: 문자열 s가 입력되었을 때 다음 규칙을 따라서 이 문자열을 여러 문자열로 분해하려고 합니다. 먼저 첫 글자를 읽습니다. 이 글자를 x라고 합시다. 이제 이 문자열을 왼쪽에서 오른쪽으로 읽어나가면서, x와 x가 아닌 다른 글자들이 나온 횟수를 각각 셉니다. 처음으로 두 횟수가 같아지는 순간 멈추고, 지금까지 읽은 문자열을 분리합니다. s에서 분리한 문자열을 빼고 남은 부분에 대해서 이 과정을 반복합니다. 남은 부분이 없다면 종료합니다. 만약 두 횟수가 다른 상태에서 더 이상 읽을 글자가 없다면, 역시 지금까지 읽은 문자열을 분리하고, 종료합니다. 문자열 s가 매개변수로 주어질 때, 위 과정과 같이 문자열들로 분해하고, 분해한 문자열의 개수를 return 하는 함수 solution을 완성하세요.
문제: 두 정수 X, Y의 임의의 자리에서 공통으로 나타나는 정수 k(0 ≤ k ≤ 9)들을 이용하여 만들 수 있는 가장 큰 정수를 두 수의 짝꿍이라 합니다(단, 공통으로 나타나는 정수 중 서로 짝지을 수 있는 숫자만 사용합니다). X, Y의 짝꿍이 존재하지 않으면, 짝꿍은 -1입니다. X, Y의 짝꿍이 0으로만 구성되어 있다면, 짝꿍은 0입니다.
예를 들어, X = 3403이고 Y = 13203이라면, X와 Y의 짝꿍은 X와 Y에서 공통으로 나타나는 3, 0, 3으로 만들 수 있는 가장 큰 정수인 330입니다. 다른 예시로 X = 5525이고 Y = 1255이면, X와 Y의 짝꿍은 X와 Y에서 공통으로 나타나는 2, 5, 5로 만들 수 있는 가장 큰 정수인 552입니다 (X에는 5가 3개, Y에는 5가 2개 나타나므로 남는 5 한 개는 짝 지을 수 없습니다.) 두 정수 X, Y가 주어졌을 때, X, Y의 짝꿍을 return하는 solution 함수를 완성해주세요.
제한사항 - 3 ≤ X, Y의 길이(자릿수) ≤ 3,000,000입니다. - X, Y는 0으로 시작하지 않습니다. - X, Y의 짝꿍은 상당히 큰 정수일 수 있으므로, 문자열로 반환합니다.
function solution(X, Y) {
let arr1 = [...X];
let arr2 = [...Y];
let common = arr1.filter((val) => {
let idx = arr2.indexOf(val);
if(idx !== -1) {
arr2.splice(idx, 1);
return true;
}
});
if(common.length === 0) return '-1';
let allZero = true;
common.forEach((val) => {
if(val !== '0') allZero = false;
});
if(allZero) return '0';
return common.sort((a, b) => b - a).join('');
}
처음에는 배열로 풀었었다.
arr1과 arr2에 각각 X,Y를 배열로 담아주고,
filter 메서드를 사용해서 arr1과 arr2에 공통으로 담겨있는 수를 새로운 배열로 반환해준다.
여기서 공통된 수는 splice를 통해 arr2에서 삭제해줘야한다.
그 다음 공통된 수가 없다면 -1을 리턴해주고, 공통된 수가 모두 0이라면 0을 리턴해줘야한다.
두 조건에 다 해당되지 않는다면 common을 내림차순으로 정렬한 뒤 문자열로 반환해주면 끝이다!
하지만 이 코드로는 문제를 통과하지 못했다. 왜냐하면 X, Y의 범위가 엄청 크기 때문에 효율적으로 코드를 짜야한다.
function solution(X, Y) {
let arr1 = [...X];
let arr2 = [...Y];
let countMap = {};
for(let num of arr2) {
countMap[num] = (countMap[num] || 0) + 1;
}
let result = [];
for(let num of arr1) {
if(countMap[num] > 0) {
result.push(num);
countMap[num]--;
}
}
if(result.length === 0) return '-1';
let allZero = true;
result.forEach((val) => {
if(val !== '0') allZero = false;
});
if(allZero) return '0';
return result.sort((a, b) => b - a).join('');
}
이럴때 항상 나는 객체를 사용해줬다.
먼저 countMap 객체에 arr2의 수와 카운트를 저장해주고,
arr1을 순회하면서 countMap에 있는 수라면 result에 해당 값을 추가해준 뒤 countMap[num]-- 해준다.
그 이후는 똑같다! 객체를 활용하면 더 효율적으로 문제를 해결할 수 있다.
근데 코드가 통과하긴 했지만 그래도 느리긴했다.
배열을 사용했을 때 문제는 indexOf나 splice 등과 같이 배열 이동이 많았기 때문에 더 느렸었는데,
이 문제들을 보완해서 다시 코드를 작성해보았다.
function solution(X, Y) {
const countX = Array(10).fill(0);
const countY = Array(10).fill(0);
for (let char of X) countX[char]++;
for (let char of Y) countY[char]++;
let result = '';
for (let i = 9; i >= 0; i--) {
const commonCount = Math.min(countX[i], countY[i]);
result += i.toString().repeat(commonCount);
}
if (result === '') return '-1';
if (result[0] === '0') return '0';
return result;
}
먼저 countX, countY에 길이가 10인 배열을 0으로 채워준다.
그러는 이유는 숫자가 0 ~ 9까지이기 때문에 미리 채워서 각 인덱스를 숫자라 생각하고 count를 해주기 위해서이다.
그 다음 각각 해당 인덱스에 맞는 수를 count 해준다.
그리고 내림차순 정렬을 위해 9부터 0까지 반복을 하고, 차례대로 공통된 수를 result에 넣어준다.
Math.min을 해주는 이유는 겹치는 수를 방지하기 위해서이다.
마지막으로 result를 반환하기 전에 result가 빈 문자열이면 -1을 리턴해주고,
만약 result의 첫 문자가 0이라면 현재 내림차순 정렬이기 때문에 그 문자열은 전부 0이라는 뜻이므로 0을 리턴해주면 끝이다!
이렇게 코드를 작성하면 indexOf, map, filter 등의 메서드는 쓰지않아도 되는 제일 효율적인 코드라고 할 수 있다.
문제: 두 문자열 s와 skip, 그리고 자연수 index가 주어질 때, 다음 규칙에 따라 문자열을 만들려 합니다. 암호의 규칙은 다음과 같습니다. 문자열 s의 각 알파벳을 index만큼 뒤의 알파벳으로 바꿔줍니다. index만큼의 뒤의 알파벳이 z를 넘어갈 경우 다시 a로 돌아갑니다.skip에 있는 알파벳은 제외하고 건너뜁니다. 예를 들어 s = "aukks", skip = "wbqd", index = 5일 때, a에서 5만큼 뒤에 있는 알파벳은 f지만 [b, c, d, e, f]에서 'b'와 'd'는 skip에 포함되므로 세지 않습니다. 따라서 'b', 'd'를 제외하고 'a'에서 5만큼 뒤에 있는 알파벳은 [c, e, f, g, h] 순서에 의해 'h'가 됩니다. 나머지 "ukks" 또한 위 규칙대로 바꾸면 "appy"가 되며 결과는 "happy"가 됩니다. 두 문자열 s와 skip, 그리고 자연수 index가 매개변수로 주어질 때 위 규칙대로 s를 변환한 결과를 return하도록 solution 함수를 완성해주세요.