키보드 시퀀스로 숨겨진 페이지 접근하기

Woody·2025년 9월 1일
0
post-thumbnail

개요

iframe으로 전환된 개발자 도구에 접근할 방법이 필요했다. URL 직접 접근은 차단되었고, 사용자에게 노출되지 않는 "조용한" 접근 방식이 필요했다.

이 문서를 읽고 나면:

  • event.keyevent.code의 차이를 이해할 수 있다
  • 다국어 키보드 환경에서 안정적인 키 입력 감지를 구현할 수 있다
  • 숨겨진 기능 접근을 위한 키 시퀀스를 만들 수 있다

1. 요구사항

배경 상황

  • 숨겨진 dev-tools 페이지에 접근 필요
  • 사용자에게 노출되지 않는 접근 방식 요구
  • iframe 환경에서 기존 route 접근 불가

해결 방안

특정 키 시퀀스 입력 시 페이지 이동 (예: devmode 입력)


2. 첫 번째 시도: event.key 사용

초기 구현

// ❌ 문제가 있던 초기 구현
const SECRET_SEQUENCE = ['d', 'e', 'v', 'm', 'o', 'd', 'e'];
const SECRET_SEQUENCE_KOREAN = ['ㄹ', 'ㄷ', 'ㅍ', 'ㅡ', 'ㅗ', 'ㄹ', 'ㄷ'];

const handleKeyPress = (event: KeyboardEvent) => {
  const key = event.key.toLowerCase();
  
  // 영문자 또는 한글 자음만 허용
  if (!/^[a-z]$/.test(key) && !/^[ㄱ-ㅎ]$/.test(key)) {
    return;
  }

  // 영문/한글 시퀀스 각각 확인
  const isEnglishMatch = newSequence.every(
    (k, index) => k === SECRET_SEQUENCE[index]
  );
  const isKoreanMatch = newSequence.every(
    (k, index) => k === SECRET_SEQUENCE_KOREAN[index]
  );
};

구현 논리

  1. event.key로 입력된 문자 감지
  2. 영문 시퀀스와 한글 시퀀스를 별도로 정의
  3. 자음 필터로 한글 입력 제한
  4. 두 시퀀스 중 하나라도 매치되면 성공

3. 문제 발견

실제 입력과 감지된 값의 불일치

사용자 입력: ㄹㄷㅍㅡㅗㄹㄷ (devmode의 한글 대응)
실제 감지: ㄹㄷㅍㄹㄷ (ㅡ, ㅗ 모음이 사라짐)

콘솔 로그 분석

🔍 Key pressed: ㄹ ✅
🔍 Key pressed: ㄷ ✅
🔍 Key pressed: ㅍ ✅
🚫 Key filtered out: ㅡ ❌ (모음이라 필터됨)
🚫 Key filtered out: ㅗ ❌ (모음이라 필터됨)
🔍 Key pressed: ㄹ ✅
🔍 Key pressed: ㄷ ✅
❌ Wrong sequence: [ㄹ,ㄷ,ㅍ,ㄹ,ㄷ] vs [ㄹ,ㄷ,ㅍ,ㅡ,ㅗ,ㄹ,ㄷ]

근본 원인

if (!/^[a-z]$/.test(key) && !/^[ㄱ-ㅎ]$/.test(key)) {
  return;
}

자음만 허용하는 필터가 한글 모음을 차단했다.

event.key의 한계

키 위치영문 모드한글 모드문제점
M키"m""ㅡ" (모음)자음 필터에 걸림
O키"o""ㅗ" (모음)자음 필터에 걸림

필요한 필터가 계속 증가:

  • 영문 소문자용 시퀀스
  • 영문 대문자용 시퀀스
  • 한글 자음용 시퀀스
  • 한글 모음용 시퀀스

이는 유지보수가 어렵고 확장성이 없는 구조다.


4. 해결: event.code 사용

event.key vs event.code

event.key: "무엇이 입력되었나?" (문자 중심)
event.code: "어디가 눌렸나?" (물리적 키 위치 중심)

비교표

상황event.keyevent.code결과
영문 D키"d""KeyD"✅ 일관성
한글 D키"ㄹ""KeyD"✅ 일관성
영문 M키"m""KeyM"✅ 일관성
한글 M키"ㅡ" (모음)"KeyM"✅ 일관성
CapsLock영향받음영향받지 않음✅ 안정성

개선된 구현

// ✅ 개선된 구현
const SECRET_SEQUENCE_CODES <= [
  'KeyD', 'KeyE', 'KeyV', 'KeyM', 'KeyO', 'KeyD', 'KeyE'
];

const handleKeyPress = (event: KeyboardEvent) => {
  const code = event.code;
  
  // 해당 키 위치가 시퀀스에 포함되는지만 확인
  if (!SECRET_SEQUENCE_CODES.includes(code)) {
    return;
  }
  
  // 단일 시퀀스로 모든 입력 모드 처리
  const isMatch = newSequence.every(
    (k, index) => k === SECRET_SEQUENCE_CODES[index]
  );
};

개선 효과:

  • 하나의 시퀀스로 모든 입력 모드 처리
  • 복잡한 필터링 로직 불필요
  • 키보드 레이아웃과 무관하게 동작

5. 완성된 구현

'use client';

import { useEffect, useState, useCallback } from 'react';
import { useRouter } from 'next/navigation';

// 물리적 키 코드 기반 시퀀스
const SECRET_SEQUENCE_CODES = [
  'KeyD',
  'KeyE',
  'KeyV',
  'KeyM',
  'KeyO',
  'KeyD',
  'KeyE',
];
const SEQUENCE_TIMEOUT = 5000; // 5초 내에 입력

export const useDevToolsAccess = () => {
  const router = useRouter();
  const [keySequence, setKeySequence] = useState<string[]>([]);
  const [lastKeyTime, setLastKeyTime] = useState<number>(0);

  const handleKeyPress = useCallback(
    (event: KeyboardEvent) => {
      const currentTime = Date.now();
      const code = event.code;

      // CapsLock 키 무시
      if (event.key === 'CapsLock') {
        return;
      }

      // 물리적 키 코드가 시퀀스에 포함되는지 확인
      if (!SECRET_SEQUENCE_CODES.includes(code)) {
        return;
      }

      setKeySequence((prevSequence) => {
        let newSequence: string[];

        // 시간 초과 시 시퀀스 리셋
        if (currentTime - lastKeyTime > SEQUENCE_TIMEOUT) {
          newSequence = [code];
        } else {
          newSequence = [...prevSequence, code];
        }

        // 시퀀스 완성 확인
        if (newSequence.length === SECRET_SEQUENCE_CODES.length) {
          const isMatch = newSequence.every(
            (k, index) => k === SECRET_SEQUENCE_CODES[index]
          );

          if (isMatch) {
            setTimeout(() => router.push('/dev-tools'), 0);
            return [];
          } else {
            return [];
          }
        }

        // 시퀀스가 너무 길면 마지막 N개만 유지
        if (newSequence.length > SECRET_SEQUENCE_CODES.length) {
          return newSequence.slice(-SECRET_SEQUENCE_CODES.length + 1);
        }

        return newSequence;
      });

      setLastKeyTime(currentTime);
    },
    [lastKeyTime, router]
  );

  useEffect(() => {
    document.addEventListener('keydown', handleKeyPress);

    return () => {
      document.removeEventListener('keydown', handleKeyPress);
    };
  }, [handleKeyPress]);

  return {
    currentSequence: process.env.NODE_ENV === 'development' ? keySequence : [],
    isActive:
      process.env.NODE_ENV === 'development' ? keySequence.length > 0 : false,
  };
};

6. 주요 기능

타임아웃 처리

if (currentTime - lastKeyTime > SEQUENCE_TIMEOUT) {
  newSequence = [code];
}

5초 이내에 시퀀스를 완성해야 한다.

시퀀스 길이 제한

if (newSequence.length > SECRET_SEQUENCE_CODES.length) {
  return newSequence.slice(-SECRET_SEQUENCE_CODES.length + 1);
}

불필요하게 긴 시퀀스를 방지한다.

개발 환경 디버깅

return {
  currentSequence: process.env.NODE_ENV === 'development' ? keySequence : [],
  isActive: process.env.NODE_ENV === 'development' ? keySequence.length > 0 : false,
};

개발 환경에서만 현재 진행 상황을 노출한다.


7. 배운 점

event.key와 event.code의 차이

  • event.key: 문자 중심, 입력 모드에 영향받음
  • event.code: 위치 중심, 입력 모드와 무관

다국어 환경 고려

  • 물리적 키 위치는 키보드 레이아웃과 무관하게 일관됨
  • 사용자는 입력 모드를 의식하지 않고 사용 가능

디버깅의 중요성

console.log('Key:', event.key, 'Code:', event.code);

두 값을 함께 로깅하여 차이점을 명확히 파악할 수 있다.

실제 사용 환경 테스트

  • 가정을 검증하라: 초기 구현이 동작해도 모든 상황에서 동작하지 않을 수 있다
  • 다국어를 고려하라: 한국어 사용자는 한글 입력이 자연스럽다
  • 브라우저 API를 깊이 이해하라: API의 미묘한 차이가 큰 영향을 미친다

참고 자료

profile
프론트엔드 개발자로 살아가기

0개의 댓글