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주차] 송은수 미션 제출합니다. #15

Open
wants to merge 71 commits into
base: main
Choose a base branch
from

Conversation

songess
Copy link

@songess songess commented May 3, 2024

배포링크

3주차 때 저장했던 localStorage값이 남아있으면 개발자도구에서 초기화시키고 실행해주세요!
카카오톡

구현기능 및 후기

  • 3주차 피드백 반영
  • FriendListPage, MyProfilePage, NotFoundPage 구현
  • react-router-dom을 사용한 라우팅기능 구현
  • 채팅전송 시
    • lastMessage 갱신
    • lastMessageDate 갱신, 오늘이면 시간, 오늘 이전이면 날짜가 뜨도록 구현
    • unReadCount말풍선 있었다면 제거
src
 ┣ assets
 ┃ ┣ data
 ┃ ┣ img
 ┃ ┣ svg
 ┣ components
 ┣ hooks
 ┣ layout
 ┣ pages
 ┃ ┣ ChattingListPage
 ┃ ┃ ┣ components
 ┃ ┃ ┗ ChattingListPage.tsx
 ┃ ┣ ChattingRoomPage
 ┃ ┃ ┣ components
 ┃ ┃ ┗ ChattingRoomPage.tsx
 ┃ ┣ FriendListPage
 ┃ ┃ ┣ components
 ┃ ┃ ┗ FriendListPage.tsx
 ┃ ┣ MyProfilePage
 ┃ ┃ ┣ components
 ┃ ┃ ┗ MyProfilePage.tsx
 ┃ ┗ NotFoundPage
 ┃ ┃ ┗ NotFoundPage.tsx
 ┣ recoil
 ┃ ┣ chatAtom.ts
 ┃ ┗ userAtom.ts
 ┣ styles
 ┃ ┣ GlobalStyle.ts
 ┃ ┗ theme.ts
 ┣ types
 ┃ ┗ common.ts
 ┣ util
 ┃ ┣ calculateDate.ts
 ┃ ┗ routes.tsx
 ┣ App.tsx
 ┣ index.tsx
 ┣ svg.d.ts
 ┗ types.d.ts

공통으로 사용되는 컴포넌트는 src/components폴더에서 사용했고, 각 페이지에서 사용하는 컴포넌트는 src/pages/*/components폴더에서 개별적으로만 사용했다.

chattingRoomData.json하나만으로 모든 상태를 제어하려고 시간을 많이 쓴 거 같다.

tsconfig.paths.json

{
	"compilerOptions": {
		"baseUrl": "./src",
		"paths": {
		"@components/*": ["./components/*"],
		"@assets/*": ["./assets/*"],
		"@styles/*": ["./styles/*"],
		"@types/*": ["./types/*"],
		"@hooks/*": ["./hooks/*"],
		"@pages/*": ["./pages/*"],
		"@recoil/*": ["./recoil/*"]
		}
	}
}

tsconfig.paths.json파일을 다음과 작성했더니 @types가 호출이 되지 않았다. 다른 것들엔 문제가 없는데 이것만 안돼서 도대체 뭐가 문제지 싶었다.

이유는 @types/ 접두사는 DefinitelyTyped에서 제공하는 타입 정의 패키지에 사용됨으로, 표준 JavaScript 라이브러리에서 사용하는 예약어이기 때문에 생기는 오류인 듯 했다. @type으로 변경하니 정상적으로 별칭을 사용할 수 있었다.

라우팅을 사용하면서 기본적인 뷰의 틀로 모바일화면 사이즈를 원했기 때문에 <Layout>을 라우팅하고 중첩라우팅을 사용해 내부에 페이지들을 구현했다.

채팅방에서 이름을 클릭하여 사용자가 전환된 상태에서 나가면 user가 달라져 꼬일 수 있기에 unmountuser를 기본값으로 바꿔줬다.

Key Questions

Routing이란?

라우팅이란 하나의 URL에 하나의 페이지를 맵핑해주는 것을 말한다. 네트워크에서도 라우팅이라는 단어가 있는데 이는 다른 개념이다. 네트워킹에서는 출발지에서 목적지까지 라우터간 이동의 최적경로를 찾는 방법을 라우팅이라 하고, 리액트에서 라우팅은 URL에 따른 데이터를 받아와 화면에 렌더링 해주는 것이다.

라우팅을 사용하면 페이지가 새로고침되지 않고 흐름을 유지하면서 필요한 부분이 렌더링되어 화면이 전환된다.

흐름 유지

흐름을 유지한다는 것은 상태가 유지된다는 것이다. 기존 js에서 사용하는 를 사용하면 화면은 새로고침된다. 그러면 이전 페이지에서 가지고 있던 상태를 잊어버려 초기화된다.

UX 향상

새로운 페이지를 여는게 아니고 필요한 UI만 업데이트 하기 때문에 더 나은 UX를 제공한다.

리액트에서는 react-router-dom을 사용해 라우팅을 구현한다.

기본적으로 다음 구조를 사용해 라우팅을 구현한다.

const App = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/link1" element={<Component1 />} />
        <Route path="/link2" element={<Component2 />} />
        ...
      </Routes>
    </BrowserRouter>
  );
};

하지만 React Router v6.4에서는 RouterProviderCreateBrowerRouter를 사용하여 다른 방식으로도 라우팅이 가능하다.

//routes.js
export const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      {
        path: 'path1',
        element: <Component1 />,
      },
    ],
    errorElement: <NotFoundPage />,
  },
  ...
]);

//App.js
import { RouterProvider } from 'react-router-dom';
import { router } from '@util/routes';

function App() {
  return (
    <Fragment>
      <RouterProvider router={router} />
    </Fragment>
  );
}

export default App;

페이지가 많아지면 가독성이 좋아보이고 Error페이지도 한눈에 들어오는 방식이라 이 방법을 사용했다.

Link

라우터 내에서 페이지 간 이동을 위해 대신 사용된다.

<Link to="/"> ... </Link>

useNavigate

태그를 감싸는 용도로 사용되는 Link와 달리 특정상황에 호출해 사용할 수 있다.

const navigate = useNavigate();

const onClick = () => {
  navigate('/');
};

중첩라우팅

다음과 같이 outlet을 활용해 사용할 수 있다.

//App.js
<Route path="/first" element={<First />}>
  <Route path="second" element={<Second />}></Route>
</Route>;

//Second.js
function About() {
  return (
    <div>
      ...
      <Outlet />
    </div>
  );
}

중첩된 라우팅의 path에는 /를 사용하지 않는다. /first에서 /first/second페이로 이동하면 컴포넌트가 자리에 렌더링 될 것이다. 하지만 나는 비슷한 상황에서 중첩라우팅을 사용하지 않고 분기처리를 사용해 display:none을 부여하거나 부여하지 않는 방식으로 해결했었다. 그래서 중첩라우팅을 어떻게 활용할 수 있을까 찾아보았다.

페이지는 동일한데 사용자가 고객인지 관리자인지에 따라 다른 사이드바를 표기해야 할 수 있다. 사이드바를 열기 위해 새로운 변수를 선언할 수도 있지만 대신 중첩라우팅을 활요해 고객이면 고객맞춤 사이드바가, 관리자라면 관리자맞춤 사이드바가 나오게 할 수 있다. 두 방법 다 가능하기 때문에 개발하면서 상황에 맞춰 활용할 수 있을 것 같다.

파라미터

  • url 파라미터
    주소경로 내부에 특정한 매개변수를 집어넣어 사용한다. useParams()를 사용해 값을 파라미터를 꺼내온다.
  • 쿼리스트링(쿼리파라미터)
    URL 뒤에 물음표와 함께 붙는 Key-Value 쌍. useLocation()이나 useSearchParams()를 사용해 값을 꺼내온다.

아이디나 이름처럼 특정데이터만을 필요로한다면 url 파라미터를, 여러 개의 변수가 필요하다면 쿼리스트링을 사용하면 되겠다.

SPA란?

Single Page Application의 약자로 직역하면 한 페이지에서 수행되는 어플리케이션이다.

전통적인 웹페이지는 다른 페이지로 넘어갈 때마다 새로운 html을 받아오는데, 리액트같은 라이브러리를 사용하여 뷰 렌더링은 브라우저에게 맡기고 주소변경이 이루어지면 필요한 부분을 자바스크립트를 사용하여 업데이트 해주는 것이다. 모든 작업이 한 페이지에서 이루어지기 때문에 URL(주소)를 통해 다른 페이지로 넘어가는 것은 사실 같은페이지에서 컴포넌트가 리렌더링 되는 것이다. 리액트 라우터는 SPA를 수행하기 위한 툴이다.

공부를 하다보니 궁금한 점이 생겼다. SPA를 사용하면 모든 파일들이 번들러를 통해 번들링 되어 하나의 파일로 만들어지고, 하나의 HTML에서 JS를 사용해 DOM을 수정하여 리렌더링 하는 방식으로 작동한다고 이해했다. 그렇게 되면 합쳐진 파일을 URL에 따라 관리하는 건 브라우저에서 할 일이고, 이게 CSR이다. 그렇다면 Next.js는 어떻게 SPA에서 SSR를 구현한거지? SPA는 CSR인거고, MPA가 SSR인거 아닌가?

CSR과 SSR는 렌더링 방식이고, SPA와 MPA는 웹페이지의 구성방식일 뿐 정확히 일치한다고 할수는 없다.

Next.js는 SSR과 hydration을 사용한다.

SSR방식을 사용하면 pre-rendering이 된다. pre-rendering이란 서버사이드에서 DOM 요소들을 build하여 HTML 문서를 렌더링하는 것이다. 이렇게 pre-rendering된 HTML이 먼저 뷰에 보이기 때문에 빈화면이 보이지 않고 HTML문서들을 볼 수 있다. 이후 hydration과정을 통해 미리 렌더링된 HTML에 JS가 결합된다.

SSR 방식은 크게 SSR방식과 SSG방식으로 구분된다. 상황에 따라 필요한 아키텍처를 사용한다.

  • Server Side Rendering 방식
    사용자가 페이지를 요청할 때마다 새로운 HTML문서가 생성된다. 즉 클라이언트의 요청에 따라 pre-rendering이 진행된다.
  • Static Site Generation 방식
    빌드 시 HTML이 생성되고 매 요청마다 HTML을 재사용한다. pre-renderingbuild될때 진행되고 이후 계속 재활용된다. 데이터가 변경되면 다시빌드해야되기 때문에 ISR을 사용해 일정시간마다 특정페이지만 다시 빌드해 페이지를 업데이트 할 수 있다.

Next.js는 자체 웹서버를 가지고 있기 때문에 서버에서 수행되는 로직들은 여기서(자체 웹서버에서) 수행되어 값이 다시 컴포넌트로 반환된다. 이렇게 하여 Next.js는 SPA에서 SSR방식을 수행한다.

상태관리란?

컴포넌트에는 현재 상태가 있고, useState을 통해 상태가 생성되고 관리된다. 하지만 useState를 사용한 상태관리는 본인 컴포넌트만 관리하여 다른 컴포넌트에서 무슨일이 일어나는지 알지 못한다. 여러 컴포넌트에서 동일한 상태를 공유할 때 이 상태들을 어떻게 해야할 지 관리해야한다.

가장 일반적인 방식으로 props drilling을 사용할 수 있다. 상태를 props로 전달해 다른 컴포넌트에서도 사용할 수 있게 한다. 하지만 drilling이 깊어 질수록 로직이 복잡해지고, 어디서부터 온 데이터인지 알 수 없기 때문에 전역상태관리를 사용한다.

전역상태관리는 말 그대로 상태를 모든 컴포넌트에서 접근 가능하게 만들어준다. 리액트는 기본적으로 context API를 제공한다.

Context API

createContext()로 초기값을 선언하고 context.provider로 컴포넌트를 감싼다. useContext()를 통해 구독한 컴포넌트에서는 특정 값을 꺼내와 사용할 수 있다.

다만 값이 변경되면 무슨 값이든 구독중인 모든 컴포넌트가 리렌더링이 발생 불필요한 리렌더링이 발생한다. 이를 보안하기 위해 여러가지 대체 전역상태관리 라이브러리가 존재한다. 대표적으로 redux, zustand, recoil, jotai가 있다. 이들은 모두 구독중인 값이 바뀔때에만 변경되고, 구독중이지 않은 값이 변경될 때는 변경되지 않아 불필요한 리렌더링이 발생하지 않는다.

redux

대중화된 라이브러리이다. FLUX패턴을 사용한다. FLUX패턴은 ACTION -> DISPATCHER -> MODEL(store) -> VIEW -> ACTION -> DISPATCHER -> MODEL(store) -> VIEW ... 방식으로 단방향 순환구조를 보여줘서 MODEL과 VIEW간 단방향 통신이 가능하게 도와준다. redux는 하나의 store공간을 사용하고 안에 상태(상태라는 것은 읽기 전용)와 리듀서를 정의한다. ACTION이 발생하면 dispatcher가 실행되어 액션과 이전 상태를 참조해 상태를 갱신한다.

redux는 많은 보일러플레이트를 사용하고, 비동기처리를 하기 쉽지 않다. 이를 위해 redux toolkit이 존재하고 미들웨어가 존재하지만 러닝커브가 있고 여전히 복잡하다.

zuztand

redux의 복잡한 패턴을 해결하기 위해 만들어진 라이브러리 이다. 똑같이 FLUX패턴을 사용하는데 보일러플레이트가 없고 필요하다면 비동기 처리가 가능하다.

recoil

각각의 상태를 쪼개어 atom이란 단위로 저장한다. 이후 useState처럼 useRecoilState를 사용해 atom을 사용할 수 있다. selector를 사용해 비동기처리를 할 수도 있다.

jotai

recoil과 유사하게 atom단위를 사용한다. 이후 useAtom을 통해 atom을 사용할 수 있다.

recoil과의 차이점은 key값 없이도 그 자체만으로 구분이 된다는 것과 selector()를 사용하지 않고도 다른 atom을 만들 수 있다는 것, 즉 jotai가 더 미니멀리즘하게 구현할 수 있다는 것이다. 하지만 recoil은 SSR과 코드스플리팅을 내장지원하고, jotai에 비해 큰 지원과 커뮤니티를 가지고 있다.

React Query

React Query는 서버와의 통신간 데이터 관리를 도와주는 상태관리 라이브러리이다. 소개했던 상태관리 라이브러리들에서도 비동기처리를 할 수 있지만, 캐싱, 동기화등은 직접 구현해줘야 한다. React Query는 데이터 fetching, 캐싱, 동기화, 서버쪽 데이터 업데이트 등을 쉽게 만들어 준다.

비동기처리를 할 때 다른 라이브러리에 비해 보일러플레이트가 적고, 빌트인 함수가 많아서 현재 로딩상태, 에러가 났는지 여부등을 쉽게 확인할 수 있다. 또한 캐시처리 기능도 지원해 주기적으로 최신데이터로 갱신해준다.

songess added 27 commits May 1, 2024 16:54
feat: ChattingListPage 생성
feat: FooterCard 구현
feat: readme 작성
Copy link

@CSE-pebble CSE-pebble left a comment

Choose a reason for hiding this comment

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

은수님!
저는 이번 과제 때 redux를 사용했고 recoil 사용 경험은 없는데 은수님이 recoil을 사용해주셔서 코드 읽으면서 간접적으로 배울 수 있었어요 ㅎㅎ
그리고 공감 기능, 읽음 처리 기능 등 다양한 기능을 구현해주셔서 보는 맛이 났습니다! 넘 멋져요👍

이번주 과제도 수고하셨습니다~!

Comment on lines +15 to +20
export default function FooterCard({
TitleComponent,
title,
currentURL,
onClick,
}: FooterCardProps) {

Choose a reason for hiding this comment

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

props가 많을 경우에 이렇게 받아오면 가독성이 좀 떨어지더라구요! 저도 계속 이렇게 작성해오다가 이번에 민영님께 받은 코드 리뷰인데 참고해보시면 좋을 것 같아요 ㅎㅎ

Choose a reason for hiding this comment

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

우왕 더블클릭하면 공감되는 기능까지!!! 넘 멋져요👍

Comment on lines +32 to +42
<SNSBox
img="/instagram.png"
title="Instagram"
onClick={handleClickSNSBox}
/>
<SNSBox img="/github.png" title="Github" onClick={handleClickSNSBox} />
<SNSBox
img="/behance.png"
title="Behance"
onClick={handleClickSNSBox}
/>

Choose a reason for hiding this comment

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

반복되는 부분은 데이터를 배열로 뺀 후에 map 함수를 이용해서 중복을 없애는건 어떨까요?
제 코드 남기고 갑니다!! 참고만 해주세요!

Choose a reason for hiding this comment

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

이렇게 interface를 하나의 파일에 넣어서 필요할 때마다 import 할 수 있도록 해주는거 코드 가독성에 너무너무 좋을 것 같아요!! 배워갑니다!

Copy link

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
const handleNameToggle = () => {
if (userName.user === '송은수') {
setUserName(prev => ({ ...prev, user: opponentName }));
} else {
setUserName(prev => ({ ...prev, user: '송은수' }));
}
};

Choose a reason for hiding this comment

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

chattingRoomData.json을 확인해보니 자기자신에 대한 프로필 이미지 데이터 없이 이 img에는 상대방의 프로필로 이미지 데이터로만 고정되어있다보니 채팅 토글 시 프로필 이미지는 토글이 되지 않는 문제가 발생하고 있어요!
만약 데이터를 수정하지 않는다면 if문에 img를 세팅해주는 코드를 추가적으로 넣어줘야 할 것 같습니다!!

Comment on lines +84 to +88
const theme = {
colors,
textStyles,
};

Choose a reason for hiding this comment

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

저는 color랑 typography(text styles)를 각각 다른 파일로 나눴는데, 이렇게 한 파일에 넣어주고 theme이라는 하나의 객체에 넣어서 내보내는 것도 정말 좋은 방법이네요!

Copy link

@noeyeyh noeyeyh left a comment

Choose a reason for hiding this comment

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

안녕하세요 은수님!
깔끔하게 코드 작성하셔서 이해하는 데 도움이 된 것 같아요.! 로컬 스토리지 저장부터 테마 파일로 스타일 관리까지 저와 다르게 구현한 부분도 보고 배워갑니다. 👏

return (
<LayoutStyle>
<Outlet />
</LayoutStyle>
Copy link

Choose a reason for hiding this comment

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

Outlet을 이용해서 중첩 라우팅하신 거 보고 배워가요!

);
}
return messageComponent;
};
Copy link

Choose a reason for hiding this comment

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

저는 날짜 포맷 함수를 따로 작성하였는데 은수님처럼 이런 식으로 간단하게 작성한다면 별도의 함수 없이도 좋을 것 같아요!

onSet((newValue: ChattingRoom[], _: any, isReset: boolean) => {
isReset
? localStorage.removeItem('chatData')
: localStorage.setItem('chatData', JSON.stringify(newValue));
Copy link

Choose a reason for hiding this comment

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

저는 로컬스토리지 저장 기능을 구현하지 못했는데 저장까지 해주셨네요. 보고 배워갑니다. 👍

textStyles,
};

export default theme;
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.

맞아요.. 따로 파일에 관리해서 가독성에 좋은 것 같아요!

let hour = date.getHours().toString().padStart(2, '0');
let minute = date.getMinutes().toString().padStart(2, '0');
return `${hour}:${minute}`;
};
Copy link

Choose a reason for hiding this comment

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

위에서 본 날짜 출력과 별도로 다른 부분은 날짜와 시간을 포맷 함수로 따로 작성해주셨군요..!!
간단한 함수여도 따로 작성해주셔서 가독성이 좋아요.

element: <MyProfilePage />,
},
],
errorElement: <NotFoundPage />,
Copy link

Choose a reason for hiding this comment

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

NotFoundPage도 섬세하게 지정해주셨네요 👍

Copy link
Member

@leejin-rho leejin-rho left a comment

Choose a reason for hiding this comment

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

스크린샷 2024-05-05 오후 6 54 25

이 부분은 word-break 해서 수정해주시는게 좋을 것 같습니다~!

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