본문으로 건너뛰기
튜토리얼20분 분량

자신의 서버에서 jsPsych로 Stroop 과제 실행하기

Potato로 대학 서버에 jsPsych Stroop 과제를 호스팅하고, 색각 선별 탈락과 Prolific 완료 코드를 추가하고, R에서 반응 시간을 분석합니다.

Potato Team

jsPsych로 과제를 작성하는 인지심리학자는 이 튜토리얼을 따라 연구실이 통제하는 서버에서 Stroop 과제를 실행할 수 있으며, Potato 2.13.1이 페이지를 제공하고 데이터를 저장합니다. 이 연구에는 동의 페이지, 빨강, 초록, 파랑을 구별하는 데 어려움이 있는 사람의 세션을 종료하는 색각 선별 검사, 참여자의 절반에게 시행별 피드백을 주는 jsPsych 타임라인, 과제에 대한 두 가지 평가, 디브리핑이 있습니다. 참여자는 Prolific에서 도착해 완료 코드와 함께 떠나며, 시행 데이터는 R로 들어갑니다. 이 페이지의 설정, 명령, 내보내기 출력은 헤드리스 브라우저에서 모의 참여자로 Potato 2.12.1을 실행한 결과에서 나왔습니다.

jsPsych 연구의 호스팅 선택지

jsPsych는 참여자의 브라우저에서 실행되며 자체로는 아무것도 호스팅하지 않습니다. jsPsych 연구에는 페이지를 전달하고 데이터를 받을 서버가 필요합니다. PsychoPy의 2026년 10월 21일 업데이트된 요금 페이지는 Pavlovia 크레딧을 참여자당 £0.26, 대학 라이선스를 연 £2,000으로 나열합니다. Gorilla는 연구 전체를 호스팅하며, 요금 FAQ는 학술용 종량제 토큰을 참여자당 £1.09, 최소 구매 25토큰으로 나열합니다. 두 서비스 모두 데이터를 자체 서버에 보관합니다. PsychoPy 포럼 글 "Running Experiments on Own Server"에서 PsychoPy 스태프는 2021년에 사이트 라이선스를 가진 기관이 Pavlovia의 로컬 설치를 이용할 수 있다고 썼습니다.

JATOS는 Apache 2.0 라이선스의 무료 오픈 소스로, 자신의 서버에 설치하며 jsPsych, lab.js, OpenSesame 연구를 실행합니다. 연구가 jsPsych 타임라인뿐이라면 JATOS가 확립된 선택입니다. Potato는 타임라인이 동의 및 선별 페이지, 조건에 대한 균형 배정, 평가나 설문, Prolific으로의 복귀도 필요한 세션의 한 부분일 때 알맞습니다. Potato는 이 각 부분을 JavaScript가 아니라 YAML 설정으로 구성합니다. 행동 실험 도구 비교가 도구들을 나란히 놓고 비교합니다.

연구

세션은 다음 순서로 진행됩니다.

  1. 동의. 한 페이지가 연구를 설명하고 동의를 요청합니다.
  2. 선별 검사. 빨강, 초록, 파랑을 구별하는 데 어려움이 있다고 답한 참여자는 탈락하고 선별 탈락 코드와 함께 Prolific으로 돌려보내집니다.
  3. Stroop 과제. jsPsych 타임라인이 각 색 단어를 각 잉크 색으로 두 번씩, 총 18개 시행으로 보여 줍니다. immediate 조건의 참여자는 각 반응 뒤에 "Correct", "Incorrect", "Too slow"를 보고, none 조건의 참여자는 피드백을 보지 않습니다.
  4. 과제 평가. 5점 평가 두 개가 과제가 얼마나 어려웠는지와 참여자가 얼마나 잘했는지를 묻습니다.
  5. 디브리핑. 한 페이지가 Stroop 효과와 피드백 조작을 설명하고 의견 상자를 제공합니다.

피드백은 피험자 간 요인이며 각 참여자는 세션 내내 하나의 조건을 유지합니다. Potato는 도착할 때 조건을 배정하고 그 조건을 타임라인에 전달합니다.

Potato 설치와 프로젝트 구성

이 튜토리얼에서 확인한 릴리스를 설치하세요.

bash
pip install potato-annotation==2.13.1

프로젝트 폴더에는 설정, 설문 페이지 세 개, 타임라인, 데이터 파일 하나가 있습니다.

text
stroop-study/
├── config.yaml
├── data/task.jsonl
├── surveys/consent.json
├── surveys/screener.json
├── surveys/debrief.json
└── tasks/stroop.js

설정

아래 설정은 완전하며 2.12.1과 2.13.1에서 python -m potato.validate_cli config.yaml --strict를 통과합니다. 검증기는 tasks/stroop.js가 존재하는지도 확인합니다.

yaml
annotation_task_name: Stroop feedback study
task_dir: .
output_annotation_dir: annotation_output/
data_files:
  - data/task.jsonl
item_properties:
  id_key: id
  text_key: text
 
login:
  type: url_direct
  url_argument: PROLIFIC_PID
user_config:
  allow_all_users: true
  users: []
 
crowdsourcing:
  provider: prolific
  prolific:
    completion:
      code: C1STROOP
      screened_out_code: C1SCREEN
 
conditions:
  factors:
    feedback: [none, immediate]
  assignment: balanced
  balance_on: completed
 
phases:
  order: [consent, screener, stroop, annotation, debrief]
  consent:
    type: consent
    file: surveys/consent.json
  screener:
    type: prestudy
    file: surveys/screener.json
    exit_when:
      show_when:
        - {schema: color_vision, operator: equals, value: "Yes"}
  stroop:
    type: jspsych
    timeline: tasks/stroop.js
    plugins: [instructions, html-keyboard-response]
    save: incremental
  annotation:
    type: annotation
  debrief:
    type: poststudy
    file: surveys/debrief.json
 
annotation_schemes:
  - annotation_type: likert
    name: difficulty
    description: The color-naming task was difficult.
    size: 5
    min_label: Strongly disagree
    max_label: Strongly agree
    label_requirement:
      required: true
  - annotation_type: likert
    name: performance
    description: I did well on the color-naming task.
    size: 5
    min_label: Strongly disagree
    max_label: Strongly agree
    label_requirement:
      required: true
 
export_include_phase_data: true

login.type: url_direct는 로그인 페이지 없이 연구 링크의 PROLIFIC_PID로 각 참여자를 로그인시킵니다. balance_on: completed는 선별 탈락 없이 끝낸 참여자를 기준으로 두 피드백 조건의 균형을 맞춥니다. 중도 포기하거나 선별 검사에서 탈락한 참여자는 어느 조건에도 계산되지 않습니다. Potato 2.13.1부터는 아직 진행 중인 참여자가 마지막 활동 후 in_progress_minutes(기본값 60) 동안 자신의 조건에 계산되므로, 같은 시간에 시작한 사람들이 두 조건에 나뉩니다. save: incremental은 참여자가 끝낼 때마다 각 시행을 저장합니다. 기본값인 save: on_finish에서는 도중에 탭을 닫으면 시행이 하나도 남지 않습니다. export_include_phase_data: true는 동의, 선별 검사, 디브리핑 답변을 CSV 내보내기에 넣습니다.

모든 Potato 연구에는 항목이 하나 이상 있는 어노테이션 단계가 있으며, 2.13.1부터 potato validate는 그런 단계가 없는 설정에 오류를 보고합니다. Stroop 연구는 이 단계를 항목 하나에 대한 두 과제 평가에 사용합니다. data/task.jsonl에는 한 줄이 있습니다.

json
{"id": "stroop_task", "text": "The color-naming task you just finished"}

동의와 색각 선별 검사

Prolific은 연구자에게 동의하지 않는 참여자를 반환 경로로 보내고 그런 참여자를 절대 거절하지 말라고 요청합니다(연구 안에서 참여자를 선별하는 Prolific의 안내). Potato 2.13.1에서는 모든 exit_when 규칙이 같은 선별 탈락 코드를 줍니다. 따라서 동의 페이지에는 exit_when이 없습니다. 동의하지 않는 참여자에게는 Prolific에서 연구를 반환하라고 요청하며, 선별 탈락 코드는 선별 검사가 제외하는 참여자에게만 갑니다. surveys/consent.json은 다음과 같습니다.

json
[
  {
    "name": "study_information",
    "annotation_type": "pure_display",
    "description": "About this study",
    "labels": [
      "You will press keys for the ink color of color words, then answer a few questions. The study takes about 10 minutes.",
      "Your responses are stored on a university server under your Prolific ID.",
      "If you do not want to take part, close this page and return the study on Prolific."
    ]
  },
  {
    "name": "consent",
    "annotation_type": "radio",
    "description": "Do you agree to take part in this study?",
    "labels": ["I agree to take part"],
    "label_requirement": {"required_label": ["I agree to take part"]}
  }
]

설문 페이지에서 서버는 required_label을 검사하며, 2.13.1부터는 required: true도 검사합니다. 2.12.1에서는 required: true가 브라우저에서만 검사되었습니다. 2.12.1에서 동의 질문을 건너뛰고 페이지를 직접 제출한 테스트 요청은 400 응답을 받았고 동의 페이지에 머물렀습니다. surveys/screener.json의 선별 검사는 두 선택지를 모두 required_label 아래에 나열합니다. 그러면 서버는 특정한 답을 요구하지 않고 답변을 요구합니다.

json
[
  {
    "name": "color_vision",
    "annotation_type": "radio",
    "description": "Do you have difficulty telling red, green, or blue apart?",
    "labels": ["Yes", "No"],
    "label_requirement": {"required_label": ["Yes", "No"]}
  }
]

"Yes"라고 답한 참여자는 C1SCREEN과 함께 세션의 끝으로 바로 갑니다. 선별 검사는 과제 앞의 자체 페이지에 있습니다. Potato는 jsPsych 페이지의 exit_when을 타임라인이 끝난 뒤에야, 즉 참여자가 이미 과제를 한 뒤에 확인합니다. Prolific은 또한 가능하면 참여자가 연구에 도달하기 전에 자체 사전 선별로 거르라고 권장합니다.

Stroop 타임라인

타임라인 파일은 window.potatoTimeline에 함수를 할당합니다. Potato는 jsPsych 인스턴스를 만들고, 그 인스턴스와 potato 객체로 함수를 호출하며, 타임라인이 끝나면 데이터를 저장합니다. 아래 타임라인은 Potato에 포함된 examples/advanced/jspsych-stroop 예제를 바탕으로 한 것입니다.

javascript
window.potatoTimeline = function (jsPsych, potato) {
    var inks = [
        {name: 'RED', color: '#c62828', key: 'r'},
        {name: 'GREEN', color: '#2e7d32', key: 'g'},
        {name: 'BLUE', color: '#1565c0', key: 'b'}
    ];
 
    // Every word in every ink: 3 congruent and 6 incongruent pairs.
    var stimuli = [];
    inks.forEach(function (word) {
        inks.forEach(function (ink) {
            stimuli.push({
                word: word.name,
                ink: ink.name,
                color: ink.color,
                correct_response: ink.key,
                congruent: word.name === ink.name
            });
        });
    });
 
    var instructions = {
        type: jsPsychInstructions,
        pages: [
            '<p>A color word will appear in colored ink.</p>' +
            '<p>Press the key for the <strong>ink color</strong>, not the word:</p>' +
            '<p><strong>R</strong> = red, <strong>G</strong> = green, <strong>B</strong> = blue</p>',
            '<p>Answer as quickly and accurately as you can.</p>'
        ],
        show_clickable_nav: true
    };
 
    var fixation = {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: '<div style="font-size:48px;">+</div>',
        choices: 'NO_KEYS',
        trial_duration: function () {
            return jsPsych.randomization.sampleWithoutReplacement([400, 500, 600], 1)[0];
        },
        data: {task: 'fixation'}
    };
 
    var trial = {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: function () {
            return '<div style="font-size:64px;font-weight:700;color:' +
                jsPsych.evaluateTimelineVariable('color') + ';">' +
                jsPsych.evaluateTimelineVariable('word') + '</div>';
        },
        choices: ['r', 'g', 'b'],
        trial_duration: 2500,
        data: {
            task: 'stroop',
            word: jsPsych.timelineVariable('word'),
            ink: jsPsych.timelineVariable('ink'),
            congruent: jsPsych.timelineVariable('congruent'),
            correct_response: jsPsych.timelineVariable('correct_response')
        },
        on_finish: function (data) {
            data.correct = jsPsych.pluginAPI.compareKeys(data.response, data.correct_response);
        }
    };
 
    // Runs only for participants in the feedback: immediate condition.
    var feedback = {
        timeline: [{
            type: jsPsychHtmlKeyboardResponse,
            stimulus: function () {
                var last = jsPsych.data.get().last(1).values()[0];
                if (last.response === null) { return '<p>Too slow</p>'; }
                return last.correct ? '<p>Correct</p>' : '<p>Incorrect</p>';
            },
            choices: 'NO_KEYS',
            trial_duration: 600,
            data: {task: 'feedback'}
        }],
        conditional_function: function () {
            return potato.conditions.feedback === 'immediate';
        }
    };
 
    var block = {
        timeline: [fixation, trial, feedback],
        timeline_variables: stimuli,
        repetitions: 2,
        randomize_order: true
    };
 
    return [instructions, block];
};

potato.conditions.feedback은 Potato가 참여자에게 배정한 수준이며, conditional_function은 immediate 조건에서만 피드백 화면을 보여 줍니다. Potato가 둘 다 하므로 파일은 initJsPsych를 호출하지 않고 실험 수준의 on_finish도 설정하지 않습니다. 시행 수준의 on_finish는 다른 jsPsych 연구와 똑같이 작동합니다.

Potato 2.13.1은 jsPsych 8.3.0과 그 플러그인 15개를 패키지 안에 포함하며, 페이지는 둘 다 CDN이 아니라 Potato 서버에서 불러옵니다. 설정의 plugins 목록은 타임라인이 쓰는 두 플러그인을 지정합니다. jspsych-contrib의 플러그인처럼 Potato가 포함하지 않는 플러그인은 브라우저 빌드로 extra_scripts에 넣습니다. 18개 시행은 각 참여자에게 일치 시행 6개를 주며, 실제 연구에서는 repetitions를 늘립니다.

평가와 디브리핑

두 평가는 과제 뒤의 어노테이션 단계에서 실행됩니다. 두 평가 모두 label_requirement: required: true를 설정합니다. 서버는 어노테이션 항목에서 이 요건을 검사하며, 참여자는 둘 다 답하지 않으면 디브리핑에 도달할 수 없습니다. surveys/debrief.json의 디브리핑은 조작을 설명하고 의견은 선택 사항으로 둡니다.

json
[
  {
    "name": "debriefing",
    "annotation_type": "pure_display",
    "description": "What this study measured",
    "labels": [
      "Naming the ink color takes longer when the word names a different color. This slowdown is the Stroop effect. Half of the participants saw feedback after each response, and the study compares the Stroop effect between the two groups."
    ]
  },
  {
    "name": "comments",
    "annotation_type": "text",
    "description": "Anything else you want to tell us? (optional)"
  }
]

자신의 컴퓨터에서 연구 테스트하기

프로젝트 폴더에서 서버를 시작합니다.

bash
potato start config.yaml -p 8000

http://localhost:8000/?PROLIFIC_PID=TEST1&STUDY_ID=s1&SESSION_ID=x1을 열고 테스트 참여자마다 새 PROLIFIC_PID를 쓰세요. 연구를 한 번은 끝까지, 한 번은 선별 검사에서 "Yes"라고 답하며, 한 번은 Stroop 블록 중간에 페이지를 새로 고치며 실행해 보세요. 끝낸 참여자는 "Return to Prolific" 버튼과 함께 C1STROOP를 보고, 선별 탈락한 참여자는 C1SCREEN을 봅니다. 2.12.1에서 선별 탈락한 참여자의 페이지는 끝낸 참여자의 페이지와 같은 "You have completed the annotation task" 텍스트를 보여 주며, 코드만 다릅니다.

jsPsych 페이지에는 다음 버튼이 없습니다. 타임라인이 끝나고 서버가 실행을 저장할 때까지 서버는 넘어가라는 모든 요청에 오류로 응답하며, 페이지를 건너뛰라는 직접 요청은 테스트에서 400을 반환했습니다. jsPsych는 중간부터 재개할 수 없으므로 새로 고치면 타임라인이 처음부터 다시 시작됩니다. save: incremental에서는 새로 고치기 전의 시행이 상태가 partial인 실행으로 내보내기에 남고, 새 실행은 시도 번호 2를 받습니다.

테스트 참여자는 조건에 계산되고 내보내기에 나타납니다. 시작 전에 annotation_output/과 project.sqlite를 삭제하세요.

연구를 서버에 올리기

Prolific 참여자는 공개 주소를 통해서만 연구에 도달합니다. 프로덕션 설정은 대학 서버에서 TLS와 함께 nginx나 Apache 뒤에 Potato를 실행하는 방법을, VPS에 Potato 배포하기는 임대한 서버를 다룹니다. 시행 데이터는 프로젝트 폴더의 project.sqlite에, 평가와 설문 답변은 annotation_output/에 남으며, 둘 다 서버를 실행하는 컴퓨터에 있습니다.

Prolific에서는 URL 매개변수로 Prolific ID를 기록하도록 선택하고, 연구 URL을 세 매개변수가 붙은 서버 주소로 설정하세요.

text
https://your-server.example/?PROLIFIC_PID={{%PROLIFIC_PID%}}&STUDY_ID={{%STUDY_ID%}}&SESSION_ID={{%SESSION_ID%}}

Prolific에 완료 경로 두 개를 추가하세요. 끝낸 참여자를 위한 C1STROOP와, Prolific의 "Screened out" 라벨이 붙은 C1SCREEN입니다. Prolific은 선별 탈락한 참여자에게 설정한 고정 선별 탈락 보상을 지급하며, 그들의 제출은 자리 수에 계산되지 않습니다. 동의하지 않는 참여자는 Prolific에서 연구를 반환하며 코드를 받지 않습니다. 크라우드소싱 통합에 제출을 추적하는 Prolific API 설정이 있습니다.

시행 데이터 내보내기

Potato는 각 jsPsych 실행을 참여자의 user_state.json 파일이 아니라 project.sqlite(테이블 runtime_runs)에 저장합니다. jspsych 내보내기 형식은 실행과 시행을 CSV로 씁니다.

bash
python -m potato.export --config config.yaml --format jspsych --output export/

모의 참여자 일곱 명으로 Potato 2.12.1에서 한 테스트 실행은 다음 파일을 썼습니다.

파일한 행의 단위내용
jspsych_runs.csv타임라인의 실행참여자, 페이지, 시도, 상태(complete 또는 partial), 시작 및 종료 시간, 시행 수, condition_feedback
jspsych_trials.csvjsPsych 시행run_status, attempt, condition_feedback, 그리고 rt, response, plugin_version, 타임라인 자체의 task, word, ink, congruent, correct를 포함해 jsPsych가 기록한 모든 열
jspsych_raw/<participant>/stroop-<run>.json실행jsPsych가 만든 그대로의 실행 데이터

jsPsych가 블러, 포커스, 전체 화면 이벤트를 기록하면 네 번째 파일 jspsych_interaction.csv가 나타납니다. 아래 행은 피드백 조건의 한 참여자의 첫 Stroop 시행이며 열의 일부만 보여 줍니다.

text
user_id,attempt,run_status,trial_index,rt,response,condition_feedback,plugin_version,task,word,ink,congruent,correct
5f1a02,1,complete,2,604,r,immediate,2.2.0,stroop,BLUE,RED,False,True
5f1a02,1,complete,5,538,b,immediate,2.2.0,stroop,RED,RED,True,False
5f1a02,1,complete,8,545,g,immediate,2.2.0,stroop,BLUE,GREEN,False,True

rt는 밀리초 단위입니다. 시간이 초과된 시행에서는 rt와 response가 비어 있고 correct는 False입니다. jsPsych는 모든 시행에 플러그인 버전을 기록하며, 여기서는 html-keyboard-response가 2.2.0, instructions가 2.1.0입니다. jsPsych 코어 버전은 데이터에 없으므로, 방법 절에는 "jsPsych 8.3.0, Potato 2.13.1이 제공"처럼 두 버전을 모두 밝혀야 합니다. CSV 파일은 참과 거짓 값에 True와 False를 쓰며, 원본 JSON은 JSON 불리언을 유지합니다.

CSV 내보내기는 연구의 나머지를 씁니다.

bash
python -m potato.export --config config.yaml --format csv --output results/

results/participants.csv는 참여자당 한 행으로, condition_feedback, screened_out, screened_out_page, 그리고 Stroop 실행의 시도 횟수와 상태를 알려 주는 runtime_runs 열이 있습니다. results/annotations.csv에는 척도 점수마다 열 하나씩(difficulty.1부터 difficulty.5까지) 두 평가가 있고, results/phase_responses.csv에는 동의, 선별 검사, 디브리핑 답변이 있습니다. 데이터 저장소용으로 --format psych_ds는 다섯 표를 모두 Psych-DS 파일 이름으로 data/ 아래에 쓰고, 모든 열을 나열하는 dataset_description.json도 씁니다.

R에서 반응 시간 분석

아래 R 코드는 시행 표를 읽고, 완료된 실행의 Stroop 시행을 남기고, 각 참여자의 Stroop 효과를 불일치 시행의 정답 반응 시간 평균에서 일치 시행의 평균을 뺀 값으로 계산합니다.

r
trials <- read.csv("export/jspsych_trials.csv")
stroop <- subset(trials, task == "stroop" & run_status == "complete")
stroop$congruent <- as.logical(stroop$congruent)
stroop$correct <- as.logical(stroop$correct)
 
accuracy <- aggregate(correct ~ user_id + condition_feedback, data = stroop, FUN = mean)
 
fast <- subset(stroop, correct & rt >= 200)
cells <- aggregate(rt ~ user_id + condition_feedback + congruent, data = fast, FUN = mean)
wide <- reshape(cells, idvar = c("user_id", "condition_feedback"),
                timevar = "congruent", direction = "wide")
wide$stroop_effect <- wide$rt.FALSE - wide$rt.TRUE
 
aggregate(stroop_effect ~ condition_feedback, data = wide, FUN = mean)
t.test(stroop_effect ~ condition_feedback, data = wide)

as.logical은 True와 False 문자열을 읽고 read.csv가 이미 변환한 열은 그대로 둡니다. 200ms 하한은 예측성 키 누름을 제외하며, 사전 등록된 기준이 있으면 그 기준이 하한을 대신합니다. 시간이 초과된 시행은 correct가 거짓이므로 정확도 표는 시간 초과를 오류로 셉니다.

완료된 실행의 attempt가 2인 참여자는 도중에 페이지를 새로 고쳤으며 일부 시행을 두 번 보았습니다. subset(stroop, attempt == 1)은 첫 시도만 남기며, 이 선택은 분석 계획에 속합니다. 시행 수준 모형에는 lme4가 참여자별 무작위 일치 기울기와 함께 일치 효과, 피드백 조건, 둘의 상호작용을 적합합니다.

r
library(lme4)
fit <- lmer(rt ~ congruent * condition_feedback + (1 + congruent | user_id), data = fast)
summary(fit)

congruentTRUE:condition_feedbacknone 계수는 Stroop 효과가 두 피드백 조건 사이에서 얼마나 다른지를 추정합니다. 참여자 흐름을 위해 선별 탈락을 세려면 results/participants.csv를 읽고 screened_out_page를 집계하세요.

다음으로 볼 자료