Next.js 14버전부터 서버 액션이 정식 기능에 포함되면서, 이를 토이 프로젝트에서 사용해보고 있다.
문제 상황은 서버 컴포넌트에서 클라이언트 컴포넌트로 서버 액션을 전달하고, 서버 액션에서 mongodb에 접근하여 문서를 검색하고 그 결과를 반환하는 코드에서 발생했다.
Warning: Only plain objects can be passed to Client Components from Server Components. Objects with toJSON methods are not supported. Convert it manually to a simple value before passing it to props.
{_id: {}, id: ..., comment: ..., updatedAt: ..., expireAt: ..., }
^^
사용자 인터렉션이 일어나는 부분을 클라이언트 컴포넌트로 분리하고, 서버 액션을 props로 전달한다.
// /app/auth/page.tsx
import * as styles from '@/app/auth/AuthPage.css';
import RegisterCertificationSection from '@/app/auth/_component/RegisterCertificationSection';
import { getCertification } from '@/app/auth/actions';
export default async function AuthPage() {
return (
<div>
{/* ... */}
<div className={styles.registerCertificationZone}>
{/* ... */}
<RegisterCertificationSection actions={{ getCertification }} />
</div>
</div>
);
}
apiKey 입력 -> id 계산 -> 데이터베이스 조회 순서로 작동하는 서버 액션 함수를 작성한다.
// /app/auth/actions
'use server';
import { findCertification } from '@/app/auth/database';
export const getCertification = async (apiKey: string) => {
const ouid = await fetchOuid(apiKey);
const id = hashOuid(ouid);
const certification = await findCertification(id);
return certification; // WithId<Certification> | null
};
// /app/auth/database.ts
'use server';
import { Certification } from '@/app/_model/maplestory/certification';
import mongoClient from '@/app/_config/database';
export const findCertification = async (userId: string) => {
const database = (await mongoClient).db('데이터베이스이름');
const collection = database.collection<Certification>('certifications');
const certification = await collection.findOne({ id: userId });
return certification; // WithId<Certification> | null
};
사용자에게서 API Key를 입력받고, 폼을 제출할 때 서버에서 해당 키와 동일한 인증서를 가져온다.
'use client';
import * as styles from './RegisterCertificationSection.css';
import { ChangeEventHandler, FormEventHandler, useState } from 'react';
import { Certification } from '@/app/_model/maplestory/certification';
export default function RegisterCertificationSection({ actions }: Props) {
const [apiKeyInput, setApiKeyInput] = useState(nxopenApiKey);
const onSubmitApiKey: FormEventHandler<HTMLFormElement> = async (e) => {
// ...중간 로직 생략
const certification = await actions.getCertification(apiKeyInput);
console.log(certification);
};
return (
<div>
{/* ... */}
<form className={styles.validationForm} onSubmit={onSubmitApiKey}>
<input
className={styles.apiKeyInput}
value={apiKeyInput}
onChange={onChangeApiKey}
placeholder="API Key를 입력해주세요."
/>
<button className={styles.searchButton} type="submit">
검색하기
</button>
</form>
</div>
);
}
mongodb의 findOne()
의 결과로 WithId<T>
타입의 데이터가 반환된다.
WithId<T>
속에는 _id
라는 필드가 자동으로 ObjectId
클래스의 인스턴스를 반환한다.
에러 메세지에 나와 있는 것 처럼, 순수한 자바스크립트 객체만을 넘겨줄 수 있기 때문에 이러한 경고 메세지가 발생한다.
간단하게 certification._id = certification._id.toString()
과 같이 처리해도 되지만, 설정에 따라 타입스크립트 경고 메세지가 표시된다.
나는 별도의 withStringId
라는 함수를 구현하였고, 서버에서 데이터를 반환할 때 감싸주도록 코드를 수정했다.
// /app/_utils/database/withStringId.ts
import { WithId } from 'mongodb';
export type WithStringId<T> = Omit<WithId<T>, '_id'> & {
_id: string;
};
type FindResult<T> = WithId<T> | null;
const withStringId = <T>(result: FindResult<T>): WithStringId<T> | null => {
if (result === null) {
return null;
}
const { _id, ...rest }: WithId<T> = result;
return {
_id: _id.toString(),
...rest,
};
};
export default withStringId;
여기서 값을 반환하는 부분을 데이터베이스에 접근하는 함수 자체에서 처리할 수 있다. 하지만 데이터베이스에 접근하는 코드는 해당 기능만 담당하고, 이를 활용하고 응답하는 서버 액션 함수에서 감싸는게 더 좋다고 생각했다.
// /app/auth/actions.ts
'use server'
import { findCertification } from '@/app/auth/database';
import { withStringId } from '@/app/_utils/database';
export const getCertification = async (apiKey: string) => {
const ouid = await fetchOuid(apiKey);
const id = hashOuid(ouid);
const certification = await findCertification(id);
return withStringId(certification);
};
mongoose
를 사용한다면 조회 이후 lean()
이라는 메소드를 통해 단순화 시킬 수 있다고 한다. 이번 문제도 동일하게 해결할 수 있지 않을까?