5. 토큰 및 채점 기준 설정하기

5. 토큰 및 채점 기준 설정하기

채점 기준은 어떻게 만들어야 좋을까?

채점 서비스에서 채점하려면 그 기준이 반드시 필요하다.

따라서 프론트엔드 쪽에서 어떻게 채점 기준을 추가하고 세분화할 수 있는지 고민했다.



파일로 받기? 직접 입력하기?

채점 기준을 파일로 받는다면 사실 프론트엔드 쪽에서는 너무 간단하다.

백엔드 쪽으로 해당 파일만 그대로 넘겨주면 되니까.

하지만 파일 방식은 형식이 일정해야 하고, 보기에도 불편했다.

따라서 웹에서 채점 기준을 직접 입력하는 것이 훨씬 직관적일 것 같아 이 방식으로 구현할 것이다.



클래스는 어떻게 하지?

클래스는 (만약 수업에서 사용한다면) 각 과제 또는 실습 단위로 구성하려고 했다.

간단히 말해서 자바 프로그래밍 수업에서 과제가 5개만 나온다면 교수님은 5개의 클래스만 만드시면 되도록!


가장 오랫동안 고민한 것은 로그인 기능이었다.

  • 구글 로그인 API와 같이 라이브러리를 사용해서 로그인을 구현해서 해당 교수님의 클래스로 Join하는 방식을 구현할 것인지,
  • 로그인 없이 특정 인증 방법으로 클래스에 Join할 것인지.

각 루트는 장단점이 확실했다.

로그인을 구현한다면,

일단 학생들의 로그인 정보를 어떻게든 확인할 수 있으므로, 치팅이나 고의로 시스템을 훼손하는 행위 등은 거의 없을 것이다.

클래스 개설 및 입장이 굉장히 직관적일 것이다. (과제1, 과제2, … 이렇게 찾아서 들어갈 수도 있을 것)

로그인 API를 사용하는 것은 어렵지 않았으나, 세션 유지와 개인정보 이슈 (사용자 정보가 들어가 있으니까)가 발생한다.


로그인을 구현하지 않는다면,

위 로그인의 장점이 모두 단점으로 바뀐다😢

사용자의 정보를 일체 수집하지 않으므로 개인정보 이슈는 없다.

로그인을 대신할 인증 방식을 고민해봐야 하며, 상대적으로 구현이 편할 수 있다.

로그인 방식이 굉장히 보편적이고 클래스 분류, 입장 기능이 강력할 것 같아 이쪽으로 가닥을 잡고 구현하고 있었으나,

개인정보를 어떻게 관리할 것인지에 대한 피드백을 받았고 세션 유지도 구현 당시에는 어려워서 로그인 방식을 폐기했다.

그럼 어떻게 클래스룸을 확인하고 인증할 것인가?



토큰 인증 방식 구현하기

거창한 방식인 것 같지만 굉장히 간단하다.

클래스룸을 개설할 때 문자열 + 숫자를 조합한 난수 문자열(토큰)을 생성한다.

채점자가 채점 기준까지 모두 설정하면 토큰을 비롯한 채점 기준표를 백엔드로 전송한다.

클래스룸에 들어가고자 하는 학생들은 미리 배포된 토큰을 이용하며, 입력된 토큰이 실제 데이터베이스에 저장되어 있는지 확인한다.


그럼, 난수부터 만들어 보자!



// file: 'EntranceInstructor.tsx'
...

const handleGenerate = () => {
        let random =  Math.random().toString(36).substr(2, 11);
        let irandom = Math.random().toString(36).substr(2, 6);
        setInfo({ ...info, token: random, itoken: irandom });
    }


토큰은 학생들에게 배포되는 random 토큰, 일종의 관리자 페이지를 위한 itoken을 각각 만들어 주었다.

그리고 이를 백엔드에 보낼 간단한 정보를 저장하는 info에 React hook을 사용해 저장한다.

여기서도 ...info를 사용해 spreading operator로 나머지 배열들을 그대로 저장하고,

itoken, token만 업데이트해준다.



그 다음, 개설할 클래스 이름과 채점자 이름을 입력하고,

채점 결과를 보여줄 것인지 여부를 체크하는 변수도 추가한다.

(채점 결과는 점수, 채점 기준 중 어느 것을 틀렸는지에 대한 간단한 메시지를 줄 예정인데, 아예 보여주지 않는 것도 필요할 것 같아 옵션으로 넣었다.)

// file: 'EntranceInstructor.tsx'

return (

  ...

  {open &&
          <Dialog
              open={open}
              onClose={handleClose}
              aria-labelledby="instructor-token-generator"
              aria-describedby="instructor-token-description"
              disableBackdropClick={true}
              disableEscapeKeyDown={true}
              maxWidth="sm"
              scroll='paper'
          >
              <DialogTitle id="instructor-token-title">
                다이얼로그 제목
              </DialogTitle>
              <DialogContent>
                  <DialogContentText>
                      다이얼로그 내용
                  </DialogContentText>
                  <Grid container spacing={2}>
                      <Grid xs={12} item>
                          <FormControl margin="normal">
                              <TextField
                                  value={info.className || ""}
                                  variant="outlined"
                                  label="클래스 네임"
                                  size="medium"
                                  className={classes.dialogText}
                                  onChange={handleInfoChange("className")}
                              />
                              <TextField
                                  value={info.instructor || ""}
                                  variant="outlined"
                                  label="개설자 네임"
                                  size="medium"
                                  className={classes.dialogText}
                                  onChange={handleInfoChange("instructor")}
                              />
                          </FormControl>
                      </Grid>
                      <Grid xs={12} item>
                      <Button variant="contained" 
                              color="primary" 
                              onClick={handleGenerate} 
                              disabled={info.className.length < 3 || info.instructor.length < 3}>
                          생성하기
                      </Button>
                      </Grid>
                  </Grid>

                  ...
                  


채점 기준을 입력하기 위해 링크를 누르면 Dialog가 뜨도록 했다.

페이지를 이동해 작성하는 것보다 코드를 구성하기 편하기도 하고, 데이터를 모아두는 것도 보다 쉽다.


Material-UI의 Dialog 컴포넌트와 Grid 컴포넌트를 임포트해 사용했다.

DialogContent 컴포넌트에 다이얼로그 내부 내용을 지정할 수 있으며,

여기에 클래스 이름과 개설자 이름을 넣고 토큰 생성 버튼을 누르면 출력하도록 했다.


🔥 TextField 컴포넌트와 종일 씨름한 순간이다.

특히,


...

<TextField
  value={info.className || ""}
  variant="outlined"
  label="클래스 네임"
  size="medium"
  className={classes.dialogText}
  onChange={handleInfoChange("className")}
/>

...

이 부분이었는데, 각 TextField에 커서를 놓고 입력해도 입력이 안 되거나,

입력이 들어가도 두 TextField에 같은 내용이 찍혀버리는 상황이 발생했다.


이는 TextFieldvalueonChange props를 잘 구성했어야 했다.

초기 value에 지정된 변수의 값이 없으면 ""을 넣고, 뭔가 입력된다 싶으면 onChange로 핸들링해줬다.


// file: 'EntranceInstructor.tsx'

interface InfoProps {
    className: string;
    instructor: string;
    token: string;
    itoken: string;
    direct: boolean;
}

...

const handleInfoChange = (prop: keyof InfoProps) => (e: React.ChangeEvent<HTMLInputElement>) => {
        setInfo({ ...info, [prop]: e.target.value });
}

...

핸들링 함수는 위와 같으며, 핸들링 함수를 호출할 때 같이 넘겨준 매개변수로 해당 TextField에 맞는 값을 업데이트한다.

인터페이스는 InfoProps와 같이 해당 다이얼로그 페이지에서 받을 정보의 타입을 지정했다.

onChange는 이벤트가 발생하고, 입력 시 발생하는 이벤트는 React에서 React.ChangeEvent<HTMLInputElement>로 타입이 정의되어 있다.


저렇게 TextFieldonChange 핸들링 함수 호출 시 매개변수를 다르게 주고, hook으로 변경하면 아주 잘 동작한다!


버튼을 누르면 handleGenerate 핸들링 함수를 호출하여 토큰을 생성한다.

최소한의 제약조건을 위하여 클래스 이름과 개설자 이름의 길이가 3을 넘지 않으면 비활성화했다.



// file: 'EntranceInstructor.tsx'

...

  <Typographic variant="h3" color="inherit">
      <Typographic variant="caption" color="inherit">
          클래스 토큰 <br />
      </Typographic>
      {info.token}
      <br />
      <Typographic variant="caption" color="inherit">
          채점용 토큰 <br />
      </Typographic>
      {info.itoken}
  </Typographic>

  <FormControlLabel
      control={
          <Checkbox checked={info.direct}
                  onChange={handleChecked}
                  name="directFeedback"
                  color="primary" />}
                  label="즉시 피드백 제공 활성화"
   />

</DialogContent>

...


그 다음, 버튼을 눌러 생성했다면 토큰을 보여줘야 한다.

처음 info의 토큰들은 비어 있는 문자열로 초기화되어 있으므로 아무것도 출력되지 않지만

handleGenerate 핸들링 함수로 생성되면 즉시 출력한다.


피드백을 바로 줄 것인지 결정하는 Checkbox 컴포넌트 또한 handleChecked 핸들링 함수를 선언하여

handleInfoChange 핸들링 함수와 비슷하게 구성했다.

이벤트 타입은 같지만, 사용하지 않는다.



// file: 'EntranceInstructor.tsx'
...

  <DialogActions>
      <Button onClick={handleClose} color="primary">
          닫기
      </Button>
      <Button onClick={handlePOpen} color="primary" disabled={info.token.length > 0 ? false : true}>
          다음으로
      </Button>
  </DialogActions>
</Dialog>
        


DialogActions 컴포넌트는 다이얼로그 최하단의 닫기, 다음 버튼 등과 같은 이벤트를 위한 컴포넌트다.

닫기, 다음으로 버튼을 눌렀을 때 각기 핸들링 함수로 처리해줬으며,

특히 다음으로 버튼은 토큰이 생성되지 않았다면 넘어가지 않도록 비활성화했다.


Token


입력, 토큰 생성, 체크박스 모두 잘 동작한다✨



다음 차례는 채점 기준을 설정하는 기능이다.

채점해야 할 항목이 많지만, 그만큼 중복되는 것도 많을 것이다.

이것도 어떻게 구성해야 할 지 고민해야겠다!





🔥 TroubleShooting & Review

Issue:

  • TextField를 핸들링하는 것이 은근히 힘들었다. 변수를 hook으로 업데이트하고 유지하는 것에 시간을 많이 쏟음!
  • 토큰은 난수 생성으로 구성했는데, 겹칠 확률이 있지 않을까?라는 피드백이 있었다. 백엔드로부터 전체 토큰 리스트를 받아서 확인하기에는 오버헤드가 클 것 같은데…
  • 타입 확인하는 것이 은근히 어렵다. 공부해야겠다..

Review

  • Dialog 방식 굉장히 맘에 든다! 찾아보니 Nested Dialog 방식이 있던데, 이대로 구현하면 채점 기준 설정하는 Dialog로 바로 넘어갈 수 있지 않을까?!
  • 조건부 렌더링을 애용 중이다. 필요할 때만 렌더링하는 방식이 너무 좋다🥺