오늘은 프론트엔드에서도 DDD를 왜 적용해야하는지에 대해서 기록을 하고 정리를 하는 글을 작성할 것이다.

프론트엔드에서 DDD를 적용한다고 하면, 흔히 백엔드 개념 아닌가?라는 반응이 나오곤 한다. 하지만 프론트엔드도 점점 복잡한 도메인을 다루게 되면서, 백엔드처럼 체계적으로 도메인 중심 설계를 적용하는 것이 중요해졌다.

 

프론트엔드에서 DDD를 적용해야 하는 이유

  • 도메인 로직이 복잡해진다. → 단순한 UI만 처리하는 것이 아니라, 상태 관리, 도메인 규칙, 비즈니스 로직까지 포함되기 때문이다.
  • 백엔드와의 협업이 중요하다. → 백엔드 DDD와 정합성을 맞추어야 하기 때문이다.
  • 프론트엔드 규모가 커진다. → 컴포넌트와 상태 관리를 효율적으로 분리해야 하기 때문이다.
  • 유지보수성이 증가한다. → 도메인 단위로 설계하면 변경에 강한 구조가 될 수 있기 때문이다.

 

프론트엔드에서 DDD 적용 핵심 개념

1. 도메인 모델링: Entity와 Value Object

프론트엔드에서도 도메인 모델을 정의해야 한다.

  • Entity: 식별자가 있는 객체를 의미한다. (예: User, Order, Product)
  • Value Object: 변경 불가능하고 식별자가 없는 객체를 의미한다. (예: Money, Address, DateRange)

적용 예시

// Value Object: 가격을 다룰 때
class Money {
  constructor(private readonly amount: number, private readonly currency: string) {}

  add(other: Money): Money {
    if (this.currency !== other.currency) {
      throw new Error("Different currencies cannot be added.");
    }
    return new Money(this.amount + other.amount, this.currency);
  }
}

// Entity: 제품 정보
class Product {
  constructor(public readonly id: string, public name: string, public price: Money) {}
}

const apple = new Product("1", "Apple", new Money(1000, "KRW"));

 

이렇게 하면?

  • 가격 계산 로직이 여기저기 흩어지지 않고 Money 안에 집중될 수 있다.
  • Product의 가격을 직접 수정하는 대신 Money 객체를 활용하여 유지보수성이 높아진다.

2. 애그리게이트(Aggregate) 패턴 적용

애그리게이트는 도메인의 일관성을 유지하는 그룹을 의미한다.

  • 핵심 개념은 **하나의 애그리게이트 루트(Aggregate Root)**를 통해서만 내부 데이터를 변경할 수 있다는 점이다.
  • 예를 들어, Order가 OrderItem을 포함하지만, OrderItem을 직접 수정할 수 없다.

적용 예시

class OrderItem {
  constructor(public readonly id: string, public readonly product: Product, public readonly quantity: number) {}
}

class Order {
  private items: OrderItem[] = [];

  constructor(public readonly id: string, public readonly userId: string) {}

  addItem(product: Product, quantity: number) {
    this.items.push(new OrderItem(`${product.id}-${quantity}`, product, quantity));
  }

  getTotalPrice(): Money {
    return this.items.reduce((total, item) => total.add(item.product.price), new Money(0, "KRW"));
  }
}

const order = new Order("order-123", "user-456");
order.addItem(apple, 2);
console.log(order.getTotalPrice()); // Money { amount: 2000, currency: 'KRW' }

 

이렇게 하면?

  • OrderItem을 Order 내부에서만 관리할 수 있어 도메인의 일관성이 유지된다.
  • 총 가격 계산 로직이 Order 내부에서만 관리될 수 있다.

3. 리포지토리 패턴 적용

프론트엔드에서도 데이터를 효율적으로 관리하기 위해 리포지토리 패턴을 적용할 수 있다.

  • 서버 API를 호출하는 역할을 리포지토리로 분리하면 좋다.
  • 상태 관리(store)와 직접 결합하지 않도록 설계해야 한다.

적용 예시

interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: string): Promise<Order | null>;
}

class HttpOrderRepository implements OrderRepository {
  async save(order: Order): Promise<void> {
    await fetch(`/api/orders`, {
      method: "POST",
      body: JSON.stringify(order),
    });
  }

  async findById(id: string): Promise<Order | null> {
    const res = await fetch(`/api/orders/${id}`);
    if (!res.ok) return null;
    const data = await res.json();
    return new Order(data.id, data.userId);
  }
}

const orderRepo = new HttpOrderRepository();
await orderRepo.save(order);

 

이렇게 하면?

  • Order 도메인을 직접 API 호출과 결합하지 않아서 유연한 구조가 될 수 있다.
  • API 변경이 있어도 OrderRepository만 수정하면 되기 때문에 유지보수성이 높아진다.

 


4. 프론트엔드 상태 관리와 DDD의 조합

프론트엔드에서는 상태 관리 라이브러리(Redux, Recoil, Zustand 등)와 DDD를 조합할 수 있다.

  • Entity를 상태로 관리하면 도메인 중심의 설계를 유지할 수 있다.
  • 도메인 서비스와 리포지토리를 분리하면 상태 관리와 비즈니스 로직을 분리할 수 있다.
  • 비즈니스 로직을 도메인 모델 안에 캡슐화하면 유지보수가 쉬워진다.

적용 예시 (Zustand + DDD)

import { create } from "zustand";

interface OrderState {
  orders: Order[];
  addOrder: (order: Order) => void;
}

const useOrderStore = create<OrderState>((set) => ({
  orders: [],
  addOrder: (order) => set((state) => ({ orders: [...state.orders, order] })),
}));

// 사용 예시
useOrderStore.getState().addOrder(new Order("order-001", "user-123"));

 

이렇게 하면?

  • Zustand 상태 관리와 DDD 구조를 자연스럽게 조합할 수 있다.
  • Redux를 사용할 경우 Entity Adapter를 활용하여 관리할 수도 있다.

 

결론: 프론트엔드에서 DDD를 적용하면?

비즈니스 로직이 명확하게 분리된다.
도메인 모델이 명확해지고 재사용 가능해진다.
API 변경에 유연하게 대응할 수 있다.
프론트엔드에서도 백엔드와 일관성 있는 구조를 유지할 수 있다.

이런 방식으로 프론트엔드를 설계하면, 단순히 UI를 만드는 개발자가 아니라 도메인을 깊이 이해하는 프론트엔드 엔지니어로 인정받을 수 있을 것이다. 

이번회사는 정말 특수한 케이스인 것 같다.. 다른 병원 인터페이스 업체나.. 전산팀과 협업을 진행해서 업무를 끝내야하는 

일들이 정말 많고 도메인지식도 너무 어려워서.. 고생고생하면서 클라이언트 요구사항 + 전산팀 업무 수준 고려 + 현회사 

임원들에 의견을 수립하여 문서를 작성했다. 참 고단하지만 다음부터는 규격도 없고 틀도 없는 부분에서 다른 회사에 넘길 수 있는 문서를 만들어서 공유하는게 좋겠다라고 생각해서 해당 문서를 작성했다.

 

목차는

이런식으로 나오고 공유 폴더를 병원 전산, 인터페이스 업체에서 많이 쓰다보니

먼저 작성하게 됐다.

 

구성도에 대한 그림을 제공

파일형식 작성

hl7에 대한 정의작성

전체 구조 작성

혹시나 .. 정보유출이 걱정돼서 부분적으로 모자이크 처리했다..

위에 작성한 것 처럼 문서로 작성해서 보내드리니 작업 하기 너무 편하시다고 연락이 왔다.

다음부터는 문서 작업에 좀 더 힘을 쓰면 전화기를 붙들고 설명하는 부분이 적어진다는걸 깨달았다.

다음에는 HTTP 프로토콜에대한 문서를 작성해야할 것 같다.

이번 회사에서는 멀티뷰어처럼 제한적인 환경에서 5명정도 트래픽을 감당할 수 있는 저사용 리눅스 서버를 요청했다.

(클라우드 하고 싶다..ㅋㅋㅋ) 개념을 잡는겸 대용량 트래픽처리는 없으나 도커 컨테이너를 실행 시켜서 웹 백엔드 , 웹 프론트엔드

개발 서버처럼 구현 하고자 했다. 구동 방식에 대해서 정리 하고자 블로그에 글을 남기면서 정리하고자 내용을 남기겠다.

 

sudo apt update 

sudo apt install firewalld

 
sudo apt-get update 
sudo apt-get install ufw

 

1) apt 업데이트

sudo apt-get update

 

2) mysql 설치

sudo apt-get install mysql-server

3) mysql 버전 확인

mysql --version

# 결과 (버전 확인 되면 설치 성공)
mysql  Ver 8.0.31-0ubuntu0.20.04.1 for Linux on x86_64 ((Ubuntu))

 

3. 접속

1) root 유저로 접속

sudo service mysql start

sudo mysql -u root -p

[Enter password : 가 나오면 password 입력하면 된다.

 

상태 확인 명령어

sudo service mysql status

 

mysql 필요 폴더 설치

sudo mkdir -p /var/run/mysqld sudo chown mysql:mysql /var/run/mysqld

 

서버 재시작

sudo systemctl start mysql

 

 

mysql 워크벤치 설치 방법

sudo apt install snapd

 

sudo snap install mysql-workbench-community

 

방화벽 설정 확인

sudo ufw status

방화벽 설정 명령어

sudo ufw allow 3306

 

mysql 설정 파일 수정

sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf

bind-address 설정이 127.0.0.1 또는 0.0.0.0 으로 변경

sudo systemctl restart mysql

 

mysql 접속 후 권한 체크

sudo mysql -u root -p

SELECT host, user FROM mysql.user;

없으면 root 권한 전체 주기

GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION;

적용 저장

FLUSH PRIVILEGES;

 

비번 설정하기

sudo mysql

 

계정 확인

SELECT User, Host, plugin FROM mysql.user WHERE User = 'root';

 

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'uimd5191!';

FLUSH PRIVILEGES; 

EXIT;

 

다시 접속 확인

mysql -u root -p

 

위에 과정 다 진행 후 서비스로 시작하기

sudo systemctl stop mysql

sudo systemctl start mysql

sudo systemctl enable mysql

레디스 서버 설치

sudo apt install redis-server

 

nginx 설치

sudo apt install nginx

 

nginx 권한 설정

sudo chown -R www-data:www-data /etc/nginx

 

레디스 서버 외부 접속 허용

sudo nano /etc/redis/redis.conf

bind 0.0.0.0 으로 변경 후 저장

protected-mode no 로 변경

 

설치를 다 하고 나서

https://www.docker.com/products/docker-desktop/ 

 

 

Docker Desktop: The #1 Containerization Tool for Developers | Docker

Docker Desktop is collaborative containerization software for developers. Get started and download Docker Desktop today on Mac, Windows, or Linux.

www.docker.com

도커데스크탑 윈도우 다운로드

 
 

생성된 계정에 프론트, 백엔드 프로젝트

프론트
이미지 빌드 – (프론트 디렉토리에서 진행 해야함)
docker build -t vue3-app .
도커 이미지 태그 추가 – (현재 버전명으로 통일해서 올려야 함)
docker tag vue3-app coin255/vue3-app:0.1v
docker login - > 이미지 업로드 시 로그인 필요
docker push coin255/vue3-app:0.1v

백엔드
이미지 빌드 – (백엔드 경로로 이동 후 빌드 명령어 입력)
docker build -t nestjs-backend .
docker tag nestjs-backend coin255/nestjs-backend:0.1v
docker login - > 이미지 업로드 시 로그인 필요
docker push coin255/nestjs-backend:0.1v

 

각 프로젝트에서 위 명령어 실행 해서 이미지들 생성 후


리눅스 서버에 Docker와 Docker Compose를 설치

sudo apt update
sudo apt install -y docker.io
sudo curl -L "https://github.com/docker/compose/releases/download/2.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

 

초기 설치 진행 후 

리눅스 도커이미지 받기
docker login
id -> coin255
pw => 132645df!@

Docker Hub에서 이미지 가져오기

프론트 이미지 가져오기
sudo docker pull coin255/vue3-app:0.1v

프론트 백엔드 이미지 가져오기
sudo docker pull coin255/nestjs-backend:0.1

compose 실행 리눅스 서버에서 실행
sudo docker-compose up -d

nginx 로그 폴더 생성해줘야함
sudo mkdir -p /var/log/nginx
sudo chmod 755 /var/log/nginx
nginx 재시작
sudo docker-compose restart nginx

로그 확인 방법 -> 컨테이명 치면 나옴
sudo docker logs mysql
sudo docker logs redis

 

위 작업을 진행 후

docker-compose.yml 을 수정

version: '3.8'
services:
  backend:
    container_name: backend_service
    image: coin255/nestjs-backend:0.3v
    ports:
      - "3002:3002"
    restart: always
    environment:
      - DB_HOST=host.docker.internal
      - DB_PORT=3306
      - DB_USER=root
      - DB_PASSWORD=uimd5191!
      - DB_NAME=pb_db_web
    extra_hosts:
      - "host.docker.internal:192.168.0.xx"  # 리눅스에서 실제 IP 주소 사용
    volumes:
      - /home/coin/Desktop/backend
    networks:
      - mynetwork

  frontend:
    container_name: frontend_service
    image: coin255/vue3-app:0.3v
    ports:
      - "8080:8080"
    restart: always
    volumes:
      - /home/coin/Desktop/f
    networks:
      - mynetwork

  nginx:
    image: nginx:alpine
    container_name: nginx_service
    volumes:
      - ./logs:/etc/nginx/logs
      - /etc/nginx/nginx.conf:/etc/nginx/nginx.conf
    ports:
      - "80:80"
    restart: always
    networks:
      - mynetwork


networks:
  mynetwork:
    driver: bridge

 

sudo docker-compose up -d

 

정상 작동 TCP 등 API IP 우회를 위해서

백엔드 프론트 코드를 변경해준다.

 

웹소켓 + tcp 설정 TS 파일에 아래 구문을 추가해서 Ai 측 TCP 수락을 하는 부분에 코드를 추가하기 힘들다고 웹쪽으로 업무가 넘어와서  끊고 다시 시작 할 수 있도록 변경하는 구문으로 AI 측 오류를 해결하는 방안으로 코드 수정

restartTcpConnection() {
    if (this.connectedClient) {
      this.logger.warn('⚠️ 기존 TCP 연결을 종료하고 다시 연결을 시도');
      this.connectedClient.end(); // 안전하게 종료 요청
      this.connectedClient.destroy(); // 강제 종료
      this.connectedClient = null;
    }

    setTimeout(() => {
      this.logger.warn('🔄 TCP 서버 재연결을 시도합니다.');
      this.setupTcpServer('192.168.0.131', 11235);
    }, 500);
  }

 

async sendDataToEmbeddedServer(data: any): Promise<void> {
    // 데이터 중복 체크
    if (
      this.tcpQueue.some(
        (item) => JSON.stringify(item) === JSON.stringify(data),
      )
    ) {
      this.logger.warn('⚠️ 중복 데이터로 인해 전송이 무시되었습니다.');
      return;
    }

    // 데이터 큐에 추가
    this.tcpQueue.push(data);

    await this.processQueue(); // 큐 처리 시작
  }

  private async processQueue(): Promise<void> {
    if (this.isProcessing || !this.tcpQueue.length) {
      return;
    }

    this.isProcessing = true; // 처리 중 상태로 설정
    const data = this.tcpQueue.shift(); // 큐에서 데이터 가져오기

    try {
      if (this.connectedClient && !this.connectedClient.destroyed) {
        const serializedData = JSON.stringify(data.payload);

        if (serializedData && this.isNotDownloadOrUploading) {
          this.connectedClient.write(serializedData);
          this.logger.log(`웹백엔드 -> 코어로 전송: ${serializedData}`);
          this.notRes = true;

          // 데이터 전송 후 일정 시간 대기 (예: 100ms)
          await new Promise((resolve) => setTimeout(resolve, 100));
        }
      } else {
        this.logger.warn('⚠️ 활성화된 코어 TCP 없음. 데이터 전송 안됨.');
        this.notRes = false;
      }
    } catch (error) {
      this.logger.error(`🚨 TCP 데이터 전송 오류: ${error.message}`);
    } finally {
      this.isProcessing = false; // 처리 상태 해제
      await this.processQueue(); // 다음 큐 처리
    }
  }

  stopTcpServer(): void {
    if (this.connectedClient) {
      this.connectedClient.destroy();
    }
  }

  setupTcpServer(newAddress: string, newPort: number): void {
    const connectClient = () => {
      if (!this.connectedClient || this.connectedClient.destroyed) {
        const newClient = new net.Socket();

        newClient.setTimeout(10000); // 10초 동안 클라이언트 소켓이 데이터를 송수신하지 않으면 timeout 이벤트가 발생하도록 설정

        newClient.connect(newPort, newAddress, () => {
          this.logger.warn('코어 TCP 웹 백엔드 연결 성공');
          this.connectedClient = newClient;
          this.wss.emit('isTcpConnected', true);
          this.reconnectAttempts = 0; // 재연결 시도 횟수 초기화
          this.notRes = false;
        });

        newClient.on('timeout', () => {
          this.logger.error('🚨 코어 TCP 웹 백엔드 연결 타임아웃');
          this.handleReconnectFailure(newClient);
        });

        newClient.on('data', (chunk) => {
          this.logger.warn(`코어 TCP 서버로부터 데이터 수신 성공`); // 추가된 로깅
          if (this.wss) {
            this.sendDataToWebSocketClients(chunk);
            this.notRes = false;
          } else {
            this.logger.error('🚨 WebSocketService가 초기화되지 않았습니다.');
          }
        });

        newClient.on('end', () => {
          this.logger.warn('코어 TCP 클라이언트 연결 종료');
          this.sendDataToWebSocketClients({ err: true });
          this.handleReconnectFailure(newClient);
        });

        newClient.on('error', (err: any) => {
          this.logger.error(
            `🚨[${err.code} - 코어 서버 연결 거부] 코어 TCP 연결 오류 - ${err}`,
          );
          this.sendDataToWebSocketClients({ err: true });
          this.handleReconnectFailure(newClient);
        });
      } else {
        this.logger.warn(
          '⚠️ 이미 클라이언트 연결이 활성화되어 있습니다. 연결 재활성화 시 문제 없음 정상 코드',
        );
      }
    };

    connectClient();
  }

  private handleReconnectFailure(client: net.Socket) {
    if (!this.mainPc) {
      return;
    }
    this.reconnectAttempts++;
    client.destroy(); // 기존 소켓 종료
    this.connectedClient = null;

    this.logger.warn(
      `⚠️ TCP 연결 실패, 재연결 시도 중 (${this.reconnectAttempts}/${this.maxReconnectAttempts})... 재 연결 텀 1초`,
    );

    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      setTimeout(
        () => this.setupTcpServer('192.168.0.131', 11235),
        this.reconnectDelay,
      );
      // 연결 실패 후 즉시 재시도를 방지 - 끊기고 나서 바로 재연결 시도하면 여러가지 문제발생 할 수 있어서 바로 재시작 안함
      // 재연결 지연 시간을 두어, 자원 낭비를 줄이고 시스템을 안정화하려는 목적
    } else {
      this.logger.error('🚨 최대 재연결 시도 횟수 초과.');
    }
  }

 

허트비트 도입으로 불안정한 TCP 끊길경우 재연결 시도 코드 추가를 하고

프론트 쪽에서는 이부분도 사실 윈도우 변수를 사용하고 싶지 않았지만..

AI 측에서 계속 응용프로그램처럼.. 쉽게 설치가 되었으면 좋겠다는.. 요구사항이 있어서 어쩔 수 없이.. exe 로 강제로 설치가 가능하게 IP도 수시로 바꿀 수 있게 윈도우 변수를 사용하여 전역으로 추가하였다.

config 에 윈도우 변수를 추가.. 한 후 공통으로 사용되는 http 정의 ts 파일을 수정해준다.

export function useHttpClient() {
    let apiBaseUrl: any = window.APP_API_BASE_URL || 'http://192.168.0.XX:3002';
    // type 용도 -> ? 쿼리 스트링으로 보낼지 여부
    const httpGet = async <T>(url: Endpoint, parameters?: string, type?: boolean, linuxServeSet = false): Promise<ApiResponse<T>> => {
        return httpGetAct(url.endpoint, parameters, type, linuxServeSet);
    };

    const httpGetAct = async <T>(url: string, parameters?: string, type?: boolean, linuxServeSet = false): Promise<ApiResponse<T>> => {
        const options: AxiosRequestConfig = {
            headers: {
                'Content-Type': 'application/json; charset=UTF-8',
                'Cache-Control': 'public, max-age=3600' // 응답을 1시간 동안 캐싱하도록 지정
            },
        };

        axios.defaults.withCredentials = true;
        const slush = parameters && parameters !== '' ? (type ? '?' : '/') : '';
        parameters = parameters || '';
        if (linuxServeSet) {
            apiBaseUrl = window.LINUXSERVERIP;
        } else {
            apiBaseUrl = window.APP_API_BASE_URL;
        }
        try {
            const response: HttpResponse<T> = await axios.get(`${apiBaseUrl}/${url}${slush}${parameters}`, options);
            return Promise.resolve(response.data || {code: 500, data: undefined, success: false});
        } catch (e) {
            return Promise.reject(e);
        }
    };

 

export const createH17 = async (request): Promise<ApiResponse<void>> => {
    return httpClient.httpPost(apiConstants.Hl7Create.post, request, '', false, window.LINUX_SERVER_SET );
};

 

config.js

window.APP_API_BASE_URL='http://192.168.0.xx:80/api'; // MultiViewer - 'http://192.168.0.xx:80/api',   Main PC Only - 'http://127.0.0.1:3002'
window.MAIN_API_IP = 'http://192.168.0.xx:80/api'; // MultiViewer - 'http://192.168.0.Xx:80/api',   Main PC Only - 'http://127.0.0.1:3002'
window.MAIN_API = '192.168.0.43'; // MultiViewer - 'http://192.168.0.xx:80/api',   Main PC Only - 'http://127.0.0.1:3002'
window.MAIN_WEBSOCKET_IP = 'http://192.168.0.xx:3002';  // MultiViewer - 'http://192.168.0.xx:3002', Main PC Only - 'http://127.0.0.1:3002'
window.PROJECT_TYPE='pb';  // pb or bm
window.PROJECT_VERSION='02.02.009';
window.WEB_BACKEND_VERSION='0.0.94v';
window.WEB_FRONTEND_VERSION='0.3.03v';
window.MACHINE_VERSION='100a';  // 12a or 100a
window.FORCE_VIEWER = 'main'; // main or viewer or exhibition
window.PORT = '8080';
window.LINUXSERVERIP = 'http://192.168.0.xx:3020';
window.LINUX_SERVER_SET = true;

 

위에처럼 로직을 전체 변경 추가 해준 후 내부망 IP 로 접근하면 아래와 같이 접속이 가능하다.

 

 

'DevOps > docker' 카테고리의 다른 글

프론트 코드 도코 이미지로 도커 허브에 올리기  (0) 2023.11.21

현재 회사의 특성상 병원이라는 특수한 장소에서 인터넷 속도가 느려지는 문제가 발생하고 있었다.

 

현 회사에 특수한 상황은 아래와 같았다.

1. 장비PC에서는 딥넷, AI 백엔드와 같이 CPU를 같이 사용

2. 웹에서 리소스사용량보다 현저히 높은 사용량

3. 느린 네트워크 속도

4. 로컬 환경에서 웹소켓 양방향으로 송수신으로 통한 실시간 데이터 새로고침

 

첫 해결책으로 image lazy loading 도입으로 리소스 자원 낭비를 줄이는 방법을 실행 하기로 했다.

WBC Images 이미지를 CDN 으로 200개이상 불러오는 화면

 

장비의 CPU와 메모리가 한정적이며, 온프레미스로 구축하여 사용하다 보니 큰 이슈가 발생했다. 속도가 너무 느려서 사용이 불가능할 정도였고, 확인해본 결과 서버 응답 시간이 약 5초 정도 걸리는 부분이 있었다.

이에 따라 이미지 지연 로딩(image lazy loading)을 도입하거나 불필요한 리소스를 줄여 자원 낭비를 최소화하도록 코드를 수정하기로 결정했다.

 

페이지에서 실제로 필요할 때까지 리소스 로딩을 미루는 방식으로 웹 최적화를 고려했다. 기획적인 측면에서 중요하지 않다고 판단되는 부분이 있어서 임상병리사 직원분에게 조언을 구했다. 그 결과, 해당 부분에 이미지를 페이징 처리하거나 Intersection Observer API를 도입해서 스크롤 내릴 때 로딩되는 방식에 대한 의견을 들었다. 이후 페이징 처리로 협의해서 문제를 해결해 나갔다. 

페이징 처리로 수정 후

페이징 처리

0.915 초로 단축 완료 기존 5001ms 가 걸리던 부분을 인터넷 속도에 대한 이슈부분을 해결 했다.

프론트엔드 개발은 가장 핵심적이고 기본적인 부분은 속도부분에 최적화를 하는 부분이라고 생각한다.

초심을 잃지 않고 꼼꼼하게 체크하는 습관을 들여야겠다..

현회사에서 무분별하게 사용되는 타입스크립트 스타일을 바꿔보고자 아래와 같이 정리 하였다.

 

변수 함수 명칭 규칙

변수 명 – camelCase 형식 사용 _ 사용 금지, 첫 글자 대문자 금지

함수 명 - camelCase 형식 사용 _ 사용 금지, 첫 글자 대문자 금지

예시: barFunc() { } , const barConst

 

class 명칭 규칙

클래스 맨 앞은 대문자로 시작 PascalCase 사용

클래스 멤버와 메소드에는 camelCase 형식 사용

예시

class Foo {

    bar: number;

    baz() { }

}

 

인터페이스 규칙

인터페이스 명 PascalCase 사용 멤버 camelCase 형식 사용

I를 접두어로 사용 x

예시

Interface Foo { }

 

타입 규칙

확장성이 있는 경우 사용 x, union이나 intersection이 필수로 필요할 경우에만 사용

이름 PascalCase 사용

멤버 camelCase 형식 사용

 

Enum 예시

이름 PascalCase 사용

예시

enum Color {

}

 

Null undefined 규칙

빈 값을 넘길 때 ->? 선택적 연결 사용

또는 무효화 합체 ->?? (null, undefined) 일 때 만 사용

또는 || (null, undefined, false, 0, 빈문자열) 일 때 만 사용

?? 사용 시 에는 ( ) 로 묶어서 순위를 제대로 표현 해줘야 함

 

Return 규칙

값이 없을 경우 Null 이 아닌 Undefined로 반환

 

매개변수 빈 값 규칙

빈 값을 넘길 때 Null을 사용 undefined 사용 x

예시 -> cb(null)

 

If null, undefined 체크 규칙

If (err !== null) 사용 금지 x if (err)로 체크

 

 

 

 

따옴표 규칙

따옴표가 겹치는 상황이 아니라면 작은 따옴표 사용

 

세미콜론

함수 또는 변수 규칙 등 작성 후 세미콜론을 무조건 사용

 

파일명 규칙

camelCase 형식 사용

 

 

 

프론트엔드 개발자이기전에 우리 직종도 개발자이기에 너무 딥하게 알 필요도 없긴 하지만 그래도 어느 정도 기초적인 부분은 알고 사용할 수 있어야한다고 생각을 한다.

 

코테를 위한 알고리즘 공부가 아닌 실무에서 어떤 식으로 사용이 가능할지 생각하고 분석하고 문제점을 파악하는 부분이 꼭 필요하다고 느낀다. 하지만 프론트개발에서 알고리즘은 그닥 그렇게 중요하지가 않다. 이미 추상화가 되어있는 노드JS API 이고 브라우저 단에서는 대용량 데이터를 처리하지 않고 간단한 필터링 조건으로만 사용될 뿐이지 그다지 중요성을 못느끼긴 했다. getElementById 함수가 dom 트리구조를 DFS 로 탐색하는것을 굳이 알지 않아도 된다 ID 속성을 받고, 출력으로 DOM 엘리먼트가 반환된다는 사실 자체만 이해하고 쓰는게 더 경제적이다.

 

추상화된 형태로 제공되는 알고리즘의 입출려만 잘 알면 실무에서는 크게 신경쓸게 없다. 하지만 여러가지 상황에서는 알고리즘이 필요한 부분이 있다.큰 배열이 아닌 정렬이 되어 있다면 이분 탐색으로 값을 찾는 것이 훨씬 더 빠르다. 그리고 서버에서 받은 데이터를 메모이제이션 동적프로그래밍으로 똑같은 계산을 다시 할 때에는 기억해둔 결과를 가져다 쓰는 것으로 성능을 끌어올릴 수 있다. 리액트에서는 useMemo, useCallback 으로 내장 API 로 제공한다. 동적 프로그래밍을 활용하여 중복된 계산을 줄이고 큰 문제를 작은 문제로 분할하여 해결함으로써 복잡한 계산의 효율성을 높일 수 있다.

 

프론트개발에 있어서 알고리즘 공부가 딱히 필요성을 못느꼈던건 연차가 적어서 작은일을 맡아서 작업을 해서 그랬던 것 같다. 요구사항에만 중점을 두고 거기에 맞춰서 작업을 하는라 제대로 된 알고리즘의 개념을 도입하지 못하고 효율성, 성능을 무시하고 만들었던 것 같았다. 특히 많이 필요성을 느낀 부분에 알고리즘 부터 정리를 해보겠다.


1. 정렬 알고리즘

정렬 알고리즘은 컴퓨터가 데이터를 특정한 순서대로 나열하는 방법을 뜻하고 가나다순으로 정리 또는 사람 키를 작은 순서대로 세우는 것과 같음 

 

 사용 되는 이유 

  • 프론트는 유독 배열을 가지고 작업을 많이 한다.  1년차일 때 정말 힘들었다.. 여기서 정렬 알고리즘을 알고 코드를 만들었다면 성능, 최적화 등등 개발 시간 단축을 하였을것이다. 특정 데이터를 찾는 시간을 단축 시켜주고 단어를 찾듯이 정렬된 데이터에서 원하는 값을 빠르게 찾을 수 있다. 그리고 통계분석, 데이터 시각화 등 다양한 분석 작업을 수월하게 만들어 준다. 많은 알고리즘들이 정렬된 데이터를 기반으로 작동하기에 정렬 알고리즘은 프론트개발 하는 사람들도 필수적으로 꼭 알아야하는 기본인 부분인 것 같다. 예시로 내가 전에 다니던 회사에서 가격, 인기순, 이름순 등등 여러가지 필터링 조건으로 화면에 뿌려주는 작업이 있었는데 여기서 정렬 알고리즘을 개념을 좀 더 빠삭하게 알고 있었더라면 수월하게 빠른 시간내에 끝내지 않았을까 하는 생각이 든다.

종류

  • 버블 정렬: 인접한 두 원소를 비교 자리를 변경하여 반복하는 알고리즘 효율성 안 좋음
const bubbleSort = (arr) => {
    const n = arr.length; // 배열의 길이를 저장
    for (let i = 0; i < n - 1; i++) { // 배열을 n-1번 반복
        for (let j = 0; j < n - i - 1; j++) { // 매 반복마다 큰 수를 뒤로 보냄
            if (arr[j] > arr[j + 1]) { // 인접한 두 요소 비교
                // 스왑: 두 요소의 위치를 교환
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
            }
        }
    }
    return arr; // 정렬된 배열 반환
};

// 사용 예
console.log(bubbleSort([64, 34, 25, 12, 22, 11, 90]));
  • 선택 정렬: 가장 작은 값을 찾아 앞으로 보내는 방식 반복하는 알고리즘 버블 보다 성능이 좋으나 느림
const selectionSort = (arr) => {
    const n = arr.length; // 배열의 길이를 저장
    for (let i = 0; i < n - 1; i++) { // 배열을 n-1번 반복
        let minIndex = i; // 현재 인덱스를 최소값 인덱스로 설정
        for (let j = i + 1; j < n; j++) { // 현재 인덱스 이후의 요소들 비교
            if (arr[j] < arr[minIndex]) { // 더 작은 요소 발견
                minIndex = j; // 최소값 인덱스 업데이트
            }
        }
        // 스왑: 최소값을 현재 인덱스와 교환
        [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
    }
    return arr; // 정렬된 배열 반환
};

// 사용 예
console.log(selectionSort([64, 34, 25, 12, 22, 11, 90]));
  • 삽입 정렬: 정렬된 부분과 정렬되지 않은 부분을 나눠 정렬되지 않은 부분의 원소를 적절한 위치에 삽입하는 알고리즘
const insertionSort = (arr) => {
    const n = arr.length; // 배열의 길이를 저장
    for (let i = 1; i < n; i++) { // 1부터 시작 (첫 번째 요소는 이미 정렬됨)
        const key = arr[i]; // 현재 요소를 key로 저장
        let j = i - 1; // 정렬된 부분의 마지막 인덱스

        // key보다 큰 요소를 오른쪽으로 이동
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j]; // 요소 이동
            j--; // 인덱스 감소
        }
        arr[j + 1] = key; // key를 올바른 위치에 삽입
    }
    return arr; // 정렬된 배열 반환
};

// 사용 예
console.log(insertionSort([64, 34, 25, 12, 22, 11, 90]));
  • 퀵 정렬: 기준 값을 설정하고 기준 값보다 작은 값과 큰 값으로 나누는 방식을 재귀적으로 반복하는 알고리즘
const quickSort = (arr) => {
    if (arr.length <= 1) return arr; // 배열의 길이가 1 이하이면 그대로 반환
    const pivot = arr[arr.length - 1]; // 마지막 요소를 피벗으로 선택
    const left = []; // 피벗보다 작은 요소를 저장할 배열
    const right = []; // 피벗보다 큰 요소를 저장할 배열

    for (let i = 0; i < arr.length - 1; i++) { // 피벗 제외하고 모든 요소 비교
        if (arr[i] < pivot) {
            left.push(arr[i]); // 피벗보다 작은 요소 추가
        } else {
            right.push(arr[i]); // 피벗보다 큰 요소 추가
        }
    }
    // 재귀적으로 왼쪽과 오른쪽 배열을 정렬 후 병합
    return [...quickSort(left), pivot, ...quickSort(right)];
};

// 사용 예
console.log(quickSort([64, 34, 25, 12, 22, 11, 90]));
  • 합병 정렬: 리스트를 계속해서 반으로 나누어 정렬한 후 다시 합치는 방식을 반복하는 알고리즘 가장 안정적인 알고리즘임
const mergeSort = (arr) => {
    if (arr.length <= 1) return arr; // 배열의 길이가 1 이하이면 그대로 반환
    const mid = Math.floor(arr.length / 2); // 배열의 중간 인덱스
    const left = mergeSort(arr.slice(0, mid)); // 왼쪽 절반 정렬
    const right = mergeSort(arr.slice(mid)); // 오른쪽 절반 정렬

    return merge(left, right); // 두 정렬된 배열 병합
};

const merge = (left, right) => {
    const result = []; // 결과를 저장할 배열
    let i = 0, j = 0; // 왼쪽과 오른쪽 배열의 인덱스

    // 두 배열을 비교하여 정렬된 형태로 병합
    while (i < left.length && j < right.length) {
        if (left[i] < right[j]) {
            result.push(left[i]); // 왼쪽 요소 추가
            i++; // 왼쪽 인덱스 증가
        } else {
            result.push(right[j]); // 오른쪽 요소 추가
            j++; // 오른쪽 인덱스 증가
        }
    }
    // 남아 있는 요소 추가
    return result.concat(left.slice(i)).concat(right.slice(j));
};

// 사용 예
console.log(mergeSort([64, 34, 25, 12, 22, 11, 90]));

 

 

정렬 알고리즘은 특성에 맞게 적용을 시키면 간결하고 성능, 최적화, 개발 속도가 더 빨라질 것 이다.

데이터의 크기가 작다면 삽입 정렬과 같은 간단한 알고리즘을 사용하고 데이터가 거의 정렬되어 있다면 삽입 정렬이 

효율적이고 성능 적으로 빠른 처리 속도가 필요하다면 퀵 정렬이나 합병 절렬을 사용하는 것이 좋다.

 

2. 탐색 알고리즘

 

  • 선형 탐색

- 배열의 처음부터 끝까지 모든 요소를 차례대로 확인하여 원하는 값을 찾는 방법

- 시간 복잡도 : O(n) (n은 배열의 크기) 

- 단순하고 구현 쉬움, 정렬 여부와 관계없이 사용 가능, 데이터가 많은 경우 비효율적임

const linearSearch = (arr, target) => {
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] === target) {
            return i; // 목표값을 찾으면 인덱스 반환
        }
    }
    return -1; // 목표값을 찾지 못함
};

// 사용 예
const numbers = [5, 3, 8, 1, 2];
console.log(linearSearch(numbers, 8)); // 2

 

  • 이분 탐색

- 정렬된 배열에서 중간값과 비교하여 탐색 범위를 반으로 줄여가면 값을 찾는 방법

- 시간 복잡도 O(log n)

- 효율적이고 대규모 데이터에 적합함

- 데이터가 정렬되어 있어야 사용 가능

const binarySearch = (arr, target) => {
    let left = 0;
    let right = arr.length - 1;

    while (left <= right) {
        const mid = Math.floor((left + right) / 2);
        if (arr[mid] === target) {
            return mid; // 목표값을 찾으면 인덱스 반환
        }
        if (arr[mid] < target) {
            left = mid + 1; // 오른쪽 절반 탐색
        } else {
            right = mid - 1; // 왼쪽 절반 탐색
        }
    }
    return -1; // 목표값을 찾지 못함
};

// 사용 예
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(binarySearch(numbers, 7)); // 6
  • 깊이 우선 탐색 ( DFS )

- 그래프나 트리에서 가능한 한 깊게 탐색하다가 더 이상 갈 수 없게 되면 이전단계로 돌아가는 방법

- 시간 복잡도 O(V+E) (V는 정점(꼮지점) 수, E는 간선(모서리 엣지) 수)

- 스택을 사용하여 구현 가능

- 경로 탐색 문제에 유용함

const dfs = (graph, start, visited = new Set()) => {
    if (!visited.has(start)) {
        console.log(start); // 방문한 정점 출력
        visited.add(start);
        const neighbors = graph[start] || [];
        neighbors.forEach(neighbor => dfs(graph, neighbor, visited)); // 재귀 호출
    }
};

// 사용 예
const graph = {
    A: ['B', 'C'],
    B: ['D'],
    C: ['E'],
    D: [],
    E: []
};

dfs(graph, 'A'); // A B D C E
  • 너비 우선 탐색 ( BFS )

- 그래프나 트리에서 현재 정점(꼭지점) 의 이웃을 모두 탐색한 후, 다음 정점(꼭지점)으로 넘어가는 방법

- 시간 복잡도ㅓ: O(V+E)

- 큐를 사용 하여 구현가능

- 최단 경로 탐색 문제에 유용

const bfs = (graph, start) => {
    const visited = new Set();
    const queue = [start];

    while (queue.length > 0) {
        const vertex = queue.shift(); // 큐에서 정점 꺼내기
        if (!visited.has(vertex)) {
            console.log(vertex); // 방문한 정점 출력
            visited.add(vertex);
            const neighbors = graph[vertex] || [];
            queue.push(...neighbors); // 이웃 추가
        }
    }
};

// 사용 예
const graph2 = {
    A: ['B', 'C'],
    B: ['D'],
    C: ['E'],
    D: [],
    E: []
};

bfs(graph2, 'A'); // A B C D E

 

탐색 알고리즘은 데이터 구노 내에서 원하는 값을 찾는 데 필수적임 각 알고리즘은 특정 상황에서 더 효율적으로 유용하게 

사용 될 수 있음 데이터의 구조와 요구 사항에 따라 적절한 탐색 알고리즘을 선택하는것이 중요하다.

 

추후에 더 많은 내용을 추가하고 실무에서 적용한 내용과 사례들을 가지고 와보겠다.!

+ Recent posts