Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[4주차] 권혜인 과제 제출합니다. #12

Open
wants to merge 182 commits into
base: master
Choose a base branch
from

Conversation

hae2ni
Copy link

@hae2ni hae2ni commented Nov 1, 2024

✨ 구현

구현링크호ㅇ홍

🌠 Key Questions

  • React Router의 동적 라우팅(Dynamic Routing)이란 무엇이며, 언제 사용하나요?
  • 네트워크 속도가 느린 환경에서 사용자 경험을 개선하기 위해 사용할 수 있는 UI/UX 디자인 전략과 기술적 최적화 방법은 무엇인가요?
  • React에서 useState와 useReducer를 활용한 지역 상태 관리와 Context API 및 전역 상태 관리 라이브러리의 차이점을 설명하세요.

블로그에 정리해두었습니다


🌟 미션 목표

  • TypeScript를 적극적으로 활용하여 코드의 타입 안정성을 확보합니다.
  • React Router를 사용해 페이지 간 라우팅을 구현하고, 동적 경로와 URL 파라미터를 이해합니다.
  • useState와 useReducer를 사용하여 컴포넌트의 로컬 상태를 관리하고, 상태 관리 로직을 최적화합니다.
  • Tailwind CSS 혹은 Styled Components로 스타일링하여 일관된 디자인 시스템을 구축합니다.

--

💎 필수 요건

  • 피그마를 보고 결과화면과 같이 구현합니다.
  • 친구 목록 기능 구현
  • React Router를 사용하여 친구 목록, 채팅방 목록, 채팅방 등의 페이지를 각각 구성합니다.
  • 로컬스토리지에 채팅방 내용을 저장하여 새로고침시에도 데이터가 유지되도록 합니다.
  • 메세지에 유저 정보(프로필 사진, 이름)를 표시합니다.
  • user와 message 데이터를 json 파일에 저장합니다.
  • UI는 반응형을 제외하고 피그마파일을 따라서 진행합니다.

🧩 PR Point

솔직히,,, 막 디자인적으로 어려웠던 부분도 많이 없었고, 로직도 복잡한 부분은 3주차 때 이미 쳐낸 뒤라,,, 😢

❤️ Router

필수 요건이었던 Router 관련입니다..

export const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
    children: [
      { index: true, element: <Home /> },
      {
        path: "/message/:userId",
        element: <ChattingRoom />,
      },
      { path: "/chattinglist", element: <Chattings /> },
      { path: "/myprofile", element: <MyProfile /> },
    ],
  },
]);

💚 Layout

사실 디자인을 쭊 보고 Layout을 좀더 확실히 했어야 했는데
얼레벌레 시간에 쫓기다 보니, 눈에 크게 보인 부분만 Layout으로 뺴놔서 나중에 좀 고생하긴 했지만,,,네,,

export default function Layout() {
  const location = useLocation();
  const pathname = location.pathname;
  const isMessageRoom = pathname.includes("/message");
  const isMyPage = pathname.includes("/myprofile");

  return (
    <Container image={backgroundImg} $isMyPage={isMyPage} $isMessageRoom={isMessageRoom}>
      {!isMobile && <MemoizedPhoneHeader isMyPage={isMyPage} />}
      <Content>
        <Outlet />
      </Content>

      {!isMessageRoom && !isMyPage && <NavigateBar />}

      {!isMobile && (
        <footer>
          {isMyPage ? <HomeBarImg src={LightHomeBar} alt="light" /> : <HomeBarImg src={HomeBar} alt="HomeBar" />}
        </footer>
      )}
    </Container>
  );
}

💙 채팅 리스트

export default function List() {
  const newDummyText = useStore((state) => state.dummyText);

  function getLastMessages(chatData: chatType[]) {
    return chatData.map((chat) => {
      const lastMessage = chat.messages[chat.messages.length - 1];
      return {
        userId: chat.userId,
        user: chat.user,
        lastText: lastMessage.text,
        lastTime: lastMessage.time,
      };
    });
  }

  const lastMessages = getLastMessages(newDummyText);

  return (
    <Container>
      {lastMessages.map((message) => {
        const { lastText, lastTime, user, userId } = message;

        return (
          <Link to={`/message/${userId}`}>
            <OneChat key={userId} lastText={lastText} lastTime={lastTime} user={user} userId={userId} />
          </Link>
        );
      })}
    </Container>
  );
}

마지막 메세지가 리스트에 보일 수 있도록 구현했습니다!

💛 zustand 사용

import { CHAT_DATA } from "constant/chatData";
import { chatType } from "types/chatType";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";

interface StoreState {
  inputValue: string;
  setInputValue: (text: string) => void;
  dummyText: chatType[];
  addNewText: (userId?: string) => void;
}

export const useStore = create<StoreState>()(
  persist(
    (set) => ({
      dummyText: CHAT_DATA,
      inputValue: "",

      setInputValue: (text) => set({ inputValue: text }),

      addNewText: (userId) =>
        set((state) => {
          const newText = {
            sender: "me",
            text: state.inputValue,
            time: new Date().toISOString(),
          };

          const updatedDummyText = state.dummyText.map((friend) =>
            friend.userId === userId
              ? {
                  ...friend,
                  messages: [...friend.messages, newText],
                }
              : friend,
          );

          return {
            dummyText: updatedDummyText,
            inputValue: "",
          };
        }),
    }),
    {
      name: "chat-storage",
      storage: createJSONStorage(() => localStorage),
    },
  ),
);

🔥 Truble Shooting

요거는 월욜 당일에 승완오빠가 알려줘서 알게 된 에러들인데,

  1. 맥북에서 input창에 글자 치고 enter 누르면 한글이 중복되는 이슈
  2. 베포판에서 메세지를 보냈을 때 새로고침을 하면 404에러가 뜨는 이슈

일단 1번 이슈는 단톡방에 올라온
블로그를 참고하여 고쳤습니다.

문제는 2번 이슌데 일단 배포판에서 그런 거 발견해서 root 폴더 안에
vercel.json 파일을 설정해 두었으나,
아직 해결이 안된 상태이고
시간 이슈로,,,, 아직 깊이 고민해보지 못했습니다.... (오늘 4연강^^)


⏲️ 과제 후기

생각보다 시간이 없다고 생각했는데,,(수욜 오후 시작) 생각보다 빨리 끝났습니다..
아무래도 제가 다른 분들에 비해 디자인, 기능 모두 심플한 편이라 그런 것 같습니다!
그래도,,, 예쁘다고 생각해요,,, ㅎㅎㅎ
고새 머리가 많이 굳어서, next 들어가면서 한번 공부 날 잡고 해야할 것 같다는 생각도 들었습니다
얼레벌레 코딩은 그만 하구 싶어여,,ㅎㅎ

value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onFocus={handleFocus}
onKeyDown={handlePressEnter}
Copy link

@westofsky westofsky Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

사진처럼 onKeyDown 썼을 때 한글 마지막이 두번 입력 되는데 한번 읽어보세요

Comment on lines +60 to +69
<RightWrapper>
<SmileICon />
{!isFocus ? (
<HashtagICon />
) : inputValue?.length !== 0 ? (
<ActiveIcon onClick={handleText} />
) : (
<InActiveIcon />
)}
</RightWrapper>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

내부 로직을 밖으로 분리하면 가독성이나 유지보수하기 좋을 것 같아요

Suggested change
<RightWrapper>
<SmileICon />
{!isFocus ? (
<HashtagICon />
) : inputValue?.length !== 0 ? (
<ActiveIcon onClick={handleText} />
) : (
<InActiveIcon />
)}
</RightWrapper>
function renderIcon() {
if (!isFocus) {
return <HashtagIcon />;
} else if (inputValue?.length !== 0) {
return <ActiveIcon onClick={handleText} />;
} else {
return <InActiveIcon />;
}
}
<RightWrapper>
<SmileIcon />
{renderIcon()}
</RightWrapper>

Comment on lines +30 to +32
<Link to={`/message/${userId}`}>
<OneChat key={userId} lastText={lastText} lastTime={lastTime} user={user} userId={userId} />
</Link>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key를 Link에 적용하는 건 어떨까요

Suggested change
<Link to={`/message/${userId}`}>
<OneChat key={userId} lastText={lastText} lastTime={lastTime} user={user} userId={userId} />
</Link>
<Link to={`/message/${userId}`} key={userId}>
<OneChat lastText={lastText} lastTime={lastTime} user={user} userId={userId} />
</Link>

Copy link

@jiwonnchoi jiwonnchoi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 혜인님:) 지난 주차에 이어서 이번 주 과제도 너무 수고하셨습니다🔥
컴포넌트를 잘 쪼개어주셔서 한 파일 당 코드가 길어지지 않아 리뷰하면서도 편했던 것 같아요! 코드 보면서 저도 새롭게 많이 알아갑니다❤️‍🔥

Comment on lines +23 to +29
"import/no-extraneous-dependencies": 0, // 테스트 또는 개발환경을 구성하는 파일에서는 devDependency 사용을 허용
"react/prop-types": 0,
"react/jsx-filename-extension": [2, { extensions: [".js", ".jsx", ".ts", ".tsx"] }],
"jsx-a11y/no-noninteractive-element-interactions": 0,
"eol-last": ["error", "always"], // line의 가장 마지막 줄에는 개행 넣기"
"simple-import-sort/imports": "error", // import 정렬
"no-multi-spaces": "error", // 스페이스 여러개 금지

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import를 저는 수동으로 구분해서 정렬해주었는데 eslint 설정을 잘 활용하신 것 같아요!! 저도 다음에 써봐야겠습니당👍

function App() {
return (
<ThemeProvider theme={theme}>
<RouterProvider router={router} />

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 기존에 browserRouter로 감싸고 경로를 설정하는 식으로 썼는데 찾아보니 RouterProvider가 v6.4에 추가된 방식이군요..! 아래쪽 router 정의해주신 부분 보니 children 옵션에 중첩 라우팅을 추가해주신 부분까지 정말 잘 쓰신 것 같아요 👍❤️‍🔥

import styled from "styled-components";
import { friendListType } from "types/friendListType";

type FavProfileProps = Pick<friendListType, "profile" | "user">;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존 타입에서 특정 속성만 선택해서 새로운 타입을 만드는 Pick 새롭네요 !! 불필요한 속성 없이 객체를 가볍게 가져갈 수 있어서 좋은 것 같습니다

Comment on lines +8 to +9
export default function FavProfile(props: FavProfileProps) {
const { profile, user } = props;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수 인자에서 바로 props를 구조 분해 할당하면 어떨까요!

Suggested change
export default function FavProfile(props: FavProfileProps) {
const { profile, user } = props;
export default function FavProfile({ profile, user }: FavProfileProps) {

Comment on lines +23 to +27
{isFavListOpen && (
<>
<FavFriendList />
</>
)}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요 빈 꺾쇠는 컴포넌트 하나여서 없애고 간결하게 써도 될 것 같습니당


if (sender != "me") {
return (
<FromContainer key={Math.random()}>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key값에 랜덤함수를 쓰신 이유가 있을까요? key는 컴포넌트의 고유성을 보장하기 위해 필요해서 난수보다는 uuid나 여기서는 해당 index, 시간 등을 조합하는 것도 방법일 것 같은데 아래 참고자료 한번 확인해보시면 좋을 것 같습니다!

https://velog.io/@jerrychu/React-%EC%98%AC%EB%B0%94%EB%A5%B4%EA%B2%8C-key-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-index-uuid-%EB%8C%80%EC%B2%B4-%EA%B0%80%EB%8A%A5%ED%95%A0%EA%B9%8C


//다음 메세지랑 동시에 엔터엔터,,,를 했냐마냐
const isSameTime =
nextMessage?.sender === sender && new Date(nextMessage?.time).getTime === new Date(time).getTime;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new Date().getTime 에서 getTime은 메서드라 괄호 ()를 붙여야 한다고 알고 있는데 이렇게 써도 무방한가요..!?

Comment on lines +13 to +14
<MessageTimeWrapper $showProfile={showProfile}>
<FromMessageBox showProfile={showProfile} message={message} />

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

showProfile이 스타일에만 쓰이는 prop인 경우를 표기해주신 점 넘 세심하네요..💫

Copy link

@yyj0917 yyj0917 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추후 코드리뷰를 더 이어나가고 싶을 정도로 많은 로직과 구현에도 완성도가 뛰어난 것 같습니다. 제가 보지 못 했던 설정들이나 컴포넌트 분리 방식 등에서 배울 점이 많아 얻어가는 게 많은 코드리뷰였습니다. 코드 구현하시느라 고생많으셨습니다! 시간되는대로 계속 들어와서 코드보고 리뷰남기겠습니다!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eslint적용 정말 적절히 잘해주신 것 같아요! 많이 배워갈게요

const location = useLocation();
const pathname = location.pathname;

useEffect(() => {}, [location]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 어떤 용도인지 궁금합니다!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

설정부분에서 처음 보는 게 많은 것 같아요! 기회되었을 때 어떤 설정을 하시고, 어떤 효과가 있는지 한번 찾아봐야겠습니다!!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

피그마 디자인 시스템을 themeprovider로 하신 부분이 인상깊습니다. 깔끔하고 직관적으로 보일 거 같아요

Comment on lines +11 to +18
{FRIEND_LIST.map((friend) => {
const { userId, user, statusMessage, profile } = friend;

return (
<Link to={`/message/${userId}`}>
<FriendProfile key={userId} user={user} statusMessage={statusMessage} profile={profile} />
</Link>
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

map을 정말 잘 활용하시는 것 같아요! 이런 식으로 해당하는 friend로 계속 생성하는 것은 생각못해봤습니다

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

새 채팅방에는 메세지가 보내지지 않는 것 같아요!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants