블로그TECH NOTE
개발

의료 웹사이트 보안과 개인정보보호 구현 가이드

이 글에서 해결할 것

의료 웹사이트는 민감한 건강정보를 다루므로 일반 웹사이트보다 높은 보안 수준이 요구됩니다. 개인정보보호법부터 OWASP Top 10 대응까지 실전 보안 구현 가이드.

POEMORA · 편집팀2025-10-2014
여러 겹의 투명한 보호막이 추상적인 의료 데이터 코어를 감싼 모습
의료 웹 보안은 데이터 흐름 전체에 겹겹의 보호 조치를 적용하는 작업입니다.
#보안#개인정보보호#의료법#HTTPS#데이터 암호화

읽기 전에 핵심만

  1. 수집부터 삭제까지 개인정보 흐름을 그린 뒤 각 단계의 접근 권한과 보호 조치를 점검합니다.
  2. HTTPS만으로 끝내지 말고 인증, 세션, 저장 데이터, API 권한과 보안 로그를 함께 검토합니다.
  3. 보안 설정은 문서에만 두지 말고 배포 전 자동 검사와 정기적인 운영 점검으로 확인합니다.

2024년 한국인터넷진흥원(KISA) 보고서에 따르면, 의료기관 대상 사이버 공격이 전년 대비 41% 증가했습니다. 의료 데이터는 다크웹에서 금융 데이터보다 10배 이상 높은 가격에 거래되며, 한번 유출되면 되돌릴 수 없습니다.

이 글에서는 의료 웹사이트 개발 시 반드시 구현해야 할 보안 조치를 코드 레벨에서 구체적으로 다룹니다.

보안 레이어 다이어그램 / Security Layers Network Security • HTTPS/TLS Encryption • Web Application Firewall (WAF) • DDoS Protection • IP Whitelisting • Rate Limiting Application Security • Authentication (OAuth, JWT) • Role-Based Access Control (RBAC) • Input Validation & Sanitization • CSRF/XSS Protection Data Security • End-to-End Encryption • Automated Backups • Access Logs
Defense-in-depth approach with multiple security layers

1. 의료 웹사이트의 보안 위협 현황

주요 공격 유형

공격 유형비율주요 대상피해 사례
랜섬웨어34%환자 DB, 진료 기록서울 A성형외과 3일 마비
피싱/사회공학28%직원 계정 탈취환자 연락처 2만건 유출
SQL Injection18%예약/상담 폼DB 전체 유출
API 취약점12%모바일 앱 API인증 우회
기타8%DDoS, XSS 등서비스 중단

2. 개인정보보호법 핵심 요구사항

의료 웹사이트에서 수집하는 정보는 크게 일반 개인정보민감정보(건강정보)로 나뉩니다.

정보 유형별 보호 수준

정보 유형예시수집 동의암호화보관 기한
일반 개인정보이름, 전화번호, 이메일필수전송 시 암호화목적 달성 후 즉시 파기
민감정보건강상태, 시술이력, 증상별도 동의 필수저장+전송 암호화진료기록: 5년 (의료법)
고유식별정보주민등록번호, 여권번호법적 근거 필요AES-256 이상최소화 원칙

동의서 폼 구현

tsx
'use client';
import { useState } from 'react';

interface ConsentFormProps {
  onConsent: (consents: ConsentData) => void;
}

interface ConsentData {
  personalInfo: boolean;
  sensitiveInfo: boolean;
  marketing: boolean;
  thirdParty: boolean;
  timestamp: string;
  ipAddress: string;
}

export function ConsentForm({ onConsent }: ConsentFormProps) {
  const [consents, setConsents] = useState({
    personalInfo: false,
    sensitiveInfo: false,
    marketing: false,
    thirdParty: false,
  });

  const requiredConsents = consents.personalInfo && consents.sensitiveInfo;

  return (
    <div className="space-y-4 p-6 bg-gray-50 rounded-xl">
      <h3 className="font-semibold">개인정보 수집 및 이용 동의</h3>

      {/* 필수 동의: 개인정보 수집 */}
      <label className="flex items-start gap-3 p-4 bg-white rounded-lg border cursor-pointer">
        <input
          type="checkbox"
          checked={consents.personalInfo}
          onChange={(e) => setConsents(prev => ({ ...prev, personalInfo: e.target.checked }))}
          className="mt-1"
        />
        <div>
          <span className="font-medium">[필수] 개인정보 수집 및 이용 동의</span>
          <p className="text-sm text-gray-500 mt-1">
            수집 항목: 성명, 연락처, 이메일<br />
            수집 목적: 진료 상담 예약 및 안내<br />
            보유 기간: 예약일로부터 1년 (관련 법령에 따라 연장 가능)
          </p>
          <button className="text-xs text-accent underline mt-1">전문 보기</button>
        </div>
      </label>

      {/* 필수 동의: 민감정보 수집 */}
      <label className="flex items-start gap-3 p-4 bg-white rounded-lg border cursor-pointer">
        <input
          type="checkbox"
          checked={consents.sensitiveInfo}
          onChange={(e) => setConsents(prev => ({ ...prev, sensitiveInfo: e.target.checked }))}
          className="mt-1"
        />
        <div>
          <span className="font-medium">[필수] 민감정보(건강정보) 수집 동의</span>
          <p className="text-sm text-gray-500 mt-1">
            수집 항목: 관심 시술, 현재 건강상태, 알레르기 정보<br />
            수집 목적: 맞춤 시술 상담 제공<br />
            보유 기간: 진료기록 보관 의무에 따름 (의료법 제22조)
          </p>
        </div>
      </label>

      {/* 선택 동의: 마케팅 */}
      <label className="flex items-start gap-3 p-4 bg-white rounded-lg border cursor-pointer">
        <input
          type="checkbox"
          checked={consents.marketing}
          onChange={(e) => setConsents(prev => ({ ...prev, marketing: e.target.checked }))}
          className="mt-1"
        />
        <div>
          <span className="font-medium">[선택] 마케팅 정보 수신 동의</span>
          <p className="text-sm text-gray-500 mt-1">
            이벤트, 할인 정보, 시술 안내 등의 마케팅 정보를 받으실 수 있습니다.
          </p>
        </div>
      </label>

      <button
        onClick={() => onConsent({
          ...consents,
          timestamp: new Date().toISOString(),
          ipAddress: '', // 서버에서 수집
        })}
        disabled={!requiredConsents}
        className="w-full py-3 bg-accent text-white rounded-lg font-medium disabled:bg-gray-300 disabled:cursor-not-allowed"
      >
        동의하고 상담 신청하기
      </button>
    </div>
  );
}

3. HTTPS와 보안 헤더 구성

Next.js 보안 헤더 설정

tsx
// next.config.ts
const securityHeaders = [
  {
    key: 'Strict-Transport-Security',
    value: 'max-age=63072000; includeSubDomains; preload',
  },
  {
    key: 'Content-Security-Policy',
    value: [
      "default-src 'self'",
      "script-src 'self' 'unsafe-eval' 'unsafe-inline' https://t1.kakaocdn.net",
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: blob: https:",
      "font-src 'self' https://fonts.gstatic.com",
      "connect-src 'self' https://kapi.kakao.com",
      "frame-ancestors 'none'",
    ].join('; '),
  },
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff',
  },
  {
    key: 'X-Frame-Options',
    value: 'DENY',
  },
  {
    key: 'Referrer-Policy',
    value: 'strict-origin-when-cross-origin',
  },
  {
    key: 'Permissions-Policy',
    value: 'camera=(), microphone=(), geolocation=(self)',
  },
];

const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: securityHeaders,
      },
    ];
  },
};

export default nextConfig;

4. 데이터 암호화 전략

전송 중 암호화 vs 저장 중 암호화

구분전송 중 (In Transit)저장 중 (At Rest)
방법HTTPS/TLS 1.3AES-256-GCM
대상모든 HTTP 통신DB 민감 필드
구현인증서 + 서버 설정애플리케이션 레벨
키 관리인증 기관(CA)환경 변수 + KMS

필드 레벨 암호화 구현

tsx
// lib/crypto.ts
import crypto from 'crypto';

const ALGORITHM = 'aes-256-gcm';

export class FieldEncryption {
  private key: Buffer;

  constructor() {
    const keyHex = process.env.ENCRYPTION_KEY;
    if (!keyHex || keyHex.length !== 64) {
      throw new Error('ENCRYPTION_KEY must be 32 bytes (64 hex chars)');
    }
    this.key = Buffer.from(keyHex, 'hex');
  }

  encrypt(plaintext: string): string {
    const iv = crypto.randomBytes(12); // GCM 권장 12바이트
    const cipher = crypto.createCipheriv(ALGORITHM, this.key, iv);
    const encrypted = Buffer.concat([
      cipher.update(plaintext, 'utf8'),
      cipher.final(),
    ]);
    const authTag = cipher.getAuthTag();
    // iv:authTag:ciphertext (모두 base64)
    return [
      iv.toString('base64'),
      authTag.toString('base64'),
      encrypted.toString('base64'),
    ].join('.');
  }

  decrypt(ciphertext: string): string {
    const [ivB64, tagB64, dataB64] = ciphertext.split('.');
    const iv = Buffer.from(ivB64, 'base64');
    const authTag = Buffer.from(tagB64, 'base64');
    const data = Buffer.from(dataB64, 'base64');

    const decipher = crypto.createDecipheriv(ALGORITHM, this.key, iv);
    decipher.setAuthTag(authTag);
    return Buffer.concat([
      decipher.update(data),
      decipher.final(),
    ]).toString('utf8');
  }
}

// 사용 예시
const enc = new FieldEncryption();
const encrypted = enc.encrypt('010-1234-5678');
const decrypted = enc.decrypt(encrypted); // '010-1234-5678'

5. 인증/인가 시스템

Next.js 미들웨어 인증

tsx
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyToken } from '@/lib/auth';

const PROTECTED_PATHS = ['/admin', '/api/admin'];
const PUBLIC_PATHS = ['/login', '/api/auth'];

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 공개 경로는 통과
  if (PUBLIC_PATHS.some(p => pathname.startsWith(p))) {
    return NextResponse.next();
  }

  // 보호 경로 접근 시 토큰 검증
  if (PROTECTED_PATHS.some(p => pathname.startsWith(p))) {
    const token = request.cookies.get('auth-token')?.value;

    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url));
    }

    try {
      const payload = await verifyToken(token);

      // RBAC: 관리자 경로는 admin/doctor 역할만
      if (pathname.startsWith('/admin') && !['admin', 'doctor'].includes(payload.role)) {
        return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
      }

      // 요청 헤더에 사용자 정보 추가
      const headers = new Headers(request.headers);
      headers.set('x-user-id', payload.userId);
      headers.set('x-user-role', payload.role);

      return NextResponse.next({ headers });
    } catch {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }

  return NextResponse.next();
}

RBAC (역할 기반 접근 제어)

역할환자 정보 조회예약 관리콘텐츠 편집시스템 설정
원장 (admin)전체전체전체전체
의료진 (doctor)담당 환자만본인 일정만시술 정보만X
스태프 (staff)연락처만전체공지사항X
환자 (patient)본인만본인만XX

6. OWASP Top 10 대응

네트워크와 애플리케이션, 저장소와 감사 계층이 서로 다른 장벽으로 보호된 구조
주요 웹 위험은 한 가지 도구가 아니라 계층별 설계, 구현과 운영 점검으로 줄여야 합니다.

SQL Injection 방어

tsx
// Prisma ORM 사용 시 자동 방어 (파라미터 바인딩)
const patient = await prisma.patient.findFirst({
  where: { phone: userInput }, // 안전: Prisma가 자동 이스케이프
});

// 주의: Raw Query 사용 시 반드시 파라미터 바인딩
const result = await prisma.$queryRaw`
  SELECT * FROM patients WHERE phone = ${userInput}
`;
// 절대 하지 말 것: prisma.$queryRawUnsafe(`SELECT * FROM patients WHERE phone = '${userInput}'`)

XSS 방어

tsx
// React는 기본적으로 JSX 내 값을 이스케이프
// 하지만 dangerouslySetInnerHTML 사용 시 주의
import DOMPurify from 'dompurify';

function SafeHTML({ content }: { content: string }) {
  const sanitized = DOMPurify.sanitize(content, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
    ALLOWED_ATTR: [],
  });

  return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}

CSRF 방어

tsx
// Next.js Server Actions는 CSRF 토큰 자동 처리
// API Routes 사용 시 수동 구현 필요

import crypto from 'crypto';

export function generateCSRFToken(): string {
  return crypto.randomBytes(32).toString('hex');
}

export function validateCSRFToken(token: string, sessionToken: string): boolean {
  return crypto.timingSafeEqual(
    Buffer.from(token),
    Buffer.from(sessionToken)
  );
}

7. 접근 로그와 감사 추적

개인정보보호법은 개인정보 접근에 대한 로그 기록 유지 의무를 부과합니다.

tsx
// lib/audit-log.ts
interface AuditEntry {
  userId: string;
  action: 'VIEW' | 'CREATE' | 'UPDATE' | 'DELETE' | 'EXPORT';
  resource: string;
  resourceId: string;
  details?: string;
  ipAddress: string;
  timestamp: Date;
}

export async function logAccess(entry: AuditEntry) {
  await prisma.auditLog.create({
    data: {
      ...entry,
      timestamp: new Date(),
    },
  });
}

// 사용 예시: 환자 정보 조회 시
const patient = await prisma.patient.findUnique({ where: { id: patientId } });
await logAccess({
  userId: currentUser.id,
  action: 'VIEW',
  resource: 'patient',
  resourceId: patientId,
  ipAddress: request.headers.get('x-forwarded-for') || 'unknown',
  timestamp: new Date(),
});

8. 보안 감사 체크리스트


마무리

의료 웹사이트 보안은 기술적 조치만으로 완성되지 않습니다. 개인정보보호법 준수, 조직 내 보안 교육, 정기적 보안 점검 3가지가 함께 이루어져야 합니다.

POEMA는 의료기관의 보안 요구사항을 이해하고, 개인정보보호법과 의료법을 준수하는 안전한 웹사이트를 설계합니다. 보안 감사나 의료 웹사이트 구축에 대한 상담이 필요하시면 연락해 주세요.

개인정보 흐름과 웹 보안 항목을 함께 점검하려면 POEMORA 문의하기를 이용해 주세요.

FAQ

자주 묻는 질문

의료 웹사이트 보안 점검은 어디서 시작해야 하나요?

먼저 어떤 개인정보가 어느 화면과 API에서 수집되고 저장, 조회, 전송, 삭제되는지 데이터 흐름을 작성하세요. 그다음 각 단계의 접근 권한, 암호화, 로그와 삭제 절차를 실제 구현과 대조합니다.

HTTPS를 적용하면 개인정보 보호가 충분한가요?

HTTPS는 전송 구간 보호의 한 요소일 뿐입니다. 서버의 접근 통제, 안전한 인증과 세션 관리, 저장 데이터 보호, 보안 업데이트와 로그 점검도 별도로 설계하고 검증해야 합니다.

보안 로그에 개인정보를 남겨도 되나요?

문제 분석에 꼭 필요한 항목만 기록하고, 비밀번호나 인증 비밀값과 불필요한 민감정보는 로그에서 제외하세요. 로그 접근 권한, 보관 기간과 삭제 절차도 운영 정책에 포함해야 합니다.

REFERENCES

확인한 자료

관련 글