[Notion] 방명록 만들기 (feat. 엄준식 사태)

차차·2024년 3월 31일
2
post-thumbnail

↗️ 방명록 써보기!

갑자기 노션에 방명록 ?

노션 구현을(+ 배포) 마치고, 구현한 기능을 활용하여 특정 문서에 방명록 서식을 작성했다.
한 4~5 명의 방명록이 작성되어 있었는데 🥲 .. 예상치 못한 상황이 벌어졌다!

아래 사진과 같이 노션이 ‘엄준식’ 으로 도배를 당한 것이다.

방명록에 있었던 친구의 응원 메시지와 감사한 피드백이 다 없어져있었다. 비록 많지는 않았지만 나름 방명록을 보고 뿌듯함을 느끼고 있었기 때문에, 조금은 속상했다!

따라서 이런 사태를 막을 수 있는 방법에 대해 고민했고, 방명록 기능을 구현하게 되었다 (도배해주신 분 감사합니다 재밌었어요 👍)



방명록 기능 생각하기

비밀번호 걸기

방명록 기능을 새로 구현하여 해결해야 하는 문제는 분명하다.

" 본인이 작성하지 않은 글은 건드리지 못하게 하기 "

비밀번호를 입력하여 글을 작성하고, 수정/삭제를 하려면 비밀번호를 맞춰야 하는 방명록을 구현하고자 했다.

  1. 방명록 작성

    • 닉네임 / 비밀번호 / 내용 / 프로필 입력
  2. 방명록 수정 또는 삭제

    • 해당하는 글의 수정/삭제 버튼 클릭 ⇒ 비밀번호 입력
    • 비밀번호 틀리면? ⇒ 틀렸다고 알려주기
    • 비밀번호 맞추면? ⇒ 수정 폼으로 바뀌기

방명록 API 는 없다

노션 프로젝트는 데브코스 때 제공받은 api 를 사용한 것이다. 문서 추가, 조회, 수정, 삭제가 가능하다. 당연히도, 방명록을 위한 기능이 따로 있지는 않았다.
따라서 하나의 문서를 방명록 전용으로 활용하기로 하고, 기존 api 를 활용할 수 있는 방법에 대해 생각했다.

노션 api 에서는 response, request 에서 아래와 같은 데이터를 주고 받는다.

특정 Document의 content 조회하기

// response data
{
  id: "1",
  title: "문서 제목",
  content: "문서 내용",
  documents: [
    // 하위 문서들의 정보
  ],
  createdAt: "",
  updatedAt: ""
}

특정 Document 수정하기

// request body
{
  title: "문서 제목",
  content: "문서 내용"
}

이러한 기존 API 를 활용하려면 특정 필드안에 데이터를 담아야 한다.
문서 내용이 담겨있는 content 필드를 활용하여, [id]: 내용 으로 이루어진 방명록 글 리스트를 넣기로 했다.

// content 에 들어가야 하는 내용
{
	content: {
		'id1': {
			username: "작성자1",
			content: "내용",
			password: "123456",
			updateAt: "2024-03-28",
			profile: { 
				background: "ffffff",
				charactor: "Tom"
			},
		},
		'id2': {
			username: "작성자2",
			content: "내용",
			password: "121212",
			updateAt: "2024-03-28",
			profile: { ... },
		}
	}
}


NotionAPI 확장하기

위와 같이 특정 필드를 원하는 객체 형식으로 확장하기 위해서는, 중간 작업을 누군가가 해주어야 한다.

데이터를 받아올 때는 사용하려는 용도에 맞게 확장하고, 서버에 보낼 때는 API 명세에 맞게 다시 축소시켜야 한다. 자유로운 확장 및 축소를 위해, string 타입과 객체 타입을 왔다 갔다 할 수 있도록 JSON 을 활용할 수 있다!


객체 ↔ 문자열

객체와 문자열 변환을 위해서는, JSON.parse(...)JSON.stringify(...) 를 활용하면 된다.

주의할 점은, JSON.parse 에 변환이 불가능한 문자열을 넣게 되면 에러가 난다는 것이다. 따라서 객체로 변환하는 메소드(expandValue)는 초기값도 같이 받아주어야 한다.

// 객체를 문자열로 축소
const reduceValue = (target: any) => {
  return JSON.stringify(target);
};

// 문자열을 객체로 확장
const expandValue = (initial: any, target: string) => {
  try {
    return JSON.parse(target);
  } catch (error) {
    return initial;
  }
};

api 트랜스포머 만들기

이제 위에서 구현한 간단한 메소드를 활용하여, responseDatarequestBody 를 변환시켜주는 친구를 만들면 된다!

여러가지 방법이 있겠지만.. 많은 시행착오 끝에..
원하는 형태를 담아서 보내면, 확장/축소 메소드를 반환해주는 인터페이스로 구현하였다.

// 이렇게 사용!

const { reduceRequest, expandResponse } = transformAPI({ content: {} }) 
// transformAPI 타입

type TransformAPI = <From, To>(initialObj: Partial<To>) => {
  reduceRequest: (requestBody: To) => From;
  expandResponse: (responseBody: From) => To;
}

매개변수 initialObj 는 '이렇게 사용하고 싶어요!' 를 담은 객체이다. 즉, initialObj 의 키 값들은 변형이 이루어져야 하는 키이다.

위 예시라면, content 값을 변환한다는 뜻이다.

PUT 요청 시에는 reduceRequest 를 거쳐서 보내고, GET 요청 시에는 expandResponse 를 거쳐서 받으면 된다.

reduceRequest
객체를 requestBody 용으로 바꾸기

확장된 객체인 expandedBody 를 올바른 requestBody 로 축소하는 메소드이다.
expandedBody 를 순회하면서, 변형이 이루어져야 하는 값을 문자열로 축소시킨다.

// 변형이 일어나야 하는 키값들
const targetKeys = Object.keys(initialObj);

const reduceRequest = (expandedBody: To): From => {
  const requestBody: Default = {};
  
  // 확장 객체 순회
  for (const key of Object.keys(expandedBody)) {
	  // 축소해야 하는 키라면 축소하기
    if (targetKeys.includes(key)) {
      requestBody[key] = reduceValue(expandedBody[key]);
      
    // 아니라면 그대로 넣기
    } else {
      requestBody[key] = expandedBody[key];
    }
  }
  return requestBody as From; // 값들을 문자열로 축소한 requestBody 반환
};

expandResponse
responseData 를 원하는 형태로 확장하기

서버에서 받아온 responseData 를 입맛에 맞게 expandedData 로 확장하는 메소드이다.
responseData 를 순회하면서, 확장되어야 하는 값을 만나면 문자열을 객체로 변환한다.

const expandResponse = (responseData: From): To => {
  const expandedData: Default = {};
  
  // responseData 순회
  for (const key of Object.keys(responseData)) {
	  // 확장해야 하는 키라면 확장하기
    if (targetKeys.includes(key)) {
      expandedData[key] = expandValue(initialObj[key], responseData[key]);
      
    // 아니라면 그대로 넣기
    } else {
      expandedData[key] = responseData[key];
    }
  }
  return expandedData as To; // 값들을 객체로 확장한 expandedData 반환
};

이제 올바르게 작동하는 지 테스트를 거쳐보자!

interface FromType {
  title: string;
  content: string;
}

interface ToType {
  title: string;
  content: {
    user: string;
    password: string;
    content: string;
  }[];
}

const { reduceRequest, expandResponse } = transformAPI<FromType, ToType>({
  content: [],
});

위에서 반환된 메소드들을 사용한 데이터의 변화 과정을 콘솔에 출력해 보았다.

콘솔 출력 내용

  1. 기본 데이터 (FromType)
  2. 확장 (ToType)
  3. 데이터 추가 (아마도 방명록 작성!)
  4. 축소 (FromType)
  5. 확장 (ToType)

reduce 를 거치면 content 값이 문자열로, expand 를 거치면 객체로 잘 변환되는 것이 보인다 👍
(아! 어디까지나 이 api는 헤더에 넣어 전송하는 username을 기준으로, 혼자 쓸 수 있기 때문에 유연성을 추가할 수 있는 것이다!)



방명록 뚝딱뚝딱

비밀번호 폼 구현

구현한 transformAPI 를 활용하여, 서버에 방명록을 저장하고 불러오는 부분은 쉽게 해결할 수 있었다. 하지만 방명록 기능의 목적은 어디까지나 비밀번호를 통한 글 잠금!!

몇 개의 익명게시판 UI 를 참고하여 동작 방식을 구상할 수 있었다.
수정/삭제 버튼을 클릭하면 비밀번호 입력창이 나타나고, 입력한 비밀번호가 일치해야 수정UI 로 바뀌는 방식이다.

코드로 간단하게 작성하면 아래와 같다.
(기존 코드베이스를 바탕으로 구현했기 때문에 다 쓰면 TMI가 될 것 같아서 .. 핵심 부분만 작성했다! 거의 의사코드)

1. 수정/취소 버튼 클릭 시 비밀번호 입력창으로 전환

특정 글에서 수정/취소 버튼을 클릭하면, 버튼을 비밀번호 입력창으로 전환한다.
렌더링 부분에서, 방명록 글의 id 가 editingId 와 같다면 비밀번호 입력창을 보여주어야 한다.

// 수정/취소 버튼 클릭 시
this.addEvent("click", (...) => {
  const id = 수정 버튼 클릭이 발생한 글의 ID;
  
  // 찾은 id를 editingId 상태값으로 설정 + 리렌더링
  this.setState({ editingId: id });
});

2. 비밀번호 검사 후 수정 컴포넌트로 전환

비밀번호 입력 후 제출하면 입력한 비밀번호를 확인한다.
입력 값과 저장된 비밀번호를 비교하여 일치하지 않으면 알림 메시지를 표시하고, 일치하면 수정 폼으로 전환!

// 비밀번호 입력 후 제출 시
this.addEvent("submit", (...) => {
  const editingId = 수정을 시도하는 글의 아이디
  const password = 저장되어 있는 진짜 비밀번호
  const passwordValue = 입력창에 작성한 비밀번호

  // 비밀번호가 틀리면 알려주고 끝내기
  if (
    password !== passwordValue &&
    MASTER_PASSWORD !== passwordValue
  ) {
    alert("비밀번호가 일치하지 않습니다.");
    return;
  }

  // 비밀번호 일치하면 수정 컴포넌트로 변환
  this.addComponent(WriteForm, ...)
})

방명록글 → 비밀번호입력 → 글수정 순으로 UI가 전환된다!


귀여운 프로필 넣기

프로필을 넣을 수 있도록 하고 싶었다. 하지만 requestBody 를 만드는 과정에서 문자열로 변환되어야 하기 때문에, 이미지 파일을 업로드하는 기능은 매우 어려운 일이다. 그러던 중 꽤나 유용한 것을 발견했다!

Playground | DiceBear
원하는 테마/캐릭터/배경화면을 주소에 담아 보내면, 이미지를 주는 아바타 생성기 api 이다.

아래와 같은 주소로 리소스를 요청하면, notionists-neutral 테마 + 키값이 Princess + 배경색상이 #ffd5dc 인 아바타 이미지를 받을 수 있다.

https://api.dicebear.com/8.x/notionists-neutral/svg?seed=Princess&backgroundColor=ffd5dc

이 친구를 활용하여, 프로필을 고를 수 있는 기능을 쉽게 완성할 수 있었다.
기능적으로는 상당히 간단하다! 사용자가 선택한 background 값과 charactor 값을 저장하고, 받아온 데이터는 이미지 주소로 변환하면 된다.

// background, charactor 저장
{
  content: {
    'id1': {
        profile: { 
          background: "ffd5dc",
          charactor: "Princess"
        },
        // 생략
    },
  }
}
// 이미지로 변환
const makeImageSrc = (
	charactor: string, 
	background: string
) => `https://api.dicebear.com/8.x/notionists-neutral/svg?seed=${charactor}&backgroundColor=${background}`

// `<img src='${makeImageSrc(charactor, background)}' />`

짜자잔..! ✨



정리와 미니 회고

  1. 도배로 인해 다른 분들이 작성해 주신 방명록이 다 날라감
  2. 그래서 비밀번호 기능 있는 방명록 만듦
  3. api 유연하게 사용할 수 있는 메소드 구현..!
  4. 보너스로 귀여운 프로필도 넣음..!

약 3개월 간 붙잡고 있었던 노션 프로젝트를 일단 마무리 짓고자 한다 🥳

타입스크립트로 스토어, 라우터 등등을 구현하여 SPA를 구축해나가기도 했고, 어려울까봐 미뤄왔던 리치 에디터를 구현하기도 했다. 흠.. 느낀점은 상당히 많지만... 레슨런으로 스스로에게 기록하고 싶은 부분은, '틀이 깔끔해야 내부 내용을 쌓기 편하다는 것'이다.
많은 고민을 거쳐서 틀(추상화, 비즈니스 레이어)을 만들었기 때문에, 자잘한 기능(에디터, 이모지, 방명록 ...)을 쉽게 완성할 수 있었다고 생각한다. 아키텍처와 디자인 패턴에 힘을 쏟는 이유를 조금이나마 알게 되었달까..!

숲을 보라는 명언이 있는데.. 더 나아가 숲을 그릴 수 있는 사람이 되어야 겠다고 생각한다. 그래야 나무를 심을 수 있기 때문이다 😄😄😄


0개의 댓글