프로토콜(Protocol)이란? 

     기계(컴퓨터)끼리 데이터를 주고 받기 위한 규칙 / 약속 / 표준을 말한다.


프로토콜은 어디에 쓰일까?
분야 예시 프로토콜 설명
HTTP, HTTPS 브라우저 - 서버 통신
파일 전송 FTP, SFTP 파일을 네트워크로 전송
이메일 SMTP, IMAP 이메일 보내고 받는 규칙
로컬 네트워크 TCP/IP 데이터 전송의 기본 규칙
보안 SSL/TLS 암호화와 인증을 담당

 


프로토콜은 어떻게 생겼을까? 🤔

    프로토콜은 "이런 순서로 이런 정보들을 보내고 받아라"와 같은 구체적인 형식과 순서를 정해놓은 문서 혹은 표준이다.

    예시로 HTTP 요청 프로토콜을 보면 아래와 같은 형식을 가지고 있다.

GET /about HTTP/1.1
Host: www.example.com

 

  • 첫 줄은 메서드(GET), 경로(/about), 버전(HTTP/1.1)
  • 두 번째 줄은 헤더 정보
  • 마지막은 본문 (필요 시)

     위와 같은 형식으로 보내지 않으면 서버가 요청을 이해하지 못한다.


 

프로토콜이 중요한 이유 🔍

 

    프로토콜이 중요한 이유는 컴퓨터끼리는 사람처럼 추측이 불가능하기 때문에 조금이라도 틀린 형식이라면 통신이 되지 않는다.

    그래서 모두가 따를 수 있는 공식 규칙이 필요했고 그렇게해서 생긴 것이 프로토콜이다!


 

 

나는 더 나아가서 프로토콜이 네트워크 계층 구조(OSI 7계층) 속에서 어떻게 동작하는지 알아보려고 한다!


네트워크 계층 구조(OSI 7계층)란?

    컴퓨터와 네트워크 장비가 어떻게 데이터를 주고받는지를 7단계 계층으로 나눈 모델이다.

    각 계층은 특정한 역할을 맡아서 서로 계층 간에 작업을 나눠서 협력하는 구조이다.

 

[OSI 7계층 전체 구조]
계층 이름 역할 예시 프로토콜
7 응용 계층 사용자와 가장 가까운 계층, 실제 요청/응답 HTTP, FTP, SMTP
6 표현 계층 데이터 인코딩, 암호화, 압축 JPEG, SSL/TLS, GZIP
5 세션 계층 통신 세션(접속, 유지, 종료) 관리 NetBIOS, RPC
4 전송 계층 데이터의 신뢰성있는 전송 TCP, UDP
3 네트워크 계층 경로 선택, 주소 지정 IP, ICMP
2 데이터 링크 계층 MAC 주소, 프레임 전달 이더넷, 스위치
1 물리 계층 실제 전기 신호, 하드웨어, 케이블 LAN, USB, 전선

 

 

브라우저의 렌더링 과정은 웹 개발자라면 필수적으로 알아야 할 지식 중 하나이다!

간단히 말하자면, 우리가 작성한 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은 웹 페이지의 뼈대 구조라고도 할 수 있다.

 


[예시]

<body>
  <h1>브라우저의 렌더링 과정</h1>
  <p>HTML 문서 파싱</p>
  <p>DOM 트리 생성</p>
</body>

⬇️


2단계: CSS 파싱 ➡️ CSSOM 트리 생성

     DOM 트리와 마찬가지로 style 태그나 .css 파일을 파싱하여 CSSOM(CSS Object Model) 트리를 구축한다.

[💡 개념 알고가기!]
1. CSSOM(CSS Object Model)이란? ➡️ "CSS를 트리 구조로 정리한 것"
   - CSS도 처음에는 그냥 텍스트이기 때문에 브라우저가 파싱해서 구조화해야하는데, 이를 CSSOM이라고 한다.
      만약 HTML에서 <h1>를 빨간색 폰트로 지정했다면 "h1이라는 요소는 빨간색 글자"라고 저장해놓는 객체 구조이다.  

[예시]

<body style="font-size: 10px;">
  <h1 style="font-size: 20px;">브라우저의 렌더링 과정</h1>
  <p style="color: red;">HTML 문서 파싱</p>
  <p style="display: none;">DOM 트리 생성</p>
</body>

⬇️


3단계: DOM + CSSOM = Render 트리 생성

     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란? ➡️ "요소의 시각적인 스타일만 변경되었을 때 발생"
   배경색을 변경한다던지 글자색을 변경하는 등의 시각적인 스타일만 변경될 경우,
   브라우저는 위치나 크기를 다시 계산하지 않고 화면에 그리는 작업만 다시 수행하는데, 이를 리페인트라고 한다.

   리플로우에 비해 상대적으로 가벼운 작업이지만 이 또한 빈번하게 발생하면 성능에 영향을 줄 수 있다.

 

그렇다면 JavaScript 코드는 언제 실행될까? 🤔

 

기본적으로 HTML 파싱 중에 <script>를 만나면 바로 실행된다!

<body>
  <h1>안녕!</h1>
  <script src="main.js"></script>
  <h1>반가워요!</h1>
</body>

하지만 위의 코드에서는 DOM을 다 생성하기 전에 실행되므로 '안녕'이 아닌 null이 출력될 수 있다.

그래서 원하는 타이밍에 맞게 자바스크립트를 실행할 수 있도록 조절해야한다.


1. 기본 <script>  (동기 실행)

     위에서 말했듯이 기본적으로 파싱 중에 <script>를 만나면 파싱이 중단되고 JS가 실행된다.

     한마디로 동기적으로 실행되는 것인데, 그렇기 때문에 <script>의 위치에 따라 실행되는 타이밍이 달라진다.


  • <head>안에 <script> 사용
<head>
  <script src="main.js"></script>
</head>
<body>
  <h1>안녕!</h1>
  <h1>반가워요!</h1>
</body>

이 경우 스크립트 파일을 전부 불러올 때 까지 body 파싱을 멈추기 때문에 브라우저의 초기 렌더링이 느려진다.

그렇게 되면 스크립트 파일에 따라 사용자가 빈 화면을 보고있는 시간이 길어질 수 있기 때문에 좋지않은 방식이다.


  • <body> 중간에 <script> 사용
<body>
  <h1>안녕!</h1>
  <script src="main.js"></script>
  <h1>반가워요!</h1>
</body>

중간에 스크립트를 사용하면 body 파싱 중에 script를 만나기 때문에 그 다음 요소들은 파싱되지 않고,

스크립트 파일이 전부 실행될 때까지 파싱이 멈추게 된다.


  • <body> 끝에 <script> 사용
<body>
  <h1>안녕!</h1>
  <h1>반가워요!</h1>
  <script src="main.js"></script>
</body>

마지막에 스크립트를 사용하게 되면 이미 위에 요소들을 전부 파싱한 후 실행되는 것이므로 DOM을 조작하기에 안정적인 시점이다.


2. defer 사용
<head>
  <script src="main.js" defer></script>
</head>

defer를 사용하면 script 파일은 HTML 파싱이 끝날 때까지 기다렸다가 실행된다.

따라서 DOM이 완성된 후에 실행되고 순서도 보장할 수 있다.

대부분 head안에 <script defer>로 사용한다.


3. async 사용
<head>
  <script src="main.js" async></script>
</head>

많이 보는 async는 비동기적으로 실행된다.

script 파일을 불러오는 동안 HTML을 파싱하고 다 불러와지면 바로 스크립트가 실행된다.

이렇게 비동기적으로 실행되기 때문에 순서를 보장할 수 없다.


[요약]

방식 실행 시점 HTML 파싱 중단 여부 순서 보장
<head>에 <script> 사용 즉시 실행 ⭕️ ⭕️
<body> 중간에 <script> 사용 즉시 실행 ⭕️ ⭕️
<body> 끝에 <script> 사용 즉시 실행 ⭕️
defer 사용 파싱이 완료된 후 실행 ⭕️
async 사용 다운로드 완료 후 즉시 실행 실행 시 중단

[브라우저 렌더링 과정] 최종 요약 ⭐️

문제:
이차원 정수 배열 arr이 매개변수로 주어집니다. 
arr의 행의 수가 더 많다면 열의 수가 행의 수와 같아지도록 각 행의 끝에 0을 추가하고,
열의 수가 더 많다면 행의 수가 열의 수와 같아지도록 각 열의 끝에 0을 추가한
이차원 배열을 return 하는 solution 함수를 작성해 주세요.

 

문제 풀어보기: https://school.programmers.co.kr/learn/courses/30/lessons/181830

 

풀이보기
더보기
function solution(arr) {
    let result = [...arr];
    let row = arr.length;
    let column = arr[0].length;
    
    if(row > column) {
        for(let i = 0; i < result.length; i++) {
            while(result[i].length !== row) {
                result[i].push(0);
            }
        }
    } else if(row < column) {
        for(let i = 0; i < result.length; i++) {
            while(result.length !== column) {
                result.push(Array(column).fill(0));
            }
        }
    }
    
    return result;
}

먼저 row와 column에 각각 배열의 길이를 저장해준다.

만약 row가 더 크면 열에 0을 추가해줘야하므로 reulst[i]의 길이가 행의 크기와 같아질때까지 0을 푸쉬한다.

만약 column이 더 크면 행에 열의 길이만큼 0이 채워진 배열을 추가해줘야하므로

result의 길이가 열의 크기와 같아질때까지 푸쉬해주면 된다.

 

문제:
머쓱이는 프로그래머스에 로그인하려고 합니다.
머쓱이가 입력한 아이디와 패스워드가 담긴 배열 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입니다.

 

문제 풀어보기: https://school.programmers.co.kr/learn/courses/30/lessons/120883#

 

풀이보기
더보기
function solution(id_pw, db) {
    let isID = false;
    let isPW = false;
    
    for(let i = 0; i < db.length; i++) {
        if(id_pw[0] === db[i][0]){
            isID = true;
            if(id_pw[1] === db[i][1]) isPW = true;
        }
    }
    
    return isID && isPW ? 'login' : isID && !isPW ? 'wrong pw' : 'fail';
}

먼저 ID와 PW가 올바른지 판단하는 boolean 변수를 각각 선언해준다.

그리고 db 배열을 순회하며 아이디와 비밀번호가 올바른지 확인해주면 된다.

아이디와 비밀번호의 위치는 고정되어있기 때문에 각각 0, 1 인덱스로 고정해주고,

먼저 id_pw의 아이디가 db의 아이디와 일치하면 isID를 true로 바꿔준다.

그리고 아이디가 일치한 상태에서 해당 아이디의 비밀번호도 알맞다면 isPW도 true로 바꿔준다.

마지막으로 문제에 맞게 리턴해주면 끝!

 

 

 

동기(Synchronous)란? : 일이 순서대로 끝날 때까지 기다리는 방식

 

라면으로 예시를 들어보자면, 라면을 먹기 위해 물을 끓이기 시작하고, 물이 다 끓으면 면을 넣고 끓인 뒤 라면을 먹는다.

코드로 표현하자면 다음과 같다.

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은 뭘까?

 

콜백 함수란? : 콜백은 “나중에 실행할 함수”를 인자로 전달해서, 어떤 일이 끝난 뒤 실행되게 하는 방식
function cook(callback) {
    console.log("요리 시작");
    setTimeout(() => {
        console.log("요리 끝!");
        callback();
    }, 2000);
}

cook(() => {
    console.log("먹자! 🍽️");
});

위의 코드를 보면, cook이 먼저 호출되고 '요리 시작'이 출력된다.

그 후 2초 뒤에 '요리 끝'이 출력된 다음 아규먼트로 넘겨준 callback 함수가 실행되면서 '먹자!'가 출력된다.

 

콜백 함수에서의 주의점은 콜백 지옥(Callback Hell)을 조심해야하는데,

이는 함수 안에 함수, 또 함수 안에 함수와 같은 형태로 가독성이 떨어지고 디버깅이 어려워진다.

function login(id, callback) {
    setTimeout(() => {
        console.log("로그인 성공");
        callback(id);
    }, 1000);
}

function getUserInfo(id, callback) {
    setTimeout(() => {
        console.log(`사용자 정보 불러오기: ${id}`);
        callback({ id, name: "Alice" });
    }, 1000);
}

function getFriends(user, callback) {
    setTimeout(() => {
        console.log(`${user.name}의 친구 목록`);
        callback(["Bob", "Charlie"]);
    }, 1000);
}

// 콜백 지옥 시작
login("user123", (id) => {
    getUserInfo(id, (user) => {
        getFriends(user, (friends) => {
            console.log("친구 목록:", friends);
        });
    });
});

 

 

Promise란? : 작업이 끝났을 때 성공/실패 결과를 주겠다고 약속하는 객체

 

Promise는 .then()과 .catch()를 이용해서 결과를 처리할 수 있다.

function cook() {
    return new Promise((resolve) => {
        console.log("요리 시작");
        setTimeout(() => {
            console.log("요리 끝!");
            resolve("라면 완성 🍜");
        }, 2000);
    });
}

cook().then((result) => {
    console.log(result); // 라면 완성 🍜
});

cook이 호출되면서 '요리 시작'이 출력되고, 2초 후 '요리 끝'이 출력된다.

그 순간 resolve("라면 완성 🍜")가 호출되어 Promise가 성공(이행)되고, .then()이 그 값을 받아 '라면 완성 🍜'을 출력한다.

 

하지만 Promise를 사용하면서도 주의할 점은 Promise 체인이 길어지면 가독성이 떨어진다.

function login(id) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log("로그인 성공");
            resolve(id);
        }, 1000);
    });
}

function getUserInfo(id) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log(`사용자 정보 불러오기: ${id}`);
            resolve({ id, name: "Alice" });
        }, 1000);
    });
}

function getFriends(user) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log(`${user.name}의 친구 목록`);
            resolve(["Bob", "Charlie"]);
        }, 1000);
    });
}

login("user123")
    .then(getUserInfo)
    .then(getFriends)
    .then((friends) => {
        console.log("친구 목록:", friends);
    });

 

async/await 이란? : Promise를 더 간단하고 동기 코드처럼 보이게 만드는 문법

 

택배로 예시를 들어보면, Promise 같은 경우는 택배가 오면 그때 열어보는 것이고 async/await은 택배가 올 때까지 기다렸다가 열어보는 것이라고 할 수 있다.

function cook() {
    return new Promise((resolve) => {
        console.log("요리 시작");
        setTimeout(() => {
            console.log("요리 끝!");
            resolve("라면 완성 🍜");
        }, 2000);
    });
}

async function eat() {
    const food = await cook(); // cook() 끝날 때까지 기다림
    console.log(food);
    console.log("먹자! 😋");
}

eat();

코드를 보면, 먼저 eat이 호출되고 cook이 실행된다.

cook이 전부 실행될 때 까지 기다린 후에 food가 출력되고 마지막으로 '먹자'가 출력된다.

food에는 cook에서 resolve가 호출되면서 Promise가 성공했기 때문에 '라면 완성'이 저장된다.

가독성도 좋기 때문에 많이 쓰이는 문법이다.


요약
콜백 함수 함수 안에 함수
Promise 성공/실패를 나중에 알려주는 약속
async / await Promise를 더 깔끔하게 쓰는 방식

 

문제:
문자열 s가 입력되었을 때 다음 규칙을 따라서 이 문자열을 여러 문자열로 분해하려고 합니다.
먼저 첫 글자를 읽습니다. 이 글자를 x라고 합시다.
이제 이 문자열을 왼쪽에서 오른쪽으로 읽어나가면서, x와 x가 아닌 다른 글자들이 나온 횟수를 각각 셉니다.
처음으로 두 횟수가 같아지는 순간 멈추고, 지금까지 읽은 문자열을 분리합니다.
s에서 분리한 문자열을 빼고 남은 부분에 대해서 이 과정을 반복합니다.
남은 부분이 없다면 종료합니다.
만약 두 횟수가 다른 상태에서 더 이상 읽을 글자가 없다면, 역시 지금까지 읽은 문자열을 분리하고, 종료합니다.
문자열 s가 매개변수로 주어질 때, 위 과정과 같이 문자열들로 분해하고, 분해한 문자열의 개수를 return 하는 함수 solution을 완성하세요.

 

문제 풀어보기: https://school.programmers.co.kr/learn/courses/30/lessons/140108#

 

풀이보기
더보기
function solution(s) {
    let arr = [...s];
    let result = [];
    
    let countX = 0;
    let countOther = 0;
    
    let i = 0;
    while(i < arr.length) {
        let x = arr[0];
        
        x === arr[i] ? countX++ : countOther++;
        
        if(countX === countOther) {
            result.push(arr.slice(0, i + 1));
            arr.splice(0, i + 1);
            
            i = 0;
            countX = 0;
            countOther = 0;
        } else {
            i++;
        }
    }
    
    if(countX !== countOther) {
        result.push(arr.slice(0));
    }
    
    return result.length;
}

사실 이 문제는 예시도 자세히 설명이 안되어있고 그래서 문제를 이해하는데 좀 시간이 걸렸다 ㅎㅎ;

나는 while문을 통해 문제를 풀었다.

 

countX와 countOther을 각각 선언해주고,

만약 첫 문자와 나머지 문자가 같다면 countX를 더해주고 아니면 countOther을 더해준다.

그리고 countX와 countOther이 같아지면 result에 arr의 첫문자부터 같아지는 i 값까지 잘라서 push 해준다.

그 후 arr에서 자른 값을 삭제해주고 다시 처음부터 돌아야하기 때문에 i, countX, countOther을 다시 0으로 초기화한다.

만약 countX와 countOther이 같지않으면 i만 증가시켜주면 된다.

 

그리고 반복문을 나왔을 때, countX와 countOther이 같지 않다면 마지막 요소까지 result에 push되지 않고 그냥 나온 것이므로

첫문자부터 마지막 문자까지 push 해준다.

마지막으로 result의 길이를 반환해주면 끝!

 

문제:
두 정수 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의 짝꿍은 상당히 큰 정수일 수 있으므로, 문자열로 반환합니다.

 

문제 풀어보기: https://school.programmers.co.kr/learn/courses/30/lessons/131128

 

풀이보기
더보기
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 함수를 완성해주세요.

 

문제 풀어보기: https://school.programmers.co.kr/learn/courses/30/lessons/155652#

 

풀이보기
더보기
function solution(s, skip, index) {
    const alphabets = Array.from({ length: 26 }, (_, i) => String.fromCharCode(97 + i));
    let result = [];
    
    for(let i = 0; i < s.length; i++) {
        let skips = [];
        let indexOfChar = alphabets.indexOf(s[i]);
        
        let j = 1;
        while (skips.length < index) {
            let nextIndex = (indexOfChar + j) % 26;
            let nextChar = alphabets[nextIndex];
            
            if (!skip.includes(nextChar)) {
                skips.push(nextChar);
            }
            j++;
        }
        
        result.push(skips[skips.length - 1]);
    }
    
    return result.join('');
}

 일단 알파벳을 담은 배열을 만들어주고 s 문자열을 순회해준다.

그 다음 skip 안에 있는 문자들을 제외한 문자를 skips에 저장해준다.

26으로 나눠주는 이유는 인덱스가 알파벳 길이를 넘어가면 다시 a부터 시작해야하기 때문이다.

그 후 skip에 포함된 문자가 아닐때만 skips에 push해주고 이를 토대로 skips의 마지막 문자를 result에 넣어주면 된다.

근데 이렇게 푼게 정말 바보 같은 짓인걸 다른 사람 풀이를 보고 알았다 ㅎㅎ ;

미리 알파벳에서 skip 문자들을 제외시키면 더 쉬운걸 생각도 안하고 있었다 ㅎ

 

function solution(s, skip, index) {
    const alphabets = Array.from({ length: 26 }, (_, i) => String.fromCharCode(97 + i))
                      .filter((char) => !skip.includes(char));
    
    return [...s].map((char) => alphabets[(alphabets.indexOf(char) + index) % alphabets.length]).join('');
}

이렇게 미리 filter로 제외시켜주고 이를 토대로 인덱스를 더해주면 더 간단히 작성할 수 있다 ㅎㅎ .. 

 

+ Recent posts