프로젝트를 진행 중, API의 기능에 대한 질문을 했습니다. API의 역할 중 '상태 동기화'라는 새로운 패턴을 배우기까지의 과정을 기록해 보려고 합니다.


문제 : 몇 개의API를 만들어야 하는가?

관리자 페이지에서 '방' 목록을 자유롭게 편집(추가, 수정, 삭제)하고 '저장' 버튼을 누르면 그 결과를 서버에 반영하는 기능을 개발해야 합니다.


'새로 추가된 방은 POST로 보내고, 수정된 건 PATCH로, 삭제된 건 DELETE로 각각 API를 호출해야 한다' 라고 생각을 하였고, '클라이언트에서 방에 대하여 추가, 수정, 삭제하는로직이 필요하다' 라고 생각을 하였습니다.

 

그러나,  "여러 개의 로직을 만들 필요 없이, PUT API 하나로 클라이언트에서 최종 방 목록 전체를 PUT으로 보내면, 서버에서 기존 데이터와 비교해서 한 번에 처리하면 된다"라는 동료의 의견을 얻었습니다.

원인 분석

처음에는 동료의 의견에 대하여 이해하지 못했습니다.
'PUT은 상태 변경에 대하여 업데이트를 하는 api이고 어떤 방이 삭제되었는지에 대한 정보가 없으므로, 서버가 어떻게 삭제를 하는 것인가? DELETE API는 꼭 필요하지 않은가?' 라고 생각이 되었습니다.

 

이후, 동료에게 PUT 메소드에 대해 설명을 들었으며 그 내용은 "PUT은 단순히 데이터를 '수정'하는 것이 아니라, 리소스의 상태를 요청받은 상태로 '교체'하는 역할이라는 것" 입니다.

 

즉, 서버의 이전 상태가 어땠는지는 중요하지 않고 리소스의 최종 상태는 내가 보낸 모습과 똑같이 만든다는 것 입니다.

DELETE API가 필요 없는 이유는 '삭제'라는 행위 자체가 PUT의 '교체'라는 개념 안에 이미 포함되어 있기 때문입니다. 최종 목록에 없는 데이터는 자연스럽게 '제거되어야 할 대상'입니다.

 

이 설명을 듣고 다음과 같은 서버의 로직을 생각했습니다.

'상태 동기화' 패턴의 서버 로직

  1. 현실 파악 (SELECT): 먼저, DB에서 '기존 상태'의 방 목록을 전부 가져온다.
  2. 차이점 분석 (Compare): 클라이언트가 보낸 '최종 상태'와 '기존 상태'를 비교한다.
  3. 작업 수행 (Execute): 비교 결과를 바탕으로 필요한 C/U/D 작업을 한 번에 처리한다.
    Create: '최종 상태'에만 있는 방은 새로 추가한다.
    Update: 양쪽 모두에 있는 방은 정보를 갱신한다. (UPSERT)
    Delete: '기존 상태'에만 있던 방은 삭제한다.

해결

1. 클라이언트 : 최종 상태만 전송

클라이언트는 현재 화면에 보이는 rooms 배열을 PUT을 요청합니다. 복잡한 상태를 추적할 필요가 없습니다.

const spaceSave = async () => {
  try {
    await fetch(`/api/admin/spaces/${spaceId}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        spaceName: adminName,
        // 현재 클라이언트의 rooms 상태 전체를 그대로 전달
        rooms: rooms.map(room => ({
          roomId: typeof room.id === 'number' ? room.id : undefined,
          name: room.name ?? '',
          // ... 나머지 데이터
        })),
      }),
    });
    alert('저장되었습니다.');
  } catch (error) {
    console.error('저장 실패:', error);
  }
};

2. 서버: 상태 동기화 로직 수행

서버의 PUT 핸들러가 '상태 동기화' 역할을 수행합니다.

export async function PUT(request: NextRequest, { params }) {
  try {
    const spaceId = Number(params.spaceId);
    const { spaceName, rooms: incomingRooms } = await request.json();

    // 1. DB에서 기존 방 목록 조회
    const existingRooms = await roomRepository.findBySpaceId(spaceId) ?? [];
    const existingRoomIds = new Set(existingRooms.map(r => r.id as number));

    // 2. 요청과 비교하여 C/U/D 목록 분류
    const incomingRoomIds = new Set(incomingRooms.filter(r => r.roomId).map(r => r.roomId));
    
    const roomsToCreate = incomingRooms.filter(r => !r.roomId);
    const roomsToUpdate = incomingRooms.filter(r => r.roomId);
    const roomIdsToDelete = [...existingRoomIds].filter(id => !incomingRoomIds.has(id));

    // 3. DB 작업 실행
    if (roomsToCreate.length > 0) { /* ... saveAll 로직 ... */ }
    if (roomsToUpdate.length > 0) { /* ... upsert 로직 ... */ }
    if (roomIdsToDelete.length > 0) { /* ... deleteByIds 로직 ... */ }

    return NextResponse.json({ message: '성공적으로 업데이트되었습니다.' });
  } catch (error) {
    return NextResponse.json({ error: '업데이트 실패' }, { status: 500 });
  }
}

정리

'상태 동기화' 패턴은 상황에 잘 맞추어서 사용해야 합니다.

장점 : 게시물의 태그 목록, 장바구니, 설문조사 항목처럼 하나의 리소스에 속한 여러 자식 요소들을 한 화면에서 자유롭게 편집할 때  권장됩니다. 클라이언트 로직이 단순해지며 단일 API 호출로 데이터 정합성을 지키기 쉬워집니다.

단점 : 데이터 양이 아주 많을 때는 전체 목록을 보내는 것이 비효율적입니다. 하나의 데이터만 바뀌어도 전체를 바꿔야 하는 문제가 생길 수 있으며, 이러한 경우에는 PATCH 같은 다른 방법을 고민해야 합니다.


간단한 CRUD 기능만 구현하는 작은 프로젝트이거나 개발 일정이 매우 촉박한 경우에는 사용을 추천합니다.

그러나,  '기존 데이터 조회 -> 비교 -> C/U/D 실행' 이라는 비교적 복잡한 로직 구현을 거쳐야 합니다. 이 구현 비용이 프로젝트의 규모나 일정에 비해 과도할 수 있습니다.

728x90

PWD(Print Working Directoy)

터미널에서 입력시 컴퓨터에게 현재 위치를 표시하도록 지시한다.

 

LS(List Items)

모든 폴더와 폴더 내의 모든 파일을 나열하여 위치를 확인한다.

 

CD(Change Directory)

디렉토리의 위치를 변경하는데 사용된다.

(cd .. 입력 시, 한 수준 위의 폴더로 이동한다.)

 

Clear

clear 입력 시 이전의 명령문들이 사라지며, 이는 위로 스크롤시 다시 나타나게 된다.

 

mkdir

현재 위치에 새로운 디렉토리를 생성하는 명령어이다.

(mkdir 폴더이름 입력 시, 폴더의 이름이 정해지며 생성된다.)

 

touch

현재 위치에 새로운 파일을 생성하는 명령어이다.

(touch 파일명.확장자명 입력 시, 파일의 이름과 확장자가 생성된다.)

 

rmdir

현재 위치에 존재하는 폴더를 삭제하는 명령어이다.

(rmdir 폴더이름 입력 시, 폴더가 삭제되며 이 폴더가 빈 폴더일 때 명령어가 작동한다.)

 

rm

현재 위치에 존재하는 파일을 삭제하는 명령어이다.

(rm 파일명. 확장자명 입력 시, 파일이 삭제된다.)

728x90

명령줄 인터페이스(CLI)와 그래픽 사용자 인터페이스(GUI)와 같은 방법들로 인에 사람이 컴퓨터를 통해 다양한 프로그램을 열 수 있게 만들어준다.

 

명령줄 인터페이스(Command Line Interface)

만약 사용자가 컴퓨터 사용에 익숙하지 않다면 컴퓨터와 상호작용 하는 데 있어 더욱 편리하다.

 

그래픽 사용자 인터페이스(Grahpical User Interface)

터미널 혹은 명령 프롬프트라고 불리며 명령을 기반 컴퓨터 상호작용한다. 마우스를 유지하지 않고 컴퓨터와 상호작용 할 수 있다는 이점이 있다.

728x90

'ETC' 카테고리의 다른 글

상태 동기화 패턴(state synchronization pattern) 에 대하여  (3) 2025.07.26
맥OS 터미널(terminal) 명령어  (0) 2024.11.23
Git, Github 명령어  (0) 2024.11.21

 

init

레포지토리를 초기화하는 명령어이다.

 

status

파일의 상태를 확인하는 명령어이다.

 

add

변경사항을 스테이징 영역에 추가하는 명령어이다.

(’git add .’를 입력하면 모든 변경사항이 스테이징 영역에 추가된다.)

 

commit

변경사항을 저장소에 기록하는 명령어이다.

(’git commit -m “text”’를 입력하면 저장소에 기록하며 ‘””’ 안에 있는 메세지가 함께 기록된다.)

 

log

변경사항 기록을 확인하는 명령어이다.

 

git branch

현재 브랜치 목록을 확인하는 명령어이다.

(’git branch -m 이름’을 입력하면 현재 위치하는 브렌치의 이름을 바꾼다.)

 

git checkout -b

현재 위치하는 브랜치에서 나오며 새로운 브랜치를 생성하고, 그 이름을 지정하는 명령어이다.

('git checkout -b 이름'을 입력하면 현재 위치하는 브랜치에서 나오며 새로운 브랜치를 생성하고, 그 이름을 지정하는 명령어이다.)

 

git checkout -- .

브랜치를 마지막 커밋 단계로 리셋하는 명령어이다.

(’git checkout -- 이름’을 입력하면 해당 이름의 파일이 리셋된다.)

 

git merge

두 개의 브랜치를 하나로 합치는 명령어이며, 주로 개발이 끝난 기능을 메인 브랜치에 통합할 때 사용한다.

('git merge feature-branch'를 실행하면 현재 브랜치에 'feature-branch'의 변경 사항이 병합된다.)

 

cd

경로를 이동하는 명령어이다.

(’cd 이름’을 입력하면 해당 이름의 경로로 이동한다.)

 

git rm

저장소에서 파일을 삭제하는 명령어이다.

(’git rm 파일명’을 입력하면 해당 파일이 저장소와 디렉토리에서 모두 제거하며 변경사항을 자동으로 스테이징 영역에 추가한다.)

 

git reset

헤드를 이전 상태로 되돌려주는 명령어이다.

(’git reset —hard HEAD~1’를 입력하면 HEAD에 있는 기록을 1단계 이전으로 되돌려준다.)

 

git branch -D

브랜치를 삭제하는 명령어이다.

(’git branch - D 이름’을 입력하면 해당 이름의 브랜치가 삭제된다.)

 

git remote, git remote add

원격 저장소에 연결을 추가하는 명령어이이며 뒤에 원격 저장소의 링크를 입력해야 한다.

 

git push

코드를 로컬 저장소에서 원격 저장소로 원격 저장소로 가져온다.

(’git push 저장소명 브랜치명’을 입력하면 브랜치에 있는 코드 변경 이력을 저장소에 옮긴다.)

728x90