나만의 자바스크립트 컨텍스트 메뉴 만들기
자바스크립트를 사용해서 웹 페이지에 나만의 컨텍스트 메뉴(오른쪽 클릭 메뉴)를 만드는 건 굉장히 유용해요. 기본 브라우저 메뉴를 대체하거나, 특정 요소에 대한 추가 기능을 제공할 때 활용할 수 있죠.
컨텍스트 메뉴를 만드는 과정은 크게 세 가지 단계로 나눌 수 있어요:
- HTML 구조 만들기: 메뉴가 나타날 컨테이너와 메뉴 아이템들을 정의해요.
- CSS로 스타일링하기: 메뉴가 예쁘게 보이도록 디자인하고, 기본적으로는 숨겨두었다가 필요할 때만 나타나도록 해요.
- JavaScript로 동적 기능 추가하기: 오른쪽 클릭 이벤트를 감지하고, 메뉴를 적절한 위치에 표시하며, 메뉴 아이템 클릭 시 특정 동작을 수행하도록 해요.
아래는 각 단계별 코드와 상세 설명이에요.
1. HTML 구조 만들기
먼저, 웹 페이지에 컨텍스트 메뉴로 사용할 HTML 요소를 정의해야 해요. 일반적으로 div
태그를 사용하고, 그 안에 메뉴 아이템들을 ul
과 li
태그로 구성하는 게 일반적이죠.
<!-- 컨텍스트 메뉴가 나타날 대상 요소 (예시) -->
<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
확장자로 저장한 후 웹 브라우저에서 열어보시면 직접 만든 컨텍스트 메뉴가 동작하는 것을 확인할 수 있어요. 이 예시 코드에는 **블로그 본문에 설명된 targetElement
와 customContextMenu
요소가 포함**되어 있으니, 직접 테스트해보실 때 활용하시면 됩니다.
<!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>
댓글 없음:
댓글 쓰기