pop()은 O(1), shift()는 O(n)

자바스크립트에서 배열을 사용하다 보면, pop()과 shift() 메서드로 배열의 원소를 제거할 수 있다. 일반적으로 pop()은 배열의 맨 뒤 원소를, shift()는 배열의 맨 앞 원소를 제거하는 메서드로 알려져 있다. 그런데, pop()과 shift()를 구글에서 찾다 보면, 시간복잡도 이야기가 늘 따라붙는다. pop()은 O(1) time에, shift()는 O(n) time에 수행된다는 점이다.

 

기존 구현방식: 배열에 역순으로 값 넣기

배열의 가장 뒤에서 원소를 뽑아내는 것이 효율적이므로, 나는 큐를 사용하는 경우에 역순으로 배열에 집어넣는 선택을 하고 큐를 사용하고 있었다. 이 방법은 큐의 길이를 동적으로 조정해줘야 하는 경우가 아니라면 꽤 간단히 구현하고 사용할 수 있는 방법이었다. 그래서 최초 구현 당시 아무 고민없이 구현하고 사용했다. 1-2-3-4-5 의 순서로 이루어진 데이터를 5-4-3-2-1 순서로 배열에 넣으면 pop() 메서드로 1을 꺼낼 수 있기 때문이다.

하지만 이 방식도 늘 좋은 것은 아니다. 큐의 크기가 변하면 길이를 조정해 주어야 하는 문제가 있고, FIFO 원칙이 직관적으로 드러나지 않아 유지보수에 어려움을 겪을 수 있어 보인다. 향후 개발의 번거로움을 줄이기 위해 새로운 방식을 찾아보기로 했다.

 

대안1: 스택(Stack) 두 개를 사용하여 FIFO 원칙 구현

스택은 push()와 pop()이 O(1) 타임에 동작하기 때문에 이를 두 개 사용하여 효율적인 큐를 만들 수 있다. 첫 번째 스택(stack1)은 원소를 큐에 넣을 때 사용하고, 두 번째 스택(stack2)은 원소를 큐에서 꺼낼 때 사용하는 방법이다. 원소를 큐에서 꺼낼 때, stack2가 비어 있으면, stack1의 모든 원소를 pop()하여 stack2에 push()하고, 그 후 stack2에서 원소를 pop()하여 꺼내는 방식이다. 이렇게 하면 두 스택을 이용해 큐의 앞부분에서 꺼내는 것처럼 동작하게 된다. 그럼 개별 원소 입장에서 생각해보면, stack1.pop() -> stack2.push() -> stack2.pop() 순서로 꺼내지므로 O(3)로 사실상 O(1)같이 동작할 수 있는 것이다.

class Queue {
  constructor() {
    this.stack1 = [];
    this.stack2 = [];
  }

  // 큐에 원소를 추가하는 메서드
  enqueue(value) {
    this.stack1.push(value);
  }

  // 큐에서 원소를 제거하고 반환하는 메서드
  dequeue() {
    if (this.stack2.length === 0) {
      // stack2가 비어 있으면 stack1의 모든 원소를 옮긴다
      while (this.stack1.length > 0) {
        this.stack2.push(this.stack1.pop());
      }
    }
    return this.stack2.pop();
  }

  // 큐가 비어 있는지 확인하는 메서드
  isEmpty() {
    return this.stack1.length === 0 && this.stack2.length === 0;
  }
}

 

대안2: Linked List를 이용한 Queue 클래스 구현

링크드 리스트를 사용하면, 맨 앞에서 원소를 제거하는 dequeue()와 뒤에 원소를 삽입하는 enqueue() 모두 O(1) 시간복잡도를 유지할 수 있다. 성능을 더욱 최적화해야 하는 경우에는 링크드 리스트를 사용하는 것이 좋아 보인다.

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

class Queue {
  constructor() {
    this.front = null;
    this.rear = null;
  }

  enqueue(value) {
    const newNode = new Node(value);
    if (!this.rear) {
      this.front = this.rear = newNode;
    } else {
      this.rear.next = newNode;
      this.rear = newNode;
    }
  }

  dequeue() {
    if (!this.front) return null;
    const value = this.front.value;
    this.front = this.front.next;
    if (!this.front) this.rear = null;
    return value;
  }

  isEmpty() {
    return !this.front;
  }
}

 

Linked List를 이용한 Queue 구현이 O(n) 타임 피하기라는 원래의 목적과, 클래스화를 통한 유지보수 용이성 개선이라는 목적을 모두 달성할 수 있어서 이 방식으로 가기로 마음먹었다. 코드 수정하러 가자.

728x90

문제상황

유저가 textarea에서 입력을 하면, 드랍다운에 추천 텍스트를 보여주고, 드랍다운 클릭을 통해 텍스트를 자동으로 채워주는 기능을 개발하고 있었다. 그런데 textarea가 비활성화되는 blur 이벤트가 click 이벤트보다 먼저 발생하여 드랍다운 메뉴가 먼저 사라지고, 유저는 드랍다운 메뉴가 있던 빈 자리를 클릭하게 되는 것이었다.

 

ChatGPT의 해결 방법 : setTimeout()

gpt는 setTimeout()을 사용할 것을 추천해주었다. 즉, blur 이벤트의 처리를 약간 지연시켜 click 이벤트가 먼저 발생하도록 만드는 것이다. setTimeout()을 이용해 blur 이벤트의 콜백 함수(드랍다운의 display를 none으로 설정)를 100ms정도 되는 짧은 시간 뒤에 실행되도록 지연시킨다. 이렇게 하면 click 이벤트가 먼저 발생하고, 그 후에 blur 이벤트가 발생하게 되어 드롭다운 메뉴 등을 정상적으로 선택할 수 있게 된다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Textarea Blur Event Example</title>
</head>
<body>
    <textarea id="myTextarea" rows="4" cols="50">Click outside of this textarea to see the issue.</textarea>
    <div id="dropdownMenu" style="display: none; border: 1px solid #000; padding: 10px; position: absolute;">
        <p>Dropdown Item 1</p>
        <p>Dropdown Item 2</p>
        <p>Dropdown Item 3</p>
    </div>

    <script>
        const textarea = document.getElementById('myTextarea');
        const dropdownMenu = document.getElementById('dropdownMenu');

        textarea.addEventListener('focus', () => {
            // Textarea가 포커스를 얻으면 드롭다운 메뉴를 보여줍니다.
            dropdownMenu.style.display = 'block';
        });

        textarea.addEventListener('blur', () => {
            // setTimeout을 사용하여 blur 이벤트의 처리를 지연시킵니다.
            setTimeout(() => {
                dropdownMenu.style.display = 'none';
            }, 100);
        });

        dropdownMenu.addEventListener('click', (event) => {
            // 드롭다운 메뉴가 클릭되었을 때의 동작을 정의합니다.
            alert('Dropdown item clicked!');
        });
    </script>
</body>
</html>

 

실제 해결 방법 : mousedown 이벤트로 blur 이벤트 리스너 제거

우선 기능 개발이 급해 gpt의 추천대로 setTimeout()을 사용했지만, 급한 불을 끄고 난 뒤에 일정 시간을 지연시키는 방법이 밀리세컨을 직접 저렇게 숫자로 코드에 넣는 것이 하드코딩스럽기도 하고 괜히 마음에 걸렸다. 나중에 수정한 방법은 mousedown 이벤트를 활용한 방법이다.

mousedown 이벤트는 사용자가 마우스 버튼을 누를 때 발생하므로, click 이벤트보다 먼저 발생한다. 이 이벤트를 활용해 사용자가 클릭하는 순간, blur 이벤트 리스너를 제거하여 클릭이 먼저 처리되도록 해결하였다. (나중에 gpt에게 다시 물어보니 mouseup 이벤트에서 다시 blur 이벤트 리스너를 추가하여 클릭이 완료된 후에 blur 이벤트가 발생할 수 있도록 원상복구 하란다. 출근하면 바꿔놔야겠다.)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Textarea Blur Event Example</title>
</head>
<body>
    <textarea id="myTextarea" rows="4" cols="50">Click outside of this textarea to see the issue.</textarea>
    <div id="dropdownMenu" style="display: none; border: 1px solid #000; padding: 10px; position: absolute;">
        <p>Dropdown Item 1</p>
        <p>Dropdown Item 2</p>
        <p>Dropdown Item 3</p>
    </div>

    <script>
        const textarea = document.getElementById('myTextarea');
        const dropdownMenu = document.getElementById('dropdownMenu');

        function hideDropdown() {
            dropdownMenu.style.display = 'none';
        }

        // blur 이벤트 리스너를 추가하는 함수
        function addBlurListener() {
            textarea.addEventListener('blur', hideDropdown);
        }

        // blur 이벤트 리스너를 제거하는 함수
        function removeBlurListener() {
            textarea.removeEventListener('blur', hideDropdown);
        }

        textarea.addEventListener('focus', () => {
            // Textarea가 포커스를 얻으면 드롭다운 메뉴를 보여줍니다.
            dropdownMenu.style.display = 'block';
        });

        textarea.addEventListener('mousedown', () => {
            // mousedown 시점에 blur 이벤트 리스너를 제거합니다.
            removeBlurListener();
        });

        document.addEventListener('mouseup', () => {
            // mouseup 시점에 blur 이벤트 리스너를 다시 추가합니다.
            addBlurListener();
        });

        dropdownMenu.addEventListener('click', (event) => {
            // 드롭다운 메뉴가 클릭되었을 때의 동작을 정의합니다.
            alert('Dropdown item clicked!');
        });

        // 초기에는 blur 이벤트 리스너가 활성화된 상태로 시작합니다.
        addBlurListener();
    </script>
</body>
</html>

 

결론

사실 두 방식 모두 잘 작동했기에 뭐가 더 나은 방법이라 말하긴 어렵다. 내부 구현이 어떻든 유저가 체감하는 퍼포먼스만 좋으면 되니까...ㅋㅋㅋ 그래도 역시 지연시킬 시간을 얼마를 주는게 좋을지 고민하는 것보단 동적으로 이벤트 리스너를 제거하고 다시 추가해주는게 좀 더 내스타일 해결방법이란 생각이 든다.

728x90

1. textarea의 focus 이벤트

 

2. blur 이벤트와 click이벤트 순서 정리

포스팅: https://luceeverde.tistory.com/entry/blur-이벤트와-click-이벤트-발생-순서-문제

 

 

3. setTimeout()을 이용한 디바운싱 구현

 

4. JWT를 이용한 로그인 기능 구현 - JWT 개념정리 + 어떻게 사용하는지 정리

728x90

'🔨 개발 > 📚 개발지식모음집' 카테고리의 다른 글

파이썬 람다 함수 사용법 정리  (2) 2024.11.04
YAML 파일이란?  (1) 2024.09.13
20240903 requirements.txt 문제  (0) 2024.09.03
20240808  (0) 2024.08.08
20240807  (0) 2024.08.07

+ Recent posts