나만의 자바스크립트 컨텍스트 메뉴 | Custom JavaScript Context Menu

나만의 자바스크립트 컨텍스트 메뉴 | Custom JavaScript Context Menu

나만의 자바스크립트 컨텍스트 메뉴 만들기

자바스크립트를 사용해서 웹 페이지에 나만의 컨텍스트 메뉴(오른쪽 클릭 메뉴)를 만드는 건 굉장히 유용해요. 기본 브라우저 메뉴를 대체하거나, 특정 요소에 대한 추가 기능을 제공할 때 활용할 수 있죠.

컨텍스트 메뉴를 만드는 과정은 크게 세 가지 단계로 나눌 수 있어요:

  1. HTML 구조 만들기: 메뉴가 나타날 컨테이너와 메뉴 아이템들을 정의해요.
  2. CSS로 스타일링하기: 메뉴가 예쁘게 보이도록 디자인하고, 기본적으로는 숨겨두었다가 필요할 때만 나타나도록 해요.
  3. JavaScript로 동적 기능 추가하기: 오른쪽 클릭 이벤트를 감지하고, 메뉴를 적절한 위치에 표시하며, 메뉴 아이템 클릭 시 특정 동작을 수행하도록 해요.

아래는 각 단계별 코드와 상세 설명이에요.

1. HTML 구조 만들기

먼저, 웹 페이지에 컨텍스트 메뉴로 사용할 HTML 요소를 정의해야 해요. 일반적으로 div 태그를 사용하고, 그 안에 메뉴 아이템들을 ulli 태그로 구성하는 게 일반적이죠.


<!-- 컨텍스트 메뉴가 나타날 대상 요소 (예시) -->
<div id="targetElement" style="width: 300px; height: 200px; border: 2px dashed #ccc; display: flex; justify-content: center; align-items: center; margin: 50px; cursor: pointer;">
    오른쪽 클릭 대상 예시
</div>

<!-- 사용자 정의 컨텍스트 메뉴 -->
<div id="customContextMenu" class="custom-context-menu">
    <ul>
        <li data-action="action1">새 탭에서 열기</li>
        <li data-action="action2">복사하기</li>
        <li data-action="action3">붙여넣기</li>
        <li class="separator"></li> <!-- 구분선 -->
        <li data-action="action4">다른 이름으로 저장</li>
        <li data-action="action5">정보 보기</li>
    </ul>
</div>
            

HTML 코드 설명:

  • #targetElement: 이 요소 위에서 오른쪽 클릭을 했을 때 사용자 정의 컨텍스트 메뉴가 나타나도록 할 대상이에요. 실제 웹 페이지에서는 이미지, 링크, 특정 섹션 등이 될 수 있겠죠. 간단한 시각적 구분을 위해 인라인 스타일을 적용했습니다.
  • #customContextMenu: 이 부분이 실제로 컨텍스트 메뉴로 표시될 영역이에요. 초기에는 CSS로 숨겨져 있다가 JavaScript에 의해 보이게 될 거예요.
  • data-action: 각 메뉴 아이템에 고유한 data-action 속성을 부여해서 JavaScript에서 어떤 기능을 수행할지 쉽게 구분할 수 있도록 했어요. 이는 <li data-action="action1"> 형태로 사용됩니다.
  • separator: 메뉴 아이템 사이에 시각적인 구분선을 넣고 싶을 때 사용할 수 있는 클래스예요. CSS에서 이 클래스에 대한 스타일이 정의됩니다.

2. CSS로 스타일링하기

컨텍스트 메뉴는 평소에는 보이지 않다가 오른쪽 클릭 시 나타나야 해요. 그리고 브라우저 기본 컨텍스트 메뉴와 구분되도록 디자인을 입혀줄 거예요.


/* 컨텍스트 메뉴 기본 스타일 */
.custom-context-menu {
    display: none; /* 초기에는 숨김 */
    position: absolute; /* 절대 위치로 포지셔닝 */
    background-color: #fff;
    border: 1px solid #ddd;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
    border-radius: 5px;
    z-index: 1000; /* 다른 요소 위에 오도록 z-index 설정 */
    min-width: 150px; /* 최소 너비 설정 */
    padding: 5px 0; /* 상하 여백 */
}

.custom-context-menu ul {
    list-style: none; /* 목록 마커 제거 */
    margin: 0;
    padding: 0;
}

.custom-context-menu li {
    padding: 8px 15px;
    cursor: pointer;
    font-size: 14px;
    color: #333;
    white-space: nowrap; /* 텍스트 줄바꿈 방지 */
}

.custom-context-menu li:hover {
    background-color: #f0f0f0;
}

/* 구분선 스타일 */
.custom-context-menu li.separator {
    height: 1px;
    background-color: #eee;
    margin: 5px 0;
    padding: 0; /* 구분선은 패딩 없음 */
    cursor: default; /* 구분선은 클릭 안 되게 */
}

.custom-context-menu li.separator:hover {
    background-color: #eee; /* 호버 시에도 색상 유지 */
}

/* 활성화된 메뉴 스타일 (JavaScript에서 추가/제거) */
.custom-context-menu.active {
    display: block; /* 활성화 시 보이도록 */
}

/* 타겟 요소 스타일 (예시에서만 사용) */
#targetElement {
    background-color: #333;
    color: #fff;
    border: 2px dashed #ccc;
    border-radius: 8px;
    display: flex;
    justify-content: center;
    align-items: center;
    margin: 50px auto;
    cursor: pointer;
    width: 300px;
    height: 200px;
    font-size: 18px;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
}
            

CSS 코드 설명:

  • .custom-context-menu: 메뉴의 초기 상태(숨김), 위치 지정 방식, 배경색, 테두리, 그림자 효과 등을 정의합니다. z-index를 높게 설정하여 다른 요소 위에 표시되도록 합니다.
  • .custom-context-menu ul, li: 목록 마커를 제거하고, 각 메뉴 아이템의 패딩, 글꼴, 색상, 마우스 오버 효과 등을 설정합니다.
  • .custom-context-menu li.separator: 메뉴 아이템 사이의 구분선 스타일을 정의합니다.
  • .custom-context-menu.active: JavaScript에 의해 이 클래스가 추가되면 메뉴가 보이도록 display: block;을 적용합니다.
  • #targetElement: 오른쪽 클릭이 발생할 대상 요소의 기본적인 시각적 스타일을 설정합니다.

3. JavaScript로 동적 기능 추가하기

이제 가장 중요한 JavaScript 코드를 작성할 차례예요. 여기서는 다음을 처리할 거예요:

  • 오른쪽 클릭 이벤트 감지: 특정 요소에서 contextmenu 이벤트를 가로채서 기본 브라우저 메뉴가 뜨는 것을 막고, 사용자 정의 메뉴를 표시해요.
  • 메뉴 위치 설정: 마우스 클릭 지점(X, Y 좌표)에 메뉴가 나타나도록 위치를 계산해요.
  • 메뉴 아이템 클릭 처리: 메뉴 아이템이 클릭되었을 때 어떤 기능이 실행될지 정의해요.
  • 메뉴 숨기기: 메뉴 외부를 클릭하거나 스크롤했을 때 메뉴가 자동으로 사라지도록 해요.

document.addEventListener('DOMContentLoaded', () => {
    // 이 스크립트 블록은 블로그 독자가 코드를 복사하여 자신의 HTML 파일에서 실행할 때 유효합니다.
    // 블로그 게시물 자체에는 'targetElement' 또는 'customContextMenu' 요소가 존재하지 않습니다.
    
    // 예시 요소를 가정 (블로그 본문에는 없지만, 독자가 직접 만들어서 테스트할 수 있습니다.)
    const targetElement = document.getElementById('targetElement'); 
    const customContextMenu = document.getElementById('customContextMenu'); 

    if (targetElement && customContextMenu) { 
        targetElement.addEventListener('contextmenu', (e) => {
            e.preventDefault(); // 브라우저 기본 컨텍스트 메뉴 표시 방지

            let x = e.clientX;
            let y = e.clientY;

            const menuWidth = customContextMenu.offsetWidth;
            const menuHeight = customContextMenu.offsetHeight;
            const windowWidth = window.innerWidth;
            const windowHeight = window.innerHeight;

            // 메뉴가 화면 밖으로 나가지 않도록 조정
            if (x + menuWidth > windowWidth) {
                x = windowWidth - menuWidth - 5;
            }
            if (y + menuHeight > windowHeight) {
                y = windowHeight - menuHeight - 5;
            }

            customContextMenu.style.left = `${x}px`;
            customContextMenu.style.top = `${y}px`;

            customContextMenu.classList.add('active'); // 메뉴 표시 (active 클래스 처리)
        });

        customContextMenu.addEventListener('click', (e) => {
            const clickedItem = e.target.closest('li'); 
            if (clickedItem && !clickedItem.classList.contains('separator')) {
                const action = clickedItem.dataset.action; 
                performAction(action); 
                customContextMenu.classList.remove('active');
            }
        });

        document.addEventListener('click', (e) => {
            if (!customContextMenu.contains(e.target) && e.target !== targetElement) {
                customContextMenu.classList.remove('active');
            }
        });

        window.addEventListener('scroll', () => {
            if (customContextMenu.classList.contains('active')) {
                customContextMenu.classList.remove('active');
            }
        });
    } else {
        console.warn("코드 예시에 필요한 'targetElement' 또는 'customContextMenu' 요소를 찾을 수 없습니다. 이 코드는 독자가 직접 실행할 때 유효합니다.");
    }

    // 특정 액션을 수행하는 함수 (여기서 원하는 기능을 구현)
    function performAction(action) {
        switch (action) {
            case 'action1':
                alert('새 탭에서 열기 액션 실행!');
                window.open('about:blank', '_blank');
                break;
            case 'action2':
                alert('복사하기 액션 실행!');
                // navigator.clipboard API는 보안상 HTTPS 환경 또는 'localhost'에서만 작동합니다.
                if (navigator.clipboard) {
                    navigator.clipboard.writeText('이것은 복사된 텍스트입니다.')
                        .then(() => console.log('클립보드에 복사됨!'))
                        .catch(err => console.error('복사 실패:', err));
                } else {
                    console.warn('클립보드 API를 지원하지 않는 브라우저입니다.');
                }
                break;
            case 'action3':
                alert('붙여넣기 액션 실행!');
                if (navigator.clipboard) {
                    navigator.clipboard.readText()
                        .then(text => alert(`클립보드 내용: ${text}`))
                        .catch(err => console.error('붙여넣기 실패:', err));
                } else {
                    console.warn('클립보드 API를 지원하지 않는 브라우저입니다.');
                }
                break;
            case 'action4':
                alert('다른 이름으로 저장 액션 실행!');
                const content = "저장할 파일 내용입니다.";
                const blob = new Blob([content], { type: 'text/plain' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = '내파일.txt';
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
                break;
            case 'action5':
                alert('정보 보기 액션 실행!');
                console.log('정보 보기 기능 구현 예정');
                break;
            default:
                console.log(`알 수 없는 액션: ${action}`);
        }
    }
});
            

JavaScript 코드 설명:

  • DOMContentLoaded: 문서의 HTML이 완전히 로드된 후에 스크립트가 실행되도록 해요.
  • e.preventDefault(): 브라우저의 기본 contextmenu 이벤트를 막아줘요. 이게 없으면 사용자 정의 메뉴와 브라우저 메뉴가 동시에 뜨게 될 거예요.
  • 마우스 좌표 및 메뉴 위치 조정: e.clientX, e.clientY를 사용하여 마우스 클릭 지점에 메뉴가 나타나도록 하고, 메뉴가 화면 경계를 넘어가지 않도록 위치를 자동으로 조정하는 로직이 포함되어 있어요.
  • 메뉴 표시/숨김: customContextMenu.classList.add('active')를 통해 메뉴를 보이게 하고, 메뉴 아이템 클릭이나 메뉴 외부 클릭 시 classList.remove('active')를 통해 메뉴를 숨겨요.
  • 메뉴 아이템 클릭 처리: e.target.closest('li')를 사용하여 클릭된 메뉴 아이템을 찾고, 해당 아이템의 data-action 속성을 기반으로 performAction 함수를 호출해요.
  • performAction(action): 이 함수는 각 data-action 값에 따라 실제 기능을 수행해요. 여기에 원하는 자바스크립트 기능을 구현하면 돼요.
  • 클립보드 API 주의사항: navigator.clipboard API는 보안상의 이유로 **HTTPS 환경 또는 `localhost`에서만 작동**합니다. 이 점을 인지하고 개발/테스트 환경을 설정해야 해요.

전체 코드 예시 (HTML 파일로 저장하여 실행 가능)

이 모든 코드를 하나의 HTML 파일로 합치면 다음과 같습니다. 이 파일을 .html 확장자로 저장한 후 웹 브라우저에서 열어보시면 직접 만든 컨텍스트 메뉴가 동작하는 것을 확인할 수 있어요. 이 예시 코드에는 **블로그 본문에 설명된 targetElementcustomContextMenu 요소가 포함**되어 있으니, 직접 테스트해보실 때 활용하시면 됩니다.


<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>나만의 자바스크립트 컨텍스트 메뉴 예제</title>
    <!-- Prism.js CSS -->
    <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet" />
    <style>
        /* 컨텍스트 메뉴 기본 스타일 */
        .custom-context-menu {
            display: none; /* 초기에는 숨김 */
            position: absolute; /* 절대 위치로 포지셔닝 */
            background-color: #fff;
            border: 1px solid #ddd;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
            border-radius: 5px;
            z-index: 1000; /* 다른 요소 위에 오도록 z-index 설정 */
            min-width: 150px; /* 최소 너비 설정 */
            padding: 5px 0; /* 상하 여백 */
        }

        .custom-context-menu ul {
            list-style: none; /* 목록 마커 제거 */
            margin: 0;
            padding: 0;
        }

        .custom-context-menu li {
            padding: 8px 15px;
            cursor: pointer;
            font-size: 14px;
            color: #333;
            white-space: nowrap; /* 텍스트 줄바꿈 방지 */
        }

        .custom-context-menu li:hover {
            background-color: #f0f0f0;
        }

        /* 구분선 스타일 */
        .custom-context-menu li.separator {
            height: 1px;
            background-color: #eee;
            margin: 5px 0;
            padding: 0; /* 구분선은 패딩 없음 */
            cursor: default; /* 구분선은 클릭 안 되게 */
        }

        .custom-context-menu li.separator:hover {
            background-color: #eee; /* 호버 시에도 색상 유지 */
        }

        /* 활성화된 메뉴 스타일 (JavaScript에서 추가/제거) */
        .custom-context-menu.active {
            display: block; /* 활성화 시 보이도록 */
        }

        /* 타겟 요소 스타일 */
        #targetElement {
            background-color: #333;
            color: #fff;
            border: 2px dashed #ccc;
            border-radius: 8px;
            display: flex;
            justify-content: center;
            align-items: center;
            margin: 50px auto; /* 중앙 정렬 */
            cursor: pointer;
            width: 300px;
            height: 200px;
            font-size: 18px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
        }
    </style>
</head>
<body>
    <h1 style="text-align: center; margin-top: 50px;">나만의 자바스크립트 컨텍스트 메뉴 테스트 영역</h1>
    <p style="text-align: center; font-size: 1.1em; color: #555;">아래 회색 박스를 오른쪽 클릭하여 커스텀 메뉴를 확인해보세요!</p>

    <div id="targetElement">
        여기를 오른쪽 클릭해보세요!
    </div>

    <div id="customContextMenu" class="custom-context-menu">
        <ul>
            <li data-action="action1">새 탭에서 열기</li>
            <li data-action="action2">복사하기</li>
            <li data-action="action3">붙여넣기</li>
            <li class="separator"></li>
            <li data-action="action4">다른 이름으로 저장</li>
            <li data-action="action5">정보 보기</li>
        </ul>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', () => {
            const targetElement = document.getElementById('targetElement'); 
            const customContextMenu = document.getElementById('customContextMenu'); 

            // targetElement와 customContextMenu 요소가 존재하는지 확인
            if (targetElement && customContextMenu) { 
                targetElement.addEventListener('contextmenu', (e) => {
                    e.preventDefault(); // 브라우저 기본 컨텍스트 메뉴 표시 방지

                    let x = e.clientX;
                    let y = e.clientY;

                    const menuWidth = customContextMenu.offsetWidth;
                    const menuHeight = customContextMenu.offsetHeight;
                    const windowWidth = window.innerWidth;
                    const windowHeight = window.innerHeight;

                    // 메뉴가 화면 밖으로 나가지 않도록 조정
                    if (x + menuWidth > windowWidth) {
                        x = windowWidth - menuWidth - 5; 
                    }
                    if (y + menuHeight > windowHeight) {
                        y = windowHeight - menuHeight - 5; 
                    }

                    customContextMenu.style.left = `${x}px`;
                    customContextMenu.style.top = `${y}px`;

                    customContextMenu.classList.add('active'); // 메뉴 표시 (active 클래스 처리)
                });

                customContextMenu.addEventListener('click', (e) => {
                    const clickedItem = e.target.closest('li'); // 클릭된 li 요소 찾기
                    if (clickedItem && !clickedItem.classList.contains('separator')) {
                        const action = clickedItem.dataset.action; // data-action 값 가져오기
                        performAction(action); // 해당 액션 수행 함수 호출
                        customContextMenu.classList.remove('active'); // 메뉴 아이템 클릭 후 메뉴 다시 숨김
                    }
                });

                document.addEventListener('click', (e) => {
                    // 클릭된 요소가 컨텍스트 메뉴 내부가 아니고, 타겟 요소도 아니라면 메뉴 숨김
                    if (!customContextMenu.contains(e.target) && e.target !== targetElement) {
                        customContextMenu.classList.remove('active');
                    }
                });

                window.addEventListener('scroll', () => {
                    // 스크롤 시 메뉴 숨기기 (선택 사항)
                    if (customContextMenu.classList.contains('active')) {
                        customContextMenu.classList.remove('active');
                    }
                });
            } else {
                console.error("오류: 'targetElement' 또는 'customContextMenu' 요소를 찾을 수 없습니다. 예시 HTML을 확인해주세요.");
            }

            // 특정 액션을 수행하는 함수 (여기서 원하는 기능을 구현)
            function performAction(action) {
                switch (action) {
                    case 'action1':
                        alert('새 탭에서 열기 액션 실행!');
                        window.open('about:blank', '_blank');
                        break;
                    case 'action2':
                        alert('복사하기 액션 실행!');
                        // navigator.clipboard API는 보안상 HTTPS 환경 또는 'localhost'에서만 작동합니다.
                        if (navigator.clipboard) {
                            navigator.clipboard.writeText('이것은 복사된 텍스트입니다.')
                                .then(() => console.log('클립보드에 복사됨!'))
                                .catch(err => console.error('복사 실패:', err));
                        } else {
                            console.warn('클립보드 API를 지원하지 않는 브라우저입니다.');
                        }
                        break;
                    case 'action3':
                        alert('붙여넣기 액션 실행!');
                        if (navigator.clipboard) {
                            navigator.clipboard.readText()
                                .then(text => alert(`클립보드 내용: ${text}`))
                                .catch(err => console.error('붙여넣기 실패:', err));
                        } else {
                            console.warn('클립보드 API를 지원하지 않는 브라우저입니다.');
                        }
                        break;
                    case 'action4':
                        alert('다른 이름으로 저장 액션 실행!');
                        const content = "저장할 파일 내용입니다.";
                        const blob = new Blob([content], { type: 'text/plain' });
                        const url = URL.createObjectURL(blob);
                        const a = document.createElement('a');
                        a.href = url;
                        a.download = '내파일.txt';
                        document.body.appendChild(a);
                        a.click();
                        document.body.removeChild(a);
                        URL.revokeObjectURL(url);
                        break;
                    case 'action5':
                        alert('정보 보기 액션 실행!');
                        console.log('정보 보기 기능 구현 예정');
                        break;
                    default:
                        console.log(`알 수 없는 액션: ${action}`);
                }
            }
        });
    </script>
   
</body>
</html>
            

© 2025 [작성자 이름]. 모든 권리 보유.

댓글 없음:

댓글 쓰기

댓글 폭탄 해결! 자바스크립트 댓글 접기/펼치기로 가독성 200% 높이는 법(Solve Comment Chaos: Elevate Readability 200% with JS Comment Folding/Unfolding)

내 웹사이트에 적용! 초간단 자바스크립트 댓글 펼치기/숨기기 튜토리얼 내 웹사이트에 적용! 초간단 자바스크립트 댓글 펼치기/숨기기 튜토...