[JavaScript, Node.js] JS로 코딩테스트 준비하기!

Jiiker·2025년 2월 8일
post-thumbnail

프론트엔드 개발자로 취업을 준비하고 있지만, 코딩테스트를 볼 때 JavaScript를 사용하지는 않았었다. JavaScript가 알고리즘 문제를 풀기에 그렇게 좋은 언어가 아니라는 얘기를 많이 들었었고, 처음 공부를 C로 시작했기 때문에 C++을 사용하는 것이 훨씬 편해서 특별히 바꿔야겠다는 생각을 하지 않았었다. 백준 문제를 풀 때, JavaScript의 경우 표준 입력을 받는 부분부터 직관적이지 않았기 때문에 거부감도 있었던 것 같다. 특히, 최근에 본 코딩 테스트들에서 자주 등장했던 문제 유형 중 하나가 우선순위 큐를 이용하는 문제였는데, JavaScript에서는 우선순위 큐를 직접 구현해야 하는 불편함도 걸림돌 중에 하나였다.

가끔, JavaScript만 사용 가능한 코테가 있으면, 시험 보기 직전에 스택(Stack), 큐(Queue)는 어떻게 사용해야 하고, Map이나 Set 같은 자료구조는 어떻게 사용하는지 정도 체크했던 것 같다. 그리고 특정 크기의 배열을 선언하고, 특정값으로 초기화 하는 것 정도를 공부했었다. 그런데 요즘 들어서 JavaScript로 코테를 봐야하는 상황이 자주 생기는 것 같아서 이번 기회에 기본적인 것들을 정리해 보고자 이 글을 쓴다.

⌨️ JavaScript로 표준 입력 받기

이 부분은 사실 알고리즘 문제 풀이를 하기 위해서는 기본적인 부분인데, 프로그래머스 환경에서 시험을 보는 경우 이 부분이 생략되기 때문에 대비를 좀 덜 하게 되는 부분이 있다. 그리고 나의 경우 C++에서 cin >> var; 과 같이 단순하게 input을 처리하다가 이 코드를 봤을 땐 정말 이게 뭔가 싶었다. 하지만, 작년에 귀찮다고 계속 공부를 안하다가 JavaScript 코테에서 표준 입력을 받아서 처리해야 하는 문제가 나와서 시험시작 1분만에 종료 버튼을 눌렀던 경험이 있었다. 코드를 자세히 뜯어보면 그렇게 어려운 코드도 아니니 JavaScript로 코딩테스트를 준비한다면 한 번 쯤 익혀두는 것이 좋다.

보통 코딩테스트에서 제출하는 JavaScript 코드는 Node.js 환경에서 실행되기 때문에 Node.js 모듈을 사용해서 표준 입력을 처리해줄 수 있다. 오늘 오전에 치룬 현대오토에버 코딩테스트 같은 경우에는 레퍼런스로 JavaScript 문서가 아닌 Node.js 문서를 제공해주는데, 해당 링크에서 File system 모듈이나 Readline 모듈의 사용 예제 등을 찾아볼 수 있었다.

fs 모듈을 사용하는 방법

const fs = require("fs");

// 표준 입력 전체를 동기적으로 읽어옴
const input = fs.readFileSync("/dev/stdin").toString().trim().split("\n");

백준 사이트에서 안내해주는 입출력 처리 예시도 fs 모듈을 사용한 방법으로 안내가 되어있고, fs 모듈을 사용하는 쪽이 좀 더 직관적으로 와닿는 것 같아서 나는 이 방식을 선택했다. 아무래도 toString(), trim(), split()과 같은 메서드가 좀 더 직관적으로 의미가 와닿기 때문인 것 같다.

코드를 보면, fs(파일 시스템) 모듈을 불러온 다음에 해당 모듈을 이용해서 /dev/stdin 경로에 있는 파일을 동기적으로 읽어온다. linux 환경에서는 콘솔로부터의 입력을 해당 경로에 저장하기 때문에 이렇게 처리해 준다. window 환경을 고려한다면,

readFileSync(process.platform === linux ? "/dev/stdin" : "./input.txt")

와 같이 처리해 줄 수도 있지만, 대부분의 코딩테스트 채점 환경은 linux이기 때문에 "/dev/stdin"만 작성해 줘도 무방하다.

그 이후는 입력을 문자열로 바꾸어주고(toString()), 앞 뒤 공백을 제거해주고(trim()), 줄바꿈을 기준으로 나누어 배열에 담아준다(split("\n")).

readline 모듈을 사용하는 방법

const readline = require("readline");

const rl = readline.createInterface({
  input: process.stdin,   // 표준 입력
  output: process.stdout, // 표준 출력
});

let lines = [];

rl.on("line", (line) => {
  lines.push(line);
}).on("close", () => {
  // 입력이 끝났을 때 실행되는 부분
  // 여기에 solution 작성하면 됨!!
  process.exit();
});

과거의 나는 이 코드를 보고 뒤로가기를 눌렀던 것 같다. 이 코드에서 거부감이 든다면, fs 모듈을 사용하는 것으로 하고, 이번 섹션은 살포시 건너뛰도록 하자. 당시에는 이벤트를 발생(emit)시키고, 감지(listen)하는 코드를 작성해 본 적이 없었지만, 부스트캠프에서 Node.js로 이벤트리스너를 이용해서 이것저것 구현하다보니 이제는 조금 익숙한 코드가 되었다.

모듈 이름에서 어느 정도 유추할 수도 있는데, fs(파일 시스템) 모듈은 콘솔 입력이 담긴 파일을 readFileSync로 읽어서 활용하는 방식이었고, readline 모듈은 말 그대로 콘솔 입력을 한 줄씩 읽어주는 방식이다. createInterface()를 통해 어디서 입력을 받아 어디로 출력 해줄지를 정한다. process.stdinprocess.stdout이 각각 표준 입출력을 의미하고, 이를 각각 input, output으로 설정해준다.

그렇게 하면, readline 인터페이스에서 표준 입력을 한 줄 읽을 때마다 "line" 이벤트를 발생 시키고, 그 때마다 rl.on("line", (line) => { 부분에서 "line" 이벤트를 감지하게 된다.

rl.on("line", (input) => {})

이는 위에서 생성한 readline 인터페이스가 켜져있고(.on()), line 이벤트를 기다리고 있는데("line"), 이 이벤트가 감지되면 두 번째 인자의 함수((input) => {})를 실행시킨다는 의미이다. 이 때의 input 자리에는 readline 인터페이스가 이벤트를 발생시킬 때 읽었던 한 줄이 전달된다.

let lines = [];

rl.on("line", (line) => {
  lines.push(line);
})

"line" 이벤트 때 실행되는 함수와 "close" 이벤트 때 실행되는 함수는 엄연히 다른 함수이기 때문에 바로 line 값을 활용할 수는 없다. 그렇기 때문에 바깥에 전역적으로 lines 배열을 선언하고 이 배열을 활용해야 한다.

.on("close", () => {
  
	// lines 배열을 이용해서 문제 풀이 ✏️
    
	process.exit();
})

마찬가지로 입력이 끝나면 "close" 이벤트를 발생시키고, 해당 이벤트는 위의 코드에서 감지하게 된다. 입력이 끝났고, 해당 입력을 이용한 문제 풀이를 두 번째 인자 함수 내부에 작성해주면 된다. 여기서 주의할 점은 이 입력을 받는 과정이 비동기로 처리되기 때문에 반드시 해당 함수 내부에 문제 풀이를 적거나 내부에서 문제 풀이 함수를 실행시켜야 한다.

📚 JavaScript에서 여러가지 자료구조 사용법

이전에 C++로 코딩테스트를 준비할 때도 특별히 복잡한 자료구조를 사용하지는 않았다. 배열(Array)는 필수적으로 사용할 줄 알아야 하고, 알고리즘 구현에 있어서 스택(Stack)이나 큐(Queue)도 거의 필수적으로 알고 있어야 한다. 다음으로 작년 코딩테스트에서 자주 등장했던 것 같은데, 해시 테이블(Hash Table) 기반의 맵(Map)과 셋(Set) 자료구조 또한 알고 있으면 좋다. 그 이외에 다익스트라 알고리즘(Dijkstra's Algorithm)을 구현할 때 사용되기도 하고, 독립적으로 문제에 등장하기도 하는 우선순위 큐(Priority Queue)도 알고 있으면 좋긴 하지만, JavaScript에서는 우선순위 큐를 제공 해주는 라이브러리가 없기 때문에 직접 구현해야 하는데, 그렇게 간단하지는 않아서 JavaScript 코딩테스트에서는 등장하기 어려운 주제일 것으로 생각된다.

Array

배열을 선언하는 방법은 여러가지가 있고, 편한 방식을 사용하면 된다.

1차원 배열 선언 및 초기화

const arr = new Array(5).fill(0);
console.log(arr); // [0, 0, 0, 0, 0]

배열의 크기를 5로 선언하고, 모든 요소를 0으로 초기화하는 코드이다.

const arr = new Array(3).fill([]);
arr[0].push(1);
console.log(arr); // [[1], [1], [1]] ❗주의❗ (모두 같은 배열 참조)

const arr = new Array(3).fill(null).map(() => []);
arr[0].push(1);
console.log(arr); // [[1], [], []]

이 방식으로 선언할 때 조심해야할 것은 만약에 배열([ ])로 초기화 하게 되는 경우, 모든 요소에 같은 배열이 담기게 되어서 한 배열이 변경되면 모든 배열의 값이 변경되게 된다. 따라서 이 방식으로 2차원 배열을 선언할 때는 map()을 이용해서 배열을 넣어주자.

const arr = Array.from({ length: 5 }, () => 0);
console.log(arr); // [0, 0, 0, 0, 0]

const arr = Array.from({ length: 5 }, (_, i) => i + 1);
console.log(arr); // [1, 2, 3, 4, 5]

Array.from() 메서드를 이용해서 선언하는 방식도 있다. 첫 번째 인자로 length를 넘겨주고, 두 번째 인자의 함수에서 리턴해주는 값으로 초기화 하는 방식이다. 이렇게 선언하면 두 번째 인자 화살표 함수에서 인덱스를 활용해 순차적으로 값이 증가하도록 배열을 초기화 할 수도 있다.

2차원 배열 선언 및 초기화

const matrix = Array.from({ length: 3 }, () => new Array(3).fill(0));
console.log(matrix);
/*
[
  [0, 0, 0],
  [0, 0, 0],
  [0, 0, 0]
]
*/

위에서 봤던 방식을 섞어서 2차원 배열을 선언할 수도 있고,

const matrix = Array.from({ length: 3 }, () =>
  Array.from({ length: 3 }, () => 0)
);

console.log(matrix);
/*
[
  [0, 0, 0],
  [0, 0, 0],
  [0, 0, 0]
]
*/

한 가지 방법만으로 선언할 수도 있다. 위에서 말했던 fill() 메서드를 활용한 방식에서 배열로 초기화할 때만 주의해서 사용하도록 하자!

Stack

const stack = [];

stack.push(10);
stack.push(20);

console.log(stack.pop()); // 20
console.log(stack.pop()); // 10

다음으로 스택(Stack)과 큐(Queue)는 오히려 다른 언어들보다 쉬울 수도 있다. 배열을 그대로 활용하는 방법인데 배열 자체에 pop() 메서드가 존재해서 스택처럼 사용이 가능하다. stack.top()을 확인하고 싶을 땐, stack[stack.length - 1]과 같은 방식으로 확인할 수 있다.

Queue

const queue = [];

queue.push(10);
queue.push(20);

console.log(queue.shift()); // 10
console.log(queue.shift()); // 20

배열의 첫 번째 요소를 제거하는 것은 shift() 메서드를 이용해서 가능하다. 이를 통해 배열을 큐처럼 사용할 수 있고, 마찬가지로 queue.front()를 확인하고 싶으면, queue[0]을 이용해서 확인하도록 하자!

Map

const myMap = new Map();

myMap.set('key1', 'value1');
myMap.set({ a: 1 }, 'objectKey'); // 객체를 키 값으로 저장하는 것도 가능

console.log(myMap.get('key1'));   // 'value1'
console.log(myMap.has('key1'));   // true
myMap.delete('key1');
console.log(myMap.has('key1'));   // false

맵(Map)은 키(Key)와 값(Value) 형태로 데이터를 저장하는 자료구조로, 내부적으로 해시 테이블을 사용해 키를 해시 함수로 매핑한다. 이 덕분에 키를 사용해 값의 탐색, 삽입, 삭제를 시간복잡도 O(1) 에 처리할 수 있다.

Set

const mySet = new Set();

mySet.add(1);
mySet.add(2);
mySet.add(2); // 중복이므로 추가되지 않음

console.log(mySet.size);   // 2
console.log(mySet.has(2)); // true
mySet.delete(2);
console.log(mySet.has(2)); // false

셋(Set) 또한 해시 테이블 기반의 자료구조로 키 값은 없이 값(Value)만 저장하는 형태의 자료구조이다. 특징적인 것은 중복을 허용하지 않기 때문에 이미 존재하는 값을 추가(add)하게 되면 그 값은 무시되고 추가되지 않는다. 맵과 마찬가지로 탐색, 삽입, 삭제에 있어서 시간복잡도는 O(1) 이다.

🧐 그 외에 알아두면 유용한 메서드

간혹 기본 구현 문제에서 문자열을 다루는 문제가 나왔을 때 split(), join(), slice(), splice(), 메서드들을 잘 다루면 굉장히 유용할 때가 있다.

split()

const sentence = "Hello World from JavaScript!";
const words = sentence.split(" ");  // 공백을 기준으로 분리
console.log(words);  // ["Hello", "World", "from", "JavaScript!"]

const sentence = "Hello";
const chars = sentence.split("");  // 빈 문자열을 구분자로 사용하여 각 문자로 분리
console.log(chars);  // ["H", "e", "l", "l", "o"]

split() 메서드는 위에서 표준 입출력을 처리하는 부분에서도 사용했듯이 특정 문자 혹은 문자열을 구분자로 해서 문자열을 분할하고, 분할된 문자열을 배열에 담아 반환하는 메서드이다. split() 메서드의 괄호안에 아무것도 넣지 않으면 단순히 배열에 문자열을 그대로 넣어 반환해주기 때문에 한 글자씩 배열에 나눠 담고 싶다면 빈 문자열("")을 구분자로 지정해주자.

join()

const sentence = "Hello World from JavaScript!";
const words = sentence.split(" ");  // 공백을 기준으로 분리
console.log(words);  // ["Hello", "World", "from", "JavaScript!"]

const joinedSentence = words.join("-");  // 배열의 요소를 "-"로 연결
console.log(joinedSentence);  // "Hello-World-from-JavaScript!"

join() 메서드는 split()과 반대특정 문자 혹은 문자열을 구분자로 해서 배열에 있는 문자열들을 합쳐하나의 문자열로 만들어 반환하는 메서드이다. join() 메서드의 괄호안에 아무것도 넣지 않으면 기본값은 쉼표(,)이다.

slice()

// 배열에서 일부 요소 추출
const arr = [1, 2, 3, 4, 5];
const newArr = arr.slice(1, 4);  // 인덱스 1부터 4 이전까지의 요소를 반환
console.log(newArr);  // [2, 3, 4]

// 문자열에서 일부 추출
const str = "Hello, World!";
const substr = str.slice(7, 12);  // 인덱스 7부터 12 이전까지의 문자
console.log(substr);  // "World"

slice() 메서드는 문자열의 일부를 추출하여 새로운 배열이나 문자열을 반환하고, 원본 배열이나 문자열을 변경하지는 않는다.

splice()

// 배열에서 요소 제거
const arr = [1, 2, 3, 4, 5];
const removed = arr.splice(2, 2);  // 인덱스 2부터 2개 요소를 제거
console.log(arr);     // [1, 2, 5]
console.log(removed); // [3, 4]

splice() 메서드는 배열에서 요소를 추가하거나 제거할 때 사용한다. 원본 배열을 직접 변경하며, 제거된 부분을 반환한다.

arr.splice(startIdx, deleteCount, item1, item2, ..., itemN);

// 배열에서 요소 추가
const arr = [1, 2, 5];

arr.splice(2, 0, 3, 4);  // 인덱스 2에 3, 4 추가
console.log(arr);        // [1, 2, 3, 4, 5]

const arr = [1, 2, 5, 6];
const result = arr.splice(2, 1, 3, 4);

console.log(result);  // [5]
console.log(arr);     // [1, 2, 3, 4, 6]

splice() 메서드는 첫 번째 인자로 배열에서 제거할 부분의 시작 인덱스를 받고, 두 번째 인자로 해당 인덱스부터 몇 개를 제거할 것인지를 받는다. 세 번째 인자부터는 제거된 자리에 추가할 요소들을 받는다. 즉, splice()는 배열에서 요소를 제거하거나 추가하거나 둘 다 동시에 수행할 수 있는 메서드이다.

✍️ 글을 마치며...

매번 JavaScript 코딩테스트를 준비할 때마다 이 내용을 공부하는 것 같습니다. 매번 새롭게 찾아보는 것이 번거롭기도 하고, 그렇다고 또 찾아보지 않으면 은근히 헷갈리는 부분들이 많아서 이렇게 한 번에 정리를 해봤습니다! 앞으로는 시험 준비할 때 이 글 한 번 정독하면서 시작하려구요! 이번에 코딩테스트를 준비하면서 노션에 메모해 둔 내용을 어찌저찌 짜깁기만 해놓아서 보기 어려울수도 있지만, JavaScript 코딩테스트를 처음 준비하시는 분이라면 도움이 될만한 내용들이 많기 때문에 한 번쯤 읽어보면 좋을 것 같습니다! 자스코테 준비하시는 분들 화이팅입니다~!!😀

profile
Hello, world!

4개의 댓글

comment-user-thumbnail
2025년 2월 17일

🔥 와... JavaScript로 코딩 테스트 준비하는 법을 이렇게 깔끔하게 정리해주시다니 정말 대박입니다! 🚀 특히 표준 입력 처리 부분에서 fs와 readline 모듈을 비교해주신 게 너무 유용했어요! ✨ 자료구조 활용법까지 완벽 정리라니... 이 글 하나면 JS 코테 대비 끝이네요! 🙌 감사합니다! 💪🔥

1개의 답글
comment-user-thumbnail
2025년 2월 20일

JS로 코테 풀이하는게 새로 시작하기엔 조금 귀찮은 일인데.. 이렇게 상세히 정리해주시니 만족스럽네요!

저는 입력 코드 수정이 귀찮기도 하고, readline 코드가 좀 복잡하게 생겨서 fs 기반 레이아웃을 만들어 사용하고 있지만, 이 글을 읽어본 참에 readline도 한 번 써볼까 싶습니다.

좋은 글 감사드리고, (셀프 고생길인..) JS 코테 준비 화이팅입니다!

1개의 답글