React, Beyond the Basics....
기초를 넘어서기 위한 3주차 과제가 끝났다. 1, 2주차에서 JS 로 SPA 를 구현하면서 3주차가 되게 힘들거라고 예상했다.
리액트를 써보지 않은 입장에서 React hooks, React 메모이제이션, ... 험난했다. (+ Typescript 또한 안써봤다...)
그래도 1, 2 주차에 예방주사를 좀 맞아 놓은 덕에 내 예상보다는 조금은 수월하게 진행했다. 항상 과제 진척도가 40? 50% 가 되질 못했었는데 이번엔 완성에 가까운 진척도를 이뤄내서 내심 뿌듯했다.
그렇다고 절대 과제가 쉽다는 말은 아니다. (나에게는 매 순간이 고비였다 ㅋ...)
1주였지만 리액트라는 것을 조금은 알아간거에 만족하고있다. 더 알아갈 일만 남았다.
그리고 양질의 과제 준비하시느라 고생 많으셨습니다. 준일 코치님!
그리고 블루뱃지 땄잖슴 ~~~ ~~ ~ ~ ~ 🔵
과제가 끝나고 팀원들과 코드 리뷰를 했다. 그 중 아래와 같은 리뷰를 받았다.
리액트를 써보지 않은 나 + 쨰미나이(=Gemini CLI) 의 환장의 콜라보로 탄생한 코드이기 때문에 지훈님의 리뷰가 나의 궁금증을 자극했다. 교수님 최고bb 👍👍👍
그렇다면 왜? 도대체 왜?? 호출 하지말라는걸까? 하지말라면 하고 싶은게 인간인데.. 그 욕구를 어떻게 하면 참게 해줄지 정리해보겠다. 다들 잘 참아보시길...
React의 Hooks는 현재 렌더링 중인 컴포넌트가 무엇인지 아는 "컨텍스트(Context)" 내에서만 호출될 수 있습니다.
✅ JSX (<MyComponent />
): React가 MyComponent를 렌더링하기 위해 호출할 때, React는 "지금 MyComponent을 렌더링하고 있어"라고 내부적으로 기록합니다.
이 상태에서 <MyComponent />
내부의 useState나 useEffect가 호출되면, React는 해당 Hook을 MyComponent에 연결합니다.
❌ 직접 호출 (MyComponent()
): React의 렌더링 과정 외부에서 MyComponent()를 그냥 호출하면, React는 어떤 컴포넌트가 렌더링 중인지 전혀 모릅니다.
이 상태에서 useState 같은 Hook을 호출하면, React는 이 Hook을 어디에 연결해야 할지 모르기 때문에 즉시 오류를 발생시킵니다.
1 function Counter() {
2 const [percent, setPercent] = useState(0); // Hook 사용
3 return <div>{percent}% 뭉갬</div>;
4 }
5
6 // 1. 올바른 방법 (JSX)
7 const element = <땅울림 />; // React가 percent를 렌더링하고 Hook을 관리함
8
9 // 2. 잘못된 방법 (직접 호출)
10 const result = 땅울림(); // 여기서 바로 "Hooks can only be called..." 오류 발생!
React 컴포넌트는 단순한 함수가 아니라, React 런타임에 의해 관리되는 "인스턴스"입니다.
✅ JSX: 를 사용하면 React는 해당 컴포넌트의 인스턴스를 생성하고, mount (최초 렌더링), update (리렌더링), unmount (사라짐)와 같은 생명주기를 관리합니다.
useState로 상태를 변경하면 React는 이를 감지하고 update를 트리거합니다.
❌ 직접 호출: MyComponent()는 그냥 함수를 한 번 실행하고 그 반환값을 받을 뿐입니다.
여기에는 mount나 update 개념이 없습니다. 따라서 setCount와 같은 상태 변경 함수를 호출해도 리렌더링은 절대 일어나지 않습니다.
React가 해당 컴포넌트의 상태를 추적하고 있지 않기 때문입니다.
진격의 거인을 빗대서 얘기해보겠습니다. (10주 동안 계속 빗댈 예정)
조사병단 총사령부 (에르빈, 한지 등 지휘부): React 런타임
조사병단 병사 (에렌, 아르민, 미카사, 리바이, ...): 컴포넌트 (`MyComponent`)
지령서 (트로스트 구 탈환 명령 지시): Props
입체 기동 장치: Hooks
✅ JSX 사용 (<조사병단 />)
상황: 트로스트 구 탈환 작전 ㄱㄱ
1. 단쵸(React)가 출격 명령(JSX)을 내림
2. 명령에 따라 조사병단 (컴포넌트) 출격 ㄱㄱㄱ
3. 조사병단은 입체 기동 장치(Hooks) 및 가스 (state) 준비
4. 조사병단은 임무 지령서 (Props) 에 따라 임무 실행.
입체기동장치는 총사령부의 지원 (React 렌더링 컨텍스트)을 받고 있기 때문에 완벽하게 작동
5. 만약 거인화를 해야할 상황이 발생(state 변경)이 되면 총사령부는 인지하고 작전을 변경(리렌더링)
❌ 직접 호출 (조사병단())
상황: 밥 먹고 있는 조사병단에게 걍 나가서 싸우라고 함
1. 누군가(코드) 밥 먹고 있는 조사병단(컴포넌트) 발견하고 외침. "지금 당장 나가서 싸워주셈!!"
2. 조사병단: ㅇㅋ 맡겨만 줘.
3. 아.. 근데 준비가 안됐어.. 입체 기동 장치(Hooks)가 없는데.. 그리고 단쵸가 출격 명령을 안줬는데???
4. 조사병단: 장비가 없잖슴..못 싸움..("Hooks can only be called..." 오류 발생!)
그럼.. 왜?? 나의 memo.ts 는 작동을 한 것일까? (개쩔게 만들었잖슴~)
1 export function memo<P extends object>(Component: FunctionComponent<P>, equals = shallowEquals) {
2 // 이 memoizedComponent는 React에 의해 JSX로 렌더링됩니다.
3 const memoizedComponent: FunctionComponent<P> = (props) => {
4 const cache = useRef(...); // Hook이 정상 동작함
5
6 if (...) {
7 // memoizedComponent의 렌더링 "내부"에서 Component를 직접 호출
8 const newResult = Component(props);
9 // 이 시점에는 React의 "현재 렌더링 중인 컴포넌트" 컨텍스트가
10 // memoizedComponent로 설정되어 있기 때문에, Component 내부의 Hook도
11 // memoizedComponent의 일부로서 동작하게 됩니다.
12 cache.current = { preProps: props, result: newResult };
13 }
14 return cache.current.result;
15 };
16 return memoizedComponent;
17 }
상황: 리바이 병장의 현장 지휘
memo와 같은 HOC는 리바이 병장과 같다.. 멋진 친구임
1. 에르빈 단장(React)이 리바이 반(memoizedComponent)에 공식 출격 명령
2. 리바이 병장(memo HOC)이 에렌(원본 Component)에게 직접 명령:
"에렌, 저 놈은 네가 맡아라." (Component(props) 직접 호출)
3. 이미 총사령부가 승인한 공식 작전(리바이 반의 출격)에 참여.
입체기동장치(Hooks)는 이미 착용된 상태이며 정상적으로 작동
memo.ts와 같은 HOC 내부에서 사용된 것은, 부모 컴포넌트의 렌더링 컨텍스트를
"빌려 쓰는" 특수한 상황이기 때문에 가능한 것입니다.
하지만 이는 매우 예외적인 케이스이며, 일반적인 애플리케이션 코드에서는 컴포넌트를 렌더링할 때 반드시 JSX (<조사병단 />
)를 사용해야 합니다.!!!
또 하나의 궁금증 바로 Context 와 전역상태에 대한 것 입니다.
이전에도 팀별 모임에서 둘의 차이점에 대해 얘기를 했었죠... 그 때는 정확히 개념 자체를 알고 있지 않기 때문에 팀원들의 말에 집중만 했습니다..그저 집중만...
그래서 지금 위 주제에 대해 정리를 해보려합니다!
React 내장 기능으로 주된 목적은 Props Driling 을 피하는 것
내장 기능이기에 번들 크기가 늘어나지 않고 사용법이 비교적 간단합니다. 또한 자주 변경되지 않는 데이터(테마, 언어 설정, 사용자 인증 정보 등..) 를 전역적으로 공유 하기에 좋습니다.
그치만 단점은 성능 문제가 심각합니다. Context 의 value 가 변경될 때 마다 모두 리렌더링됩니다. (컴포넌트가 변경된 값을 실제로 사용하지 않아도..)
그리고 자체 상태 관리 로직을 제공하지 않기에 useState
또는 useReducer
와 함께 사용하는데 상태가 복잡해질수록 관리가 번거로워집니다.
애플리케이션의 복잡하고 자주 변경되는 상태를 효율적으로 관리하기 위해 만들어진 외부 라이브러리
대부분의 라이브러리들은 선택적 렌더링을 지원합니다. 컴포넌트는 자신이 구독하는 상태가 변경될 때만 리렌더링 되고 이것은 Context의 단점을 완벽히 보완합니다.
그리고 상태와 상태 변경 로직이 한 곳에 모여있어 코드의 예측 가능성과 유지보수성을 높이는데 기여합니다. 상태 변경을 추적하고 디버깅에도 강력한 기능을 제공합니다.
역시나 단점도 존재합니다. 별도의 라이브러리 설치로 인한 번들 크기 증대, 라이브러리 마다 초기 설정 또는 배워야할 개념등이 있습니다. 그리고 간단한 기능을 구현할지라도 작성해야 할 코드량이 증가할 수 있습니다.
구분 | React Context | 전역 상태 관리 라이브러리 (Zustand, Jotai 등) |
---|---|---|
주요 목적 | Prop Drilling 해결 | 복잡한 애플리케이션 상태의 중앙 관리 |
성능 | 값 변경 시 모든 소비자가 리렌더링 | 상태를 구독하는 컴포넌트만 선택적으로 리렌더링 |
사용 편의성 | 간단하고 배우기 쉬움 | 라이브러리에 따라 학습 곡선이 존재 (Redux > Zustand) |
의존성 | React 내장 (없음) | 외부 라이브러리 설치 필요 |
추천 사용 사례 | 테마, 인증 정보 등 자주 바뀌지 않는 데이터 | 여러 컴포넌트에서 공유하며 자주 바뀌는 복잡한 상태 |
- Zustand
Redux와 같은 중앙 집중식 스토어에 익숙한 개발자에게 좋습니다.
상태와 상태 변경 로직을 한 곳에서 관리하고 싶을 때 유용합니다.
* 상태가 서로 강하게 연관되어 있을 때 적합합니다.
- Jotai
- 상태를 더 작고 독립적인 단위로 나누어 관리하고 싶을 때 좋습니다.
- 컴포넌트별로 필요한 상태만 가져와 사용하는 "바텀업(Bottom-up)" 방식에 적합합니다.
- 애플리케이션이 커지고 상태가 복잡해질수록 확장성이 좋습니다.
그리고 3팀의 준형님이 해당 주제로 글을 작성한 포스트가 있습니다. 오프라인에서도 직접 발표를 통해 공유를 해주셨지만 개인적으로 발표를 하는 모습이 너무 예뻐서 내용이 안들어와가지고 내가 씹뜯맛즐🍗 하려고 첨부합니다... 🍯
읽으면서 준형님의 생각도 알게 돼서 좋고 공감되는 부분도 많았습니다. 모두 많이 읽어보시길!!!
어쩌면 리액트와 BF가 될지도?
▶ 허정석님의 추천 코드 : Vo6fpd
어머나 정석님 언급 감사합니다 ~ ㅎㅎ