diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 00000000..a79dbdc8
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,39 @@
+{
+ "extends": [
+ "prettier",
+ "airbnb",
+ "airbnb/hooks",
+ "plugin:react/jsx-runtime",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:prettier/recommended",
+ ],
+ "plugins": ["@typescript-eslint"],
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "project": "tsconfig.json",
+ },
+ "rules": {
+ "import/extensions": [
+ "error",
+ "ignorePackages",
+ {
+ "js": "never",
+ "jsx": "never",
+ "ts": "never",
+ "tsx": "never",
+ },
+ ],
+ "react/jsx-filename-extension": [
+ 1,
+ { "extensions": [".js", ".jsx", ".ts", ".tsx"] },
+ ],
+ },
+ "settings": {
+ "import/resolver": {
+ "node": {
+ "extensions": [".js", ".jsx", ".ts", ".tsx"], // 사용할 확장자 추가
+ },
+ "typescript": {}, // TypeScript 모듈 경로 설정과 연동
+ },
+ },
+}
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..94f480de
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto eol=lf
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 4d29575d..a3479979 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,23 +1,26 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
-
-# testing
-/coverage
-
-# production
-/build
-
-# misc
-.DS_Store
-.env.local
-.env.development.local
-.env.test.local
-.env.production.local
-
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# setting
+.vscode
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 00000000..65bcbd5b
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,12 @@
+{
+ "printWidth": 80,
+ "tabWidth": 2,
+ "useTabs": false,
+ "semi": true,
+ "singleQuote": true,
+ "trailingComma": "all",
+ "bracketSpacing": true,
+ "arrowParens": "always",
+ "htmlWhitespaceSensitivity": "css",
+ "cssWhitespaceSensitivity": "css"
+}
diff --git a/README copy.md b/README copy.md
deleted file mode 100644
index 82b92ada..00000000
--- a/README copy.md
+++ /dev/null
@@ -1,66 +0,0 @@
-# 서론
-
-안녕하세요 🙌🏻 19기 프론트 운영진 배성준입니다. 이번 미션에서는 드디어 투두리스트에서 벗어나 새로운 프로젝트인 **messenger** 만들기를 진행합니다.
-
-이번주는 특별히 **디자이너와의 협업**으로 진행되는 미션입니다. 디자이너분께서 열심히 리디자인 한 메신저 프로젝트를 여러분들께서 구현해주시면 됩니다.
-
-동시에, 이번주부터는 새로 **TypeScript**를 적용해보려고 합니다.
-
-프로젝트의 규모가 커지게 될 수록, 컴포넌트가 가지는 props의 종류 또한 다양해지게 됩니다. 무지성 코딩을 하다보면 가끔 이 props의 구성과 이름, 어떤 타입이 들어가야 하는지 헷갈리기 마련이죠. 보통 그럴 때 다시 컴포넌트 정의 부분으로 돌아가 props의 구성을 보고 오곤 합니다.
-
-하지만 이럴 때, typescript를 적용한다면 컴포넌트의 구성과 그 이름, 심지어 타입까지 한번에 자동완성으로 편리하게 관리할 수 있고, 생산성이 증대되겠죠.
-
-또한, **React Hooks**에 조금 더 익숙해지는 것을 목표로 합니다. 여러 Hook들이 있지만 그 중에서도 `useState`, `useEffect`, `useRef`를 중점적으로 사용해 보는 미션인데요, React를 사용하면서 굉장히 자주 쓰이는 Hook들이기 때문에 이 부분을 집중적으로 공부해 보아요!
-
-그럼 이번 미션도 파이팅입니다!!🎉
-
-# 미션
-
-## Key Questions
-
-- JavaScript를 사용할때에 비해 TypeScript를 사용할 때의 장점은 무엇인가요?
-- 디자이너로부터 전달받은 피그마 링크 혹은, 피그마 캡처본
-- 컴포넌트를 분리한 기준은 무엇인가요?
-- 디자인 시스템을 적용하면서 느낀 점은 무엇인가요?
-- 디자이너와 소통하며 느낀점은 무엇인가요?
-
-## 미션 목표
-
-- TypeScript를 사용해봅시다.
-- useState로 컴포넌트의 상태를 관리합니다.
-- useEffect와 useRef의 사용법을 이해합니다.
-- styled-components를 통한 CSS-in-JS 및 CSS Preprocessor의 사용법에 익숙해집니다.
-
-## 기한
-
-2024년 3월 29일 금요일
-
-## 필수 구현 기능
-
-- 피그마를 보고 [결과화면](https://3th-fb-messenger.netlify.app)과 같이 구현합니다.
-- 디자인 시스템을 구축합니다.
-- 채팅방 상단의 프로필을 클릭하면 사용자를 변경할 수 있습니다.
-- 메세지를 보내면 채팅방 하단으로 스크롤을 이동시킵니다.
-- 메세지에 유저 정보(프로필 사진, 이름)를 표시합니다.
-- user와 message 데이터를 json 파일에 저장합니다.
-- UI는 **반응형을 제외**하고 피그마파일을 따라서 진행합니다.
-
-### 추가 구현 기능
-
-- 더블 클릭 하면 감정표현을 추가합니다.
-- 그 외 추가하고 싶은 기능이 있다면 마음껏 추가해 주세요!
-
-참고로 이번 과제는 다음주까지 이어지는 과제이므로 **확장성**을 충분히 고려해 주세요. 참고로 **4주차 과제에서는 유저 및 기능 추가와 Routing을** 진행합니다. 이를 위해 [recoil](https://recoiljs.org/ko/)이나 [redux](https://ko.redux.js.org/introduction/getting-started/)를 이용한 상태관리를 미리 해보시는 것을 추천합니다!! 모두 공식문서 많이 읽어보시고 자신만의 상태관리 조합도 찾아보면 재밌을 거에요 XD
-
-## 링크 및 참고자료
-
-- [React docs - Hook](https://ko.reactjs.org/docs/hooks-intro.html)
-- [React의 Hooks 완벽 정복하기](https://velog.io/@velopert/react-hooks#1-usestate)
-- [useEffect 완벽 가이드](https://overreacted.io/ko/a-complete-guide-to-useeffect/)
-- [코딩 컨벤션](https://ui.toast.com/fe-guide/ko_CODING-CONVENTION)
-- [타입스크립트 핸드북](https://joshua1988.github.io/ts/intro.html)
-- [리액트 프로젝트에서 타입스크립트 사용하기 (시리즈)](https://velog.io/@velopert/series/react-with-typescript)
-- [디자인 시스템 구축기](https://yozm.wishket.com/magazine/detail/1830/)
-- [[영상] : 컴포넌트에 대한 이해](https://www.youtube.com/watch?v=21eiJc90ggo)
-- [Styled Component로 디자인 시스템 구축하기](https://zaat.dev/blog/building-a-design-system-in-react-with-styled-components/)
-- [ts 절대경로 설정하기](https://tesseractjh.tistory.com/232)
diff --git a/README.md b/README.md
index 5109415c..44b85ef4 100644
--- a/README.md
+++ b/README.md
@@ -1,61 +1,164 @@
-# 서론
-
-안녕하세요 🙌🏻 20기 프론트 운영진 김동혁입니다. 이번 미션에서는 드디어 투두리스트에서 벗어나 새로운 프로젝트인 **messenger** 만들기를 진행합니다.
-
-이번주는 특별하게 **디자이너와의 협업**으로 진행되는 미션입니다. 디자이너분께서 열심히 리디자인 한 메신저 프로젝트를 여러분들께서 구현해주시면 됩니다.
-
-동시에, 이번주부터는 새로 **TypeScript** 와 **tailwindcss** 혹은 **StyledComponent** 를 적용해보려고 합니다. 스타일 라이브러리는 취향차이라고 생각해서, 둘 중 본인의 취향에 맞춰서 진행해주면 됩니다!!
-
-프로젝트의 규모가 커지게 될 수록, 컴포넌트가 가지는 props의 종류 또한 다양해지게 됩니다. 무지성 코딩을 하다보면 가끔 이 props의 구성과 이름, 어떤 타입이 들어가야 하는지 헷갈리기 마련이죠. 보통 그럴 때 다시 컴포넌트 정의 부분으로 돌아가 props의 구성을 보고 오곤 합니다.
-
-하지만 이럴 때, typescript를 적용한다면 컴포넌트의 구성과 그 이름, 심지어 타입까지 한번에 자동완성으로 편리하게 관리할 수 있고, 생산성이 증대되겠죠.
-
-또한, **React Hooks**에 조금 더 익숙해지는 것을 목표로 합니다. 여러 Hook들이 있지만 그 중에서도 `useState`, `useEffect`, `useRef`를 중점적으로 사용해 보는 미션인데요, React를 사용하면서 굉장히 자주 쓰이는 Hook들이기 때문에 이 부분을 집중적으로 공부해 보아요!
-
-그럼 이번 미션도 파이팅입니다!!🎉
-
-# 미션
-
-## Key Questions
-- 디자이너로부터 전달받은 피그마 링크, 피그마 캡처본, 디자이너와의 소통 tmi 등
-- JSX, JS, TSX, TS 각각의 확장자 개념 사용이유와 차이점.
-- TypeScript를 사용하는 이유.
-- SSR과 CSR 특성 및 차이점.
-
-## 미션 목표
-- TypeScript를 사용해봅시다.
-- useState로 컴포넌트의 상태를 관리합니다.
-- useEffect와 useRef의 사용법을 이해합니다.
-- tailwindcss의 사용법에 익숙해집니다.
-
-## 기한
-2024년 9월 27일 금요일
-
-## 필수 구현 기능
-- 피그마를 보고 [결과화면](https://react-messenger-shu.vercel.app/chat/1)과 같이 구현합니다.
-- 디자인 시스템을 구축합니다.
-- tailwindcss 혹은 styledComponent 스타일 라이브러리를 사용합니다.
-- 채팅방 상단의 프로필을 클릭하면 사용자를 변경할 수 있습니다.
-- 메세지를 보내면 채팅방 하단으로 스크롤을 이동시킵니다.
-- 메세지에 유저 정보(프로필 사진, 이름)를 표시합니다.
-- user와 message 데이터를 json 파일에 저장합니다.
-- UI는 **반응형을 제외**하고 피그마파일을 따라서 진행합니다.
-
-### 추가 구현 기능
-- 더블 클릭 하면 감정표현을 추가합니다.
-- 그 외 추가하고 싶은 기능이 있다면 마음껏 추가해 주세요!
-
-참고로 이번 과제는 다음 과제까지 이어지는 과제이므로 **확장성**을 충분히 고려해 주세요. 참고로 **4주차 과제에서는 유저 및 기능 추가와 Routing을** 진행합니다. 이를 위해 [recoil](https://recoiljs.org/ko/)이나 [redux](https://ko.redux.js.org/introduction/getting-started/)를 이용한 상태관리를 미리 해보시는 것을 추천합니다!! 모두 공식문서 많이 읽어보시고 자신만의 상태관리 조합도 찾아보면 재밌을 거에요 XD
-
-## 링크 및 참고자료
-
-- [React docs - Hook](https://ko.reactjs.org/docs/hooks-intro.html)
-- [React의 Hooks 완벽 정복하기](https://velog.io/@velopert/react-hooks#1-usestate)
-- [useEffect 완벽 가이드](https://overreacted.io/ko/a-complete-guide-to-useeffect/)
-- [코딩 컨벤션](https://ui.toast.com/fe-guide/ko_CODING-CONVENTION)
-- [타입스크립트 핸드북](https://joshua1988.github.io/ts/intro.html)
-- [리액트 프로젝트에서 타입스크립트 사용하기 (시리즈)](https://velog.io/@velopert/series/react-with-typescript)
-- [디자인 시스템 구축기](https://yozm.wishket.com/magazine/detail/1830/)
-- [[영상] : 컴포넌트에 대한 이해](https://www.youtube.com/watch?v=21eiJc90ggo)
-- [Styled Component로 디자인 시스템 구축하기](https://zaat.dev/blog/building-a-design-system-in-react-with-styled-components/)
-- [ts 절대경로 설정하기](https://tesseractjh.tistory.com/232)
+# 3 & 4주차 미션: React-Messenger
+
+# 🌊 결과물
+
+배포 링크 :
+[https://react-messenger-20th.vercel.app/chat](https://react-messenger-20th.vercel.app/chat)
+
+## 기능 구현
+
+### /chat 페이지
+
+- 대화를 클릭해 해당 대화 페이지로 이동할 수 있다.
+
+### /chat/{id} 페이지
+
+- 하단 입력창에서 메시지를 입력하고, 엔터 혹은 오른쪽 파란 전송 버튼을 통해 메시지 전송이 가능하다.
+ - Shift + enter를 통해 빈 줄을 추가할 수 있다.
+- 상단 navbar 프로필 사진 및 이름을 클릭하면 상대방<->나 유저 전환이 가능하다.
+- 더블클릭을 통해 상대방의 메시지에 리액션을 보낼 수 있다.
+
+# 🌊 Key Questions - 4주차
+
+## React Router의 동적 라우팅(Dynamic Routing)이란 무엇이며, 언제 사용하나요?
+
+- 동적 라우팅은 웹에서 URL 경로에 따라 다른 컴포넌트를 동적으로 렌더링하는 방식이다.
+- React에서는 React Router 라이브러릴르 추가해 동적 라우팅을 간편하게 사용할 수 있다.
+
+예시
+
+- 사용자의 개별 페이지
+ - `/profile/:id`처럼 사용해 id에 따라 각각 다른 사용자의 정보를 보여줄 수 있다.
+- 검색 결과 페이지
+ - `/search/:query` 경로를 이용하면 `query`에 따라 검색 결과를 보여줄 수 있다.
+
+## 네트워크 속도가 느린 환경에서 사용자 경험을 개선하기 위해 사용할 수 있는 UI/UX 디자인 전략과 기술적 최적화 방법은 무엇인가요?
+
+### 이미지 최적화
+
+- 이미지를 압축하거나, 크기를 조절해 이미지의 용량을 줄인다.
+- 웹에서는 webp 확장자를 주로 사용한다.
+
+### code splitting
+
+- 코드를 여러 개의 번들로 나누어 필요한 부분만 로드한다.
+- 초기 로딩 시에도 모든 코드를 한 번에 불러오지 않고, 각 페이지별로 필요한 코드만 필요시에 로드한다.
+- 불필요한 코드나 라이브러리는 로드하지 않는다.
+
+## React에서 useState와 useReducer를 활용한 지역 상태 관리와 Context API 및 전역 상태 관리 라이브러리의 차이점을 설명하세요.
+
+### useState
+
+- 가장 기본적으로 사용되는 상태 관리 hook이다.
+- 코드가 간결하고 직관적이다.
+- 상태 변경 로직이 복잡한 경우, 코드 가독성을 해칠 수 있다.
+
+### useReducer
+
+- 상태와 상태 변경 로직을 분리해서 관리한다.
+- 복잡한 상태 로직을 reducer 함수로 깔끔하게 관리할 수 있다.
+
+
+
+- useState나 useReducer는 하나의 컴포넌트 또는 컴포넌트의 트리에서 지역적으로 상태를 관리할 사용한다. 여러 컴포넌트에 걸쳐 관리되는 상태의 경우에는 props drilling 문제가 발생할 수 있으므로 다른 방법을 사용할 수 있다.
+
+### context API 및. 전역 상태 관리 라이브러리
+
+- 상태를 상위 컴포넌트에서 전달하지 않고도 하위 컴포넌트에서 접근할 수 있다.
+- 예시) 테마, 시간대, 다국어 설정 등 여러 컴포넌트에서 공통적으로 사용되는 상태
+
+# 🌊 Key Questions - 3주차
+
+## 디자이너로부터 전달받은 피그마 링크, 피그마 캡처본, 디자이너와의 소통 tmi 등
+
+- [피그마 링크](https://www.figma.com/design/UOmRWwpa4lobfHI11jIIMy/%EB%A9%94%EC%8B%A0%EC%A0%80-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A6%AC%EB%94%94%EC%9E%90%EC%9D%B8_%EC%A0%9C%EC%B6%9C?node-id=0-1&t=l1Ggohdqn2UCpvUS-1)
+- 캡처본
+- 소통 TMI
+ - 디자이너님과 소통하면서 플젝을 하는게 꽤 오랜만이네용ㅎㅎㅎ,, 주어진 디자인을 잘 구현하는 것도 FE 개발자에게 중요한 역량이라고 생각하기 때문에 최대한 노력했는데 잘 됐는지 모르겠어요...!
+ - 중간에 제가 "채팅 메시지 최대 길이 정해주면 좋을 것 같아요!"라고 말씀을 드렸는데, 알고 보니 피그마에 지정을 해 놓으셨더라구요! 정신 바짝 차리겠습니당,,🫠🫠
+ - 사실 저는 css에서 단위를 px, pt보다는 rem을 선호하는 편이라 보통 root font size 16px에 0.25rem / 0.5rem / 0.75rem / 1rem 간격으로 사이즈를 조절했었는데요...! 이번 디자이너님은 5px, 6px, 7px, 8px,.... 25px 등 단위를 다양하게 사용하셔서 어떻게 구현할 지 고민이 됐었어요. 하필 이번에 tailwind를 써보자고 마음을 먹었는데 tailwind와 px 단위 디자인은 궁합이 잘 안 맞는 것 같더라고요...ㅎㅎ 만약 실제 플젝이었으면 디자인을 수정해주실 수 있는지 말씀드리거나 제가 기술 스택을 바꿨을텐데, 이번에는 간단한 과제이다보니 그냥 tailwind도 쓰면서 디자이너님의 디자인을 정말 충실히 구현해보자! 하는 기분으로 코딩했어요🥰🥰
+
+## JSX, JS, TSX, TS 각각의 확장자 개념 사용이유와 차이점.
+
+### JS
+
+- JS는 프로그래밍 언어로, 웹 페이지를 만드는 데에 많이 사용된다. 꼭 웹 페이지에 한정된 것은 아니고, 비 브라우저 환경에서도 작동한다.
+- 프로토타입 기만, 단일 스레드, 동적 언어로, 객체지향형, 명령형, 선언형 스타일을 지원한다.
+- 웹 페이지에서는 보통 동적인 웹을 구축하는 역할을 맡는다.
+
+### JSX
+
+- JSX는 JS의 구문을 확장한 것으로, JS 파일 안에 HTML과 유사한 마크업을 작성할 수 있게 한 것이다.
+- 기존에는 콘텐츠를 HTML, 디자인을 CSS, 로직을 JS 파일에 작성했다. 하지만 JSX를 이용하면 마크업과 렌더링 로직을 같은 파일에 작성할 수 있다!
+- JSX element는 syntactic sugar로, JSX 코드는 JS 코드로 변환될 수 있다.
+
+ - 따라서 아래와 같은 JSX 코드는
+
+ ```jsx
+ class Hello extends React.Component {
+ render() {
+ return
+ );
+}
diff --git a/src/components/MessageList/index.tsx b/src/components/MessageList/index.tsx
new file mode 100644
index 00000000..dbee5105
--- /dev/null
+++ b/src/components/MessageList/index.tsx
@@ -0,0 +1,106 @@
+import { useEffect, useRef } from 'react';
+
+import { type Message } from 'src/types/message';
+import { type User } from 'src/types/user';
+
+import ProfileCard from '@components/ProfileCard';
+import formatDate from 'src/utils/formatDate';
+import UserMessage from './UserMessage';
+import SystemMessage from './SystemMessage';
+
+export default function MessageList({
+ messageList,
+ userId,
+ otherUser,
+ reactToMessage,
+}: {
+ messageList: Message[];
+ userId: string;
+ otherUser: User;
+ reactToMessage: (messageId: string, reactor: string, emoji: string) => void;
+}) {
+ const messageEndRef = useRef(null);
+
+ useEffect(() => {
+ messageEndRef.current?.scrollIntoView();
+ }, [messageList.length]);
+
+ function isDifferentDate(a: string | Date, b: string | Date) {
+ return new Date(a).toDateString() !== new Date(b).toDateString();
+ }
+
+ function isStartOfDate(index: number) {
+ const prevMessage = messageList[index - 1];
+ const currentMessage = messageList[index];
+
+ return (
+ index === 0 || isDifferentDate(prevMessage.sentAt, currentMessage.sentAt)
+ );
+ }
+
+ /**
+ * Checks if the message at the given index is the first message
+ * in the message list or if the sender changes
+ *
+ * @param index - Index of the target message
+ * @returns True if it's the first message or the moment when sender changes, otherwise false
+ */
+ function isFirst(index: number) {
+ const prevMessage = messageList[index - 1];
+ const currentMessage = messageList[index];
+
+ return (
+ index === 0 || // first message in total
+ prevMessage.sender !== currentMessage.sender || // When the sender changes
+ isDifferentDate(prevMessage.sentAt, currentMessage.sentAt) || // When the date changes
+ !!prevMessage?.reactionList?.length
+ );
+ }
+
+ /**
+ * Checks if the message at the given index is the last message
+ * in the message list or if the sender changes
+ *
+ * @param index - Index of the target message
+ * @returns True if it's the last message or the moment when changes, otherwise false
+ */
+ function isLast(index: number) {
+ const currentMessage = messageList[index];
+ const nextMessage = messageList[index + 1];
+
+ return (
+ index === messageList.length - 1 || // last message in total
+ currentMessage.sender !== nextMessage.sender || // When the sender changes
+ isDifferentDate(currentMessage.sentAt, nextMessage.sentAt) || // When the date changes
+ !!currentMessage?.reactionList?.length
+ );
+ }
+
+ return (
+
+
+
+
+ {messageList.map((message, messageIndex) => (
+
+ {isStartOfDate(messageIndex) && (
+
+ )}
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/NavBar/index.tsx b/src/components/NavBar/index.tsx
new file mode 100644
index 00000000..f2de4629
--- /dev/null
+++ b/src/components/NavBar/index.tsx
@@ -0,0 +1,39 @@
+import BackIcon from '@assets/BackIcon';
+import PhoneIcon from '@assets/PhoneIcon';
+import VideoIcon from '@assets/VideoIcon';
+import { type User } from 'src/types/user';
+
+export default function NavBar({ otherUser }: { otherUser: User }) {
+ return (
+
+ );
+}
diff --git a/src/components/ProfileCard/index.tsx b/src/components/ProfileCard/index.tsx
new file mode 100644
index 00000000..c1e8397a
--- /dev/null
+++ b/src/components/ProfileCard/index.tsx
@@ -0,0 +1,41 @@
+import { type User } from 'src/types/user';
+
+export default function ProfileCard({ otherUser }: { otherUser: User }) {
+ const followedCount = 1000;
+ const postCount = 999;
+ const representativeFollowerId = '누구누구';
+ const followedByKnownCount = 3;
+
+ return (
+
+
+
+
+
{otherUser.name}
+
{otherUser.id}
+
+
+
+
+
{`팔로워 ${followedCount}명`}
+
+
{`게시물 ${postCount}개`}
+
+
{`${representativeFollowerId}님 외 ${followedByKnownCount}명이 팔로우합니다`}
+
+
+
+
+ );
+}
diff --git a/src/components/SearchBar/index.tsx b/src/components/SearchBar/index.tsx
new file mode 100644
index 00000000..f0958651
--- /dev/null
+++ b/src/components/SearchBar/index.tsx
@@ -0,0 +1,13 @@
+import SearchIcon from '@assets/SearchIcon';
+
+export default function SeacrhBar() {
+ return (
+