이번에 React를 지금까지 배운내용을 토대로 혼자 프로젝트를 진행해 보았다.
index.js파일에선 <Main />컴포넌트를 render해주었다. 기존엔 App.js파일을 써서 렌더링 시켰지만 이번엔 최상위 컴포넌트를 Main.js로 한다. Main.js파일에선 ReactRouter(BrowserRouter)를 사용했다.
Main은 최상위에 라우터를 씌워주고 그 바로 아래에 App컴포넌트를 씌워주며 App컴포넌트에선 사이에 모든 route들을 {children}으로 씌워서 App컴포넌트 내부엔 Nav, 메인, footer로 나눠서 배치하며 컴포넌트를 조립했다.
function App({children}) {
return (
<>
<ContextProvider>
<Nav />
<div>{children}</div>
<Footer />
</ContextProvider>
</>
)
}
테이블에 border를 주려면, html테이블 태그의 기본 속성을 초기화 해줘야 하므로,
table {
border-collapse : collapse;
border-spacing : 0;
}
이런식으로 초기화 해주어야 된다.
그냥 뜬금없이
const navigate = useNavigate();
if(isLogin) {
navigate('/some-path');
}
이렇게 쓰면 react가 예측하지 못한 상태를 만들어서 문제가 생긴다. 위 코드는 컴포넌트가 렌더링될때 조건적으로 useNavigate를 실행하면 무한렌더링되는 코드이다.
그래서 올바르게 사용하는 위치는
useEffect나 이벤트핸들러 내부에 사용해주어야 한다.
const navigate = useNavigate();
useEffect(() => {
if(isLogin) {
navigate('/some/path');
}
}, [isLogin]);
const handleByClick = () => {
navigate('/some/path');
}
의존성 배열의 역할을 잘 이해해야 한다.
useEffect(() => {...}, []); : 컴포넌트가 마운트될때 한번만 실행
useEffect(() => {...}, [dep]) : 특정 디펜던시가 변경될때마다 실행
useEffect(() => {...}) : 리렌더링 될때마다 실행
주요 패턴으로는
초기 데이터 로딩
useEffect(() => {
fetchData();
}, []);
의존성 관리
useEffect(() => {
console.log(dep);
}, [dep]);
클린업 함수 사용 등이 있다.
무한 루프 방지 : 의존성 배열이 없거나, 불필요한 값을 넣으면 무한 렌더링이 발생한다. 의존성 배열을 정확히 설정해야 한다.
상태 업데이트 타이밍 : 상태 업데이트 후 결과를 다시 의존성으로 사용하면 문제가 될 수 있다. 예를 들어
useEffect(() => {
setState(prev => prev + 1);
}, [state]); // 무한루프 발생
1. useEffect는 상태변화에 민감한것이 아니라 의존성 배열에 포함된 값의 변화에 반응한다.
2. 조건문 자체는 별개의 논리이다.
useEffect(() => {
if (someValue > 10) {
console.log('someValue가 10을 초과함');
}
}, [someValue]);
여기서 someValue가 11에서 12로 변하면 useEffect는 실행되며, 조건문을 확인한다. 그러나 someValue가 여전히 12일때는 실행되지 않는다. 이 점에서 의존성과 조건문은 별개이다.
3. 조건이 참인가를 평가
주의 : 디펜던시 리스트 안의 값이 계속해서 똑같은 값으로 할당이된다면, (예 : 12라는 값이 계속해서 할당중...) useEffect는 매번실행된다. 왜냐하면 react는 의존성 배열의 값이 "변경되었다고 판단"하는 기준이 값이 참조 변경이기 때문이다.
react가 상태(state), 속성(props), 그리고 기타값이 변할때, UI를 새롭게 계산하고 DOM을 업데이트하는 과정을 말한다.
렌더링 과정은 이렇다.
const MyComponent = () => {
return <div>Hello React</div>
}
여기서 <div>Hello React</div>라는 react 엘리먼트가 반환된다.
가상 DOM 생성
React는 실제 DOM이 아니라 가상DOM이라는것을 사용한다. 컴포넌트가 렌더링 되면 가상DOM이 생성된다.
변경 사항 비교(Diffing)
React는 이전의 가상 DOM과 새로운 가상 DOM을 비교하여 어떤 부분이 달라졌는지 확인한다. 이것을 Diffing이라고 부른다.
필요한 부분만 업데이트
변경된 부분만 실제 DOM에 반영해서 성능을 최적화한다.
렌더링이 발생하는 이유
렌더링은 특정 조건이 충족될때마다 발생한다.
const [count, setCount] = useState();
const increment = () => {
setCount(count + 1);
}
상태가 변경되면 react는 이 컴포넌트를 다시 실행해야 한다고 판단해서 렌더링을 트리거 한다.
속성(Props)가 변경될때 : 부모 컴포넌트에서 자식 컴포넌트로 전달되는 props가 변경되면 자식 컴포넌트가 다시 렌더링 된다.
부모 컴포넌트가 렌더링 될때 자식 컴포넌트도 같이 렌더링 된다.
useRef는 리렌더링 없이 값을 저장하고 추적할 수 있는 훅이다.
주된 사용 사례는 DOM엘리먼트를 참조하는 것이지만,
1. useRef의 값은 컴포넌트가 리렌더링 되더라도 초기화되지않고 유지된다.
2. useRef의 값(current)은 변경해도 리렌더링이 발생하지 않는다.
3. DOM엘리먼트를 참조하는것 외에 값 상태를 저장하거나 특정 동작의 한번 실행여부를 기록하는데에도 유용하다.
즉, useRef는 리렌더링 영향을 받지않는 독립적 저장소이다.
navigate함수에서 replace : true옵션을 사용하면 히스토리 스택에 현재 페이지를 대체하여 뒤로가기버튼으로 이전페이지로 돌아가는것을 방지할 수 있다.
동작 원리 : replace : true 사용시
navigate('/signin', {replace : true})는 현재 페이지를 '/signin'으로 교체한다. 결과적으로 이전 페이지가 히스토리 스택에 남지 않아서 사용자가 브라우저 뒤로가기 버튼을 눌러도 해당 페이지로 돌아갈 수 없다.
상황 : 새로운 폼 컴포넌트에서 데이터를 POST요청으로 서버에 보냄
문제 :
해결방법 :
1. 부모 컴포넌트에서 상태를 업데이트 하는 함수 정의
부모 컴포넌트에서 데이터를 관리하는 상태를 업데이트할 수 있는 함수를 만들고, 이를 자식 컴포넌트에 props로 전달한다.
즉, 부모가 데이터를 새로 요청하지 않고도 자식이 데이터를 보낸 직후 부모상태를 업데이트하고, 서버와의 통신 결과를 바로 부모 상태에 반영시킴으로 불필요한 리렌더링과 API호출을 방지하고, 부모가 상태를 업데이트하는 책임을 가지되, 자식이 데이터를 전송한 결과를 부모에 즉시 전달하여 UI를 최신 상태로 유지한다.
onClick속성에는 함수를 선언했다면, onClick={handleByClick}이런식으로 그 함수 자체만 전달해야 한다. 만약 onClick={handleByClick(a, b)}이런 식으로 파라미터를 넘겨준다면 함수가 클릭했을때가 아니라 그 즉시 호출되게 된다. 그리하여 handleByClick(a, b)함수는 바로 호출 결과를 반환하고, 변환된 값이 onClick에 전달되므로, 원하는 결과를 얻지 못할 수도 있다. 이를 해결하려면(아규먼트를 주고 싶을때) 익명함수 또는 화살표 함수로 handleByClick함수를 감싸야 한다. 이렇게 하면 클릭이벤트가 발생할 때만 함수가 실행된다.
onClick={() => handleByClick(a, b)}
즉, 파라미터가 없을땐 함수명만 전달하고, 파라미터가 있다면, () => 이런 화살표 함수로 감싸자
배열의 특정 항목을 업데이트하는 방법중 하나인데, map함수를 사용하여 기존 배열에서 특정 조건에 맞는 항목만 교체하고 나머지는 그래도 유지하는 패턴을 써보았다.
상황 :
1. 문제 :
const editComplete = (editComment) => {
setCommentsList((prevList) => prevList.map((item) => {
return item.commentId === editComment.commentId ? editComment : item;
}));
};
처음에 다크모드의 true혹은 false값을 Context로 관리하고, useState는 휘발성이니까, useRef로 하려고 했지만, useRef는 useRef의 값이 변경되어도 자동으로 렌더링되지 않는 문제점이 있었다. 그래서 useState로
const [themeStatus, setThemeStatus] = useState(
localStorage.getItem('theme')
)
useEffect(() => {
if(!themeStatus) {
localStorage.setItem('theme', 'false');
}
}, [themeStatus]);
이런식으로 useEffect를 활용해서 사용자가 홈페이지에 처음 들어왔을때 로컬스토리지에 기본값을 false로 할당하는 식으로 했고, 다른 컴포넌트에서 활용할땐 true, false값에 반전을 주며 document.body.className으로 클래스 이름을 주어 테마를 바꾸려고 했다.
하지만, 페이지 최초렌더링시에 localStorage.get('theme')의 값이 null이 할당이 되는것이다. (로컬스토리지에서 값을 가져오지 못하면 null이 반환됨)
그 이유를 알아보니
useEffect는 컴포넌트가 렌더링 된 이후에 실행되므로, useEffect에서 setItem을 해주기 전에 localStorage.getItem('theme')의 값을 쓸 수 없었고, 최초 렌더링 이후에 새로고침을 해야 값을 쓸 수 있는 문제점이었다. 그래서 처음부터 코드를 갈았다.
const [themeStatus, setThemeStatus] = useState(() => {
return localStorage.getItem('theme') || 'false';
});
이렇게 해주었고,
useEffect(() => {
localStorage.setItem('darkmode', themeStatus); // themeStatus값이 변경되면 localStorage에 저장
document.body.className = themeStatus === 'false' ? '' : 'dark-theme';
}, [themeStatus])
이렇게 해줌으로 코드를 깔끔하게 했다.
...
const {themeStatus, setThemeStatus} = useContext(ThemeContext);
const handleByThemeChange = () => {
const newTheme = themeStatus === 'false' ? 'true' : 'false';
setThemeStatus(newTheme);
};
정리 :
정신없이 정리하다보니 다듬어지지않았다.
useEffect에서 로컬 스토리지와 DOM동기화의 중요성을 깨달았다. useEffect를 통해 react상태가 변경될때 마다 로컬스토리지와 DOM요소의 클래스를 동기화하는 과정을 알게되었고, useEffect는 이런 동기화를 수행하는 이상적인 장소라는 생각이 들었다.
이 방식은 상태가 변경될때마다 수동으로 로컬 스토리지나 DOM을 조작해야 하는 번거로움을 줄일 수 있다.
또, useRef와 useState의 차이도 자세히 알게됐는데,
useRef는 값 변경시에 렌더링을 트리거하지 않기때문에 다크모드와 같은 UI상태 변경에는 적합하지 않다. 즉, 컴포넌트를 다시 렌더링할 필요가 없는 경우에 적합하다. useRef의 주된 목적은 React의 렌더링 흐름과 독립적으로 값을 저장하거나 DOM요소에 직접적으로 접근하는 것이다.
useState는 사용하면 상태 변경시 컴포넌트를 다시 렌더링 하기 때문에 테마가 즉시 반영될 수 있었다.
useEffect의 의존성배열의 값이 변경될때만 useEffect가 실행되어 불필요한 렌더링 및 동작을 방지할 수 있었다.
++ 생소해서 적는 코드
아까 위에 useState에 콜백함수를 전달했는데,
const [themeStatus, setThemeStatus] = useState(() => {
return localStorage.getItem('theme') || 'false';
});
와 같이 콜백함수를 전달하는 방식은 "초기 상태 값을 지연 계산하기 위한 방법"이다. 이 방식은 초기화 단계에서 불필요한 연산을 방지하고 성능을 최적화 한다.
useState 콜백의 역할
useState에 콜백을 전달하면, React는 컴포넌트가 처음 렌더링 될때 콜백함수를 실행하고, 그 반환값을 상태의 초기값으로 설정한다. 이후 렌더링에선 이 콜백함수가 실행되지 않고, themeStatus의 현재 상태 값만 사용하게 되는것이다.
초기화 시점에서 값 계산
위 코드에서는 localStorage.getItem('theme')을 호출하여 theme라는 키에 저장된 값을 가져온다. 만약 값이 없다면 null이 반환되므로, || 'false'에 의해 'false'가 기본값으로 설정된다.
지연계산의 장점
localStorage.getItem()같은 작업은 상태적 비용이 비쌀 수 있다. 상태를 콜백으로 초기화하면 상태 업데이트로 인해 다시 렌더링 될때 불필요한 초기화 작업이 재실행되지 않는다. 따라서 성능 개선이 될 수 있다.
핵심
1. 콜백은 초기 렌더링시 딱 한번만 실행된다.
2. React의 상태 관리에서 초기화를 효율적으로 처리하는 방법이다.
3. 상태 초기화에 시간이 걸리거나, 복잡한 계산이 필요한 경우 반드시 콜백 방식을 사용하자.
이론은 어느정도 알고 있었지만, 명확하지않았고 오해가 많았다. 특히 실제 동작 타이밍, 의존성 배열의 역할, 초기값 처리와 sideEffect관리의 관계에 대해 헷갈렸기 때문에 다시 적는다.
페이지 최초렌더링시에 useEffect가 왜 실행되지 않는지 헷갈렸지만 useEffect는 렌더링 후에 실행된다는 점을 알게됐고, 만약 렌더링 전에 실행되어야 할 작업이 필요하다면, 초기값 설정을 useEffect가 아닌 useState초기값 설정 및 초기값 설정 함수가 적절하다.
또 의존성배열을 useEffect가 실행될 조건을 제어한다.
빈배열은 컴포넌트가 마운트될때 한번만 실행하고, 특정 의존성이 포함되어 있다면, 해당 의존성의 값이 변경될때 마다 실행된다. 또, 의존성 배열이 없다면 컴포넌트가 렌더링 될때마다 실행된다.
useEffect로 절대 초기값을 설정하려는 시도는 하지않도록 하자. 초기값설정은 아까 말했듯이, useState초기값 설정 및 useState초기값 설정 함수로 한다.
잘못된 URL에 접근했을 때 404페이지를 보여주려면 React Router를 사용하여 기본 라우팅 설정 맨 아래에
function Main() {
return (
<>
<BrowserRouter>
<Routes>
...
<Route path="mypage">
<Route index element={<MyPage />} />
<Route path="mypageEdit" element={<MyPageEdit />} />
</Route>
...
<Route path="*" element={<NotFoundPage />}/>
...
</Routes>
</BrowserRouter>
</>
)
}
이렇게 맨 아래에 path="*" 그리고 보여줄 페이지를 적어주면 된다.
React Router의 useSearchParams는 URL의 쿼리 파라미터를 읽거나 업데이트 할 수 있는 React 훅이다. 이 훅을 사용하면, 컴포넌트에서 URL의 상태를 관리할 수 있어, 필터링, 검색, 정렬과 같은 동작을 구현할 때 유용하다.
[searchParmas, setSearchParams] 형태의 배열을 반환쿼리 파라미터 읽기
`const [searchParams, setSearchParams] = useSearchParams();`
const filterQuery = searchParams.get('filter') || '';
검색어 반영 (파라미터 설정)
setSearchParams({filter : searchString});
useEffect(() => {
const filterQuery = searchParams.get('filter') || '';
setSearchString(filterQuery);
}, [searchParams])
const handleBySearch = () => {
setSearchParams({filter : searchString});
};
const filterList = postList.filter((item) => {
const filterQuery = searchParams.get('filter') || '';
return item.postTitle.toLowerCase().includes(filterQuery.toLowerCase());
});
이렇게 검색 기능을 구현했다.
유용한 팁
1. 쿼리 파라미터 초기화 또는 삭제
setSearchParams({}); // 모든 쿼리 파라미터 제거
setSearchParams({ filter : '' }); // 특정 파라미터 제거
setSearchParams({filter : '검색어', page : '2'});
주의사항
URLSearchParams는 문자열값만 처리한다. 숫자나 복잡한 데이터는 문자열로 변환해야한다.
기본값처리는 파라미터가 없는 경우를 대비해서 기본값을 설정하는 로직이 꼭 필요하다.
const filterQuery = searchParams.get('filter') || '';
결론 : useSearchParams는 React컴포넌트와 URL상태를 연결하여, 검색과 필터링 같은 동작을 URL기반으로 구현할 때 매우 유용하다.