[항해99] 클론코딩 후기 feat. SockJS, Stomp

김헤일리·2022년 12월 29일
0

TIL

목록 보기
16/46

미니 프로젝트가 끝나고 클론코딩 프로젝트를 진행했다.

클론코딩 때 우리 조의 목적은 완벽한 결과물을 제출하는 것 보다 실전때도 활용할 수 있는 새로운 기술에 도전하고 숙지하는 것을 목표로 잡았다.

그렇게 채팅을 구현하기 위해 웹소켓에 도전했고... 역시 세상에 공부할 내용은 너무 많았다.


1. 웹소켓이란?

📤 양방향 통신 (TCP/IP)

기존 브라우저와의 통신이 클라이언트가 서버로 request를 보내야만 이뤄졌다면, 웹소켓은 클라이언트의 request 없이도 서버가 클라이언트로 데이터를 전송할 수 있는 양방향 통신 구조를 가진 transport protocol이다.

웹소켓도 API이기 때문에 하나의 HTTP 접속으로 양방향 메세지를 자유롭게 주고받을 수 있다.
양방향 통신의 특성상 실시간으로 이뤄지기 때문에 채팅, 게임, 실시간 차트 등 실시간이 요구되는 응용 프로그램에 자주 사용된다. 기존 단방향 통신과 다르게 새로고침이나 페이지 이동 없이도 같은 브라우저 내에서 데이터가 업데이트 되는 것이다.

새로고침 없이 데이터가 업데이트되는 것은 Ajax와 비슷하다고 볼 수 있지만, Ajax도 클라이언트가 요청을 보내야 서버가 응답하기 때문에 실시간으로 적용되지 않는다.

  • http 연결은 사진 속 예시와 같이 리퀘스트 -> 리스폰스 후 연결을 새로 시작한다.
  • websocket의 경우, websocket connection이 끝나기 전까지 연결이 지속되고, 리퀘스트 없이도 서버에서 정보를 전달 할 수 있다.
  • 데이터 전송이 statefull하기 때문에 서버의 부하도 더 낮다고 한다.

📤 웹소켓의 종류

1. socket.io

  • 인터넷 익스플로러 구버전 사용자는 웹소켓으로 작성된 웹페이지를 볼 수 없다.
  • 이런 문제를 해결하기 위해서 socket.io는 웹페이지가 열리는 브라우저가 웹소켓을 지원하면 일반 웹소켓 방식으로, 지원하지 않으면 http를 이용해 웹소켓을 흉내내는 방식으로 통신을 지원한다.

2. sockjs

  • spring에서 구버전 브라우저 문제를 해결하기 위한 방법으로 sockjs를 제공한다.
  • 서버 개발시 일반 웹소켓으로 통신할지 sockjs 호환으로 통신할지 결정할 수 있다.

3. stomp

  • 단순 텍스트 지향 메시징 프로토콜이다. spring에 종속적이고, 구독 방식으로 사용하고 있다.
  • Nodejs를 이용할 땐 socket.io를 주로 사용하고, spring을 사용할 땐 stomp, sockjs를 주로 사용한다.

이번 프로젝트에서 백엔드는 spring을 사용했기 때문에, 우리 조는 sockjs와 stomp를 사용했다.


🤯 리액트 적용

❗️ 채팅방 리스트 컴포넌트 부분

  • 생성된 채팅방의 리스트가 보이고, 채팅방 클릭 시 채팅 페이지로 이동하게 된다.
  • 웹소켓 미적용
function FriendList() {
  const chatrooms = useSelector((state) => state.rooms.rooms);
  // 1. 채팅방 리스트를 불러오기 위해 전역으로 상태 관리를 한다.
  const userInfo = useSelector((state) => state.rooms.userInfo);
  // 2. 채팅방 입장 시 사용자 정보 (닉네임)을 넘길 수 있도록 사용자 정보도 전역으로 상태관리를 한다.

  const dispatch = useDispatch();
  const navigate = useNavigate();
  const [roomTitle, setRoomTitle] = useState("");
  // 3. 채팅방 생성 시 방 제목을 기입하는 인풋필드의 value state를 감지하기 위해 useState로 선언.


  function enterRoom(roomId) {
  // 4. 입장하기 버튼 클릭 시 실행되는 함수.
  // 4-1. 특정 방에 들어가기 위해서 해당 방의 path variable(roomId)를 매개변수로 받는다.
    localStorage.setItem("wschat.nick", userInfo.nickname);
    // 4-2. 입장 시 로컬스토리지에 저장되어있던 사용자의 닉네임을 받아온다.
    localStorage.setItem("wschat.roomId", roomId);
    // 4-3. 입장 시 로컬스토리지에 저장되어있던 채팅방 id를 받아온다.
    navigate(`/chat/room/${roomId}`);
    // 4-4. roomId로 받아온 path variable의 값으로 이동해야 하는 채팅방의 url을 설정한다.
  }

  async function createARoom(roomName) {
  // 5. 방을 생성할 경우 실행되는 함수다.
  // 5-1. 인풋필드에 작성한 방 이름을 roomName이라는 변수로 받아 함수의 매개변수로 사용한다.
    if ("" === roomName) {
      alert("방 제목을 입력해 주십시요.");
      return;
    } else {
      await dispatch(createRoom(roomName));
      // 5-2. 함수 실행 시 dispatch를 통해 slice 파일에 있는 createRoom 함수로 roomName을 보낸다.
      dispatch(readAllRooms());
      // 5-3. 방이 만들어질 때 방 리스트를 다시 한번 불러와서 만들어진 방이 새롭게 리스트에 업데이트되도록 한다.
      setRoomTitle("");
      // 5-4. 인풋필드에 입력한 값을 초기화한다.
    }
  }

  useEffect(() => {
    dispatch(readAllRooms());
  }, [dispatch]);
  // 6. dispatch가 실행될때마다 readAllRooms가 실행되어 채팅방 전체 리스트를 불러온다.

❗️ 채팅방 리스트 slice 부분

  • 채팅방 Create, Read api 연결을 위해 만들어진 페이지
  • 웹소켓 미적용
const initialState = {
  rooms: [], // 1. 생성된 채팅방 리스트
  userInfo: { // 2. 사용자의 정보
    username: "",
    nickname: "",
  },
  isLoading: false,
  error: null,
};

export const readAllRooms = createAsyncThunk(
// 3. realAllRooms라는 상수에 비동기 함수를 할당한다.
  "rooms/READ_ROOMS",
  // 3-1. createAsyncThunk 함수의 첫번째 매개변수로 액션 밸류를 지정한다.
  async (payload, thunkAPI) => {
  // 3-2. 두번째 매개변수로는 서버로 보낼 payload와 데이터를 다시 extra reducer로 전달해줄 thunkAPI를 할당한다.
    try {
      const response = await authInstance.get("/chat/rooms");
      // 3-3. response라는 상수에 서버로 request를 보낼 api 주소를 적는다.
      // 3-4. get 방식으로 소통하는 경우, payload를 따로 보내지 않는다. (맞는 주소로 요청을 보내는 것이 중요)
      return thunkAPI.fulfillWithValue({
        rooms: response.data.chatRoomList,
        userInfo: response.data.userInfo,
      });
      // 3-5. 서버에서 리퀘스트에 대한 응답으로 보내준 내용중, rooms엔 response.data.chatRoomList를 할당해서 채팅방에 대한 리스트를 받는다.
      // 3-6. 사용자에 대한 정보는 userInfo에 response.data.userInfo를 지정해서 따로 받는다.
    } catch (error) {
      // 3-7. 서버로부터 모종의 이유로 에러를 전달 받으면 thunkAPI를 통해 extra reducer로 전달한다.
      return thunkAPI.rejectWithValue(error);
    }
  }
);

export const createRoom = createAsyncThunk(
// 4. 채팅방 리스트에서 방 만들기 함수가 실행되면 dispatch를 통해 roomName을 매개변수로 전달받아서 실행되는 비동기 함수이다.
  "room/CREATE_ROOM",
  // 4-1. createAsyncThunk()를 활용하게되면 첫번째 매개변수로 액션의 이름 (액션밸류)를 정의한다.
  // 4-2. createAsyncThunk()를 활용할 경우, 선언한 액션 이름에 pending, fulfilled, rejected에 대한 action을 자동으로 부여한다. 
  // 4-3. 리덕스에서 액션은 state에 어떤 변화가 필요할 때 발생되어야 하는 객체이다. 액션 객체엔 해당 객체의 type이 필수적으로 지정되어야하고, 객체에 들어갈 다른 값은 개발자의 필요에 따라서 지정된다.
  // 4-4. action value가 객체의 type을 지정하는 것
  async (payload, thunkAPI) => {
  // 4-4. createAsyncThunk의 두번째 매개변수는 미들웨어가 처리할 비동기(async) 함수이고, 실행 결과를 promise 형식으로 반환한다. (try, catch, finally)
  // 4-5. 처리할 비동기 함수에 넣는 매개변수는 해당 함수가 리턴할 값 (통칭 payload)과 promise 형태로 반환할 thunkAPI이다.
    try {
      const response = await authInstance.post(`/chat/room?name=${payload}`);
      // 4-6. response라는 변수 안에 axios instance와 메소드를 지정해서 서버에게 방을 생성할 때 필요한 데이터를 전달한다.
      // 4-7. 여기서 전댤한 데이터는 생성할 방의 roomName이었고, payload안에 roomName이 들어있기 때문에, api 주소 끝에 payload를 담아서 서버에 보내면, 서버는 프론트에게 방 생성에 필요한 데이터를 전달한다.
      return thunkAPI.fulfillWithValue(response.data);
      // 4-8. 서버가 프론트로부터 받은 payload를 기반으로 response를 request로 보내주면, 그 response에서 필요한 데이터를 뽑는다.
      // 4-9. 그래서 thunkAPI를 통해서 extra reducer로 response에서 data에 들어있는 항목만 빼서 보낸다. (response.data)
    } catch (error) {
      // 4-10. 만약 모종의 이유로 에러가 발생하면 thunkAPI로 error를 보낸다.
      return thunkAPI.rejectWithValue(error);
    }
  }
);

const chatRoomsSlice = createSlice({
// 5. createAsyncThunk() 를 통해서 thunkAPI로 보내진 데이터는 slice에 있는 extra reducer에게 보내진다.
  name: "rooms",
  // 5-1. slice의 이름을 지정해서 config store에 등록할 때 사용한다.
  initialState,
  // 5-2. 전역으로 관리할 state의 초기값을 지정한다. 
  reducers: {},
  extraReducers: {
  // 5-3. thunkAPI를 통해서 받아온 데이터는 extraRecuder로 보내진다.
    [readAllRooms.pending]: (state) => {
    // 5-4. createAsyncThunk()를 통해서 받아온 액션 중 pending 상태일때의 state를 지정한다.
      state.isLoading = true;
      // 5-5. pending 상태에서 state는 isLoading이고, 아직 로딩중이기 때문에 true라고 표시한다.
    },
    [readAllRooms.fulfilled]: (state, action) => {
    // 5-6. 이제 요청이 끝나고 액션이 fulfilled 상태일때의 state가 어떻게 되어야 하는지 지정해야한다.
    // 5-7. state는 action으로 인해 변경된다,
      state.isLoading = false;
      // 5-8. fulfilled일때 state는 더 이상 로딩중이 아니기 때문에 isLoading은 false가 된다.
      state.rooms = action.payload.rooms;
      // 5-9. state로 관리하는 rooms의 상태는 readAllRooms의 경우, 새로 변경된 값이 기존 state를 대체한다.
      // 5-10. 때문에 action.payload.rooms를 기존 state.rooms에 재할당함으로서 매번 채팅방 리스트는 갱신될 수 있다.
      state.userInfo = action.payload.userInfo;
      // 5-11. state로 관리하는 userInfo도 readAllRooms가 실행될 때 변경된 값이 기존 state를 대체하여 유저 정보를 갱신한다.
    },
    [readAllRooms.rejected]: (state, action) => {
    // 5-12. 요청이 끝나고 에러가 발생한 경우의 state의 변화도 지정해야한다.
      state.isLoading = false;
      // 5-13. 로딩은 끝났으니 false로 지정한다.
      state.error = action.payload;
      // 5-14. state의 에러 상태는 action의 결과값 (payload)기 때문에 state의 error도 action의 payload로 재할당한다.
    },
    [createRoom.pending]: (state) => {
    // 5-15. createRoom이 대기 상태일 때 state는 변화가 없고
      state.isLoading = true;
      // 5-16. state의 속성 중 isLoading이 true로 되어있다.
    },
    [createRoom.fulfilled]: (state, action) => {
    // 5-17. createRoom이 성공적으로 실행됐을 때의 state 변화를 지정한다.
      state.isLoading = false;
      // 5-18. 로딩이 끝났으니 false로 지정한다.
      // 5-19. state로 관리를 하지 않기 때문에 굳이 변화를 따로 지정하지 않았다.
    },
    [createRoom.rejected]: (state, action) => {
      state.isLoading = false;
      state.error = action.payload;
      // 5-19. state의 에러 상태는 action의 결과값 (payload)기 때문에 state의 error도 action의 payload로 재할당한다.
    },
  },
});

❗️ 채팅 페이지 컴포넌트 부분

  • 웹소켓을 적용해서 실시간으로 채팅이 일어날 수 있는 페이지다.
function Chat() {
  let SockJs = new SockJS("http://웹소켓과 연결되는 서버 주소");
  // 1. 스프링과 연결했기 때문에 SockJs를 사용했다.
  // 1-1.해당 url과 연결되어있는 웹소켓 서버에 연결하기 위한 client 객체를 만들어서 sockjs라는 변수에 저장.
  let ws = Stomp.over(SockJs);
  // 2. stomp라는 별도의 솔루션을 사용하기 때문에 sockjs 클라이언트 객체를 stopm.over 메소드로 감싸서 stompClient를 생성한다.
  let reconnect = 0;
  
  const dispatch = useDispatch();
  const navigation = useNavigate();
  const param = useParams();
  const roomId = param.id;
  
  const sender = localStorage.getItem("wschat.nick");
  // 3. localStorage에 저장된 닉네임을 출력해서 sender라는 변수에 담는다.
  const [message, setMessage] = useState("");
  // 4. 사용자가 작성하는 메세지 내용을 관리하기 위해 useState를 사용한다.
  // 4-1. 9-7 참고
  const messages = [];
  // 5. 작성된 메세지들을 하나하나 저장하기 위해 빈 배열을 선언한다,
  const [viewMessages, setViewMessages] = useState([]);
  // 6. 작성된 메세지들이 화면에 뿌려질 수 있도록 또 다른 변수로 상태 관리를 한다.
  // 6-1. map을 통해 보여지는 부분은 이 부분이다.
  const beforechat = useSelector((state) => state.chat.messageList);
  // 7. 채팅방 이전에 있던 내역을 다시 불러오기 위해서 전역 상태로 관리하고 있던 state 중 messageList를 불러와서 변수에 할당한다.

  const scrollRef = useRef();

  const scrollToBottom = () => {
    if (scrollRef.current) {
      scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
    }
  };

  function sendMessage() {
  // 9. 사용자가 메세지를 보내게되면 해당 함수가 실행된다.
    ws.send(
      "/app/chat/message",
      // 9-1. stomp client 객체를 이용해서 send 함수를 실행한다.
      // 9-2. 첫번째 매개변수로 메세지가 도달해야할 서버 주소를 할당하고
      {},
      JSON.stringify({
      // 9-3. 3번째 매개변수로 사용자가 보내게되는 메세지의 정보를 제이슨 형식으로 서버에 보낸다.
        type: "TALK",
        // 9-4. 입장 시 자동으로 전달되는 메세지가 아니라 사용자가 작성한 내용이 전달되기 때문에 해당 메세지의 type을 'talk'이라고 지정한다.
        roomId: roomId,
        // 9-5. 어떤 방에서 해당 채팅이 오갔는지 식별하기 위해 서버로 채팅방의 고유 번호를 넘기고
        sender: sender,
        // 9-6. 사용자를 식별하기 위해 사용자의 닉네임도 같이 보낸다.
        message: message,
        // 9-7. 그리고 마지막으로 사용자가 작성한 메세지의 내용도 같이 전달한다.
      })
    );
  }

  function recvMessage(recv) {
  // 10. 누군가가 보낸 메세지를 받을 때 실행되는 함수이다.
  // 10-1. roomSubscribe() 함수에서 정의된것 처럼 연결되었을 때 누군가가 메세지를 보내면 그 메세지를 자동으로 받게 된다.
    messages.push({
    // 10-2. 받게된 메세지는 messages라는 빈 배열에 추가해서 받은 메세지를 모두 저장한다.
      type: recv.type,
      // 10-3. 저장된 메세지의 타입은 내가 받은 메세지의 타입이다. (상대방의 enter 혹은 talk)
      sender: recv.type === "ENTER" ? "" : recv.sender,
      // 10-4. 만약 enter 타입의 메세지를 받은 경우 보낸이의 닉네임은 빈칸이고, 아닐 경우 메세지를 보낸이의 닉네임을 출력한다.
      message: recv.type === "ENTER" ? `[알림] ${recv.message}` : recv.message,
      // 10-5. enter 타입의 메세지를 받은 경우 메세지 내용 사전에 [알림]을 표시하고 아니라면 메세지의 내용만 표시한다.
    });
    setViewMessages([...messages]);
    // 10-6. 그리고 map을 돌려 화면에 출력하기 위헤 setViewMessages에 messages 배열을 넣어 모든 메세지를 확인할 수 있게 한다.
    // 10-7. 이때 깊은 복사를 통해서 messages 배열의 불변성을 지켜야한다.
  }

  function roomSubscribe() {
  // 8. 채팅방 입장 시 자동으로 실행되는 함수이다.
    ws.connect(
    // 8-1. stomp client 객체가 연결을 시도하고,
      {},
      function (frame) {
        ws.subscribe(`/topic/chat/room/${roomId}`, function (response) {
        // 8-2. 적혀있는 주소의 채팅방 웹소켓을 '구독'한다.
        // 8-3. 그리고 구독했을 때 response를 매개변수로 해서 또 두번째 매개변수에 있는 함수가 실행된다.
          var recv = JSON.parse(response.body);
          // 8-4. recv라는 변수에 response로 받아온 데이터 중 body에 해당하는 내용을 문자열로 변경한다.
          recvMessage(recv);
          // 8-5. 문자열로 만들어진 내용을 recvMessage라는 함수의 매개변수로 값을 넘긴다.
        });
        ws.send(
        // 8-6. 그리고 채티방에 처음 연결됐을 때 사용자는 어떠한 메세지를 보내게 된다.
          "/app/chat/message",
          // 8-7. 메세지 내용만 관리하는 url로,
          {},
          JSON.stringify({
            type: "ENTER",
            roomId: roomId,
            sender: sender,
          })
          // 8-8. 위의 내용을 제이슨 형식으로 서버에 보낸다.
          // 8-9. 들어온 방의 고유번호, 사용자의 닉네임을 "enter"라는 타입으로 서버에 보낸다.
        );
      },
      function (error) {
      // 8-10. 만약 웹소켓 연결 시도에 에러가 났다면,
        if (reconnect++ <= 5) {
        // 8-11. 재시도 횟수가 5번 이하일때까지
          setTimeout(function () {
          // 8-12. 텀을 두는 함수를 이용해서
            SockJs = new SockJS("/ws/chat");
            // 8-13. sockjs 클라이언트를 생성하고
            ws = Stomp.over(SockJs);
            // 8-14. sockjs 클라이언트를 감싸서 사용할 stopm 클라이언트 객체를 생성한다.
            roomSubscribe();
            // 8-15. 객체 생성 후 다시 해당 방을 '구독'하게 만든다.
          }, 10 * 1000);
          // 8-16. 10초마다 재연결을 시도한다.
        }
      }
    );
  }

  useEffect(() => {
    scrollToBottom();
  }, [viewMessages]);

  useEffect(() => {
    dispatch(readBeforeChat(param.id));
    // 11. 이전에 나눈 채팅 내용도 확인할 수 있도록 이전 대화 내역을 불러오는 함수를 실행시킨다.
    // 11-1. 이때 출력해야할 채팅 내용을 식별할 수 있도록 해당 채팅방의 고유 번호를 매개변수로 보낸다.
    roomSubscribe();
  }, []);

❗️ 채팅 페이지 slice 부분

  • 이전 채팅 내역을 서버에 저장하고, 해당 내역을 불러오는 api를 연결하기 위한 페이지다.
const initialState = {
  id: 11,
  roomName: "",
  createUserName: null,
  messageList: [], // 1. 메세지 내역을 전부 저장하는 곳
  isLoading: false,
  error: null,
};

export const readBeforeChat = createAsyncThunk(
// 2. readBeforeChat이라는 상수에 비동기 함수를 할당한다.
  "chat/READ_BEFORE_CHAT",
  // 2-1. createAsyncThunk 함수의 첫번째 매개변수로 액션 밸류를 지정한다.
  async (payload, thunkAPI) => {
  // 2-3. 두번째 매개변수로는 서버로 보낼 payload와 데이터를 다시 extra reducer로 전달해줄 thunkAPI를 할당한다.
    try {
      const response = await authInstance.get(`/chat/room/join/${payload}`);
      // 2-3. response라는 상수에 서버로 request를 보낼 api 주소를 적는다.
      // 2-4. 현재 서버로 전달하고자 하는 데이터는 지금까지 특정 채팅방의 id이다.
      return thunkAPI.fulfillWithValue(response.data.messageList);
      // 2-5. 서버로부터 다시 받아온 data 중에 messageList에 들어있던 내용만 extra reducer로 보낸다.
    } catch (error) {
      // 3-7. 서버로부터 모종의 이유로 에러를 전달 받으면 thunkAPI를 통해 extra reducer로 error를 전달한다.
      return thunkAPI.rejectWithValue(error);
    }
  }
);

const chatSlice = createSlice({
  name: "chat",
  initialState,
  reducers: {},
  extraReducers: {
    [readBeforeChat.pending]: (state) => {
      state.isLoading = true;
    },
    [readBeforeChat.fulfilled]: (state, action) => {
    // 4. 이제 요청이 끝나고 액션이 fulfilled 상태일때의 state가 어떻게 되어야 하는지 지정해야한다.
      state.isLoading = false;
      // 4-1. fulfilled일때 state는 더 이상 로딩중이 아니기 때문에 isLoading은 false가 된다.
      state.messageList = action.payload;
      // 4-2. state로 관리하는 messageList는 지금까지 messages에 쌓였던 내용을 모두 저장한다.
      // 4-3. 모든 메세지 내역을 불러오기 위해 thunkAPI로 받아온 response.data.messageList의 내용을 payload로서 기존 state에 재할당한다.
    },
    [readBeforeChat.rejected]: (state, action) => {
      state.isLoading = false;
      state.error = action.payload;
    },
  },
});


2. 그 밖에 클론코딩을 통해 배운 것:

🎨 Styled Component를 활용해서 UI 만들기

  • css를 잘 만들기 위해선 < div > 태그를 생각보다 많이 사용해야 한다는 것을 깨달았다...

  • 어떤 상황에서도 동일한 레이아웃을 위해 크기를 지정할 땐 px보다 % 혹은 rem을 사용하자.

  • styled.div 를 사용해서 특정 div를 생성하면, 해당 div 내부에 있는 특정 컴포넌트에게 스타일을 적용하고싶을 땐 {}를 사용하자.

    • 예시:

      const SecondLine = styled.div`
        display: flex;
        flex-direction: column;
        margin-left: 100px;
        width: 100%;
      
        gap: 20px;
      
        /* SecondLine에 해당하는 input과 button 요소는 {}를 이용해서 따로 css를 적용할 수 있다.*/
        input {
          width: 14rem;
          height: 1.5rem;
        }
        button:hover {
          background-color: #eeeeee;
          border: none;
        }
      `;
  • :hover, :focus등 다양하게 이용해서 좀 더 다이내믹한 페이지를 만들 수 있다.

  • 생각보다 css가 지원하는 디자인 요소는 무궁무진하다. 전부는 아니더라도 어느정도 개념을 숙지하자.


🔨 useForm()으로 로그인/회원가입 컴포넌트 업그레이드 도전

  • 이전 미니 프로젝트에선 각 input마다 useState()를 이용해서 유효성 검사를 했는데, useForm()을 사용하면 훨씬 더 중복되는 코드 없이 유효성 검사를 할 수 있다고 했다.
  • 보통 yup과 함께 사용한다고 하지만, 나는 일단 useForm()부터 익히기 위해 yup을 사용하지 않고 프로젝트를 진행해봤다.
const {
  register, // 1.어떤 인풋의 내용이 useForm을 사용할지 지정하고 매개 변수를 줘서 필요한 부분을 설정한다.
  handleSubmit,
  // 1-1. 그리고 useForm을 사용한 인풋의 값들을 handleSubmit을 실행할 때 가져온다.
  formState: { errors },
} = useForm();

<StInputGroup>
  <div>
  	<input
		{...register("username", {
        // 2. 해당 인풋은 username으로 정의되어 등록됐다.
          required: "이메일은 필수 입력입니다.",
          // 2-1. 필수값임을 표시한다.
        })}
        type="email"
		// 2-2. useForm에 등록된 인풋의 type
        name="username"
        placeholder="이메일"
    ></input>
    {errors.email && (
    // 2-3. 만약 해당 type에 오류가 발생할 경우,
      <p style={{ color: "red", fontSize: "12px" }}>
        {errors.email.message}
		// 2-4. required에 지정했던 에러 메세지를 출력한다.
      </p>
    )}
  </div>
  <div>
    <input
      {...register("password", {
      // 3. 해당 인풋은 username으로 정의되어 등록됐다.
        required: "비밀번호는 필수 입력입니다.",
        // 3-1. 필수값임을 표시한다.
      })}
      type="password"
	  // 3-2. useForm에 등록된 인풋의 type
      name="password"
      placeholder="비밀번호"
     ></input>
     {errors.password && (
     // 3-3. 만약 해당 type에 오류가 발생할 경우,
       <p style={{ color: "red", fontSize: "12px" }}>
         {errors.password.message}
		 // 3-4. required에 지정했던 에러 메세지를 출력한다.
       </p>
     )}
  </div>
</StInputGroup>


이번 프로젝트도 진짜 시작을 어떻게 할 지 너무 막막하고 어려운 시간이었다.
다른 팀원분들의 도움으로 프로젝트를 끝낼 수 있었는데, 정말... 내가 했다고 말 하기도 부담스러울 정도로 도움을 받았다. 😭😭😭 그래도 아주 조금은 웹소켓이 뭔지 감을 잡을 수 있는 시간이었기 때문에 값졌다고 할 수 있겠다.

다음번엔 프로젝트에 더 기여할 수 있도록 더 열심히 공부해야겠다!!!

출처:

profile
공부하느라 녹는 중... 밖에 안 나가서 버섯 피는 중... 🍄

0개의 댓글