✅ State
1. 상태 소개
일단 상태부터 만들고 보는 초보에서 벗어나기
일단 상태가 무엇일까 ?
점점 어려워지는 상태관리가 문제일까? 상태를 대하는 태도가 문제일까?
상태 종류(언제 만드는지 고민, 왜 만드는지 고민, 왜 필요한지 고민)
- 컴포넌트 상태, 전역 상태, 서버 상태, 상태 관리(상태 변경, 상태 최적화, 렌더링 최적화, 불변성, 상태 관리자)
거꾸로 생각하기
우리는 상태관리를 왜 하고 있는 것일까?
- 상태 관리는 목적인가? 수단인가?
- 상태 관리를 위해 앱을 개발하는 것일까?
- 앱을 개발하는데 상태는 왜 관리하는 것일까?
일단 상태가 무엇인가?
- 상태 = State
- 사물, 현상이 놓여 있는 모양이나 형편
- ex) 무방비 상태, 정신 상태, 건강 상태, 이미 기차가 끊긴 상태
2. 올바른 초기값 설정
올바른 초기값 설정은 왜 중요할까?
- 렌더링 에러 처리 가능
- 초기값이 없을 경우, 해당 값을 통해서 계산하는 로직에서 에러 발생을 방지 할 수 있음
초기값
- 초기에 렌더링 되는 값
- 가장 먼저 렌더링 될 때, 순간적으로 보여질 수 있는 값
초기값 지키지 않을 경우
- 렌더링 이슈, 무한 루프, 타입 불일치로 의도하지 않는 동작 발생 ⇒ 런타임 에러 발생
- 초기값 넣지 않으면
undefined
값으로 셋팅 됨 - 상태를 CRUD ⇒ 상태를 지울 때도 초기값을 잘 기억해놔야 원상태로 돌아감.
- 빈값?
null
처리 할 때 불필요한 방어코드도 줄여 줌
요약
초기 상태를 올바르게 설정하자
3. 업데이트 되지 않는 값
예시
const NotUpdateValue = (): Element => {
const INFO = {
name: 'My Component'
value: 'Clean Code React'
};
const [count, setCount] = useState(0);
const onIncrement = () => setCount((prevCount) => prevCount + 1);
const onDecrement = () => setCount((prevCount) => prevCount - 1);
return (
<div className="App">
<main className="App-main">
<header>{INFO}</header>
<ShowCount info={INFO} count={count} />
<ButtonGroup onDecrement={onDecrement} onIncrement={onIncrement} />
</main>
</div>
)
}
INFO
상수가 컴포넌트 안에 존재했을 때의 문제점
- 상수를 다루거나 아니면 일반적인 방치
- 컴포넌트가 렌더링 될 때마다 해당 객체가 새로 새성성되고 참조됨
- 업데이트가 되지 않는 일반적인 객체
- 리액트 상태로 바꾼다던가 혹은 아예 외부로 내보내야 함.
4. 불필요한 상태 제거하기
결론
// 기존
const [userList, setUserList] = useState(MOCK_DATA);
const [complUserList, setComplUserList] = useState(MOCK_DATA);
useEffect(() => {
const newList = complUserList.filter((user) => user.completed === true);
setUserList(newList);
}, [userList);
// 변경
const complUserList = complUserList.filter((user) => user.completed === true);
내용
불필요한 상태를 만든다면?
- 결국에는 리액트에 의해 관리되는 값이 늘어나는 것
- 그러다보면 렌더링에 양향을 주는 값이 늘어나서 관리 포인트가 더더욱 늘어 남
컴포넌트 내부에서의 변수는?
- 렌더링 마다 고유의 값을 가지는 계산된 값
요약
- props를 useState에 넣지 않고 바로 return 문에 사용하기
- 컴포넌트 내부 변수는 렌더링마다 고유한 값을 가짐
- 따라서 useState가 아닐, const로 상태를 선언하는게 좋은 경우도 있음
5. useState 대신 useRef
결론
// 기존
export const component = () => {
❌
const [isMount, setIsMount] = useState(false);
useEffect(() => {
if(!isMount) {
setIsMount(true);
}
}, [isMount]);
};
// 변경
export const component = () => {
💡
const isMount = useRef(false);
useEffect(() => {
isMount.current = true;
return () => (isMOunt.current = false);
}, [isMount]);
};
내용
리렌더링 방지가 필요하다면 useState
대신 useRef
useRef이란
- 가변 컨테이너
- 한번 고정된 값을 컴포넌트 내부에서 사용할 경우
useState
로 사용할 필요가 없음(컴포넌트의 전체적인 수명과 동일하게 지속된 정보를 일관적으로 제공해야 하는 경우) - 꼭 DOM을 직접 조작할 때만
useRef
를 사용하는 것이 아님
요약
useState
대신useRef
를 사용하면 컴포넌트의 생명주기와 동일한 리렌더링되지 않는 상태를 만들 수 있다.
6. 연관된 상태 단순화하기
결론
// 기존
const [isLoading, setIsLoading] = useState(false);
const [isFinish, setIsFinish] = useState(false);
// 변경
const PROMISE_STATE = {
INIT: 'init',
LOADING; 'loading',
FINISH: 'finish'
};
const [promiseState, setPromiseState] = useState(PROMISE_STATE);
내용
-
React 는 개발하는데 있어 자유로움
-
여러 연관된 state를 만들어서 관리하는게 아니라, 하나의 불변의 값으로 관리
const PROMISE_STATE = { INIT: 'init', LOADING; 'loading', FINISH: 'finish' ERROR: 'error' }; const FlatState = () => { const [promiseState, setPromiseState] = useState(PROMISE_STATE); const fetchData = () => { // fetch Data 시도 setPromiseState(PROMISE_STATE.LOADING); fetch(url) .then(() => { // fetch Data 성공 setPromiseState(PROMISE_STATE.FINISH); }) .catch(() => { // fetch Data 실패 setPromiseState(PROMISE_STATE.ERROR); }) } if (promiseState === PROMISE_STATE.LOADING) return <LoadingComponent /> if (promiseState === PROMISE_STATE.FINISH) return <FinishComponent /> if (promiseState === PROMISE_STATE.ERROR) return <ErrorComponent /> }
요약
- 리액트의 상태를 만들 때
연관된 것들끼리 묶어서 처리
하면 에러를 방지하고 코드가 간결해진다.
7. useState -> useReducer로 리팩토링
결론
// 기존
const [isLoading, setIsLoading] = useState(false);
const [isFinish, setIsFinish] = useState(false);
// 변경
const [state, dispatch] = useReducer(reducer, INIT_STATE);
내용
- 구조화된 상태를 원한다면
useReducer()
const INIT_STATE = {
isLoading: false,
isSuccess: false,
isFail: false,
};
// 오타 방지 및 타입 정확성
const ACTION_TYPE = {
FETCH_LOADING: 'FETCH_LOADING',
FETCH_SUCCESS: 'FETCH_SUCCESS',
FETCH_FAIL: 'FETCH_FAIL',
};
// 다른 곳에서도 사용 가능
// 순수 JS로 Third Party library 없이 상태를 관리 가능
// 그 상태를 조금 더 체계적으로 구조화 가능
const reducer = (state, action) => {
// 보통 type을 쓰지만 action 객체의 형태는 자유
switch (action.type) {
case 'FETCH_LOADING':
return { isLoading: true, isSuccess: false, isFail: false };
case 'FETCH_SUCCESS':
return { isLoading: false, isSuccess: true, isFail: false };
case 'FETCH_FAIL':
return { isLoading: false, isSuccess: false, isFail: true };
default:
return INIT_STATE;
}
};
const StateToReducer = () => {
const [state, dispatch] = useReducer(reducer, INIT_STATE);
const fetchData = () => {
// fetch Data 시도
// - 추상화
dispatch({ type: ACTION_TYPE.FETCH_LOADING });
fetch(url)
.then(() => {
// fetch Data 성공
dispatch({ type: ACTION_TYPE.FETCH_SUCCESS });
})
.catch(() => {
// fetch Data 실패
dispatch({ type: ACTION_TYPE.FETCH_FAIL });
});
};
if (state.isLoading === PROMISE_STATE.LOADING) return <LoadingComponent />;
if (state.isSuccess === PROMISE_STATE.FNISH) return <FinishComponent />;
if (state.isFail === PROMISE_STATE.ERROR) return <ErrorComponent />;
};
요약
- 여러 상태가 연관됐을 때, useState 대신,
useReducer를 사용하면 상태를 구조화
할 수 있음
8. 상태 로직 Custom Hooks로 뽑아내기
### 결론
// 기존
const [state, setState] = useState();
useEffect(() => {
const fetchData = () => {
setState(state);
};
fetchDate();
}, []);
if (state.isLoading) return <LoadingComponent />;
if (state.isFail) return <FailComponent />;
// 변경
const { isLoading, isFail } = useFetchData();
if (state.isLoading) return <LoadingComponent />;
if (state.isFail) return <FailComponent />;
내용
- 로직만 뺌
const INIT_STATE = {
isLoading: false,
isSuccess: false,
isFail: false,
};
// 오타 방지 및 타입 정확성
const ACTION_TYPE = {
FETCH_LOADING: 'FETCH_LOADING',
FETCH_SUCCESS: 'FETCH_SUCCESS',
FETCH_FAIL: 'FETCH_FAIL',
}
// 다른 곳에서도 사용 가능
// 순수 JS로 Third Party library 없이 상태를 관리 가능
// 그 상태를 조금 더 체계적으로 구조화 가능
const reducer = (state, action) => {
// 보통 type을 쓰지만 action 객체의 형태는 자유
switch (action.type) {
case 'FETCH_LOADING':
return { isLoading: true, isSuccess: false, isFail: false }
case 'FETCH_SUCCESS':
return { isLoading: false, isSuccess: true, isFail: false }
case 'FETCH_FAIL':
return { isLoading: false, isSuccess: false, isFail: true }
default:
return INIT_STATE;
}
};
const useFetchData = (url) => {
const [state, dispatch] = useReducer(reducer, INIT_STATE);
useEffect(() => {
const fetchData = async () => {
// fetch Data 시도
// - 추상화
dispatch({ type: ACTION_TYPE.FETCH_LOADING });
await fetch(url)
.then(() => {
// fetch Data 성공
dispatch({ type: ACTION_TYPE.FETCH_SUCCESS });
})
.catch(() => {
// fetch Data 실패
dispatch({ type: ACTION_TYPE.FETCH_FAIL });
})
}
}, [url)
return state
}
const CustomHooks= () => {
const { isLoading, isFail, isSuccess } = useFetchData('url);
if (state.isLoading === PROMISE_STATE.LOADING) return <LoadingComponent />
if (state.isSuccess === PROMISE_STATE.FNISH) return <FinishComponent />
if (state.isFail === PROMISE_STATE.ERROR) return <ErrorComponent />
}
요약
Custom Hooks를 사용하면
코드를 확장성 있고 재사용 가능하게 작성할 수 있다.
9. 이전 상태 활용하기
결론
setAge(age + 1);
setAge((prevAge) => prevAge + 1);
내용
- 타이밍을 확실히 하기 위해서 이전 상태 값을 가지고 업데이트 진행(update function)
const PrevState = () => {
const [age, setAge] = useState(0);
const updateState = () => {
setAge((prevAge) => prevAge + 1);
};
};
요약
- updater function을 사용해 prev state를 고려하면 예상치 못한 결과를 예방할 수 있다.
✅ Props
1. 불필요한 PROPS 복사 및 연산
🌈 결론
// 변경 전
function component({ value }) {
const [copyValue] = useState(무거운_연산(value));
return <div>{copyValue}</div>;
}
// 변경 후
function component({ value }) {
const [copyValue] = useMemo(() => 무거운_연산(value), [value]);
return <div>{copyValue}</div>;
}
✍️ 내용
-
props
로 전달 받은 값을useState
에서 셋팅하는 것이 아닌, 바로 사용하는 것이 좋음 -
아래와 같이
props
로 전달 받은 값을 가지고 무거운 연산을 진행하면, 렌더링할 때마다 해당 컴포넌트가 호출되어서 연산을 지속적으로 하기 때문에 비효율적임 → 그래서 애초에props
로 전달하기 전에 이미 무거운 연산을 한 결과 값을props
로 전달을 해야 함, 아니면useMemo
를 사용function CopyProps({ value }) { const copyValue = 값_비싸고_무거운_연산(value); const [copyValue] = useMemo(() => 무거운_연산(value), [value]); return <div>{copyValue}</div>; }
⭐️ 요약
불필요한 연산을 줄이는 방법
Props
바로 사용하기(useState
담기 X, 무거운 연산의props
로 사용 X)- 연산된 값을
Props
로 넘기기 useMemo
로 연산 최적화하기
2. Curly Braces
🌈 결론
- 중괄호(Curly Braces) 사용법
<Image
alt='image'
src='image.jpg'
style={{ width: 100 }}
className='clean-dev'
/>
✍️ 내용
-
Curly Brace 사용 O
- 값이 계산되는 경우(논리적인 숫자, Boolean, 객체, 배열, 함수 표현식)
- 객체를 넣어야 하는 경우
-
Curly Brace 사용 X
-
문자열일 경우
<Image alt={'image'} src={'image.jpg'} style={{ width: 100 }} className='clean-dev' />
-
⭐️ 요약
- String일 경우 Curly Brace 사용하지 않기
3. Props 축약하기
🌈 결론
// 변경 전
function component(props) {
<HeaderComponent hasPadding={props.hasPadding}>
<ChildComponent isDarkMode={props.isDarkMode} isLogin={props.isLogin} />
</HeaderComponent>;
}
// 변경 후
function component({ hasPadding, ...props }) {
<HeaderComponent hasPadding>
<ChildComponent {...props} />
</HeaderComponent>;
}
✍️ 내용
ShortHand Props는언제 사용할까?
- 토글링 값을 Props로 전달 할 때
function component({ hasPadding, ...props }) {
<HeaderComponent hasPadding>
<ChildComponent {...props} />
</HeaderComponent>
⭐️ 요약
- ShortHand Props로 Props를 축약할 수 있다.
4. Single Quotes vs Double Quotes
🌈 결론
// ✅
<a href="https://www.naver.com">Naver</a>
// ❌
<input class='ccrc' type="button" value='Clean' />
// ❌
<Clean style={{ backgroundPosition: "left" }} />
✍️ 내용
- 팀에서 일반적인 규칙 ⇒ 일관성을 지키기 위함
- HTML과 JS 환경에서 사용하는 부분 구분
- HTML은 Double Quotes 주로 사용(HTML Attribute를 위한 값)
- JS은 Single Quotes 주로 사용(객체의 값) cf) JSX는 Single Quotes
- 결론적으로 규칙을 정하고 그 맥락을 파악하고 공유하자 ⇒ Lint, 포맷팅 도구(Prettier)에 위임하자
⭐️ 요약
- HTML, JS를 구분해서 Single Quotes와 Double Quotes를 결정하자
- 규칙은 팀끼리 정해서 자동 포맷팅 시키자
5. Props 네이밍
🌈 결론
// ❌
<ChildComponent
class="mt-0"
Clean="code"
clean_code="react"
otherComponent={OtherComponent}
isShow={true}
/>
// ✅
<ChildComponent
className="mt-0"
clean="code"
cleanCode="react"
OtherComponent={OtherComponent}
isShow
/>
✍️ 내용
- React Component는 파스칼로 한다.
⭐️ 요약
- class는
className
으로 사용하기 camel case
사용하기- 무조건 true라면
isShow={true}
가 아닌,isShow
로 축약하기 - 컴포넌트라면 대문자로 시작하기
6. 인라인 스타일 주의 하기
🌈 결론
// ❌
function InlineStyle(): Element {
return (
<button style="background-color: 'red'; font-size: '14px';">
Clean Code
</button>
);
}
// ✅
function InlineStyle(): Element {
const myStyle = { backgroundColor: 'red', fontSize: '14px' };
return <button style={myStyle}>Clean Code</button>;
}
✍️ 내용
-
JS로 HTML을 표현하는 문법이 바로 JSX임
-
고정된 스타일 객체 값이라면, 컴포넌트 외부로 빼는 것이 좋음(매번 랜더링 될 때마다 계속 평가되기 때문)
const myStyle = { backgroundColor: 'red', fontSize: '14px' }; function InlineStyle(): Element { return <button style={myStyle}>Clean Code</button>; }
⭐️ 요약
- JSX에서 인라인 스타일을 쓰려면 중괄호 안에
camelCase key
를 가진 객체를 넣어야 한다.
7. CSS-in-JS 인라인 스타일 지양하기
🌈 결론
// ❌
function InlineStyle(): Element {
return (
<button css={
css`
background-color: white;
border: 1px solid #eee;
border-radius: 0.5rem;
padding: 1rem;
`}
}>
Clean Code
</button>
);
}
// ✅
function InlineStyle(): Element {
return (
<button css={cardCss.self}>Clean Code</button>
);
}
✍️ 내용
- css 백틱으로 진행했을 때에는 VSCode 자동완성과 DX 측면에서 좋지 않기 때문에 JS 스타일을 주는 것이 좋음
- 아래와 같이 스타일을 외부 뺐을 때의 장점
- 외부로 분리했기 때문에 스타일이 렌더링 될 때마다 직렬화 되지 않는다. → 한번만 된다.
- 동적인 스타일을 실수로 건드는 확률이 적어진다.
- 스타일 관련 코드를 분리해서 로직에 집중하고 JSX를 볼 때 조금 더 간결하게 볼 수 있다.
// 장점
// - 타입 안정성
// - 자동 완성으로 생산성 DX 향상
// - export 할 경우, 외부 컴포넌트에서 사용 가능
const cardCSS = {
self: css({
backgroundColor: 'white',
border: '1px solid #eee',
borderRadius: '0.5rem',
padding: '1rem',
}),
title: css({
fontSize: '1.25rem',
}),
};
// CSS IN JS 인라인 스타일 지양하기
// - 성능에 민감함
export function Card({ title, children }) {
return (
<div css={cardCss.self}>
<h5 css={cardCss.title}>{title}</h5>
{children}
</div>
);
}
⭐️ 요약
- CSS in JS 인라인 스타일을 지양해야 하는 이유 -
성능 저하 발생 유발
,휴먼 에러가 발생 가능성 존재
,export 할 수 없음
8. 객체 Props 지양하기
✍️ 내용
-
변하지 않는 값일 경우 컴포넌트 외부로 드러내기
-
필요한 값만 객체를 분해해서 Props로 내려 준다.
-
정말 값 비싼 연산, 너무 잦은 연산이 있을 경우 useMemo() 활용하여 계산된 값을 메모이제이션 한다.
-
Props 값을 나누어서 다른 컴포넌트에 Props를 전달한다.
// ❌ function SomeComponent() { return ( <ChildComponent propObj={{ hello: 'world' }} propArr={['hello', 'hello']} /> ); } // ✅ 방법 1 function SomeComponent() { const [propArr, setPropArr] = useState(['hello', 'hello']); return <ChildComponent hello1='world' hello2={propArr.at(0)} />; } // ✅ 방법 2 function SomeComponent({ heavyState }) { const [propArr, setPropArr] = useState(['hello', 'hello']); const computedState = useMemo( () => ({ heavyState: heavyState, }), [heavyState] ); return ( <ChildComponent hello1='world' hello2={propArr.at(0)} computedState={computedState} /> ); }
React 리렌더링 되는 여부 판별
- React에서
Object.is
는 컴포넌트의 props나 state가 변경되었는지 판단하는 데 사용됩니다. 이 함수는 두 값이 동일한 메모리 주소를 참조하는지를 확인합니다.
- 리렌더링과
Object.is
- React는 props나 state의 변경 여부를
Object.is
로 판단합니다. - 객체나 배열은 참조 타입이므로, 새로 생성된 객체나 배열은 이전과 다른 것으로 간주됩니다(비록 내용이 같아도).
- React는 props나 state의 변경 여부를
- ❌ 컴포넌트의 문제점
{ hello: 'world'
}와['hello', 'hello']
는 컴포넌트가 렌더링될 때마다 새로 생성됩니다.- React는 이 새로 생성된 값들이 이전 값과 다르다고 판단하여 ChildComponent를 리렌더링합니다.
- ✅ 방법 1 코드의 최적화
- 상태로 관리된 propArr는 렌더링 간에 동일한 참조를 유지합니다.
- React는
Object.is
를 통해 props가 변하지 않았음을 인식하고, 컴포넌트의 불필요한 리렌더링을 방지합니다.
9. HTML Attribute 주의하기
🌈 결론
// ❌
function MyButton({ children, type }) {
return <button type={type}>{children}</button>;
}
// ✅
function MyButton({ children, ...rest }) {
return <button {...rest}>{children}</button>;
}
✍️ 내용
-
HTML 기본 속성 주의하기
- HTML와 JSX에서 사용하는 예약어 주의
- HTML 표준어 찾아서 주의(내가 만든 Component의 Props와 겹치지는지 확인)
function HTMLDefaultAttribute() { const MyButton = ({ children, ...rest }) => ( <button {...rest}>{children}</button> ); return ( <> <MyButton className='mt-0' type='submit'> Clean Code </MyButton> <MyButton type='number' maxLength='99'> Clean Code </MyButton> </> ); }
⭐️ 요약
- HTML, JS에서 정의한 예약어와 커스텀 컴포넌트 Props가 혼용되지 않도록 주의
10. Spread 연산자 쓸 때 주의할 점
🌈 결론
// ❌
const ParentComponent = (props) => {
return <childOrHOCComponent {...props} />;
};
// ✅
const ParentComponent = (props) => {
const { 관련_없는_props, 관련_있는_props, ...나머지_props } = props;
return (
<childOrHOCComponent 관련_있는_props={관련_있는_props} {...나머지_props} />
);
};
✍️ 내용
- 코드를 예측하기 어렵다.
⭐️ 요약
- props에서 spread 연산자가 쓰이면, 관련 있는 props, 없는 props, 나머지 props로 나눠보자
11. 많은 Props 분리하기
🌈 결론
// ❌
<JoinForm
user={user}
auth={auth}
location={location}
favorite={favorite}
handleSubmit={handleSubmit}
handleReset={handleReset}
handleCancel={handleCancel}
/>
// ✅
<JoinForm
handleSubmit={handleSubmit}
handleReset={handleReset}
handleCancel={handleCancel}
>
<CheckBoxForm formData={user} />
<CheckBoxForm formData={auth} />
<RadioButtonForm formData={location} />
<SectionForm formData={favorite} />
</JoinForm>
✍️ 내용
너무 많은 Props를 넘기는 경우
- 결과보다는 일단 실행 → 분리의 대상?
- TanStack Query, Form Library, 상태 관리자, Context API, Composition
리팩토링 과정
- One Depth 분리를 한다.
- 확장성을 위한 분리를 위해 도메인 로직을 다른 곳으로 모아넣는다.
- 꼭 라이브러리를 먼저 도입하는게 아니라, 먼저 분리 후 생각한다.
⭐️ 요약
- props가 많다면 컴포넌트를 분리해보자.
12. 단순하게 Props 내리기
🌈 결론
// ❌
const UserInfo = ({ user }) => {
return (
<div>
<img src={user.avatarImgUrl} />
<h3>{user.userName}</h3>
<h4>{user.email}</h4>
</div>
);
};
// ✅
const UserInfo = ({ avatarImgUrl, userName, email }) => {
return (
<div>
<img src={avatarImgUrl} />
<h3>{userName}</h3>
<h4>{email}</h4>
</div>
);
};
✍️ 내용
- ❌ 객체를 Props로 그대로 전달하는 경우
- UserInfo 렌더링될 때마다 user 객체가 새로 생성됨. React는 이 새로운 user 객체를 보고 UserInfo 컴포넌트에 전달된 props가 변경되었다고 인식하여 UserInfo를 리렌더링함
- ✅ 객체를 분리하여 전달하는 경우
- 문자열이나 숫자와 같은 기본형 데이터는 참조값이 아닌 실제 값을 비교하기 때문에, 값이 변하지 않는 한 UserInfo는 리렌더링되지 않음
- 참고) 객체나 배열 같은 참조형 데이터는 메모리 주소(참조값)가 변경되면 React가 새로운 데이터로 인식함. 따라서 객체가 새로 생성될 때마다 자식 컴포넌트는 리렌더링될 가능성이 큼
⭐️ 요약
- props에 객체 전체를 내리지 말고 꼭 필요한 값만 내리자
✅ Component
1. Components 정의
🌈 결론
- 컴포넌트가 무엇인지 정확하게 인지하고 사용해야 함
✍️ 내용
Component 사전적 의미: 구성하는, 구성하고 있는, 성분의 구성 요소, 성분
공식 문서에서 Component 의미
- 과거 컴포넌트 의미
- 스스로 상태를 관리하는 캡슐화된 컴포넌트
- 컴포넌트를 조합해 복잡한 UI 개발 가능
- 컴포넌트 로직은 템플릿이 아닌 JS로 작성
- 이로 인해 다양한 형식의 데이터를 앱 안에서 손쉽게 전달할 수 있고, DOM과 별개로 상태를 관리 가능
- 현재 컴포넌트 의미
- 기존에는 웹 페이지를 만들 때 웹 개발자가 컨텐츠를 마크업한 다음 JS를 뿌려서 상호작용을 추가하는 방향이었으며, 이는 웹에서 상호작용이 중요했던 시절에 효과이었음
- 이제는 많은 사이트와 모든 앱에서 상호작용을 기대하고 React는 동일한 기술을 사용하면서도 상호작용을 우선시 하며, React 컴포넌트는 마크업으로 뿌릴 수 있는 JS 함수 역할을 함
- 참고
⭐️ 요약
Component 역할
- 많은 사이트와 모든 앱에서 상호작용을 기대함
- React는 동일한 기술을 사용하면서도 상호작용을 우선시함
- React 컴포넌트는 마크업으로 뿌릴 수 있는 JS 함수 임
2. Self Closing Tags
🌈 결론
Self Closing Tags
를 정확히 인지하고 사용하자
✍️ 내용
-
Self Closing Tags
의미-
명시적으로 닫는 태그가 필요가 없음
-
기본 HTML 요소인지 아닌지 명확한 차이를 가져야 함
function HelloWorld() { return ( <Clean> <Code> <img /> <br /> </Code> </Clean> ); }
-
Vue
에서는 HTML에서 사용되어지는header
와 같은 태그 사용이 불가함 대신에app-header
이런식으로 사용해야 함
-
⭐️ 요약
Self Closing Tags
를 정확히 인지하고 사용하자
3. Fragment 지향하기
🌈 결론
Fragment
가 무엇인지 알고 쓰자.
✍️ 내용
- React v16.2 출시
Fragment
런타임시 Fragment는 사라짐- Babel 버젼에 따라서 Fragment Short Cut 사용 여부도 확인해야 됨
- index를 주입할 때, Short Cut이 아닌
Fragment
컴포넌트 사용해야 함function Example() { return ( <> <Child /> </> ); }
- 참고
⭐️ 요약
Fragment
가 필요한 경우에만 사용하자.
4. Fragment 지양하기
🌈 결론
- 상황에 따라 불필요한 Fragment를 줄이자.
✍️ 내용
- 불필요한 Fragment 사용을 줄이자.
// 불필요한 계층 줄이기
function Example() {
return (
<>
<div>
<div></div>
</div>
</>
);
}
function StringRender() {
// return <>'Clean Code'</> ❌
return 'Clean Code';
}
// 렌더링 될 필요 없는 JSX 줄이기
function ConditionalRenderingEX() {
return(
<div>
<h1>{isLoggedIn ? 'User' : <></>}</h1>
<h1>{isLoggedIn ? 'User' : null}</h1>
<h1>{isLoggedIn && 'User'}</h1>
{isLoggedIn && <h1>User</h1>
</dvi>
)
}
⭐️ 요약
- 불필요한 Fragment 사용을 줄이자.
5. 알아두면 좋은 컴포넌트 네이밍
🌈 결론
function ComponentNaming() {
return (
<>
<h1></h1> // 🤔 lowercase
<h2></h2>
<div></div>
<input />
<MyuComponent></MyComponent> // 🤔 pascal case
<my-component></my-component> // 🤔 kebab case
</>
)
}
✍️ 내용
컴포넌트 네이밍
- 일반적으로 컴포넌트
PascalCase
- 기본 HTML 요소는
lower case
- route based file name
component-naming.jsx
⇒<ComponentNaming />
component-naming/index.jsx
⇒<ComponentNaming />
⭐️ 요약
- 컴포넌트 네이밍 규칙을 이해하고 사용하자
6. JSX 컴포넌트 함수로 반환
🌈 결론
// 🤔 어떤 형태가 맞을까?
return (
<div>
{TopRender()}
<TopRender />
{renderMain()}
</div>
);
✍️ 내용
function ReturnJSXFunction() {
const TopRender = () => {
return (
<header>
<h1>Clean Code JS</h1>
</header>
);
};
const renderMain = () => {
return (
<main>
<p>Clean Code</p>
</main>
);
};
return (
<div>
{TopRender()}
{renderMain()}
</div>
);
}
JSX 컴포넌트 함수로 반환시 문제점
- 스코프가 꼬임
- 언제 어떻게 쓰일지 몰라서 위험
- 컴파일 과정에서 캐치 못하면 치명적인 오류 발생
- 리턴 값이 무엇인지 파악하기 어려움
- props 넣기가 힘듦
⭐️ 요약
- 함수로 return 하는 경우 다음과 같은 단점이 발생
- scope를 알아보기 어려움
- 반환 값을 바로 알기 어려움
- props 전달 등 일반적인 패턴이 아님
7. 컴포넌트 내부에 컴포넌트 선언
🌈 결론
// ❌
function OuterComponent() {
const InnerComponent = () => {
return <div>Inner component</div>;
};
return (
<div>
<InnerComponent />
</div>
);
}
// ✅
const InnerComponent = () => {
return <div>Inner component</div>;
};
function OuterComponent() {
return (
<div>
<InnerComponent />
</div>
);
}
✍️ 내용
컴포넌트 내부에 컴포넌트 선언시 문제점
- 결합도가 증가함
- 구조적으로 스코프적으로 종속된 개발이 됨
- 나중에 확장성이 생겨서 분리될 때 굉장히 힘듦
- 성능 저하
- 상위 컴포넌트 리렌더 일어나면 ⇒ 하위 컴포넌트 재 생성
⭐️ 요약
- 컴포넌트 내부에 컴포넌트를 선언하면 결합도가 증가하고 성능이 저하될 수 있다.
8. DisplayName
🌈 결론
- 확장성이 높은 컴포넌트를 디버깅하기 위해
displayName
을 잘 활용하자
✍️ 내용
DisplayName: 디버깅 하는데 좋은 요소
// Case 1
const InputText = forwardRef((props, ref)) => {
return <input type="text" ref={ref} />;
});
// 만약 🤔 displayName을 작성 안한다면?
InputText.displayName = 'InputText'
// Case 2
const withRouter = (Component) => {
const WithRouter = (props) => {
const location = useLocation();
const navigate = useNavigate();
const params = useParams();
const navigationType = useNavigationType();
return (
<Component
{...props}
location={location}
navigate={navigate}
params={params}
navigationType={navigationType}
/>
);
};
WithRouter.displayName = Component.displayName ?? Component.name ?? 'WithRouterComponent'
return WithRouter
};
-
displayName을 설정하지 않은 경우
- React Developer Tools:
<ForwardRef>
또는<Anonymous>
- 콘솔 에러 메시지: Warning: Failed prop type: The prop
value
is marked as required inForwardRef
, but its value isundefined
.
- React Developer Tools:
-
displayName을 설정한 경우
- React Developer Tools:
<InputText>
- 콘솔 에러 메시지: Warning: Failed prop type: The prop
value
is marked as required inInputText
, but its value isundefined
.
- React Developer Tools:
⭐️ 요약
- React 개발시 디버깅을 위해 displayName을 잘 활용하자.
9. Component 구성하기
🌈 결론
- 개발 진행 시, 어떤 순서 및 흐름으로 설계 하는 것은 중요함.
✍️ 내용
// ✅ 변하지 않은 값은 컴포넌트 외부로 빼기
const DEFAULT_COUNT = 100;
const DEFAULT_DELAY = 500;
// ✅ 타입 또는 인터페이스도 컴포넌트 밖으로 빼기
interface SomeComponentProps {}
// ✅ 컴포넌트와 관련없는 로직은 컴포넌트 외부로 빼기
const handleClose = () => {
// Date
// Local Storage
}
const SomeComponent = ({ prop1, prop2 }: SomeComponentProps) => {
// ✅ flag 또는 ref는 상단에 표시
let isHold = false;
const ref = useRef(null);
// ✅ React Third-Party 라이브러리의 훅을 사용시 상단에 표시
const location = useLocation();
const queryClient = useQueryClient();
const state = useSelector((state) => state);
// ✅ 내가 만든 Hooks을 상단에 표시
const state = useCustomHooks((state) => state);
// ✅ 컴포넌트 내부 상태를 상단에 표시
const [state, setState] = useState('someState");
const onClose = () => handleClose();
// ✅ Early Return JSX
if (isHold) {
return <div>데이터가 존재하지 않습니다.</div>
}
// ✅ useEffect 사용시, Main JSX와 가장 가까운 곳에 위치
// - 최소 1개로 사용할 수 있도록 진행
useEffect(() => {
}, []);
// ✅ JSX 반환은 항상 사전에 개행을 동반
return (
<div className="tooltip">
<div className="msg">Hello World</div>
<button
className="close"
type="button"
onClick={onClose}
/>
</div>
)
}
// ✅ 컴포넌트 외부로 빼기(컴포넌트 하단)
// - 코드가 많을 경우, 파일로 빼기
const Button = styled.a<{ $primary?: boolean; }>`
padding: 0.5rem 0;
transition: all 200ms ease-in-out;
width: 11rem;
&:hover {
filter: brightness(0.85);
}
`
export default SomeComponent;
⭐️ 요약
- 개발을 할 때 규칙을 가지고 개발을 진행하자.(컨벤션 설정)
✅ Rendering
1. JSX에서의 공백 처리
// ❌
export default function App() {
return (
<div>
Welcome Clean Code
<a href='clean-code-js'>Go Clean Code</a>
</div>
);
}
// ✅
export default function App() {
return (
<div>
Welcome Clean Code <a href='clean-code-js'>Go Clean Code</a>
</div>
);
}
- JSX에서 공백 처리는 가독성과 유지보수성을 위해 중요한데,
같은 HTML 엔터티는 복잡성을 증가시킴. - 대신 문자열 사이에 자연스럽게 공백을 넣는 것이 더 나은 방식.
- 만약 여러 줄에 걸친 텍스트에서 공백을 조정해야 한다면, CSS로
margin
이나padding
을 사용하는 것도 하나의 방법
2. '0'값은 JSX에서 유효한 값
🌈 결론
export default function App() {
const [items, setItems] = useState([]);
// ❌
// 만약 item.length가 0이면 아래 0 출력됨
return (
<div>
{item.length && item.map((item) => {
return <Item item={item} />;
})
</div>
)
// ✅
return (
<div>
{item.length === 0 && item.map((item) => {
return <Item item={item} />;
})
</div>
)
}
&&
연산자는 첫 번째 값이참
일 때만 두 번째 값을 실행함- 만약
거짓
이면 첫 번째 값이 출력 됨
✍️ 내용
- 참과 거짓으로 판단하는 것이 아니라,
렌더링 유무
로 판단. - JSX에서는 어떤 값은 유효한 값인지, 렌더링 하는 값인지 확인.
- 렌더링을 조건부로 할 때에는 명확한 조건 필요.
⭐️ 요약
- JSX에서 렌더링되는 값과 아닌 값을 구분하자
3. List 내부에서 Key
🌈 결론
export default function App({ list }) {
const handleAddItem = (value) => {
setItems((prev) => [
...prev,
{
id: crypto.randomUUID(),
value
},
]);
}
useEffect(() => {
// list만들 때! 꼭 ID를 부여하자
// 혹은 새로운 아이템을 추가하는 함수륾 만들 때 그 때 고유한 값을 넣어주자!
}, [])
// ❌
return (
<ul>
{list.map((item) => {
return <li>{item}</li>;
})
</div>
)
// ❌
return (
<ul>
{list.map((item, index) => {
return <li key={index}>{item}</li>;
})
</div>
)
// 🤔
return (
<ul>
{list.map((item, index) => {
return <li key={'card-item-' + index}>{item}</li>;
})
</div>
)
// ❌
// - state, props에 따라 렌더링이 될 때마다 key가 만들어짐
// - 그래서 유효한 값이 아님
return (
<ul>
{list.map((item, index) => {
return <li key={new Date().toString()}>{item}</li>;
})
</div>
)
// ❌
return (
<ul>
{list.map((item, index) => {
return <li key={uuidv4() onClick={() => handleDelete(uuidv4()}}>{item}</li>;
})
</div>
)
// ✅
return (
<ul>
{list.map((item) => {
return <li key={item.id}>{item}</li>;
})
</div>
)
}
✍️ 내용
- List 컴포넌트를 만들 때, key props를 넣어야 추후, 리렌더링시, DOM업데이트 여부 판단 가능
- 이 때, key는 고유한 값이여야 함(단, 동적으로 생성되는 즉, 렌더링마다 생성되는 key를 넣으면 안됨)
⭐️ 요약
- List 컴포넌트를 작성할 때, 고유한 key를 props로 전달하자.
4. 안전하게 Raw HTML 다루기
🌈 결론
React 에서 HTML과 악성 스크립트를 심을 수 있는 경우를 대비해 아래의 방법으로 대처하자.
✍️ 내용
React에서 Raw HTML을 다룰 때 XSS(악성 스크립트 공격)로부터 보호하는 것이 매우 중요. 이를 위해 다음과 같은 방법을 사용할 수 있음.
import DOMPurify from 'dompurify';
const SERVER_DATA = '<p>some raw html</p>';
function DangerouslySetInnerHTMLExample() {
const post = {
// 🔥 XSS(악성 스크립트 공격) 예시
content: `<img src="" onerror='alert("you were hacked")'>`,
};
// HTML 데이터의 정제를 위해 DOMPurify 사용
const sanitizeContent = { __html: DOMPurify.sanitize(SERVER_DATA) };
// ❌ XSS에 매우 취약한 방법
// return <div>{markup}</div>;
// ✅ DOMPurify로 정제한 데이터를 사용하는 방법 (안전)
return <div dangerouslySetInnerHTML={sanitizeContent} />;
}
유저가 수정할 수 있는 콘텐츠
유저가 입력하거나 수정할 수 있는 데이터(예: input, textarea)에서 악성 스크립트가 실행되는 것을 방지해야 함. 유저 입력 데이터를 처리할 때는 항상 정제 과정을 거치고, HTML 태그가 아니라 순수 문자열로 저장하도록 주의해야 함.
function UserContentInput() {
const [userContent, setUserContent] = useState('');
return (
<textarea
value={DOMPurify.sanitize(userContent)}
onChange={(e) => setUserContent(DOMPurify.sanitize(e.target.value))}
/>
);
}
사용 가능한 도구들
-
DOMPurify
- DOMPurify는 HTML을 정제해 XSS 공격을 방지하는 인기 있는 라이브러리이다. 사용법이 간단하고 다양한 옵션을 제공한다.
-
HTML Sanitizer API
- HTML Sanitizer API는 웹 표준으로 제안된 기능이며, HTML을 안전하게 정제할 수 있지만, 현재 지원하는 브라우저가 제한적이다. 실험적인 기능이므로 현재는 DOMPurify와 같은 라이브러리와 병행하여 사용하는 것이 좋다.
-
eslint-plugin-risxss
- ESLint 플러그인은 코드 작성 중에 발생할 수 있는 XSS 취약점을 감지하고 경고해주는 도구이다. 프로젝트의 보안을 강화할 수 있는 유용한 도구이다.
⭐️ 요약
- React에서 HTML을 다룰 때에는 조심해야 함
✅ Hooks
1. Hooks API
🌈 결론
- Hook이 왜 생겨났는지 알고 쓰자
✍️ 내용
1. Hooks API 도입 배경
1.1 기존 패턴들의 한계
-
React에서는
HOC(고차 컴포넌트)
,Render Props
, 그리고클래스 컴포넌트(SFC) 패턴
을 사용하여 컴포넌트 로직을 재사용하거나 상태 관리를 해왔습니다. 하지만 이러한 패턴들에는 몇 가지 문제점이 있었습니다. -
HOC
의 단점- 깊은 컴포넌트 트리(Wrapper Hell): 여러 HOC가 중첩되면 코드가 복잡해지고 디버깅이 어려워집니다.
- 코드 가독성 저하: 로직을 이해하기 어려워지며, 코드가 장황해질 수 있습니다.
-
Render Props
의 단점- Prop Drilling: 여러 컴포넌트에 걸쳐 props를 전달해야 할 때, 중첩된 구조로 인해 관리가 힘들어집니다.
- 가독성 문제: 함수 형태로 props를 전달하는 방식이 때때로 복잡하고 헷갈릴 수 있습니다.
-
클래스 컴포넌트
의 단점- 상태 관리의 복잡성: this 키워드를 사용해야 하는 불편함, 그리고 state와 lifecycle 메서드를 나눠서 관리하는 불편함이 있었습니다.
1.2. Hooks의 등장
- React 팀은 이러한 복잡함을 줄이고, 더 직관적인 상태 관리와 사이드 이펙트 관리 방법을 제공하기 위해 Hooks API를 도입했습니다.
2. 기존 패턴들과 Hooks 비교
2.1. HOC(Higher-Order Component)
-
기능: 컴포넌트를 감싸서 새로운 기능을 추가하는 패턴.
-
문제점: HOC의 중첩은 코드 가독성을 떨어뜨리고, 컴포넌트 계층이 깊어지면서 디버깅이 어려워집니다.
// HOC는 여전히 유용할 수 있지만, 중복 로직이 많고 트리 구조가 복잡해질 수 있습니다. const EnhancedComponent = higherOrderComponent(WrappedComponent); export default connect(mapStateToProps, mapDispatchToProps)(TodoApp);
2.2 Render Props
-
기능: 컴포넌트가 함수형 props를 통해 자식에게 렌더링 로직을 전달하는 패턴.
-
문제점: 중첩된 함수 호출이 많아지면 가독성이 떨어집니다.
<DataProvider render={(data) => <h1>Hello {data.target}</h1>} />
2.3. 클래스 컴포넌트와 SFC(Stateless Functional Component)
-
클래스 컴포넌트는 this를 관리해야 하고, state와 라이프사이클 메서드를 분리해서 사용해야 하는 복잡함이 있었습니다.
class ClassComponent extends React.Component { render() { return <div>{this.props.name}</div>; } }
-
SFC는 상대적으로 간단하지만, 상태 관리와 사이드 이펙트 관리에 한계가 있었습니다.
const StatelessComponent = (props) => <div>{props.name}</div>;
3. Hooks의 핵심 개념
3.1 useState
-
함수형 컴포넌트에서 상태를 관리할 수 있게 해줍니다.
const [state, setState] = useState(initialState);
3.2. useEffect
-
사이드 이펙트(예: 데이터 가져오기, DOM 업데이트)를 처리합니다. 클래스 컴포넌트의 componentDidMount, componentDidUpdate와 비슷한 역할을 합니다.
useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); // count가 변경될 때마다 실행
3.3. 그 외 주요 Hooks
useContext
: Context API와 함께 상태를 쉽게 공유.useReducer
: 복잡한 상태 로직을 간결하게 처리.useMemo
,useCallback
: 성능 최적화를 위한 메모이제이션 제공.
4. Hooks가 제공하는 이점
4.1. 코드 간결화
- 함수형 컴포넌트에서도 상태와 라이프사이클 관리가 가능하여, 클래스형 컴포넌트에서의 복잡한 코드가 많이 줄어듭니다.
4.2. 로직 재사용
-
HOC나 Render Props 없이도 여러 컴포넌트에서 로직을 쉽게 공유할 수 있습니다. 커스텀 Hook을 만들어 재사용할 수 있습니다.
function useCustomHook() { // 커스텀 로직 }
4.3. 더 나은 가독성
- 함수형 컴포넌트에 로직을 분리해서 작성할 수 있기 때문에, 코드 가독성이 높아집니다.
⭐️ 요약
- Hooks는 기존의 HOC, Render Props, 클래스 컴포넌트의 복잡성을 해결하기 위해 도입되었습니다.
- Hooks를 사용하면 코드가 간결해지고, 재사용 가능한 로직을 쉽게 만들 수 있으며, 함수형 컴포넌트에서 상태와 사이드 이펙트를 관리할 수 있습니다.
2. useEffect 기명함수와 함께 사용하기
🌈 결론
useEffect
에러 파악할 때, 기명함수 사용하면 파악하기 쉬움
✍️ 내용
1. 기명함수를 사용한 useEffect 이점
1.1 에러 디버깅의 용이성
useEffect
내에서 기명함수를 사용하면, 함수가 어디서 호출되는지 로그를 통해 쉽게 파악할 수 있습니다.console.log
,React DevTools
,모니터링 툴
등을 사용할 때, 익명 함수로 넘기면 어디서 에러가 발생했는지 파악하기 어렵지만, 기명함수를 사용하면 로그에 함수 이름이 찍혀 에러 위치를 빠르게 찾을 수 있습니다.
1.2 코드 가독성 및 유지보수 향상
- 기명함수를 사용하면, 함수의 역할과 의도를 함수명에서 직접적으로 알 수 있습니다. 이는 코드 리뷰 시에도 도움이 되고, 나중에 코드를 유지보수할 때 로직을 빠르게 이해하는 데 유리합니다.
1.3 코드 재사용성 증가
- 복잡한 로직을 가진 경우, 해당 로직을 기명함수로 분리하면 동일한 로직을 다른 곳에서도 재사용할 수 있습니다.
useEffect
안에서 로직이 여러 번 반복되기보다 함수로 분리하여 재사용할 수 있다는 점에서 코드 효율성을 높입니다.
2. 기명함수를 활용한 useEffect 예시
// 기명함수를 사용하여 로그 확인 및 유지보수 용이하게
useEffect(
function trackIsInView() {
// 'trackIsInView' 함수 내 로직 실행
console.log('Component in view logic executed.');
},
[isInView]
);
useEffect(
function handlePopState() {
if (navigationType === 'POP') {
console.log('POP navigation detected.');
// some logic for handling 'POP' navigation
}
},
[navigationType]
);
// 의존성이 없는 초기화 로직에 기명함수 사용
useEffect(function initializeComponent() {
console.log('Component initialized.');
// 초기화 관련 로직
}, []);
// 이벤트 핸들링 시, 기명함수로 이벤트 추가 및 제거
useEffect(function manageDocumentEvent() {
const handleEvent = () => {
console.log('Document event handled.');
// some logic
};
document.addEventListener('eventName', handleEvent);
return function removeDocumentEvent() {
document.removeEventListener('eventName', handleEvent);
console.log('Document event listener removed.');
};
}, []);
3. 보충 설명
3.1 함수명 선택
- 함수명은 해당 로직이 무엇을 하는지 명확하게 설명할 수 있어야 합니다. 위 예시에서
trackIsInView
는 컴포넌트가 보이는지 여부를 추적하는 역할을 직관적으로 나타냅니다.handlePopState
는 브라우저 히스토리 변경 시 POP 상태를 처리하는 함수로 명명되어 코드의 의도를 잘 전달합니다.
3.2 의존성 배열 관리
- 기명함수를 사용하더라도,
useEffect
에서 의존성 배열을 적절하게 관리하는 것이 중요합니다. 의존성 배열에 들어가는 값에 따라useEffect
가 언제 재실행되는지 결정되므로, 필요한 값만 의존성 배열에 넣도록 주의해야 합니다.
3.3 clean up
함수
- 이벤트 핸들링처럼
useEffect
안에서 이벤트 리스너를 추가하는 경우,clean-up
함수를 제공해야 합니다. 기명함수를 사용하면clean-up
함수도 명확하게 이름을 지어 관리할 수 있어 코드의 명확성을 높입니다.
⭐️ 요약
useEffect
에서 기명함수를 사용하면 에러 파악이 쉬워지고, 코드 가독성과 유지보수성이 향상됩니다.- 기명함수는 로직의 의도를 명확히 하고, 디버깅 시에 로그에서 함수명을 통해 에러 위치를 쉽게 찾을 수 있습니다.
clean-up
도 명확하게 함수명으로 관리하면 코드 흐름을 쉽게 이해할 수 있습니다.
3. 한 가지 역할만 수행하는 useEffect
🌈 결론
useEffect
는 하나의 역할만 수행하도록 작성하자. 이를 통해 코드의 가독성 및 유지보수성을 높일 수 있다.
✍️ 내용
-
SRP (단일책임 원칙): 하나의 함수나 컴포넌트가 여러 가지 책임을 가지면 유지보수하기 어려워진다. 이 원칙을
useEffect
에 적용하면, 각useEffect
가 하나의 역할만 수행하게 작성해야 한다. -
확인 방법:
- 기명 함수 사용:
useEffect
안에 들어가는 동작이 명확하지 않다면, 해당 로직을 기명 함수로 분리해서 작성해보면 역할이 분명해진다. - 의존성 배열 검토: 의존성 배열에 너무 많은 값이 들어가 있다면,
useEffect
가 여러 가지 역할을 수행하고 있는지 다시 한 번 검토해야 한다.
- 기명 함수 사용:
잘못된 예시
function LoginPage({ token, newPath }) {
useEffect(() => {
redirect(newPath);
const userInfo = setLogin(token);
// 로그인 로직...
}, [token, newPath]);
}
- 위 코드에서는
useEffect
가 두 가지 작업을 동시에 수행합니다- 경로를 리다이렉트하는 작업 (
redirect(newPath)
) - 로그인 로직을 처리하는 작업 (
setLogin(token)
)
- 경로를 리다이렉트하는 작업 (
- 이처럼 한 가지 이상의 작업을
useEffect
에서 처리하면, 코드가 복잡해지고 버그를 유발할 가능성이 높아집니다.
올바른 예시 (분리된 useEffect)
function LoginPage({ token, newPath, options }) {
// 경로 변경 처리
useEffect(() => {
redirect(newPath);
}, [newPath]);
// 로그인 처리
useEffect(() => {
const userInfo = setLogin(token);
// 로그인 후 로직...
if (options) {
// 추가 동작 (부가적인 로직)
}
}, [token, options]);
}
- 위 코드에서는
useEffect
가 각자의 역할을 담당합니다- 첫 번째 u
seEffect
: 경로 변경에만 집중. - 두 번째
useEffect
: 로그인 처리 및 추가 옵션 적용에만 집중.
- 첫 번째 u
- 이렇게 하면 각
useEffect
가 단일 책임을 가지므로 가독성이 좋아지고, 유지보수가 훨씬 쉬워집니다.
보충해야 할 부분
- 부가적인 로직 처리:
useEffect
내에서 추가 동작이 있을 경우에도, 그 동작이 부작용을 일으키지 않는지 확인해야 합니다. 이를 위해서 조건부 로직을 간결하게 처리하는 것이 중요합니다. - 의존성 배열 최적화: 불필요한 의존성을 줄이고, 꼭 필요한 의존성만 추가해야
useEffect
가 불필요하게 다시 실행되지 않습니다. - 기명 함수의 사용 이유 명확화: 함수를 분리하는 이유는 가독성 향상뿐만 아니라 로직의 재사용성이나 테스트 용이성도 포함됩니다.
⭐️ 요약
useEffect
를 사용할 때, 한 가지 역할만 할 수 있도록 작성하자.
4. Custom Hook 반환의 종류
🌈 결론
- React에서 제공하는 컨벤션에 맞게
Custom Hook
을 사용하자. - 올바른 네이밍과 구조를 통해
Custom Hook
의 반환값을 잘 정리하고, 협업 및 유지보수에 용이하게 하자.
✍️ 내용
Custom Hook 사용시 지켜야 할 규칙들
1. 반환값 순서 지키기
-
Custom Hook
에서 배열을 반환할 때,value
가 먼저 오고,setter
함수가 그 뒤에 오는 것이 컨벤션입니다.function ReturnCustomHooks() { // ❌ 잘못된 순서로 반환 const [setValue, value] = useSomeHooks(true); // ✅ 올바른 순서로 반환 const [value, setValue] = useSomeHooks(true); }
2. 불필요한 배열 비구조화 할당 피하기
-
Custom Hook
이 단일 값을 반환할 때는, 굳이 배열 비구조화 할당을 사용할 필요가 없습니다. 단일 값을 바로 할당하는 것이 더 간결하고 명확합니다.function ReturnCustomHooks() { // ❌ 불필요한 배열 비구조화 할당 const [oneValue] = useSomeHooks(); // ✅ 단일 값 직접 할당 const oneValue = useSomeHooks(); }
3. 비구조화 할당 시 변수명 명확히 하기
-
불필요한 값을 무시하거나 네이밍을 엉성하게 할 경우 코드 가독성이 떨어집니다. 특히, 여러 개의 값을 반환하는 경우는 객체 형태로 반환하는 것이 좋습니다.
function ReturnCustomHooks() { // ❌ 불필요한 언더스코어로 변수 무시 const [firstValue, secondValue, _, thirdValue] = useSomeHooks(true); // ✅ 객체 형태로 반환 const { firstValue, secondValue, rest } = useSomeHooks(true); }
4. Query 함수와 같은 Hook 사용 시 비구조화 할당 적극 활용하기
-
예를 들어,
React Query
와 같은 라이브러리의 훅에서 반환된 값들을 여러 줄로 나누는 것보다, 비구조화 할당을 통해 한 줄로 처리하는 것이 훨씬 깔끔하고 효율적입니다.function ReturnCustomHooks() { // ❌ 여러 줄로 나누어 할당 const query = useQuery({ queryKey: ['hello'], queryFn: getHello }); const data = query.data; const refetch = query.refetch; const isSuccess = query.isSuccess; // ✅ 비구조화 할당으로 간결하게 처리 const { data, refetch, isSuccess } = useQuery({ queryKey: ['hello'], queryFn: getHello, }); }
⭐️ 요약
- 배열 반환 시 순서 유지: value가 먼저, setter 함수가 뒤에 오도록 하자.
- 단일 값은 배열 대신 직접 할당: 불필요한 배열 비구조화 할당을 피하자.
- 여러 값 반환 시 객체 사용: 필요 없는 값을 무시하기보다는 객체 형태로 반환해 가독성 유지 하자
- 비구조화 할당 적극 활용: query 함수 등 여러 값을 반환하는 훅에서는 비구조화 할당으로 코드를 간결하게 하자.
5. useEffect 내부의 비동기 함수
🌈 결론
useEffect
내부에서는 비동기 함수를 직접 실행하는 것이 아닌, 비동기 함수를 명시적으로 선언하고 호출하는 방식으로 처리하는 것이 좋다.
✍️ 내용
1. useEffect
내부에서 비동기 함수 처리
-
useEffect
는 비동기 함수를 리턴하지 못한다. 즉,useEffect
에서 바로async
키워드를 사용할 수 없기 때문에, 비동기 함수 호출을 적절히 처리하는 방법을 이해하는 것이 중요하다.// ❌ 잘못된 예시 useEffect(async () => { const result = await fetchData(); console.log(result); }, []);
- 위와 같이
useEffect
에 바로async
함수를 사용할 수 없는데, 이는useEffect
가 반환하는 값은 클린업 함수(clean-up function)이어야 하거나 아무것도 반환하지 않아야 하기 때문이다. 하지만async
함수는 항상Promise
를 반환하기 때문에 문제가 발생한다
- 위와 같이
2. 비동기 함수 선언 후 호출하기
-
useEffect
내부에서는async
함수를 따로 선언하고 그 함수를 호출하는 방식으로 비동기 처리를 할 수 있다.// ✅ 올바른 예시 useEffect(() => { const fetchData = async () => { try { const result = await someFetch(); console.log(result); } catch (error) { console.error('Error fetching data:', error); } }; fetchData(); }, []);
- 핵심 포인트:
useEffect
안에서 비동기 처리를 위해서는async
함수를 선언하고 그 함수 안에서await
처리를 해야 한다. 이렇게 하면useEffect
는Promise
를 반환하지 않고, 내부에서 비동기 로직이 적절히 처리된다. - 에러 핸들링: 비동기 함수 내에서 에러가 발생할 수 있으므로,
try-catch
블록을 활용해 에러 처리를 해주는 것이 중요하다.
- 핵심 포인트:
3. 클린업 함수와 비동기 처리
-
만약 비동기 작업이 컴포넌트가 언마운트되거나 의존성 배열의 값이 변경될 때 중단되어야 한다면, 클린업 함수를 사용해 이를 처리할 수 있다.
useEffect(() => { let isCancelled = false; const fetchData = async () => { try { const result = await someFetch(); if (!isCancelled) { console.log(result); } } catch (error) { if (!isCancelled) { console.error('Error fetching data:', error); } } }; fetchData(); return () => { isCancelled = true; }; }, []);
- 클린업 함수 사용:
useEffect
에서 반환하는 함수는 컴포넌트가 언마운트되거나, 의존성이 변경될 때 호출된다. 이 클린업 함수에서isCancelled
변수를 사용해 비동기 작업이 완료되지 않았을 때의 상태를 처리할 수 있다.
- 클린업 함수 사용:
4. 비동기 작업을 커스텀 훅으로 분리
-
복잡한 비동기 작업이 여러 곳에서 반복해서 사용된다면, 이를 커스텀 훅으로 분리하는 것도 좋은 방법이다. 이렇게 하면 코드의 재사용성과 가독성을 높일 수 있다.
const useFetchData = () => { const [data, setData] = useState(null); const [error, setError] = useState(null); useEffect(() => { const fetchData = async () => { try { const result = await someFetch(); setData(result); } catch (error) { setError(error); } }; fetchData(); }, []); return { data, error }; };
- 커스텀 훅 사용: 비동기 작업 로직을 커스텀 훅으로 분리하면, 해당 로직을 여러 컴포넌트에서 쉽게 재사용할 수 있으며, 코드의 가독성도 향상된다.
⭐️ 요약
useEffect
내부에서 바로 비동기 함수를 사용하는 대신,async
함수는 따로 선언한 후 호출하는 방식으로 처리한다.- 비동기 작업 중 컴포넌트가 언마운트되거나 의존성 배열의 값이 변경되면 중단될 수 있도록 클린업 함수를 사용해 취소 처리할 수 있다.
- 복잡한 비동기 작업은 커스텀 훅으로 분리해 재사용성과 가독성을 높이자.
✅ 기타 내용들
1. Import React
🌈 결론
- React v17 이상부터는
import React from 'react';
구문을 작성하지 않아도 됩니다.
✍️ 내용
- React 17 버전 이상에서는 JSX가 내부적으로 컴파일되므로, 컴포넌트 파일마다
import React
구문을 명시하지 않아도 정상 작동합니다. - React는 자동으로 JSX를 컴파일하게 되어, 불필요한 코드 작성을 줄여줍니다.
⭐️ 요약
- React 17 이상 버전에서는
import React
를 생략할 수 있습니다. 프로젝트에서 사용하는 React 버전을 확인하고, 필요시 최신 버전으로 업데이트하는 것도 좋은 방법입니다.
2. 디렉터리 구조
🌈 결론
- 다양한 개발자와 협업하기 위해서는
일관성 있는 디렉터리 구조
를 설계하는 것이 중요합니다. - 결합도가 높은 컴포넌트들은
같은 폴더 내에 배치
하는 것이 좋습니다.
✍️ 내용
-
정답은 없지만, 일관성 유지가 핵심
- 디렉터리 구조에 정해진 정답은 없으나, 의존성이나 결합도를 고려한 구조를 유지하는 것이 좋습니다.
-
기본 컴포넌트 구성 예시
-
❌ 안 좋은 예시
components/ |- MyButton.tsx |- ViewTable.tsx |- Icon.tsx
-
✅ 좋은 예시
components/ |- BaseButton.tsx |- BaseTable.tsx |- BaseIcon.tsx
- 결합도가 높은 컴포넌트를 묶어주는 것이 좋습니다.
-
❌ 안 좋은 예시
components/ |- TodoList.tsx |- TodoItem.tsx |- TodoButton.tsx
-
✅ 좋은 예시
components/ |- TodoList.tsx |- TodoListItem.tsx |- TodoListItemButton.tsx
- 관심사를 분리하여 디렉터리 구조를 구성합니다.
-
❌ 안 좋은 예시
components/ |- ClearSearchButton.tsx |- ExcludeFromSearchInput.tsx |- RunSearchButton.tsx |- SearchInput.tsx |- TermsCheckbox.tsx
-
✅ 좋은 예시
components/ |- SearchButtonClear.tsx |- SearchButtonRun.tsx |- SearchInputQuery.tsx |- SearchInputExcludeGlob.tsx |- SettingsCheckboxTerms.tsx |- SettingsCheckboxLaunchOnStartup.tsx
-
폴더 구조 제안
// 제안 1 components/ |- @shared |-- 공통 컴포넌트 |- Todo |-- Todo.tsx |-- Todo.hook.ts |-- todo.css // 제안 2 hooks/ |- useTodo.ts styles/ |- todo.css components/ |- Todo.tsx
⭐️ 요약
- 컴포넌트들의 결합도와 관심사에 따라 디렉터리 구조를 세우면 협업이 수월해지고 유지보수하기 쉬워집니다. 같은 팀원들과 일관된 방식을 유지하는 것이 가장 중요합니다.
3. SPA에서의 새로고침
🌈 결론
- SPA에서
window.location.reload()
사용을 주의하자
✍️ 내용
1. window.location.reload()의 동작 방식
window.location.reload()
는 브라우저에서 현재 페이지를 강제로 새로 고침하는 함수입니다. 이 함수는 브라우저가 페이지의 HTML, CSS, JS 리소스를 모두 다시 요청하고 새로 로드하게 만듭니다.- 이는 전통적인 서버 기반 웹 애플리케이션에서는 문제가 없지만, SPA에서는 비효율적일 수 있습니다.
문제점
- SPA (Single Page Application)에서는 JS를 기반으로 페이지의 상태를 관리하고 마크업과 인터렉션을 처리합니다.
window.location.reload()
를 실행하게 되면, SPA가 이미 로드한 리소스를 다시 불필요하게 요청하게 됩니다. 특히 다음과 같은 문제들이 발생할 수 있습니다불필요한 리소스 재로드
: SPA의 JS 파일들이 브라우저 캐시에 있더라도, 새로고침이 발생하면 JS 파일을 다시 요청하게 되며, 이는 네트워크 트래픽과 성능에 영향을 줍니다.상태 초기화
: 새로고침이 발생하면 SPA의 클라이언트 사이드에서 관리하는 상태들이 모두 초기화됩니다. 예를 들어, 로그인 상태나 폼 입력값, 로컬 상태 등이 모두 사라지고 처음부터 다시 로드됩니다.
2. SPA의 리소스 관리 방식
- SPA는 첫 로드 시 HTML, CSS, JS 파일을 받아오고 이후 페이지의 전환은 클라이언트 사이드에서 처리됩니다. 즉, 새로고침 없이도 필요한 데이터와 화면을 AJAX 요청이나 라우팅 라이브러리를 사용해 불러옵니다.
- SPA의 장점
빠른 화면 전환
: 서버에서 완전히 새로운 HTML을 받지 않고, 클라이언트에서 JS가 필요한 부분만 변경해주기 때문에 화면 전환이 매우 빠릅니다.리소스 절약
: 페이지가 전환될 때마다 모든 리소스를 다시 받지 않고 필요한 데이터만 주고받음으로써 리소스를 절약할 수 있습니다.
3. window.location.reload() 대신 사용할 수 있는 방법
- 상태 관리 라이브러리 사용 (예: Redux, Recoil, Context API 등)
- SPA에서 상태가 변경되었을 때 새로고침을 사용하지 않고 상태를 갱신하는 것이 바람직합니다. 상태 관리 라이브러리를 사용하면 로그인 상태, 유저 정보 등 필요한 데이터를 전역에서 관리할 수 있습니다.
- 예를 들어 로그인 성공 후, 전역 상태를 업데이트하여 다른 컴포넌트에서도 자동으로 로그인 상태가 반영되도록 처리할 수 있습니다.
- 컴포넌트 리렌더링
-
특정 상태가 변경된 후 해당 상태를 기준으로 컴포넌트가 다시 렌더링되도록 하는 방법도 있습니다. React에서는
useState
나useEffect
훅을 사용해 상태 변화에 따라 UI가 자동으로 업데이트되도록 할 수 있습니다.const [isLoggedIn, setIsLoggedIn] = useState(false); useEffect(() => { if (isLoggedIn) { // 로그인 성공 시 필요한 데이터를 새로 불러오거나 UI를 업데이트 console.log('로그인 성공!'); } }, [isLoggedIn]);
- 라우터를 이용한 페이지 이동
-
새로고침 없이 다른 화면으로 이동해야 할 때는
window.location.reload()
대신 SPA의 라우터 라이브러리(예: React Router)를 사용하여 경로를 변경할 수 있습니다. 이렇게 하면 페이지 전체를 다시 로드하지 않고 필요한 부분만 바뀌게 됩니다.import { useNavigate } from 'react-router-dom'; const handleLogin = () => { if (isSuccess) { setIsLoggedIn(true); // 로그인 후 새로운 페이지로 이동 navigate('/home'); } };
⭐️ 요약
- SPA에서
window.location.reload()
를 사용하는 것은 비효율적이며, 불필요한 리소스 재요청과 상태 초기화를 발생시킵니다. 대신, 상태 관리나 라우터를 활용하여 상태 변화와 화면 전환을 처리하는 것이 더 나은 방법입니다. 이를 통해 SPA의 장점을 최대한 활용할 수 있습니다.
4. Primitive UI
🌈 결론
- Primitive UI는 시멘틱한 HTML 표준 태그와 컴포넌트를 사용해 UI를 구성하는 방식을 의미하며, 이는 사용자 경험을 향상시키고 유지보수를 용이하게 합니다. React와 같은 자유도가 높은 라이브러리에서도 시멘틱한 구조를 유지할 수 있도록 Base 컴포넌트를 활용해 디자인 시스템과 유사한 패턴을 채택할 수 있습니다.
✍️ 내용
1.HTML 표준 태그의 중요성
- 웹 접근성(Accessibility)과 검색 엔진 최적화(SEO)를 고려할 때, 표준 HTML 태그를 사용하는 것이 매우 중요합니다.
- 표준 태그는 사용자 에이전트(브라우저, 스크린 리더 등)가 웹 페이지의 구조를 더 쉽게 이해할 수 있도록 도와줍니다.
2.시멘틱한 태그 사용 예시
- 비시멘틱한 태그 대신
header, footer, section
과 같은 HTML 표준 시멘틱 태그를 사용하는 것이 좋습니다.
3.React에서의 자유도와 시멘틱 태그
- React는 컴포넌트 단위로 동작하는 만큼 자유도가 매우 높아, 시멘틱한 HTML 태그 대신 컴포넌트 자체만으로 구성되는 경우가 많습니다.
- 이런 경우, 디자인 시스템과 비슷한 패턴을 적용해 Base 컴포넌트를 시멘틱하게 구성할 수 있습니다.
4.Base 컴포넌트를 사용한 시멘틱 확장
// ❌ 비시멘틱한 코드
<TodoList />
<TodoItem />
// ✅ 시멘틱한 Base 컴포넌트로 리팩토링한 코드
<List />
<Item />
- 위 예시에서는
TodoList
와TodoItem
같은 도메인 네임보다는, 시멘틱하고 확장 가능한List
,Item
등의 Primitive 컴포넌트를 사용하는 것이 더 적합합니다.
5.Primitive UI의 의미와 특징
- Primitive UI는 시멘틱한 기본 컴포넌트를 이용해 구성 요소를 묘사하는 방식입니다.
- 예를 들어, Radix UI, Chakra UI 같은 라이브러리에서는 UI 컴포넌트를 생김새와 시멘틱 구조로 나눠서 묘사합니다.
Box
,Circle
,List
,Square
등 형태나 레이아웃을 묘사하는 컴포넌트들이 대표적인 예입니다.
6.도메인 네임 대신 시멘틱 컴포넌트 사용
-
컴포넌트 이름을 특정 도메인(업무 목적)에 맞추기보다는 UI의 생김새와 역할을 시멘틱하게 묘사하는 것이 유지보수와 재사용성에 유리합니다.
// ✅ 생김새와 역할을 묘사한 예시 <Box /> <Circle /> <List /> <Square />
⭐️ 요약
- 디자인 시스템과 컴포넌트 라이브러리를 활용해 시멘틱한 Base 컴포넌트를 정의하고, 이를 확장하는 방식은 UI의 일관성을 유지하는 데 매우 유리합니다.
- 웹 접근성을 고려해, 사용자에게 더 직관적인 태그 구조를 제공하는 것이 좋습니다.