[태그:] 웹 개발

  • 구글 크롤러 최적화

    구글 크롤러의 동작 방식

    구글봇(Googlebot)은 단순한 텍스트 수집기가 아니라,

    실제로 크롬 기반의 렌더링 엔진을 이용해

    페이지를 처리한다. 이 과정은 보통 세 단계로 이루어진다.

    크롤링(Crawling)

    새로운 URL을 찾아내고, 서버에 요청을 보내 HTML 문서를 받아온다.

    robots.txt와 같은 파일을 참고하여 접근 가능한 URL만 수집한다.

    렌더링(Rendering)

    받은 HTML을 파싱하고, 필요한 경우 JavaScript도 실행하여 DOM을 완성한다.

    CSS, 이미지, 폰트 등 외부 리소스도 요청하기 때문에, 네트워크 환경이나 차단 정책에 따라

    일부가 누락될 수 있다.

    인덱싱(Indexing)

    완성된 DOM 트리와 메타데이터를 기반으로 페이지를 데이터베이스에 저장한다.

    이후 검색 결과에 노출될 수 있게 된다.

    렌더링 최적화

    CSR 페이지의 문제

    CSR(Client-Side Rendering) 페이지는 초기 HTML이 비어 있어,

    구글봇이 렌더링 큐에서 JS를 실행해야만 인덱싱할 수 있다.

    이 과정이 지연되면 검색 노출이 늦어지거나 일부 페이지는 아예 인덱싱되지 X

    SSR/SSG의 강점

    SSR(Server-Side Rendering)이나 SSG(Static Site Generation)는 완성된 HTML을 제공하므로,

    크롤러가 즉시 콘텐츠를 인식할 수 있다.

    Critical Rendering Path 최적화

    구글봇은 리소스 요청에 제한이 있고, JS/CSS 파일이 크면 크롤링 비용이 증가한다.

    따라서, 필수 리소스만 우선 제공하고, 나머지는 lazy 로딩하는 것이다.

    *next/script의 strategy=”lazyOnload”를 활용하면 비핵심 JS를 늦게 로딩시킬 수 있다.

    인덱싱 최적화

    메타 태그 관리

    <title>, <meta name=”description”>은 검색 결과에서 직접 노출되는 요소다.

    Next.js 13+에서는 app/ 디렉토리의 metadata API를 사용하여 페이지별 메타데이터를 선언할 수 있다.

    Canonical URL

    동일 콘텐츠가 여러 경로로 접근 가능할 경우, rel=”canonical” 태그를 통해 대표 URL을 지정해야 한다.

    e.g. /products/macbook-pro 와 /products?item=123 → canonical을 /products/macbook-pro로 통일

    구조화 데이터 (Structured Data)

    JSON-LD를 이용해 구글에 콘텐츠 의미를 전달한다.

    e.g. 제품 페이지에는 가격, 재고 상태, 리뷰 평점을 포함한 schema.org 마크업을 추가

    Core Web Vitals와 SEO

    구글은 사용자 경험 지표를 검색 랭킹에 반영한다. 대표적인 지표는 다음과 같다.

    • LCP (Largest Contentful Paint): 가장 큰 요소가 화면에 그려지는 시간 (권장 2.5초 이내)
    • FID (First Input Delay): 첫 사용자 입력에 반응하는 시간 (권장 100ms 이내)
    • CLS (Cumulative Layout Shift): 레이아웃이 얼마나 안정적인지 (권장 0.1 이내)

    Next.js에서 할 수 있는 최적화 예시:

    • next/image로 이미지 최적화
    • 코드 스플리팅과 dynamic import
    • prefetch를 통한 라우팅 최적화

    국제화(i18n)와 SEO(글로벌)

    글로벌 서비스를 운영한다면 hreflang 속성을 통해 언어/지역별 페이지를 명확히 구분해야 하며

    Next.js i18n 라우팅을 활용하면 /en, /ko, /jp와 같은 경로를 쉽게 관리할 수 있다.

    그렇지 않으면 다국어 페이지가 중복 콘텐츠로 인식될 수 있다.

  • Next js 렌더링 방식과 검색엔진

    검색엔진이 웹페이지를 처리할때

    검색엔진(특히 구글봇)은 단순히 HTML 텍스트만 긁어가는 것이 아니라, 실제 브라우저처럼 페이지를 “렌더링”하여 콘텐츠를 파악한다. 이 과정은 크게 두 단계로 나뉜다.

    이 두 단계에를 거치면서 발생하는 문제는 CSR(Client Side Rendering) 구조로,

    • CSR은 최초 HTML에 <div id=”root”></div> 같은 빈 껍데기만 제공한다.
    • 구글봇이 JS 실행까지 기다려야 하므로, 크롤링/인덱싱 속도가 늦어질 수 있다.
    • 반대로 SSR(서버사이드 렌더링)이나 SSG(정적 사이트 생성)은 완성된 HTML을 즉시 제공하므로 검색엔진 친화적이다.

    HTML 크롤링

    요청 시 서버에서 내려주는 순수 HTML 문서를 파싱한다.

    이때 <title>, <meta> 태그, <a> 링크, 텍스트 콘텐츠 등 렌더링 이전에도 노출된 요소들이 가장 먼저 수집된다.

    따라서 이 시점에 의미 있는 콘텐츠가 HTML에 포함되어 있으면, 검색엔진이 곧바로 인덱싱할 수 있다.

    렌더링 큐 처리

    필요할 경우 렌더링 큐에 페이지를 넣어 두고, 나중에 JavaScript 실행 환경에서 페이지를 완전히 렌더링한다.

    JS가 실행되어야만 생성되는 콘텐츠는 이 단계에서야 인덱싱이 가능하다.

    문제는 이 과정이 시간이 오래 걸리고, 리소스가 제한적이어서 모든 페이지를 즉시 처리하지 못한다는 점이다.

    그래서 CSR 구조의 웹사이트는 크롤링/인덱싱 속도가 현저히 늦어지거나, 심하면 누락되는 경우도 발생한다.

    결국, 검색엔진이 HTML 단계에서 바로 읽을 수 있는 정보를 얼마나 제공하느냐가 SEO의 핵심.

    CSR과 SSR의 SEO

    CSR**(Client-Side Rendering)**

    • 동작 방식: 서버에서 최소한의 HTML만 내려주고, 실제 콘텐츠는 브라우저가 JS 실행 후 생성한다.
    • 장점
      • 클라이언트 상태에 따라 화면을 유연하게 바꿀 수 있어, 사용자 경험이 풍부하다.
      • 초기 로딩 이후에는 페이지 전환이 빠르다.
    • 단점
      • 검색엔진이 최초 HTML을 읽을 때는 비어 있는 상태이므로 SEO에 불리하다.
      • 구글봇이 JS 실행까지 기다려야 하므로 인덱싱 속도가 느리다.
      • 일부 검색엔진이나 SNS 봇(카카오톡, 트위터 카드 등)은 JS 실행을 아예 하지 못해 미리보기조차 나오지 않는 경우가 있다.

    SSR(Server-Side Rendering)

    • 동작 방식: 서버에서 요청이 들어올 때마다 HTML을 완전히 렌더링해서 내려준다.
    • 장점
      • 최초 응답에 완성된 HTML이 포함 → 검색엔진이 즉시 인덱싱 가능.
      • 초기 페이지 로딩 속도가 빠르다(LCP 개선).
      • OG(Open Graph), Twitter Card 같은 소셜 메타태그도 문제없이 동작.
    • 단점
      • 모든 요청마다 서버 연산이 필요해, 트래픽이 많으면 부하가 크다.
      • CDN 캐싱 효과를 잘 활용하지 못하면 확장성에서 불리하다.

    SSG(Static Site Generation)

    • 동작 방식: 빌드 시점에 HTML을 생성해두고, 요청 시 그대로 제공한다.
    • 장점
      • 이미 완성된 HTML을 제공하므로 SEO에 최적.
      • CDN 캐싱과 함께 쓰면 서버 부하가 거의 없다.
    • 단점
      • 데이터가 자주 바뀌는 페이지에는 적합하지 않다.
      • 콘텐츠 업데이트 후 빌드 및 배포 과정이 필요하다.

    *ISR(Incremental Static Regeneration)

    • Next.js가 제공하는 SSG의 단점을 보완한 방식.
    • 특정 페이지를 미리 빌드하되, revalidate 주기마다 백그라운드에서 새 HTML을 생성한다.
    • SEO에는 SSG처럼 완성된 HTML을 제공하면서도, 데이터가 일정 주기로 업데이트된다.

    검색엔진 최적화 관점에서

    실무적으로는 Next.js가 이를 위해 페이지별로 렌더링 전략을 다르게 혼합 적용할 수 있게 설계되어 있다.

    즉, SEO가 중요한 페이지는 SSR/SSG, 로그인 뒤 내부 페이지의 일부는 CSR로 처리하는 식이다.

    즉, SEO를 고려한다면 “페이지의 특성”에 맞는 전략적 선택이 필요.

    • 실시간 업데이트 필요한 영역 → SSR + 클라이언트 하이드레이션
    • 상품 상세 → SSG / ISR
    • 랜딩 페이지 → SSR / SSG

  • 웹 개발 개념 정리

    주니어 개발자를 위한 웹 개발 기초:

    I. 핵심 CS 지식

    개발자로서 탄탄한 기본기를 쌓기 위해 반드시 알아야 할 핵심 컴퓨터 과학(CS) 지식을 정리해본다. 복잡한 개념을 명확하고 직접적인 설명으로 풀어내고자 한다.

    1. 프로그램, 프로세스, 프로세서, 그리고 스레드

    컴퓨터 작동의 기본 단위인 프로그램, 프로세스, 프로세서, 스레드의 개념을 명확히 정의하고 그 관계를 설명한다.

    • 프로그램 (Program): 정적인 명령어 집합

      프로그램이란 특정 작업을 수행하기 위해 작성된 코드와 명령어의 집합이다. 컴파일 과정을 거쳐 실행 가능한 파일 형태로 하드 디스크나 SSD 같은 보조기억장치에 저장되어 있는 ‘정적인(static)’ 상태다. 즉, 아직 실행되지 않고 저장만 되어 있는 코드 덩어리 그 자체를 의미한다.

    • 프로세스 (Process): 실행 중인 프로그램

      프로세스란 보조기억장치에 있던 프로그램이 실행을 위해 메모리(RAM)에 적재(loading)되어, 운영체제(OS)로부터 시스템 자원(CPU 시간, 메모리 등)을 할당받은 ‘동적인(dynamic)’ 상태를 말한다. 간단히 말해, ‘실행 중인 프로그램’이 바로 프로세스다. 하나의 프로그램으로 여러 개의 프로세스를 생성할 수 있다. 예를 들어, 크롬 브라우저 프로그램을 여러 개 실행하면 각 창이 독립적인 프로세스로 동작한다.

      프로세스는 운영체제가 관리하는 작업의 최소 단위이며, 고유한 생명주기(Process State)를 가진다. 프로세스는 생성(Create)되어 CPU를 할당받기 위해 대기하는 ‘준비(Ready)’ 상태, CPU를 점유하여 코드를 실행하는 ‘실행(Running)’ 상태, I/O 작업 등을 기다리며 잠시 실행을 멈춘 ‘대기(Waiting)’ 상태를 거치며, 실행이 완료되면 ‘종료(Terminated)’ 상태가 되어 모든 자원을 시스템에 반납한다. 운영체제는 이러한 프로세스들의 상태를 관리하며 시스템 자원을 효율적으로 스케줄링한다.

    • 프로세서 (Processor/CPU): 명령어 실행 주체

      프로세서는 중앙 처리 장치(CPU)를 의미하며, 메모리에서 프로세스의 명령어를 가져와 해석하고 연산을 수행하는 핵심 하드웨어 장치다. CPU 내부에는 실질적인 연산을 담당하는 ‘코어(Core)’가 있으며, 코어의 개수가 많을수록 여러 작업을 동시에 처리하는 병렬 처리(Parallelism) 능력이 향상된다.

    • 스레드 (Thread): 프로세스 내의 실행 흐름 단위

      스레드란 하나의 프로세스 내에서 실행되는 더 작은 실행 흐름의 단위다. 한 프로세스는 하나 이상의 스레드를 가질 수 있으며, 이를 멀티스레딩(Multi-threading)이라고 한다.

      프로세스 내의 스레드들은 해당 프로세스의 메모리 자원(코드, 데이터, 힙 영역)을 공유한다. 이 덕분에 스레드 간 데이터 교환이 용이하고 자원을 효율적으로 사용할 수 있다. 하지만 각 스레드는 독립적인 실행 흐름을 유지해야 하므로, 자신만의 프로그램 카운터(PC), 레지스터 집합, 그리고 스택(Stack) 공간은 별도로 할당받는다. 스레드를 활용하면 하나의 프로세스 내에서 여러 작업을 동시에 수행하는 것처럼 보이는 동시성(Concurrency)을 구현하거나, 멀티코어 환경에서 실제로 여러 작업을 병렬로 처리하여 성능을 높일 수 있다.


    2. 홈페이지 vs. 웹 서비스: 결정적 차이는 ‘CRUD’에 있다!

    ‘홈페이지’와 ‘웹 서비스’는 종종 혼용되지만, 데이터 처리 관점에서 명확한 차이가 있다. 그 핵심적인 구분 기준은 데이터 조작 기능, 즉 ‘CRUD’의 유무다.

    • 홈페이지 (정적 콘텐츠): 단방향 정보 제공

      전통적인 의미의 홈페이지는 주로 정보를 일방적으로 제공하는 데 목적이 있다. 회사 소개, 제품 설명, 연락처 등 정적인 콘텐츠를 사용자에게 보여주는 것이 주된 기능이다. 사용자는 주어진 정보를 ‘읽기(Read)’만 할 수 있을 뿐, 데이터를 직접 생성, 수정, 삭제하는 상호작용은 거의 불가능하다.

    • 웹 서비스 (동적 상호작용): 양방향 데이터 처리

      웹 서비스는 사용자가 데이터의 주체가 되어 적극적으로 상호작용하는 양방향 플랫폼이다. 사용자는 단순히 정보를 소비하는 것을 넘어, 새로운 데이터를 생성하고, 기존 데이터를 수정하며, 불필요한 데이터를 삭제할 수 있다. 이러한 동적 상호작용의 핵심에 CRUD가 있다.

    • CRUD란 무엇인가? 데이터 처리의 네 가지 기본 연산

      CRUD는 대부분의 소프트웨어가 갖는 기본적인 데이터 처리 기능인 생성(Create), 읽기(Read), 갱신(Update), 삭제(Delete)를 의미하는 약어다.

      • Create (생성): 새로운 데이터를 데이터베이스에 저장하는 연산이다.

        • 예시: 블로그에 새 글 작성, SNS에 사진 업로드, 쇼핑몰 회원가입 등


      • Read (읽기): 데이터베이스에 저장된 데이터를 조회하여 사용자에게 보여주는 연산이다.

        • 예시: 게시판 글 목록 보기, 친구 프로필 조회, 상품 상세 정보 확인 등


      • Update (수정): 이미 저장된 기존 데이터를 변경하는 연산이다.

        • 예시: 작성한 글의 오타 수정, 프로필 사진 교체, 배송지 주소 변경 등


      • Delete (삭제): 데이터베이스에서 특정 데이터를 영구적으로 제거하는 연산이다.

        • 예시: 작성한 댓글 삭제, 장바구니 상품 제거, 회원 탈퇴 등


      결론적으로, 사용자가 데이터에 대해 C(생성), U(수정), D(삭제) 연산을 수행할 수 있다면 이는 ‘웹 서비스’의 특징을 갖춘 것이다. 반면 R(읽기) 기능만 주로 제공한다면 ‘홈페이지’에 가깝다고 볼 수 있다.


    3. 대칭키와 비대칭키: 암호화의 핵심 원리

    인터넷 통신 중 데이터를 가로채도 내용을 알 수 없도록 보호하는 ‘암호화’ 기술의 핵심 원리인 대칭키와 비대칭키 방식을 알아본다.

    • 대칭키 (Symmetric Key): 단일 키 암호화

      • 원리: 데이터를 암호화(encryption)할 때와 복호화(decryption)할 때 ‘동일한 하나의 키’를 사용하는 방식이다. 송신자와 수신자가 같은 키를 공유해야만 통신이 가능하다.
      • 장점: 암호화 및 복호화 연산 과정이 단순하여 속도가 매우 빠르다. 따라서 대용량 데이터를 암호화하는 데 효율적이다.
      • 단점 (키 배송 문제): 통신을 시작하기 전에 송신자와 수신자가 어떻게든 안전하게 동일한 키를 공유해야 한다는 치명적인 문제가 있다. 만약 이 키를 전달하는 과정에서 키가 유출되면, 이후의 모든 암호화된 통신 내용은 쉽게 해독될 수 있다. 또한, 통신 상대방이 늘어날수록 관리해야 할 키의 개수가 기하급수적으로 증가한다.
    • 비대칭키 (Asymmetric Key): 공개키 암호화

      • 원리: 대칭키의 키 배송 문제를 해결하기 위해 고안된 방식으로, 암호화와 복호화에 서로 다른 키를 사용한다. 이 두 키는 수학적으로 한 쌍을 이루며, 각각 ‘공개키(Public Key)’와 ‘개인키(Private Key)’라고 불린다.
      • 작동 방식:
        1. 데이터를 수신할 주체(B)가 자신만의 ‘개인키’와 그에 맞는 ‘공개키’ 한 쌍을 생성한다.
        2. ‘개인키’는 절대로 외부에 노출하지 않고 안전하게 보관하며, ‘공개키’는 데이터를 보낼 주체(A)를 포함한 누구에게나 공개적으로 배포한다.
        3. 데이터를 보내는 A는 B로부터 받은 ‘공개키’를 사용하여 데이터를 암호화한 후 전송한다.
        4. 암호화된 데이터는 오직 그 공개키와 쌍을 이루는 B의 ‘개인키’로만 복호화할 수 있다. 따라서 중간에 데이터가 탈취되더라도 개인키가 없는 공격자는 내용을 볼 수 없다.
      • 장점: 공개키는 이름 그대로 외부에 공개되어도 되므로, 키를 안전하게 전달해야 하는 문제가 근본적으로 해결된다. 또한, 개인키 소유자만이 데이터를 복호화할 수 있다는 특성을 이용해 ‘전자 서명’을 구현함으로써 메시지 출처를 증명하고 부인 방지 기능을 제공할 수 있다.
      • 단점: 암호화 및 복호화 과정에 복잡한 수학적 연산이 필요하여 대칭키 방식에 비해 속도가 현저히 느리다.
    • 실전 적용: 하이브리드 방식 (HTTPS의 원리)

      대칭키는 빠르지만 키 교환이 위험하고, 비대칭키는 안전하지만 느리다. 따라서 실제 웹 통신 보안 표준인 SSL/TLS (HTTPS)에서는 두 방식의 장점을 결합한 ‘하이브리드 방식’을 사용한다.

      1. (비대칭키 활용) 통신 초기 단계(Handshake)에서는 안전하지만 느린 ‘비대칭키’ 방식을 사용하여, 실제 데이터를 암호화하는 데 사용할 ‘대칭키’를 안전하게 교환한다.
      2. (대칭키 활용) 양측이 안전하게 ‘대칭키’를 공유한 후에는, 실제 대용량 데이터(로그인 정보, 메시지 등)는 속도가 빠른 ‘대칭키’로 암호화하여 통신한다.

        이처럼 비대칭키는 대칭키를 안전하게 교환하는 목적만으로 사용하고, 실제 데이터 통신은 대칭키로 처리하여 보안과 성능을 모두 확보한다.
    • 대칭키 vs. 비대칭키 핵심 비교


    항목대칭키비대칭키
    키 (Key)암호화/복호화에 동일한 키 1개 사용암호화/복호화에 서로 다른 키 1쌍(공개키/개인키) 사용
    속도 (Speed)빠름느림
    키 관리키 배송/교환 문제 발생, 관리할 키가 많음키 배포가 용이하며 관리가 수월함
    보안성키가 노출되면 전체 통신이 취약해짐개인키만 안전하게 보관하면 보안성이 높음
    주요 용도대용량 데이터 암호화 (예: AES)키 교환, 디지털 서명 (예: RSA)

    II. 인증 파헤치기: Firebase 로그인 따라가 보기

    앞서 다룬 이론적 개념들이 실제 서비스에서 어떻게 적용되는지 BaaS(Backend as a Service)인 Firebase 인증 시스템을 통해 구체적으로 살펴본다.

    1. Firebase 로그인 전체 흐름

    Firebase 소셜 로그인 과정은 네 주체 간의 상호작용으로 이루어진다.

    • 참여자

      • 사용자: 서비스를 이용하려는 주체.
      • 프론트엔드 (클라이언트 앱): 사용자와 직접 상호작용하는 웹/앱.
      • Firebase 인증 서버: 구글이 운영하는 인증 전문 서버.
      • 백엔드 서버: 서비스의 핵심 비즈니스 로직을 처리하는 자체 개발 서버.
    • 로그인 흐름 (Google 로그인 예시)

      1. 사용자 → 프론트엔드: 사용자가 프론트엔드 화면의 ‘Google로 로그인’ 버튼을 클릭하여 로그인 절차를 시작한다.


      2. 프론트엔드 → Firebase 인증 서버: 프론트엔드는 Firebase SDK의 signInWithPopup()과 같은 함수를 호출하여 Firebase 인증 서버에 인증을 요청한다. 이 요청에는 사전에 Firebase 콘솔에서 설정한 제공자(Provider) 정보가 포함된다.


      3. Firebase 인증 서버 ↔ 사용자: Firebase는 구글 로그인 팝업창을 띄운다. 사용자는 이 팝업창에서 자신의 구글 계정 정보를 입력하여 인증을 완료한다. 이 과정에서 사용자의 비밀번호는 프론트엔드나 백엔드 서버에 전달되지 않는다.


      4. Firebase 인증 서버 → 프론트엔드: 인증이 성공하면, Firebase 인증 서버는 해당 사용자를 식별하는 정보가 담긴 ID 토큰과 토큰 갱신에 사용될 리프레시 토큰을 프론트엔드에 발급한다.


      5. 프론트엔드 → 백엔드 서버: 프론트엔드는 백엔드 서버의 보호된 API(예: 마이페이지 정보 요청)를 호출할 때, HTTP 요청의 Authorization 헤더에 Bearer <ID_TOKEN> 형식으로 ID 토큰을 첨부하여 전송한다.


      6. 백엔드 서버 → Firebase 인증 서버: 백엔드 서버는 클라이언트가 보낸 ID 토큰을 맹목적으로 신뢰하지 않는다. 대신 Firebase Admin SDK의 verifyIdToken() 함수를 사용하여 해당 토큰이 유효한지 Firebase 인증 서버에 직접 검증을 요청한다.


      7. Firebase 인증 서버 → 백엔드 서버: 토큰 검증이 성공하면, Firebase는 해당 토큰이 유효함을 확인하고 토큰에 포함된 사용자의 고유 식별자(UID) 등의 정보를 백엔드 서버에 반환한다.


      8. 백엔드 서버 → 프론트엔드: 백엔드 서버는 검증된 UID를 기반으로 자체 데이터베이스에서 사용자를 식별하고, 요청된 비즈니스 로직을 처리한 후 그 결과를 프론트엔드에 응답한다.


    이 흐름의 핵심은 인증 과정을 신뢰할 수 있는 제3자(Firebase)에게 위임하고, 그 결과로 발급된 암호학적으로 검증 가능한 증명서(ID 토큰)를 통해 사용자의 신원을 확인하는 것이다.

    2. 백엔드 서버의 토큰 검증

    백엔드에서 ID 토큰을 검증하는 과정은 API 보안의 핵심이다. 이 과정이 없다면 누구나 위조된 토큰으로 다른 사용자를 사칭할 수 있다.

    • 토큰의 종류: ID 토큰 vs. 리프레시 토큰

      • ID 토큰 (Access Token): 사용자의 신원과 권한을 증명하는 JWT(JSON Web Token) 형식의 인증서다. 유효 기간이 1시간으로 짧아 탈취 시 피해를 최소화할 수 있다. 백엔드 API를 호출할 때마다 자격 증명을 위해 사용된다.
      • 리프레시 토큰: ID 토큰이 만료되었을 때, 사용자가 재로그인 없이 새로운 ID 토큰을 발급받기 위해 사용되는 토큰이다. 유효 기간이 길며, Firebase 클라이언트 SDK가 이 토큰의 관리와 갱신을 자동으로 처리하므로 개발자가 직접 다룰 일은 거의 없다.
    • 백엔드 설정: Firebase Admin SDK 초기화

      백엔드 서버가 Firebase 인증 서버와 통신하려면 Admin SDK 설정이 필요하다.

      1. Firebase 프로젝트 콘솔의 ‘서비스 계정’ 탭에서 비공개 키 파일(.json)을 다운로드한다. 이 파일은 서버의 모든 권한을 가진 마스터키이므로 외부에 노출되지 않도록 안전하게 관리해야 한다.
      2. 다운로드한 키 파일을 사용하여 백엔드 애플리케이션에서 Admin SDK를 초기화한다. 파일 경로를 코드에 직접 작성하는 대신, GOOGLE_APPLICATION_CREDENTIALS 환경 변수로 경로를 지정하는 방식이 권장된다.
    • 검증 프로세스: 코드 레벨 분석

      Node.js(Express) 환경을 예로 들어 실제 검증 과정을 단계별로 설명한다.

      1. 토큰 추출: 백엔드 서버는 API 요청을 받으면, Authorization 헤더에서 Bearer 접두사를 제거하고 ID 토큰 문자열을 추출한다. 이 로직은 보통 미들웨어 함수로 구현된다.


        const authHeader = req.headers.authorization;
        if (!authHeader ||!authHeader.startsWith('Bearer ')) {
        return res.status(401).send('Unauthorized');
        }
        const idToken = authHeader.split(' ');

      2. 토큰 검증 (verifyIdToken): 추출한 ID 토큰을 admin.auth().verifyIdToken() 함수에 전달하여 검증을 수행한다.

        try {
        const decodedToken = await admin.auth().verifyIdToken(idToken);
        req.user = decodedToken; // 검증 성공 시, 요청 객체에 디코딩된 사용자 정보를 추가
        next(); // 다음 로직으로 제어 전달
        } catch (error) {
        return res.status(401).send('Unauthorized'); // 검증 실패
        }

        verifyIdToken 함수는 내부적으로 다음의 중요한 검증을 자동으로 수행한다 :

        • 서명(Signature) 검증: 토큰이 Firebase의 개인키로 올바르게 서명되었는지 확인하여 위조 여부를 판단한다.
        • 만료 시간(Expiration) 검증: 토큰의 유효 기간(1시간)이 지나지 않았는지 확인한다.
        • 발급자(Issuer) 및 대상(Audience) 검증: 토큰이 해당 Firebase 프로젝트에서 발급된 것이 맞는지 확인한다.
      3. UID 확보 및 활용: 검증이 성공하면 verifyIdToken 함수는 디코딩된 토큰 객체를 반환한다. 이 객체에 포함된 uid는 해당 사용자의 고유 식별자다.


        // API 라우터 핸들러
        app.get('/api/mypage', (req, res) => {
        const uid = req.user.uid; // 미들웨어에서 추가한 사용자 정보에서 uid를 추출

        // 이 uid를 사용하여 데이터베이스에서 사용자 정보를 조회하고 로직을 처리
        db.users.find({ id: uid }).then(userData => {
        res.json(userData);
        });
        });

        토큰이 유효하지 않으면 함수는 에러를 발생시키고, catch 블록에서 ‘401 Unauthorized’ 응답을 반환하여 접근을 차단한다. 이처럼 백엔드는 클라이언트의 주장을 신뢰하지 않고, 오직 암호학적으로 검증된 uid만을 신뢰하여 로직을 수행해야 한다.


  • [Angular]복호화(암호화 해제)

    Angular에서 암호화된 데이터 복호화(Decryption)

    백엔드(Node.js)에서 암호화된 데이터를 프론트엔드(Angular)에서 복호화하여 사용자에게 보여주는 방법을 정리했음.


    📦 1. crypto-js 모듈 설치

    Angular 프로젝트에서 crypto-js를 사용하려면 먼저 모듈을 설치해야 함.

    • 설치 명령어:
      npm i -d crypto-js

    • TypeScript 사용 시 추가 설치:
      npm i --save-dev @types/crypto-js


    🔑 2. 암호화/복호화 서비스 설정 (cryptoJs.service.ts)

    @Injectable() 데코레이터를 사용하여 CryptoJsService를 정의하고, 암호화 키를 설정했음.

    // cryptols.service.ts
    import { Injectable } from "@angular/core";
    import * as CryptoJS from "crypto-js"; // crypto-js 모듈 임포트
    
    @Injectable()
    export class CryptoJsService {
      private encryptionKey: string = "YOUR_ENCRYPTION_KEY"; // 암호화 키 설정. 실제 키로 대체 필요
    
      constructor() {}
    
      // 데이터 암호화 메서드 (Node.js와 동일한 방식으로 암호화)
      encrypt(value: string) {
        return CryptoJS.AES.encrypt(value, this.encryptionKey).toString();
      }
    
      // 데이터 복호화 메서드
      decrypt(value: string, isIpFormat: boolean) {
        // isIpFormat은 IP 형식을 체크하는 사용자 정의 함수일 수 있음
        let rtnValue = value;
        if (!isIpFormat) {
          // IP 형식이 아닌 경우에만 복호화 시도
          try {
            rtnValue = CryptoJS.AES.decrypt(value, this.encryptionKey).toString(CryptoJS.enc.Utf8);
            return rtnValue;
          } catch (e) {
            console.log(e, value);
            return rtnValue; // 에러 발생 시 원본 값 반환
          }
        } else {
          return rtnValue;
        }
      }
    }
    • encryptionKey: 백엔드에서 사용한 암호화 키와 동일해야 함.
    • decrypt 메서드는 isIpFormat이라는 추가 파라미터를 받아 특정 조건(!isIpFormat)에서만 복호화를 시도함

      이는 IP 주소와 같은 특정 형식의 데이터는 복호화하지 않기 위함으로 보임.

    💻 3. 컴포넌트에서 복호화 적용

    CryptoJsService를 컴포넌트에 주입하여 데이터를 복호화했음.

    // import {CryptoJsService} from "@app/core/service/cryptoJs.service"; // 서비스 임포트
    
    // constructor 등에서 서비스 주입
    // private cryptoJsService: CryptoJsService
    
    // 데이터 사용 예시
    // testdata.push(this.cryptoJsService.decrypt(test.testdata)); // 데이터를 복호화하여 사용

    이렇게 적용 시 일반적인 화면에서는 데이터가 정상 출력됨


    ⚠️ 4. 팝업창 복호화 문제 및 해결

    간혹 팝업창에서는 암호화된 데이터가 그대로 출력되는 문제가 발생할 수 있었음

    4.1. 문제 해결을 위한 추가 코드

    팝업창으로 데이터를 보낼 때 특정 데이터만 추출하여 다시 넣어주는 방식으로 해결했음

    // 팝업창으로 데이터를 보낼 때 적용
    if (e.colDef.field === "modi") {
      // 'modi' 필드에 해당하는 경우
      if (Array.isArray(e.data.test)) {
        // e.data.test가 배열인 경우
        let tests = [];
        e.data.test.map((ip) => tests.push(ip.testtest)); // 배열 내 객체에서 'testtest' 필드만 추출
        e.data.test = tests.toString(); // 추출된 데이터를 문자열로 변환하여 다시 할당
      }
      this.updateRow(e.data, e.colDef.field); // 행 업데이트
    }

    이 코드는 e.data.test가 배열이고 그 안에 testtest라는 필드를 가진 객체들이 있을 때, 해당 필드들의 값을 추출하여 하나의 문자열로 변환한 뒤 다시 e.data.test에 할당하는 로직으로 보임.
    이 과정을 통해 팝업창에서도 정상적으로 복호화된 데이터가 출력될 수 있도록 조치했음