
현재 지난 부트캠프 때 미니 프로젝트로 진행한 캠핑온탑 프로젝트를 리팩토링하고 있다.
이번 스프린트 이전까지는 구매자가 판매자에게 연락을 할 수 있는 방법이 없었다.
숙박 결제 후 리뷰를 남길 수 있지만 이 방식만으로는 상호 소통을 하기에 부족하다는 생각이 들었다.
캠핑온탑 의 경우,
- 국내 숙소 대상 서비스
- 도심 속 옥탑 캠핑
위의 2가지의 큰 컨셉을 가지고 있기 때문에
서비스의 특성 상 당일 결제 및 예약의 비중이 높을 것으로 생각했다.
그렇다면 전화를 하거나 메신저를 이용해서
실시간으로 구매자와 판매자가 커뮤니케이션을 할 수 있도록 하는 것이 필요하다는 판단을 하게 되었다.
그래서 다른 여러 숙박 예약 서비스처럼 이메일을 보내는 방식으로 하려고 했다가
카카오톡 처럼 실시간으로 구매자와 판매자가 일대일 채팅을 할 수 있도록 이 기능을 구현해보기로 했다.
지난 포스팅에서 스프링부트 백엔드 서버와 NOSQL인 MongoDB를 연동하는 방식에 대해 정리했는데,
이번 포스팅에서는 실시간 일대일 채팅을 어떻게 구현했는지에 대해 중점적으로 정리해보려고 한다.
- 구매자는 숙소 상세페이지에서 해당 숙소의 판매자에게 채팅을 요청할 수 있다.
- 이미 한 대화가 있는지 확인해서 있으면 해당 채팅방의 채팅창으로 바로 이동한다.
없으면 구매자와 판매자의 채팅방이 새롭게 생성된다.
- 채팅창에서 구매자와 판매자가 카카오톡처럼 실시간으로 채팅을 주고 받을 수 있다.
- 채팅룸에서 나의 채팅 목록을 조회할 수 있다.
- 채팅룸에서 채팅을 클릭하면, 이전의 대화 내용을 볼 수 있고 이어서 실시간 채팅을 할 수 있다.
웹소켓과STOMP를 사용한 실시간 채팅 시스템에서,
사용자는Vue.js기반의프론트엔드를 통해 서버의/ws웹소켓엔드포인트에 연결을 시도한다.
- 연결이 성립되면 사용자는 채팅방을 생성하거나 기존 채팅방에 참여할 수 있으며,
이 요청은 서버에 의해 처리되어MongoDB에 새 채팅방이 생성되거나 해당 채팅방 정보가 반환된다.
- 사용자가 메시지를 보내면, 이 메시지는 서버로 전송되고
STOMP프로토콜을 통해
해당 채팅방의 모든 구독자에게Broadcast되어 실시간으로 채팅을 주고 받을 수 있다.


먼저 클라이언트와 서버 간의 실시간 통신 채널을 개설한다.
프론트엔드에서 SockJS 클라이언트를 생성하여 서버의 웹소켓 엔드포인트(/ws)에 연결 요청을 보낸다.
이 때 SockJS 라이브러리를 사용했는데, 서버와의 호환성을 높이고 브라우저 간의 일관성을 유지할 수 있다.
DetailsPage.vue
<template>
<button class="chat_button" @click="startChat">채팅하기</button>
</template>
<script>
import { mapStores } from "pinia";
import { useMemberStore } from "/src/stores/useMemberStore";
import { useChatStore } from "@/stores/useChatStore";
export default {
data() {
return {
houseDetails: null,
};
},
computed: {
...mapStores(useMemberStore, useChatStore),
},
async mounted() {
const id = this.$route.params.id;
await this.houseStore.getHouseDetails(id);
this.houseDetails = this.houseStore.houseDetails;
},
methods: {
// 채팅
async startChat() {
const buyerId = this.memberStore.decodedToken.id;
const buyerNickname = this.memberStore.decodedToken.nickname;
const sellerId = this.houseDetails.userId;
const sellerNickname = this.houseDetails.userNickname;
const houseId = this.houseDetails.id;
try {
const chatRoomId = await this.chatStore.createOrJoinChatRoom(buyerId, buyerNickname, sellerId, sellerNickname, houseId);
this.$router.push(`/chat/${chatRoomId}`);
} catch (error) {
console.error("Error starting chat:", error);
alert("Failed to start chat. Error: " + error.message);
}
}
},
};
</script>
useChatRoomStore.js
import { defineStore } from 'pinia';
import axios from 'axios';
const backend = process.env.VUE_APP_API_URL;
// const backend = process.env.VUE_APP_LOCAL_URL;
export const useChatRoomStore = defineStore('chatRoom', {
state: () => ({
chatRooms: [],
}),
actions: {
async loadChatRooms(userId) {
const response = await axios.get(`${backend}/chat-room/list/${userId}`);
this.chatRooms = response.data;
return this.chatRooms;
},
async loadChatRoomById(chatRoomId) {
const response = await axios.get(`${backend}/chat-room/${chatRoomId}`);
return response.data;
},
getChatPartnerNickname(chatRoom, userId) {
return chatRoom.buyerId === userId ? chatRoom.sellerNickname : chatRoom.buyerNickname;
}
}
});
그리고 백엔드에서는 WebSocketConfig 클래스에서 registerStompEndpoints() 메소드를 통해
웹소켓 엔드포인트를 설정하고, 클라이언트의 연결 요청을 수락한다.
이 클래스는 STOMP 메시지 브로커 설정을 포함하여, 메시지 교환 경로를 명확히 정의한다.
WebSocketConfig.class
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/*
WebSocket과 STOMP 메시징을 위한 설정을 추가.
이 설정은 클라이언트가 서버에 연결할 수 있는 엔드포인트와 메시지 브로커를 설정.
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 메시지를 브로드캐스팅할 때 사용할 prefix 설정
registry.enableSimpleBroker("/topic");
// 클라이언트에서 메시지를 보낼 때 사용할 prefix 설정
registry.setApplicationDestinationPrefixes("/app");
}
}
ChatRoomController.class
@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/chat-room")
public class ChatRoomController {
private final ChatRoomService chatRoomService;
@GetMapping("/list/{userId}")
public ResponseEntity<List<ChatRoomDto>> getChatRoomsByUserId(@PathVariable Long userId) {
List<ChatRoomDto> chatRooms = chatRoomService.getChatRoomsByUserId(userId);
return ResponseEntity.ok(chatRooms);
}
@GetMapping("/{chatRoomId}")
public ResponseEntity<ChatRoomDto> getChatRoomById(@PathVariable String chatRoomId) {
ChatRoomDto chatRoom = chatRoomService.getChatRoomById(chatRoomId);
return ResponseEntity.ok(chatRoom);
}
}
ChatRoom.class
@Document(collection = "chatrooms")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatRoom {
@Id
private String id;
private Long buyerId;
private String buyerNickname;
private Long sellerId;
private String sellerNickname;
private Long houseId;
}
ChatRoomDto.class
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ChatRoomDto {
private String id;
private Long buyerId;
private String buyerNickname;
private Long sellerId;
private String sellerNickname;
private String houseName;
private Long houseId;
public String getChatPartnerNickname(Long userId) {
return userId.equals(buyerId) ? sellerNickname : buyerNickname;
}
}
ChatRoomRepository.interface
@Repository
public interface ChatRoomRepository extends MongoRepository<ChatRoom, String> {
Optional<ChatRoom> findByBuyerIdAndSellerId(Long buyerId, Long sellerId);
List<ChatRoom> findByBuyerIdOrSellerId(Long buyerId, Long sellerId);
}
ChatRoomService.class
@Service
@RequiredArgsConstructor
public class ChatRoomService {
private final ChatRoomRepository chatRoomRepository;
private final HouseRepository houseRepository;
public List<ChatRoomDto> getChatRoomsByUserId(Long userId) {
List<ChatRoom> chatRooms = chatRoomRepository.findByBuyerIdOrSellerId(userId, userId);
List<ChatRoomDto> chatRoomDtos = new ArrayList<>();
for (ChatRoom chatRoom : chatRooms) {
chatRoomDtos.add(convertToDto(chatRoom));
}
return chatRoomDtos;
}
public ChatRoomDto getChatRoomById(String chatRoomId) {
ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId)
.orElseThrow(() -> new RuntimeException("ChatRoom not found"));
return convertToDto(chatRoom);
}
private ChatRoomDto convertToDto(ChatRoom chatRoom) {
String houseName = houseRepository.findById(chatRoom.getHouseId())
.map(House::getName)
.orElse("No House Found");
return ChatRoomDto.builder()
.id(chatRoom.getId())
.buyerId(chatRoom.getBuyerId())
.buyerNickname(chatRoom.getBuyerNickname())
.sellerId(chatRoom.getSellerId())
.sellerNickname(chatRoom.getSellerNickname())
.houseName(houseName)
.houseId(chatRoom.getHouseId())
.build();
}
}
프론트엔드에서 사용자가 채팅방을 생성하려고 할 때,POST 요청을 /chat/room 엔드포인트로 보낸다.프론트엔드에서 사용자가 메시지를 입력하고 'send' 버튼을 클릭하면,JSON 형식으로 서버의 /app/send/{chatRoomId} 경로로 전송된다.ChatPage.vue
<template>
<div class="chat-page">
<div class="back-button-container">
<button @click="goToChatRooms" class="back-button">채팅 목록</button>
</div>
<div class="chat-container">
<header class="chat-header">{{ chatPartnerNickname }}</header>
<div class="chat-messages" ref="chatMessages">
<div v-for="msg in messages" :key="msg.id" :class="{'message': true, 'message-sent': isSentMessage(msg), 'message-received': !isSentMessage(msg)}">
<div :class="{'message-info': true, 'sent': isSentMessage(msg), 'received': !isSentMessage(msg)}">
<strong>{{ msg.senderNickname }}</strong>
</div>
<div :class="{'message-text': true, 'sent': isSentMessage(msg), 'received': !isSentMessage(msg)}">{{ msg.message }}</div>
<div :class="{'message-time': true, 'sent': isSentMessage(msg), 'received': !isSentMessage(msg)}">
{{ formatTime(msg.date) }}
</div>
</div>
</div>
<div class="message-input">
<input v-model="newMessage" @keyup.enter="sendMessage" placeholder="Type a message...">
<button @click="sendMessage">Send</button>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted, computed, watch, nextTick, onUnmounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useChatStore } from "@/stores/useChatStore";
import { useMemberStore } from "@/stores/useMemberStore";
import { useChatRoomStore } from "@/stores/useChatRoomStore";
export default {
setup() {
const route = useRoute();
const router = useRouter();
const roomId = route.params.roomId;
const chatStore = useChatStore();
const chatRoomStore = useChatRoomStore();
const memberStore = useMemberStore();
const newMessage = ref('');
const chatMessages = ref(null);
const chatPartnerNickname = ref('');
const loadChatPartnerNickname = async () => {
const userId = memberStore.decodedToken.id;
const chatRoom = await chatRoomStore.loadChatRoomById(roomId);
chatPartnerNickname.value = chatRoomStore.getChatPartnerNickname(chatRoom, userId);
};
onMounted(async () => {
await loadChatPartnerNickname();
await chatStore.loadMessages(roomId);
chatStore.connectToWebSocket(roomId);
nextTick(scrollToBottom);
});
onUnmounted(() => {
chatStore.disconnectWebSocket();
});
const messages = computed(() => chatStore.messages.value);
watch(messages, () => {
nextTick(scrollToBottom);
});
const sendMessage = async () => {
if (newMessage.value.trim()) {
const { nickname, id } = memberStore.decodedToken;
chatStore.sendMessage({
message: newMessage.value,
roomId,
senderNickname: nickname,
senderId: id,
});
newMessage.value = '';
setTimeout(scrollToBottom, 100);
}
};
const goToChatRooms = () => {
router.push('/chatRooms');
};
const formatTime = (dateString) => {
const date = new Date(dateString);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
const isSentMessage = (msg) => {
const { id } = memberStore.decodedToken;
return msg.senderId === id;
};
const scrollToBottom = () => {
if (chatMessages.value) {
chatMessages.value.scrollTop = chatMessages.value.scrollHeight;
}
};
return { messages, newMessage, sendMessage, goToChatRooms, formatTime, isSentMessage, chatMessages, chatPartnerNickname };
},
};
</script>
useChatStore.js
import { defineStore } from 'pinia';
import axios from 'axios';
import { Client } from '@stomp/stompjs';
import SockJS from 'sockjs-client';
import { ref } from 'vue';
const backend = process.env.VUE_APP_API_URL;
// const backend = process.env.VUE_APP_LOCAL_URL;
export const useChatStore = defineStore('chat', {
state: () => ({
stompClient: null,
messages: ref([]),
chatRoomId: null,
userId: null,
userNickname: '',
}),
actions: {
async createOrJoinChatRoom(buyerId, buyerNickname, sellerId, sellerNickname, houseId) {
const response = await axios.post(`${backend}/chat/room`, { buyerId, buyerNickname, sellerId, sellerNickname, houseId });
this.chatRoomId = response.data.id;
this.userId = buyerId;
this.userNickname = buyerNickname;
await this.connectToWebSocket(this.chatRoomId);
return response.data.id;
},
connectToWebSocket(roomId) {
const socket = new SockJS(`${backend}/ws`);
this.stompClient = new Client({
webSocketFactory: () => socket,
onConnect: () => {
this.stompClient.subscribe(`/topic/chat/${roomId}`, (message) => {
this.messages.value.push(JSON.parse(message.body));
});
},
onStompError: (error) => {
console.error('Stomp Error', error);
},
onWebSocketClose: () => {
console.error('WebSocket connection closed. Retrying in 5 seconds...');
setTimeout(() => {
this.connectToWebSocket(roomId);
}, 5000); // 5초 후에 다시 연결 시도
},
reconnectDelay: 5000, // 재연결 시도 간격
});
this.stompClient.activate();
},
disconnectWebSocket() {
if (this.stompClient) {
this.stompClient.deactivate();
}
},
async sendMessage({ message, roomId, senderNickname, senderId }) {
if (this.stompClient && this.stompClient.connected) {
const chatMessage = {
chatRoomId: roomId,
senderId: senderId,
senderNickname: senderNickname,
message: message,
date: new Date().toISOString(),
};
this.stompClient.publish({
destination: `/app/send/${roomId}`,
body: JSON.stringify(chatMessage),
});
// 화면에 바로 추가하지 않음
} else {
console.error('WebSocket is not connected.');
}
},
async loadMessages(roomId) {
const response = await axios.get(`${backend}/chat/room/${roomId}`);
this.messages.value = response.data;
}
}
});
백엔드에서는 ChatController의 createChatRoom() 메소드가 이 요청을 처리한다. 채팅방이 이미 존재하지 않으면 새로운 채팅방을 생성하고,MongoDB에 저장 후 생성된 채팅방 정보를 클라이언트에 반환한다.백엔드에서는 ChatController의 sendMessage() 메소드는 이 메시지를 받아 처리하고,STOMP를 통해 해당 채팅방을 구독하고 있는 모든 클라이언트에게 메시지를 Broadcast.@MessageMapping과 @SendTo 어노테이션을 사용하여ChatController.class
@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/chat")
public class ChatController {
private final ChatService chatService;
@PostMapping("/room")
public ResponseEntity<ChatRoom> createChatRoom(@RequestBody ChatRoomDto chatRoomDto) {
ChatRoom chatRoom = chatService.ensureChatRoom(chatRoomDto.getBuyerId(), chatRoomDto.getBuyerNickname(),
chatRoomDto.getSellerId(), chatRoomDto.getSellerNickname(), chatRoomDto.getHouseId());
return ResponseEntity.ok(chatRoom);
}
@MessageMapping("/send/{chatRoomId}")
@SendTo("/topic/chat/{chatRoomId}")
public ChatDto sendMessage(@Payload ChatDto chatDto, @DestinationVariable String chatRoomId) {
chatDto.setChatRoomId(chatRoomId);
ChatDto savedChat = chatService.saveChat(chatDto);
log.debug("Message sent to room {}: {}", chatRoomId, savedChat);
return savedChat;
}
@GetMapping("/room/{chatRoomId}")
public ResponseEntity<List<ChatDto>> getChatsByRoomId(@PathVariable String chatRoomId) {
List<ChatDto> chats = chatService.getChatsByRoomId(chatRoomId);
return ResponseEntity.ok(chats);
}
}
Chat.class
@Document(collection = "chats")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Chat {
@Id
private String id;
private String chatRoomId;
private Long senderId;
private String senderNickname;
private String message;
@CreatedDate
private LocalDateTime date;
public static ChatDto convertToDto(Chat chat) {
return ChatDto.builder()
.id(chat.getId())
.chatRoomId(chat.getChatRoomId())
.senderId(chat.getSenderId())
.senderNickname(chat.getSenderNickname())
.message(chat.getMessage())
.date(chat.getDate().atZone(ZoneId.of("Asia/Seoul")).toOffsetDateTime())
.build();
}
}
ChatDto.class
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ChatDto {
private String id;
private String chatRoomId;
private Long senderId;
private String senderNickname;
private String message;
private OffsetDateTime date;
public static Chat convertToEntity(ChatDto chatDto) {
return Chat.builder()
.id(chatDto.getId())
.chatRoomId(chatDto.getChatRoomId())
.senderId(chatDto.getSenderId())
.senderNickname(chatDto.getSenderNickname())
.message(chatDto.getMessage())
.date(chatDto.getDate().toLocalDateTime())
.build();
}
}
ChatRepository.interface
@Repository
public interface ChatRepository extends MongoRepository<Chat, String> {
List<Chat> findByChatRoomId(String chatRoomId);
}
ChatService.class
@Service
@RequiredArgsConstructor
public class ChatService {
private final ChatRoomRepository chatRoomRepository;
private final ChatRepository chatRepository;
@Transactional
public ChatRoom ensureChatRoom(Long buyerId, String buyerNickname, Long sellerId, String sellerNickname, Long houseId) {
return chatRoomRepository.findByBuyerIdAndSellerId(buyerId, sellerId)
.orElseGet(() -> {
ChatRoom newRoom = ChatRoom.builder()
.id(UUID.randomUUID().toString())
.buyerId(buyerId)
.buyerNickname(buyerNickname)
.sellerId(sellerId)
.sellerNickname(sellerNickname)
.houseId(houseId)
.build();
return chatRoomRepository.save(newRoom);
});
}
public List<ChatDto> getChatsByRoomId(String chatRoomId) {
List<Chat> chats = chatRepository.findByChatRoomId(chatRoomId);
List<ChatDto> chatDtos = new ArrayList<>();
for (Chat chat : chats) {
chatDtos.add(Chat.convertToDto(chat));
}
return chatDtos;
}
public ChatDto saveChat(ChatDto chatDto) {
Chat chat = ChatDto.convertToEntity(chatDto);
chat = chatRepository.save(chat);
return Chat.convertToDto(chat);
}
}
웹소켓은 서버와 클라이언트 간의 양방향 통신을 지원하며,웹소켓 연결은 지속적으로 유지되기 때문에STOMP는 텍스트 기반의 간단한 메시징 프로토콜로,메시징 패턴도 구현할 수 있다.HTTP 폴링에 비해 웹소켓은 서버와 네트워크 자원을 효율적으로 사용한다.배포 후 도메인에서 촬영한 영상입니다.
⚙️ 버튼 클릭 후 화질을 높여서 시청해주시면 감사하겠습니다 🤣


정말 뿌듯했다.
구현을 완료하고 나서 다시 테스트하면서
카카오톡? ㄴㄴ 캠핑온톡 ㅇㅇ
이런 생각이 감히 들었다.

에러가 계속 생기면서 개발 기간이 늘어나고 있었는데,
이렇게는 안되겠다는 생각에 작정하고 30여 시간을 밤 샌 끝에 성공한거라 만족감이 훨씬 컸다.
성능과 GUI도 생각한 그대로 되어서 정말 다행이다.
한편으로는 이미 여러 메신저들이 상용화되어 있는데,
내부적으로 어떻게 되어 있는지, 어떤 기술을 사용해서 개발한 것인지 정말 궁금했다.
대기업들은 대개 자체 NOSQL 데이터베이스를 사용한다고 들었는데,
어떤 데이터베이스를 사용하는지, 그리고 내가 한 것처럼 혹시 하지는 않았을까 기대가 되기도 했다.
그리고 내가 구현한 건, 단순히 실시간 일대일 채팅이지만 다대다 채팅, 화상 채팅 등 여러 채팅이 있는데
그런 기능들도 언젠가 도전해보고 싶다는 생각이 들었다.
총 기간은 약 5일 가량 소요되었는데, 근래 한 개발 중 가장 어려웠던 작업이었다.
우선 웹소켓, STOMP의 개념을 이해해야 내부 흐름을 파악하여 코드를 작성할 수 있기 때문에
공부를 하면서 이해하는 시간을 충분히 가졌다.
전체 개발 과정에서 위 시연 영상에서 나오는 채팅창 화면을 구현할 때, 내 수면 시간을 가장 많이 빼앗겼다.
의외로 웹소켓 연결을 해서 채팅을 보냈을 때, 저장해서 MongoDB에 저장된 데이터를 확인하기까지는
빠르게 되었다. 이미 DB에 저장이 잘 되는 것을 확인한 상태에서 웹소켓 연결만 추가한 것이기 때문이다.
하지만 카카오톡처럼 정말 실시간으로 채팅을 주고 받을 수 있도록 하는 것이 목표였기에
똑같이 구현하는 작업이 정말 쉽지 않았다.
구현에 성공한 후, 내가 왜 못했었는지 생각해보았다.
이유는 Vue에 대한 깊은 이해를 하고 있지 못했기 때문이었다.
지금까지는 store에 백엔드 API 호출하는 메소드를 정의해놓고 페이지에서 메소드를 호출해
사용하고, 그 과정에서 주고 받는 데이터들을 전달하는 부분만 잘 신경쓰면 큰 문제 없었다.
나는 백엔드 엔지니어이니까 그 정도만 알아도 충분하다고 생각했다.
하지만 Java도 그렇지만 Vue도 정말 복잡하기 때문에 어느 정도의 깊이감 있는 이해가 있지 않으면
문제가 발생했을 때, 수정하는 것이 정말 쉽지 않았다.
예를 들면
- 구매자가 채팅을 보내서 화면에 출력이 되었는데 상대방 채팅 화면에 곧바로 출력이 되지 않고
새로고침을 해야 출력이 되는 문제
- 채팅을 보냈고 상대방에서도 실시간으로 받았는데, 내가 보낸 내용이 화면에 2번 출력이 되는 문제
문제의 원인은 프론트엔드 Vue의 코드를 잘못 구성했기 때문이었다.
그래서 이번 기회에 문제를 해결하면서 Vue의 문법에 대해서 기반을 다지는 기회로 삼아 공부했다.
개발을 하면서 느낀 건, 내가 실무에 가서 프론트엔드 코드를 건드릴 일이 없다고 하더라도
내가 만든 코드가 어디에서 어떻게 사용되는지 알고 개발하는 것과 그렇지 않은 건 천지차이라는 것이다.
현재 이 프로젝트를 진행할 때, 내가 풀스택 개발자를 지망하는 것은 아니지만
부트캠프에서 배운 지식을 최대한 활용해 내가 백엔드와 프론트엔드 모두 개발을 하고 있다.
그래서 백엔드를 우선 만들어놓고 프론트엔드를 만드는 순으로 개발을 하고 있는데,
백엔드 개발자와 프론트엔드 개발자가 소통하듯이 스스로 소통을 하면서 수정을 하고 있다.
이 과정에서 서로의 상황을 고려하지 않고 개발을 했기 때문에
둘 다 수정을 해야 하는 상황이 빈번하게 발생했다.
그래서 '프론트엔드에서 이렇게 사용할 거니까 백엔드에서 이렇게 해야겠다' 라는 생각을 갖고 하면
두 번 일할 필요 없이 쉽게 바로 바로 할 수 있다.
그래서 다음 작업부터는 부트캠프에서 최종 프로젝트를 진행할 때처럼
스프린트만 정해서 하는 걸 넘어 WBS를 작성하고, API 명세서도 미리 작성해서 할 것이다.
현업에서 괜히 이렇게 하는 것이 아니라는 생각을 다시 한 번 하게 되었다.
미리 서로 약속이 되어 있으면, 앞서 언급한 불필요한 상황을 사전에 막고 빠르게 일을 처리할 수 있을 것이다.
감사합니다 덕분에 프로젝트하는데 도움이 많이 됩니다