프로젝트/하루한냥

[하루한냥] 심리 테스트 구현 : 라디오 버튼 구현하기

알파카털파카 2023. 10. 2. 13:54
[하루한냥]
심리 테스트 구현 : 라디오 버튼 구현하기

 

 

라디오 버튼은 예전에 시도했다가 어려워서 잠시 홀드해놓은 학습 내용이었다. 프로젝트를 하면서 더 이상 피할 수 없어졌고, 이참에 HTML Input 태그의 타입과 radio 버튼을 공부했다. 그러나 HTML로 구현하는 것과 React + TypeScript 프로젝트에서의 구현은 조금 차이가 있었다. 몇 가지 꼼수 같기도, 팁 같기도 한 부분이 존재했다. <input> 태그는 안 보이도록 처리하고 <i> 태그로 새로 스타일링을 하면 원하는 스타일대로 커스텀할 수 있다는 점을 배웠다. 

 

 


 

 

1. 마크업 작성하기

구현하려는 화면은 이렇다. 총 10개의 질문이 있고, 각 질문에 정도를 나타내는 5가지의 답변이 있다. 질문과 답변 5가지가 한 세트이다. 이 중 이용자가 한 가지 버튼만 선택할 수 있도록 라디오 버튼으로 구현하려고 한다. 

 

질문 페이지

 

답변의 점수와 텍스트는 배열로 정의해뒀다. 

 

export const answerTitle = [
  {
    score: 0,
    text: '전혀 아님',
  },
  {
    score: 1,
    text: '거의 아님',
  },
  {
    score: 2,
    text: '보통',
  },
  {
    score: 3,
    text: '자주 있음',
  },
  {
    score: 4,
    text: '매일',
  },
];

 

이 배열을 map으로 순회하면서 Radio라는 이름을 가진 label 태그를 렌더링한다. 내부에 input 태그를 추가하고, type으로 꼭 'radio'를 지정해주도록 한다. 라디오 그룹 요소를 정의할 때 동일한 name을 가진 묶음이 같이 묶이기 때문에 name 지정에 주의하자.

 

name을 제대로 지정하지 않은 경우

 

{
  AnswerTitle.map((answer) => (
    <Radio key={answer.text}>
      <input
        type="radio"
        name={`question-${questionIndex}`}
        value={answer.score}
        onChange={(e) => handleChangeAnswer(e, questionIndex)}
      />
      <i/>
      <Typography variant="body4" style={{marginTop: 8}}>
        {answer.text}
      </Typography>
      </Radio>
  ))
}

 

더보기

질문 + 답변 5가지 묶음 코드

{questions &&
  questions.map((question: any, questionIndex: number) => (
    <QuestionContainer key={question.text}>
      <QuestionTitle>
        <Typography variant="body4" fontWeight={600}>
          {question.seq}.
        </Typography>
        <Typography variant="body4" fontWeight={600}>
          {question.text}
        </Typography>
      </QuestionTitle>
      <AnswerContainer>
        <>
          {answerTitle.map((answer) => (
            <Radio key={answer.text}>
              <input
                type="radio"
                name={`question-${questionIndex}`}
                value={answer.score}
                onChange={(e) => handleChangeAnswer(e, questionIndex)}
              />
              <i />
              <Typography variant="body4" style={{ marginTop: 8 }}>
                {answer.text}
              </Typography>
            </Radio>
          ))}
        </>
      </AnswerContainer>
    </QuestionContainer>
  ))}

 

 

 

 

2. 스타일 입히기

이 프로젝트에서 포인트를 줄 때 연한 하늘색 색상을 사용한다. 그래서 사용자가 라디오 버튼을 클릭했을 때, 선택된 버튼에 이 색상을 주려고 한다. 체크 표시를 추가해 확실하게 선택되었음을 알린다. 몇 가지 트릭을 이용하기 때문에 이 부분을 주의해야 한다.

 

라디오 버튼 예시 이미지

 

 

input 요소 스타일링하기

기존의 input 태그를 visibility: hidden으로 숨기고, i 태그를 이용해 대신 스타일링한다. display: none과 달리 visibility: hidden은 DOM tree에 남아 있기 때문에 기능 추가하기 등이 가능하다. 

 

 

체크 표시하기

체크 이미지를 넣을까 하다가 꼼수(?)를 발견했다. 멘토님이 알려주신 방법인데 처음에 보고 이렇게도 창의력을 쓸 수 있구나 했다. 😂 transform을 이용해 직사각형을 기울여서 그리고, 일부분만 border를 줘서 체크 표시처럼 보이도록 하는 방법이다.

input[type='radio']:checked + i:before로 클릭한 input을 선택하고, 라디오 버튼에 체크를 추가한다. 참고로 가상 선택자를 이용할 때는 content를 필수로 작성해야 한다. 

 

const Radio = styled.label`
  width: 50px;
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  cursor: pointer;


  // 스타일링을 위해 추가
  i {
    position: relative;
    display: inline-block;
    height: 36px;
    width: 36px;
    outline: 0;
    background: ${styleToken.color.white};
    border: 1px solid ${styleToken.color.gray4};
    border-radius: 50%;
  }

  // 기존의 input
  input {
    visibility: hidden;
    vertical-align: middle;
    position: absolute;
    height: 100%;
  }

  // 선택 시 배경색 
  input[type='radio']:checked + i {
    background-color: ${styleToken.color.secondary};
    border: unset;
  }

  // 선택 시 체크 표시
  input[type='radio']:checked + i:before {
    content: '';
    display: block;
    position: absolute;
    top: 20%;
    left: 55%;
    transform: rotate(-45deg) translate(-50%, -50%);
    width: 49%;
    height: 30%;
    border-bottom: 2px solid ${styleToken.color.white};
    border-left: 2px solid ${styleToken.color.white};
  }
`;

 

 

 

 

3. 함수 구현하기 

라디오 버튼을 클릭하면 유저의 액션에 따라 선택값이 바뀐다. '전혀 아님'으로 대답했어도 '자주 있음'으로 답변을 교체하고 싶어질 수 있다. 그럴 때 바뀐 입력값을 반영해 업데이트 해주어야 하는데, 이 부분을 onChange 함수로 구현했다.

 

처음에는 답변을 바꿀 수 있다는 생각을 못해서 배열의 length가 10이 되면 버튼이 활성화되도록 구현했었는데, 콘솔로 확인하니 답변을 바꿀 때마다 length가 계속 계속 늘어났다. 이런 상황을 막기 위해 파라미터로 index를 같이 넘겨준다. 불변성 유지를 위해 기존의 answer 값을 복사해서 새로운 배열을 만들고, 거기서 질문 번호에 따른 답변을 저장한다. 이 값을 이용해 답변을 업데이트한다.

 

// QuestionPage.tsx

// 답변을 저장
const [answer, setAnswer] = useState<number[]>([]);
// 모든 질문에 답변을 선택했을 때만 '결과보기' 버튼이 활성화 
const [isDisabled, setIsDisabled] = useState<boolean>(true);

const handleChangeAnswer = (e: ChangeEvent<HTMLInputElement>, index: number) => {
  const { value } = e.target;
  const parseNumberAnswer = Number(value);

  // 현재 질문에 대한 답변을 복사해 새로운 배열에 저장
  const updatedAnswers = [...answer];
  updatedAnswers[index] = parseNumberAnswer; // 질문의 index값을 받아 몇 번째 질문인지 체크하고 변경

  // 변경된 답변을 상태에 업데이트
  setAnswer(updatedAnswers);
};

const handleChangeDisabled = () => {
  if (answer.length === 10) {
    setIsDisabled(false);
  }
};

 

 

 

 

마치며

라디오 버튼이 제대로 동작해서 뿌듯했다. 스타일링도 비교적 자유롭게 할 수 있어서 편했다. 도중에 name을 잘못 지정해서 이상하게 작동했지만 그러지 않았으면 name을 제대로 정하는 것에 그리 큰 주의를 기울이지 않았을 수도 있다. 실수도 하면서 차근차근 배우는 중이다.