Intro✍️
기존의 사이트를 자신만의 것으로 만들어 보는 것은 리액트를 공부하고 연습해 보는 의미에서 큰 의미가 있다고 생각한다. 파지직TV사이트는 학습한 리액트 지식을 활용하고, 더 나은 방식이 있으면 덮어씌우고, 새로운 기능의 구현 방법을 배우면 적용해 보는 방식으로 만들어진 사이트이다.
무려 3번이나 만들고 버리고를 반복해서 나름 완성의 마침표를 찍었다.
"
방법을 몰라서 안 하는 겁니다.
방법을 몰라서 못하는 겁니다.
방법을 몰라서 지속할 수 없고
방법을 몰라서 시도할 수 없습니다.
계속하십시오.
시행착오를 겪으십시오.
처음부터 완벽할 수는 절대 없습니다.
꾸중을 들어도 그러려니 하십시오.
익숙해지면 모든 것이 괜찮아질 것입니다.
"
1. 파지직TV는 어떤 웹 사이트인가?
유튜브의 매력을 그대로, 더 편리하게!
파지직TV는 YouTube Open API를 활용해 만든 특별한 공간이다.
✨ 다양한 카테고리
- 인기 영상, 실시간 스트리밍, 스트리머 채널, 영화, 음악 등
- 원하는 콘텐츠를 쉽고 빠르게 찾아볼 수 있다.
🔑 개인화된 경험
- 실제 유튜브 계정과 연동
- 구독 중인 채널 정보를 한눈에 확인
- 좋아하는 콘텐츠로 빠르게 이동
❤️ '좋아요'로 나만의 컬렉션
- 마음에 드는 영상에 '좋아요' 클릭
- 'Saved Videos'에서 언제든 다시 보기
- 나만의 특별한 콘텐츠 라이브러리 완성
파지직TV는 유튜브의 핵심을 담으면서도, 더욱 친근하고 개인화된 경험을 느낄 수 있도록 했다.
2. 사용한 라이브러리
# Tanstack/React Query
1. Infinite Queries (무한 쿼리)
- 사용 이유 : 페이지네이션된 데이터를 효율적으로 로드하기 위해 사용.
- 예시 : useInfiniteQuery훅 사용
- 이점 : 사용자가 스크롤할 때 추가 데이터를 자연스럽게 로드 가능
2. 동적 Query Key
- 사용 이유 : AccessToken에 따라 다른 쿼리를 실행하기 위해 사용.
- 예시 : queryKey: [ ' subscription ', accessToken ]
- 이점 : 사용자별 데이터를 효과적으로 캐시하고 관리 가능
3. 조건부 쿼리 실행
- 사용 이유 : 특정 조건(Access Token 존재할 때만 실행)이 충족될 때만 쿼리 실행
- 예시 : enabled: !!accessToken
- 이점 : 불필요한 API 호출 방지 및 에러 상황 예방
4. Stale Time 설정
- 사용 이유 : 데이터 신선도를 제어하고 불필요한 리페치 방지
- 예시 : staleTime : 1000 * 60 * 5 (5분)
- 이점 : 네트워크 요청 최적화 및 성능 향상
5. 커스텀 쿼리 함수
- 사용 이유 : 특정 API 호출 로직을 캡슐화하기 위해
- 예시 : queryFn : async ({ pageParm = ' ' }) => {... }
- 이점 : 코드 재사용성 향상 및 로직 분리
6. Next Page Parameter 설정
- 사용 이유 : 다음 페이지 데이터를 효과적으로 가져오기 위해
- 예시 : getNextPageParam : (lastPage) => lastPage.nextPageToken || undefined
- 이점 : 연속적인 데이터 로딩을 위한 페이지네이션 로직 구현
7. 로딩 및 에러 상태 관리
- 사용 이유 : UI에서 다양한 상태를 처리하기 위해
- 예시 : isLoading, error, isFetchingNextPage 상태 사용
- 이점 : 사용자 경험 향상 및 에러 처리 간소화
8. 커스텀 훅 사용
- 사용 이유 : 반복적인 쿼리 로직을 추상화하기 위해
- 예시 : useYoutubeInfiniteQuery 훅 사용
- 이점 : 코드 중복 감소 및 일관된 데이터 fetching 패턴 유지
1~8가지의 다양한 옵션과 기능들 덕분에 생각했던 기능들을 훨씬 더 쉽게 구현할 수 있었다. 리액트 쿼리를 사용하지 않았다면, 이 모든 것을 직접 구현해야 했을 테니 말이다.
# Axios
1. 에러 핸들링
* fetch는 HTTP 상태 코드가 400번대나 500번대일 때도 Promise를 거부하지 않는다. 따라서, 이러한 경우를 수동으로 처리해야 함
fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.catch(error => {
console.error('There was a problem with your fetch operation:', error);
});
* axios는 HTTP 상태 코드가 400번대나 500번대일 때 자동으로 Promise를 거부하고 catch 블록에서 에러를 쉽게 처리할 수 있음.
2. 응답 데이터 자동 변환
- 이점 : JSON 응답을 자동으로 JS 객체로 변환
- 예시 : response.data.items.map((itme)=> ({ ... }))
- 효과 : 별도의 파싱 과정 불필요, 코드 간소화
3. 인스턴스 생성과 기본 설정
- 이점 : axios.creatre()를 통한 재사용 가능한 인스턴스 생성
- 예시 : this.httpClient = axios.create({ ... })
- 효과 : 반복적인 설정 코드 감소, 일관된 API 요청 구성
4. 기본 매개변수 설정
- 이점 : 모든 요청에 공통적으로 적용될 매개변수를 미리 설정
- 예시 : params : { key: process.env.REACT_APP_YOUTUBE_API_KEY, .... }
- 효과 : 코드 중복 감소, 실수 방지
5. 요청 설정의 유연성
- 이점 : 메서드별로 다른 설정을 쉽게 적용 가능
- 예시 : params: { q: keyword, type: 'video', ... }
- 효과 : 다양한 엔드포인트에 대한 요청을 쉽게 구성
6. 프로미스 기반 비동기 처리
- 이점 : async/await 구문과의 호환
- 예시 : const response = await this.httpClient.get('search', { ... })
- 효과 : 비동기 코드의 가독성 향상 및 에러 처리 용이성
# Firebase
직접 백엔드 서버를 구축하는 것은 많은 시간과 노력이 필요한 작업이다. Express, MongoDB, Mongoose 등을 사용하여 백엔드를 구축할 경우, 다양한 설정과 코드를 작성해야 한다. 이는 특히 백엔드 개발에 익숙하지 않은 나에겐 큰 부담이었다.
서버 설정, 데이터베이스 연결, 사용자 인증, 데이터 모델링, 보안 설정 등과 같은 작업들은 많은 시간과 노력을 요구하며, 각 단계마다 발생할 수 있는 문제를 해결하기 위해 학습과 디버깅이 필요하다.
파이어베이스에서 제공하는 다양한 Authenticaation, Database, storage 등의 기능들은 시간을 절약하고, 기능을 신속하게 구축하며, 더 빠르게 작업을 테스트할 수 있도록 도와준다.
물론 요즘 프론트엔드 또한 반드시 백엔드까지 알아야 한다고들 하지만, 프로그래머는 프로토타입을 만드는 시기와 그렇지 않은 시기를 구분하는 것은 중요한 능력이라 생각한다. 모든 아이디어에 대해 백엔드를 직접 구현할 필요는 없다고 생각한다.
예를 들어, 어떤 아이디어가 떠올렸을 때, 그 아이디어가 성공할지 여부를 먼저 테스트해 보는 것이 중요하다. 전체 프론트엔드와 백엔드를 모두 구현하는 대신, 아이디어를 빠르고 효율적으로 테스트할 수 있는 방법을 찾아야 한다.
띠라서 파이어베이스는 프로토타입을 빠르게 만들어 아이디어를 테스트하는 데 매우 유용한 도구이다.
# Zustand
Zustand는 상태 관리 라이브러리 중 작은 패키지 크기와 직관적인 사용법 덕분에 Redux와 MobX와 더불어 개발자들로부터 선택을 받고 있는 추세이다.
Redux와 Redux-toolkit이 어렵고 낯선 상태에서 Zustand를 사용해 보니 보일러플레이트코드 없이 훅처럼 사용할 수 있어. 간편하고 편리했다.
1. 간단하고 직관적인 API
Firebase 구글 로그인과 데이터베이스 연동은 이미 복잡한 작업이므로, 상태 관리 라이브러리는 최대한 간단한 것을 선택하여 코드를 이해하는데 어려움을 줄이고자 하였다.
2. React와의 자연스러운 통합
React 컴포넌트 내에서 로그인 상태, 인증 상태, 좋아요 버튼 상태 등을 관리하기 위해, React 훅과 자연스럽게 통합되는 Zustand를 선택하여 개발의 일관성과 효율성을 높이고자 하였다.
3. 상태관리 라이브러리의 숙련도 증가를 위함
다양한 상태 관리 기술을 경험하는 것은 기술적 유연성을 높이고, 상태 관리의 핵심 개념을 조금 더 이해하기 위함이다.
# Sass & Tailwind.css
일반적으로 일관성과 유지보수성을 위해 하나의 주요 스타일링 접근 방식을 선택하는 것이 좋다. 또한 여러 라이브러리를 로드하면 페이지 로딩 시간이 늘어날 수 있지만, 내가 만든 프로젝트들은 개인의 학습 목적의 비중이 높아 여러 기술을 실험해 보는 것으로 유익했다. 각 라이브러리의 장단점을 체험해 볼 수 있었고, 비교해 볼 수 있었다.
# Swiper
순수 구현으로 단순한 캐러셀을 만들 경우 개발 시간 증가와 버그 가능성, 추가적인 기능의 구현 어려움 등의 이유로 사용했다. 하지만 순수 구현 능력은 개발자의 기본기를 향상시키고 문제 해결 능력을 키우는 데 중요하다. 이를 늘 염두하고 있다.
3. 문제점 및 어려웠던 점과 어떻게 해결하였는지에 대해서
#1. 컴포넌트 내에 비동기 통신의 내부 구현 사항이 많이 노출되어 있어 가독성, 수정하거나 유지보수성이 떨어지며, Youtube Data API의 호출 제한으로 인해 최적화 필요
Youtube Data API의 일일 할당량은 10,000 쿼터로 제한되어 있고 list 메서드를 호출할 때마다 50의 할당량이 차감되어 하루 최대 평균 200번 호출할 수 있지만, StrictMode를 켜고 작업할 시 컴포넌트가 2번씩 랜더링이 발생하고, 잘못된 호출로 인해 여러 번 호출을 하게 될 경우 금방 리밋이 걸려 작업을 할 수 없었다. 이 점을 고려하여 API 호출을 최적화하고, Mock Data와 필요한 데이터만 요청하는 전략을 세우는 것이 중요했다.
매번 비교할 때마다 통신 코드 전체를 주석 처리하는 방법은 매우 비효율적이며, 실제로도 그렇게 하는 사람은 없을 것이라고 생각한.
[1차 개선]
실제 데이터를 담당하는 비동기 네트워크 로직인 `youtube.js`와 Mock Data를 이용하는 로직 `fakeYoutube.js`로 분리하여 모듈로서 구분하였다. 이를 통해 내부 구현 로직을 숨길 수 있으며 코드의 가독성을 높일 수 있었다.
# before
const {
isLoading,
error,
data: videos,
} = useQuery({
queryKey: ['videos', keyword],
// Mock 데이터 이용 로직
queryFn: async () => {
return axios
.get(`/videos/${keyword ? 'search' : 'popular'}.json`)
.then((res) => res.data.items);
},
});
# after
const { youtube } = useYoutubeApi();
const {
data: videos,
isLoading,
error,
} = useQuery({
queryKey: ['videos', keyword],
queryFn: () => youtube.search(keyword),
});
[2차 개선]
코드 구조화와 유지보수성 : 클래스를 사용하여 관련된 메서드를 하나의 객체로 묶음. 이를 통해 코드의 구조가 명확해지고, 특정 기능이나 서비스에 대한 코드 관리를 한곳에서 할 수 있어. 유지보수성과 가독성을 높일 수 있었다.
class FakeYoutube {
// 모든 네트워크 요청 메서드를 클래스 내에 정의
async search(keyword, nextToken = '') { /* ... */ }
async #searchByKeyword(keyword, nextToken = '') { /* ... */ }
async mostPopular() { /* ... */ }
async channelImageURL() { /* ... */ }
// 기타 메서드들...
}
클래스를 통해 인스턴스를 생성하면, 각 인스턴스는 독립적인 상태를 가질 수 있다. 이를 통해 여러 개의 API 서비스 객체를 생성하여 병렬로 사용하거나, 특정 조건에 따라 다른 객체를 사용할 수 있어 실제 데이터 통신 코드와 Mock 데이터 통신 코드를 분리하여 사용할 수 있었다.
import { FakeYoutube } from 'api/fakeYoutube';
import { Youtube } from 'api/youtube';
const {
isLoading,
error,
data: videos,
} = useQuery({
queryKey : ['videos', keyword],
queryFn : () => {
const youtube = new FakeYoutbe();
const youtube = new Youtube();
// 상황에 맞게 인스턴스하여,
// 실제 데이터와 Mock 데이터 상황에 맞게 사용할 수 있음
return youtube.search(keyword);
});
[3차 개선]
위의 코드대로 매번 비동기 네트워크 통신이 필요한 컴포넌트마다 인스턴스를 생성할 경우 호출할 때마다 인스턴스가 생성되는 문제가 있다.
1. 메모리 사용 증가
많은 인스턴스를 생성하면 메모리 사용량이 증가하여 성능에 부정적인 영향을 미친다.
2. 성능 저하
많은 인스턴스를 생성하고 관리하는 과정은 CPU 리소스를 소모한다. 빈번한 인스턴스 생성과 소멸은 성능 저하를 일으킬 수 있으며, 이는 특히 렌더링이 자주 발생하는 UI 컴포넌트에서 문제를 야기할 수 있다.
3. 네트워크 부하 증가
각 인스턴스가 별도의 네트워크 요청을 처리한다면, 네트워크 트래픽이 증가할 수 있다. 이는 서버에 과부하를 주거나, 클라이언트 측에서 네트워크 지연을 초래할 수 있다.
4. 관리의 복잡성
많은 인스턴스를 생성하면 코드의 복잡성이 증가한다. 인스턴스를 추적하고 관리하는 것이 어려워지며, 특히 인스턴스 간의 상태 관리가 복잡해질 수 있다. 이는 디버깅과 유지보수를 어렵게 만든다.
이러한 문제들을 해결하기 위해 다음과 같은 해결책을 고려할 수 있다.
* Context API : React의 Context API를 사용하여 인스턴스를 중앙에서 관리한다. 이를 통해 각 컴포넌트에서 인스턴스를 새로 생성할 필요가 없어지며, 메모리 사용과 성능 저하 문제를 완화할 수 있다.
* 싱글톤 패턴 : 싱글톤 패턴을 사용하여 하나의 인스턴스만 생성하고 이를 재사용한다. 이를 통해 메모리 사용을 최소화하고, 코드 관리와 유지보수성을 높일 수 있다.
싱글톤 패턴(Singleton Patten)은 객체지향 디자인 패턴 중 하나로, 어떤 클래스의 인스턴스가 오직 하나만 존재하도록 보장하는 패턴이다. 이 패턴을 사용하면 애플리케이션 전역에서 해당 클래스의 단일 인스턴스에 접근할 수 있으며, 이를 통해 객체 생성 및 관리의 효율성을 높일 수 있다.
import { FakeYoutube } from 'api/fakeYoutube';
import { Youtube } from 'api/youtube';
import { createContext, useContext } from 'react';
export const YoutubeApiContext = createContext();
const youtube = new FakeYoutube();
// const youtube = new Youtube();
export function YoutubeApiProvider({ children }) {
return (
<YoutubeApiContext.Provider value={{ youtube }}>
{children}
</YoutubeApiContext.Provider>
);
}
export function useYoutubeApi() {
return useContext(YoutubeApiContext);
}
import { useYoutubeApi } from 'context/YoutubeApiContext';
const { youtube } = useYoutubeApi();
const {
isLoading,
error,
data: videos,
} = useQuery({
queryKey : ['videos', keyword],
queryFn : () => youtube.search(keyword));
#2. "더보기" 버튼을 클릭할 경 기존 데이터는 유지하되, 새로운 데이터를 불러오는 기능 구현
이를 구현하기 위해 useQuery로 이루어진 코드를 수정하는 부분에서 많은 시행착오가 있었다.
1.
"더 보기" 버튼을 클릭하면 데이터를 축적하는 방식으로 재렌더링하는 방식으로 재구성해야 했다. 페이지네이션된 데이터를 저장할 상태와 다음 페이지 토큰을 저장할 상태를 추가했다.
const [data, setData] = useState([]);
const [nextPageToken, setNextPageToken] = useState('');
2.
비디오 데이터를 useQuery의 queryFn 부분에서 state에 담아야 하는지, 아니면 useEffect를 이용해서 데이터가 변경될 때마다 담는 방법이 좋은지 고민하며 테스트하는 과정에서, 결국 useQuery의 onSuccess 콜백을 활용하여 상태를 업데이트는 방법과 useEffect를 사용하는 방법 중에서 고민했다.
* useEffect를 사용하는 방법
useEffect를 사용하여 데이터 페칭 후 상태를 업데이트할 수 있으며, 이 방법은 페칭 로직과 상태 업데이트 로직을 분리하는 데 유용하다.
export default function Videos() {
const { keyword } = useParams();
const { youtube } = useYoutubeApi();
const [data, setData] = useState([]);
const [nextPageToken, setNextPageToken] = useState('');
const fetchLiveVideos = async (pageToken = '') => {
const result = await youtube.search(keyword, pageToken);
return {
items: result.items,
nextPageToken: result.nextPageToken,
};
};
const {
isLoading,
error,
data: videos,
} = useQuery({
queryKey: ['videos', keyword],
queryFn: () => fetchLiveVideos(),
staleTime: 1000 * 60 * 1,
});
const fetchNextPage = async () => {
if (!nextPageToken) return;
const result = await fetchLiveVideos(nextPageToken);
setData((prevData) => [...prevData, ...result.items]);
setNextPageToken(result.nextPageToken);
};
useEffect(() => {
if (videos) {
setData(videos.items);
setNextPageToken(videos.nextPageToken);
} else {
setData([]);
setNextPageToken('');
}
}, [videos]);
return ( .... )
}
* onSuccess 콜백을 사용하는 방법
React Query의 onSuccess 콜백은 데이터가 성공적으로 페칭 되었을 때 실행된다. 이 방법은 데이터 페칭과 상태 업데이트를 하나의 흐름으로 처리할 수 있다.
const {
isLoading,
error,
data: videos,
} = useQuery({
queryKey: ['videos', keyword],
queryFn: () => fetchLiveVideos(),
staleTime: 1000 * 60 * 1,
onSuccess: (data) => {
setData(data.items);
setNextPageToken(data.nextPageToken);
},
});
두 개의 방법을 모두 해본 결과, onSuccess를 사용하는 방법에 문제가 있었다. onSuccess 콜백이 호출되기 전에 컴포너트가 다시 랜더링 되면서 data상태가 업데이트되지 않는 것이다. 아마도 onSuccess 콜백이 비동기적으로 실행되기 때문인 것 같다.
3.
https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery
useInfiniteQuery | TanStack Query React Docs
Does this replace [Redux, MobX, etc]? react
tanstack.com
충격적 이게도 onSuccess에 대해 알아보다 useInfiniteQuery를 사용하면 무한 스크롤이 가능하다는 사실을 알게 되었다.
이 쿼리를 사용하면 기존의 코드를 획기적으로 줄일 수 있었다.
const {
data: videos,
error,
fetchNextPage,
hasNextPage,
isLoading,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['videos', keyword],
queryFn: async ({pageParam = ''}) => {
return youtube.search(keyword, pageParam);
},
getNextPageParam: (lastPage) => lastPage.nextPageToken || undefined,
});
getNextPageParam 함수는 useInfiniteQuery 훅에서 매우 중요한 역할은 한다. 이 함수는 마지막으로 가져온 페이지의 데이터를 기반으로 다음 페이지를 가져올 때 사용할 매개변수를 결정한다.
getNextPageParam의 매개변수로 전달되는 lastPage는 pages 배열의 마지막 요소이다. 즉, 마지막으로 가져온 페이지의 데이터이다.
* 반환된 데이터의 구조
{
"pages": [
{
"items": [
// 25개의 아이템들
],
"nextPageToken": "CBkQAA"
}
],
"pageParams": [undefined]
}
pages: 각 페이지의 데이터를 포함하는 배열
pageParams: 각 페이지를 가져올 때 사용된 파라미터를 포함하는 배열
* 반환된 데이터의 구조에서 lastPage의 데이터 형태는 다음과 같다.
{
"items": [
// 25개의 아이템들
],
"nextPageToken": "CBkQAA"
}
getNextPageParam의 매개변수로 전달되는 lastPage는 pages 배열의 마지막 요소로, 마지막으로 가져온 페이지의 데이터다. 이 데이터에는 현재 페이지의 항목들(items)과 다음 페이지를 가져오기 위한 토큰(nextPageToken)이 포함되어 있다.
위의 코드로 기존의 긴 코드를 대체할 수 있다는 사실에 신기함과 허망함이 들었다. 하지만 이렇게 React-Query에 대해 또 한 가지 배울 수 있어서 좋았다.
#3. 데이터의 형식 통일의 어려움
받은 비디오 데이터의 개별 id형식이 달라서, 이를 활용하는 컴포넌트에서 구조 분해를 해야 하는 번거로움과 props로 넘겨줄 때 어려움이 있어 이를 어떻게 하면 통일할 수 있을까 고민을 했다.
해결 방법은 스프레드 문법을 사용하는 것이었다. 스프레드 문법은 JS에서 객체나 배열을 쉽게 복사하거나 확장할 수 있게 해 준다. 이는 '...' 연산자를 사용하여 객체나 배열의 요서를 다른 객체나 배열로 확장하는 데 사용된다.
스프레드 문법을 활용하면 데이터를 쉽게 조작하고, 기존 데이터를 변형하지 않고 새로운 데이터를 생성할 수 있다.
예시
const originalObject = [
{
name: 'Alice',
details: {
age: {
만나이: 20,
나이: 21,
},
city: 'New York',
},
},
{
name: 'Alice',
details: {
age: {
만나이: 22,
나이: 23,
},
city: 'Seoul',
},
},
];
const updatedObject = originalObject.map((item) => ({
...item,
details: {
...item.details,
age: item.details.age.만나이,
},
}));
console.log(updatedObject);
위와 같이 나이의 값이 만 나이와 나이가 있을 경우 만 나이로만 가져오고 싶은 경우이다.
async #searchByKeyword(keyword, pageToken) {
try {
const response = await this.httpClient.get('search', {
params: {
q: keyword,
type: 'video',
order: 'relevance',
pageToken,
},
});
return {
items: response.data.items.map((item) => ({
...item,
id: item.id.videoId,
})),
nextPageToken: response.data.nextPageToken,
};
} catch (error) {
console.error('Error in #searchByKeyword:', error);
throw error;
}
}
위와 같이 수정하여 데이터마다의 id값을 통일시킬 수 있었다.
#4. OAuth2.0 로그인 통해 얻은 accessToken의 유효시간에 따른 갱신문제
파이어베이스에서 제공하는 구글 로그인 후 OAuth 2.0 인증된 accessToken을 발급받아 실제 유튜브 개인구독 채널 데이터를 받아오는 로직이 있다. 이 인증된 accessToken은 유효시간이 짧다.(1시간 정도)
상황을 가정해 보자. 유효시간에 맞춰 인증된 accessToken을 갱신해주지 않는다면 페이지를 새로고침하거나 다른 페이지에서 home으로 돌아올 경우 실제 유튜브 구독채널 데이터를 불러오지 못하는 상황이 발생된다.
Firebase에서 제공하는 Google 로그인을 사용하여 OAuth 2.0 토큰을 갱신할 때, 리프레시 토큰을 이용하여 새로운 액세스 토큰을 받기 위해 https://oauth2.googleapis.com/token 엔드포인트로 POST 요청을 보내야 한다. 이 과정에서 데이터를 application/x-www-form-urlencoded 형식으로 전송하는 이유와 어떻게 코드 작성을 해야 하는 알아보았다.
이유 및 과정 설명
1. OAuth 2.0 프로토콜
- OAuth 2.0 프로토콜은 토큰 갱신을 위해 정해진 규격을 따르는데, 이 규격은 보통 URL 인코딩 형식의 데이터 전송을 요구한다.
- `application/x-www-form-urlencoded` 형식은 브라우저에서 HTML 폼을 제출할 때 사용하는 기본 형식이며, 이는 서버에서 쉽게 파싱 할 수 있도록 돕는다.
2. 데이터 인코딩
- `application/x-www-form-urlencoded`는 데이터가 key=value 쌍으로 인코딩 되며, 각 쌍은 &로 구분된다.
- URL에 특수 문자가 포함되면, 자동으로 URL 인코딩이 적용된다.
3. Axios
- `axios.post`를 사용할 때 두 번째 매개변수로 데이터 본문(HTTP 요청에서 서버로 전송되는 데이터를 포함하는 부분 request body)을 전달하고, 세 번째 매개변수로 헤더를 설정한다.
- `URLSearchParams`를 사용하여 데이터를 자동으로 인코딩하고, 이를 문자열로 변환하여 요청 본문에 포함시킨다.
const response = await axios.post(
tokenEndpoint,
new URLSearchParams({
code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri,
grant_type: 'authorization_code',
}).toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
const { access_token, refresh_token } = response.data;
* `new URLSearchParams({ ... })`
데이터 객체를 `URLSearchParams`로 감싸면 각 키-값 쌍을 URL 인코딩 형식으로 변환한다.
예를 들어, `{ client_id: 'abc', client_secret: 'xyz' }`는 `client_id=abc&client_secret=xyz`로 변환된다.
* `toString()` 호출
`URLSearchParams` 객체를 문자열로 변환하여 Axios에 전달한다.
* `header` 설정
요청 헤더에 `Content-Typ: application/x-www-form-urlencoded`를 설정하여 서버에 데이터 형식을 알려준다.
application/x-www-form-urlencoded 형식은 웹에서 데이터를 전송하는 가장 기본적인 방법 중 하나이다. 주로 브라우저에서 HTML 폼을 서버로 제출할 때 사용됩니다. 이 형식을 이해하려면, HTML 폼과 HTTP 요청의 기본적인 동작 방식을 이해할 필요가 있다.
HTML 폼과 HTTP 요청
1. HTML 폼 구성:
- HTML 폼은 사용자로부터 데이터를 입력받기 위한 웹 페이지 요소이다.
- 각 입력 필드에는 `name` 속성이 부여되며, 사용자가 데이터를 입력하고 폼을 제출하면 이 데이터가 서버로 전송된다.
2. 폼 제출 과정:
- 사용자가 폼을 작성하고 제출 버튼을 클릭하면, 브라우저는 폼에 입력된 데이터를 서버로 전송한다.
- 기본적으로 폼 데이터는 `POST` 요청을 통해 서버로 전송되며, 이때의 데이터 형식이
`application/x-www-form-urlencoded`이다.
3. 데이터 인코딩 방식:
- `application/x-www-form-urlencoded`형식에서는 폼 데이터를 `key = value` 쌍으로 인코딩한다.
- 각 `key = value` 쌍은 앰퍼샌드(`&`)로 구분된다.
- 예를 들어, 사용자가 두 개의 입력 필드에 이름과 이메일을 입력했다면
name=JohnDoe&email=johndoe%40example.com
- 여기서 `name`과 `email`은 각각의 입력 필드의 `name` 속성이다.
- `application/x-www-form-urlencoded`에서 `%40`는 URL 인코딩 된 형식이다.
4. 브라우저 역할:
- 브라우저는 사용자가 입력한 데이터를 자동으로 URL 인코딩하여 서버로 전송한다.
- 서버는 이 데이터를 받아 파싱하여 각각의 `key`에 해당하는 값을 처리한다.
5. HTML 폼의 예
<form action="/submit" method="post">
<label for="name">Name:</label>
<input type="text" id="name" name="name" />
<label for="email">Email:</label>
<input type="email" id="email" name="email" />
<button type="submit">Submit</button>
</form>
사용자 입력
- `Name`에 `Jone Doe`, `Email`에 `johndoe@example.com`을 입력했다고 가정하자.
전송 형식
- 폼이 제출되면 브라우저는 다음과 같은 데이터를 서버로 전송한다.
name=John+Doe&email=johndoe%40example.com
`+`는 공백을 나타내고, `%40`은 `@`을 나타낸다.
요약
1. application/x-www-form-urlencoded 형식은 HTML 폼 데이터를 전송하는 전통적인 방식이다.
2. 데이터를 key=value 형식으로 인코딩하고, 각 쌍을 &로 구분한다.
3. 브라우저가 자동으로 이 형식으로 데이터를 인코딩하여 서버에 전송한다.
4. 이 방식은 주로 간단한 폼 데이터를 전송할 때 사용된다.
이 방식은 특히 간단한 폼 데이터를 전송할 때 유용하다.
JSON과 비교하면, 구조가 단순하며 모든 브라우저와 서버에서 기본적으로 지원한다.
JSON은 더 복잡한 데이터 구조를 처리할 때 주로 사용된다.
로그인 후 user 데이터를 console.log로 살펴보면
stsTokenManager 안에 OAuth 2.0 인증된 accessToken과 리프레시 토큰이 보인다.
이 두 값을 state로 저장하여 갱신해 보았으나....
invalid_grant에러가 발생한다. 로그인 후 얻은 리프레시 토큰을 토큰을 갱신하는 엔드포인트에 params을 잘 전달했으나 에러가 발생한다. 정말 이유를 모르겠다.
어쩌면 클라이언트 단에서 갱신할 수 있는데, 나의 지식의 부족이나 정보 검색의 부족일까.
많은 검색과 문서를 찾아봤다.
공식 사이트에서 이에 대한 내용을 찾기가 쉽지 않아 구글링과 AI을 활용해 많은 시간을 검색하고, 물어봤다.
" 이와 같은 방법으로는 할 수 없다. "
" 찾았다... "
gapi-script 라이브러리를 사용하면 Google Cloud 클라이언트에 접근하여 인증된 accessToken을 얻을 수 있다는 것이다.
그러나 이 방법은 구글에서 지원중
여기서 한 가지를 깨달을 수 있었다.
문제를 해결하기 위해서 근본적으로 그 서비스가 어떤 과정을 걸쳐서 나에게 데이터를 전달하는 지를 알아야 한다는 것이다.
https://goldenrabbit.co.kr/2023/08/07/oauth%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EA%B5%AC%EA%B8%80-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9D%B8%EC%A6%9D%ED%95%98%EA%B8%B0-1%ED%8E%B8/
OAuth를 사용한 구글 로그인 인증하기 1편 - OAuth 소개와 준비하기 - 골든래빗
'[Node.js] 자바스크립트 비동기 개념에 익숙해지기'는 총 3편에 걸쳐서 콜백 함수, 프로미스, async await 구문을 소개할 예정입니다. 1편에서는 자바스크립트 비동기 개념을 이해하고, 콜백 함수 예
goldenrabbit.co.kr
위 사이트에서 OAuth 프로토콜 흐름과 액세스 토큰을 재발행하는 흐름을 살펴보고 Google Cloud 클라이언트에 접근할 수 있는 라이브러리나 방법이 있을지 검색하여 gapi-script 라이브러리를 찾을 수 있었다.
https://developers.google.com/identity/sign-in/web/reference?hl=ko#gapiauth2getauthinstance
Google 로그인 자바스크립트 클라이언트 참조 | Authentication | Google for Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 의견 보내기 Google 로그인 자바스크립트 클라이언트 참조 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세
developers.google.com
https://github.com/google/google-api-javascript-client
GitHub - google/google-api-javascript-client: Google APIs Client Library for browser JavaScript, aka gapi.
Google APIs Client Library for browser JavaScript, aka gapi. - google/google-api-javascript-client
github.com
1. Google API 클라이언트 초기화
useEffect(() => {
function initClient() {
gapi.client
.init({
clientId: 'YOUR_CLIENT_ID',
scope: 'https://www.googleapis.com/auth/youtube.readonly',
})
.then(() => {
console.log('GAPI client initialized');
})
.catch((error) => {
console.error('Error initializing GAPI client:', error);
});
}
gapi.load('client:auth2', initClient);
}, []);
Google API 클라이언트를 초기화한다는 것은, Google의 다양한 API 서비스를 사용하기 위해 필요한 설정과 인증 과정을 완료하여 클라이언트 준비 상태로 만드는 것을 의미한다. 이를 통해 이 라이브러리를 사용할 수 있는 준비를 마친 것이다.
간단히 표현하면 "Google API 클라이언트와 OAuth 2.0 인증 기능을 사용할 거니까, 준비해! " 이런 느낌이다.
2. 갱신을 위해 refreshToken
const refreshToken = async () => {
try {
// 현재 Google 인증 인스턴스를 가져옴
const authInstance = gapi.auth2.getAuthInstance();
console.log('Auth instance:', authInstance);
// 사용자가 로그인되어 있는지 확인함
if (authInstance.isSignedIn.get()) {
// 현재 사용자의 인증 응답을 새로 고침하고, 이를 통해 새로운 액세스 토큰 얻음
const authResponse = await authInstance.currentUser
.get()
.reloadAuthResponse();
console.log('authResponse', authResponse);
const newToken = authResponse.access_token;
setAccessToken(newToken);
console.log('New access token:', newToken);
// 새로운 토큰을 사용하여 YouTube API 호출
fetchSubscriptions(newToken);
} else {
console.log('User is not signed in');
// gapi.auth2를 통해 사용자를 다시 로그인 시도
await authInstance.signIn();
// 로그인 후, 현재 사용자의 인증 응답을 가져옴
const authResponse = await authInstance.currentUser
.get()
.getAuthResponse();
const newToken = authResponse.access_token;
setAccessToken(newToken);
console.log('User re-signed in, new access token:', newToken);
// 새로운 토큰을 사용하여 YouTube API 호출
fetchSubscriptions(newToken);
}
} catch (error) {
console.error('Error refreshing access token:', error);
}
};
gapi.auth2.getAuthInstance()가 어떤 역할을 하는 메서드인지 이해하는 부분이 어려웠다.
우선 Google 인증 인스턴스를 알아야 한다.
Google 인증 인스턴스란? 먼저, "인스턴스"라는 단어부터 이해하면 어떤 프로그램이나 기능을 실제로 사용할 수 있도록 만든 "실체"를 의미한다. Google 인증 인스턴스는 Google 계정으로 로그인하거나 로그아웃을 하는 기능을 실제로 사용할 수 있게 만든 "실체"이다. 이것을 통해 Google 계정으로 로그인할 수 있고, 로그인한 상태를 확인할 수 있다.
위의 로직을 간략하게 요약하면!
1. `const authInstance = gapi.auth2.getAuthInstance()`를 통해 Google Authentication Instance를 가져옴. 이 인스턴스는 사용자 인증과 관련된 여러 메서드를 제공해 줌.
2. `authInstance.isSignedIn.get()`를 통해 로그인 상태를 true/false로 반환받음
3. `const authResponse = await authInstance.currentUser.get().reloadAuthResponse()`
authInstance(인증 인스턴스)의 속성 중 하나인 currentUser는 현재 로그인된 사용자객체를 나타내고 get()을 통해 현재 로그인된 사용자의 정보(GoogleUser 객체 안의 정보)를 가져옴. 그리고 reloadAuthResponse메서드로 현재 사용자의 인증 정보를 새로고침함.
이 과정을 걸쳐서 새로운 인증된 토큰을 반환받아 Youtube API 다시 호출하여 정보를 갱신한다.
4. 정리
이 프로젝트를 통해 나는 단순한 기술적 도전을 넘어서, 개발자로서의 성장 과정을 생생히 경험할 수 있었다. 코드 한 줄 한 줄을 작성하면서, 각 요소의 선택과 구현 방식에 대해 깊이 고민하고 이해하는 과정은 매우 값진 경험이었다.
특히 이번 프로젝트에서 얻은 주요 배움들을 꼽자면, 먼저 불균일한 JSON 데이터 처리를 위한 창의적 해결책을 고안하면서 데이터 핸들링의 중요성을 체감했다. 또한, 실제 통신 로직과 mock 데이터 사용을 효과적으로 분리함으로써 코드의 유지보수성을 크게 향상시킬 수 있었다.
React Query의 다양한 기능을 활용하여 정교한 데이터 캐싱 전략을 수립하고, 복잡한 중첩 데이터 구조를 효율적으로 다루는 방법을 익혔다.
상태 관리 측면에서는 Zustand 라이브러리를 사용해 보며 기존의 Context API와 Reducer 조합과 비교 분석할 수 있었고, 이를 통해 상태 관리에 대한 폭넓은 이해를 갖게 되었다.
Firebase를 활용한 로그인 및 데이터베이스 구현 경험은 백엔드 지식이 부족했던 나에게 특히 소중한 기회였다. 액세스 토큰 갱신 과정에서 겪은 어려움과 그를 극복하는 과정은 비록 시간이 많이 소요되었지만, 문제 해결 능력을 한층 높이는 계기가 되었다.
이러한 경험들을 바탕으로, 나는 앞으로도 개발자로서의 여정을 계속해 나가고자 한다. 더 많은 동료 개발자들과 소통하며 지식을 나누고, 새로운 도전에 맞서는 과정에서 나의 열정과 목표를 끊임없이 되새기고 싶다. 이 프로젝트는 나의 성장 여정에 중요한 이정표가 되었으며, 앞으로의 발전 가능성에 대한 기대감을 한층 더 높여주었다.
'Project' 카테고리의 다른 글
[React Project] Momentum - Lighthouse 최적화 (5) | 2024.11.06 |
---|---|
[React Project] Momentum ver 1 .1 .1 리팩토링 과정 중 문제&해결 (0) | 2024.10.18 |
[React Project] Momentum ver 1 .0 .1 (0) | 2024.10.08 |
[React Project]F.ROCK🥻(Shopping mall) (0) | 2024.08.15 |
[React Project] Momentum (0) | 2024.05.25 |