-
React 개발을 해야하는데 말이야, 어떻게 시작하면 되는건데?
Library/React 2024. 2. 3. 13:49Vue ➔ React 마이그레이션
시작하기기존에도 리액트를 조금씩 공부하긴 했지만 넥스트와 타입스크립트까지는 아니었다. 그런데 사내 이커머스 솔루션의 프론트엔드가 vue에서 react로 마이그레이션을 진행하면서 급하게 범위를 넓혀 공부를 하게 되었다. 공부 기간은 약 2-3주로 여유가 있지는 않고 지금도 스토리보드를 보면서 어떻게 개발을 하면 좋을지 감을 잡아가고 있는 단계이다.
지난번 NextJS의 앱 라우터 방식을 살짝 맛봤었는데, 사실 이것도 프로젝트에 적용할 예정이기 때문에 공식 문서를 참고했었다는 사실. 그래서 오늘 정리해볼 내용은 '스토리보드와 화면을 봤는데, 어떻게 시작하면 좋을까?'에 대한 내용이다. 정답은 없으니 참고만 하시고, 혹시 더 좋은 제안이 있다면 댓글로 남겨주시길...
Vue는 이번에 처음 봤고, 이건 필요할 때마다 검색해가면서 리액트에서 어떻게 구현하면 좋을지 생각하고 있다. Vue 잘 모른다는 이야기임. 그래서 이 부분은 건너뛰고 리액트를 볼 예정이다.
앱 라우팅의 트리 구조 아래와 같은 구조에서 리액트는 app layout 내에 children 영역에 dashboard를 그린다. dashboard 하위에 layout.tsx 와 page.js가 있다면 app layout 의 children 영역에 dashboard layout 그리고 dashboard layout 의 children 영역에 dashboard page를 렌더링한다.
NextJS는 서버 사이드 렌더링을 위해 사용한다. 하지만 사용자와 인터랙션이 필요한 페이지 혹은 컴포넌트의 경우에는 서버 사이드 렌더링이 불가하다. 서버에서 한 번 그린 화면은 다시 렌더링하지 않기 때문이다.
예를 들면, 장바구니 화면에서 택배 발송과 정기 주문 탭이 있다면 탭을 번갈아가며 클릭하는 경우에 장바구니 아이템들을 재조회해야 한다. 재조회 시에 품절 여부에 따라 그려지는 아이템 컴포넌트도 달라진다. 그렇기 때문에 'use client'를 사용하여 이 화면은 클라이언트 사이드 렌더링해야 한다.
그런데 서버 사이드 렌더링을 하는 이유는 사용자가 계속 빈 화면만 보고 있지 않도록 하기 위해, 그리고 SEO(검색 엔진 최적화)를 위해서이다. 그럼 이제 아래 샘플을 보고 컴포넌트를 나누고 렌더링 전략을 생각해봐야 한다.
렌더링
이번에 작업할 화면이 장바구니이기 때문에 '네이버 장바구니'를 참고자료로 가져왔다. 만약
app > (root) > cart
경로라면 cart page.js는 서버 사이드 렌더링으로 작업한다. (root) 경로의 layout을 설계할 때 <head> 영역에 <meta> 데이터를 담을 수 있도록 한다.네이버 장바구니 화면 샘플 그럼 내가 개발해야 하는 부분을 cart 디렉토리 하위 파일들이라고 생각했을 때, layout 구성이 어떻게 될지 고민해봤다. 일단 header 영역과 아래 장바구니 목록을 보여주는 영역으로 나누어진다. 그럼 header를 공통으로 사용하지 않는다고 가정하고 아래와 같이 레이아웃을 구성하면 프레임워크의 라우팅에 의해 레이아웃 영역에 {children} 으로 된 영역에 page.tsx 코드가 위치하게 된다.
cart > layout.tsx
interface Props { children: React.ReactNode } export default ({children}: Props) => { return ( <> <CartHeader></CartHeader> {children} </> ) }
cart > page.tsx
import Cart from '/src/component/cart/cart.tsx' export default () => { return <Cart></Cart> }
그럼 클라이언트 사이드 렌더링으로 진행해야 하는 부분은 app 하위가 아닌 component 하위의 cart > cart.tsx 파일이다. 서버 사이드 렌더링에서 주의할 점은 'use client'라고 클라이언트 사이드 렌더링임을 명시하지 않으면 서버 컴포넌트로 간주되지만, 클라이언트 컴포넌트 하위에 위치한 컴포넌트들은 모두 클라이언트 컴포넌트가 된다.
상태 관리
page 내에 위치한 컴포넌트들은 이제 상태 관리가 유기적이다. 전체 선택 체크박스를 클릭 시에 목록의 체크박스들이 전부 클릭되어야 하고 목록의 체크박스가 개별적으로 클릭될 시에 전체 선택 체크박스도 영향을 받는다. 또한 체크박스 change 이벤트가 발생할 시마다 상품가격 + 배송비 계산이 아래에 표시되어야 한다. 선택 삭제 시에는 선택된 상품들을 전부 삭제하는 api를 호출해야 하고, 개별적으로 삭제 시에는 상품 1개에 대한 삭제 api를 호출해야 한다.
그렇기 때문에 상태 관리가 필요한 것들을 cart > page.tsx 에 선언해야 한다.
tabCount: {type: string, count: number} => 일반배송 / 지정배송 탭에 표시하기 위한 아이템 개수
checkedRows: string[] => 체크된 아이템의 번호 배열
cartItems: cartItem[] => 장바구니 아이템 목록
특히 체크박스가 골치가 아팠는데 vue 에서는 v-model 양방향 바인딩을 사용해서 간단하게 구현이 되었다. 리액트에도 양방향 바인딩을 위해 useState를 활용할 수 있으나, setState는 아무 곳에서나 할 수 없기 때문에 하위 컴포넌트에 setState하는 함수를 인자(argument)로 전달해야 한다는 번거로움이 있다.
그래서 간단하게 보자면 아래와 같이 checkedRows는 cart > page.tsx 내에 위치시키고 전체선택 체크박스와 아이템 리스트의 체크박스는 각각의 컴포넌트 내에 위치시킨 후 hadle function (함수도 일급 객체이다) 전달한다.
export defalt () => { const [checkedRows, setCheckedRows] = useState<string[]>(); const handleCheckedRows = useCallback((e) => { // 개별선택 체크박스용 이벤트 핸들러 const checkedNum = e.target.key if (e.target.checked) { setCheckedRows((checkedRows) => [...checkedRows, checkedNum]) } else { setCheckedRows((checkedRows) => [...checkedRows].filter((value) => value !== checkedNum) ) } }, []); const handleCheckedAllRows = useCallback((e) => { // 전체선택 체크박스용 이벤트 핸들러 }, []) ... 생략 ... return ( <> ...생략... <CartItems handleCheckedRows={handleCheckedRows}></CartItems> </> ) }
개발을 진행할 때 아래 두 가지 사항을 유의할 것.
useState의 setState 사용 시 setState는 함수 내 중간에 위치해 있어도 set 발생하는 시점은 가장 마지막이다라는 점을 유의해야 한다. 중간에 set 하고 checkedRows 값을 꺼내어 사용하려고 해도 기존 값 그대로이기 때문에 반드시 함수 내에서 새로운 배열을 선언하여 가공을 한 후에 마지막에 set을 한다.
함수를 사용할 시 가급적이면 메모이제이션 훅, useCallback과 useMemo를 사용한다. 함수가 메모이제이션 되면서 deps의 변화가 일어나기 전까지는 동일한 함수, 값을 재사용하기 때문에 성능에 유리하다. [] deps 내에 아무것도 작성하지 않을 시 초기 마운트 후에는 리렌더링이 되지 않는다.
'Library > React' 카테고리의 다른 글
프로젝트에 타입스크립트와 스토리북 적용하기 (0) 2024.04.14 React #3 리액트 Hooks - useMemo와 useCallback (0) 2023.08.25 React #2 React Router 중첩경로 (0) 2023.08.15 React #1 컴포넌트의 라이프사이클 메서드 (0) 2023.07.13