Skip to main content

Next.js에 Google Drive API를 달아보자

Google API의 경우, 대부분이 서버에서 API를 호출해야합니다.

그렇기에 React보다는 Next.js에서 서버상에서 호출 후, 프론트에서 해당 API 엔드포인트에서 값을 얻는 것이 가장 빠르다고 봅니다.

// pages/index.tsx

const HomePage = () => {
const [isSuccess, setIsSuccess] = useState(false);
const [authUrl, setAuthUrl] = useState("");

// 로그인/인증 URL 얻기
const fetchAuthUrl = useCallback(async () => {
try {
const response = await axios.get(
`${process.env.API_ENDPOINT_ORIGIN}/api/auth`
);
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
if (code) {
fetchTokens(code);
setIsSuccess(response.data.success);
} else if (!response.data.success) {
setAuthUrl(response.data.url);
setIsSuccess(response.data.success);
} else {
setIsSuccess(response.data.success);
}
} catch (err) {
console.log("err", err);
}
}, []);

const getFiles = async () => {
const response = await axios.get(
`${process.env.API_ENDPOINT_ORIGIN}/api/drive`
);
// Google Drive 파일 목록이 나옴
console.log(response);
};

// Google에서 리다이렉트된 후 인증 코드 사용하여 토큰 얻기
const fetchTokens = async (code: string) => {
try {
await axios.post(`${process.env.API_ENDPOINT_ORIGIN}/api/auth`, {
code,
});
} catch (err) {
console.log("fetchToken error", err);
}
};

// 로그아웃
const handleLogout = async () => {
const res = await axios.delete(
`${process.env.API_ENDPOINT_ORIGIN}/api/auth`
);
// 로그아웃 후 페이지 리프레시 또는 다른 처리
if (res) {
window.location.pathname = "";
window.location.search = "";
window.location.reload();
}
};

useEffect(() => {
fetchAuthUrl();
}, [fetchAuthUrl]);

return (
<>
{isSuccess ? (
<>
<button onClick={handleLogout}>로그아웃</button>
<button onClick={getFiles}>파일 가져오기</button>
</>
) : (
<button onClick={(window.location.href = authUrl)}>
구글 드라이브 연결하기
</button>
)}
</>
);
};

export default HomePage;

API_ENDPOINT_ORIGIN의 경우, 로컬에서는 localhost:3000을 따르고, 배포 시에는 해당 오리진에 맞게 할당하면 됩니다.

이제 서버 로직을 짭니다.

// pages/api/auth.ts
import oauth2Client from "@/utils/googleAuth";
import { NextApiRequest, NextApiResponse } from "next";

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
// CORS 프리플라이트 요청 처리
if (req.method === "OPTIONS") {
res.status(200).end();
return;
}

if (req.method === "GET") {
const refreshToken = req.cookies.refresh_token;

// 이미 로그인했었던 이력이 있는지 확인
if (!refreshToken) {
const url = oauth2Client.generateAuthUrl({
access_type: "offline",
prompt: "consent",
scope: ["https://www.googleapis.com/auth/drive"],
redirect_uri: process.env.API_ENDPOINT_ORIGIN,
});
res.status(200).json({ success: false, url });
}

// 리프레시 토큰으로 액세스 토큰 갱신
oauth2Client.setCredentials({ refresh_token: refreshToken });

const newTokens = await oauth2Client.refreshAccessToken();
oauth2Client.setCredentials(newTokens.credentials);

res.status(200).json({ success: true });
} else if (req.method === "POST") {
const { code } = req.body;
const { tokens } = await oauth2Client.getToken(code);
oauth2Client.setCredentials(tokens);

// 리프레시 토큰을 HTTP Only 쿠키에 저장
if (tokens.refresh_token) {
res.setHeader(
"Set-Cookie",
`refresh_token=${tokens.refresh_token}; HttpOnly; Path=/; Max-Age=${
7 * 24 * 60 * 60
}`
);
}

res.status(200).json({ success: true });
} else if (req.method === "DELETE") {
// 로그아웃 처리
// 리프레시 토큰 쿠키 삭제
res.setHeader(
"Set-Cookie",
"refresh_token=; HttpOnly; Path=/; Max-Age=0"
);
res.status(200).json({ success: true });
} else {
res.status(405).end(); // Method Not Allowed
}
} catch (err) {
console.log("ERR", err);
res.status(500).json({ error: "Internal Server Error" });
}
}