웹서비스와 애플리케이션에 대하여 공부한 내용을 정리합니다.

웹과 인터넷의 기본 개념부터 시작하여, 현대 웹 애플리케이션이 어떻게 동적인 콘텐츠를 제공하는지에 대한 근본적인 원리를 다룹니다.

 

웹과 인터넷의 차이

인터넷과 웹은 명확히 구분되는 개념입니다. 인터넷(Internet)은 전 세계의 컴퓨터를 연결하는 거대한 물리적 네트워크, 즉 '연결망' 자체를 의미합하며, 웹(World Wide Web)은 이 인터넷이라는 인프라 위에서 동작하는 '정보 공간'입니다. 사용자는 웹 브라우저를 통해 이 정보 공간에 접근하며, 모든 상호작용은 클라이언트-서버 모델을 기반으로 이루어집니다. 클라이언트가 특정 정보를 요청(Request)하면, 서버는 그에 맞는 데이터를 처리하여 응답(Response)하는 구조입니다.

웹을 구성하는 기술

웹이 원활하게 동작하기 위해 여러 기술들이 유기적으로 결합되어 있습니다.

HTTP(HyperText Transfer Protocol)는 클라이언트와 서버가 웹에서 통신하기 위해 사용하는 핵심 규약입니다. 사용자가 특정 자원의 위치를 나타내는 주소 체계인 URL(Uniform Resource Locator)을 브라우저에 입력하면, 브라우저는 HTTP 규약에 맞춰 서버에 데이터를 요청합니다.

이때 사용자가 입력한 도메인 이름은 컴퓨터가 직접 이해할 수 없으므로, DNS(Domain Name System)가 이를 서버의 실제 IP 주소로 변환해주는 역할을 합니다.

서버로부터 응답받은 데이터는 주로 HTML(HyperText Markup Language)로 작성되어 있습니다. HTML은 웹 페이지의 구조와 내용을 정의하는 표준 언어로, 웹 브라우저는 이 HTML 문서를 해석하여 우리가 보는 화면을 구성합니다.

이러한 모든 통신은 인터넷의 TCP/IP 프로토콜 위에서 이루어집니다. TCP/IP는 데이터를 패킷 단위로 나누고 주소를 지정하여 전송하는 규칙을 정의하며, 신뢰성 있는 데이터 전송을 보장합니다.

동적인 웹 페이지의 구현 원리

웹 페이지는 모든 사용자에게 동일한 내용을 보여주는 정적 페이지와 사용자의 요청에 따라 실시간으로 내용이 변경되는 동적 페이지로 나뉩니다. 예를 들어, 뉴스 기사 본문은 정적일 수 있지만, 로그인 후 보이는 '홍길동 님, 환영합니다!'와 같은 개인화된 메시지는 동적 콘텐츠의 대표적인 예입니다.

이러한 동적 콘텐츠는 서버 측(Server-side)에서 처리됩니다. 사용자가 로그인을 시도하거나 특정 데이터를 요청하면, 서버는 단순한 HTML 파일을 보내는 것을 외에 내부적인 로직을 실행합니다. 이 로직은 데이터베이스에서 사용자 정보를 조회하거나, 입력된 값을 계산하고, 현재 시간에 맞는 데이터를 가공하는 등의 작업을 포함합니다.

이러한 서버 측 로직을 구현하기 위해 다양한 기술이 사용됩니다. 대표적으로 Node.js(JavaScript), Python(Django, Flask), PHP, Ruby(Ruby on Rails), Java 등 수많은 프로그래밍 언어와 프레임워크가 있으며, 개발자는 서비스의 목적과 환경에 맞는 기술을 선택하여 웹 애플리케이션의 백엔드(Back-end) 시스템을 구축합니다.

현대 웹 서비스와 API

최근의 웹 서비스는 단순히 웹 페이지만을 제공하는 것이 아닌 다른 애플리케이션과 데이터를 주고받는 역할까지 수행합니다. 예를 들어, 스마트폰의 날씨 앱은 화면 전체(HTML)를 날씨 서버로부터 받는 것이 아니라, 온도, 습도, 풍속과 같은 순수한 데이터만을 요청하고 받아옵니다.

이렇게 애플리케이션 간의 데이터 통신을 가능하게 하는 약속이자 창구를 API(Application Programming Interface)라고 부릅니다. 서버는 웹 페이지를 제공하는 동시에, 외부 앱이나 다른 서비스가 사용할 수 있도록 데이터를 정해진 형식(주로 JSON)으로 제공하는 API 서버의 역할도 겸하게 됩니다. 이것이 현대적인 '웹 서비스'의 핵심 개념 중 하나입니다.


정리

웹은 인터넷이라는 네트워크 인프라 위에서 특정 프로토콜과 기술들로 구현된 정보 서비스라는 것을 이해했습니다. 특히 정적 콘텐츠와 동적 콘텐츠의 차이, 그리고 서버 측 로직과 API가 어떻게 현대 웹 애플리케이션을 구동하는지에 대한 원리는 앞으로 웹 기술을 이해하는 데 도움이 될 것 입니다.

728x90

'웹서비스와애플리케이션' 카테고리의 다른 글

HTTP  (0) 2025.10.17
웹 브라우저의 역할과 기능  (0) 2025.10.16
웹서비스 아키텍처의 구성  (0) 2025.09.23
아키텍쳐와 객체지향  (0) 2025.09.22

프로젝트를 진행하며 부트캠프, 팀프로젝트 등으로 인해 글로 정리하는 부분은 소홀했습니다.

프로젝트를 마무리 하였으며, 성능 최적화 단계를 진행한 글을 써봅니다.


  - 시작점: 81점 (개선 필요)
  - 최종 결과: 첫 방문 95점, 재방문 98점 (우수)
  - 핵심 전략: 이미지 최적화 + 지연 로딩 + 캐싱 전략
  - 용량 절약: 5.3MB → 1.1KB (99.98% 절약)


기본 최적화 (81점 → 92점)

1. 이미지 최적화 (가장 큰 효과)

  PNG를 WebP로 변환하면서 크기도 최적화
  cwebp -resize 48 48 -q 80 greyheart.png -o greyheart.webp
  cwebp -resize 24 24 -q 80 triangleDown.png -o triangleDown.webp

  - greyheart: 1.4MB → 392 bytes (99.97% 절약)
  - triangle icons: 1.3MB → 248 bytes (99.98% 절약)
  - 총 절약량: 5.3MB → 1.1KB


  2. 빌드 최적화

  // vite.config.ts
  export default defineConfig({
    build: {
      minify: 'esbuild',
      rollupOptions: {
        output: {
          manualChunks: {
            vendor: ['react', 'react-dom'],
            router: ['react-router-dom'],
          },
        },
      },
      cssCodeSplit: true,
      sourcemap: false,
    },
  })

이미지 지연 로딩 (92점 → 93점)

모든 외부 이미지에 loading="lazy" 속성 추가

  // Before
  https://image.tmdb.org/t/p/w185${actor.profile_path}`} alt={actor.name} />

  // After  
  https://image.tmdb.org/t/p/w185${actor.profile_path}`} 
    alt={actor.name} 
    loading="lazy" 
  />

 

적용 범위 : 13개 컴포넌트, 영화 포스터, 출연진 사진, OTT 로고 등


그 외 최적화 (93점 → 95점/98점)

폰트 최적화

// 폰트 비동기 로딩으로 렌더링 차단 방지
  <link
    href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap"
    rel="stylesheet"
    media="print" 
    onload="this.media='all'" />


API 서버 DNS 미리 해석

<link rel="dns-prefetch" href="//api.themoviedb.org" />
  <link rel="dns-prefetch" href="//image.tmdb.org" />
  <link rel="dns-prefetch" href="//http://www.omdbapi.com" />


Critical CSS 인라인화

  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: 'Noto Sans KR', system-ui, sans-serif;
      background-color: #0a0a0a;
      color: #ffffff;
      -webkit-font-smoothing: antialiased;
    }
    .loading {
      display: flex; justify-content: center; align-items: center;
      height: 50vh; color: #ff8500;
    }
  </style>



Service Worker 캐싱

  // 이미지 : Stale While Revalidate (즉시 응답 + 백그라운드 업데이트)
  if (url.hostname === 'image.tmdb.org') {
    return cache.match(request).then((cachedResponse) => {
      const fetchPromise = fetch(request).then((networkResponse) => {
        cache.put(request, networkResponse.clone());
        return networkResponse;
      });
      return cachedResponse || fetchPromise;
    });
  }

  // API : Network First with 캐시 폴백
  if (url.hostname === 'api.themoviedb.org') {
    return fetch(request)
      .then((response) => {
        if (response.ok) {
          cache.put(request, response.clone());
        }
        return response;
      })
      .catch(() => caches.match(request));
  }

결과

- 첫방뭉 : 95점

- 재방문 : 98점

- 점수 향상 : +17점 (21% 개선)

 

- LCP : 2.8초 -> 1초 미만 (64% 개선)

- FCP : 1.0초 -> 0.5초 미만 (50% 개선)


정리

이미지 최적화는 단일 작업으로 11점을 향상하여 가장 큰 효과를 가져왔습니다.

이외에도 단순한 기능 구현 외에 사용자 경험 향상을 위하여 성능 최적화를 처음으로 경험해보았는데, 결과물은 실제로 아주 강한 만족감으로 나타났습니다. 이는 더욱 사용자의 입장에서 개발을 할 수 있게 해주는 좋은 경험이였습니다.

https://developers.google.com/web/tools/lighthouse
https://developers.google.com/speed/webp
https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook

728x90

'무비넷' 카테고리의 다른 글

로컬스토리지 연결(워치리스트 컴포넌트)  (0) 2025.05.29
[React] 클릭시 이미지 변경하기  (0) 2025.05.28
[React]리액트 타입 연결  (0) 2025.05.23
프롭스 연결하기  (0) 2025.05.22
프로젝트 시작  (0) 2025.05.08

프로젝트의 '루틴 완료' 기능은 'userId'를 사용하나, 다른 핵심 기능인 '챌린지'는 'nickName'을 사용하는 데이터 처리 패턴을 가지고 있었습니다. 이를 통일하기 위한 작업을 진행하며 문제가 생긴 부분을 정리했습니다.

순수하지 않은 UseCase

기존 '루틴 완료' 기능의 가장 큰 문제는 UseCase 계층이 너무 많은 책임을 지고 있다는 것이었습니다.

// 기존 UseCase (복잡한 구조)
constructor(
  // 💥 문제: UseCase가 두 개의 다른 도메인 Repository에 의존하고 있다.
  private readonly routineCompletionsRepository: IRoutineCompletionsRepository,
  private readonly userRepository: IUserRepository 
) {}

async execute(request) {
  // 1. nickname으로 User 정보를 조회하고, (User 도메인의 책임)
  const user = await this.userRepository.findByNickname(request.nickname);
  // 2. userId로 변환한 뒤, (데이터 변환 책임)
  // 3. RoutineCompletion을 생성한다. (RoutineCompletion 도메인의 책임)
  const createdCompletion = await this.routineCompletionsRepository.create({
    userId: user.id,
    // ...
  });
}

이 구조는 클린 아키텍처 원칙을 위반합니다. AddRoutineCompletionUseCase User 도메인의 내부 사정(userRepository)까지 알아야만 했습니다. 이는 결합도(Coupling)를 높이고, 응집도(Cohesion)를 낮추는 전형적인 안티 패턴이었습니다.

책임의 재분배 

해결책은 간단했습니다. "nickname userId로 변환하고 데이터를 생성하는 책임" UseCase에서 떼어내어, 그 일의 책임을 Infrastructure 계층의 Repository에게 위임하는 것이었습니다.

먼저, IRoutineCompletionsRepository 인터페이스에 nickname을 직접 받아 처리할 수 있는 새로운 '창구'를 열었습니다.

// IRoutineCompletionsRepository.ts (개선)

// UseCase는 이제 이 메소드만 호출
createByNickname(request: {
  nickname: string;
  routineId: number;
  // ...
}): Promise<RoutineCompletion>;

그리고 PrRoutineCompletionsRepository 구현체에서, 기존 '챌린지' 기능이 사용하던 효율적인 Prisma 쿼리 패턴을 그대로 적용했습니다.

// PrRoutineCompletionsRepository.ts (개선)

async createByNickname(request: CreateByNicknameRequest): Promise<RoutineCompletion> {
  // 1. nickname으로 user를 먼저 찾고,
  const user = await prisma.user.findUnique({
    where: { nickname: request.nickname }
  });

  if (!user) {
    throw new Error(`사용자를 찾을 수 없습니다: ${request.nickname}`);
  }

  // 2. 찾아낸 user.id를 사용하여 한 번의 쿼리로 데이터를 생성한다.
  const createdCompletion = await prisma.routineCompletion.create({
    data: {
      userId: user.id, // 💥 책임이 이곳으로 이동했다!
      routineId: request.routineId,
      // ...
    },
  });

  return { /* ... */ };
}

 

 

결과

이 리팩토링으로 유즈케이스는 순수한 본인의 역할에만 집중하게 되었습니다.

// 개선된 UseCase (단순한 구조)
constructor(
  // 이제 자신의 도메인 Repository에만 의존한다.
  private readonly routineCompletionsRepository: IRoutineCompletionsRepository
) {}

async execute(request) {
  // 데이터 변환의 복잡한 과정은 모두 숨겨졌다.
  // UseCase는 그저 "nickname으로 완료 기록을 생성해줘" 라고 말할 뿐이다.
  return this.routineCompletionsRepository.createByNickname({
    nickname: request.nickname,
    // ...
  });
}

 

  •  구조 일관성 확보 : 이제 '챌린지'와 '루틴 완료' 기능은 완전히 동일한 패턴으로 동작합니다.
  •  클린 아키텍처 준수 : 각 계층이 자신의 책임만을 수행하게 되었습니다. UseCase는 더 이상 다른 도메인의 데이터베이스 접근 방식에 대해 알 필요가 없습니다.
  •  코드 간결화 : 복잡했던 UseCase의 의존성과 로직이 사라지면서, 전체 코드 라인 수가 12줄 감소했습니다. 더 적은 코드로 더 명확한 의도를 표현하게 된 것입니다.

결과

이번 리팩토링으로 코드가 동작하는 것 이상인 "올바른 위치에서 올바르게 동작한다"는 것에 대해 고민하였습니다. UseCase의 불필요한 책임을 덜어내고 Repository에게 위임하여 더 견고하고 더 일관성 있으며 더 유지보수하기 쉬운 아키텍처를 구축했습니다. 

728x90

기능별로 다르게 구현되었던 코드 패턴을 통일하고 타입 안전성과 코드 품질을 개선하였습니다.

아키텍처 패턴 통일

프로젝트가 커짐에 따라 각 기능별로 달랐던 구현 방식을 하나로 통일하는 작업을 진행했습니다. 이는 팀원 모두가 서로의 코드를 쉽게 이해하고 예측 가능하게 만들기 위함입니다.

  • UseCase 명명 규칙 표준화 : '생성'을 의미하는 UseCase의 접두사를 Create에서 Add로 통일했습니다. (예: CreateRoutineUseCase  AddRoutineUseCase) 이는 프로젝트 전반의 용어 일관성을 높여줍니다.
  • Delete 로직 패턴 통일 : 삭제 UseCase가 단순히 ID 값만 받던 방식에서, Entity 객체를 받도록 변경했습니다. 또한, 반환 타입을 Promise<boolean>에서 Promise<void>로 변경하여, "성공하면 아무것도 반환하지 않고, 실패하면 에러를 던진다"는 규칙을 적용했습니다.

타입 안전성 강화

TypeScript를 활용하여 코드에 존재하던 타입 관련 허점들을 보완했습니다.

  • 정확한 타입 적용 : Ant Design의 Checkbox 컴포넌트 이벤트 핸들러에 any 대신, 공식적으로 제공되는 CheckboxChangeEvent 타입을 적용하여 타입 추론의 정확성을 높였습니다.
  • API 응답 타입 제네릭 도입 : API 응답 데이터에 무분별하게 사용되던 any 타입을, 제네릭 <T>를 사용한 공통 응답 타입으로 대체했습니다. 이를 통해 API를 호출하는 쪽에서 응답 데이터의 타입을 명확하게 알 수 있게 되어, 런타임 에러 발생 가능성을 크게 줄였습니다.

코드  개선

  • 컨벤션 준수 : 변수명 컨벤션을 수정하여 IRoutinesRepository(클래스명처럼 보임)를 routinesRepository(인스턴스)로 변경하고, 파일 중간에 섞여 있던 import 문들을 모두 파일 상단으로 이동시켜 코드 구조를 깔끔하게 정리했습니다.
  • 절대 경로 도입 : 복잡한 상대 경로를 절대 경로로 모두 변경했습니다. 이는 파일 위치가 변경되더라도 import 경로를 수정할 필요가 없게 만들어 리팩토링을 용이하게 합니다.

API 응답 구조 표준화

모든 API Route에 명시적인 Response 타입을 정의하여, 성공 시와 실패 시의 데이터 구조를 표준화했습니다. 챌린지 기능에서 사용된 우수한 패턴을 루틴 기능에도 동일하게 적용함으로써, 프로젝트 전체 API의 일관성을 확보했습니다.


정리

오늘은 새로운 기능을 추가한 날은 아니었지만 장기적인 안정성과 유지보수성을 위한 작업을 진행 했으며, 다음과 같은 작업을 진행 예정입니다.

로그인 후 로그인 한 유저의 개인 routines 가져오기 : 현재는 userId를 하드코딩하거나 쿼리 파라미터로 받고 있습니다. 실제 로그인 세션(NextAuth)과 연동하여, 인증된 사용자 본인의 루틴 데이터만을 안전하게 불러오는 기능을 구현해야 합니다.

인증샷 실제 업로드 기능 완성 : 지금은 소감과 인증샷이 UI상으로만 존재합니다. 사용자가 업로드한 이미지 파일을 실제 AWS S3 서버에 업로드하고, 반환된 proofImgUrl을 데이터베이스에 저장하는 기능을 완성하여 사진 인증 플로우를 마무리해야 합니다.

728x90

루틴 완료 시스템 구현

사용자가 가치를 느끼게 될 '루틴 완료' 기능 구현에 집중했습니다. 사용자의 클릭이 데이터베이스에 전달되어 그 결과가 실시간으로 화면에 반영되는 것을 구축하는 것이 목표였습니다.

  • 구현된 사용자 경험(UX) 흐름
    1. 사용자가 루틴 옆 체크박스를 클릭합니다.
    2. 소감 작성을 위한 모달 창이 오픈됩니다.
    3. '제출' 버튼 클릭 시, 백엔드 API가 호출됩니다.
    4. 성공 시, UI가 실시간으로 업데이트 됩니다.

TanStack Query 커스텀 훅 시스템 구축

이전 팀원의 구축을 따라서 API 통신과 관련된 복잡한 로직을 모두 커스텀 훅으로 분리하여 컴포넌트의 책임을 줄였습니다.

  • API 클라이언트 분리 (/libs/api) : 실제 axios 호출 로직을 별도 파일로 분리했습니다.
  • 커스텀 훅 구현 (/libs/hooks) :useGetRoutineCompletionsByChallenge: useQuery를 기반으로, 특정 챌린지의 모든 완료 기록을 가져오는 훅을 구현했습니다.
    useCreateRoutineCompletion : useMutation을 기반으로 새로운 완료 기록을 생성하는 훅을 구현했습니다.
    useDeleteRoutineCompletion : useMutation을 기반으로 완료 기록을 삭제하는 훅을 구현했습니다.
  • 실시간 동기화 구현 : useMutation의 onSuccess 콜백에서 queryClient.invalidateQueries를 호출하여, 데이터 변경이 성공하면 관련 useQuery가 자동으로 데이터를 다시 불러오도록 설정했습니다. 이로써 수동으로 상태를 관리하거나 데이터를 새로고침할 필요가 없는 완벽한 실시간 동기화를 구현할 수 있었습니다.

디버깅 : DTO 필드명 불일치 문제 해결

실제 API를 연동하는 과정에서 UI가 예상대로 동작하지 않는 문제를 마주했습니다.

  • 문제점 : 컴포넌트에서는 인증샷 URL을 photoUrl로 사용하고 있었지만, 백엔드 DTO와 DB 스키마에서는 proofImgUrl로 정의되어 있었습니다. 이 차이로 인해 인증 완료 상태가 UI에 반영되지 않았습니다.
  • 해결 과정 : 프론트엔드 컴포넌트의 코드(completion?.proofImgUrl)를 백엔드 DTO 명세에 맞춰 수정함으로써 문제를 해결했습니다. API 명세서를 꼼꼼히 확인해야 한다는는 디테일의 중요성을 깨닫는 계기가 되었습니다.

최종 결과 및 검증

실제 API 연동을 완료한 후, 테스트 데이터를 기반으로 모든 기능이 의도대로 동작함을 검증했습니다.

  • 테스트 데이터 : '김강현 챌린지'에 3개의 테스트 루틴을 추가하여 진행했습니다.
  • 검증 완료
    체크박스 클릭 및 해제 시 DB 데이터 생성/삭제를 확인했습니다.
    UI 실시간 업데이트 및 캐시 무효화가 정상 동작함을 확인했습니다.
    소감 작성 모달 및 인증샷 업로드 버튼 표시 로직이 정상 동작함을 확인했습니다.

정리

프론트엔드와 백엔드를 잇는 부분을 작업했습니다. TanStack Query를 통해 복잡한 서버 상태를 관리하는 법을 배웠고, API 명세의 사소한 불일치가 에러를 만드는 것을 경험하며 디테일의 중요성을 깨달았습니다.

현재는 소감과 인증샷이 UI상으로만 존재합니다. 다음 단계는 오늘 구축한 시스템을 확장하여 실제 소감 내용을 Review 테이블에 저장하고, 이미지 파일을 서버에 업로드하여 실제 proofImgUrl을 저장하는 기능을 완성하는 것입니다. 또한, 하드코딩된 userId를 실제 로그인 정보와 연동하여 사용자별 권한 관리 기능을 구현해야 합니다.

728x90