WeniVooks

검색

아는 만큼 보이는 웹접근성 WCAG 2.2

가이드라인 2.2 충분한 시간 제공

고령자
전맹장애인과 운동장애인

충분한 시간 지침은 사용자가 콘텐츠를 읽고 사용하는 데 충분한 시간을 제공해야 한다는 원칙입니다. 우리 모두가 정보를 처리하는 속도가 다르듯이, 웹 사용자들도 각자의 속도로 콘텐츠를 읽고 이해합니다. 특히 인지적인 어려움이 있거나 읽기 속도가 느린 사용자들을 배려하는 것이 이 지침의 핵심입니다. 이 지침을 준수함으로써 사용자들은 스트레스 없이 자신의 속도로 콘텐츠를 이용할 수 있게 됩니다

1. 성공 기준 2.2.1 시간 조절

(레벨 A)

콘텐츠에 의해 시간 제한이 설정된 경우, 다음 옵션 중 하나 이상이 적용되어야 합니다.

  1. 끄기

    시간 조절 비활성화

    ‘시간 제한 해제’ 와 같은 기능을 제공하여 사용자가 원하는 만큼 시간을 가질 수 있게 합니다.

  2. 조정

    시간 제한 조정 가능

    기본 시간 제한이 1시간이라면, 사용자가 최대 10시간까지 시간을 연장할 수 있는 옵션을 제공합니다.

  3. 연장

    로그인 세션 연장

    온라인 예약 시스템에서 세션 시간이 만료되기 30초 전에 ‘세션을 연장하시겠습니까?’ 라는 알림을 띄우고, 사용자가 간단히 '예' 버튼을 클릭하여 시간을 연장할 수 있게 합니다. 이 과정을 최소 10번 반복할 수 있어야 합니다.

  4. 실시간 예외

    한국거래소

    실시간 주식 거래 플랫폼에서는 시장 마감 시간이 고정되어 있으므로 이 시간 제한은 조정할 수 없습니다.

  5. 필수적인 예외

    프로그래머스 코딩테스트

    온라인 시험에서 시험 시간은 공정성을 위해 연장할 수 없을 수 있습니다. 단, 이 경우에도 장애 학생을 위한 별도의 조치가 필요할 수 있습니다.

  6. 20시간 예외

    Google Drive 업로드 한도 규정

    대용량 파일 업로드와 같이 장기간 실행되는 작업일 경우 20시간 이상의 시간 제한은 이 기준의 적용을 받지 않습니다.

이 성공 기준의 목적은 시간 제한이 있는 콘텐츠에서 사용자에게 충분한 제어권을 제공하는 것입니다. 시간 제한은 일부 사용자, 특히 장애가 있는 사용자에게 큰 어려움을 줄 수 있기 때문입니다.

개발자들은 이러한 기준을 구현할 때, 사용자 인터페이스를 직관적으로 설계하고 시간 제한과 관련된 옵션을 명확하게 제시해야 합니다. 또한, 이러한 기능이 키보드로 접근 가능해야 하며, 스크린 리더와 같은 보조 기술과 호환되어야 합니다.

2. 성공 기준 2.2.2 일시 정지, 중지, 숨기기

(레벨 A)

움직이거나, 깜박이거나, 스크롤되거나, 자동으로 갱신되는 동적인 요소들은 다음 조건을 모두 충족해야합니다.

  1. 움직임, 깜박임, 스크롤

    움직이는 베너를 일시정지, 정지, 숨길 수 있는 버튼

    자동으로 시작되고, 5초 이상 지속되며, 다른 콘텐츠와 병행하여 제시되는 움직이거나 깜박이거나 스크롤되는 정보에 대해, 사용자가 일시 정지, 중지, 또는 숨길 수 있는 메커니즘이 있어야 합니다.

  2. 자동 갱신

    출처: 라이브 주식 차트(https://kr.investing.com/charts/stocks-charts)

    자동으로 시작되는 자동 갱신 정보에 대해, 사용자가 일시 정지, 중지, 숨기거나 갱신 빈도를 제어할 수 있는 메커니즘이 있어야 합니다.

그러나 이 움직임, 깜박임, 스크롤 또는 자동 갱신이 필수적인 부분인 활동의 경우는 예외입니다.

유튜브 재생바

그 예로 비디오 플레이어의 재생 진행 바와 같이 움직임이 필수적인 경우가 있습니다. 이러한 요소는 콘텐츠의 핵심적인 부분이며, 정지시키면 기능을 상실하기 때문에 이 성공 기준에서 예외됩니다.

이 기준은 페이지의 모든 요소에 적용됩니다. 이 기준을 충족하지 못하는 모든 콘텐츠는 사용자가 전체 페이지를 사용하는 데 방해가 될 수 있으므로 웹 페이지의 모든 콘텐츠는 이 성공 기준을 충족해야 합니다. 예를 들어, 사용자가 페이지의 다른 부분에 집중하는데 방해 될 수 있는광고 배너의 움직임 제어할 수 있어야 합니다.

유튜브 라이브 채팅의 "맨 아래로 가기" 버튼

소프트웨어에 의해 주기적으로 업데이트되는 경우나 실시간 채팅 창과 같은 스트리밍 콘텐츠의 경우, 일시 정지 중에 발생한 메시지를 모두 보여줄 필요는 없습니다. 대신, 일시 정지 해제 시 최신 정보만 표시해도 됩니다.

클로드 AI 로딩 애니메이션

웹 페이지가 로딩 중일 때 나타나는 진행 상태 애니메이션은 모든 사용자가 해당 단계에서 상호작용을 수행할 수 없는 경우 필수적인 것으로 간주될 수 있습니다. 이는 사용자에게 페이지가 여전히 로딩 중임을 알려주는 중요한 역할을 하기 때문입니다.

이 기준을 구현할 때 제어 버튼은 명확하고 쉽게 찾을 수 있어야 합니다. 예를 들어, 움직이는 콘텐츠 근처에 ‘일시정지’ 버튼을 배치할 수 있습니다. 키보드로도 위와 같은 제어가 가능해야 합니다. 예를 들어, Tab 키로 제어 버튼에 접근하고 Enter 키로 활성화할 수 있어야 합니다. 스크린 리더 사용자를 위해 제어 버튼에 ‘동영상 일시정지’와 같은 적절한 레이블을 제공해야 합니다. 자동 갱신의 경우, 갱신 주기를 선택할 수 있는 드롭다운 메뉴를 제공하는 것도 좋은 방법입니다.

이러한 기준을 따르면 다양한 사용자, 특히 주의력 결핍이나 인지적 어려움이 있는 사용자들이 웹 콘텐츠를 더 쉽게 이용할 수 있게 됩니다. 또한 모든 사용자에게 더 나은 사용자 경험을 제공할 수 있습니다.

3. 성공 기준 2.2.3 시간 제한 없음

(레벨 AAA)

타이밍은 콘텐츠가 제시하는 이벤트나 활동의 필수적인 부분이 아니어야 합니다. 비대화형 동기화 미디어와 실시간 이벤트는 제외입니다.

4. 성공 기준 2.2.4 작업 방해 금지

(레벨 AAA)

사용자는 긴급 상황과 관련된 경우를 제외하고 작업을 연기하거나 억제할 수 있어야 합니다.

5. 성공 기준 2.2.5 재인증

(레벨 AAA)

인증된 세션이 만료되면, 사용자는 재인증 후 데이터 손실 없이 활동을 계속할 수 있어야 합니다.

6. 성공 기준 2.2.6 타임아웃

(레벨 AAA)

데이터 손실을 야기할 수 있는 사용자 비활동 기간에 대해 사용자에게 경고해야 합니다. 단, 데이터가 20시간 이상 보존되는 경우는 제외합니다.

개인정보 보호 규정은 사용자 식별이 인증되고 사용자 데이터가 보존되기 전에 명시적인 사용자 동의를 요구할 수 있습니다. 사용자가 미성년자인 경우 대부분의 관할권, 국가 또는 지역에서 명시적인 동의를 구할 수 없습니다. 이 성공 기준을 충족하는 방법으로 데이터 보존을 고려할 때 개인정보 보호 전문가 및 법률 고문과 상의하는 것이 좋습니다.

7. 프론트엔드 개발자를 위한 실제 적용 방법

7.1 시간 제한 기능 구현
  • setTimeout() 또는 setInterval()을 사용하여 시간 제한 구현
  • 사용자 설정에 따라 시간 제한을 조절하거나 해제할 수 있는 UI 컴포넌트 제공
  • 코드
<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>시간 제한 기능</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
        background-color: #f0f0f0;
      }
      .container {
        background-color: white;
        padding: 20px;
        border-radius: 10px;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
      }
      #timer {
        font-size: 48px;
        text-align: center;
        margin-bottom: 20px;
      }
      .controls {
        display: flex;
        justify-content: space-between;
        margin-bottom: 20px;
      }
      button,
      input {
        font-size: 16px;
        padding: 5px 10px;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div id="timer">05:00</div>
      <div class="controls">
        <button id="startStop">시작</button>
        <button id="reset">리셋</button>
        <input type="number" id="timeInput" min="1" max="60" value="5" />
        <button id="setTime">시간 설정</button>
      </div>
      <label>
        <input type="checkbox" id="disableTimer" /> 시간 제한 해제
      </label>
    </div>
 
    <script>
      let timer;
      let timeLeft = 300; // 5분 (초 단위)
      let isRunning = false;
      let isDisabled = false;
 
      const timerDisplay = document.getElementById('timer');
      const startStopButton = document.getElementById('startStop');
      const resetButton = document.getElementById('reset');
      const timeInput = document.getElementById('timeInput');
      const setTimeButton = document.getElementById('setTime');
      const disableCheckbox = document.getElementById('disableTimer');
 
      function updateDisplay() {
        const minutes = Math.floor(timeLeft / 60);
        const seconds = timeLeft % 60;
        timerDisplay.textContent = `${minutes
          .toString()
          .padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
      }
 
      function startStopTimer() {
        if (isDisabled) return;
 
        if (isRunning) {
          clearInterval(timer);
          startStopButton.textContent = '시작';
        } else {
          timer = setInterval(() => {
            if (timeLeft > 0) {
              timeLeft--;
              updateDisplay();
            } else {
              clearInterval(timer);
              alert('시간이 종료되었습니다!');
              startStopButton.textContent = '시작';
              isRunning = false;
            }
          }, 1000);
          startStopButton.textContent = '정지';
        }
        isRunning = !isRunning;
      }
 
      function resetTimer() {
        clearInterval(timer);
        timeLeft = 300;
        updateDisplay();
        startStopButton.textContent = '시작';
        isRunning = false;
      }
 
      function setTime() {
        const newTime = parseInt(timeInput.value);
        if (newTime > 0 && newTime <= 60) {
          timeLeft = newTime * 60;
          updateDisplay();
          if (isRunning) {
            clearInterval(timer);
            startStopTimer();
          }
        } else {
          alert('1분에서 60분 사이의 값을 입력해주세요.');
        }
      }
 
      function toggleDisable() {
        isDisabled = disableCheckbox.checked;
        if (isDisabled) {
          clearInterval(timer);
          timerDisplay.textContent = '--:--';
          startStopButton.disabled = true;
          resetButton.disabled = true;
          timeInput.disabled = true;
          setTimeButton.disabled = true;
        } else {
          updateDisplay();
          startStopButton.disabled = false;
          resetButton.disabled = false;
          timeInput.disabled = false;
          setTimeButton.disabled = false;
        }
      }
 
      startStopButton.addEventListener('click', startStopTimer);
      resetButton.addEventListener('click', resetTimer);
      setTimeButton.addEventListener('click', setTime);
      disableCheckbox.addEventListener('change', toggleDisable);
 
      updateDisplay();
    </script>
  </body>
</html>
<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>시간 제한 기능</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
        background-color: #f0f0f0;
      }
      .container {
        background-color: white;
        padding: 20px;
        border-radius: 10px;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
      }
      #timer {
        font-size: 48px;
        text-align: center;
        margin-bottom: 20px;
      }
      .controls {
        display: flex;
        justify-content: space-between;
        margin-bottom: 20px;
      }
      button,
      input {
        font-size: 16px;
        padding: 5px 10px;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div id="timer">05:00</div>
      <div class="controls">
        <button id="startStop">시작</button>
        <button id="reset">리셋</button>
        <input type="number" id="timeInput" min="1" max="60" value="5" />
        <button id="setTime">시간 설정</button>
      </div>
      <label>
        <input type="checkbox" id="disableTimer" /> 시간 제한 해제
      </label>
    </div>
 
    <script>
      let timer;
      let timeLeft = 300; // 5분 (초 단위)
      let isRunning = false;
      let isDisabled = false;
 
      const timerDisplay = document.getElementById('timer');
      const startStopButton = document.getElementById('startStop');
      const resetButton = document.getElementById('reset');
      const timeInput = document.getElementById('timeInput');
      const setTimeButton = document.getElementById('setTime');
      const disableCheckbox = document.getElementById('disableTimer');
 
      function updateDisplay() {
        const minutes = Math.floor(timeLeft / 60);
        const seconds = timeLeft % 60;
        timerDisplay.textContent = `${minutes
          .toString()
          .padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
      }
 
      function startStopTimer() {
        if (isDisabled) return;
 
        if (isRunning) {
          clearInterval(timer);
          startStopButton.textContent = '시작';
        } else {
          timer = setInterval(() => {
            if (timeLeft > 0) {
              timeLeft--;
              updateDisplay();
            } else {
              clearInterval(timer);
              alert('시간이 종료되었습니다!');
              startStopButton.textContent = '시작';
              isRunning = false;
            }
          }, 1000);
          startStopButton.textContent = '정지';
        }
        isRunning = !isRunning;
      }
 
      function resetTimer() {
        clearInterval(timer);
        timeLeft = 300;
        updateDisplay();
        startStopButton.textContent = '시작';
        isRunning = false;
      }
 
      function setTime() {
        const newTime = parseInt(timeInput.value);
        if (newTime > 0 && newTime <= 60) {
          timeLeft = newTime * 60;
          updateDisplay();
          if (isRunning) {
            clearInterval(timer);
            startStopTimer();
          }
        } else {
          alert('1분에서 60분 사이의 값을 입력해주세요.');
        }
      }
 
      function toggleDisable() {
        isDisabled = disableCheckbox.checked;
        if (isDisabled) {
          clearInterval(timer);
          timerDisplay.textContent = '--:--';
          startStopButton.disabled = true;
          resetButton.disabled = true;
          timeInput.disabled = true;
          setTimeButton.disabled = true;
        } else {
          updateDisplay();
          startStopButton.disabled = false;
          resetButton.disabled = false;
          timeInput.disabled = false;
          setTimeButton.disabled = false;
        }
      }
 
      startStopButton.addEventListener('click', startStopTimer);
      resetButton.addEventListener('click', resetTimer);
      setTimeButton.addEventListener('click', setTime);
      disableCheckbox.addEventListener('change', toggleDisable);
 
      updateDisplay();
    </script>
  </body>
</html>
7.2 움직이는 콘텐츠 제어
  • CSS 애니메이션이나 JavaScript 기반 애니메이션에 대한 제어 메커니즘 구현
  • 'play', 'pause', 'stop' 기능을 가진 커스텀 비디오 또는 애니메이션 플레이어 개발
<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>커스텀 애니메이션 플레이어</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
        background-color: #f0f0f0;
      }
      .container {
        text-align: center;
      }
      .animation-container {
        width: 300px;
        height: 200px;
        border: 2px solid #333;
        margin-bottom: 20px;
        overflow: hidden;
        position: relative;
      }
      .animated-element {
        width: 50px;
        height: 50px;
        background-color: #3498db;
        position: absolute;
        top: 75px;
        left: 0;
      }
      .controls button {
        font-size: 16px;
        padding: 5px 15px;
        margin: 0 5px;
      }
      @keyframes moveRight {
        0% {
          left: 0;
        }
        100% {
          left: calc(100% - 50px);
        }
      }
      .running {
        animation: moveRight 4s linear infinite alternate;
      }
      .paused {
        animation-play-state: paused;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="animation-container">
        <div class="animated-element" id="animatedElement"></div>
      </div>
      <div class="controls">
        <button id="playBtn">재생</button>
        <button id="pauseBtn">일시정지</button>
        <button id="stopBtn">정지</button>
      </div>
    </div>
 
    <script>
      const animatedElement = document.getElementById('animatedElement');
      const playBtn = document.getElementById('playBtn');
      const pauseBtn = document.getElementById('pauseBtn');
      const stopBtn = document.getElementById('stopBtn');
 
      let isPlaying = false;
 
      function play() {
        if (!isPlaying) {
          animatedElement.classList.add('running');
          animatedElement.classList.remove('paused');
          isPlaying = true;
        }
      }
 
      function pause() {
        if (isPlaying) {
          animatedElement.classList.add('paused');
          isPlaying = false;
        }
      }
 
      function stop() {
        animatedElement.classList.remove('running', 'paused');
        // 애니메이션을 즉시 멈추고 초기 위치로 돌아가기 위해
        // 요소를 재생성하는 트릭을 사용합니다.
        const newElement = animatedElement.cloneNode(true);
        animatedElement.parentNode.replaceChild(newElement, animatedElement);
        animatedElement = newElement;
        isPlaying = false;
      }
 
      playBtn.addEventListener('click', play);
      pauseBtn.addEventListener('click', pause);
      stopBtn.addEventListener('click', stop);
    </script>
  </body>
</html>
<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>커스텀 애니메이션 플레이어</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
        background-color: #f0f0f0;
      }
      .container {
        text-align: center;
      }
      .animation-container {
        width: 300px;
        height: 200px;
        border: 2px solid #333;
        margin-bottom: 20px;
        overflow: hidden;
        position: relative;
      }
      .animated-element {
        width: 50px;
        height: 50px;
        background-color: #3498db;
        position: absolute;
        top: 75px;
        left: 0;
      }
      .controls button {
        font-size: 16px;
        padding: 5px 15px;
        margin: 0 5px;
      }
      @keyframes moveRight {
        0% {
          left: 0;
        }
        100% {
          left: calc(100% - 50px);
        }
      }
      .running {
        animation: moveRight 4s linear infinite alternate;
      }
      .paused {
        animation-play-state: paused;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="animation-container">
        <div class="animated-element" id="animatedElement"></div>
      </div>
      <div class="controls">
        <button id="playBtn">재생</button>
        <button id="pauseBtn">일시정지</button>
        <button id="stopBtn">정지</button>
      </div>
    </div>
 
    <script>
      const animatedElement = document.getElementById('animatedElement');
      const playBtn = document.getElementById('playBtn');
      const pauseBtn = document.getElementById('pauseBtn');
      const stopBtn = document.getElementById('stopBtn');
 
      let isPlaying = false;
 
      function play() {
        if (!isPlaying) {
          animatedElement.classList.add('running');
          animatedElement.classList.remove('paused');
          isPlaying = true;
        }
      }
 
      function pause() {
        if (isPlaying) {
          animatedElement.classList.add('paused');
          isPlaying = false;
        }
      }
 
      function stop() {
        animatedElement.classList.remove('running', 'paused');
        // 애니메이션을 즉시 멈추고 초기 위치로 돌아가기 위해
        // 요소를 재생성하는 트릭을 사용합니다.
        const newElement = animatedElement.cloneNode(true);
        animatedElement.parentNode.replaceChild(newElement, animatedElement);
        animatedElement = newElement;
        isPlaying = false;
      }
 
      playBtn.addEventListener('click', play);
      pauseBtn.addEventListener('click', pause);
      stopBtn.addEventListener('click', stop);
    </script>
  </body>
</html>
7.3 세션 관리
  • JWT(JSON Web Tokens)를 활용한 클라이언트 사이드 세션 관리 구현4
  • 세션 만료 전 경고 메시지 표시 및 재인증 프로세스 구현
<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>JWT 세션 관리</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jwt-decode/3.1.2/jwt-decode.min.js"></script>
    <style>
      body {
        font-family: Arial, sans-serif;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
        background-color: #f0f0f0;
      }
      .container {
        background-color: white;
        padding: 20px;
        border-radius: 10px;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
      }
      button {
        margin: 5px;
        padding: 5px 10px;
      }
      #sessionInfo {
        margin-top: 20px;
      }
      .modal {
        display: none;
        position: fixed;
        z-index: 1;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        background-color: rgba(0, 0, 0, 0.4);
      }
      .modal-content {
        background-color: #fefefe;
        margin: 15% auto;
        padding: 20px;
        border: 1px solid #888;
        width: 300px;
        text-align: center;
      }
      .modal-buttons {
        margin-top: 20px;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>JWT 세션 관리</h1>
      <button id="loginBtn">로그인</button>
      <button id="logoutBtn">로그아웃</button>
      <div id="sessionInfo"></div>
    </div>
 
    <div id="sessionModal" class="modal">
      <div class="modal-content">
        <p>
          로그인을 유지하시겠습니까? (로그아웃까지
          <span id="logoutCountdown"></span>초)
        </p>
        <div class="modal-buttons">
          <button id="keepSessionBtn">네</button>
          <button id="endSessionBtn">아니오</button>
        </div>
      </div>
    </div>
 
    <script>
      const loginBtn = document.getElementById('loginBtn');
      const logoutBtn = document.getElementById('logoutBtn');
      const sessionInfo = document.getElementById('sessionInfo');
      const sessionModal = document.getElementById('sessionModal');
      const keepSessionBtn = document.getElementById('keepSessionBtn');
      const endSessionBtn = document.getElementById('endSessionBtn');
      const logoutCountdown = document.getElementById('logoutCountdown');
 
      let sessionCheckInterval;
      let countdownInterval;
      const WARNING_TIME = 60; // 만료 60초 전에 경고
      let isWarningShown = false;
      let countdown;
 
      function createToken(expiresIn = 70) {
        // 기본 70초
        const now = Math.floor(Date.now() / 1000);
        const token = btoa(
          JSON.stringify({
            exp: now + expiresIn,
            iat: now,
            user: 'example@example.com',
          }),
        );
        localStorage.setItem('token', token);
        return token;
      }
 
      function decodeToken(token) {
        return JSON.parse(atob(token));
      }
 
      function login() {
        const token = createToken();
        startSessionCheck();
        updateSessionInfo();
      }
 
      function logout() {
        localStorage.removeItem('token');
        clearInterval(sessionCheckInterval);
        clearInterval(countdownInterval);
        sessionInfo.textContent = '로그아웃 상태';
        isWarningShown = false;
      }
 
      function refreshToken() {
        const token = createToken();
        startSessionCheck();
        updateSessionInfo();
        hideModal();
        isWarningShown = false;
      }
 
      function updateSessionInfo() {
        const token = localStorage.getItem('token');
        if (token) {
          const decoded = decodeToken(token);
          const expiresIn = decoded.exp - Math.floor(Date.now() / 1000);
          sessionInfo.textContent = `세션 정보: ${decoded.user} (만료까지 ${expiresIn}초)`;
        } else {
          sessionInfo.textContent = '로그아웃 상태';
        }
      }
 
      function startSessionCheck() {
        clearInterval(sessionCheckInterval);
        sessionCheckInterval = setInterval(() => {
          const token = localStorage.getItem('token');
          if (token) {
            const decoded = decodeToken(token);
            const now = Math.floor(Date.now() / 1000);
            const timeLeft = decoded.exp - now;
 
            updateSessionInfo();
 
            if (timeLeft <= WARNING_TIME && !isWarningShown) {
              showModal(timeLeft);
              isWarningShown = true;
            }
 
            if (timeLeft <= 0) {
              logout();
              alert('세션이 만료되었습니다. 다시 로그인해주세요.');
            }
          }
        }, 1000);
      }
 
      function showModal(timeLeft) {
        sessionModal.style.display = 'block';
        countdown = timeLeft;
        updateCountdown();
        countdownInterval = setInterval(() => {
          countdown--;
          updateCountdown();
          if (countdown <= 0) {
            clearInterval(countdownInterval);
            hideModal();
            logout();
          }
        }, 1000);
      }
 
      function hideModal() {
        sessionModal.style.display = 'none';
        clearInterval(countdownInterval);
      }
 
      function updateCountdown() {
        logoutCountdown.textContent = countdown;
      }
 
      loginBtn.addEventListener('click', login);
      logoutBtn.addEventListener('click', logout);
      keepSessionBtn.addEventListener('click', refreshToken);
      endSessionBtn.addEventListener('click', () => {
        hideModal();
        logout();
      });
 
      // 페이지 로드 시 세션 체크 시작
      const token = localStorage.getItem('token');
      if (token) {
        startSessionCheck();
        updateSessionInfo();
      } else {
        sessionInfo.textContent = '로그아웃 상태';
      }
    </script>
  </body>
</html>
<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>JWT 세션 관리</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jwt-decode/3.1.2/jwt-decode.min.js"></script>
    <style>
      body {
        font-family: Arial, sans-serif;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
        background-color: #f0f0f0;
      }
      .container {
        background-color: white;
        padding: 20px;
        border-radius: 10px;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
      }
      button {
        margin: 5px;
        padding: 5px 10px;
      }
      #sessionInfo {
        margin-top: 20px;
      }
      .modal {
        display: none;
        position: fixed;
        z-index: 1;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        background-color: rgba(0, 0, 0, 0.4);
      }
      .modal-content {
        background-color: #fefefe;
        margin: 15% auto;
        padding: 20px;
        border: 1px solid #888;
        width: 300px;
        text-align: center;
      }
      .modal-buttons {
        margin-top: 20px;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>JWT 세션 관리</h1>
      <button id="loginBtn">로그인</button>
      <button id="logoutBtn">로그아웃</button>
      <div id="sessionInfo"></div>
    </div>
 
    <div id="sessionModal" class="modal">
      <div class="modal-content">
        <p>
          로그인을 유지하시겠습니까? (로그아웃까지
          <span id="logoutCountdown"></span>초)
        </p>
        <div class="modal-buttons">
          <button id="keepSessionBtn">네</button>
          <button id="endSessionBtn">아니오</button>
        </div>
      </div>
    </div>
 
    <script>
      const loginBtn = document.getElementById('loginBtn');
      const logoutBtn = document.getElementById('logoutBtn');
      const sessionInfo = document.getElementById('sessionInfo');
      const sessionModal = document.getElementById('sessionModal');
      const keepSessionBtn = document.getElementById('keepSessionBtn');
      const endSessionBtn = document.getElementById('endSessionBtn');
      const logoutCountdown = document.getElementById('logoutCountdown');
 
      let sessionCheckInterval;
      let countdownInterval;
      const WARNING_TIME = 60; // 만료 60초 전에 경고
      let isWarningShown = false;
      let countdown;
 
      function createToken(expiresIn = 70) {
        // 기본 70초
        const now = Math.floor(Date.now() / 1000);
        const token = btoa(
          JSON.stringify({
            exp: now + expiresIn,
            iat: now,
            user: 'example@example.com',
          }),
        );
        localStorage.setItem('token', token);
        return token;
      }
 
      function decodeToken(token) {
        return JSON.parse(atob(token));
      }
 
      function login() {
        const token = createToken();
        startSessionCheck();
        updateSessionInfo();
      }
 
      function logout() {
        localStorage.removeItem('token');
        clearInterval(sessionCheckInterval);
        clearInterval(countdownInterval);
        sessionInfo.textContent = '로그아웃 상태';
        isWarningShown = false;
      }
 
      function refreshToken() {
        const token = createToken();
        startSessionCheck();
        updateSessionInfo();
        hideModal();
        isWarningShown = false;
      }
 
      function updateSessionInfo() {
        const token = localStorage.getItem('token');
        if (token) {
          const decoded = decodeToken(token);
          const expiresIn = decoded.exp - Math.floor(Date.now() / 1000);
          sessionInfo.textContent = `세션 정보: ${decoded.user} (만료까지 ${expiresIn}초)`;
        } else {
          sessionInfo.textContent = '로그아웃 상태';
        }
      }
 
      function startSessionCheck() {
        clearInterval(sessionCheckInterval);
        sessionCheckInterval = setInterval(() => {
          const token = localStorage.getItem('token');
          if (token) {
            const decoded = decodeToken(token);
            const now = Math.floor(Date.now() / 1000);
            const timeLeft = decoded.exp - now;
 
            updateSessionInfo();
 
            if (timeLeft <= WARNING_TIME && !isWarningShown) {
              showModal(timeLeft);
              isWarningShown = true;
            }
 
            if (timeLeft <= 0) {
              logout();
              alert('세션이 만료되었습니다. 다시 로그인해주세요.');
            }
          }
        }, 1000);
      }
 
      function showModal(timeLeft) {
        sessionModal.style.display = 'block';
        countdown = timeLeft;
        updateCountdown();
        countdownInterval = setInterval(() => {
          countdown--;
          updateCountdown();
          if (countdown <= 0) {
            clearInterval(countdownInterval);
            hideModal();
            logout();
          }
        }, 1000);
      }
 
      function hideModal() {
        sessionModal.style.display = 'none';
        clearInterval(countdownInterval);
      }
 
      function updateCountdown() {
        logoutCountdown.textContent = countdown;
      }
 
      loginBtn.addEventListener('click', login);
      logoutBtn.addEventListener('click', logout);
      keepSessionBtn.addEventListener('click', refreshToken);
      endSessionBtn.addEventListener('click', () => {
        hideModal();
        logout();
      });
 
      // 페이지 로드 시 세션 체크 시작
      const token = localStorage.getItem('token');
      if (token) {
        startSessionCheck();
        updateSessionInfo();
      } else {
        sessionInfo.textContent = '로그아웃 상태';
      }
    </script>
  </body>
</html>
7.4 사용자 입력 데이터 보존
  • localStorage 또는 sessionStorage를 사용하여 임시로 사용자 입력 데이터 저장
  • 폼 제출 전 자동 저장 기능 구현으로 데이터 손실 방지
<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>회원가입 폼</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
        background-color: #f0f0f0;
      }
      .container {
        background-color: white;
        padding: 20px;
        border-radius: 10px;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        width: 300px;
      }
      form {
        display: flex;
        flex-direction: column;
      }
      label {
        margin-top: 10px;
      }
      input {
        margin-top: 5px;
        padding: 5px;
        border: 1px solid #ddd;
        border-radius: 4px;
      }
      button {
        margin-top: 20px;
        padding: 10px;
        background-color: #4caf50;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
      }
      button:hover {
        background-color: #45a049;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h2>회원가입</h2>
      <form id="signupForm">
        <label for="username">사용자 이름:</label>
        <input type="text" id="username" name="username" required />
 
        <label for="email">이메일:</label>
        <input type="email" id="email" name="email" required />
 
        <label for="password">비밀번호:</label>
        <input type="password" id="password" name="password" required />
 
        <label for="confirmPassword">비밀번호 확인:</label>
        <input
          type="password"
          id="confirmPassword"
          name="confirmPassword"
          required
        />
 
        <button type="submit">가입하기</button>
      </form>
    </div>
 
    <script>
      const form = document.getElementById('signupForm');
      const inputs = form.querySelectorAll('input');
 
      // 페이지 로드 시 저장된 데이터 불러오기
      window.addEventListener('load', () => {
        inputs.forEach((input) => {
          const savedValue = localStorage.getItem(input.name);
          if (savedValue) {
            input.value = savedValue;
          }
        });
      });
 
      // 입력 필드 변경 시 자동 저장
      inputs.forEach((input) => {
        input.addEventListener('input', () => {
          localStorage.setItem(input.name, input.value);
        });
      });
 
      // 폼 제출
      form.addEventListener('submit', (e) => {
        e.preventDefault();
 
        // 비밀번호 확인
        if (form.password.value !== form.confirmPassword.value) {
          alert('비밀번호가 일치하지 않습니다.');
          return;
        }
 
        // 여기에 실제 폼 제출 로직 구현
        console.log('폼 제출:', {
          username: form.username.value,
          email: form.email.value,
          password: form.password.value,
        });
 
        // 폼 제출 후 localStorage 초기화
        localStorage.clear();
        form.reset();
        alert('회원가입이 완료되었습니다!');
      });
    </script>
  </body>
</html>
<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>회원가입 폼</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
        background-color: #f0f0f0;
      }
      .container {
        background-color: white;
        padding: 20px;
        border-radius: 10px;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        width: 300px;
      }
      form {
        display: flex;
        flex-direction: column;
      }
      label {
        margin-top: 10px;
      }
      input {
        margin-top: 5px;
        padding: 5px;
        border: 1px solid #ddd;
        border-radius: 4px;
      }
      button {
        margin-top: 20px;
        padding: 10px;
        background-color: #4caf50;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
      }
      button:hover {
        background-color: #45a049;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h2>회원가입</h2>
      <form id="signupForm">
        <label for="username">사용자 이름:</label>
        <input type="text" id="username" name="username" required />
 
        <label for="email">이메일:</label>
        <input type="email" id="email" name="email" required />
 
        <label for="password">비밀번호:</label>
        <input type="password" id="password" name="password" required />
 
        <label for="confirmPassword">비밀번호 확인:</label>
        <input
          type="password"
          id="confirmPassword"
          name="confirmPassword"
          required
        />
 
        <button type="submit">가입하기</button>
      </form>
    </div>
 
    <script>
      const form = document.getElementById('signupForm');
      const inputs = form.querySelectorAll('input');
 
      // 페이지 로드 시 저장된 데이터 불러오기
      window.addEventListener('load', () => {
        inputs.forEach((input) => {
          const savedValue = localStorage.getItem(input.name);
          if (savedValue) {
            input.value = savedValue;
          }
        });
      });
 
      // 입력 필드 변경 시 자동 저장
      inputs.forEach((input) => {
        input.addEventListener('input', () => {
          localStorage.setItem(input.name, input.value);
        });
      });
 
      // 폼 제출
      form.addEventListener('submit', (e) => {
        e.preventDefault();
 
        // 비밀번호 확인
        if (form.password.value !== form.confirmPassword.value) {
          alert('비밀번호가 일치하지 않습니다.');
          return;
        }
 
        // 여기에 실제 폼 제출 로직 구현
        console.log('폼 제출:', {
          username: form.username.value,
          email: form.email.value,
          password: form.password.value,
        });
 
        // 폼 제출 후 localStorage 초기화
        localStorage.clear();
        form.reset();
        alert('회원가입이 완료되었습니다!');
      });
    </script>
  </body>
</html>
3.2 가이드라인 2.1 키보드 접근성3.4 가이드라인 2.3 발작 및 신체적 반응 예방