✍️Intro.
https://snowman-seolmini.tistory.com/70
[React Project] Momentum
✍️Intro.리액트를 배우면서 간단하게 만들어 놓은 웹 앱들이 많았지만, 이를 어떻게 하면 더 활용할 수 있을지 고민하던 차에 매번 보았던 크롬 브라우저의 앱인 momentum을 보고 동기를 받았다.
snowman-seolmini.tistory.com
https://snowman-seolmini.tistory.com/100
[React Project] Momentum - Lighthouse 최적화
Vercel을 통해 배포한 프로젝트를 Lighthouse로 검사한 후 최적화를 진행해 보기로 했다. Lighthouse는 웹 페이지의 성능, 접근성, SEO 및 모범 사례를 평가하여 개선할 수 있는 영역을 제시해 주기 때문
snowman-seolmini.tistory.com
위의 문서들의 내용을 규합하여, Momentum의 레이아웃 안정화 및 UI 수정, 코드 리팩토링, Lighthouse를 이용한 최적화를 통해 각 페이지마다의 기능에 대한 문제 상황, 해결 방안, 결과 순으로 문서를 작성하려 한다.
문제 상황
- 내가 만들고 싶은 기능
- 내가 해결해야하는 문제
- 프로젝트를 진행하면서 맞닥뜨릴 수 있는 문제
[ Background Image ]
1. Unsplash의 API를 통해 고화질 이미지 URL을 받아서 보여줌
2. 새로고침 및 화면 이탈 시 재렌더링 될 때마다 Unsplash API를 호출하는 것이 아닌, 일정시간을 기준으로 재호출하여 이미지를 변경함
3. 2번 조건을 이루기 위해 리액트쿼리의 설정 조건이 무엇들이 있는지 파악, setTime메서드와 유사한 방법이 있는지, 로컬 및 세션 스토리지를 사용해야 하는지 파악
[ Famous saying ]
1. 리액트쿼리로 명언 데이터를 받아와 보여주되, 사용자가 클릭 시 재호출이 일어나 새로운 데이터를 보여줌
2. refetch와 Invalidation의 사용을 고려하여 최적의 방법을 사용함
[ Weather ]
1. Lighthouse의 권장 사항을 지켜 페이지 로드 시 위치정보 권한 요청 후 날씨 데이터를 받아옴
2. refetch와 Invalidation의 사용을 고려하여 최적의 방법을 사용함
[ Todo List ]
1. TODO 데이터에 대한 "추가", "업데이트", "삭제" 같은 상태 변경 로직의 구조화
2. 각 상태 변경 로직을 가진 컴포넌트의 책임 분리 및 독립성 확보
3. TODO에 대한 "진행", "완성" 조건을 구분하여 카테고리별 필터링 기능
4. 작성한 TODO에 대한 검색 필터 기능
5. Dark / Light Theme mode 기능
[ Pomodoro ]
1. Work / Rest Time을 설정하여 타이머 초기 상태 변경 기능
2. 타이머를 계산하고 각 모드에 대한 시간이 끝나면 알람 소리와 함께 모드 자동 변경 기능
3. 커스텀 훅을 사용하여 복잡한 계산 함수 로직 분리
[ AI 분석 ]
1. GPT API에 대한 공식 사이트 문서를 바탕으로 role을 구분하여 분석 조건 설정
2. TODO 리스트에 작성한 데이터를 기반으로 분석 기능
3. TextArea에 작성한 내용을 기반으로 분석 가능
해결 방안
- 어떤 기술로 이 문제를 해결했는지
- 왜 이 스택을 선택했는지
- 왜 이런 로직을 작성했는지
- 리팩토링을 진행했다면 어떻게 했는지
[ Background Image ]
1. Unsplash의 API를 통해 고화질 이미지 URL을 받아서 보여줌
- TanStack Query 와 Axios를 사용
- 일반적으로 데이터를 가져오거나 업데이트하는 과정에서 캐싱, 호출 조건, 동일한 데이터에 대한 여러 요청 방지, 오래된 데이터 업데이트, 로딩 상태, 메모리 관리 등 많은 부분을 신경 써야 하는데 이러한 문제를 조금 더 쉽게 처리할 수 있어 사용함.
- 자동 JSON 변환 : `axios`는 응답 데이터를 자동으로 JSON으로 변환해 주기에 매번 `response.json()`을 호출할 필요가 없으므로 코드를 조금이나마 간결하게 만들어줌
- HTTP 상태 코드에 따른 자동 에러 처리 : `axios`는 HTTP 상태 코드가 200 - 299 범위를 벗어날 경우 자동으로 에러를 발생시킨다. 이를 통해 개발자는 성공적인 응답과 에러 응답을 간편하게 구분할 수 있다. 하지만 `fetch`는 네트워크 오류가 발생하지 않는 한 에러를 자동으로 발생시키지 않으므로, 개발자가 직접 상태 코드를 체크하여 에러 처리 로직을 작성해야 한다.
2. 매번 렌더링이 될 때마다 Unsplash API를 호출하는 것이 아닌, 일정시간을 기준으로 재호출하여 이미지를 변경함
3. 2번 조건을 이루기 위해 리액트쿼리의 설정 조건이 무엇들이 있는지 파악, setTime메서드와 유사한 방법이 있는지, 로컬 및 세션 스토리지를 사용해야 하는지 파악
# 코드의 주요 흐름
1. 로컬 스토리지에 배경 이미지 URL을 저장하고, 필요할 때 API를 호출하여 새로운 이미지를 받아온다.
2. 이미지를 받아올 때 5시간이 지난 경우에만 새로 이미지를 받아온다.
3. 이미지를 받아오는 동안 로딩 상태를 관리한다.
# 조건 구체화
if (!backgroundImageUrl || Date.now() - lastFetchTime > 1000 * 60 * 60 * 5) {
refetch(); // 필요할 때만 명확하게 API 호출
}
첫 방문할 때 API 호출
1. !backgroundImageUrl 조건 :
- 이 조건은 backgroundImageUrl이 없을 때를 의미
- 첫 방문자는 로컬스토리지에 backgroundImageUrl 이 저장되어 있지 않기 때문에 backgroundImageUrl 값이 null 또는 빈 문자열 " "을 가짐
- 그래서 첫 방문자는 이 조건이 참(true)이 되어, refetch()가 호출됨
2.Date.now() - lastFetchTime > 1000 * 60 * 60 * 5 조건:
- 만약 backgroundImageUrl 이 이미 있다면, 5시간이 지나면 이미지를 다시 받아오기 위해 이 조건이 참이 됨
- 즉, 캐시된 시간이 오래됐으면 API를 다시 호출
- 위의 이미지처럼 새로고침하여도 일정 시간이 지나지 않으면 이미지가 변경되지 않음을 알 수 있음
- 그렇다면 호출 시 호출되는 시간을 저장하여 이를 비교하여 조건을 만족하면 재호출이 일어나도록 해야 한다. 이를 위해서 서버에 저장하는 방법이 좋으나 클라이언트단으로만 이루어져 있으므로 스토리지에 저장하는 방법을 선택함.
- 세션 스토리지는 페이지가 닫히면 데이터가 삭제되어, 브라우저 세션이 끝나면 자동으로 정리되어 시간 저장값을 장시간 보관할 수 없음
- 쿠키는 보통 인증이나 사용자 상태 관리에 사용되어 이미지 URL과 같은 데이터는 쿠키로 저장할 필요가 없으며, 저장 가능한 용량이 적기 때문에 이미지 URL 같은 데이터를 관리하기엔 적합하지 않음
- 로컬 스토리지는 데이터가 브라우저를 닫아도 유지되며, 보관기간이 상대적으로 길다. 그러나 영구적으로 데이터가 남을 수 있어 민감한 데이터를 저장하는 데 부적합하나 민감한 정보가 아니므로 선택하게 됨
- useQuery에서 enabled 옵션을 사용하여 특정 조건(이미지url이 없거나, 일정 시간이 지났을 경우)을 만족할 때만 쿼리 함수를 실행하도록 함
[코드 리팩토링]
이전 코드 구조 문제:
const { isLoading, error } = useQuery({
queryKey: ['backgroundImage'],
queryFn: fetchImage,
enabled: !backgroundImageUrl,
staleTime: 1000 * 60 * 60 * 5,
onSuccess: (data) => setBackgroundImageUrl(data),
});
useEffect(() => {
let isMounted = true;
const fetchAndSetImage = async () => {
try {
const imageUrl = await fetchImage();
if (isMounted) {
setBackgroundImageUrl(imageUrl);
}
} catch (error) {
console.error('Failed to fetch background image:', error);
}
};
if (!backgroundImageUrl) {
fetchAndSetImage();
} else {
// 이미지 URL이 있을 경우, lastFetchTime을 확인하여 5시간이 지났는지 검사
const lastFetchTime = parseInt(localStorage.getItem('lastFetchTime'), 10);
if (Date.now() - lastFetchTime > 1000 * 60 * 60 * 5 || !lastFetchTime) {
fetchAndSetImage();
}
}
return () => (isMounted = false);
}, [backgroundImageUrl, fetchImage]);
문제 1: useQuery와 useEffect가 중복 처리
- useQuery는 이미 enabled 조건을 통해 backgroundImageUrl이 없을 때만 API를 호출하도록 하고 있음.
- 그런데 useEffect 안에서도 같은 조건을 확인한 후 fetchAndSetImage() 함수를 호출하려고 함.
- 이로 인해 이미지가 없을 때 useQuery와 useEffect가 중복으로 API를 호출할 수 있음
문제 2: useEffect 안의 불필요한 API 호출
- useQuery 는 내부적으로 상태와 타이밍을 관리하는데, useEffect 안에서 별도로 호출하는 fetchAndSetImage 가 같은 데이터를 가져오는 역할을 중복해서 할 수 있음
- 예를 들어, backgroundImageUrl 이 없을 때 useQuery 에서 API를 호출이 발생하고, 그 후에 useEffect에서 동일한 조건을 확인하여 다시 불필요한 API 호출이 발생할 수 있음
문제 3: 비동기 코드 정리
- 비동기로 데이터를 받아올 때 컴포넌트가 언마운트되었을 때도 데이터가 받아지면 오류가 발생할 수 있어, 이걸 방지하기 위해 isMounted 플래그를 쓰고 있지만, React Query에서는 이런 비동기 처리를 알아서 해주기 때문에 굳이 클린업 함수를 사용하지 않아도 됨
개선된 부분
useEffect(() => {
const lastFetchTime = parseInt(localStorage.getItem('lastFetchTime'), 10);
// 5시간이 지났거나 처음 로드된 경우 refetch() 호출
if (!backgroundImageUrl || Date.now() - lastFetchTime > 1000 * 60 * 60 * 5) {
refetch(); // 필요할 때만 API를 호출함
}
}, [backgroundImageUrl, refetch]);
2차 개선된 부분
https://snowman-seolmini.tistory.com/103
[React Project] Momentum ver 1 .1 .1 리팩토링 과정 중 문제&해결
1. 문제 상황useBackgroundImage 훅에서 backgroundImageUrl 값이 업데이트 됨Main 컴포넌트에서 backgroundImageUrl 값이 제대로 반영되지 않는 현상- 비동기 요청을 통해 배경 이미지를 가져오는 useBackgroundImage
snowman-seolmini.tistory.com
useEffect(() => {
const fetchImageData = async () => {
const lastFetchTime = parseInt(localStorage.getItem('lastFetchTime'), 10);
if (!backgroundImageUrl || Date.now() - lastFetchTime > 1000 * 60 * 60 * 5 || !lastFetchTime) {
const newImageUrl = await fetchImage(); // 비동기 호출
setBackgroundImageUrl(newImageUrl); // 상태 업데이트
}
};
fetchImageData(); // 비동기 함수 실행
}, [backgroundImageUrl]); // 의존성에 backgroundImageUrl 추가
일정시간동안 배경 이미지 데이터를 유지할 수 있으면서, LCP시간도 단축시킬 수 있어서 유의미한 성과를 챙길 수 있었다.
내가 클라이언트 측에서 최대한 할 수 있는 최적화 방식이라 생각한다. 특히나 서버가 없는 환경에서 선택지가 한정되기 때문이다.
*왜 로컬스토리지 캐싱을 했는가?
1. 서버가 없다면 클라이언트 측에서만 데이터를 저장하고 사용자가 매번 이미지를 다운로드하지 않도록 하는 로컬스토리지가 비용 면에서도 이점이 크다고 생각했다.
2. 네트워크가 없는 상태에서도 로컬스토리지에 저장된 이미지를 불러 올 수 있어, 오프라인 상태에서도 콘텐츠 접근성을 유지할 수 있다.
3. LCP 속도 개선을 통해 사용자가 빠르게 메인 콘텐츠를 보게 하여 페이지 로딩 시간을 크게 줄이는 효과가 있기 때문에, 실제 사용자 경험을 상당히 향상시켰다고 생각한다. 사용자 입장에서 로딩 시간이 짧아지고 콘텐츠가 빠르게 노출되면 긍정적인 경험으로 이어진다.
4. 서버 없이도 클라이언트 단에서 빠르게 이미지를 캐싱하여 표시하는 구조는 설정이 간편하고, 유지보수 면에서도 이점이 있다고 생각한다.
다른 서비스와 비교해도 충분히 좋은 방식인가?
- 크롬 앱 스토어의 Momentum 앱처럼 서버 캐싱을 활용하는 경우네는 보다 복잡한 방식으로 백엔드에서 캐싱 정책을 관리하고, 여러 사용자 데이터를 기반으로 최적화할 수 있는 장점이 있겠지만, 프론트엔드 단에서 간단하게 사용자를 위해 할 수 있는 최대한의 최적화를 달생했다는 점에서 로컬스토리지를 이용한 방식도 굉장히 효율적인 방법이라 생각한다.
[ Famous saying ]
1. 리액트쿼리로 명언 데이터를 받아와 보여주되, 사용자가 클릭 시 재호출이 일어나 새로운 데이터를 보여줌
https://github.com/chkim116/kadvice
- 위의 라이브러리를 사용한 이유는 다양한 명언API가 있었지만, 네트워크 통신을 하지 않고 간단하게 사용하여 불러올 수 있음
- 리액트 쿼리에서 refetch는 이미 가져온 데이터를 다시 요청하는 것을 의미한다. 즉, 현재 상태의 데이터가 있는 경우에도 강제로 서버에 다시 요청하여 최신 데이터를 가져온다. 이때, 이전의 데이터가 어떻게 되든 상관없이 새로 가져온 데이터로 업데이트된다.
- invalidation은 데이터가 변경되었을 수 있는 상황에서 사용한다. 예를 들어, 데이터가 변경되거나 새로 추가될 가능성이 있을 때, 해당 데이터가 유효하지 않다고 표시한다. 그러면 리액트 쿼리는 다음에 해당 데이터를 사용할 때 자동으로 다시 요청한다.
차이점 요약
- refetch : 현재 데이터를 강제로 새로 요청하는 것.(다시 확인하려고), 정확히 원하는 시점에 데이터를 새로 가져옴
- invalidation : 데이터가 변경될 수 있다는 신호를 보내고, 다음 요청 시 자동으로 데이터를 새로 요청하게 만드는 것.(업데이트가 있을 것 같아서), 변경된 데이터를 기반으로 나중에 요청할 때 자동으로 업데이트
2. refetch와 Invalidation의 사용을 고려하여 최적의 방법을 사용함
[ Weather ]
1. Lighthouse의 권장 사항을 지켜 페이지 로드 시 위치정보 권한 요청 후 날씨 데이터를 받아옴
- Permissions API를 통해 웹 애플리케이션이 사용자의 권한을 요청하거나 현재 권한 상태를 확인할 수 있음
Permissions API를 이해하기 앞서, navigator라는 전역 객체에 대해 알게 된 사실이 흥미로워. 설명을 붙이겠다.
navigator는 브라우저의 여러 기능에 접근할 수 있게 해주는 전역 객체이다. 그 하위에 geolocation이라는 메서드가 있어서 사용자의 현재 위치 정보를 얻을 수 있다.
구조 설명
- navigator : 브라우저와 관련된 정보를 제공하는 객체. 예를 들어, 사용자의 브라우저 종류, 버전, 위치 정보, 디바이스 정보 등을 포함한다.
- navigator.geolocation : 위치 정보에 접근할 수 있는 메서드로, 사용자의 지리적 위치를 가져오는 데 사용된다. 이 객체는 주로 다음과 같은 메서드를 포함하고 있다.
- getCurrentPosition ( success, error ) : 사용자의 현재 위치를 가져온다. 위치를 성공적으로 가져오면 success 콜백이 호출되고, 에러가 발생하면 error 콜백이 호출된다.
- watchPosition ( success, error ) : 사용자의 위치가 변경될 때마다 해당 위치를 가져오는 메서드이다.
- clearWatch ( watchId ) : watchPosition 메서드로 시작한 위치 추적을 중지할 때 사용한다.
예시
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
console.log('Latitude:', position.coords.latitude);
console.log('Longitude:', position.coords.longitude);
},
(error) => {
console.error('Error getting location:', error);
}
);
} else {
console.log('Geolocation is not supported by this browser.');
}
# 왜 if문으로 navigator.geolocation을 찍을까?
브라우저 호환성 이유가 크다.
- 모든 브라우저가 geolocation API를 지원하는 것은 아니다. 예를 들어, 오래된 브라우저나 특정 모바일 브라우저에서는 이 기능이 제공되지 않을 수 있다.
- navigator.geolocation이 존재하는지 확인함으로써, 사용자가 사용하는 브라우저가 API를 지원하는지 판별할 수 있다.
# navigator.geolocation.getCurrentPosition( successCallback, errorCallback ) 메서드를 실행하면 성공적으로 위치를 가져왔을 때 position 객체를 인자를 생성하고, 이 position 객체를 콜백 함수에 전달한다.
# position 객체는 두 가지 주요 정보를 포함한다.
- coords 객체 : 위도, 경도, 고도, 속도 등 위치 정보.
- timestamp : 위치 정보를 가져온 시간.
Permissions API란?
Permissions API는 웹 애플리케이션이 사용자의 권한을 요청하거나 현재 권한 상태를 확인하는 데 사용하는 도구이다. 사용자가 웹사이트에 어떤 권한은 주었는지, 또는 어떤 권한(예 : 위치 정보, 카메라, 마이크 등)이 필요한지를 알 수 있게 해 준다.
1. 사용법
navigator.permissions.query({ name: 'geolocation' })
.then((permissionStatus) => {
console.log(permissionStatus.state);
})
.catch((error) => {
console.error('Permissions API를 사용할 수 없습니다.', error);
});
2. name 매개변수
name 매개변수는 쿼리하고자 하는 권한의 종류를 문자열로 지정한다.
예를 들어,
- 'geolocation' : 위치 정보 권한
- 'camera' : 카메라 접근 권한
- 'microphone' : 마이크 접근 권한
- 'notifications' : 알림 권한
name은 권한의 종류에 따라 정해진 값이어야 하며, 아무렇게 작성할 수 없다. 사용 가능한 권한 종류는 브라우저에서 지원하는 권한 목록에 따라 다르므로, 적절한 값을 사용해야 한다.
3. 반환 값
navigator.permissions.query() 메서드는 PermissionStatus 객체를 반환한다. 이 객체는 state 속성을 가지며
- granted : 권한이 허용됨.
- denied : 권한이 거부됨.
- prompt : 사용자가 권한 요청을 승인하거나 거부할 수 있는 상태.
위의 세 가지 값 중 하나를 가질 수 있다.
navigator.permissions.query({ name: 'geolocation' })
.then((permissionStatus) => {
if (permissionStatus.state === 'granted') {
console.log('위치 권한이 허용되었습니다.');
} else if (permissionStatus.state === 'denied') {
console.log('위치 권한이 거부되었습니다.');
} else {
console.log('위치 권한을 요청할 수 있습니다.');
}
})
.catch((error) => {
console.error('Permissions API를 사용할 수 없습니다.', error);
});
- navigator.permissions.query() 메서드는 특정 권한의 현재 상태를 확인하는 데 사용된다.
- name 매개변수에는 쿼리 할 권한의 종류를 지정해야 하며, 임의의 문자열을 사용할 수 없다.
- 반환된 PermissionStatus 객체의 state 속성은 granted, denied, 또는 prompt 값을 가진다. 이를 통해 권한 상태를 확인할 수 있다.
그렇다면 navigator.permissions.query({ name: 'geolocation' })를 통해 permissionStatus.state의 값을 확인 후 진행된다면, navigator.permissions를 쓰지 않았던 기존의 코드에서는 navigator.geolocation.getCurrentPosition 사용할 때 팝업이 생기지 않았는데 왜 그런 걸까?
기존 코드에서는 navigator.permissions.query()를 사용하지 않았기 때문에, 브라우저에서 직접 navigator.geolocation.getCurrentPosition() 호출 시에 자동으로 권한을 묻는 팝업이 뜨게 된다.
Permissions API를 사용하지 않은 기존 코드:
navigator.geolocation.getCurrentPosition(
(position) => {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
// 위치 정보를 처리
},
(error) => {
console.error('Geolocation error:', error);
}
);
- 이 코드는 **직접적으로 getCurrentPosition**을 호출하고, 브라우저가 내부적으로 알아서 권한을 요청함.
- 권한 상태에 대해 사전 확인 없이 바로 요청하므로, 권한이 없을 때는 팝업창을 통해 사용자에게 바로 요청하게 된다.
Permissions API를 사용한 코드:
navigator.permissions.query({ name: 'geolocation' }).then((permissionStatus) => {
if (permissionStatus.state === 'granted') {
// 권한이 이미 허용된 경우 바로 위치 정보 가져오기
navigator.geolocation.getCurrentPosition(...);
} else if (permissionStatus.state === 'prompt') {
// 권한을 요청할 때 팝업창이 뜸
navigator.geolocation.getCurrentPosition(...);
} else {
// 권한이 거부된 경우 처리
}
});
- 이 코드는 permissions.query()로 권한 상태를 먼저 확인한 후, 상태에 따라 **getCurrentPosition()**을 호출
- permissionStatus.state === 'prompt'일 때에만 브라우저가 팝업을 띄워서 권한을 묻는다.
- 기존 코드에서 팝업이 안 뜬 이유는 이미 사용자가 브라우저 내 이전에 권한을 허용했거나 브라우저 설정에서 권한을 저장했기 때문일 수 있다.
- 권한은 'granted'로 설정되어 있다면, 팝업 없이 바로 위치 정보를 가져온다.
2. refetch와 Invalidation의 사용을 고려하여 최적의 방법을 사용함
[ Todo List ]
1. TODO 데이터에 대한 "추가", "업데이트", "삭제" 같은 상태 변경 로직의 구조화
2. 각 상태 변경 로직을 가진 컴포넌트의 책임 분리 및 독립성 확보
- 단순한 상태 관리를 위해 useState를 사용할 수 있지만, 상태가 복잡해지거나 여러 동작(추가, 수정, 삭제 등)이 필요할 때는 관리가 어려울 수 있다.
- 따라서, 복잡도를 높이지 않으면서도 구조적인 상태 관리를 위해 useReducer와 Context API를 결합하여 전역적으로 상태를 관리하기로 했다. 이는 별도의 상태 관리 라이브러리 없이도 각 컴포넌트의 독립성과 책임을 분리할 수 있게 해 준다.
3. TODO에 대한 "진행", "완성" 조건을 구분하여 카테고리별 필터링 기능
4. 작성한 TODO에 대한 검색 필터 기능
5. Dark / Light Theme mode 기능
https://tailwindcss.com/docs/dark-mode#toggling-dark-mode-manually
Dark Mode - Tailwind CSS
Using Tailwind CSS to style your site in dark mode.
tailwindcss.com
- 위의 tailwind의 다크모드 문서를 참고
- 컴포넌트가 처음 마운트될 때 useEffect훅이 실행
- useEffect 훅 내에서 로컬 스토리지와 사용자 시스템 설정을 확인하여 초기 다크 모드를 설정함
- 다크 모드 토글 함수를 호출하여 다크 모드 상태가 반전되고, 새로운 상태로 업데이트됨
- 커스텀 훅과 Context API를 사용해 전역적으로 상태 관리 가능하도록 만듦
1. 상태 선언 및 초기화
- `useState`훅을 사용하여 `darkMode`상태를 관리하고 초기값을 `false`로 설정하여 기본적으로 라이트 모드로 설정
const [darkMode, setDarkMode] = useState(false);
2. 다크 모드 토글 함수
-`toggleDarkMode`함수는 현재의 `darkMode` 상태를 반전시킴
3. 다크 모드 상태 업데이트 함수
- `darkMode`가 `true`이면 HTML의 최상위 요소 <html>에 `dark`클래스를 추가하고, 로컬 스토리지에 테마를 `dark`로 저장함
- `darkMode`가 `false`이면 HTML의 최상위 요소 <html>에 `dark`클래스를 제거하고, 로컬 스토리지에 테마를 `light`로 저장함
function updateDarkMode(darkMode) {
if (darkMode) {
document.documentElement.classList.add('dark');
localStorage.theme = 'dark';
} else {
document.documentElement.classList.remove('dark');
localStorage.theme = 'light';
}
}
4. 초기 모드 설정
useEffect(() => {
const isDark =
localStorage.theme === 'dark' ||
(!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
setDarkMode(isDark);
updateDarkMode(isDark);
}, []);
- `useEffect` 훅을 사용하여 컴포넌트가 처음 마운트될 때 실행됨
- 로컬 스토리지의 테마가 `dark`이면 `isDark`를 `true`로 설정함
- 로컬 스토리지에 테마가 설정되어 있지 않은 경우, 사용자의 시스템 설정(다크 모드 선호도)을 확인하여 `isDark`를 설정
- `setDarkMode(isDark)`를 호출하여 `darkMode`상태를 설정하고, `updateDarkMode(isDark)`를 호출하여 HTML 클래스와 로컬 스토리지를 업데이트 함
[ Pomodoro ]
https://www.npmjs.com/package/react-circular-progressbar
react-circular-progressbar
A circular progress indicator component. Latest version: 2.1.0, last published: 2 years ago. Start using react-circular-progressbar in your project by running `npm i react-circular-progressbar`. There are 385 other projects in the npm registry using react-
www.npmjs.com
https://www.npmjs.com/package/react-slider
react-slider
Slider component for React. Latest version: 2.0.6, last published: a year ago. Start using react-slider in your project by running `npm i react-slider`. There are 265 other projects in the npm registry using react-slider.
www.npmjs.com
https://velog.io/@goldbear2022/setInterval-%ED%8A%B9%EC%A7%95%EA%B3%BC-useInterval
React에서는 setInterval대신 useInterval을 사용하자
setInterval이란? setInterval은 호출 스케줄링이다 : 일정한 시간이 지난 후에 원하는 함수를 호출하는 메소드이다. 호출 스케줄링은 setTimeout이라는 메소드도 있지만 **setInterval은 주기적으로 실행해
velog.io
1. Work / Rest Time을 설정하여 타이머 초기 상태 변경 기능
2. 타이머를 계산하고 각 모드에 대한 시간이 끝나면 알람 소리와 함께 모드 자동 변경 기능
3. setInterval 대신 useInterval 사용하여 코드의 간결성, 렌더링 감소
- useRef로 콜백 함수를 저장해서 리렌더링을 하지 않으면서도 최신 상태의 콜백을 참조할 수 있음
- 이를 통해 setInterval을 사용하는 상황에서 최신 props나 state에 접근하고자 할 때 더 효율적으로 관리할 수 있고, 블필요한 리렌더링도 방지할 수 있음.
useRef는 리액트에서 값을 저장하지만, 그 값이 변경되더라도 리렌더링을 일으키지 않는 특징을 가지고 있다. 그 이유는 useRef가 리액트 컴포넌트의 상태와 다르게 동작하기 때문이다.
# 리액트 컴포넌트에서의 렌더링과 useRef의 관계 :
1. useState는 값이 변경되면 리렌더링을 트리거한다. 즉, 컴포넌트가 화면에 다시 그려져야 할 때 주로 사용하는 방식이다.
- 예 : 사용자 버튼을 클릭해서 count를 증가시키면, 그 값을 다시 보여줘야 하기 때문에 리렌더링이 발생함.
2. 반면에, useRef는 값이 바뀌더라도 리액트가 그 변화를 인지하지 않고, 컴포넌트는 리렌더링하지 않는다.
- 이유는 useRef는 컴포넌트가 가지고 있는 값 저장소일 뿐이고, 그 값을 리액트가 화면을 다시 그려야 할 정보로 간주하지 않기 때문이다.
- 즉, useRef에 저장된 값은 UI에 직접적은 영향을 주는 상태로 간주되지 않아서, 리렌더링을 유발하지 않는 거다.
- 좀 더 구체적으로, useRef는 단순한 "참조" 객체이기 때문에 값이 바뀌더라도 리액트에게 굳이 "화면을 다시 그려야 해"라고 알리지 않는다. 예를 들어, DOM 요소에 직접 접근하거나, 타이머처럼 UI와 상관없이 값만 저장하고 유지하는 역할에 적합하다. (그렇기에 타이머를 계산하는 메서드로 적격이다.)
- useRef가 반환하는 객체는 { current : value }의 형태로, 여기에 있는 current 값을 수정해도 그 변경은 리액트 컴포넌트 렌더 사이클에 영향을 미치지 않는다.
[ AI 분석 ]
1. GPT API에 대한 공식 사이트 문서를 바탕으로 role을 구분하여 분석 조건 설정
https://platform.openai.com/docs/overview
- quickstart의 문서를 보면 3단계로 설정하는 부분이 있는데, 여기서 OpenAI 라이브러리를 굳이 설치하지 않아도 비동기 통신으로만으로도 GPT에게 데이터를 받아올 수 있다.
2. TODO 리스트에 작성한 데이터를 기반으로 분석 기능
3. TextArea에 작성한 내용을 기반으로 분석 가능
결과
- 각 문제 상황에 대한 output
- 프로젝트 결과
'Project' 카테고리의 다른 글
[React Project] Momentum - Lighthouse 최적화 (5) | 2024.11.06 |
---|---|
[React Project] Momentum ver 1 .1 .1 리팩토링 과정 중 문제&해결 (0) | 2024.10.18 |
[React Project]F.ROCK🥻(Shopping mall) (0) | 2024.08.15 |
[React Project] 파지직TV⚡[Ver2.0] (0) | 2024.07.27 |
[React Project] Momentum (0) | 2024.05.25 |