Profile 컴포넌트가 좀 심심해보여서 공식사이트 바로가기 링크를 넣었는데, 계속 화면에 나타나지 않는 것처럼 보였다. 알고보니 글로벌 스타일 색상인 white가 적용돼서 style.ts에 있는 css가 적용되지 않았다. 임시방편으로 !important를 작성하니 화면에 나오긴했는데, !important를 남발하는건 유지보수에도 힘들거란 생각이 들어 다른 방법을 찾아보았다.
⬇️문제상황 (!important는 제거한 상태)

⬇️원래 작성했던 코드
//src/components/Profile/index.tsx
<OfficialLink href='https://www.edsheeran.com'>
공식사이트 바로가기
</OfficialLink>
//style.ts
const OfficialLink = styled.a`
&&& {
text-decoration: underline;
color: red;
}
`;
그러던 중, 스택오버플로우에서 "You can use the & selector more than once to increase specificity"라는 답변을 발견했다.
⬇️ 수정한 코드
//style.ts
const OfficialLink = styled.a`
&&& {
text-decoration: underline;
color: red;
}
`;

&&를 쓰면 쓸수록 클래스가 늘어난다...!
&를 2개 쓰면 클래스 2개, 3개 쓰면 3개...! (참고로 2개부터 작동가능!)
SNS 종류별로 react-icons에 내장되어있는 Configuration을 이용해 style을 줬고, <a>태그로 감싸 클릭하면 새 탭으로 이동하게끔 했다.
//src/components/Profile/index.tsx
<IconContext.Provider
value={{ color: 'black', style: { cursor: 'pointer' } }}>
<a href='https://www.instagram.com/teddysphotos/' target='_blank'>
<FaInstagram />
</a>
</IconContext.Provider>
<IconContext.Provider
value={{ color: 'black', style: { cursor: 'pointer' } }}>
<a href='https://www.youtube.com/user/EdSheeran' target='_blank'>
<FaYoutube />
</a>
</IconContext.Provider>
아니 달랑 인스타그램이랑 유튜브 아이콘 2개일뿐인데 너무 복잡하지 않은가..! (심지어 실제 코드에는 SNS 종류가 4개라 이것보다 더 길었다.)
어차피 똑같은 내용 복붙하느니 함수를 하나 만드는 게 낫겠다는 생각이 들었다.
//src/components/Profile/index.tsx
interface SocialMediaLinkProps {
href: string;
children: ReactNode;
}
const SocialMediaLink: React.FC<SocialMediaLinkProps> = ({ href, children }) => {
return (
<IconContext.Provider
value={{ color: 'black', style: { cursor: 'pointer' } }}
>
<a href={href} target='_blank'>
{children}
</a>
</IconContext.Provider>
);
};
function Profile() {
return (
(어쩌구저쩌구)
<SocialMediaLink href='https://www.instagram.com/teddysphotos/'>
<FaInstagram />
</SocialMediaLink>
<SocialMediaLink href='https://www.youtube.com/user/EdSheeran'>
<FaYoutube />
</SocialMediaLink>
);
}
export default Profile;
이왕 만든 김에 다른 파일에 작성했었던 <a>태그에도 써먹으려고 좀 더 수정해봤다.
export interface SocialMediaLinkProps {
href: string;
children: ReactNode;
iconColor?: string;
}
export const SocialMediaLink: React.FC<SocialMediaLinkProps> = ({
href,
children,
iconColor = 'black',
}) => {
const iconContextValue = { color: iconColor, style: { cursor: 'pointer' } };
return (
<IconContext.Provider value={iconContextValue}>
<a href={href} target='_blank'>
{children}
</a>
</IconContext.Provider>
);
};
iconColor의 기본값은 'black'이고, 다른 검은색 바탕에서 icon을 쓸 때는 'white'로 바꾸어서 사용했다!
내가 가장 좋아하는 유튜버 Dave Gray 아조씨의 React의 무한 스크롤 | 전체 튜토리얼를 보면서 구현한 infinite scroll!
공부량이 꽤 많아 이틀에 나눠 작업했다.

일단, 무한스크롤을 구현하기 앞서, 이전에 연결한 api를 통해 화면에 이미지가 잘 나옴을 확인했다.

페이지네이션 VS 더보기 버튼 VS 무한스크롤
페이징 기능들은 여러가지가 있지만, 무한스크롤은 사용자의 클릭을 최소화하면서 한 번에 많은 양의 데이터를 보여줄 수 있기에, 클릭하는 페이징 기능보다 더 나은 사용자 경험을 제공한다고 생각했다.
또한, 추후에 반응형으로 모바일 화면까지 제작할 때 상하 스크롤링만 해서 데이터를 계속해서 접할 수 있다는 건 유저에게 무척 자연스러우면서 편리하지 않을까 라고 생각했다.
물론 원하는 데이터의 위치를 기억하기 어렵다는 단점이 있기도 하다..!
throttle VS intersection Observer API vs 라이브러리스크롤이 페이지 끝에 닿는 것을 감지하여 추가 데이터를 받아오는 방식
scroll 이벤트가 발생할 때마다, scrollHeight와 scrollTop+offsetHeight를 비교하면 페이지의 끝에 닿았는지를 알 수 있다. 하지만, "scroll 이벤트가 발생할때마다" 와 같은 과도한 이벤트는 성능저하로 이어질 수 있고, 이를 개선하기 위해 throttle을 적용할 수 있다.
그러나, throttle도 성능을 개선해줄뿐이지, 스크롤 이벤트로 인해 계속되는 reflow가 발생하므로 근본적으로는 불필요한 이벤트가 발생하는것은 여전하다.
그래서 intersection Observer 방식을 택했다!
특정한 뷰포트 내에서 요소가 교차지점을 지나는지 감시하여 추가 데이터를 받아오는 방식
뷰포트는 우리가 보는 문서의 테두리가 될 수도 있고, 특정한 요소가 될 수도 있는데,
intersectionObserver는 명령대로 잘 감시하고 있다가 선을 넘는 순간 우리에게 알려주는 것이다!

(선 넘네)
Intersection Observer를 본격적으로 사용하기 전,
다음과 같이 naverAPI로 이미지를 요청하는 코드를 만들었다.
//src/api/axios.ts
import axios, {
InternalAxiosRequestConfig,
AxiosResponse,
AxiosError,
HttpStatusCode,
} from 'axios';
export const getPhotosPage = async (pageParam = 3, options = {}) => {
const response = await axios.get('/api', {
params: {
query: '에드시런노래',
display: 100,
filter: 'small',
start: `${pageParam}`,
},
headers: {
'X-Naver-Client-Id': import.meta.env.VITE_NAVER_CLIENT_ID,
'X-Naver-Client-Secret': import.meta.env.VITE_CLIENT_SECRET,
},
});
// console.log('response', response.data.items);
return response.data.items;
};
const controller=new AbortController()
//웹 요청을 중단할 수 있는 AbortController 인터페이스를 생성한다.
const signal = controller.signal
//요청을 취소할 수 있는 AbortSignal 객체 인터페이스를 반환한다.
const res=await fetch(url, { signal });
// 웹 요청 API에 AbortSignal객체의 signal의 값을 인자로 넘기면,
// 요청을 취소할 수 있는 상태가 된다.
const controller = new AbortController();
controller.abort();
//abort()를 사용하면 signal이 할당된 웹 요청을 취소할 수 있다.
//사실상 저 위의 코드나 이거나 목적은 같다.
//src/hooks/usePhotos.tsx
import { useState, useEffect } from 'react';
import { getPhotosPage } from '@src/api/axios';
const usePhotos = (pageNum = 3) => {
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [error, setError] = useState({});
const [hasNextPage, setHasNextPage] = useState(false);
useEffect(() => {
setIsLoading(true);
setIsError(false);
setError({});
//웹 요청 취소 가능한 컨트롤러 생성
const controller = new AbortController();
const { signal } = controller;
// 함수 호출해 이미지 데이터 가져오기
getPhotosPage(pageNum, { signal })
.then((data) => {
setResults((prev) => [...prev, ...data]);
setHasNextPage(Boolean(data.length));
setIsLoading(false);
})
.catch((e) => {
setIsLoading(false);
if (signal.aborted) return;
setIsError(true);
setError({ message: e.message });
});
return () => controller.abort();
}, [pageNum]);
return { isLoading, isError, error, results, hasNextPage };
};
export default usePhotos;
오늘은 intersection Observer에 대한 개념을 공부하고, 커스텀훅을 만드는 것까지 완료했다. 내일은 intersection observer를 직접 사용해보도록 하자!
참고 사이트: [React] 무한 스크롤 적용하기by sjoleee_
실무에서 느낀 점을 곁들인 Intersection Observer API 정리 by elrion018
[JS] 알아두면 쓸데있는 🔭Intersection Observer by Caesiumy
[JS] AbortController로 웹 요청 취소하기(비동기, dom 이벤트) by GicoMomg
오늘의 일기: