[원티드 프리온보딩 프론트엔드][3주차 1차 과제] 고객센터 페이지 리팩토링 - 정적 페이지 렌더링, 엔드포인트 상수화, 중복 로직 제거

프로젝트를 진행하면서 시간에 쫓기다보니 고객센터 페이지를 정적방식으로 렌더링하는 것은 우선순위에 밀리게 되었다.
스스로도 보기 민망할만큼 좋지 않은 코드를 썼다는 것은 알고 있었다..ㅠ
하루 안에 프로젝트 자체를 완성하는 것에 초점을 두되, 반드시 돌아보고 더 나은 코드로 개선하는 과정을 거치기로 마음먹었기 때문에..!
프로젝트가 끝난 시점에서 다시한번 리팩토링해보았다.


소스코드를 보고 싶다면
👉 깃허브 바로가기


1️⃣ 정적 페이지 렌더링 하기

이 부분은 엄밀히 말하면 리팩토링보다는 시간관계상 끝마치지 못했던 부분을 제대로 마무리하는 것이다. 고객 센터 페이지는 유동적으로 데이터를 호출해올 필요가 없기 때문에 Static rendering을 사용하기로 결정했고, 이 부분을 추가했다.

❌ Before

일반적인 CSR방식으로 데이터를 호출했다.


function Contacts({ data }) {
  const [qaTypes, setQaTypes] = useState([]);
  const [qas, setQas] = useState([]);
  const [clickedType, setClickedType] = useState(1);

  
  async function fetchFAQTypes() {
    const res = await axios.get('https://api2.ncnc.app/qa-types');
    setQaTypes(res.data.qaTypes);
  }
  
  async function fetchFAQ(qaTypeId = 1) {
    const res = await axios.get(
      `https://api2.ncnc.app/qas?qaTypeId=${qaTypeId}`,
    );
    setQas(res.data.qas);
  }

  function onClickFAQType(typeId) {
    fetchFAQ(typeId);
    setClickedType(typeId);
  }

  useEffect(() => {
    fetchFAQTypes();
    fetchFAQ();
  }, []);

}


export default Contacts;

⭕️ After

getStaticProps로 데이터를 호출한다음 props로 전달 받아 사용했다.


function Contacts({ qaTypes, qas }) {
  const [clickedType, setClickedType] = useState(1);
  const [qaList, setQaList] = useState(qas[1]);
  
  function onClickFAQType(typeId) {
    setClickedType(typeId);
    setQaList(qas[typeId]);
  }
}

// 생략

export const getStaticProps: GetStaticProps = async (context) => {
  const qaType = {
    buy: 1,
    sell: 2,
  };
  const qaTypesRes = await axios.get(FAQ_TYPE);
  const buyQaRes = await axios.get(FAQ_CONTENT(qaType.buy));
  const sellQaRes = await axios.get(FAQ_CONTENT(qaType.sell));

  return {
    props: {
      qaTypes: qaTypesRes.data.qaTypes,
      qas: {
        1: buyQaRes.data.qas,
        2: sellQaRes.data.qas,
      },
    },
  };
};

export default Contacts;

2️⃣ api 엔드포인트 상수화

엔드포인트 자체를 잘 관리했어야 했는데, 그대로 넣어 호출했다.
팀 내에서 호출하는 api의 기본 url은 같고 파라미터가 달랐기 때문에 더더욱 일일히 넣어 호출하는 것이 좋지 않았다.

❌ Before

  async function fetchFAQTypes() {
    const res = await axios.get('apiEndpointIsHere');
    setQaTypes(res.data.qaTypes);
  }
  
  async function fetchFAQ(qaTypeId = 1) {
    const res = await axios.get(
      `apiEndpointIsHere=${qaTypeId}`,
    );
    setQas(res.data.qas);
  }

⭕️ After

constas/api.ts에 api 엔드포인트를 별도로 관리했다.

const BASE_URL = 'apiEndpointBaseUrlIsHere';
export const FAQ_TYPE = `${BASE_URL}/qa-types`;
export const FAQ_CONTENT = (qaTypeId: number) =>
  `${BASE_URL}/qas?qaTypeId=${qaTypeId}`;

3️⃣ 함수 로직 수정

❌ Before

const [qaList, setQaList] = useState(qas.buyQa);
  function onClickFAQType(typeId) {
    setClickedType(typeId);
    if (typeId === 1) {
      setQaList(qas.buyQa);
    } else {
      setQaList(qas.sellQa);
    }

조건문을 불필요하게 사용했고, 이후 유지보수에도 좋지 않은 코드라고 판단했다.


⭕️ After

데이터에서 각 qaType의 id는 1,2 숫자로 구분된다.
받아온 qaTypes를 맵핑하면서 qaType.id를 바로 전달해주어 이것을 바로 사용했다.

 {qaTypes.map((qaType) => (
            <S.FAQButton
              isClicked={qaType.id === clickedType}
              key={qaType.id}
              onClick={() => onClickFAQType(qaType.id)}

getStaticProps 함수에서도 이와 동일하게 정의했다.

  return {
    props: {
      qaTypes: qaTypesRes.data.qaTypes,
      qas: {
        1: buyQaRes.data.qas,
        2: sellQaRes.data.qas,
      },

이 id를 그대로 받아 props로 받은 qas의 key로 사용해 값을 사용한다.

  const [qaList, setQaList] = useState(qas[1]);
  function onClickFAQType(typeId) {
    setClickedType(typeId);
    setQaList(qas[typeId]);
  }

즉,

  1. qaTypes는 id가 1인 구매, id가 2인 판매로 나뉜다. 이것을 맵핑해 2개의 구매와 판매 버튼을 만들었다.

  2. 각각의 버튼을 클릭하면 버튼의 각 id가 함수의 인자로 전달되고, onClick에 등록된 함수는 이 id를 인자로 받는다.

  3. FAQ데이터는 해당하는 qaTypes의 id를 key로 하여 객체 안에서 각각의 배열에 저장되어 있기 때문에 전달받은 id를 key로 하여 해당하는 qaType의 FAQ질문답변 데이터를 qaList라는 state에 넣는다.

  4. 이 qaList는 질문 목록을 맵핑하는데 사용되기 때문에, 클릭한 버튼의 타입에 해당하는 데이터를 넣어준다. 따라서 클릭한 버튼의 qaType에 해당하는 질문목록을 보여줄 수 있다.


4️⃣ 중복되는 조건부 렌더링 없애기

❌ Before


클릭하지 않은 버튼과 클릭한 버튼을 조건부렌더링했다.
조건부렌더링은 가독성을 향상시키는데 써야 하는데, 오히려 가독성을 해치는 코드가 되었다. 한 눈에 파악하기도 힘들 뿐더러 비효율적인 코드였다.

          {qaTypes.map((qaType) =>
            qaType.id === clickedType ? (
              <S.ClickedFAQButton
                key={qaType.id}
                onClick={() => setQaList(qas)}
              >
                {qaType.name}
              </S.ClickedFAQButton>
            ) : (
              <S.FAQButton
                key={qaType.id}
                onClick={() => onClickFAQType(qaType.id)}
              >
                {qaType.name}
              </S.FAQButton>
            ),
          )}

버튼과 클릭한 버튼의 클래스를 구분해 둘 다 작성해두었기 때문에 중복되는 속성이 많아졌다.

export const FAQButton = styled.button`
  width: 169px;
  height: 40px;
  margin: 0 ${contactsStyle.margin.M} 0 ${contactsStyle.margin.M};
  color: ${({ theme }) => theme.color.textGray};
  &:hover {
    margin: 0 ${contactsStyle.margin.M} 0 ${contactsStyle.margin.M};
    border-bottom: 2px solid ${({ theme }) => theme.color.pointRed};
    color: ${({ theme }) => theme.color.pointRed};
  }
`;

export const ClickedFAQButton = styled.button`
  width: 169px;
  height: 40px;
  margin: 0 ${contactsStyle.margin.M} 0 ${contactsStyle.margin.M};
  border-bottom: 2px solid ${({ theme }) => theme.color.pointRed};
  color: ${({ theme }) => theme.color.pointRed};
`;

⭕️ After

isClicked라는 boolean 값을 넘겨주었다.

  return (
    <>
          {qaTypes.map((qaType) => (
            <S.FAQButton
              isClicked={qaType.id === clickedType}
              key={qaType.id}
              onClick={() => onClickFAQType(qaType.id)}
            >
              {qaType.name}
            </S.FAQButton>
          ))}
    </>

interface를 정의한 뒤, isClicked props에 따라 보더와 글씨 색상만 조건부로 결정하도록 했다.

interface FAQButton {
  isClicked: boolean;
}

export const FAQButton = styled.button<FAQButton>`
  width: 169px;
  height: 40px;
  margin: 0 ${contactsStyle.margin.M} 0 ${contactsStyle.margin.M};
  border-bottom: 2px solid
    ${({ isClicked, theme }) =>
      isClicked ? theme.color.pointRed : theme.color.white};
  color: ${({ isClicked, theme }) =>
    isClicked ? theme.color.pointRed : theme.color.black};
`;

좋은 웹페이지 즐겨찾기