Clean React 학습 정리

Clean React 학습한 내용을 정리했습니다.

✅ 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);

 내용

불필요한 상태를 만든다면?

  • 결국에는 리액트에 의해 관리되는 값이 늘어나는 것
  • 그러다보면 렌더링에 양향을 주는 값이 늘어나서 관리 포인트가 더더욱 늘어 남

컴포넌트 내부에서의 변수는?

  • 렌더링 마다 고유의 값을 가지는 계산된 값

요약

  1. props를 useState에 넣지 않고 바로 return 문에 사용하기
  2. 컴포넌트 내부 변수는 렌더링마다 고유한 값을 가짐
  3. 따라서 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'
/>

✍️ 내용

  1. Curly Brace 사용 O

    • 값이 계산되는 경우(논리적인 숫자, Boolean, 객체, 배열, 함수 표현식)
    • 객체를 넣어야 하는 경우
  2. 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가 변경되었는지 판단하는 데 사용됩니다. 이 함수는 두 값이 동일한 메모리 주소를 참조하는지를 확인합니다.
  1. 리렌더링과 Object.is
    • React는 props나 state의 변경 여부를 Object.is로 판단합니다.
    • 객체나 배열은 참조 타입이므로, 새로 생성된 객체나 배열은 이전과 다른 것으로 간주됩니다(비록 내용이 같아도).
  2. ❌ 컴포넌트의 문제점
    • { hello: 'world' }와 ['hello', 'hello']는 컴포넌트가 렌더링될 때마다 새로 생성됩니다.
    • React는 이 새로 생성된 값들이 이전 값과 다르다고 판단하여 ChildComponent를 리렌더링합니다.
  3. ✅ 방법 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>;
}

✍️ 내용

  1. 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

리팩토링 과정

  1. One Depth 분리를 한다.
  2. 확장성을 위한 분리를 위해 도메인 로직을 다른 곳으로 모아넣는다.
  3. 꼭 라이브러리를 먼저 도입하는게 아니라, 먼저 분리 후 생각한다.

⭐️ 요약

  • 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>
  );
};

✍️ 내용

  1. ❌ 객체를 Props로 그대로 전달하는 경우
  • UserInfo 렌더링될 때마다 user 객체가 새로 생성됨. React는 이 새로운 user 객체를 보고 UserInfo 컴포넌트에 전달된 props가 변경되었다고 인식하여 UserInfo를 리렌더링함
  1. ✅ 객체를 분리하여 전달하는 경우
  • 문자열이나 숫자와 같은 기본형 데이터는 참조값이 아닌 실제 값을 비교하기 때문에, 값이 변하지 않는 한 UserInfo는 리렌더링되지 않음
  • 참고) 객체나 배열 같은 참조형 데이터는 메모리 주소(참조값)가 변경되면 React가 새로운 데이터로 인식함. 따라서 객체가 새로 생성될 때마다 자식 컴포넌트는 리렌더링될 가능성이 큼

⭐️ 요약

  • props에 객체 전체를 내리지 말고 꼭 필요한 값만 내리자

✅ Component

1. Components 정의

🌈 결론

  • 컴포넌트가 무엇인지 정확하게 인지하고 사용해야 함

✍️ 내용

Component 사전적 의미: 구성하는, 구성하고 있는, 성분의 구성 요소, 성분

공식 문서에서 Component 의미

  1. 과거 컴포넌트 의미
    • 스스로 상태를 관리하는 캡슐화된 컴포넌트
    • 컴포넌트를 조합해 복잡한 UI 개발 가능
    • 컴포넌트 로직은 템플릿이 아닌 JS로 작성
    • 이로 인해 다양한 형식의 데이터를 앱 안에서 손쉽게 전달할 수 있고, DOM과 별개로 상태를 관리 가능
  2. 현재 컴포넌트 의미
    • 기존에는 웹 페이지를 만들 때 웹 개발자가 컨텐츠를 마크업한 다음 JS를 뿌려서 상호작용을 추가하는 방향이었으며, 이는 웹에서 상호작용이 중요했던 시절에 효과이었음
    • 이제는 많은 사이트와 모든 앱에서 상호작용을 기대하고 React는 동일한 기술을 사용하면서도 상호작용을 우선시 하며, React 컴포넌트는 마크업으로 뿌릴 수 있는 JS 함수 역할을 함
    • 참고

⭐️ 요약

Component 역할

  • 많은 사이트와 모든 앱에서 상호작용을 기대함
  • React는 동일한 기술을 사용하면서도 상호작용을 우선시함
  • React 컴포넌트는 마크업으로 뿌릴 수 있는 JS 함수 임


2. Self Closing Tags

🌈 결론

  • Self Closing Tags를 정확히 인지하고 사용하자

✍️ 내용

  1. 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>
  );
}

✍️ 내용

컴포넌트 내부에 컴포넌트 선언시 문제점

  1. 결합도가 증가함
    • 구조적으로 스코프적으로 종속된 개발이 됨
    • 나중에 확장성이 생겨서 분리될 때 굉장히 힘듦
  2. 성능 저하
    • 상위 컴포넌트 리렌더 일어나면 ⇒ 하위 컴포넌트 재 생성

⭐️ 요약

  • 컴포넌트 내부에 컴포넌트를 선언하면 결합도가 증가하고 성능이 저하될 수 있다.


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
};
 
  1. displayName을 설정하지 않은 경우

    • React Developer Tools: <ForwardRef> 또는 <Anonymous>
    • 콘솔 에러 메시지: Warning: Failed prop type: The prop value is marked as required in ForwardRef, but its value is undefined.
  2. displayName을 설정한 경우

    • React Developer Tools: <InputText>
    • 콘솔 에러 메시지: Warning: Failed prop type: The prop value is marked as required in InputText, but its value is undefined.

⭐️ 요약

  • 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&nbsp;
      <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에서 공백 처리는 가독성과 유지보수성을 위해 중요한데, &nbsp; 같은 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))}
    />
  );
}

사용 가능한 도구들

  1. DOMPurify

    • DOMPurify는 HTML을 정제해 XSS 공격을 방지하는 인기 있는 라이브러리이다. 사용법이 간단하고 다양한 옵션을 제공한다.
  2. HTML Sanitizer API

    • HTML Sanitizer API는 웹 표준으로 제안된 기능이며, HTML을 안전하게 정제할 수 있지만, 현재 지원하는 브라우저가 제한적이다. 실험적인 기능이므로 현재는 DOMPurify와 같은 라이브러리와 병행하여 사용하는 것이 좋다.
  3. 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가 하나의 역할만 수행하게 작성해야 한다.

  • 확인 방법:

    1. 기명 함수 사용: useEffect 안에 들어가는 동작이 명확하지 않다면, 해당 로직을 기명 함수로 분리해서 작성해보면 역할이 분명해진다.
    2. 의존성 배열 검토: 의존성 배열에 너무 많은 값이 들어가 있다면, useEffect가 여러 가지 역할을 수행하고 있는지 다시 한 번 검토해야 한다.

잘못된 예시

function LoginPage({ token, newPath }) {
  useEffect(() => {
    redirect(newPath);
 
    const userInfo = setLogin(token);
    // 로그인 로직...
  }, [token, newPath]);
}
  • 위 코드에서는 useEffect가 두 가지 작업을 동시에 수행합니다
    1. 경로를 리다이렉트하는 작업 (redirect(newPath))
    2. 로그인 로직을 처리하는 작업 (setLogin(token))
  • 이처럼 한 가지 이상의 작업을 useEffect에서 처리하면, 코드가 복잡해지고 버그를 유발할 가능성이 높아집니다.

올바른 예시 (분리된 useEffect)

function LoginPage({ token, newPath, options }) {
  // 경로 변경 처리
  useEffect(() => {
    redirect(newPath);
  }, [newPath]);
 
  // 로그인 처리
  useEffect(() => {
    const userInfo = setLogin(token);
    // 로그인 후 로직...
 
    if (options) {
      // 추가 동작 (부가적인 로직)
    }
  }, [token, options]);
}
  • 위 코드에서는 useEffect가 각자의 역할을 담당합니다
    1. 첫 번째 useEffect: 경로 변경에만 집중.
    2. 두 번째 useEffect: 로그인 처리 및 추가 옵션 적용에만 집중.
  • 이렇게 하면 각 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 처리를 해야 한다. 이렇게 하면 useEffectPromise를 반환하지 않고, 내부에서 비동기 로직이 적절히 처리된다.
    • 에러 핸들링: 비동기 함수 내에서 에러가 발생할 수 있으므로, 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'; 구문을 작성하지 않아도 됩니다.

✍️ 내용

  1. React 17 버전 이상에서는 JSX가 내부적으로 컴파일되므로, 컴포넌트 파일마다 import React 구문을 명시하지 않아도 정상 작동합니다.
  2. React는 자동으로 JSX를 컴파일하게 되어, 불필요한 코드 작성을 줄여줍니다.

⭐️ 요약

  • React 17 이상 버전에서는 import React를 생략할 수 있습니다. 프로젝트에서 사용하는 React 버전을 확인하고, 필요시 최신 버전으로 업데이트하는 것도 좋은 방법입니다.


2. 디렉터리 구조

🌈 결론

  • 다양한 개발자와 협업하기 위해서는 일관성 있는 디렉터리 구조를 설계하는 것이 중요합니다.
  • 결합도가 높은 컴포넌트들은 같은 폴더 내에 배치하는 것이 좋습니다.

✍️ 내용

  1. 정답은 없지만, 일관성 유지가 핵심

    • 디렉터리 구조에 정해진 정답은 없으나, 의존성이나 결합도를 고려한 구조를 유지하는 것이 좋습니다.
  2. 기본 컴포넌트 구성 예시

  • ❌ 안 좋은 예시

    components/
    |- MyButton.tsx
    |- ViewTable.tsx
    |- Icon.tsx
  • ✅ 좋은 예시

    components/
    |- BaseButton.tsx
    |- BaseTable.tsx
    |- BaseIcon.tsx
  1. 결합도가 높은 컴포넌트를 묶어주는 것이 좋습니다.
  • ❌ 안 좋은 예시

    components/
    |- TodoList.tsx
    |- TodoItem.tsx
    |- TodoButton.tsx
  • ✅ 좋은 예시

    components/
    |- TodoList.tsx
    |- TodoListItem.tsx
    |- TodoListItemButton.tsx
  1. 관심사를 분리하여 디렉터리 구조를 구성합니다.
  • ❌ 안 좋은 예시

    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. 폴더 구조 제안

    // 제안 1
    components/
    |- @shared
    |-- 공통 컴포넌트
    |- Todo
    |-- Todo.tsx
    |-- Todo.hook.ts
    |-- todo.css
     
    // 제안 2
    hooks/
    |- useTodo.ts
     
    styles/
    |- todo.css
     
    components/
    |- Todo.tsx

⭐️ 요약

  • 컴포넌트들의 결합도와 관심사에 따라 디렉터리 구조를 세우면 협업이 수월해지고 유지보수하기 쉬워집니다. 같은 팀원들과 일관된 방식을 유지하는 것이 가장 중요합니다.


3. SPA에서의 새로고침

🌈 결론

  1. SPA에서 window.location.reload() 사용을 주의하자

✍️ 내용

1. window.location.reload()의 동작 방식

  • window.location.reload()는 브라우저에서 현재 페이지를 강제로 새로 고침하는 함수입니다. 이 함수는 브라우저가 페이지의 HTML, CSS, JS 리소스를 모두 다시 요청하고 새로 로드하게 만듭니다.
  • 이는 전통적인 서버 기반 웹 애플리케이션에서는 문제가 없지만, SPA에서는 비효율적일 수 있습니다.

문제점

  • SPA (Single Page Application)에서는 JS를 기반으로 페이지의 상태를 관리하고 마크업과 인터렉션을 처리합니다. window.location.reload()를 실행하게 되면, SPA가 이미 로드한 리소스를 다시 불필요하게 요청하게 됩니다. 특히 다음과 같은 문제들이 발생할 수 있습니다
    1. 불필요한 리소스 재로드: SPA의 JS 파일들이 브라우저 캐시에 있더라도, 새로고침이 발생하면 JS 파일을 다시 요청하게 되며, 이는 네트워크 트래픽과 성능에 영향을 줍니다.
    2. 상태 초기화: 새로고침이 발생하면 SPA의 클라이언트 사이드에서 관리하는 상태들이 모두 초기화됩니다. 예를 들어, 로그인 상태나 폼 입력값, 로컬 상태 등이 모두 사라지고 처음부터 다시 로드됩니다.

2. SPA의 리소스 관리 방식

  • SPA는 첫 로드 시 HTML, CSS, JS 파일을 받아오고 이후 페이지의 전환은 클라이언트 사이드에서 처리됩니다. 즉, 새로고침 없이도 필요한 데이터와 화면을 AJAX 요청이나 라우팅 라이브러리를 사용해 불러옵니다.
  • SPA의 장점
    1. 빠른 화면 전환: 서버에서 완전히 새로운 HTML을 받지 않고, 클라이언트에서 JS가 필요한 부분만 변경해주기 때문에 화면 전환이 매우 빠릅니다.
    2. 리소스 절약: 페이지가 전환될 때마다 모든 리소스를 다시 받지 않고 필요한 데이터만 주고받음으로써 리소스를 절약할 수 있습니다.

3. window.location.reload() 대신 사용할 수 있는 방법

  1. 상태 관리 라이브러리 사용 (예: Redux, Recoil, Context API 등)
  • SPA에서 상태가 변경되었을 때 새로고침을 사용하지 않고 상태를 갱신하는 것이 바람직합니다. 상태 관리 라이브러리를 사용하면 로그인 상태, 유저 정보 등 필요한 데이터를 전역에서 관리할 수 있습니다.
  • 예를 들어 로그인 성공 후, 전역 상태를 업데이트하여 다른 컴포넌트에서도 자동으로 로그인 상태가 반영되도록 처리할 수 있습니다.
  1. 컴포넌트 리렌더링
  • 특정 상태가 변경된 후 해당 상태를 기준으로 컴포넌트가 다시 렌더링되도록 하는 방법도 있습니다. React에서는 useStateuseEffect 훅을 사용해 상태 변화에 따라 UI가 자동으로 업데이트되도록 할 수 있습니다.

    const [isLoggedIn, setIsLoggedIn] = useState(false);
     
    useEffect(() => {
      if (isLoggedIn) {
        // 로그인 성공 시 필요한 데이터를 새로 불러오거나 UI를 업데이트
        console.log('로그인 성공!');
      }
    }, [isLoggedIn]);
  1. 라우터를 이용한 페이지 이동
  • 새로고침 없이 다른 화면으로 이동해야 할 때는 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 />
  • 위 예시에서는 TodoListTodoItem 같은 도메인 네임보다는, 시멘틱하고 확장 가능한 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의 일관성을 유지하는 데 매우 유리합니다.
  • 웹 접근성을 고려해, 사용자에게 더 직관적인 태그 구조를 제공하는 것이 좋습니다.

참고