-
Notifications
You must be signed in to change notification settings - Fork 10
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주차] 김승완 미션 제출합니다. #11
base: main
Are you sure you want to change the base?
[4주차] 김승완 미션 제출합니다. #11
Conversation
1. ThemeProvider 컴포넌트 이용
1. styled-reset 모듈을 통해 컴포넌트 방식으로 적용
1. 기존의 react-scripts 러너는 상대 경로를 설정해주더라도 알아서 이를 씹고 CRA의 기본 설정으로 경로를 인식하여 오류가 생김 2. tsconfig.paths.json 파일에 기존의 설정을 해주고 @craco/craco 모듈과 react-app-alias 모듈을 설치 3. craco.config.json 파일에 craco 관련 설정을 해줌 4. 애플리케이션 러너 환경 자체를 react-scripts가 아닌, craco 가 돌릴 수 있게 설정
1. context/state 디렉터리에서 원하는 js파일을 통해 상태들을 정의해주고 이를 export해준다. 2. 프로젝트의 최상위 컴포넌트(CRA에서는 App.tsx 컴포넌트)에서 RecoilRoot 컴포넌트로 wrapping 3. 원하는 파일에서 1에서 정의해준 상태를 import하여 useRecoilState() 훅으로 사용. 받는 것은 useState와 매우 흡사하다.
1. before와 after psuedo element를 이용하여 만듦. 주의할 점은 왼쪽으로 위치하려면 -8px, 오른쪽은 -8px 느낌으로 해야 겹치지 않고 보여줄 수 있음
1. 위의 디스코드 버튼이나 이름을 누르면 사용자가 전환되고 관련 UI도 함께 바뀜
…nt-box의 크기를 제한해주었음
…Blur 핸들러 함수 내부의 조건문 분기로 해결
1. 사용자가 채팅을 입력하면 상태도 바꿔주고 로컬 스토리지도 동기화시켜줌
1. 기존의 chatMessageData의 특정 날짜에 해당하는 배열과 주소값이 달라야함 2. 따라서 prevDateArray라는 변수를 기존의 것을 배열 구조 분해하여 받고 그다음 새로운 데이터를 push함 3. 이렇게 바뀌었는데 메모리 주소값이 다른 데이터를 새로운 상태로 설
1. 스크롤을 아래로 내일 chatBody와 chatInput은 부모-자식 관계가 아니므로 prop으로 콜백을 전달할 수 없음 2. 따라서 전역 상태 관리로 이를 구현해야하는데 커스텀 훅을 통해서 진행
…하는 useEffect() 훅에서, 커스텀 훅의 scrollToBottom을 해주어야함
…처리는 배치되기 때문에 핸들러 함수 내부에서 분기하여 작성해주었음
1. 그냥 TabIconConatiner에서 userPageMode는 url과 곧바로 연동되면 끝이므로 useLocation() 훅의 pathname 정도만을 확인하는 로직이면 충분하다.
1. 렌더링 도중에 상태의 변경이 일어나면 안된다. 이를 useEffect() 훅 내부로 옮겨주었음
1. 다양한 메시지 리스트들을 렌더링함 2. 추후에 특정 메시지 아이템을 클릭하면 채팅방으로 이동하게 만듬. 근데 일단 김정민을 누르면 내가 원래 구현했던 /chat으로 이동하게 할거고 나머지는 아무 것도 없는 채팅 페이지로 이동하게 할 것임
1. 절대경로로 import 하도록 자잘한 코드 수정
1. 김정민 chat 말고도 다른 chat에서도 뒤로 가기 화살표를 누르면 /messages url로 갈 수 있도록 설정
1. 기존의 메시지 데이터가 있는 상황에 대해서 상태관리 설정을 추후에 해줘야 함
1. 사용자를 누르면 전환되는 것을 추가적으로 구현해야함
1. 앞에 $를 붙여주는 방식으로 해결
1. img를 담은 div를 img 보다 너비 높이를 높게 잡고 border radius를 원형으로 만든 뒤, background color를 적용함
1. 로고의 너비 높이는 80px 이지만 이를 위치시키는 컨테이너는 90px 이므로 이를 고려해야 레이아웃 배치
1. 사용자가 예상치 못한 접근을 하면 not found page로 갈 수 있도록 라우팅 설정
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
과제하느라 수고하셨습니다! 제가 승완님 코드에 대한 완전한 이해 없이 남기는 리뷰들이라 잘못 판단한 부분이 있을 수 있다는 걸 감안하고 봐주시면 감사하겠습니다. 전반적인 틀이나 개념적인 부분에 대한 피드백보다는 너무 마이크로한 피드백만 드린 거 같아 아쉽네요 ㅠㅠㅋㅋ
const [userNumber, setUserNumber] = useRecoilState(userNumberState); | ||
|
||
useEffect(() => { | ||
setUserNumber(1); | ||
}, []); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이미 userNumberState의 디폴트 값이 1이고, 현재 이곳에서 userNumber를 사용하는 곳이 없음에도 useEffect를 이용해 1로 설정해준 이유가 궁금합니다..!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아 그 부분은 채팅방 페이지(여기에서는 /chat/:username
경로에서 상단의 사용자를 클릭하면 userMode가 전환되는 것을 맞춰주기 위함이에요!
사용자가 채팅방에서 해당 부분을 몇번 클릭하든, 다시 친구 목록이 있는 /friends
경로로 나오면 userNumber 상태를 1로 변환하여 다시 다른 채팅방으로 접속해도 정상적으로 앱이 동작하도록 만들어 주기 위해 Friends 컴포넌트가 처음 마운트되는 시점에 useEffect() 훅을 이용하여 상태를 바꿔주는 코드를 짰습니다.
const [userPageMode, setUserPageMode] = useRecoilState(userPageModeState); | ||
const navigate = useNavigate(); | ||
|
||
// 렌더링 도중에 상태 변경을 진행하면 안됨 => 첫 렌더링 이후에 useEffect() 훅을 통해서 진행해줌ㄴ | ||
useEffect(() => { | ||
if (location.pathname === '/') { | ||
setUserPageMode('friends'); | ||
} else if (location.pathname === '/messages') { | ||
setUserPageMode('messages'); | ||
} else if (location.pathname === '/profile') { | ||
setUserPageMode('profile'); | ||
} | ||
}, [location, setUserPageMode]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 페이지에서만 사용하는 상태값이라고 판단되는데 그렇다면 로컬 상태로 관리하는 게 더 좋았을 거 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
네 저두 과제를 제출하고 너무 과도하게 모든 것을 전역 상태관리로 해결하려고 한 것이 아닌가...하는 생각이 들었어요. 유담님 말씀처럼 여기에서는 useState()
훅을 이용하여 colocation의 원칙을 지키는 게 더 나았을 것 같네요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저라면 바로 FriendsBody 컴포넌트에서 SearchHead를 호출했을 거 같아요. FriendsSearchHead라는 컴포넌트에 SearchHead를 호출하는 부분밖에 없어서요..! 컴포넌트를 잘게 쪼개다보면 관리하는 게 너무 어렵더라구요 ㅠ 그래서 승완님만의 컴포넌트 쪼개는 방식이 궁금하네요!
const tmpFriendsList = [ | ||
'discord_redesign1', | ||
'김정민', | ||
'discord_redesign2', | ||
'김승완', | ||
'discord_redesign3', | ||
'CEOS', | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분 하드 코딩하셨군요! 데이터와 연동해서 사람 이름을 클릭하면 그 사람과의 채팅방으로 연결되는 방식으로 구현하셨다면 더 챌린징했을 거 같아요 :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여기도 하드코딩 하기보다 json에 있는 더미데이터와 연결되어서 각 유저와 나눈 마지막 메세지를 보여주는 식으로 구현했다면 더 챌린징했을 거 같아요! 실제로 서버에서 받아온 데이터를 보여준다고 생각하면서요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
맞아요....! 차라리 저는 메시지 데이터를 json 더미 데이터나 로컬 스토리지에 넣어놓고 컴포넌트가 처음 마운트 되는 시점에 useEffect() 훅을 이용해서 보여주는 방식을 택했는데, 분기의 로직을 통해 하드 코딩된 데이터를 chatList UI를 보여주기보다 실제 마지막 대화 데이터를 보여주는 것이 앱의 사용성에 있어서 더 좋다고 생각해요!
좋은 지적 감사합니🙏🏼
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요 부분이 아마 서치바가 없다는 이유로 SearchHead 컴포넌트와 다르게 분리된 거 같은데, 저라면 SearchHead 컴포넌트에서 분기를 치거나 서치바도 따로 뺀 다음 사용하는 부분에서 조합을 했을 거 같아요! head 부분이 세 페이지 모두에서 같은 용도로 사용되고 있는 거 같아서 ProfilePageHead를 따로 빼기보단 SearchHead와 같은 컴포넌트로 취급되는 게 좋을 거 같아요 : )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
notFound까지 챙겨주신 꼼꼼함..
useEffect(() => { | ||
if ( | ||
localStorage.getItem(`chatMessageData${username}`) === null && | ||
localStorage.getItem(`chatMessageDateArray${username}`) === null | ||
) { | ||
setMessageData({}); | ||
setMessageDateArray([]); | ||
} else if ( | ||
// 데이터가 있는 경우 -> 상태를 변경시키고 이를 이용하여 리렌더링 진행 | ||
localStorage.getItem(`chatMessageData${username}`) !== null && | ||
localStorage.getItem(`chatMessageDateArray${username}`) !== null | ||
) { | ||
const lstrgChatMessageData = JSON.parse( | ||
localStorage.getItem(`chatMessageData${username}`) as string | ||
); | ||
const lstrgChatMessageDateArray = JSON.parse( | ||
localStorage.getItem(`chatMessageDateArray${username}`) as string | ||
); | ||
|
||
setMessageData(lstrgChatMessageData); | ||
setMessageDateArray(lstrgChatMessageDateArray); | ||
} | ||
}, []); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useEffect(() => { | |
if ( | |
localStorage.getItem(`chatMessageData${username}`) === null && | |
localStorage.getItem(`chatMessageDateArray${username}`) === null | |
) { | |
setMessageData({}); | |
setMessageDateArray([]); | |
} else if ( | |
// 데이터가 있는 경우 -> 상태를 변경시키고 이를 이용하여 리렌더링 진행 | |
localStorage.getItem(`chatMessageData${username}`) !== null && | |
localStorage.getItem(`chatMessageDateArray${username}`) !== null | |
) { | |
const lstrgChatMessageData = JSON.parse( | |
localStorage.getItem(`chatMessageData${username}`) as string | |
); | |
const lstrgChatMessageDateArray = JSON.parse( | |
localStorage.getItem(`chatMessageDateArray${username}`) as string | |
); | |
setMessageData(lstrgChatMessageData); | |
setMessageDateArray(lstrgChatMessageDateArray); | |
} | |
}, []); | |
useEffect(() => { | |
const chatMessageData = localStorage.getItem(`chatMessageData${username}`); | |
const chatMessageDateArray = localStorage.getItem(`chatMessageDateArray${username}`); | |
if (chatMessageData && chatMessageDateArray) { | |
setMessageData(JSON.parse(chatMessageData)); | |
setMessageDateArray(JSON.parse(chatMessageDateArray)); | |
} else { | |
setMessageData({}); | |
setMessageDateArray([]); | |
} | |
}, []); | |
이런 식으로 반복되는 코드를 변수로 빼서 사용하면 더 간소화된 코드를 작성할 수 있을 거 같아요!
isEqual: boolean; | ||
from: number; | ||
createdAt: string; | ||
content: string; | ||
like: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이렇게 생긴 타입이 다른 컴포넌트에서도 반복되던데 types 폴더에 따로 지정해둔 뒤 import해서 사용하는 게 더 좋을 거 같습니다!
export default function MessageListContainer() { | ||
const navigate = useNavigate(); | ||
|
||
function handleClickMessageListItem(path: string) { | ||
navigate(`/chat/${path}`); | ||
} | ||
|
||
return ( | ||
<StyledMessageListContainer> | ||
<MessageListItem | ||
discordLogoColor="green" | ||
name="김정민" | ||
$ifBlueSignal={true} | ||
content={'내일 회의 괜찮으실까요?'} | ||
dateString={'20:45'} | ||
navigateToChatFunc={handleClickMessageListItem} | ||
path="" | ||
/> | ||
<MessageListItem | ||
discordLogoColor="purple" | ||
name="CEOS2024" | ||
$ifBlueSignal={true} | ||
content={'내일 숙제 제출 마감일입니다! 늦지 않게 제출해주세요!'} | ||
dateString={'어제'} | ||
navigateToChatFunc={handleClickMessageListItem} | ||
path="CEOS2024" | ||
/> | ||
<MessageListItem | ||
discordLogoColor="purple" | ||
name="홍길동" | ||
$ifBlueSignal={false} | ||
content={'내일 회의 괜찮으실까요?'} | ||
dateString={'3월 10일'} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
어차피 url path에 name이라면 name 프로퍼티만 넘긴 뒤 navigate하는 부분은 MessageListItem에서 처리한다면 불필요한 프롭 전달을 하지 않아도 될 거 같아요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
앗 그 부분은 제가 3주차 과제에 만든 채팅방을 /chat/userId
형식이 아닌, 그냥 가장 기본적인 /chat
url path로 설정하여 첫 아이템만 name과 path가 달라지기에 이렇게 짰습니다.
근데 오히려 아예 유담님 말씀을 참고해보니 MessageListItem
컴포넌트에서 name을 받아온 뒤 분기의 로직으로 해줘도 좋았겠네요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
마지막으로 svg 관련해서 네이밍만 보더라도 어떤 아이콘인지 알 수 있게 피그마에서 가져온 네임을 그대로 사용하기 보다 다른 네임으로 변경하는 게 좋을 거 같습니다! 그리고 svgr을 사용하게 된다면 편리하게 svg의 속성(크기, 색상)을 변경할 수 있는데요. svg 코드의 색상에 해당하는 부분에 fill = "current"라는 값을 넣어주면 import 하는 측에서 fill: 원하는 색상 을 넣음으로써 동적 스타일링을 편하게 할 수 있습니다! 저는 하단 네비게이션 바 아이콘 클릭시 svg 색상 변경에 사용했는데요. 제 코드 레퍼런스 남기고 가요 :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
역시나 승완님답게
엄청나게 구조화된 구조의 코드
라우팅, recoil , 커스텀 훅 등을 필요에 맞게 최대한으로 잘 활용하신 점이 너무나 인상깊습니다.
recoil과 라우팅을 처음 이용해본 사람으로써, 많이 배워가는 시간이 되었던 것 같습니다.
<Route path="/" element={<CommonLayout />}> | ||
<Route index={true} element={<Friends />}></Route> | ||
<Route path="messages" element={<Messages />}></Route> | ||
<Route path="profile" element={<Profile />}></Route> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
index={true} 속성을 활용한 라우팅 처리 방식이 매우 효율적인 것 같습니다
CommonLayout을 부모 컴포넌트로 사용하면서, / 경로에서 Friends 컴포넌트를 기본 페이지로 설정한 것은 사용자가 사이트에 접속했을 때 바로 친구 목록을 볼 수 있게 하셨네요..!
messages와 profile을 하위 라우트로 구성해 중첩을 구현하셨네요ㅠ 저는 그냥 분리해서 구현했는데, 유지보수와 확장성 면에서도 좋은 코들르 짜신 것 같네요 bbb 많이 배워갑ㄴ디ㅏ.
return ( | ||
<StyledCommonLayout> | ||
<IphoneStatusBar /> | ||
<Outlet /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IphoneStatusBar와 HomeIndicator 컴포넌트는 상단과 하단에 고정으로 배치하고
중앙의 Outlet을 통해 주요 콘텐츠를 변경할 수 있게 구현하신 점이 정말 인상적입니다. 라우팅 구현을 정말 잘 하시는 것 같네요ㅠ
특히 저는 레이아웃 간에 중복 문제 해결을 할 떄, css를 통해 위치를 고정하는 원시적인 방식으로 해결했는데,
Outlet을 사용하면 여러 레이아웃 간에 중복을 피하면서도 코드의 가독성을 높일 수 있는 것 같아 정말 좋은 것 같아 리팩토링할 때 꼭 이용해봐야겠습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useScrollToBottom' 훅은 스크롤 관련 로직을 커스텀 훅으로 분리하셨네요..!!
어떤 컴포넌트에서든 동일한 스크롤 기능을 재사용할 수 있어, 기능을 더 확장하면 효율적인 방식인 것 같네요!
overflow-y: scroll; | ||
`; | ||
|
||
export default function NotFound() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
notfound 구현할 생각을 못했는데, 승완님 코드 보면 항상 꼼꼼하게 구현하시는 것 같아서 많이 배우고 갑니다ㅠ
@@ -0,0 +1,39 @@ | |||
const theme = { | |||
color: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이렇게 자주 사용하는 style을 묶어서 관리하는게 정말 효율적인 방식인 것 같네요!
📌 구현 기능
styled-components
의ThemeProvider
를 통해 디자인 시스템 구축🧠 느낀 점 및 시간 투자 부분
시간 투자 💪🏼
url 라우팅을 진행하는 데에 시간을 꽤 투자했던 것 같습니다. 3주차의 과제가 react로 채팅방까지를 구현하는 것이라, 이를 루트(/) 디렉터리로 설정했습니다.
이번 과제는 친구 목록, 채팅 목록, 채팅방 입장, 개인 프로필 페이지를 모두 구현해야 함과 동시에 기존의 채팅방을 녹여내는 것까지라 프로젝트 url 설계를 먼저 진행했습니다.
공통적으로 많이 사용되는 header와 tabBar 같은 경우에는
CommonLayout
컴포넌트에 넣어주고, 주소에 따라서 바뀌는 부분을Outlet
컴포넌트를 통해 유동적으로 바꿔주었습니다.또한 사용자가 입장한 채팅방에서 채팅을 입력한 후, 새로 고침을 하거나 퇴장한 후 다시 입장해도 기존의 메시지 데이터가 남아있어야 하기에 이를
localStorage
에 넣어주는 방식을 이용했습니다.느낀 점 ❗️
localStorage
에 key 값으로 사용자명을 썼는데, 어떻게 하면 primary한 데이터로 바꿀 수 있을지 더 고민해보면 좋을 것 같습니다.recoil
이나redux
전역 상태 관리 라이브러리로 관리할 필요는 없을 것 같습니다. 사용자의 인터렉션에 따라 상태가 변화하고 관련 UI가 구현되는 정도라면 해당 상태는 컴포넌트에서useState()
훅을 이용하는 것이 단일 책임의 원칙에도 더 맞을 것 같습니다.❓ Key Questions
라우팅이란, 프론트엔드에서 웹 애플리케이션 내의 다양한 페이지를 탐색할 수 있도록 도와주는 기술이다. 네이버, 다음, 쿠팡, 배달의 민족과 같이 우리가 자주 사용하는 웹 사이트는 랜딩 페이지 이외에도 여러 세부 페이지로 나뉘어져 있다.
기존의 html, css, vanilla Javascript 만을 이용하는 방식에서는
a
태그를 통해 특정 페이지로의 전환을 하고 해당 페이지에 알맞은 html 파일을 통해 사용자에게 적절한 UI를 렌더링한다.요즘 많이 사용되는 AngularJS(인기가 좀 죽긴 했다), vueJS, reactJS에는 Angular-router, vue-router, react-router 등의 방식으로 라우팅을 지원하는데, 이를 사용하지 않았을 때와 비교해서 살펴보면 좋다.
위와 같은 방식은 react 코드 단에서 react-router-dom의 도움 없이 상태(state)만으로 라우팅을 진행하고 있다. 버튼을 누르면 상태가 변경되어
main
태그 내에서 렌더링되는 children 속성 컴포넌트가 바뀔 수 있게 한다. 하지만 이와 같은 로직은 페이지가 추가될 때마다 관련 핸들러 함수를 만드는 등 복잡도가 증가할 수 있다. 이를 위해 react는react-router-dom
모듈의 Router 개념을 통해 해결한다위의 코드는 react-router-dom 모듈의
BrowserRouter
,Routes
,Route
컴포넌트 들을 이용해 app.tsx 파일 내에서 라우팅을 진행해준 것이다(ThemeProvider와 RecoilRoot 컴포넌트는 각각 디자인 시스템, 전역 상태 관리를 위해 존재하는 wrapper 컴포넌트이니 신경 쓰지 않아도 된다).이와 같은 구조를 이용하면 웹 사이트의 도메인 주소의 특정
url path
로 사용자가 접근하면 Route 컴포넌트의 element 속성으로 바인딩 된 컴포넌트가 렌더링 되는 구조인 것이다.하지만 이와 같은 라우팅 방식은 ReactJS 같은 CSR(클라이언트 사이드 렌더링) 방식의 웹 페이지에서 보이는 특성이고, nextJS나 Remix 같은 SSR(서버 사이드 렌더링), SSG(Static Site Generation)을 지원하는 풀스택 프레임워크에서는 주로 src 디렉터리 내부의 폴더 구조를 통해 라우팅을 진행한다. 즉, 디렉터리가 하나의 url path에 대응하는 것이다.
SPA 란?
SPA는 Single Page Application의 약자로, 특정 웹 애플리케이션이 구동될 떄 하나의 html 파일만을 기반으로 하여 작동한다는 것이다. reactJS를 이용하여 프로젝트를 진행할 때, CRA이면 public 디렉터리 내에 index.html이, vite를 이용하면 루트 디렉터리에 index.html 파일 하나만이 존재하는 것을 볼 수 있다. 해당 파일을 들여다보면
body
컴포넌트 내에 id가 root인 div 태그 하나 만이 외롭게 있는 것일 알 수 있다.이는 위에서 말한 react의 라우팅 방식에 의해 매 페이지에 해당하는 컨텐츠들이 id 가 root인 태그 내에 채워지며 상황마다 바뀌기 때문이다. 뉴진스의 홈페이지에서 페이지 소스를 살펴보면 클라이언트(브라우저)에 전달된 리소스는 텅 빈 html 파일만이 전달된 것을 알 수 있다.
SPA는 세부 페이지마다 바뀌는 내용을 AJAX(Asnycronous Javascript and Xml)라는 기술을 통해 필요한 부분의 데이터에 대한 요청을 실행하여 렌더링하기 때문에 html 문서 자체가 바뀌면서 생기는 full page refresh와 같은 현상을 막을 수 있다. 이는 페이지 간의 부드러운 전환으로 사용자에게 더 나은 UX를 선사할 수 있다. 하지만 위에서 언급한 바와 같이 처음 클라이언트에게 전달되는 html 문서에는 아무런 컨텐츠가 존재하지 않기 때문에 검색 엔진 최적화(SEO)의 측면에서는 다소 불리하다고 할 수 있다.
react 같은 SPA 기술이 등장 이전에는 Java의 서블릿과 같은 개념, 여러 템플릿 엔진을 통해 사용자의 동적 요청에 따른 웹 페이지 반환 SSR(Server Side Rendering) 와 미리 만들어둔 컨텐츠가 자주 변하지 않는 정적 컨텐츠를 생성하는 SSG(Static Site Generation) 개념이 주를 이루었다. 이는 복수의 html 파일을 사용자에게 전달할 수 있다는 점에서 MPA(Multi Page Application)이라고 하며, 미리 컨텐츠가 채워져 있어 검색 엔진 최적화를 할 수 있지만 페이지 전환 시 뚝뚝 끊기는 현상이 발생한다는 단점이 있다.
위의 두 방식의 단점을 보완하고 장점을 챙기는 방식으로 나온 것들이 바로 nextJS와 nuxtJS 같은 풀스택 프레임워크 개념이다. 이들은 페이지, 컴포넌트 기반의 SSG, SSR 방식의 구현을 지원함과 동시에 웹 페이지의 변경되는 부분에 대한 추가적인 네트워크 데이터 요청으로 부드러운 페이지 전환 효과를 기대할 수 있다.
상태 관리란?
별도의 상태 관리 라이브러리를 사용하지 않아도 react 프로젝트를 진행할 수 있다. 이 경우에는 상태가 필요한 컴포넌트에서
useState()
훅을 통해 상태를 선언하여 UI를 렌더링 하면 된다. 하지만 복수의 컴포넌트에서 하나의 상태를 통해 렌더링 해야하는 로직이 존재한다면 상태 끌어올리기(state lifting)이 필요하다. 이는 해당 컴포넌트들이 공통적으로 가지는 부모 컴포넌트, 혹은 그 상위의 컴포넌트로 상태의 선언을 끌어올린 후,상태를 prop으로 계속해서 전달하여 사용하는 것이다. 단순한 프로젝트에서는 큰 상관이 없겠지만 이는 여러 문제점을 내포하고 있다.이를 위해 contextAPI 같은 reactJS 자체의 방식, flux 패턴을 적용한 redux 라이브러리, atomic 패턴의 recoilJS와 그 이외의 mobx, zustand, jotai 등을 활용하면 보다 더 편리하게 상태 관리를 진행할 수 있다.